您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds channel names as prefixes to YouTube video titles and formats playlist titles better. Adds video length. Makes it easier to understand YouTube Sidebery tabs.
// ==UserScript== // @name YouTube Tab Title Improver Extreme Deluxe // @namespace http://tampermonkey.net/ // @version 2.0 // @license MIT // @description Adds channel names as prefixes to YouTube video titles and formats playlist titles better. Adds video length. Makes it easier to understand YouTube Sidebery tabs. // @author // @match https://www.youtube.com/* // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // ==/UserScript== (function() { 'use strict'; // ----------------------------- // 1. SETTINGS + MENU FUNCTIONS // ----------------------------- // Default settings const settings = { removeNotificationCounter: true, addChannelName: true, addVideoLength: true }; // Load settings from storage or use defaults const userSettings = GM_getValue('userSettings', settings); function saveSettings(newSettings) { GM_setValue('userSettings', newSettings); } function editSettings() { const menu = `Edit Settings:\n 1. Remove notification counter: ${userSettings.removeNotificationCounter ? 'ON' : 'OFF'}\n 2. Add channel name: ${userSettings.addChannelName ? 'ON' : 'OFF'}\n 3. Add video length: ${userSettings.addVideoLength ? 'ON' : 'OFF'}\n\nEnter the number to toggle:`; const choice = prompt(menu); if (choice === '1') { userSettings.removeNotificationCounter = !userSettings.removeNotificationCounter; } else if (choice === '2') { userSettings.addChannelName = !userSettings.addChannelName; } else if (choice === '3') { userSettings.addVideoLength = !userSettings.addVideoLength; } else { alert('Invalid choice. Please try again.'); return; } saveSettings(userSettings); alert('Settings updated!'); } GM_registerMenuCommand('Edit Script Settings', editSettings); // Load the channel name map from storage or use default values const defaultChannelNameMap = { 'Digital Foundry': 'DF', 'Kurzgesagt – In a Nutshell': 'Kurzgesagt' }; const channelNameMap = GM_getValue('channelNameMap', defaultChannelNameMap); function saveChannelNameMap(newMap) { GM_setValue('channelNameMap', newMap); } function editChannelNameMap() { const keys = Object.keys(channelNameMap); const values = Object.values(channelNameMap); let menu = 'Edit Channel Name Substitutions:\n'; keys.forEach((key, index) => { menu += `${index + 1}. ${key} -> ${values[index]}\n`; }); menu += '\nEnter the number of the channel to edit, or type "add" to add a new substitution, or "delete" to remove one:'; const userChoice = prompt(menu); if (userChoice === "add") { const newKey = prompt("Enter the full channel name:"); const newValue = prompt("Enter the short name or abbreviation:"); if (newKey && newValue) { channelNameMap[newKey] = newValue; saveChannelNameMap(channelNameMap); alert("Channel added successfully!"); } else { alert("Invalid input. Please try again."); } } else if (userChoice === "delete") { const deleteKey = prompt("Enter the full channel name to delete:"); if (deleteKey && channelNameMap[deleteKey]) { delete channelNameMap[deleteKey]; saveChannelNameMap(channelNameMap); alert("Channel removed successfully!"); } else { alert("Channel not found. Please try again."); } } else if (!isNaN(userChoice) && userChoice > 0 && userChoice <= keys.length) { const index = parseInt(userChoice) - 1; const selectedKey = keys[index]; const newValue = prompt(`Enter a new short name for "${selectedKey}" (currently "${values[index]}"):`); if (newValue) { channelNameMap[selectedKey] = newValue; saveChannelNameMap(channelNameMap); alert("Channel updated successfully!"); } else { alert("Invalid input. Please try again."); } } else { alert("Invalid choice. Please try again."); } } GM_registerMenuCommand('Edit Channel Name Substitutions', editChannelNameMap); // --------------------------------- // 2. GET VIDEO INFO FROM JSON-LD // --------------------------------- /** * Attempts to parse the JSON-LD from the <script type="application/ld+json"> * found inside "#microformat player-microformat-renderer". * Returns an object like: * { * name: "Video Title", * author: "Channel Name", * duration: "PT542S" // ISO 8601 * } * or null if not found/failed. */ function getVideoInfoFromJSONLD() { const scriptTag = document.querySelector( '#microformat player-microformat-renderer > script[type="application/ld+json"]' ); if (scriptTag) { try { const data = JSON.parse(scriptTag.textContent); // We'll only return the fields we actually use: return { name: data.name || '', author: data.author || '', // e.g. "PT542S" duration: data.duration || '' }; } catch (err) { console.error('JSON parse error:', err); } } return null; } // -------------------------------- // 3. CORE LOGIC: MODIFY TITLES // -------------------------------- // Converts an ISO 8601 duration (e.g. "PT542S") into something like "9:02" // If you prefer to keep using the .ytp-time-duration from the player, skip this function. function parseIsoDuration(isoDuration) { // Quick-and-dirty parse for "PT#M#S" or "PT#S" or "PT#H#M#S" etc. const match = isoDuration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/); if (!match) return ''; let hours = parseInt(match[1] || '0', 10); let minutes = parseInt(match[2] || '0', 10); let seconds = parseInt(match[3] || '0', 10); let result = ''; if (hours > 0) { result += hours + ':'; // pad minutes if < 10 result += minutes < 10 ? '0' + minutes : minutes; result += ':'; // pad seconds if < 10 result += seconds < 10 ? '0' + seconds : seconds; } else { // just minutes:seconds result += minutes + ':'; result += seconds < 10 ? '0' + seconds : seconds; } return result; } function modifyVideoTitle() { const info = getVideoInfoFromJSONLD(); if (!info) { // No JSON-LD found? YouTube might be in a weird state, or // the page hasn't loaded that script yet. We'll skip for now. return; } // Extract from JSON let videoTitle = (info.name || '').trim(); let originalChannelName = (info.author || '').trim(); // If the "author" is sometimes an object, you may need something like: // originalChannelName = typeof info.author === 'object' ? info.author.name : info.author; // But in your example, "author" is just "Max Fosh". // Possibly read the actual rendered video length from .ytp-time-duration // This is the official YouTube overlay. If we rely on the ISO parse, skip this. // We'll do it your old way: const videoLengthElement = document.querySelector('.ytp-time-duration'); let finalLength = ''; if (userSettings.addVideoLength && videoLengthElement) { finalLength = videoLengthElement.textContent.trim(); } // Or you can parse the ISO for a guaranteed result, even if not playing yet: // let finalLength = parseIsoDuration(info.duration); // Let user’s short channel name override the original const shortChannelName = channelNameMap[originalChannelName] || originalChannelName; // Add channel name as prefix if (userSettings.addChannelName && shortChannelName) { const prefix = `${shortChannelName} - `; if (!videoTitle.startsWith(prefix)) { videoTitle = prefix + videoTitle; } } // Add video length at the end if (userSettings.addVideoLength && finalLength) { if (!videoTitle.endsWith(` (${finalLength})`)) { videoTitle += ` (${finalLength})`; } } // Remove notification counter (e.g. (3) ) if enabled if (userSettings.removeNotificationCounter) { videoTitle = videoTitle.replace(/^\(\d+\)\s*/, ''); } // Finally, set the document title if (document.title !== videoTitle) { document.title = videoTitle; } } // (Optional) old function to modify playlist title (unchanged for now) function modifyPlaylistTitle() { const playlistTitleSelector = 'h1.title.style-scope.ytd-playlist-header-renderer yt-formatted-string'; const channelNameSelector = '.yt-simple-endpoint.style-scope.yt-formatted-string'; const playlistTitleElement = document.querySelector(playlistTitleSelector); const channelNameElement = document.querySelector(channelNameSelector); if (playlistTitleElement && channelNameElement) { const playlistTitle = playlistTitleElement.textContent.trim(); let channelName = channelNameElement.textContent.trim(); channelName = channelName.replace(/^[^\w\s]*\s*/, '').trim(); const newTitle = `${playlistTitle} - ${channelName}`; if (userSettings.removeNotificationCounter) { document.title = document.title.replace(/^\(\d+\)\s*/, ''); } if (document.title !== newTitle) { document.title = newTitle; } } } // Decides which function to use based on URL function detectAndModifyTitle() { const url = window.location.href; if (url.includes('playlist')) { modifyPlaylistTitle(); } else if (url.includes('watch')) { modifyVideoTitle(); } } // ----------------------------- // 4. DEBOUNCING HELPER // ----------------------------- function debounce(fn, delay) { let timeoutID; return function(...args) { if (timeoutID) clearTimeout(timeoutID); timeoutID = setTimeout(() => fn.apply(this, args), delay); }; } const debouncedDetectAndModifyTitle = debounce(detectAndModifyTitle, 300); // ----------------------------- // 5. OBSERVERS & EVENT HOOKS // ----------------------------- // (A) Broad observer on <ytd-app> to detect major DOM changes function initObserver() { const targetNode = document.querySelector('ytd-app'); if (!targetNode) { console.log('ytd-app not found. Retrying in 1 second...'); setTimeout(initObserver, 1000); return; } const observerConfig = { childList: true, subtree: true }; const observer = new MutationObserver((mutations) => { for (let mutation of mutations) { if (mutation.type === 'childList') { debouncedDetectAndModifyTitle(); break; } } }); observer.observe(targetNode, observerConfig); console.log('Broad MutationObserver initialized on <ytd-app>.'); } // (B) More specific observer: watch the microformat script area // If YouTube updates the JSON-LD for queued videos, we can catch it. function initMicroformatObserver() { const microformatContainer = document.querySelector('#microformat player-microformat-renderer'); if (!microformatContainer) { console.log('Microformat container not found. Retrying in 1 second...'); setTimeout(initMicroformatObserver, 1000); return; } const observerConfig = { childList: true, subtree: true, characterData: true }; const observer = new MutationObserver(() => { debouncedDetectAndModifyTitle(); }); observer.observe(microformatContainer, observerConfig); console.log('Observer initialized on the microformat container.'); } // (C) Listen for history changes (SPA navigation) function listenForHistoryChanges() { const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function(...args) { originalPushState.apply(this, args); window.dispatchEvent(new Event('locationchange')); }; history.replaceState = function(...args) { originalReplaceState.apply(this, args); window.dispatchEvent(new Event('locationchange')); }; window.addEventListener('popstate', () => { window.dispatchEvent(new Event('locationchange')); }); window.addEventListener('locationchange', () => { debouncedDetectAndModifyTitle(); }); console.log('History change listeners set up.'); } // ----------------------------- // 6. INITIALIZATION // ----------------------------- function initialize() { initObserver(); // broad observer on <ytd-app> initMicroformatObserver(); // observer on the JSON-LD microformat area listenForHistoryChanges(); detectAndModifyTitle(); // Initial run } if (document.readyState === 'complete' || document.readyState === 'interactive') { initialize(); } else { document.addEventListener('DOMContentLoaded', initialize); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址