// ==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 // @downloadURL none // ==/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 = ` `.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()); })(); })();