Instantly share code, notes, and snippets.
Last active
September 12, 2024 14:36
-
Star
(5)
5
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save aequabit/9f2dcd6af2e7d8958fcc1ffe8e87199a to your computer and use it in GitHub Desktop.
YouTube Sidebar Playlists
This file contains 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 Sidebar Playlists | |
// @description Shows playlists in the sidebar again | |
// @version 0.0.4 | |
// @author aequabit | |
// @match https://www.youtube.com/* | |
// @grant none | |
// @run-at document-start | |
// @downloadURL https://gist.github.com/aequabit/9f2dcd6af2e7d8958fcc1ffe8e87199a/raw/youtube-sidebar-playlists.user.js | |
// ==/UserScript== | |
(async () => { | |
const INITIAL_PLAYLIST_COUNT = 10; | |
const ytInitialData_getPlaylists = (ytInitialData) => { | |
const playlistRenderers = ytInitialData.contents?.twoColumnBrowseResultsRenderer?.tabs[0]?.tabRenderer?.content?.richGridRenderer?.contents; | |
if (!playlistRenderers) return null; | |
const playlists = []; | |
for (const playlistRenderer of playlistRenderers) { | |
const playlistId = playlistRenderer.richItemRenderer?.content?.lockupViewModel?.contentId; | |
if (!playlistId) return null; | |
const playlistTitle = playlistRenderer.richItemRenderer?.content?.lockupViewModel?.metadata?.lockupMetadataViewModel?.title?.content; | |
if (!playlistTitle) return null; | |
playlists.push({ id: playlistId, title: playlistTitle }); | |
} | |
return playlists; | |
}; | |
// https://stackoverflow.com/a/35385518 | |
const fromHTML = (html, trim = true) => { | |
// Process the HTML string. | |
html = trim ? html.trim() : html; | |
if (!html) return null; | |
// Then set up a new template element. | |
const template = document.createElement('template'); | |
template.innerHTML = html; | |
const result = template.content.children; | |
// Then return either an HTMLElement or HTMLCollection, | |
// based on whether the input HTML had one or more roots. | |
if (result.length === 1) return result[0]; | |
return result; | |
}; | |
const wait_for = (conditional, interval = 20) => { | |
return new Promise((resolve) => { | |
const _wait_for_interval = setInterval(() => { | |
if (conditional() === true) { | |
clearInterval(_wait_for_interval); | |
resolve(); | |
} | |
}, interval); | |
}); | |
}; | |
// | |
// Get playlists page | |
const playlistOverviewResponse = await fetch('/feed/playlists'); | |
const playlistOverviewHtml = await playlistOverviewResponse.text(); | |
// Parse DOM and get the script tag containing session data | |
const domParser = new DOMParser(); | |
const playlistOverviewDOM = domParser.parseFromString(playlistOverviewHtml, 'text/html'); | |
let dataScriptTag; | |
for (const scriptTag of playlistOverviewDOM.querySelectorAll('script[nonce]')) { | |
if (scriptTag.textContent === undefined) continue; | |
if (!scriptTag.textContent.startsWith('var ytInitialData =')) continue; | |
dataScriptTag = scriptTag; | |
break; | |
} | |
// Put the data into a variable (dirty but should be safe) | |
eval(dataScriptTag.textContent.replace('var ytInitialData =', 'var ytPlaylistFeedInitialData =')); | |
// Wait for the sidebar to appear | |
await wait_for(() => document.querySelector('a#endpoint[title=Playlists]') !== null); | |
const playlistsSidebarEndpoint = document.querySelector('a#endpoint[title=Playlists]'); | |
const playlistsSidebarEndpointNext = playlistsSidebarEndpoint?.nextSibling; | |
// Get the playlist data and put it into a usable format | |
const playlists = ytInitialData_getPlaylists(ytPlaylistFeedInitialData); | |
if (!playlists) return console.error("YouTube Sidebar Playlists: Failed to get playlists"); | |
const ytdApp = document.querySelector("ytd-app"); | |
if (!ytdApp) return console.error("YouTube Sidebar Playlists: Failed to get ytd-app component"); | |
window.navigateToPlaylist = playlistId => ytdApp.handleNavigate({ | |
type: 0, | |
command: { | |
browseEndpoint: { | |
browseId: "VL" + playlistId | |
}, | |
clickTrackingParams: "", | |
commandMetadata: { | |
webCommandMetadata: { | |
apiUrl: "/youtubei/v1/browse", | |
rootVe: 5754, | |
url: "/playlist?list=" + playlistId, | |
webPageType: "WEB_PAGE_TYPE_PLAYLIST" | |
} | |
} | |
}, | |
form: { | |
params: undefined, | |
tempData: {}, | |
requestType: undefined, | |
createScreenConfig: undefined, | |
reload: false | |
} | |
}); | |
// Map the playlist data to sidebar link elements | |
const sidebarPlaylistElements = playlists.map(playlist => fromHTML(` | |
<a id="endpoint" class="yt-simple-endpoint style-scope ytd-guide-entry-renderer" tabindex="-1" role$="" title="${playlist.title}" href="/playlist?list=${playlist.id}" onclick="navigateToPlaylist('${playlist.id}'); return false;" style="margin-left: 6px;"> | |
<tp-yt-paper-item role="link" class="style-scope ytd-guide-entry-renderer" style-target="host" tabindex="0" aria-disabled="false"><!--css-build:shady--> | |
<span class="guide-icon style-scope ytd-guide-entry-renderer"><!--css-build:shady--><!--css-build:shady--><span class="style-scope yt-icon"><icon-shape class="yt-spec-icon-shape"><div style="width: 100%; height: 100%; display: block; fill: currentcolor; --darkreader-inline-fill: currentcolor;" data-darkreader-inline-fill=""><svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false" style="pointer-events: none; display: inherit; width: 100%; height: 100%;"><path d="M22 7H2v1h20V7zm-9 5H2v-1h11v1zm0 4H2v-1h11v1zm2 3v-8l7 4-7 4z"></path></svg></div></icon-shape></span></span> | |
<yt-img-shadow height="24" width="24" class="style-scope ytd-guide-entry-renderer" disable-upgrade="" hidden=""></yt-img-shadow> | |
<span class="title style-scope ytd-guide-entry-renderer">${playlist.title}</span> | |
<span class="arrow-icon style-scope ytd-guide-entry-renderer" icon="chevron_right" size="16" disable-upgrade="" hidden=""></span> | |
<span class="guide-entry-count style-scope ytd-guide-entry-renderer"></span> | |
<span class="guide-entry-badge style-scope ytd-guide-entry-renderer" size="16" disable-upgrade=""></span> | |
<div id="newness-dot" class="style-scope ytd-guide-entry-renderer"></div> | |
</tp-yt-paper-item> | |
</a> | |
`)); | |
// Split the links into recents and the ones that appear in the "Show more" view | |
const recentPlaylists = sidebarPlaylistElements.slice(0, INITIAL_PLAYLIST_COUNT); | |
const morePlaylists = sidebarPlaylistElements.slice(INITIAL_PLAYLIST_COUNT); | |
const morePlaylistsContainer = fromHTML(`<div id="ytsp-more-playlists" style="display:none"></div>`); | |
for (const playlistElement of morePlaylists) { | |
morePlaylistsContainer.append(playlistElement); | |
} | |
// Create "Show less" button | |
const showLessButton = fromHTML(` | |
<a id="endpoint" class="ytsp-show-less yt-simple-endpoint style-scope ytd-guide-entry-renderer" tabindex="-1" role$="" title="Show less" onclick="document.querySelector('#ytsp-more-playlists').style.display='none';document.querySelector('.ytsp-show-more').style.display='block';return false;" href="#" style="margin-left: 6px;"> | |
<tp-yt-paper-item role="link" class="style-scope ytd-guide-entry-renderer" style-target="host" tabindex="0" aria-disabled="false"><!--css-build:shady--> | |
<span class="guide-icon style-scope ytd-guide-entry-renderer"><!--css-build:shady--><!--css-build:shady--><span class="style-scope yt-icon"><icon-shape class="yt-spec-icon-shape"><div style="width: 100%; height: 100%; display: block; fill: currentcolor; --darkreader-inline-fill: currentcolor;" data-darkreader-inline-fill=""><svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false" style="pointer-events: none; display: inherit; width: 100%; height: 100%;"><path d="M18.4 14.6 12 8.3l-6.4 6.3.8.8L12 9.7l5.6 5.7z"></path></svg></div></icon-shape></span></span> | |
<yt-img-shadow height="24" width="24" class="style-scope ytd-guide-entry-renderer" disable-upgrade="" hidden=""></yt-img-shadow> | |
<span class="title style-scope ytd-guide-entry-renderer">Show less</span> | |
<span class="arrow-icon style-scope ytd-guide-entry-renderer" icon="chevron_right" size="16" disable-upgrade="" hidden=""></span> | |
<span class="guide-entry-count style-scope ytd-guide-entry-renderer"></span> | |
<span class="guide-entry-badge style-scope ytd-guide-entry-renderer" size="16" disable-upgrade=""></span> | |
<div id="newness-dot" class="style-scope ytd-guide-entry-renderer"></div> | |
</tp-yt-paper-item> | |
</a> | |
`); | |
morePlaylistsContainer.append(showLessButton); | |
// Create recent playlists | |
for (const playlistElement of recentPlaylists) { | |
playlistsSidebarEndpoint.parentElement.insertBefore(playlistElement, playlistsSidebarEndpointNext); | |
} | |
// Create more playlists | |
playlistsSidebarEndpoint.parentElement.insertBefore(morePlaylistsContainer, playlistsSidebarEndpointNext); | |
// Create "Show more" button | |
const showMoreButton = fromHTML(` | |
<a id="endpoint" class="ytsp-show-more yt-simple-endpoint style-scope ytd-guide-entry-renderer" tabindex="-1" role$="" title="Show more" onclick="document.querySelector('#ytsp-more-playlists').style.display='unset';document.querySelector('.ytsp-show-more').style.display='none';return false;" href="#" style="margin-left: 6px;"> | |
<tp-yt-paper-item role="link" class="style-scope ytd-guide-entry-renderer" style-target="host" tabindex="0" aria-disabled="false"><!--css-build:shady--> | |
<span class="guide-icon style-scope ytd-guide-entry-renderer"><!--css-build:shady--><!--css-build:shady--><span class="style-scope yt-icon"><icon-shape class="yt-spec-icon-shape"><div style="width: 100%; height: 100%; display: block; fill: currentcolor; --darkreader-inline-fill: currentcolor;" data-darkreader-inline-fill=""><svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false" style="pointer-events: none; display: inherit; width: 100%; height: 100%;"><path d="m18 9.28-6.35 6.35-6.37-6.35.72-.71 5.64 5.65 5.65-5.65z"></path></svg></div></icon-shape></span></span> | |
<yt-img-shadow height="24" width="24" class="style-scope ytd-guide-entry-renderer" disable-upgrade="" hidden=""></yt-img-shadow> | |
<span class="title style-scope ytd-guide-entry-renderer">Show more</span> | |
<span class="arrow-icon style-scope ytd-guide-entry-renderer" icon="chevron_right" size="16" disable-upgrade="" hidden=""></span> | |
<span class="guide-entry-count style-scope ytd-guide-entry-renderer"></span> | |
<span class="guide-entry-badge style-scope ytd-guide-entry-renderer" size="16" disable-upgrade=""></span> | |
<div id="newness-dot" class="style-scope ytd-guide-entry-renderer"></div> | |
</tp-yt-paper-item> | |
</a> | |
`); | |
showMoreButton.style.display = playlists.length >= INITIAL_PLAYLIST_COUNT ? "block" : "none"; | |
playlistsSidebarEndpoint.parentElement.insertBefore(showMoreButton, playlistsSidebarEndpointNext); | |
})(); |
Author
aequabit
commented
May 23, 2024
Stopped working for me recently, was getting error "This document requires 'TrustedHTML' assignment".
Fixed by adding
window.trustedTypes.createPolicy('default', {
createHTML: string => string,
createScriptURL: string => string,
createScript: string => string,
});
at the beginning of the script
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment