YouTube All Videos Playlists (YAVP)

Adds buttons for various channel playlists: All Uploads, Videos, Shorts, Streams, Members-Only

스크립트 설치?
개발자의 추천 스크립트

YTBetter - Enable Rewind/DVR는 어떤가요?

스크립트 설치
질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name         YouTube All Videos Playlists (YAVP)
// @namespace    c0d3r
// @license      MIT
// @version      0.4
// @description  Adds buttons for various channel playlists: All Uploads, Videos, Shorts, Streams, Members-Only
// @author       c0d3r
// @match        https://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @run-at       document-idle
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

const options = {
    playNext: GM_getValue('playNext', true),
    newTabs: GM_getValue('newTabs', false)
};

const onClass = 'yt-spec-button-shape-next--outline';
const offClass = 'yt-spec-button-shape-next--disabled';

// custom buttons use native classes and tags so both Light and Dark theme is supported
function btnHtml(text, title, href, pos) {
    let html = '';
    let size = '';
    let tagStart = '';
    let tagEnd = '';
    let posClass = '';
    let stateClass = '';

    const target = options.newTabs ? 'target="_blank"' : '';

    if (pos) {
        posClass = `yt-spec-button-shape-next--segmented-${pos}`;
    }

    if (pos !== 'end') {
        html += '<div class="yt-flexible-actions-view-model-wiz__action" style="display: flex;">';
    }

    if (href.startsWith('http')) {
        size = 's';
        tagStart = `<a href="${href}"`;
        tagEnd = '</a>';
    } else {
        size = 'xs';
        tagStart = `<span id="${href}"`;
        tagEnd = '</span>';
        posClass = 'yt-spec-button-shape-next--workaround-icon-no-border-radius';
        stateClass = options[href] ? onClass : offClass;
    }

    html += `
    <button-view-model class="yt-spec-button-view-model" style="align-items: center;">
        ${tagStart} title="${title}" ${target}
        style="cursor: pointer;"
        class="yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono
        yt-spec-button-shape-next--size-${size} ${posClass} ${stateClass}">${text}${tagEnd}
    </button-view-model>`;

    if (pos !== 'start') {
        html += '</div>';
    }

    return html;
}

// generate URL based on playlist type and channel ID
function listUrl(listType, chanId) {
    const root = 'https://www.youtube.com/playlist?list=';
    let url = `${root}${listType}${chanId}`;

    // "&playnext=1" starts playing the first video right away; required for Popular Uploads
    // not added for Members-Only playlists since it won't work unless you're a paid member
    if ((options.playNext && listType !== 'UUMO') || listType === 'PU') {
        url += '&playnext=1';
    }

    return url;
}

function btnBlock(data, chanId) {
    let html = '<div id="yavp-wrap" style="display: flex; flex: 0.8;">';

    // only add relevant buttons based on available channel tabs
    // for IDs see https://github.com/RobertWesner/YouTube-Play-All/blob/main/documentation/available-lists.md
    // and also    https://wiki.archiveteam.org/index.php/YouTube/Technical_details
    html += btnHtml('All', 'All Uploads', listUrl('UU', chanId), 'start');
    html += btnHtml('Pop', 'Popular Uploads', listUrl('PU', chanId), 'end');

    data.response.contents.twoColumnBrowseResultsRenderer.tabs.forEach(function(tab) {
        if (tab.hasOwnProperty('tabRenderer')) {
            // check for URLs instead of tab titles because languages might be different
            const url = tab.tabRenderer.endpoint.commandMetadata.webCommandMetadata.url;
            if (url.endsWith('/videos')) {
                html += btnHtml('VA', 'All Videos', listUrl('UULF', chanId), 'start');
                html += btnHtml('VP', 'Popular Videos', listUrl('UULP', chanId), 'end');
            } else if (url.endsWith('/shorts')) {
                html += btnHtml('SA', 'All Shorts', listUrl('UUSH', chanId), 'start');
                html += btnHtml('SP', 'Popular Shorts', listUrl('UUPS', chanId), 'end');
            } else if (url.endsWith('/streams')) {
                html += btnHtml('LA', 'All Streams', listUrl('UULV', chanId), 'start');
                html += btnHtml('LP', 'Popular Streams', listUrl('UUPV', chanId), 'end');
            }
        }
    });

    // add Members-Only button only if Join button exists
    let addMemberBtn = false;
    const head = data.response.header;

    // when not logged in
    if (head.hasOwnProperty('c4TabbedHeaderRenderer') &&
        head.c4TabbedHeaderRenderer.hasOwnProperty('sponsorButton')) {
        addMemberBtn = true;
    }

    // when logged in
    if (head.hasOwnProperty('pageHeaderRenderer')) {
        const actionRows = head.pageHeaderRenderer.content.pageHeaderViewModel.actions.flexibleActionsViewModel.actionsRows;
        actionRows.forEach(function(row) {
            row.actions.forEach(function(action) {
                if (action.hasOwnProperty('buttonViewModel')) {
                    addMemberBtn = true;
                }
            });
        });
    }

    if (addMemberBtn) {
        html += btnHtml('Mem', 'Members-Only Videos', listUrl('UUMO', chanId));
    }

    html += btnHtml('AP', 'AutoPlay (PlayNext)', 'playNext', 'start');
    html += btnHtml('NT', 'Open in New Tabs', 'newTabs', 'end');

    html += '</div>';
    return html;
}

// button click action
function addClickEvents(selector) {
    document.querySelector('#' + selector).addEventListener('click', function() {
        if (this.classList.contains(onClass)) {
            this.classList.replace(onClass, offClass);
            options[selector] = false;
            GM_setValue(selector, false);
        } else {
            this.classList.replace(offClass, onClass);
            options[selector] = true;
            GM_setValue(selector, true);
        }
    });
}

(function() {
    'use strict';

    // WORKAROUND: TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.
    if (window.trustedTypes && trustedTypes.createPolicy) {
        if (!trustedTypes.defaultPolicy) {
            const passThroughFn = (x) => x;
            trustedTypes.createPolicy('default', {
                createHTML: passThroughFn,
                createScriptURL: passThroughFn,
                createScript: passThroughFn,
            });
        }
    }

    let oldChanId;

    // native event that fires on each page or tab change
    window.addEventListener('yt-navigate-finish', function() {
        const path = window.location.pathname;
        // run only on channel pages, see https://support.google.com/youtube/answer/6180214
        if (path.startsWith('/channel/') || path.startsWith('/@') ||
            path.startsWith('/c/') || path.startsWith('/user/')) {
            const pageMan = document.querySelector('#page-manager');
            // native function that returns useful data
            const pageData = pageMan.getCurrentData();
            // get channel ID, but remove UC from the start
            const chanId = pageData.response.metadata.channelMetadataRenderer.externalId.substring(2);

            // proceed only if a new channel is opened, don't react to tab changes
            if (oldChanId !== chanId) {
                // remove the wrapper if it exists
                const wrapper = document.querySelector('#yavp-wrap');
                if (wrapper) {
                    wrapper.remove();
                }

                const tabBlock = document.querySelector('#tabsContainer');
                if (tabBlock) {
                    // tabs container might already exist in DOM
                    tabBlock.insertAdjacentHTML('afterEnd', btnBlock(pageData, chanId));
                    addClickEvents('playNext');
                    addClickEvents('newTabs');
                } else {
                    // Chrome needs to wait for this block to be added at first
                    let stopObserve = false;
                    const observer = new MutationObserver(function(mutList) {
                        for (let i = 0; i < mutList.length; i++) {
                            for (let j = 0; j < mutList[i].addedNodes.length; j++) {
                                if (mutList[i].addedNodes[j].tagName === 'TP-YT-APP-HEADER-LAYOUT') {
                                    const tabs = mutList[i].addedNodes[j].querySelector('#tabsContainer');
                                    tabs.insertAdjacentHTML('afterEnd', btnBlock(pageData, chanId));
                                    addClickEvents('playNext');
                                    addClickEvents('newTabs');
                                    observer.disconnect();
                                    stopObserve = true;
                                    break;
                                }
                            }
                            if (stopObserve) {
                                break;
                            }
                        }
                    });

                    observer.observe(pageMan, {
                        subtree: true, childList: true
                    });
                }

                // save channel ID for future checks
                oldChanId = chanId;
            }
        }
    });
})();