-
-
Save CaptainJack0404/5580fd74b71e5edb0d6aff2a8dd84ad0 to your computer and use it in GitHub Desktop.
// ==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(); | |
})(); |
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.
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