您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
每行合并 6 个缩略图、删除 Shorts、禁用 AV1/WebRTC、添加视频适配切换、清理 URL。
当前为
// ==UserScript== // @name Fuck-YouTube // @namespace https://t.me/Impart_Chat // @version 0.1 // @description 每行合并 6 个缩略图、删除 Shorts、禁用 AV1/WebRTC、添加视频适配切换、清理 URL。 // @author https://t.me/Impart_Chat // @match https://*.youtube.com/* // @exclude https://accounts.youtube.com/* // @exclude https://studio.youtube.com/* // @exclude https://music.youtube.com/* // @grant GM_addStyle // @grant unsafeWindow // @run-at document-start // @license MIT; https://opensource.org/licenses/MIT // ==/UserScript== (function() { 'use strict'; // --- Helper Functions from Bilibili Script --- const o$1 = () => {}; // No-op function const noopNeverResolvedPromise = () => new Promise(o$1); /* eslint-disable no-restricted-globals -- logger */ const consoleLog = unsafeWindow.console.log; const consoleError = unsafeWindow.console.error; const consoleWarn = unsafeWindow.console.warn; const consoleInfo = unsafeWindow.console.info; const consoleDebug = unsafeWindow.console.debug; const consoleTrace = unsafeWindow.console.trace; const consoleGroup = unsafeWindow.console.group; const consoleGroupCollapsed = unsafeWindow.console.groupCollapsed; const consoleGroupEnd = unsafeWindow.console.groupEnd; const logger = { log: consoleLog.bind(console, '[YT Enhanced]'), error: consoleError.bind(console, '[YT Enhanced]'), warn: consoleWarn.bind(console, '[YT Enhanced]'), info: consoleInfo.bind(console, '[YT Enhanced]'), debug: consoleDebug.bind(console, '[YT Enhanced]'), trace(...args) { consoleGroupCollapsed.bind(console, '[YT Enhanced]')(...args); consoleTrace(...args); consoleGroupEnd(); }, group: consoleGroup.bind(console, '[YT Enhanced]'), groupCollapsed: consoleGroupCollapsed.bind(console, '[YT Enhanced]'), groupEnd: consoleGroupEnd.bind(console) }; function defineReadonlyProperty(target, key, value, enumerable = true) { Object.defineProperty(target, key, { get() { return value; }, set: o$1, configurable: false, // Make it harder to change enumerable }); } // Simple template literal tag for CSS readability function e(r, ...t) { return r.reduce((e, r, n) => e + r + (t[n] ?? ""), "") } // --- Feature Modules --- // 1. Disable AV1 Codec (From Bilibili Script) const disableAV1 = { name: 'disable-av1', description: 'Prevent YouTube from using AV1 codec', apply() { try { const originalCanPlayType = HTMLMediaElement.prototype.canPlayType; // Check if prototype and function exist before overriding if (HTMLMediaElement && typeof originalCanPlayType === 'function') { HTMLMediaElement.prototype.canPlayType = function(type) { if (type && type.includes('av01')) { logger.info('AV1 canPlayType blocked:', type); return ''; } // Ensure 'this' context is correct and call original return originalCanPlayType.call(this, type); }; } else { logger.warn('HTMLMediaElement.prototype.canPlayType not found or not a function.'); } const originalIsTypeSupported = unsafeWindow.MediaSource?.isTypeSupported; if (typeof originalIsTypeSupported === 'function') { unsafeWindow.MediaSource.isTypeSupported = function(type) { if (type && type.includes('av01')) { logger.info('AV1 isTypeSupported blocked:', type); return false; } return originalIsTypeSupported.call(this, type); }; } else { logger.warn('MediaSource.isTypeSupported not found or not a function, cannot block AV1 via MediaSource.'); } logger.log(this.name, 'applied'); } catch (err) { logger.error('Error applying', this.name, err); } } }; // 2. Disable WebRTC (From Bilibili Script) const noWebRTC = { name: 'no-webrtc', description: 'Disable WebRTC Peer Connections', apply() { try { const rtcPcNames = []; if ('RTCPeerConnection' in unsafeWindow) rtcPcNames.push('RTCPeerConnection'); if ('webkitRTCPeerConnection' in unsafeWindow) rtcPcNames.push('webkitRTCPeerConnection'); if ('mozRTCPeerConnection' in unsafeWindow) rtcPcNames.push('mozRTCPeerConnection'); const rtcDcNames = []; if ('RTCDataChannel' in unsafeWindow) rtcDcNames.push('RTCDataChannel'); if ('webkitRTCDataChannel' in unsafeWindow) rtcDcNames.push('webkitRTCDataChannel'); if ('mozRTCDataChannel' in unsafeWindow) rtcDcNames.push('mozRTCDataChannel'); class MockDataChannel { close = o$1; send = o$1; addEventListener = o$1; removeEventListener = o$1; onbufferedamountlow = null; onclose = null; onerror = null; onmessage = null; onopen = null; get bufferedAmount() { return 0; } get id() { return null; } get label() { return ''; } get maxPacketLifeTime() { return null; } get maxRetransmits() { return null; } get negotiated() { return false; } get ordered() { return true; } get protocol() { return ''; } get readyState() { return 'closed'; } get reliable() { return false; } get binaryType() { return 'blob'; } set binaryType(val) {} get bufferedAmountLowThreshold() { return 0; } set bufferedAmountLowThreshold(val) {} toString() { return '[object RTCDataChannel]'; } } class MockRTCSessionDescription { type; sdp; constructor(init){ this.type = init?.type ?? 'offer'; this.sdp = init?.sdp ?? ''; } toJSON() { return { type: this.type, sdp: this.sdp }; } toString() { return '[object RTCSessionDescription]'; } } const mockedRtcSessionDescription = new MockRTCSessionDescription(); class MockRTCPeerConnection { createDataChannel() { return new MockDataChannel(); } close = o$1; createOffer = noopNeverResolvedPromise; setLocalDescription = async () => {}; setRemoteDescription = async () => {}; addEventListener = o$1; removeEventListener = o$1; addIceCandidate = async () => {}; getConfiguration = () => ({}); getReceivers = () => []; getSenders = () => []; getStats = () => Promise.resolve(new Map()); getTransceivers = () => []; addTrack = () => null; removeTrack = o$1; addTransceiver = () => null; setConfiguration = o$1; get localDescription() { return mockedRtcSessionDescription; } get remoteDescription() { return mockedRtcSessionDescription; } get currentLocalDescription() { return mockedRtcSessionDescription; } get pendingLocalDescription() { return mockedRtcSessionDescription; } get currentRemoteDescription() { return mockedRtcSessionDescription; } get pendingRemoteDescription() { return mockedRtcSessionDescription; } get canTrickleIceCandidates() { return null; } get connectionState() { return 'disconnected'; } get iceConnectionState() { return 'disconnected'; } get iceGatheringState() { return 'complete'; } get signalingState() { return 'closed'; } onconnectionstatechange = null; ondatachannel = null; onicecandidate = null; onicecandidateerror = null; oniceconnectionstatechange = null; onicegatheringstatechange = null; onnegotiationneeded = null; onsignalingstatechange = null; ontrack = null; createAnswer = noopNeverResolvedPromise; toString() { return '[object RTCPeerConnection]'; } } for (const rtc of rtcPcNames) defineReadonlyProperty(unsafeWindow, rtc, MockRTCPeerConnection); for (const dc of rtcDcNames) defineReadonlyProperty(unsafeWindow, dc, MockDataChannel); defineReadonlyProperty(unsafeWindow, 'RTCSessionDescription', MockRTCSessionDescription); logger.log(this.name, 'applied'); } catch (err) { logger.error('Error applying', this.name, err); } } }; // 3. Player Video Fit (Adapted from Bilibili Script) const playerVideoFit = { name: 'player-video-fit', description: 'Adds a toggle for video fit mode (cover/contain)', apply() { try { // Inject CSS first GM_addStyle(e` /* Style for the body when fit mode is active */ body[video-fit-mode-enabled] .html5-video-player video.video-stream, body[video-fit-mode-enabled] .html5-video-player .html5-main-video { object-fit: cover !important; } /* Style for the button in the settings menu */ .ytp-settings-menu .ytp-menuitem[aria-haspopup="false"][role="menuitemcheckbox"] { justify-content: space-between; /* Align label and checkbox */ } .ytp-settings-menu .ytp-menuitem-label { flex-grow: 1; margin-right: 10px; /* Space before checkbox */ } .ytp-menuitem-toggle-checkbox { /* Style the checkbox appearance if needed */ margin: 0 !important; /* Reset margin */ height: 100%; display: flex; align-items: center; } `); let fitModeEnabled = localStorage.getItem('yt-enhanced-video-fit') === 'true'; function toggleMode(enabled) { fitModeEnabled = enabled; if (enabled) { document.body.setAttribute('video-fit-mode-enabled', ''); localStorage.setItem('yt-enhanced-video-fit', 'true'); } else { document.body.removeAttribute('video-fit-mode-enabled'); localStorage.setItem('yt-enhanced-video-fit', 'false'); } } function injectButtonLogic() { // Renamed function for clarity // Use MutationObserver to detect when the settings menu is added const observer = new MutationObserver((mutationsList, obs) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { const settingsMenu = document.querySelector('.ytp-settings-menu'); const panelMenu = settingsMenu?.querySelector('.ytp-panel-menu'); // Target the inner menu list // Check if the menu is visible and our button isn't already there if (settingsMenu && panelMenu && !panelMenu.querySelector('#ytp-fit-mode-toggle')) { // Check if settings menu is actually visible (has style other than display: none) const style = window.getComputedStyle(settingsMenu); if (style.display !== 'none') { logger.debug('Settings menu opened, attempting to inject button.'); addButtonToMenu(panelMenu); // Maybe disconnect observer once button is added, or keep it for dynamic changes? // obs.disconnect(); // Disconnect if only needed once per menu open } } } } }); // Observe the player container or body for changes const player = document.getElementById('movie_player'); if (player) { observer.observe(player, { childList: true, subtree: true }); logger.log('MutationObserver attached to player for settings menu.'); } else { // Wait a bit and try again if player isn't immediately available setTimeout(() => { const playerRetry = document.getElementById('movie_player'); if (playerRetry) { observer.observe(playerRetry, { childList: true, subtree: true }); logger.log('MutationObserver attached to player after retry.'); } else { logger.warn('Player element not found for MutationObserver, Fit Mode button might not appear.'); } }, 2000); // Wait 2 seconds } // Initial check in case the menu is already open when script runs const initialPanelMenu = document.querySelector('.ytp-settings-menu .ytp-panel-menu'); if (initialPanelMenu && !initialPanelMenu.querySelector('#ytp-fit-mode-toggle')) { const style = window.getComputedStyle(initialPanelMenu.closest('.ytp-settings-menu')); if (style.display !== 'none') { addButtonToMenu(initialPanelMenu); } } // Initial body attribute application if (fitModeEnabled) { document.body.setAttribute('video-fit-mode-enabled', ''); } } function addButtonToMenu(panelMenu) { if (!panelMenu || panelMenu.querySelector('#ytp-fit-mode-toggle')) return; // Already added or menu gone try { const newItem = document.createElement('div'); newItem.className = 'ytp-menuitem'; newItem.setAttribute('role', 'menuitemcheckbox'); newItem.setAttribute('aria-checked', fitModeEnabled.toString()); newItem.id = 'ytp-fit-mode-toggle'; newItem.tabIndex = 0; const label = document.createElement('div'); label.className = 'ytp-menuitem-label'; label.textContent = '裁切模式 (Fit Mode)'; // Or 'Video Fit Mode' const content = document.createElement('div'); content.className = 'ytp-menuitem-content'; // Simple checkbox look-alike content.innerHTML = `<div class="ytp-menuitem-toggle-checkbox"> ${fitModeEnabled ? '☑' : '☐'} </div>`; newItem.appendChild(label); newItem.appendChild(content); newItem.addEventListener('click', (e) => { // Use event object e.stopPropagation(); // Prevent menu closing const newState = !fitModeEnabled; toggleMode(newState); newItem.setAttribute('aria-checked', newState.toString()); content.innerHTML = `<div class="ytp-menuitem-toggle-checkbox"> ${newState ? '☑' : '☐'} </div>`; }); // Insert before the "Stats for nerds" or Quality item, or just append const qualityItem = Array.from(panelMenu.children).find(el => el.textContent.includes('Quality') || el.textContent.includes('画质')); // Added Chinese Quality if (qualityItem) { panelMenu.insertBefore(newItem, qualityItem.nextSibling); // Insert after Quality } else { // Try inserting before Loop or Stats if Quality not found const loopItem = Array.from(panelMenu.children).find(el => el.textContent.includes('Loop') || el.textContent.includes('循环播放')); if (loopItem) { panelMenu.insertBefore(newItem, loopItem); } else { const statsItem = Array.from(panelMenu.children).find(el => el.textContent.includes('Stats for nerds') || el.textContent.includes('详细统计信息')); if (statsItem) { panelMenu.insertBefore(newItem, statsItem); } else { panelMenu.appendChild(newItem); // Append as last resort } } } logger.log('Fit Mode button injected.'); } catch (e) { logger.error("Error injecting Fit Mode button:", e); } } // Wait for the page elements to likely exist if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', injectButtonLogic); } else { injectButtonLogic(); // Already loaded } logger.log(this.name, 'applied'); } catch (err) { logger.error('Error applying', this.name, err); } } }; // 4. Remove Black Backdrop Filter (From Bilibili Script - Generic) const removeBlackBackdropFilter = { name: 'remove-black-backdrop-filter', description: 'Removes potential site-wide grayscale filters', apply() { try { GM_addStyle(e`html, body { filter: none !important; -webkit-filter: none !important; }`); logger.log(this.name, 'applied'); } catch (err) { logger.error('Error applying', this.name, err); } } }; // 5. Remove Useless URL Parameters (Adapted from Bilibili Script) const removeUselessUrlParams = { name: 'remove-useless-url-params', description: 'Clean URLs from tracking parameters', apply() { try { // Common YouTube tracking parameters (add more as needed) const youtubeUselessUrlParams = [ 'si', // Share ID? Added recently 'pp', // ??? Related to recommendations/playback source? 'feature', // e.g., feature=share, feature=emb_logo 'gclid', // Google Click ID 'dclid', // Google Display Click ID 'fbclid', // Facebook Click ID 'utm_source', // Urchin Tracking Module params 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'oac', // ?? Found sometimes '_hsenc', // HubSpot '_hsmi', // HubSpot 'mc_eid', // Mailchimp 'mc_cid', // Mailchimp ]; function removeTracking(url) { if (!url) return url; let urlObj; try { // Handle relative URLs and ensure it's a valid URL format if (typeof url === 'string' && (url.startsWith('/') || url.startsWith('./') || url.startsWith('../'))) { urlObj = new URL(url, unsafeWindow.location.href); } else if (typeof url === 'string') { urlObj = new URL(url); // Assume absolute if not clearly relative } else if (url instanceof URL){ urlObj = url; } else { logger.warn('Invalid URL type for removeTracking:', url); return url; // Return original if type is wrong } if (!urlObj.search) return urlObj.href; // No params to clean const params = urlObj.searchParams; let changed = false; // Iterate over a copy of keys because deleting modifies the collection const keysToDelete = []; for (const key of params.keys()) { for (const item of youtubeUselessUrlParams) { let match = false; if (typeof item === 'string') { if (item === key) match = true; } else if (item instanceof RegExp && item.test(key)) { match = true; } if (match) { keysToDelete.push(key); break; // Move to next key once a match is found } } } if (keysToDelete.length > 0) { keysToDelete.forEach(key => params.delete(key)); changed = true; } // Return original string if no changes, href otherwise return changed ? urlObj.href : (typeof url === 'string' ? url : url.href); } catch (e) { // Catch potential URL parsing errors if (e instanceof TypeError && e.message.includes("Invalid URL")) { // Ignore invalid URL errors often caused by non-standard URIs like about:blank return url; } logger.error('Failed to remove useless urlParams for:', url, e); return (typeof url === 'string' ? url : url?.href ?? ''); // Return original on other errors } } // Initial clean const initialHref = unsafeWindow.location.href; const cleanedHref = removeTracking(initialHref); if (initialHref !== cleanedHref) { logger.log('Initial URL cleaned:', initialHref, '->', cleanedHref); // Use try-catch for replaceState as well, as it can fail on certain pages/frames try { unsafeWindow.history.replaceState(unsafeWindow.history.state, '', cleanedHref); } catch (histErr) { logger.error("Failed to replaceState for initial URL:", histErr); } } // Hook history API const originalPushState = unsafeWindow.history.pushState; unsafeWindow.history.pushState = function(state, title, url) { const cleaned = removeTracking(url); if (url && url !== cleaned) { // Check if url is not null/undefined logger.log('pushState URL cleaned:', url, '->', cleaned); } // Use try-catch for safety try { return originalPushState.call(unsafeWindow.history, state, title, cleaned ?? url); // Pass original url if cleaning fails } catch (pushErr) { logger.error("Error in hooked pushState:", pushErr); // Attempt to call original with original URL as fallback return originalPushState.call(unsafeWindow.history, state, title, url); } }; const originalReplaceState = unsafeWindow.history.replaceState; unsafeWindow.history.replaceState = function(state, title, url) { const cleaned = removeTracking(url); if (url && url !== cleaned) { // Check if url is not null/undefined logger.log('replaceState URL cleaned:', url, '->', cleaned); } // Use try-catch for safety try { return originalReplaceState.call(unsafeWindow.history, state, title, cleaned ?? url); // Pass original url if cleaning fails } catch (replaceErr) { logger.error("Error in hooked replaceState:", replaceErr); // Attempt to call original with original URL as fallback return originalReplaceState.call(unsafeWindow.history, state, title, url); } }; logger.log(this.name, 'applied'); } catch (err) { logger.error('Error applying', this.name, err); } } }; // 6. Use System Fonts (Adapted from Bilibili Script) const useSystemFonts = { name: 'use-system-fonts', description: 'Force system default fonts instead of YouTube specific fonts', apply() { try { // Force system UI font on main elements GM_addStyle(e` html, body, #masthead, #content, ytd-app, tp-yt-app-drawer, #guide, input, button, textarea, select, .ytd-video-primary-info-renderer, .ytd-video-secondary-info-renderer, #comments, #comment, .ytd-rich-grid-media .ytd-rich-item-renderer #video-title, /* Titles in grids */ .ytp-tooltip-text, .ytp-menuitem-label, .ytp-title-text /* Player UI elements */ { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important; } `); logger.log(this.name, 'applied'); } catch (err) { logger.error('Error applying', this.name, err); } } }; // 7. 6 Thumbnails Per Row (Original YouTube Script's Core Function) const sixThumbs = { name: 'six-thumbnails-per-row', description: 'Sets YouTube grid items to 6 per row', apply() { try { GM_addStyle(e` /* Set the number of items per row in main grids (Home, Subscriptions, etc.) */ ytd-rich-grid-renderer { --ytd-rich-grid-items-per-row: 6 !important; } /* Handle browse grids (e.g., channel pages, maybe search, subscriptions) more broadly */ ytd-two-column-browse-results-renderer[is-grid] #primary #contents.ytd-section-list-renderer > *.ytd-section-list-renderer, ytd-browse #primary #contents.ytd-section-list-renderer > *.ytd-section-list-renderer:has(ytd-rich-grid-renderer), /* Target sections containing a rich grid */ ytd-browse[page-subtype="subscriptions"] #contents.ytd-section-list-renderer /* Specifically target subs grid */ { --ytd-rich-grid-items-per-row: 6 !important; } /* Wider container for grids to accommodate 6 items better */ ytd-rich-grid-renderer #contents.ytd-rich-grid-renderer { /* Use viewport width units for better scaling, with a max-width */ width: calc(100vw - var(--ytd-guide-width, 240px) - 48px); /* Adjust guide width and margins */ max-width: calc(var(--ytd-rich-grid-item-max-width, 360px) * 6 + var(--ytd-rich-grid-item-margin, 16px) * 12 + 24px); /* Original max-width as fallback */ margin: auto; /* Center the grid */ } /* Ensure shelf renderers also use 6 */ ytd-shelf-renderer[use-show-fewer] #items.ytd-shelf-renderer { --ytd-shelf-items-per-row: 6 !important; } `); logger.log(this.name, 'applied'); } catch (err) { logger.error('Error applying', this.name, err); } } }; // 8. Remove Shorts (NEW MODULE) const removeShorts = { name: 'remove-shorts', description: 'Hides YouTube Shorts elements from the UI', apply() { try { GM_addStyle(e` /* Hide Shorts tab in sidebar guide */ ytd-guide-entry-renderer:has(a#endpoint[title='Shorts']), ytd-guide-entry-renderer:has(yt-icon path[d^='M10 14.14V9.86']), /* Alternative selector based on SVG icon path (might change) */ ytd-mini-guide-entry-renderer[aria-label='Shorts'] { display: none !important; } /* Hide Shorts shelves/sections */ ytd-reel-shelf-renderer, ytd-rich-shelf-renderer[is-shorts] { display: none !important; } /* Hide individual Shorts videos in feeds/grids */ ytd-grid-video-renderer:has(ytd-thumbnail-overlay-time-status-renderer[overlay-style='SHORTS']), ytd-video-renderer:has(ytd-thumbnail-overlay-time-status-renderer[overlay-style='SHORTS']), ytd-rich-item-renderer:has(ytd-reel-item-renderer) { display: none !important; } /* Hide Shorts tab on Channel pages */ tp-yt-paper-tab:has(.tab-title) { /* Using attribute selector for potential future proofing if YT adds one */ &[aria-label*="Shorts"], /* Check title attribute as well */ &.ytd-browse[title="Shorts"], /* Fallback using text content - least reliable */ &:has(span.tab-title:only-child:contains("Shorts")) { display: none !important; } } /* Hide the "Shorts" header above grid sections on channel pages */ ytd-rich-grid-renderer #title-container.ytd-rich-grid-renderer:has(h2 yt-formatted-string:contains("Shorts")) { display: none !important; } `); logger.log(this.name, 'applied'); } catch (err) { logger.error('Error applying', this.name, err); } } }; // --- Apply Features --- logger.log('Initializing YouTube Enhanced script...'); // Apply features immediately at document-start where possible disableAV1.apply(); noWebRTC.apply(); removeUselessUrlParams.apply(); // Apply CSS-based features sixThumbs.apply(); useSystemFonts.apply(); removeBlackBackdropFilter.apply(); removeShorts.apply(); // Apply the new feature playerVideoFit.apply(); // Sets up button injection logic logger.log('YouTube Enhanced script initialization complete.'); })();