- // ==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.');
-
- })();