🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

YouTube Bulk Remove Videos from Playlists (including Watch Later & Liked Videos)

Clear videos from Watch Later or Liked Videos playlists (note: may require multiple runs)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name:en      YouTube Bulk Remove Videos from Playlists (including Watch Later & Liked Videos)
// @name         YouTube 批量移除播放列表内视频(包括稍后再看、点赞过的视频)
// @namespace   http://tampermonkey.net/
// @version      1.0.0
// @description:en  Clear videos from Watch Later or Liked Videos playlists (note: may require multiple runs)
// @description  清除稍后再看、点赞过的视频列表的视频(注:可能得重复开启尝试)
// @match        https://www.youtube.com/playlist?*
// @match        https://www.youtube.com/watch?*&list*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @license      MIT
// @author kaesinol
// ==/UserScript==

(function () {
    'use strict';

    // ---------- i18n ----------
    const lang = (() => {
        try {
            const l = (navigator.language || navigator.userLanguage || 'en').toLowerCase();
            return l.startsWith('zh') ? 'zh' : 'en';
        } catch (e) {
            return 'en';
        }
    })();

    const I18N = {
        en: {
            defaultText: 'Remove from playlist',
            menuRun: 'Run: Remove from playlist (manual)',
            menuSetText: 'Set: match text',
            menuSetDelay1: 'Set: delay before find (ms)',
            menuSetDelay2: 'Set: delay after click (ms)',
            promptSetText: 'Enter the menu text to match (partial text allowed):',
            promptDelay1: 'Milliseconds to wait after clicking menu before searching for listbox:',
            promptDelay2: 'Milliseconds to wait after clicking the remove item:',
            alertSaved: 'Saved.',
            alertNoItems: 'No menu buttons found. Page may not be loaded or structure changed.',
            alertCompleted: 'Operation completed.',
            alertCancelled: 'Cancelled.',
        },
        zh: {
            defaultText: '从播放列表中移除',
            menuRun: 'Run:从播放列表中移除(手动)',
            menuSetText: '设置:匹配文字',
            menuSetDelay1: '设置:点击后查找延迟(毫秒)',
            menuSetDelay2: '设置:点击后等待(毫秒)',
            promptSetText: '请输入要匹配的菜单文字(可部分匹配):',
            promptDelay1: '点击菜单后,等待多少毫秒再查找 listbox(建议 600-1500):',
            promptDelay2: '点击“移除”后等待多少毫秒(用于让请求/动画完成):',
            alertSaved: '已保存。',
            alertNoItems: '未找到菜单按钮。页面可能未加载或结构已变更。',
            alertCompleted: '操作完成。',
            alertCancelled: '已取消。',
        }
    };

    const t = I18N[lang];

    // ---------- keys & defaults ----------
    const KEY_TEXT = 'yt_remove_menu_text';
    const KEY_DELAY1 = 'yt_delay_before_find';
    const KEY_DELAY2 = 'yt_delay_after_click';
    const DEFAULT_TEXT = t.defaultText;
    const DEFAULT_DELAY1 = 600;
    const DEFAULT_DELAY2 = 800;

    // ---------- utilities ----------
    const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

    const getSetting = (key, def) => {
        try {
            const v = GM_getValue(key);
            return v === undefined ? def : v;
        } catch (e) {
            return def;
        }
    };

    const setSetting = (key, val) => {
        try { GM_setValue(key, val); } catch (e) { /* noop */ }
    };

    // ---------- menu registration helpers ----------
    let registeredIds = [];

    function unregisterAll() {
        if (!registeredIds || !registeredIds.length) return;
        try {
            if (typeof GM_unregisterMenuCommand === 'function') {
                for (const id of registeredIds) {
                    try { GM_unregisterMenuCommand(id); } catch (e) { /* ignore individual errors */ }
                }
            }
        } catch (e) {
            // ignore
        } finally {
            registeredIds = [];
        }
    }

    function registerMenus(processFn) {
        unregisterAll();
        try {
            const curText = getSetting(KEY_TEXT, DEFAULT_TEXT);
            const idRun = GM_registerMenuCommand(t.menuRun, processFn);
            const idSetText = GM_registerMenuCommand(`${t.menuSetText} (current: "${curText}")`, () => {
                const cur = getSetting(KEY_TEXT, DEFAULT_TEXT);
                const v = prompt(t.promptSetText, cur);
                if (v === null) { alert(t.alertCancelled); return; }
                setSetting(KEY_TEXT, v.trim());
                alert(t.alertSaved);
                // re-register menus so labels refresh
                registerMenus(processFn);
            });
            const idDelay1 = GM_registerMenuCommand(`${t.menuSetDelay1} (current: ${getSetting(KEY_DELAY1, DEFAULT_DELAY1)})`, () => {
                const cur = String(getSetting(KEY_DELAY1, DEFAULT_DELAY1));
                const v = prompt(t.promptDelay1, cur);
                if (v === null) { alert(t.alertCancelled); return; }
                const n = Math.max(0, parseInt(v) || 0);
                setSetting(KEY_DELAY1, n);
                alert(t.alertSaved);
                registerMenus(processFn);
            });
            const idDelay2 = GM_registerMenuCommand(`${t.menuSetDelay2} (current: ${getSetting(KEY_DELAY2, DEFAULT_DELAY2)})`, () => {
                const cur = String(getSetting(KEY_DELAY2, DEFAULT_DELAY2));
                const v = prompt(t.promptDelay2, cur);
                if (v === null) { alert(t.alertCancelled); return; }
                const n = Math.max(0, parseInt(v) || 0);
                setSetting(KEY_DELAY2, n);
                alert(t.alertSaved);
                registerMenus(processFn);
            });

            // store IDs if provided by manager
            registeredIds = [idRun, idSetText, idDelay1, idDelay2].filter(Boolean);
        } catch (e) {
            // GM_registerMenuCommand may be unavailable; fallback will be handled by caller
            registeredIds = [];
        }
    }

    // ---------- core worker ----------
    async function processAll() {
        const menuText = getSetting(KEY_TEXT, DEFAULT_TEXT);
        const delay1 = Number(getSetting(KEY_DELAY1, DEFAULT_DELAY1)) || DEFAULT_DELAY1;
        const delay2 = Number(getSetting(KEY_DELAY2, DEFAULT_DELAY2)) || DEFAULT_DELAY2;

        // strict selector (no fallback)
        const items = Array.from(document.querySelectorAll('#items ytd-menu-renderer button'));

        if (!items.length) {
            try { alert(t.alertNoItems); } catch (e) { }
            return;
        }

        for (const el of items) {
            try {
                // scroll element into view and focus it before clicking
                try {
                    el.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' });
                    el.focus && el.focus();
                } catch (e) {
                    // ignore scroll errors
                }
                // small extra wait to ensure visibility/render
                await sleep(150);

                // click the menu button (el is expected to be a <button>)
                el.click();
                await sleep(delay1);

                const boxes = Array.from(document.querySelectorAll('tp-yt-paper-listbox'));
                const box = boxes.find(b => b.innerText && b.innerText.includes(menuText));
                if (!box) {
                    continue;
                }

                const target = Array.from(box.querySelectorAll('*')).find(n => n.innerText && n.innerText.includes(menuText));
                if (!target) continue;

                // ensure the target is visible as well
                try { target.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' }); } catch (e) { }
                target.click();
                await sleep(delay2);
            } catch (e) {
                // swallow errors silently
            }
        }

        try { alert(t.alertCompleted); } catch (e) { }
    }

    // ---------- init: register menus or expose fallback ----------
    try {
        registerMenus(processAll);
    } catch (e) {
        // in case menu registration fails, expose run for manual use from console
        try { window.YTRemoveAssistant = { run: processAll }; } catch (err) { /* noop */ }
    }

    // do not auto-run
})();