Bilibili 列表随机播放

自动获取播放列表并随机播放视频

// ==UserScript==
// @name         Bilibili 列表随机播放
// @namespace    http://tampermonkey.net/
// @version      0.8
// @description  自动获取播放列表并随机播放视频
// @author       0808
// @match        https://www.bilibili.com/list/*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    "use strict";

    /** 配置选项 **/
    const CONFIG = {
        randomButtonClass: "randomBtn",
        activeClass: "startRandom",
        scrollDelay: 200,
        timeoutDelay: 5000,
        waitTime: 2000,
        excludeLastX: 0, // 忽略最后的 X 个视频
        maxScrollAttempts: 1, // 滚动到顶部或底部后再滚动 X 次
        buttonText: "随机",
        logPrefix: "[列表随机播放]", // 日志前缀
        colorEnable: 'rgba(0,174,236, 1)',
        colorDisable: 'rgba(0,174,236, 0.4)',
        colorHover: 'rgba(0,174,236, 0.7)',
        localStorageKey: "randomVideoEnabled",
        videoSelectorList: [
            "ul.list-box > li > a > div.clickitem",
            ".base-video-sections-v1 .video-section-list .video-episode-card",
            "#playlist-video-action-list .action-list-inner .action-list-item .title",
        ],
        scrollContainerSelector: "#playlist-video-action-list",
        targetElementSelector: 'div.list-playway-btn.list-tool-btn[title="列表循环"]',
    };

    /** 初始化脚本 **/
    const state = {
        isRandomEnabled: getLocalStorage() === 1,
        videoList: [],
        isScrolling: false, // 添加滚动标志位
    };

    /** 封装 console.log,自动添加前缀 **/
    function log(message) {
        console.log(`${CONFIG.logPrefix} ${message}`);
    }

    setTimeout(() => {
        init();
    }, CONFIG.timeoutDelay);

    /** 主初始化函数 **/
    function init() {
        getVideoList();
        createRandomButton();
        if (state.isRandomEnabled) startRandomPlayback();
    }

    /** 创建随机播放按钮 **/
    function createRandomButton() {
        // 找到目标元素
        const targetElement = document.querySelector(CONFIG.targetElementSelector);

        if (targetElement) {
            // 创建按钮
            const button = document.createElement("button");
            button.textContent = CONFIG.buttonText;
            button.className = `${CONFIG.randomButtonClass} ${state.isRandomEnabled ? CONFIG.activeClass : ""}`;

            // 设置按钮样式
            button.style.backgroundColor = state.isRandomEnabled ? CONFIG.colorEnable : CONFIG.colorDisable;
            button.style.transition = 'background-color 0.3s';
            button.style.color = '#ffffff';
            button.style.fontSize = '15px';
            button.style.cursor = 'pointer';
            button.style.borderRadius = '10px';
            button.style.border = '0px solid #ffffff';
            button.style.paddingLeft = '10px';
            button.style.paddingRight = '10px';
            button.style.marginBottom = '2px';
            button.style.marginLeft = '10px'; // 添加左边距,与目标元素保持一定距离

            // 添加悬停效果
            button.addEventListener("mouseover", function () {
                button.style.backgroundColor = CONFIG.colorHover;
            });
            button.addEventListener("mouseout", function () {
                button.style.backgroundColor = state.isRandomEnabled ? CONFIG.colorEnable : CONFIG.colorDisable;
            });

            // 添加点击事件
            button.addEventListener("click", toggleRandomPlayback);

            // 将按钮插入到目标元素后面
            targetElement.insertAdjacentElement('afterend', button);
        } else {
            log("未找到目标元素,按钮创建失败");
        }
    }

    /** 切换随机播放状态 **/
    function toggleRandomPlayback(event) {
        event.stopPropagation();
        const button = event.target;

        state.isRandomEnabled = !state.isRandomEnabled;

        if (state.isRandomEnabled) {
            log("已开启随机播放");
            button.classList.add(CONFIG.activeClass);
            button.style.backgroundColor = CONFIG.colorEnable;
            setLocalStorage(1);
            startRandomPlayback();
            // 如果正在滚动,则忽略点击事件
            if (!state.isScrolling) {
                getVideoList();
            }
        } else {
            log("已关闭随机播放");
            button.classList.remove(CONFIG.activeClass);
            button.style.backgroundColor = CONFIG.colorDisable;
            setLocalStorage(0);
        }
    }

    /** 开始随机播放功能 **/
    function startRandomPlayback() {
        const videoEl = document.querySelector("div.bpx-player-video-wrap > video");
        if (videoEl) {
            videoEl.addEventListener("ended", handleVideoEnd);
        }
    }

    /** 视频播放结束时的处理 **/
    function handleVideoEnd(event) {
        if (state.isRandomEnabled) {
            event.stopImmediatePropagation();
            playNextVideo();
        }
    }

    /** 随机播放下一个视频 **/
    function playNextVideo() {
        const availableVideos = state.videoList.slice(0, state.videoList.length - CONFIG.excludeLastX); // 排除最后X个视频

        if (availableVideos.length > 0) {
            const randomIndex = Math.floor(Math.random() * availableVideos.length);
            const nextVideo = availableVideos[randomIndex];
            if (nextVideo) {
                nextVideo.click();
                log(`当前正在随机播放第 ${randomIndex + 1} / ${state.videoList.length - CONFIG.excludeLastX} 个视频: [${nextVideo.title}]`);
            }
        } else {
            log("没有可用的视频进行随机播放");
        }
    }

    /** 获取视频列表 **/
    function getVideoList() {
        const container = document.querySelector(CONFIG.scrollContainerSelector);
        if (container) {
            simulateScroll(container, () => {
                for (const selector of CONFIG.videoSelectorList) {
                    const videos = Array.from(document.querySelectorAll(selector));
                    if (videos.length > 0) {
                        state.videoList = videos;
                        log(`视频列表更新成功,读取到 ${state.videoList.length - CONFIG.excludeLastX} 个视频`);
                        return;
                    }
                }
                log("获取视频列表失败,请刷新重试");
            });
        }
    }

    /** 模拟滚动 **/
    function simulateScroll(container, callback) {
        // 设置滚动标志位
        state.isScrolling = true;
        log("开始滚动");

        let scrollAttemptsTop = 0; // 向上滚动次数计数器
        let scrollAttemptsBottom = 0; // 向下滚动次数计数器

        function scrollToTop(container, onComplete) {
            const tolerance = 2; // 容许的误差
            const interval = setInterval(() => {
                const { scrollTop } = container;

                // 检查是否接近顶部
                if (scrollTop <= tolerance) {
                    log("已到达顶部");
                    clearInterval(interval);

                    // 如果未达到最大滚动次数,继续滚动
                    if (scrollAttemptsTop < CONFIG.maxScrollAttempts) {
                        scrollAttemptsTop++;
                        log(`滚动到顶部,正在进行第 ${scrollAttemptsTop} 次滚动`);
                        setTimeout(() => scrollToTop(container, onComplete), CONFIG.waitTime);
                    } else {
                        // 达到最大滚动次数,结束滚动
                        onComplete();
                    }
                } else {
                    // 动态调整滚动步长,避免跳动
                    container.scrollTop -= container.clientHeight / 4 * 3;
                }
            }, CONFIG.scrollDelay);
        }

        function scrollToBottom(container, onComplete) {
            const tolerance = 2; // 容许的误差
            const interval = setInterval(() => {
                const { scrollTop, scrollHeight, clientHeight } = container;
                const distanceToBottom = scrollHeight - (scrollTop + clientHeight);

                // 检查是否接近底部
                if (distanceToBottom <= tolerance) {
                    log("已到达底部");
                    clearInterval(interval);

                    // 如果未达到最大滚动次数,继续滚动
                    if (scrollAttemptsBottom < CONFIG.maxScrollAttempts) {
                        scrollAttemptsBottom++;
                        log(`滚动到底部,正在进行第 ${scrollAttemptsBottom} 次滚动`);
                        setTimeout(() => scrollToBottom(container, onComplete), CONFIG.waitTime);
                    } else {
                        // 达到最大滚动次数,结束滚动
                        onComplete();
                    }
                } else {
                    // 动态调整滚动步长,避免跳动
                    container.scrollTop += clientHeight / 4 * 3;
                }
            }, CONFIG.scrollDelay);
        }

        // 先滚动到顶部
        scrollToTop(container, () => {
            // 滚动到顶部完成后,再滚动到底部
            setTimeout(() => {
                scrollToBottom(container, () => {
                    // 滚动结束后清除标志位
                    state.isScrolling = false;
                    log("滚动结束");
                    callback();
                });
            }, CONFIG.waitTime);
        });
    }

    /** 本地存储操作 **/
    function setLocalStorage(value) {
        localStorage.setItem(CONFIG.localStorageKey, value);
    }

    function getLocalStorage() {
        return parseInt(localStorage.getItem(CONFIG.localStorageKey), 10) || 0;
    }
})();