FuckYouTube

每行合并 5 个缩略图、删除 Shorts、禁用 AV1/WebRTC、添加视频适配切换、清理 URL、调整字体大小;

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name          FuckYouTube
// @namespace     https://t.me/Impart_Chat
// @version       0.1.3
// @description   每行合并 5 个缩略图、删除 Shorts、禁用 AV1/WebRTC、添加视频适配切换、清理 URL、调整字体大小;
// @author        ChuwuYo
// @match         https://*.youtube.com/*
// @match         https://www.youtubekids.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
// @icon          https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @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
    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
    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
    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);
            }
        }
    };

    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
                    'igshid',     // Instagram 跟踪ID
                    'ttclid',     // TikTok Click ID
                    'yclid',      // Yandex Click ID
                    'msclkid',    // Microsoft Click ID
                    'oly_anon_id',// Outbrain 跟踪
                    'oly_enc_id', // Outbrain 跟踪
                    /^vtm_[a-z]+$/, // 匹配类似vtm_campaign、vtm_source等
                ];

                function removeTracking(url) {
                    if (!url) return url;

                    // 跳过非HTTP(S)和非相对路径的URL
                    if (typeof url === 'string' && /^(?:javascript|data|about|blob):/i.test(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 (optimized with shared wrapper)
                const wrapHistoryMethod = (originalMethod) => {
                    return function(state, title, url) {
                        const cleaned = url ? removeTracking(url) : url; // Handle null/undefined
                        if (url && url !== cleaned) {
                            logger.log('History API URL cleaned:', url, '->', cleaned);
                        }
                        try {
                            return originalMethod.call(unsafeWindow.history, state, title, cleaned ?? url);
                        } catch (e) {
                            logger.error("History method error:", e);
                            return originalMethod.call(unsafeWindow.history, state, title, url); // Fallback
                        }
                    };
                };

                unsafeWindow.history.pushState = wrapHistoryMethod(unsafeWindow.history.pushState);
                unsafeWindow.history.replaceState = wrapHistoryMethod(unsafeWindow.history.replaceState);

                logger.log(this.name, 'applied');
            } catch (err) {
                logger.error('Error applying', this.name, err);
            }
        }
    };

    // 6. Use System Fonts & Adjust Font Size (Enhanced)
    const useSystemFonts = {
        name: 'use-system-fonts',
        description: 'Force system fonts and adjust font size for better readability',
        apply() {
            try {
                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,
                    .ytp-tooltip-text, .ytp-menuitem-label, .ytp-title-text {
                        font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important;
                    }

                    /* ===== 全局字体大小调整 ===== */
                    body, yt-formatted-string, span, a, div {
                        font-size: 14px !important;  /* 默认字体大小 */
                    }

                    /* 视频标题(首页/频道页) */
                    #video-title,
                    .ytd-rich-grid-media #video-title {
                        font-size: 14px !important;
                        font-weight: 500 !important;  /* 适当加粗 */
                    }

                    /* 播放器内的标题 */
                    .ytp-title-text {
                        font-size: 18px !important;
                    }

                    /* 侧边栏导航文字 */
                    ytd-guide-entry-renderer #label,
                    #guide-content yt-formatted-string {
                        font-size: 14px !important;
                    }

                    /* 评论区字体 */
                    #comment #content-text,
                    ytd-comment-renderer #content-text {
                        font-size: 14px !important;
                        line-height: 1.4 !important;  /* 改善行距 */
                    }

                    /* 设置菜单字体(如播放速度、画质选项) */
                    .ytp-settings-menu .ytp-menuitem-label {
                        font-size: 14px !important;
                    }
                `);
                logger.log(this.name, 'applied');
            } catch (err) {
                logger.error('Error applying', this.name, err);
            }
        }
    };

    // 7. 5 Thumbnails Per Row
    const sixThumbs = {
        name: 'six-thumbnails-per-row',
        description: 'Sets YouTube grid items to 5 per row (excluding @xxx/videos pages)',
        apply() {
            try {
                GM_addStyle(e`
                    /* ===== 默认规则:大部分页面 5 个/行 ===== */
                    ytd-rich-grid-renderer {
                        --ytd-rich-grid-items-per-row: 5 !important;
                    }

                    /* ===== 排除 @xxx/videos 页面===== */
                    ytd-browse[page-subtype="channels"] ytd-rich-grid-renderer,
                    ytd-browse[page-subtype="channels-videos"] ytd-rich-grid-renderer {
                        --ytd-rich-grid-items-per-row: 4 !important; /* 强制 4 个/行 */
                    }

                    /* ===== 修复容器宽度(防止第 5 个视频溢出)===== */
                    ytd-rich-grid-renderer #contents.ytd-rich-grid-renderer {
                        width: calc(100vw - var(--ytd-guide-width, 240px) - 48px);
                        max-width: calc(var(--ytd-rich-grid-item-max-width, 360px) * 5 + var(--ytd-rich-grid-item-margin, 16px) * 12 + 24px);
                        margin: auto;
                    }

                    /* ===== 确保其他页面(如订阅、搜索)仍然是 5 个/行 ===== */
                    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),
                    ytd-browse[page-subtype="subscriptions"] #contents.ytd-section-list-renderer {
                        --ytd-rich-grid-items-per-row: 5 !important;
                    }

                    /* ===== 确保 shelf 缩略图也是 5 个/行 ===== */
                    ytd-shelf-renderer[use-show-fewer] #items.ytd-shelf-renderer {
                        --ytd-shelf-items-per-row: 5 !important;
                    }
                `);
                logger.log(this.name, 'applied');
            } catch (err) {
                logger.error('Error applying', this.name, err);
            }
        }
    };

    // 8. Remove Shorts
    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.');

})();