Skip to content

Instantly share code, notes, and snippets.

@neonfuz
Last active April 5, 2022 00:11
Show Gist options
  • Save neonfuz/ea14fe2ad32c4caa860f36bb521b9a60 to your computer and use it in GitHub Desktop.
Save neonfuz/ea14fe2ad32c4caa860f36bb521b9a60 to your computer and use it in GitHub Desktop.
youtube quick playlist button userscript
// ==UserScript==
// @name youtube quick playlist button
// @namespace http://neonfuz.xyz/
// @author neonfuz
// @version 0.9
// @match https://www.youtube.com/*
// @icon https://www.google.com/s2/favicons?domain=youtube.com
// @updateURL https://gist.github.com/neonfuz/ea14fe2ad32c4caa860f36bb521b9a60/raw/youtube-quick-playlist.user.js
// @downloadURL https://gist.github.com/neonfuz/ea14fe2ad32c4caa860f36bb521b9a60/raw/youtube-quick-playlist.user.js
// ==/UserScript==
(function() {
'use strict';
const buttonProps = {
className: 'quick-playlist-button',
innerHTML: '+',
onclick: function () {
const popupContainer = document.querySelector('ytd-popup-container');
popupContainer.style.opacity = 0;
setTimeout(() => {
// Close existing dialog
const dialog = document.querySelector('tp-yt-paper-dialog');
if (dialog !== null && dialog.style.display !== 'none') {
dialog.querySelector('#close-button').click();
}
// Return opacity on next event cycle
setTimeout(() => {popupContainer.style.opacity = 1});
// Click save to playlist button
Array.prototype.find.call(
document.querySelectorAll('ytd-menu-service-item-renderer'),
e => e.innerText === 'Save to playlist' // TODO multilingual
).click();
}, 0);
},
};
function elem(tag, props) { return Object.assign(document.createElement(tag), props); }
function debounce(fn, time) {
let timeout = null;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), time);
}
}
class LogUnique {
constructor(time) {
this.seen = new Map();
this.log = debounce(console.log, time);
}
get(item) { return this.seen.get(item); }
add(key, val) {
this.log(this.seen);
return this.seen.set(
key,
[ ...(this.seen.get(key) ?? []), val ]
);
}
}
const seenUnknownMenuClasses = new LogUnique(3000);
function appendButton(menu) {
if (menu.querySelector('.' + buttonProps.className)) return;
switch (menu.className.split(' ')[1]) {
// Useless:
case 'ytd-shelf-renderer':
case 'ytd-rich-shelf-renderer':
case 'ytd-playlist-sidebar-primary-info-renderer':
case 'ytd-comment-renderer':
return; //skip
// TODO fix these:
case 'ytd-video-renderer': // in youtube history
case 'ytd-video-primary-info-renderer': // under each playing video
case 'ytd-engagement-panel-title-header-renderer': // what is this?
case 'ytd-notification-renderer': // notification videos
return; //skip
case 'ytd-rich-grid-media':
case 'ytd-grid-video-renderer':
case 'ytd-playlist-video-renderer':
case 'ytd-compact-video-renderer':
case 'ytd-video-preview':
case 'ytd-playlist-panel-video-renderer':
menu = menu.querySelector('yt-icon-button');
Object.assign(menu.style, {display: 'flex', 'flex-direction': 'column', 'align-items': 'center'});
menu.appendChild(elem('button', buttonProps));
break;
default:
seenUnknownMenuClasses.add(menu.className, menu);
}
}
document.body.appendChild(elem('style', {innerHTML: `
.${buttonProps.className} {
color: var(--yt-spec-text-primary);
font-size: calc(var(--yt-icon-width)/2);
cursor: pointer;
background: none;
border: none;
}
`}));
document.body.querySelectorAll('YTD-MENU-RENDERER').forEach(appendButton);
new MutationObserver(function(mutationList, observer) {
for (const mutation of mutationList) {
if (mutation.target.tagName === 'YTD-MENU-RENDERER') {
appendButton(mutation.target);
}
}
}).observe(document.body, {childList: true, subtree: true});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment