Last active
March 4, 2025 10:16
-
-
Save lbmaian/228de31c3203b82e4d6dccb17fced67a to your computer and use it in GitHub Desktop.
YouTube - Hide Breaking News section
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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