YouTube - Force Compact Grid (increases max # videos per row)

Force YouTube to show compact grid (max 6 videos per row) rather than "slim" grid (max 3 videos per row)

当前为 2023-02-04 提交的版本,查看 最新版本

// ==UserScript==
// @name         YouTube - Force Compact Grid (increases max # videos per row)
// @namespace    https://gist.github.com/lbmaian/8c6961584c0aebf41ee7496609f60bc3
// @version      0.2
// @description  Force YouTube to show compact grid (max 6 videos per row) rather than "slim" grid (max 3 videos per row)
// @author       lbmaian
// @match        https://www.youtube.com/*
// @exclude      https://www.youtube.com/embed/*
// @icon         https://www.youtube.com/favicon.ico
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const DEBUG = false;

    const logContext = '[YouTube - Force Compact Grid]';

    var debug;
    if (DEBUG) {
        debug = function(...args) {
            console.debug(logContext, ...args);
        };
    } else {
        debug = function() {};
    }

    function log(...args) {
        console.log(logContext, ...args);
    }

    function info(...args) {
        console.info(logContext, ...args);
    }

    function warn(...args) {
        console.warn(logContext, ...args);
    }

    function error(...args) {
        console.error(logContext, ...args);
    }

    function updateResponseData(response, label) {
        const tabs = response?.contents?.twoColumnBrowseResultsRenderer?.tabs;
        if (DEBUG) {
            debug(label, 'contents.twoColumnBrowseResultsRenderer.tabs (snapshot)', window.structuredClone(tabs));
        }
        if (tabs) {
            for (const tab of tabs) {
                const tabRenderer = tab.tabRenderer;
                if (tabRenderer) {
                    const richGridRenderer = tabRenderer.content?.richGridRenderer;
                    if (richGridRenderer && (!richGridRenderer.style || richGridRenderer.style == 'RICH_GRID_STYLE_SLIM')) {
                        log(label, 'tab', tabRenderer.title ?? tabRenderer.tabIdentifier,
                            'tabRenderer.content.richGridRenderer.style:', richGridRenderer.style, '=> RICH_GRID_STYLE_COMPACT');
                        richGridRenderer.style = 'RICH_GRID_STYLE_COMPACT';
                    }
                }
            }
        }
    }

    // Note: Both of the following commented-out event listeners are too late:
    // ytd-app's own yt-page-data-fetched event listener (onYtPageDataFetched) already fires
    // by the time our own yt-page-data-fetched event listener fires,
    // and yt-page-data-fetched fires before yt-navigate-finish fires

    // document.addEventListener('yt-page-data-fetched', evt => {
    //     debug('Navigated to', evt.detail.pageData.url);
    //     debug(evt);
    //     updateResponseData(evt.detail.pageData.response, 'yt-page-data-fetched pageData.response');
    // });

    // document.addEventListener('yt-navigate-finish', evt => {
    //     debug('Navigated to', evt.detail.response.url);
    //     debug(evt);
    //     updateResponseData(evt.detail.response.response, 'yt-navigate-finish response.response');
    // });

    // yt-page-data-fetched event fires on both new page load and channel tab change
    // Need to hook into ytd-app's ytd-app's own yt-page-data-fetched event listener (onYtPageDataFetched),
    // so that we can modify the data before that event listener fires
    function setupYtdApp(ytdApp) {
        const origOnYtPageDataFetched = ytdApp.onYtPageDataFetched;
        ytdApp.onYtPageDataFetched = function(evt, detail) {
            debug('Navigated to', detail.pageData.url);
            debug(evt);
            updateResponseData(evt.detail.pageData.response, 'yt-page-data-fetched pageData.response');
            return origOnYtPageDataFetched.call(this, evt, detail);
        };
        log('ytd-app onYtPageDataFetched hook set up');
    }

    // Wait for ytd-app element to exist AND for its prototype to be populated with the onYtPageDataFetched method
    const ytdApp = document.getElementsByTagName('ytd-app')[0];
    if (ytdApp && ytdApp.onYtPageDataFetched) {
        debug('ytd-app immediately found', ytdApp);
        setupYtdApp(ytdApp);
    } else {
        new MutationObserver((records, observer) => {
            const ytdApp = document.getElementsByTagName('ytd-app')[0];
            if (ytdApp && ytdApp.onYtPageDataFetched) {
                observer.disconnect();
                debug('ytd-app found', ytdApp);
                setupYtdApp(ytdApp);
            }
        }).observe(document, {
            childList: true,
            subtree: true,
        });
    }

    // Note: updating ytInitialData may not be necessary, since yt-page-data-fetched also fires for new page load,
    // and in that case, the event's detail.pageData.response is the same object as ytInitialData,
    // but DOMContentLoaded sometimes fires before ytd-app's onYtPageDataFetched fires (or rather, before we can hook into it),
    // so this is done just in case
    document.addEventListener('DOMContentLoaded', evt => {
        debug('ytInitialData', window.ytInitialData);
        updateResponseData(window.ytInitialData, 'ytInitialData');
    });
})();