Skip to content

Instantly share code, notes, and snippets.

@lbmaian
Last active March 4, 2025 10:16
Show Gist options
  • Save lbmaian/228de31c3203b82e4d6dccb17fced67a to your computer and use it in GitHub Desktop.
Save lbmaian/228de31c3203b82e4d6dccb17fced67a to your computer and use it in GitHub Desktop.
YouTube - Hide Breaking News section
// ==UserScript==
// @name YouTube - Hide Breaking News section
// @namespace https://gist.github.com/lbmaian/228de31c3203b82e4d6dccb17fced67a
// @downloadURL https://gist.github.com/lbmaian/228de31c3203b82e4d6dccb17fced67a/raw/youtube-hide-breaking-news.user.js
// @updateURL https://gist.github.com/lbmaian/228de31c3203b82e4d6dccb17fced67a/raw/youtube-hide-breaking-news.user.js
// @version 0.3
// @description Hide Breaking News section on YouTube homepage (works only in EN(-US) and JP localization)
// @author lbmaian
// @match https://www.youtube.com/*
// @match https://m.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 - Hide Breaking News]';
function consoleLog(level, ...args) {
if (typeof(args[0]) === 'string') {
args[0] = logContext + ' ' + args[0];
window.console[level](...args);
} else {
window.console[level](logContext, ...args);
}
}
const console = {
...window.console,
debug(...args) {
if (DEBUG) {
consoleLog('debug', ...args);
}
},
log(...args) {
consoleLog('log', ...args);
},
warn(...args) {
consoleLog('warn', ...args);
},
error(...args) {
consoleLog('error', ...args);
},
};
// Note: Following all relies on YT internals and is based off https://gist.github.com/lbmaian/8c6961584c0aebf41ee7496609f60bc3
function updateResponseData(response, logContext) {
const tabs = response?.contents?.twoColumnBrowseResultsRenderer?.tabs;
if (DEBUG) {
console.debug(logContext, 'contents.twoColumnBrowseResultsRenderer.tabs (snapshot)', window.structuredClone(tabs));
}
if (tabs) {
for (let tabIdx = 0; tabIdx < tabs.length; tabIdx++) {
const tab = tabs[tabIdx];
const tabRenderer = tab.tabRenderer;
if (tabRenderer) {
const richGridRenderer = tabRenderer.content?.richGridRenderer;
if (richGridRenderer?.contents) {
richGridRenderer.contents = richGridRenderer.contents.filter((content, contentIdx) => {
for (let [contentRendererName, contentRenderer] of Object.entries(content)) {
if (contentRendererName === 'richSectionRenderer' && contentRenderer.content) {
console.debug('.tabs[%d].tabRenderer.content.richGridRenderer.contents[%d].%s.content:',
tabIdx, contentIdx, contentRendererName, contentRenderer);
for (let [sectionContentRendererName, sectionContentRenderer] of Object.entries(contentRenderer.content)) {
console.debug('.tabs[%d].tabRenderer.content.richGridRenderer.contents[%d].%s.content.%s:',
tabIdx, contentIdx, contentRendererName, sectionContentRendererName, sectionContentRenderer);
if (sectionContentRendererName === 'richShelfRenderer') {
// Block any section with a title of "Breaking News"
let title = sectionContentRenderer.title?.runs?.[0]?.text;
// TODO: Generalize for more languages?
// Maybe https://github.com/kakajuro/tidytube/blob/main/src/util/languageAdapter.ts
if (title === 'Breaking news' || title === 'ニュース速報') {
console.log('contents.twoColumnBrowseResultsRenderer.tabs[%d].tabRenderer.content.richGridRenderer.contents[%d] filtered due to .%s.content.%s.title "%s":',
tabIdx, contentIdx, contentRendererName, sectionContentRendererName, title, content);
return false;
}
// Also block any section with a "YouTube featured" badge
// TODO: Generalize for more languages, or for any metadata badge?
let badges = sectionContentRenderer.badges;
if (badges) {
for (let badgeIdx = 0; badgeIdx < badges.length; badgeIdx++) {
let badgeLabel = badges[badgeIdx].metadataBadgeRenderer?.label;
if (badgeLabel === 'YouTube featured') {
console.log('contents.twoColumnBrowseResultsRenderer.tabs[%d].tabRenderer.content.richGridRenderer.contents[%d] ' +
'filtered due to .%s.content.%s.badges[%d].metadataBadgeRenderer.label "%s":',
tabIdx, contentIdx, contentRendererName, sectionContentRendererName, badgeIdx, badgeLabel);
return false;
}
}
}
}
}
}
}
return true;
});
}
}
}
}
}
const symSetup = Symbol(logContext + ' setup');
// 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, logContext, errorFunc=console.error) {
// ytd-app's prototype is initialized after the element is created,
// so need to check that the onYtPageDataFetched method exists.
if (!ytdApp || !ytdApp.onYtPageDataFetched) {
return errorFunc('unexpectedly could not find ytd-app.onYtPageDataFetched');
}
if (ytdApp[symSetup]) {
return;
}
console.debug('found yt-App', ytdApp, logContext);
const origOnYtPageDataFetched = ytdApp.onYtPageDataFetched;
ytdApp.onYtPageDataFetched = function(evt, detail) {
console.debug(evt);
updateResponseData(evt.detail.pageData.response, 'at yt-page-data-fetched pageData.response');
return origOnYtPageDataFetched.call(this, evt, detail);
};
console.debug('ytd-app onYtPageDataFetched hook set up');
ytdApp[symSetup] = true;
}
// By the time ytd-page-manager's attached event fires, ytd-app both exists
// and has its prototype initialized as needed in the above setup functions.
// (This also fires sooner than a MutationObserver would find such a ytd-app.)
// This is also the perfect hook for hooking into ytd-page-manager,
// which in turn allows hooking into ytd-browse's computeFluidWidth.
document.addEventListener('attached', evt => {
const ytdApp = document.getElementsByTagName('ytd-app')[0];
console.debug(evt);
setupYtdApp(ytdApp, 'at ytd-page-manager.attached');
});
// In case, ytd-app somehow already exists at this point.
const ytdApp = document.getElementsByTagName('ytd-app')[0];
setupYtdApp(ytdApp, 'at document-start', () => {});
// 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 => {
console.debug('ytInitialData', window.ytInitialData);
updateResponseData(window.ytInitialData, 'at DOMContentLoaded ytInitialData');
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment