8chan YouTube Link Enhancer

Cleans up YouTube links and adds video titles in 8chan.moe posts

当前为 2025-04-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         8chan YouTube Link Enhancer
// @namespace    sneed
// @version      1.2.1
// @description  Cleans up YouTube links and adds video titles in 8chan.moe posts
// @author       DeepSeek
// @license      MIT
// @match        https://8chan.moe/*
// @match        https://8chan.se/*
// @grant        GM.xmlHttpRequest
// @grant        GM.setValue
// @grant        GM.getValue
// @connect      youtube.com
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const DELAY_MS = 200; // Delay between YouTube API requests (only for uncached)
    const CACHE_EXPIRY_DAYS = 7;
    const CACHE_CLEANUP_PROBABILITY = 0.1; // 10% chance to run cleanup

    // --- YouTube Link Cleaning (unchanged) ---
    function cleanYouTubeUrl(url) {
        if (!url || (!url.includes('youtube.com') && !url.includes('youtu.be'))) {
            return url;
        }

        let cleaned = url;
        if (cleaned.startsWith('https://youtu.be/')) {
            const videoIdPath = cleaned.substring('https://youtu.be/'.length);
            const paramIndex = videoIdPath.search(/[?#]/);
            const videoId = paramIndex === -1 ? videoIdPath : videoIdPath.substring(0, paramIndex);
            const rest = paramIndex === -1 ? '' : videoIdPath.substring(paramIndex);
            cleaned = `https://www.youtube.com/watch?v=${videoId}${rest}`;
        }

        if (cleaned.includes('youtube.com/live/')) {
             cleaned = cleaned.replace('/live/', '/watch?v=');
        }

        cleaned = cleaned.replace(/[?&]si=[^&]+/, '');

        if (cleaned.endsWith('?') || cleaned.endsWith('&')) {
            cleaned = cleaned.slice(0, -1);
        }

        return cleaned;
    }

    function processLink(link) {
        const currentUrl = link.href;
        if (!currentUrl.includes('youtube.com') && !currentUrl.includes('youtu.be')) {
            return;
        }

        const cleanedUrl = cleanYouTubeUrl(currentUrl);
        if (cleanedUrl !== currentUrl) {
            link.href = cleanedUrl;
            if (link.textContent.trim() === currentUrl.trim()) {
                 link.textContent = cleanedUrl;
            }
        }
    }

    // --- YouTube Enhancement with Smart Caching ---
    const svgIcon = `
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
            <path d="M549.7 124.1c-6.3-23.7-24.9-42.4-48.6-48.6C456.5 64 288 64 288 64s-168.5 0-213.1 11.5
            c-23.7 6.3-42.4 24.9-48.6 48.6C16 168.5 16 256 16 256s0 87.5 10.3 131.9c6.3 23.7
            24.9 42.4 48.6 48.6C119.5 448 288 448 288 448s168.5 0 213.1-11.5
            c23.7-6.3 42.4-24.9 48.6-48.6 10.3-44.4 10.3-131.9 10.3-131.9s0-87.5-10.3-131.9zM232
            334.1V177.9L361 256 232 334.1z"/>
        </svg>
    `.replace(/\s+/g, " ").trim();

    const encodedSvg = `data:image/svg+xml;base64,${btoa(svgIcon)}`;

    const style = document.createElement("style");
    style.textContent = `
        .youtubelink {
            position: relative;
            padding-left: 20px;
        }
        .youtubelink::before {
            content: '';
            position: absolute;
            left: 2px;
            top: 1px;
            width: 16px;
            height: 16px;
            background-color: #FF0000;
            mask-image: url("${encodedSvg}");
            mask-repeat: no-repeat;
            mask-size: contain;
            opacity: 0.8;
        }
    `;
    document.head.appendChild(style);

    // Cache management (unchanged)
    async function getCachedTitle(videoId) {
        try {
            const cache = await GM.getValue('ytTitleCache', {});
            const item = cache[videoId];
            if (item && item.expiry > Date.now()) {
                return item.title;
            }
            return null;
        } catch (e) {
            console.warn('Failed to read cache:', e);
            return null;
        }
    }

    async function setCachedTitle(videoId, title) {
        try {
            const cache = await GM.getValue('ytTitleCache', {});
            cache[videoId] = {
                title: title,
                expiry: Date.now() + (CACHE_EXPIRY_DAYS * 24 * 60 * 60 * 1000)
            };
            await GM.setValue('ytTitleCache', cache);
        } catch (e) {
            console.warn('Failed to update cache:', e);
        }
    }

    async function clearExpiredCache() {
        try {
            const cache = await GM.getValue('ytTitleCache', {});
            const now = Date.now();
            let changed = false;

            for (const videoId in cache) {
                if (cache[videoId].expiry <= now) {
                    delete cache[videoId];
                    changed = true;
                }
            }

            if (changed) {
                await GM.setValue('ytTitleCache', cache);
            }
        } catch (e) {
            console.warn('Failed to clear expired cache:', e);
        }
    }

    function getVideoId(href) {
        const YOUTUBE_REGEX = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
        const match = href.match(YOUTUBE_REGEX);
        return match ? match[1] : null;
    }

    function delay(ms) {
        return new Promise((resolve) => setTimeout(resolve, ms));
    }

    function fetchVideoData(videoId) {
        const url = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`;
        return new Promise((resolve, reject) => {
            GM.xmlHttpRequest({
                method: "GET",
                url: url,
                responseType: "json",
                onload: function (response) {
                    if (response.status === 200 && response.response) {
                        resolve(response.response);
                    } else {
                        reject(new Error(`Failed to fetch data for ${videoId}`));
                    }
                },
                onerror: function (err) {
                    reject(err);
                },
            });
        });
    }

    async function enhanceLinks(links) {
        // Clear expired cache entries occasionally
        if (Math.random() < CACHE_CLEANUP_PROBABILITY) {
            await clearExpiredCache();
        }

        // Process cached links first (no delay)
        const uncachedLinks = [];

        for (const link of links) {
            if (link.dataset.ytEnhanced || link.dataset.ytFailed) continue;

            processLink(link);
            const href = link.href;
            const videoId = getVideoId(href);

            if (!videoId) continue;

            // Check cache first
            const cachedTitle = await getCachedTitle(videoId);
            if (cachedTitle) {
                link.textContent = `[YouTube] ${cachedTitle} [${videoId}]`;
                link.classList.add("youtubelink");
                link.dataset.ytEnhanced = "true";
                continue;
            }

            // If not cached, add to queue for delayed processing
            uncachedLinks.push({ link, videoId });
        }

        // Process uncached links with delay
        for (const { link, videoId } of uncachedLinks) {
            try {
                const data = await fetchVideoData(videoId);
                const title = data.title;
                link.textContent = `[YouTube] ${title} [${videoId}]`;
                link.classList.add("youtubelink");
                link.dataset.ytEnhanced = "true";

                await setCachedTitle(videoId, title);
            } catch (e) {
                console.warn(`Error enhancing YouTube link:`, e);
                link.dataset.ytFailed = "true";
            }

            // Only delay if there are more links to process
            if (uncachedLinks.length > 1) {
                await delay(DELAY_MS);
            }
        }
    }

    // --- DOM Functions ---
    function findAndProcessLinksInNode(node) {
        if (node.nodeType === Node.ELEMENT_NODE) {
            let elementsToSearch = [];
            if (node.matches('.divMessage')) {
                elementsToSearch.push(node);
            }
            elementsToSearch.push(...node.querySelectorAll('.divMessage'));

            elementsToSearch.forEach(divMessage => {
                const links = divMessage.querySelectorAll('a');
                links.forEach(processLink);
            });
        }
    }

    function findYouTubeLinks() {
        return [...document.querySelectorAll('.divMessage a[href*="youtu.be"], .divMessage a[href*="youtube.com/watch?v="]')];
    }

    // --- Main Execution ---
    document.querySelectorAll('.divMessage a').forEach(processLink);

    const observer = new MutationObserver(async (mutationsList) => {
        let newLinks = [];

        for (const mutation of mutationsList) {
            if (mutation.type === 'childList') {
                for (const addedNode of mutation.addedNodes) {
                    findAndProcessLinksInNode(addedNode);

                    if (addedNode.nodeType === Node.ELEMENT_NODE) {
                        const links = addedNode.querySelectorAll ?
                            addedNode.querySelectorAll('a[href*="youtu.be"], a[href*="youtube.com/watch?v="]') : [];
                        newLinks.push(...links);
                    }
                }
            }
        }

        if (newLinks.length > 0) {
            await enhanceLinks(newLinks);
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // Initial enhancement
    (async function init() {
        await enhanceLinks(findYouTubeLinks());
    })();
})();