Force YouTube to show compact grid (max 6 videos per row) rather than "slim" grid (max 3 videos per row)
Stan na
// ==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');
});
})();