Greasy Fork

Greasy Fork is available in English.

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.0
// @description  Cleans up YouTube links and adds video titles in 8chan.moe posts
// @author       anon, Gemini, DeepSeek
// @license      MIT
// @match        https://8chan.moe/*
// @match        https://8chan.se/*
// @grant        GM.xmlHttpRequest
// @connect      youtube.com
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const DELAY_MS = 200; // Delay between YouTube API requests to avoid rate limiting

    // --- YouTube Link Cleaning Functions ---

    // Function to clean a single YouTube URL string
    function cleanYouTubeUrl(url) {
        if (!url || (!url.includes('youtube.com') && !url.includes('youtu.be'))) {
            return url; // Not a YouTube link, return as is
        }

        let cleaned = url;

        // 1. Handle youtu.be
        if (cleaned.startsWith('https://youtu.be/')) {
            const videoIdPath = cleaned.substring('https://youtu.be/'.length);
            // Find the end of the video ID or the start of parameters/hash
            const paramIndex = videoIdPath.search(/[?#]/);
            const videoId = paramIndex === -1 ? videoIdPath : videoIdPath.substring(0, paramIndex);
            const rest = paramIndex === -1 ? '' : videoIdPath.substring(paramIndex); // Keep parameters/hash
            cleaned = `https://www.youtube.com/watch?v=${videoId}${rest}`;
        }

        // 2. Handle /live/ (only applies to youtube.com after youtu.be conversion if applicable)
        if (cleaned.includes('youtube.com/live/')) {
             cleaned = cleaned.replace('/live/', '/watch?v=');
        }

        // 3. Remove ?si= parameter (and the preceding ? or &)
        // This regex handles ?si=... at the start of parameters or &si=... later
        cleaned = cleaned.replace(/[?&]si=[^&]+/, '');

        // Clean up potentially resulting trailing ? or & if the removed param was the only one
        if (cleaned.endsWith('?') || cleaned.endsWith('&')) {
            cleaned = cleaned.slice(0, -1);
        }

        return cleaned;
    }

    // Function to process a single link element
    function processLink(link) {
        const currentUrl = link.href; // Get the fully resolved URL from the href attribute

        // Quickly check if it's potentially a YouTube link to avoid unnecessary processing
        if (!currentUrl.includes('youtube.com') && !currentUrl.includes('youtu.be')) {
            return;
        }

        const cleanedUrl = cleanYouTubeUrl(currentUrl);

        // If the URL was changed
        if (cleanedUrl !== currentUrl) {
            // Update the href attribute
            link.href = cleanedUrl;

            // Update the visible text ONLY if it was originally the exact URL string
            // This prevents changing user-provided link text like "My cool video"
            if (link.textContent.trim() === currentUrl.trim()) {
                 link.textContent = cleanedUrl;
            }
        }
    }

    // --- YouTube Link Enhancement Functions ---

    // Red YouTube icon as a masked SVG
    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)}`;

    // Add styles for YouTube links
    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);

    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) {
        for (const link of links) {
            if (link.dataset.ytEnhanced || link.dataset.ytFailed) continue;

            // First clean the link if needed
            processLink(link);

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

            if (!videoId) continue;

            try {
                const data = await fetchVideoData(videoId);
                link.textContent = `${data.title} [${videoId}]`;
                link.classList.add("youtubelink");
                link.dataset.ytEnhanced = "true";
            } catch (e) {
                console.warn(`Error enhancing YouTube link:`, e);
                link.dataset.ytFailed = "true";
            }

            await delay(DELAY_MS);
        }
    }

    // --- Common DOM Functions ---

    function findAndProcessLinksInNode(node) {
        // Check if the node itself is a divMessage or contains divMessage descendants
        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 ---

    // 1. Process all existing links when the script first runs
    document.querySelectorAll('.divMessage a').forEach(processLink);

    // 2. Set up MutationObservers to handle dynamically loaded content
    const observer = new MutationObserver(async (mutationsList) => {
        let newLinks = [];
        let needsCleaning = false;

        for (const mutation of mutationsList) {
            if (mutation.type === 'childList') {
                for (const addedNode of mutation.addedNodes) {
                    // Process any added node that contains or is a .divMessage
                    findAndProcessLinksInNode(addedNode);

                    // Check for new YouTube links
                    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);
        }
    });

    // Start observing the document body for additions of new nodes
    observer.observe(document.body, { childList: true, subtree: true });

    // 3. Initial enhancement of existing links
    (async function init() {
        await enhanceLinks(findYouTubeLinks());
    })();
})();