[v4.6] YouTube Music - Skip Liked/Disliked & Pause (with Libraries)

Автоматически пропускает лайкнутые/дизлайкнутые песни. Клавиша 'D' для дизлайка. Стабильная и отлаженная версия.

当前为 2025-11-03 提交的版本,查看 最新版本

// ==UserScript==
// @name         [v4.6] YouTube Music - Skip Liked/Disliked & Pause (with Libraries)
// @namespace    http://tampermonkey.net/
// @version      4.6
// @description  Автоматически пропускает лайкнутые/дизлайкнутые песни. Клавиша 'D' для дизлайка. Стабильная и отлаженная версия.
// @author       torch
// @match        https://music.youtube.com/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dayjs.min.js
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Helper для логгирования с временной меткой
    const logWithTimestamp = (message, isWarning = false) => {
        const timestamp = dayjs().format('HH:mm:ss');
        const logFunction = isWarning ? console.warn : console.log;
        logFunction(`[${timestamp}] ${message}`);
    };

    logWithTimestamp("[YT Music Skipper] Version 4.6 (with Lodash & Day.js) loaded.");

    let isScriptPaused = false;
    let currentSongTitle = "";
    let isSkipping = false; // Флаг "иммунитета" после пропуска

    // =================================================
    // ЧАСТЬ 0: УПРАВЛЕНИЕ СОСТОЯНИЕМ СКРИПТА
    // =================================================

    function createPauseButton() {
        try {
            const controlsContainer = document.createElement('div');
            controlsContainer.id = 'userscript-controls';
            const statusSpan = document.createElement('span');
            statusSpan.id = 'script-status';
            statusSpan.textContent = 'Статус: Активен';
            const toggleButton = document.createElement('button');
            toggleButton.id = 'toggle-script-btn';
            toggleButton.textContent = 'Пауза';

            controlsContainer.append(statusSpan, toggleButton);
            document.body.appendChild(controlsContainer);

            toggleButton.addEventListener('click', () => {
                isScriptPaused = !isScriptPaused;
                statusSpan.textContent = `Статус: ${isScriptPaused ? 'Пауза' : 'Активен'}`;
                toggleButton.textContent = isScriptPaused ? 'Возобновить' : 'Пауза';

                logWithTimestamp(`Скрипт ${isScriptPaused ? 'приостановлен' : 'возобновлен'}.`);
                if (!isScriptPaused) checkCurrentTrack();
            });
        } catch (e) {
            logWithTimestamp(`Не удалось создать кнопку управления: ${e}`, true);
        }
    }

    GM_addStyle(`
        #userscript-controls {
            position: fixed; bottom: 15px; right: 15px; z-index: 10001;
            background-color: rgba(28, 28, 28, 0.95); border: 1px solid #555;
            border-radius: 8px; padding: 10px; display: flex; align-items: center;
            font-family: 'Roboto', 'Arial', sans-serif; color: white;
            box-shadow: 0 4px 12px rgba(0,0,0,0.6);
        }
        #script-status { margin-right: 12px; font-size: 14px; }
        #toggle-script-btn {
            background-color: #f44336; color: white; border: none; padding: 8px 12px;
            font-size: 14px; border-radius: 5px; cursor: pointer; transition: background-color 0.3s;
        }
        #toggle-script-btn:hover { background-color: #d32f2f; }
    `);

    // =================================================
    // ЧАСТЬ 1: АВТОМАТИЧЕСКИЙ ПРОПУСК ПЕСЕН
    // =================================================

    function activateImmunityAndSkip(songTitle, status) {
        const skipButton = document.querySelector('ytmusic-player-bar .next-button');
        if (skipButton && skipButton.offsetParent !== null) {
            logWithTimestamp(`Пропускаем трек: "${songTitle}". Статус: ${status}.`);
            isSkipping = true; // Активируем "иммунитет"
            skipButton.click();

            // Используем _.delay вместо setTimeout для консистентности
            _.delay(() => {
                logWithTimestamp("Период 'иммунитета' завершен.");
                isSkipping = false;
                checkCurrentTrack.cancel();
                checkCurrentTrack();
            }, 1500);
        }
    }

    function skipTrackIfNecessary(songTitle) {
        if (isScriptPaused || isSkipping) return;

        let attempts = 0;
        const maxAttempts = 15;
        const checkInterval = setInterval(() => {
            if (isScriptPaused || isSkipping || attempts++ >= maxAttempts) {
                clearInterval(checkInterval);
                return;
            }

            const likeStatus = document.querySelector('ytmusic-player-bar ytmusic-like-button-renderer')?.getAttribute('like-status');
            if (!likeStatus) return;

            if (likeStatus === 'LIKE' || likeStatus === 'DISLIKE') {
                clearInterval(checkInterval);
                activateImmunityAndSkip(songTitle, likeStatus);
            } else if (likeStatus === 'INDIFFERENT') {
                clearInterval(checkInterval);
            }
        }, 100);
    }

    const checkCurrentTrack = _.debounce(() => {
        if (isScriptPaused || isSkipping) return;
        const titleElement = document.querySelector('ytmusic-player-bar .title.style-scope.ytmusic-player-bar');
        const newTitle = titleElement?.getAttribute('title');

        if (newTitle && newTitle !== currentSongTitle) {
            currentSongTitle = newTitle;
            logWithTimestamp(`Новый трек: "${newTitle}". Начинаем проверку статуса...`);
            skipTrackIfNecessary(newTitle);
        }
    }, 250, { leading: false, trailing: true });

    // =================================================
    // ЧАСТЬ 2: ДИЗЛАЙК ПО КЛАВИШЕ "D"
    // =================================================

    document.addEventListener('keydown', (e) => {
        if (isScriptPaused || isSkipping) return;
        const activeElement = document.activeElement;
        const isInput = activeElement.tagName === 'INPUT' || activeElement.closest('ytmusic-search-box') || activeElement.isContentEditable;

        if (e.key.toUpperCase() === 'D' && !isInput) {
            e.preventDefault();

            const songTitleForLog = document.querySelector('ytmusic-player-bar .title')?.getAttribute('title') || "Неизвестный трек";
            const likeButtonRenderer = document.querySelector('ytmusic-player-bar ytmusic-like-button-renderer');
            const currentStatus = likeButtonRenderer?.getAttribute('like-status');

            if (currentStatus === 'DISLIKE') {
                logWithTimestamp(`Трек "${songTitleForLog}" уже дизлайкнут. Действие пропущено.`);
                return;
            }

            const dislikeButton = document.querySelector('ytmusic-player-bar #button-shape-dislike button');
            if (dislikeButton) {
                logWithTimestamp(`Дизлайк и пропуск трека: "${songTitleForLog}"`);
                isSkipping = true;
                dislikeButton.click();

                // Используем _.delay вместо setTimeout для консистентности
                _.delay(() => {
                    logWithTimestamp("Период 'иммунитета' (после дизлайка) завершен.");
                    isSkipping = false;
                    checkCurrentTrack.cancel();
                    checkCurrentTrack();
                }, 1500);

            } else {
                 logWithTimestamp(`Не удалось найти кнопку дизлайка.`, true);
            }
        }
    });

    // =================================================
    // ЗАПУСК СКРИПТА
    // =================================================

    function initialize() {
        createPauseButton();
        const targetNode = document.querySelector('ytmusic-player-bar .title.style-scope.ytmusic-player-bar');
        if (targetNode) {
            logWithTimestamp("Плеер обнаружен. Запускаем MutationObserver.");
            const observer = new MutationObserver(checkCurrentTrack);
            observer.observe(targetNode, { attributes: true, attributeFilter: ['title'] });
            checkCurrentTrack();
        } else {
            // Используем _.delay вместо setTimeout для консистентности
            _.delay(initialize, 500);
        }
    }

    if (document.readyState === 'complete') {
        initialize();
    } else {
        window.addEventListener('load', initialize);
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址