Fuck-YouTube

每行合并 6 个缩略图、删除 Shorts、禁用 AV1/WebRTC、添加视频适配切换、清理 URL。

  1. // ==UserScript==
  2. // @name Fuck-YouTube
  3. // @namespace https://t.me/Impart_Chat
  4. // @version 0.1
  5. // @description 每行合并 6 个缩略图、删除 Shorts、禁用 AV1/WebRTC、添加视频适配切换、清理 URL。
  6. // @author https://t.me/Impart_Chat
  7. // @match https://*.youtube.com/*
  8. // @exclude https://accounts.youtube.com/*
  9. // @exclude https://studio.youtube.com/*
  10. // @exclude https://music.youtube.com/*
  11. // @grant GM_addStyle
  12. // @grant unsafeWindow
  13. // @run-at document-start
  14. // @license MIT; https://opensource.org/licenses/MIT
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // --- Helper Functions from Bilibili Script ---
  21. const o$1 = () => {}; // No-op function
  22. const noopNeverResolvedPromise = () => new Promise(o$1);
  23.  
  24. /* eslint-disable no-restricted-globals -- logger */
  25. const consoleLog = unsafeWindow.console.log;
  26. const consoleError = unsafeWindow.console.error;
  27. const consoleWarn = unsafeWindow.console.warn;
  28. const consoleInfo = unsafeWindow.console.info;
  29. const consoleDebug = unsafeWindow.console.debug;
  30. const consoleTrace = unsafeWindow.console.trace;
  31. const consoleGroup = unsafeWindow.console.group;
  32. const consoleGroupCollapsed = unsafeWindow.console.groupCollapsed;
  33. const consoleGroupEnd = unsafeWindow.console.groupEnd;
  34. const logger = {
  35. log: consoleLog.bind(console, '[YT Enhanced]'),
  36. error: consoleError.bind(console, '[YT Enhanced]'),
  37. warn: consoleWarn.bind(console, '[YT Enhanced]'),
  38. info: consoleInfo.bind(console, '[YT Enhanced]'),
  39. debug: consoleDebug.bind(console, '[YT Enhanced]'),
  40. trace(...args) {
  41. consoleGroupCollapsed.bind(console, '[YT Enhanced]')(...args);
  42. consoleTrace(...args);
  43. consoleGroupEnd();
  44. },
  45. group: consoleGroup.bind(console, '[YT Enhanced]'),
  46. groupCollapsed: consoleGroupCollapsed.bind(console, '[YT Enhanced]'),
  47. groupEnd: consoleGroupEnd.bind(console)
  48. };
  49.  
  50. function defineReadonlyProperty(target, key, value, enumerable = true) {
  51. Object.defineProperty(target, key, {
  52. get() {
  53. return value;
  54. },
  55. set: o$1,
  56. configurable: false, // Make it harder to change
  57. enumerable
  58. });
  59. }
  60.  
  61. // Simple template literal tag for CSS readability
  62. function e(r, ...t) {
  63. return r.reduce((e, r, n) => e + r + (t[n] ?? ""), "")
  64. }
  65.  
  66. // --- Feature Modules ---
  67.  
  68. // 1. Disable AV1 Codec (From Bilibili Script)
  69. const disableAV1 = {
  70. name: 'disable-av1',
  71. description: 'Prevent YouTube from using AV1 codec',
  72. apply() {
  73. try {
  74. const originalCanPlayType = HTMLMediaElement.prototype.canPlayType;
  75. // Check if prototype and function exist before overriding
  76. if (HTMLMediaElement && typeof originalCanPlayType === 'function') {
  77. HTMLMediaElement.prototype.canPlayType = function(type) {
  78. if (type && type.includes('av01')) {
  79. logger.info('AV1 canPlayType blocked:', type);
  80. return '';
  81. }
  82. // Ensure 'this' context is correct and call original
  83. return originalCanPlayType.call(this, type);
  84. };
  85. } else {
  86. logger.warn('HTMLMediaElement.prototype.canPlayType not found or not a function.');
  87. }
  88.  
  89.  
  90. const originalIsTypeSupported = unsafeWindow.MediaSource?.isTypeSupported;
  91. if (typeof originalIsTypeSupported === 'function') {
  92. unsafeWindow.MediaSource.isTypeSupported = function(type) {
  93. if (type && type.includes('av01')) {
  94. logger.info('AV1 isTypeSupported blocked:', type);
  95. return false;
  96. }
  97. return originalIsTypeSupported.call(this, type);
  98. };
  99. } else {
  100. logger.warn('MediaSource.isTypeSupported not found or not a function, cannot block AV1 via MediaSource.');
  101. }
  102.  
  103. logger.log(this.name, 'applied');
  104. } catch (err) {
  105. logger.error('Error applying', this.name, err);
  106. }
  107. }
  108. };
  109.  
  110. // 2. Disable WebRTC (From Bilibili Script)
  111. const noWebRTC = {
  112. name: 'no-webrtc',
  113. description: 'Disable WebRTC Peer Connections',
  114. apply() {
  115. try {
  116. const rtcPcNames = [];
  117. if ('RTCPeerConnection' in unsafeWindow) rtcPcNames.push('RTCPeerConnection');
  118. if ('webkitRTCPeerConnection' in unsafeWindow) rtcPcNames.push('webkitRTCPeerConnection');
  119. if ('mozRTCPeerConnection' in unsafeWindow) rtcPcNames.push('mozRTCPeerConnection');
  120.  
  121. const rtcDcNames = [];
  122. if ('RTCDataChannel' in unsafeWindow) rtcDcNames.push('RTCDataChannel');
  123. if ('webkitRTCDataChannel' in unsafeWindow) rtcDcNames.push('webkitRTCDataChannel');
  124. if ('mozRTCDataChannel' in unsafeWindow) rtcDcNames.push('mozRTCDataChannel');
  125.  
  126. class MockDataChannel {
  127. close = o$1; send = o$1; addEventListener = o$1; removeEventListener = o$1;
  128. onbufferedamountlow = null; onclose = null; onerror = null; onmessage = null; onopen = null;
  129. get bufferedAmount() { return 0; } get id() { return null; } get label() { return ''; }
  130. get maxPacketLifeTime() { return null; } get maxRetransmits() { return null; } get negotiated() { return false; }
  131. get ordered() { return true; } get protocol() { return ''; } get readyState() { return 'closed'; }
  132. get reliable() { return false; } get binaryType() { return 'blob'; } set binaryType(val) {}
  133. get bufferedAmountLowThreshold() { return 0; } set bufferedAmountLowThreshold(val) {}
  134. toString() { return '[object RTCDataChannel]'; }
  135. }
  136. class MockRTCSessionDescription {
  137. type; sdp;
  138. constructor(init){ this.type = init?.type ?? 'offer'; this.sdp = init?.sdp ?? ''; }
  139. toJSON() { return { type: this.type, sdp: this.sdp }; }
  140. toString() { return '[object RTCSessionDescription]'; }
  141. }
  142. const mockedRtcSessionDescription = new MockRTCSessionDescription();
  143. class MockRTCPeerConnection {
  144. createDataChannel() { return new MockDataChannel(); }
  145. close = o$1; createOffer = noopNeverResolvedPromise; setLocalDescription = async () => {};
  146. setRemoteDescription = async () => {}; addEventListener = o$1; removeEventListener = o$1;
  147. addIceCandidate = async () => {}; getConfiguration = () => ({}); getReceivers = () => [];
  148. getSenders = () => []; getStats = () => Promise.resolve(new Map()); getTransceivers = () => [];
  149. addTrack = () => null; removeTrack = o$1; addTransceiver = () => null; setConfiguration = o$1;
  150. get localDescription() { return mockedRtcSessionDescription; } get remoteDescription() { return mockedRtcSessionDescription; }
  151. get currentLocalDescription() { return mockedRtcSessionDescription; } get pendingLocalDescription() { return mockedRtcSessionDescription; }
  152. get currentRemoteDescription() { return mockedRtcSessionDescription; } get pendingRemoteDescription() { return mockedRtcSessionDescription; }
  153. get canTrickleIceCandidates() { return null; } get connectionState() { return 'disconnected'; }
  154. get iceConnectionState() { return 'disconnected'; } get iceGatheringState() { return 'complete'; }
  155. get signalingState() { return 'closed'; }
  156. onconnectionstatechange = null; ondatachannel = null; onicecandidate = null; onicecandidateerror = null;
  157. oniceconnectionstatechange = null; onicegatheringstatechange = null; onnegotiationneeded = null;
  158. onsignalingstatechange = null; ontrack = null; createAnswer = noopNeverResolvedPromise;
  159. toString() { return '[object RTCPeerConnection]'; }
  160. }
  161.  
  162. for (const rtc of rtcPcNames) defineReadonlyProperty(unsafeWindow, rtc, MockRTCPeerConnection);
  163. for (const dc of rtcDcNames) defineReadonlyProperty(unsafeWindow, dc, MockDataChannel);
  164. defineReadonlyProperty(unsafeWindow, 'RTCSessionDescription', MockRTCSessionDescription);
  165.  
  166. logger.log(this.name, 'applied');
  167. } catch (err) {
  168. logger.error('Error applying', this.name, err);
  169. }
  170. }
  171. };
  172.  
  173. // 3. Player Video Fit (Adapted from Bilibili Script)
  174. const playerVideoFit = {
  175. name: 'player-video-fit',
  176. description: 'Adds a toggle for video fit mode (cover/contain)',
  177. apply() {
  178. try {
  179. // Inject CSS first
  180. GM_addStyle(e`
  181. /* Style for the body when fit mode is active */
  182. body[video-fit-mode-enabled] .html5-video-player video.video-stream,
  183. body[video-fit-mode-enabled] .html5-video-player .html5-main-video {
  184. object-fit: cover !important;
  185. }
  186. /* Style for the button in the settings menu */
  187. .ytp-settings-menu .ytp-menuitem[aria-haspopup="false"][role="menuitemcheckbox"] {
  188. justify-content: space-between; /* Align label and checkbox */
  189. }
  190. .ytp-settings-menu .ytp-menuitem-label {
  191. flex-grow: 1;
  192. margin-right: 10px; /* Space before checkbox */
  193. }
  194. .ytp-menuitem-toggle-checkbox {
  195. /* Style the checkbox appearance if needed */
  196. margin: 0 !important; /* Reset margin */
  197. height: 100%;
  198. display: flex;
  199. align-items: center;
  200. }
  201. `);
  202.  
  203. let fitModeEnabled = localStorage.getItem('yt-enhanced-video-fit') === 'true';
  204.  
  205. function toggleMode(enabled) {
  206. fitModeEnabled = enabled;
  207. if (enabled) {
  208. document.body.setAttribute('video-fit-mode-enabled', '');
  209. localStorage.setItem('yt-enhanced-video-fit', 'true');
  210. } else {
  211. document.body.removeAttribute('video-fit-mode-enabled');
  212. localStorage.setItem('yt-enhanced-video-fit', 'false');
  213. }
  214. }
  215.  
  216. function injectButtonLogic() { // Renamed function for clarity
  217. // Use MutationObserver to detect when the settings menu is added
  218. const observer = new MutationObserver((mutationsList, obs) => {
  219. for (const mutation of mutationsList) {
  220. if (mutation.type === 'childList') {
  221. const settingsMenu = document.querySelector('.ytp-settings-menu');
  222. const panelMenu = settingsMenu?.querySelector('.ytp-panel-menu'); // Target the inner menu list
  223.  
  224. // Check if the menu is visible and our button isn't already there
  225. if (settingsMenu && panelMenu && !panelMenu.querySelector('#ytp-fit-mode-toggle')) {
  226. // Check if settings menu is actually visible (has style other than display: none)
  227. const style = window.getComputedStyle(settingsMenu);
  228. if (style.display !== 'none') {
  229. logger.debug('Settings menu opened, attempting to inject button.');
  230. addButtonToMenu(panelMenu);
  231. // Maybe disconnect observer once button is added, or keep it for dynamic changes?
  232. // obs.disconnect(); // Disconnect if only needed once per menu open
  233. }
  234. }
  235. }
  236. }
  237. });
  238.  
  239. // Observe the player container or body for changes
  240. const player = document.getElementById('movie_player');
  241. if (player) {
  242. observer.observe(player, { childList: true, subtree: true });
  243. logger.log('MutationObserver attached to player for settings menu.');
  244. } else {
  245. // Wait a bit and try again if player isn't immediately available
  246. setTimeout(() => {
  247. const playerRetry = document.getElementById('movie_player');
  248. if (playerRetry) {
  249. observer.observe(playerRetry, { childList: true, subtree: true });
  250. logger.log('MutationObserver attached to player after retry.');
  251. } else {
  252. logger.warn('Player element not found for MutationObserver, Fit Mode button might not appear.');
  253. }
  254. }, 2000); // Wait 2 seconds
  255. }
  256.  
  257. // Initial check in case the menu is already open when script runs
  258. const initialPanelMenu = document.querySelector('.ytp-settings-menu .ytp-panel-menu');
  259. if (initialPanelMenu && !initialPanelMenu.querySelector('#ytp-fit-mode-toggle')) {
  260. const style = window.getComputedStyle(initialPanelMenu.closest('.ytp-settings-menu'));
  261. if (style.display !== 'none') {
  262. addButtonToMenu(initialPanelMenu);
  263. }
  264. }
  265.  
  266. // Initial body attribute application
  267. if (fitModeEnabled) {
  268. document.body.setAttribute('video-fit-mode-enabled', '');
  269. }
  270. }
  271.  
  272. function addButtonToMenu(panelMenu) {
  273. if (!panelMenu || panelMenu.querySelector('#ytp-fit-mode-toggle')) return; // Already added or menu gone
  274.  
  275. try {
  276. const newItem = document.createElement('div');
  277. newItem.className = 'ytp-menuitem';
  278. newItem.setAttribute('role', 'menuitemcheckbox');
  279. newItem.setAttribute('aria-checked', fitModeEnabled.toString());
  280. newItem.id = 'ytp-fit-mode-toggle';
  281. newItem.tabIndex = 0;
  282.  
  283. const label = document.createElement('div');
  284. label.className = 'ytp-menuitem-label';
  285. label.textContent = '裁切模式 (Fit Mode)'; // Or 'Video Fit Mode'
  286.  
  287. const content = document.createElement('div');
  288. content.className = 'ytp-menuitem-content';
  289. // Simple checkbox look-alike
  290. content.innerHTML = `<div class="ytp-menuitem-toggle-checkbox"> ${fitModeEnabled ? '☑' : '☐'} </div>`;
  291.  
  292.  
  293. newItem.appendChild(label);
  294. newItem.appendChild(content);
  295.  
  296. newItem.addEventListener('click', (e) => { // Use event object
  297. e.stopPropagation(); // Prevent menu closing
  298. const newState = !fitModeEnabled;
  299. toggleMode(newState);
  300. newItem.setAttribute('aria-checked', newState.toString());
  301. content.innerHTML = `<div class="ytp-menuitem-toggle-checkbox"> ${newState ? '☑' : '☐'} </div>`;
  302. });
  303.  
  304. // Insert before the "Stats for nerds" or Quality item, or just append
  305. const qualityItem = Array.from(panelMenu.children).find(el => el.textContent.includes('Quality') || el.textContent.includes('画质')); // Added Chinese Quality
  306. if (qualityItem) {
  307. panelMenu.insertBefore(newItem, qualityItem.nextSibling); // Insert after Quality
  308. } else {
  309. // Try inserting before Loop or Stats if Quality not found
  310. const loopItem = Array.from(panelMenu.children).find(el => el.textContent.includes('Loop') || el.textContent.includes('循环播放'));
  311. if (loopItem) {
  312. panelMenu.insertBefore(newItem, loopItem);
  313. } else {
  314. const statsItem = Array.from(panelMenu.children).find(el => el.textContent.includes('Stats for nerds') || el.textContent.includes('详细统计信息'));
  315. if (statsItem) {
  316. panelMenu.insertBefore(newItem, statsItem);
  317. } else {
  318. panelMenu.appendChild(newItem); // Append as last resort
  319. }
  320. }
  321. }
  322. logger.log('Fit Mode button injected.');
  323. } catch (e) {
  324. logger.error("Error injecting Fit Mode button:", e);
  325. }
  326. }
  327.  
  328. // Wait for the page elements to likely exist
  329. if (document.readyState === 'loading') {
  330. document.addEventListener('DOMContentLoaded', injectButtonLogic);
  331. } else {
  332. injectButtonLogic(); // Already loaded
  333. }
  334.  
  335. logger.log(this.name, 'applied');
  336. } catch (err) {
  337. logger.error('Error applying', this.name, err);
  338. }
  339. }
  340. };
  341.  
  342. // 4. Remove Black Backdrop Filter (From Bilibili Script - Generic)
  343. const removeBlackBackdropFilter = {
  344. name: 'remove-black-backdrop-filter',
  345. description: 'Removes potential site-wide grayscale filters',
  346. apply() {
  347. try {
  348. GM_addStyle(e`html, body { filter: none !important; -webkit-filter: none !important; }`);
  349. logger.log(this.name, 'applied');
  350. } catch (err) {
  351. logger.error('Error applying', this.name, err);
  352. }
  353. }
  354. };
  355.  
  356. // 5. Remove Useless URL Parameters (Adapted from Bilibili Script)
  357. const removeUselessUrlParams = {
  358. name: 'remove-useless-url-params',
  359. description: 'Clean URLs from tracking parameters',
  360. apply() {
  361. try {
  362. // Common YouTube tracking parameters (add more as needed)
  363. const youtubeUselessUrlParams = [
  364. 'si', // Share ID? Added recently
  365. 'pp', // ??? Related to recommendations/playback source?
  366. 'feature', // e.g., feature=share, feature=emb_logo
  367. 'gclid', // Google Click ID
  368. 'dclid', // Google Display Click ID
  369. 'fbclid', // Facebook Click ID
  370. 'utm_source', // Urchin Tracking Module params
  371. 'utm_medium',
  372. 'utm_campaign',
  373. 'utm_term',
  374. 'utm_content',
  375. 'oac', // ?? Found sometimes
  376. '_hsenc', // HubSpot
  377. '_hsmi', // HubSpot
  378. 'mc_eid', // Mailchimp
  379. 'mc_cid', // Mailchimp
  380. ];
  381.  
  382. function removeTracking(url) {
  383. if (!url) return url;
  384. let urlObj;
  385. try {
  386. // Handle relative URLs and ensure it's a valid URL format
  387. if (typeof url === 'string' && (url.startsWith('/') || url.startsWith('./') || url.startsWith('../'))) {
  388. urlObj = new URL(url, unsafeWindow.location.href);
  389. } else if (typeof url === 'string') {
  390. urlObj = new URL(url); // Assume absolute if not clearly relative
  391. } else if (url instanceof URL){
  392. urlObj = url;
  393. } else {
  394. logger.warn('Invalid URL type for removeTracking:', url);
  395. return url; // Return original if type is wrong
  396. }
  397.  
  398. if (!urlObj.search) return urlObj.href; // No params to clean
  399.  
  400. const params = urlObj.searchParams;
  401. let changed = false;
  402.  
  403. // Iterate over a copy of keys because deleting modifies the collection
  404. const keysToDelete = [];
  405. for (const key of params.keys()) {
  406. for (const item of youtubeUselessUrlParams) {
  407. let match = false;
  408. if (typeof item === 'string') {
  409. if (item === key) match = true;
  410. } else if (item instanceof RegExp && item.test(key)) {
  411. match = true;
  412. }
  413. if (match) {
  414. keysToDelete.push(key);
  415. break; // Move to next key once a match is found
  416. }
  417. }
  418. }
  419.  
  420. if (keysToDelete.length > 0) {
  421. keysToDelete.forEach(key => params.delete(key));
  422. changed = true;
  423. }
  424.  
  425.  
  426. // Return original string if no changes, href otherwise
  427. return changed ? urlObj.href : (typeof url === 'string' ? url : url.href);
  428. } catch (e) {
  429. // Catch potential URL parsing errors
  430. if (e instanceof TypeError && e.message.includes("Invalid URL")) {
  431. // Ignore invalid URL errors often caused by non-standard URIs like about:blank
  432. return url;
  433. }
  434. logger.error('Failed to remove useless urlParams for:', url, e);
  435. return (typeof url === 'string' ? url : url?.href ?? ''); // Return original on other errors
  436. }
  437. }
  438.  
  439.  
  440. // Initial clean
  441. const initialHref = unsafeWindow.location.href;
  442. const cleanedHref = removeTracking(initialHref);
  443. if (initialHref !== cleanedHref) {
  444. logger.log('Initial URL cleaned:', initialHref, '->', cleanedHref);
  445. // Use try-catch for replaceState as well, as it can fail on certain pages/frames
  446. try {
  447. unsafeWindow.history.replaceState(unsafeWindow.history.state, '', cleanedHref);
  448. } catch (histErr) {
  449. logger.error("Failed to replaceState for initial URL:", histErr);
  450. }
  451. }
  452.  
  453.  
  454. // Hook history API
  455. const originalPushState = unsafeWindow.history.pushState;
  456. unsafeWindow.history.pushState = function(state, title, url) {
  457. const cleaned = removeTracking(url);
  458. if (url && url !== cleaned) { // Check if url is not null/undefined
  459. logger.log('pushState URL cleaned:', url, '->', cleaned);
  460. }
  461. // Use try-catch for safety
  462. try {
  463. return originalPushState.call(unsafeWindow.history, state, title, cleaned ?? url); // Pass original url if cleaning fails
  464. } catch (pushErr) {
  465. logger.error("Error in hooked pushState:", pushErr);
  466. // Attempt to call original with original URL as fallback
  467. return originalPushState.call(unsafeWindow.history, state, title, url);
  468. }
  469. };
  470.  
  471. const originalReplaceState = unsafeWindow.history.replaceState;
  472. unsafeWindow.history.replaceState = function(state, title, url) {
  473. const cleaned = removeTracking(url);
  474. if (url && url !== cleaned) { // Check if url is not null/undefined
  475. logger.log('replaceState URL cleaned:', url, '->', cleaned);
  476. }
  477. // Use try-catch for safety
  478. try {
  479. return originalReplaceState.call(unsafeWindow.history, state, title, cleaned ?? url); // Pass original url if cleaning fails
  480. } catch (replaceErr) {
  481. logger.error("Error in hooked replaceState:", replaceErr);
  482. // Attempt to call original with original URL as fallback
  483. return originalReplaceState.call(unsafeWindow.history, state, title, url);
  484. }
  485. };
  486.  
  487. logger.log(this.name, 'applied');
  488. } catch (err) {
  489. logger.error('Error applying', this.name, err);
  490. }
  491. }
  492. };
  493.  
  494. // 6. Use System Fonts (Adapted from Bilibili Script)
  495. const useSystemFonts = {
  496. name: 'use-system-fonts',
  497. description: 'Force system default fonts instead of YouTube specific fonts',
  498. apply() {
  499. try {
  500. // Force system UI font on main elements
  501. GM_addStyle(e`
  502. html, body, #masthead, #content, ytd-app, tp-yt-app-drawer, #guide,
  503. input, button, textarea, select, .ytd-video-primary-info-renderer,
  504. .ytd-video-secondary-info-renderer, #comments, #comment,
  505. .ytd-rich-grid-media .ytd-rich-item-renderer #video-title, /* Titles in grids */
  506. .ytp-tooltip-text, .ytp-menuitem-label, .ytp-title-text /* Player UI elements */
  507. {
  508. font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important;
  509. }
  510. `);
  511. logger.log(this.name, 'applied');
  512. } catch (err) {
  513. logger.error('Error applying', this.name, err);
  514. }
  515. }
  516. };
  517.  
  518. // 7. 6 Thumbnails Per Row (Original YouTube Script's Core Function)
  519. const sixThumbs = {
  520. name: 'six-thumbnails-per-row',
  521. description: 'Sets YouTube grid items to 6 per row',
  522. apply() {
  523. try {
  524. GM_addStyle(e`
  525. /* Set the number of items per row in main grids (Home, Subscriptions, etc.) */
  526. ytd-rich-grid-renderer {
  527. --ytd-rich-grid-items-per-row: 6 !important;
  528. }
  529. /* Handle browse grids (e.g., channel pages, maybe search, subscriptions) more broadly */
  530. ytd-two-column-browse-results-renderer[is-grid] #primary #contents.ytd-section-list-renderer > *.ytd-section-list-renderer,
  531. ytd-browse #primary #contents.ytd-section-list-renderer > *.ytd-section-list-renderer:has(ytd-rich-grid-renderer), /* Target sections containing a rich grid */
  532. ytd-browse[page-subtype="subscriptions"] #contents.ytd-section-list-renderer /* Specifically target subs grid */ {
  533. --ytd-rich-grid-items-per-row: 6 !important;
  534. }
  535.  
  536. /* Wider container for grids to accommodate 6 items better */
  537. ytd-rich-grid-renderer #contents.ytd-rich-grid-renderer {
  538. /* Use viewport width units for better scaling, with a max-width */
  539. width: calc(100vw - var(--ytd-guide-width, 240px) - 48px); /* Adjust guide width and margins */
  540. 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 */
  541. margin: auto; /* Center the grid */
  542. }
  543. /* Ensure shelf renderers also use 6 */
  544. ytd-shelf-renderer[use-show-fewer] #items.ytd-shelf-renderer {
  545. --ytd-shelf-items-per-row: 6 !important;
  546. }
  547.  
  548. `);
  549. logger.log(this.name, 'applied');
  550. } catch (err) {
  551. logger.error('Error applying', this.name, err);
  552. }
  553. }
  554. };
  555.  
  556. // 8. Remove Shorts (NEW MODULE)
  557. const removeShorts = {
  558. name: 'remove-shorts',
  559. description: 'Hides YouTube Shorts elements from the UI',
  560. apply() {
  561. try {
  562. GM_addStyle(e`
  563. /* Hide Shorts tab in sidebar guide */
  564. ytd-guide-entry-renderer:has(a#endpoint[title='Shorts']),
  565. ytd-guide-entry-renderer:has(yt-icon path[d^='M10 14.14V9.86']), /* Alternative selector based on SVG icon path (might change) */
  566. ytd-mini-guide-entry-renderer[aria-label='Shorts'] {
  567. display: none !important;
  568. }
  569.  
  570. /* Hide Shorts shelves/sections */
  571. ytd-reel-shelf-renderer,
  572. ytd-rich-shelf-renderer[is-shorts] {
  573. display: none !important;
  574. }
  575.  
  576. /* Hide individual Shorts videos in feeds/grids */
  577. ytd-grid-video-renderer:has(ytd-thumbnail-overlay-time-status-renderer[overlay-style='SHORTS']),
  578. ytd-video-renderer:has(ytd-thumbnail-overlay-time-status-renderer[overlay-style='SHORTS']),
  579. ytd-rich-item-renderer:has(ytd-reel-item-renderer) {
  580. display: none !important;
  581. }
  582.  
  583. /* Hide Shorts tab on Channel pages */
  584. tp-yt-paper-tab:has(.tab-title) {
  585. /* Using attribute selector for potential future proofing if YT adds one */
  586. &[aria-label*="Shorts"],
  587. /* Check title attribute as well */
  588. &.ytd-browse[title="Shorts"],
  589. /* Fallback using text content - least reliable */
  590. &:has(span.tab-title:only-child:contains("Shorts")) {
  591. display: none !important;
  592. }
  593. }
  594.  
  595. /* Hide the "Shorts" header above grid sections on channel pages */
  596. ytd-rich-grid-renderer #title-container.ytd-rich-grid-renderer:has(h2 yt-formatted-string:contains("Shorts")) {
  597. display: none !important;
  598. }
  599. `);
  600. logger.log(this.name, 'applied');
  601. } catch (err) {
  602. logger.error('Error applying', this.name, err);
  603. }
  604. }
  605. };
  606.  
  607.  
  608. // --- Apply Features ---
  609. logger.log('Initializing YouTube Enhanced script...');
  610.  
  611. // Apply features immediately at document-start where possible
  612. disableAV1.apply();
  613. noWebRTC.apply();
  614. removeUselessUrlParams.apply();
  615.  
  616. // Apply CSS-based features
  617. sixThumbs.apply();
  618. useSystemFonts.apply();
  619. removeBlackBackdropFilter.apply();
  620. removeShorts.apply(); // Apply the new feature
  621. playerVideoFit.apply(); // Sets up button injection logic
  622.  
  623. logger.log('YouTube Enhanced script initialization complete.');
  624.  
  625. })();