Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save CaptainJack0404/5580fd74b71e5edb0d6aff2a8dd84ad0 to your computer and use it in GitHub Desktop.
Save CaptainJack0404/5580fd74b71e5edb0d6aff2a8dd84ad0 to your computer and use it in GitHub Desktop.
Youtube script: When saving a video to a playlist, 1) add a button to sort the list alphabetically and 2) add a filter textbox to filter the list by title
// ==UserScript==
// @name Youtube Sort & Filter Playlists When Saving Video
// @version 2024.04.13.1
// @namespace https://gist.github.com/CaptainJack0404
// @description When saving a video to a playlist, 1) add a button to sort the list alphabetically and 2) add a filter textbox to filter the list by title
// @author CaptainJack0404
// @noframes
// @match https://www.youtube.com/*
// @match https://youtube.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant none
// ==/UserScript==
// When saving a video to a playlist:
// 1) add a button to sort the list alphabetically
// 2) add a filter textbox to filter the list by title
(function () {
'use strict';
const selectorApp = 'ytd-app';
const selectorPopupContainer = 'ytd-popup-container';
const selectorPopupDialog = `tp-yt-paper-dialog`;
// Handles filter textbox input (immediate filter on each keyup)
function keyupSearch(e = null) {
var arr = [];
var elPlayList = document.querySelector("#playlists.ytd-add-to-playlist-renderer");
elPlayList.style.display = "flex";
elPlayList.style.flexDirection = "column";
// Get each Playlist element
document.querySelectorAll("#playlists yt-formatted-string").forEach(function (item) {
arr.push(item.innerHTML);
});
// Filter the junk out of the playlist element array
let filtered = arr.filter(function (item) {
return (item != '<!--css-build:shady-->' && (e == null || item.toLowerCase().includes((e.target.value).toLowerCase())));
});
document.querySelectorAll("#playlists.ytd-add-to-playlist-renderer #label").forEach(function (el1, index) {
// Get main element wrapper for the current playlist and hide it
el1.closest("ytd-playlist-add-to-option-renderer.ytd-add-to-playlist-renderer").style.display = "none";
// Check to see if playlist is in the filtered array and if so then show it
filtered.forEach(function (item) {
if (el1.innerHTML == item) {
// Get main element wrapper for the current playlist and show it
el1.closest("ytd-playlist-add-to-option-renderer.ytd-add-to-playlist-renderer").style.display = "block";
}
});
});
document.querySelector("ytd-playlist-add-to-option-renderer.style-scope.ytd-add-to-playlist-renderer:last-child").style.marginBottom = '16px';
}
function addSortAndFilterToPopup() {
if(!document.querySelector("ytd-add-to-playlist-renderer")) return;
// Check to see if the popup dialog is on the page and is visible
const popupDialog = document.querySelector(selectorPopupDialog);
if (!popupDialog || getComputedStyle(popupDialog).display == 'none') return;
// Check to see if the Sort button and Filter textbox have already been added to the popup dialog
if (popupDialog.querySelector('#sort_save_to') && popupDialog.querySelector('.filter_save_to')) {
// Sort button and Filter textbox already exist on the page, so exit this function
// run filter function to reset the visibility of the playlist items
keyupSearch();
return;
}
var isPlaylistSorted = false;
function alterPopupTitleBar() {
const selectorPlayListMenuHeaderTitle = `ytd-add-to-playlist-renderer > #header > ytd-menu-title-renderer`;
const playListMenuHeaderTitle = document.querySelector(selectorPlayListMenuHeaderTitle);
const selectorPlayListFilterInput = selectorPlayListMenuHeaderTitle + ' input.filter_save_to';
const selectorPlayListSortButton = selectorPlayListMenuHeaderTitle + ' button#sort_save_to';
// Check to see if the popup dialog is on the page
if (!playListMenuHeaderTitle) return;
// We found the popup dialog, now add the sort button and filter textbox
playListMenuHeaderTitle.style.width = '300px';
// holds both the Sort button and Filter textbox
const containerDiv = document.createElement('div');
containerDiv.style.margin = '10px 0 0 10px';
// Sort Button
const sortButton = document.createElement('button');
sortButton.id = 'sort_save_to';
sortButton.style.cssText = 'background: #f8f8f8; border: 1px solid rgb(211,211,211); border-radius: 2px; color: #000; padding: 8px 16px; margin-right: 16px;';
sortButton.textContent = 'A-Z ↓';
// Filter Textbox
const filterInput = document.createElement('input');
filterInput.type = 'text';
filterInput.className = 'filter_save_to';
filterInput.style.cssText = 'background: #ffffff; color: #111111; padding: 8px 16px; border: 1px solid rgb(211,211,211); border-radius: 2px; width: 30%;';
containerDiv.appendChild(sortButton);
containerDiv.appendChild(filterInput);
playListMenuHeaderTitle.appendChild(containerDiv);
// Filter Textbox Input event handler
if (document.querySelector(selectorPlayListFilterInput)) {
document.querySelector(selectorPlayListFilterInput).addEventListener('keyup', function (e) {
keyupSearch(e);
})
}
// Sort Button event handler
if (document.querySelector(selectorPlayListSortButton)) {
document.querySelector(selectorPlayListSortButton).addEventListener('click', function (e) {
var elPlayList = document.querySelector("#playlists.ytd-add-to-playlist-renderer");
elPlayList.style.display = "flex";
elPlayList.style.flexDirection = "column";
if (!isPlaylistSorted) {
// sort the list
var arr = [];
document.querySelectorAll("#playlists yt-formatted-string").forEach(function (item) {
arr.push(item.innerHTML); // arr.push(item.textContent);
})
let filtered = arr.filter(function (item) {
return item != '<!--css-build:shady-->'
})
filtered.sort().forEach(function (sort1, sortedIndex) {
document.querySelectorAll("#playlists yt-formatted-string").forEach(function (elPlaylistName, index) {
if (sort1 == elPlaylistName.innerHTML) { //if (sort1 === elPlaylistName.textContent) {
var elPlaylistItem = elPlaylistName.closest("ytd-playlist-add-to-option-renderer");
var originalSortOrder = index;
elPlaylistItem.setAttribute('data-origOrder', originalSortOrder); // store the original sort order so we can undo the sort operation
elPlaylistItem.style.order = sortedIndex + 1; // new sort order
}
})
})
document.querySelector("ytd-playlist-add-to-option-renderer.style-scope.ytd-add-to-playlist-renderer:last-child").style.marginBottom = '16px';
isPlaylistSorted = true;
document.querySelector(selectorPlayListSortButton).style.background = '#88d988'; //bdbdbd
} else {
// unsort the list
document.querySelectorAll("#playlists yt-formatted-string").forEach(function (elPlaylistName) {
var elPlaylistItem = elPlaylistName.closest("ytd-playlist-add-to-option-renderer");
var originalSortOrder = elPlaylistItem.getAttribute('data-origOrder');
elPlaylistItem.style.order = originalSortOrder; // new sort order
})
document.querySelector("ytd-playlist-add-to-option-renderer.style-scope.ytd-add-to-playlist-renderer:last-child").style.marginBottom = '16px';
isPlaylistSorted = false;
document.querySelector(selectorPlayListSortButton).style.background = '#f8f8f8';
}
})
}
}
// We found the popup dialog, now add the sort button and filter textbox
alterPopupTitleBar();
}
/* This area for Observers that will trigger addSortAndFilterToPopup() when popup appears on page
Scenario 1: when <tp-yt-paper-dialog> is appended to <ytd-popup-container>
Scenario 2: when "display: none" style is removed from the element <tp-yt-paper-dialog>
We may need to use different observers to handle all scenarios.
Observer (observePopupDisplayed): when "display: none" style is removed from the element <tp-yt-paper-dialog>
Observer (observePopupCreated): when <tp-yt-paper-dialog> is appended to <ytd-popup-container>
*/
// Use IntersectionObserver to observe when popup dialogue is displayed
const observePopupDisplayed = new IntersectionObserver((entries, observePopupDisplayed) => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
// Visible
addSortAndFilterToPopup();
keyupSearch();
}
});
});
function watchPopupDialogueVisibility() {
// Begin observing playlist dialogue visibility changes so that we can re-add the Sort button and Filter textbox to the popup dialog if they are removed
//observePopupDisplayed.observe(document.querySelector(selectorPopupDialog), { attributes: true, attributeFilter: ['style', 'class'], });
observePopupDisplayed.observe(document.querySelector(selectorPopupDialog), { root: document.documentElement, threshold: 0.1 });
}
// Observer (observePopupCreated): when <tp-yt-paper-dialog> is appended to <ytd-popup-container> ; This only happens once (after initial page load of site).
const observePopupCreated = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.addedNodes.length) {
mutation.addedNodes.forEach(function (addedNode) {
// String comparison is case-sensitive, so we need to use localeCompare() to do case-insensitive string comparison
if (addedNode.nodeName.localeCompare(selectorPopupDialog, undefined, { sensitivity: 'base' }) === 0) {
// Popup dialogue has been added to the page, so now we can start observing when popup dialogue is displayed
// We no longer need this observer, so disconnect it
observePopupCreated.disconnect();
// Run the function to add the Sort button and Filter textbox to the popup dialog
addSortAndFilterToPopup();
// Start observing popup dialogue visibility changes so that we can re-add the Sort button and Filter textbox to the popup dialog if they are removed
watchPopupDialogueVisibility();
}
})
}
});
});
function waitForPopupDialogueCreation() {
// Check to see if the popup container is on the page
if (document.querySelector(selectorPopupDialog)) {
// Popup container already exists, move onto next step
watchPopupDialogueVisibility();
} else {
// Begin observing when popup dialogue is added to popup container
observePopupCreated.observe(document.querySelector(selectorPopupContainer), { childList: true, subtree: false });
}
}
function waitForPopupContainerLoad() {
if (document.querySelector(selectorPopupContainer)) {
// Popup container already exists, move onto next step
waitForPopupDialogueCreation();
} else {
// Begin observing when popup container is added to page
// Create interval to check for popup container
var intervalCheckForPopupContainer = setInterval(() => {
if (document.querySelector(selectorPopupContainer)) {
// We no longer need this interval, so clear it
clearInterval(intervalCheckForPopupContainer);
// Popup container exists on page, so now we can start observing when popup dialogue is added to popup container
waitForPopupDialogueCreation();
}
}, 250);
}
}
function waitForAppLoad() {
if (document.querySelector(selectorApp)) {
// App already exists, move onto next step
waitForPopupContainerLoad();
} else {
// Begin observing when app is added to page
// Create interval to check for app
var intervalCheckForApp = setInterval(() => {
if (document.querySelector(selectorApp)) {
// We no longer need this interval, so clear it
clearInterval(intervalCheckForApp);
waitForPopupContainerLoad();
}
}, 250);
}
}
waitForAppLoad();
})();
@bakereliz
Copy link

Very cool. But, how do I use/implement it?
I have done some coding in the distant past, but I don't know much about how YouTube actually works.
--elizabeth

@CaptainJack0404
Copy link
Author

Very cool. But, how do I use/implement it? I have done some coding in the distant past, but I don't know much about how YouTube actually works. --elizabeth

You need some sort of userscript manager extension for your browser. Then you will create a new userscript and then copy this whole script and paste it in for it and click save (make sure to enable the userscript in the manager afterwards). Then go to youtube (or refresh it if you already have it open). The userscript manager will automatically inject the script into the page to run.

As for how you see the script in action, go to any Youtube video and click save (to save the video to a playlist), and you will see that this script will add a textbox filter to the top of the playlist menu alongside a button to sort the list of playlists alphabetically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment