RobertWesner / YouTube Play All

// ==UserScript==
// @name            YouTube Play All
// @description     Adds the Play-All-Button to the videos and shorts sections of a YouTube-Channel
// @version         2024-05-20
// @author          Robert Wesner (https://robert.wesner.io)
// @license         MIT
// @namespace       http://robert.wesner.io/
// @match           https://*.youtube.com/*
// @icon            https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant           none
// ==/UserScript==

(function() {
    'use strict';

    document.head.insertAdjacentHTML('beforeend', `<style>
        .play-all-button {
            border-radius: 8px;
            background-color: #bf4bcc;
            color: white;
            font-family: 'Roboto','Arial',sans-serif;
	        font-size: 1.4rem;
	        line-height: 2rem;
	        font-weight: 500;
            padding: 0.5em;
            margin-left: 0.6em;
            text-decoration: none;
        }

        .play-all-button:hover {
            background-color: #d264de;
        }
        
        /* fetch() API introduces a race condition. This hides the occasional duplicate buttons */
        .play-all-button ~ .play-all-button {
            display: none;
        }
        
        /* Fix for mobile view */
        ytm-feed-filter-chip-bar-renderer .play-all-button {
            margin-left: 0;
            padding: 0.4em;
        }
    </style>`);

    let id;
    const apply = () => {
        let [parent, latestVideo] =
            location.host ===
            'm.youtube.com' ? [
                // mobile view
                document.querySelector('ytm-feed-filter-chip-bar-renderer > div'),
                document.querySelector('ytm-compact-video-renderer a, .reel-item-endpoint'),
            ] : [
                // desktop view
                document.querySelector('ytd-feed-filter-chip-bar-renderer iron-selector#chips'),
                document.querySelector('div#primary a#thumbnail'),
            ];

        if (!latestVideo) {
            // content was not loaded yet, no latest video found -> retry next cycle
            return;
        }

        // See: available-lists.md
        let allPlaylist, popularPlaylist;
        if (window.location.pathname.endsWith('/videos')) {
            // Normal videos
            allPlaylist = 'UULF';
            popularPlaylist = 'UULP';
            latestVideo = latestVideo.attributes.href.value;
        } else {
            // Shorts
            allPlaylist = 'UUSH';
            popularPlaylist = 'UUPS';
            // get just the short ID and play it in regular video mode
            latestVideo = latestVideo.attributes.href.value.split('/');
            latestVideo = `/watch?v=${latestVideo[latestVideo.length - 1]}`;
        }

        parent.insertAdjacentHTML(
            'beforeend',
            // Check if popular videos are displayed
            parent.querySelector(':nth-child(2).selected, :nth-child(2).iron-selected')
                // list=UULP has the all videos sorted by popular
                ? `<a class="play-all-button" href="${latestVideo}&list=${popularPlaylist}${id}">Play Popular</a>`
                // list=UU<ID> adds shorts into the playlist, list=UULF<ID> has videos without shorts
                : `<a class="play-all-button" href="${latestVideo}&list=${allPlaylist}${id}">Play All</a>`,
        );
    };

    window.addEventListener('yt-navigate-finish',async () => {
        const observer = new MutationObserver(apply);
        observer.disconnect();

        if (!(window.location.pathname.endsWith('/videos') || window.location.pathname.endsWith('/shorts'))) {
            return;
        }

        if (document.querySelector('.play-all-button')) {
            return;
        }

        const html = await (await fetch('.')).text();
        const i = html.indexOf('<link rel="canonical" href="https://www.youtube.com/channel/UC') + 60 + 2 /* ID starts with "UC" */;
        id = html.substring(i, i + 22);

        // Initially generate button
        apply();

        // Regenerate button if switched between Latest and Popular
        observer.observe(document.querySelector('ytd-rich-grid-renderer'), {
            attributes: true,
            childList: false,
            subtree: false
        });
    });
})();