Greasy Fork is available in English.
Cleans up YouTube links and adds video titles in 8chan.moe posts
当前为
// ==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());
})();
})();