Skip to content

Instantly share code, notes, and snippets.

@aequabit
Last active September 12, 2024 14:36
Show Gist options
  • Save aequabit/9f2dcd6af2e7d8958fcc1ffe8e87199a to your computer and use it in GitHub Desktop.
Save aequabit/9f2dcd6af2e7d8958fcc1ffe8e87199a to your computer and use it in GitHub Desktop.
YouTube Sidebar Playlists
// ==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);
})();
@aloneunix
Copy link

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