YouTube Music: Like All Songs in a Playlist

Automates liking songs in the current open playlist via a sidebar button.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         YouTube Music: Like All Songs in a Playlist
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Automates liking songs in the current open playlist via a sidebar button.
// @author       KRYX0N
// @match        https://music.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=music.youtube.com
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        scrollDelay: 2000,
        clickDelay: 500,
        maxScrollAttempts: 3,
        anchorLabel: "Library",
        btnId: 'ytm-like-all-sidebar-btn',
        selectors: {
            sidebarSection: 'ytmusic-guide-section-renderer',
            sidebarItems: '#items',
            playlistShelf: 'ytmusic-playlist-shelf-renderer',
            albumShelf: 'ytmusic-section-list-renderer > #contents > ytmusic-shelf-renderer',
            likeButton: 'ytmusic-like-button-renderer button[aria-label="Like"][aria-pressed="false"]'
        }
    };

    let isRunning = false;
    let stopSignal = false;

    const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
    const log = (msg) => console.log(`[YTM-AutoLike] ${msg}`);

    function getSidebarItems() {
        const sections = document.querySelectorAll(CONFIG.selectors.sidebarSection);
        for (const section of sections) {
            const items = section.querySelector(CONFIG.selectors.sidebarItems);
            if (items && items.innerText.includes(CONFIG.anchorLabel)) return items;
        }
        return null;
    }

    function getActiveContainer() {
        // Priority: Specific playlist container > Album container > Body fallback
        return document.querySelector(CONFIG.selectors.playlistShelf) ||
               document.querySelector(CONFIG.selectors.albumShelf) ||
               document.body;
    }

    function updateUI(icon, text, status, active) {
        text.innerText = status;
        icon.innerText = status.includes('Liking') ? '⏳' : (status.includes('Done') ? '✅' : '👍');
        text.parentElement.style.opacity = active ? '0.7' : '1';
    }

    async function executeLikeProcess(icon, text) {
        isRunning = true;
        stopSignal = false;
        let totalLiked = 0;
        let noNewItems = 0;
        let lastHeight = 0;

        const container = getActiveContainer();
        log(`Target scope: ${container.tagName}`);

        while (!stopSignal) {
            const buttons = Array.from(container.querySelectorAll(CONFIG.selectors.likeButton));

            if (buttons.length > 0) {
                updateUI(icon, text, `Liking (${buttons.length})...`, true);
                for (const btn of buttons) {
                    if (stopSignal) break;
                    btn.click();
                    totalLiked++;
                    await wait(CONFIG.clickDelay);
                }
                noNewItems = 0;
            }

            if (stopSignal) break;

            updateUI(icon, text, 'Scrolling...', true);
            lastHeight = document.documentElement.scrollHeight;
            window.scrollTo(0, document.documentElement.scrollHeight);
            await wait(CONFIG.scrollDelay);

            if (document.documentElement.scrollHeight <= lastHeight) {
                noNewItems++;
            } else {
                noNewItems = 0;
            }

            if (noNewItems >= CONFIG.maxScrollAttempts) break;
        }

        isRunning = false;
        updateUI(icon, text, `Done (${totalLiked})`, false);
        setTimeout(() => updateUI(icon, text, 'Like All Songs', false), 4000);
        if (!stopSignal) alert(`Operation complete. Liked ${totalLiked} songs.`);
    }

    function injectButton() {
        if (document.getElementById(CONFIG.btnId)) return;

        const container = getSidebarItems();
        if (!container) return;

        const btn = document.createElement('div');
        btn.id = CONFIG.btnId;

        Object.assign(btn.style, {
            display: 'flex', alignItems: 'center', height: '48px', padding: '0 20px',
            cursor: 'pointer', color: '#ffffff', fontFamily: 'Roboto, sans-serif',
            fontSize: '14px', fontWeight: '500', borderRadius: '10px',
            transition: 'background-color 0.2s', marginBottom: '8px',
            marginLeft: '4px', marginRight: '4px'
        });

        btn.onmouseenter = () => btn.style.backgroundColor = 'rgba(255,255,255,0.1)';
        btn.onmouseleave = () => btn.style.backgroundColor = 'transparent';

        const icon = document.createElement('span');
        icon.innerText = '👍';
        Object.assign(icon.style, { marginRight: '24px', width: '24px', textAlign: 'center', fontSize: '20px' });

        const label = document.createElement('span');
        label.innerText = 'Like All Songs';
        Object.assign(label.style, { flex: '1', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' });

        btn.append(icon, label);

        btn.onclick = () => {
            if (isRunning) {
                stopSignal = true;
                updateUI(icon, label, 'Stopping...', true);
            } else if (confirm('Like all visible songs in this playlist?')) {
                executeLikeProcess(icon, label);
            }
        };

        const anchor = Array.from(container.children).find(c => c.innerText.includes(CONFIG.anchorLabel));
        anchor && anchor.nextSibling ? container.insertBefore(btn, anchor.nextSibling) : container.appendChild(btn);
    }

    // Observers & Initialization
    setInterval(() => {
        const btn = document.getElementById(CONFIG.btnId);
        if (!btn || !document.body.contains(btn)) injectButton();
    }, 2000);

    new MutationObserver(() => {
        if (!document.getElementById(CONFIG.btnId)) injectButton();
    }).observe(document.body, { childList: true, subtree: true });

})();