Fuck-YouTube

Combines 6-thumbnails-per-row, removes Shorts, disables AV1/WebRTC, adds video fit toggle, cleans URLs.

当前为 2025-04-21 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name          Fuck-YouTube
// @namespace     https://t.me/Impart_Chat
// @version       0.1
// @description   Combines 6-thumbnails-per-row, removes Shorts, disables AV1/WebRTC, adds video fit toggle, cleans URLs.
// @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.');

})();