哔哩哔哩(B站|Bilibili)收藏夹Fix (检测隐藏视频)

检测收藏夹中被UP主设置为仅自己可见的视频

// ==UserScript==
// @name              bilibili favlist hidden video detection
// @name:zh-CN        哔哩哔哩(B站|Bilibili)收藏夹Fix (检测隐藏视频)
// @name:zh-TW        哔哩哔哩(B站|Bilibili)收藏夹Fix (检测隐藏视频)
// @namespace         http://tampermonkey.net/
// @version           14
// @description       detect videos in favlist that only visiable to upper
// @description:zh-CN 检测收藏夹中被UP主设置为仅自己可见的视频
// @description:zh-TW 检测收藏夹中被UP主设置为仅自己可见的视频
// @author            YTB0710
// @match             https://space.bilibili.com/*
// @connect           bilibili.com
// @grant             GM_openInTab
// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_xmlhttpRequest
// @grant             GM_cookie
// ==/UserScript==

(function () {
    'use strict';

    const AVRegex = /^[1-9]\d*$/;
    const BVRegex = /^BV[A-Za-z0-9]{10}$/;
    const startsWithAVRegex = /^av/i;
    const favlistURLRegex = /https:\/\/space\.bilibili\.com\/\d+\/favlist.*/;
    const getFidFromURLRegex = /fid=(\d+)/;
    const getUIDFromURLRegex = /https:\/\/space\.bilibili\.com\/(\d+)/;
    const getBVFromURLRegex = /video\/(\w+)/;
    const getHttpsFromURLRegex = /^(https?:\/\/|\/\/)/;
    const getAvifFromURLRegex = /@.*/;

    let onFavlistPage = false;
    let newFreshSpace;
    let classAppendNewFreshSpace;
    let pageSize;
    let divMessage;
    let divMessageHeightFixed = false;
    let order = 'mtime';
    const activeControllers = new Set();

    const detectionScope2Range = {
        apiURL1DetectionScope2RangeStart: 1,
        apiURL1DetectionScope2RangeEnd: 1000,
        apiURL2DetectionScope2RangeStart: 1,
        apiURL2DetectionScope2RangeEnd: 200
    };

    const settings = {
        ...{
            apiURL: 1, // v9
            apiURL1DetectionScope: 'page', // v8 v9
            apiURL2DetectionScope: 'page', // v14
            displayAdvancedControls: false, // v14
            apiDelay: 500, // v13 v14
            divMessageHeight: 25, // v14
            addMessageJumpRemoveAdd: false, // v14
            autoClearMessage: true, // v9
        },
        ...GM_getValue('settings', null)
    };

    ///////////////////////////////////////////////////////////////////////////////////
    // v9
    if (GM_getValue('detectionScope', '')) {
        settings.apiURL1DetectionScope = GM_getValue('detectionScope', '');
        GM_setValue('detectionScope', '');
        GM_setValue('settings', settings);
    }
    // v14
    if (settings.hasOwnProperty('apiURL1Delay')) {
        settings.apiDelay = settings.apiURL1Delay;
        delete settings.apiURL1Delay;
        GM_setValue('settings', settings);
    }
    ///////////////////////////////////////////////////////////////////////////////////

    const sideObserver = new MutationObserver((_mutations, observer) => {
        if (document.querySelector('div.favlist-aside')) {
            observer.disconnect();
            newFreshSpace = true;
            classAppendNewFreshSpace = '-newFreshSpace';
            pageSize = window.innerWidth < 1760 ? 40 : 36;
            initControls();
            radioFilterObserver.observe(document.querySelector('div.fav-list-header-filter__left > div'), { subtree: true, characterData: false, attributeFilter: ['class'] });
            headerFilterLeftChildListObserver.observe(document.querySelector('div.fav-list-header-filter__left'), { childList: true, attributes: false, characterData: false });
            return;
        }
        if (document.querySelector('div.fav-sidenav')) {
            observer.disconnect();
            newFreshSpace = false;
            classAppendNewFreshSpace = '';
            pageSize = 20;
            initControls();
            return;
        }
    });

    const radioFilterObserver = new MutationObserver(mutations => {
        for (const mutation of mutations) {
            if (mutation.target.classList.contains('radio-filter__item--active')) {
                const orderText = mutation.target.textContent;
                if (orderText.includes('收藏')) {
                    order = 'mtime';
                } else if (orderText.includes('播放')) {
                    order = 'view';
                } else if (orderText.includes('投稿')) {
                    order = 'pubtime';
                } else {
                    addMessage('无法确定各个视频的排序方式, 请反馈该问题', false, 'red');
                }
            }
        }
    });

    const headerFilterLeftChildListObserver = new MutationObserver(mutations => {
        for (const mutation of mutations) {
            for (const addedNode of mutation.addedNodes) {
                if (addedNode.nodeType === 1 && addedNode.classList.contains('radio-filter')) {
                    order = 'mtime';
                    radioFilterObserver.observe(addedNode, { subtree: true, characterData: false, attributeFilter: ['class'] });
                }
            }
            for (const removedNode of mutation.removedNodes) {
                if (removedNode.nodeType === 1 && removedNode.classList.contains('radio-filter')) {
                    radioFilterObserver.disconnect();
                }
            }
        }
    });

    checkURL();

    const originalPushState = history.pushState;
    history.pushState = function (...args) {
        originalPushState.apply(this, args);
        checkURL();
    };

    const originalReplaceState = history.replaceState;
    history.replaceState = function (...args) {
        originalReplaceState.apply(this, args);
        checkURL();
    };

    window.addEventListener('popstate', checkURL);

    function checkURL() {
        if (favlistURLRegex.test(location.href)) {
            if (!onFavlistPage) {
                onFavlistPage = true;
                sideObserver.observe(document.body, { subtree: true, childList: true, attributes: false, characterData: false });
            }
        } else {
            if (onFavlistPage) {
                abortActiveControllers();
                onFavlistPage = false;
                sideObserver.disconnect();
            }
        }
    }

    function initControls() {

        const style = document.createElement('style');
        style.textContent = `
            .detect-div-first {
                padding: 2px;
            }
            .detect-div-first-newFreshSpace {
                padding: 2px 0;
            }
            .detect-div-second {
                padding: 2px 0 2px 16px;
            }
            .detect-div-second-newFreshSpace {
                padding: 2px 0 2px 16px;
            }
            .detect-button, .detect-button-newFreshSpace {
                border: 1px solid var(--Ga3, #cccccc);
                color: var(--Ga10, #000000);
                background-color: var(--Ga1, #f0f0f0);
                line-height: 1;
                cursor: pointer;
            }
            .detect-button {
                border-radius: 2px;
                padding: 2px;
                font-size: 14px;
            }
            .detect-button-newFreshSpace {
                border-radius: 3px;
                padding: 3px;
                font-size: 16px;
            }
            .detect-label, .detect-label-newFreshSpace {
                line-height: 1;
            }
            .detect-inputCheckbox, .detect-inputCheckbox-newFreshSpace {
                margin-left: 0;
            }
            .detect-inputCheckbox {
                margin-right: 1px;
            }
            .detect-inputCheckbox-newFreshSpace {
                margin-right: 3px;
            }
            .detect-inputRadio, .detect-inputRadio-newFreshSpace {
                margin-left: 0;
            }
            .detect-inputRadio {
                margin-right: 1px;
            }
            .detect-inputRadio-newFreshSpace {
                margin-right: 3px;
            }
            .detect-inputText1, .detect-inputText1-newFreshSpace {
                box-sizing: content-box;
                border: 1px solid var(--Ga3, #cccccc);
                color: var(--Ga10, #000000);
                background-color: var(--Ga0, #ffffff);
                line-height: 1;
            }
            .detect-inputText1 {
                width: 140px;
                height: 14px;
                border-radius: 2px;
                padding: 2px;
                font-size: 14px;
            }
            .detect-inputText1-newFreshSpace {
                width: 160px;
                height: 16px;
                border-radius: 3px;
                padding: 3px;
                font-size: 16px;
            }
            .detect-inputText2, .detect-inputText2-newFreshSpace {
                box-sizing: content-box;
                border: 1px solid var(--Ga3, #cccccc);
                color: var(--Ga10, #000000);
                background-color: var(--Ga0, #ffffff);
                padding: 1px 2px;
                line-height: 1;
            }
            .detect-inputText2 {
                width: 28px;
                height: 14px;
                border-radius: 2px;
                font-size: 14px;
            }
            .detect-inputText2-newFreshSpace {
                width: 32px;
                height: 16px;
                border-radius: 3px;
                font-size: 16px;
            }
            .detect-inputText3, .detect-inputText3-newFreshSpace {
                box-sizing: content-box;
                border: 1px solid var(--Ga3, #cccccc);
                color: var(--Ga10, #000000);
                background-color: var(--Ga0, #ffffff);
                padding: 1px 2px;
                line-height: 1;
            }
            .detect-inputText3 {
                width: 56px;
                height: 14px;
                border-radius: 2px;
                font-size: 14px;
            }
            .detect-inputText3-newFreshSpace {
                width: 64px;
                height: 16px;
                border-radius: 3px;
                font-size: 16px;
            }
            .detect-divMessage, .detect-divMessage-newFreshSpace {
                overflow-y: auto;
                background-color: var(--Ga1, #eeeeee);
                line-height: 1.5;
                scrollbar-width: none;
            }
            .detect-divMessage {
                margin: 2px;
            }
            .detect-divMessage::-webkit-scrollbar {
                display: none;
            }
            .detect-divMessage-newFreshSpace {
                margin: 2px 0;
            }
            .detect-divMessage-newFreshSpace::-webkit-scrollbar {
                display: none;
            }
            .detect-disabled, .detect-disabled-newFreshSpace {
                opacity: 0.5;
                pointer-events: none;
            }
            .detect-hidden, .detect-hidden-newFreshSpace {
                display: none;
            }
        `;
        document.head.appendChild(style);

        const styleDivMessageHeightFixed = document.createElement('style');
        styleDivMessageHeightFixed.id = 'detect-style-divMessage-heightFixed';
        styleDivMessageHeightFixed.textContent = `
            .detect-divMessage-heightFixed {
                height: ${settings.divMessageHeight * 18}px;
            }
            .detect-divMessage-heightFixed-newFreshSpace {
                height: ${settings.divMessageHeight * 20}px;
            }
        `;
        document.head.appendChild(styleDivMessageHeightFixed);

        const divSide = document.querySelector(newFreshSpace ? 'div.favlist-aside' : 'div.fav-sidenav');
        if (!newFreshSpace && divSide.querySelector('a.watch-later')) {
            divSide.querySelector('a.watch-later').style.borderBottom = '1px solid #eeeeee';
        }

        const divControls = document.createElement('div');
        divControls.classList.add('detect-div-first' + classAppendNewFreshSpace);
        divControls.style.color = 'var(--Ga10, #000000)';
        if (!newFreshSpace) {
            divControls.style.borderTop = '1px solid #e4e9f0';
        }
        divSide.appendChild(divControls);

        const divInputTextAVBV = document.createElement('div');
        divInputTextAVBV.classList.add('detect-div-first' + classAppendNewFreshSpace);
        divControls.appendChild(divInputTextAVBV);

        const inputTextAVBV = document.createElement('input');
        inputTextAVBV.type = 'text';
        inputTextAVBV.classList.add('detect-inputText1' + classAppendNewFreshSpace);
        inputTextAVBV.placeholder = '输入AV号或BV号';
        divInputTextAVBV.appendChild(inputTextAVBV);

        const divButtonDetect = document.createElement('div');
        divButtonDetect.classList.add('detect-div-first' + classAppendNewFreshSpace);
        divButtonDetect.setAttribute('title',
            '接口1和接口2在未来可能会失效。');
        divControls.appendChild(divButtonDetect);

        const buttonDetect = document.createElement('button');
        buttonDetect.type = 'button';
        buttonDetect.classList.add('detect-button' + classAppendNewFreshSpace);
        buttonDetect.textContent = '检测隐藏视频';
        buttonDetect.addEventListener('click', async () => {

            abortActiveControllers();
            let controller;

            try {

                controller = new AbortController();
                activeControllers.add(controller);

                if (settings.autoClearMessage) {
                    clearMessage();
                }

                let currentFavlist;
                if (newFreshSpace) {
                    currentFavlist = document.querySelector('div.vui_sidebar-item--active');
                    if (!document.querySelector('div.fav-collapse').contains(currentFavlist)) {
                        throw ['不支持处理特殊收藏夹'];
                    }
                } else {
                    currentFavlist = document.querySelector('.fav-item.cur');
                    if (!document.querySelector('div.nav-container').contains(currentFavlist)) {
                        throw ['不支持处理特殊收藏夹'];
                    }
                }

                let fid;
                if (newFreshSpace) {
                    const getFidFromURLMatch = location.href.match(getFidFromURLRegex);
                    if (getFidFromURLMatch) {
                        fid = parseInt(getFidFromURLMatch[1], 10);
                    } else {
                        throw ['无法获取当前收藏夹的fid, 刷新页面可能有帮助'];
                    }
                } else {
                    fid = document.querySelector('.fav-item.cur').getAttribute('fid');
                }

                let pageNumber;
                if (newFreshSpace) {
                    const pagenation = document.querySelector('button.vui_pagenation--btn-num.vui_button--active');
                    if (!pagenation) {
                        pageNumber = 1;
                    } else {
                        pageNumber = parseInt(pagenation.textContent, 10);
                    }
                } else {
                    pageNumber = parseInt(document.querySelector('li.be-pager-item-active > a').textContent, 10);
                }

                let searchKeyword = '';
                const inputKeyword = document.querySelector(newFreshSpace ? 'input.fav-list-header-filter__search' : 'input.search-fav-input');
                if (inputKeyword) {
                    searchKeyword = inputKeyword.value;
                }

                let searchType;
                const divType = document.querySelector(newFreshSpace ? 'div.vui_input__prepend' : 'div.search-types > div');
                let typeText = '当前';
                if (divType) {
                    typeText = divType.textContent;
                }
                if (!searchKeyword) {
                    typeText = '当前';
                }
                if (typeText.includes('当前')) {
                    searchType = 0;
                } else if (typeText.includes('全部')) {
                    searchType = 1;
                } else {
                    throw ['无法确定搜索的范围为当前收藏夹还是全部收藏夹, 请反馈该问题'];
                }

                const defaultFilter = await appendParamsForApiURL2(fid, 1, 20) === `https://api.bilibili.com/medialist/gateway/base/spaceDetail?media_id=${fid}&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0&jsonp=jsonp`;

                if (settings.apiURL === 1) {
                    if (settings.apiURL1DetectionScope === 'page') {
                        if (!defaultFilter) {
                            throw ['请刷新页面, 恢复默认的排序方式和筛选条件'];
                        }
                        await apiURL1DetectionScope1(fid, pageNumber);
                    } else {
                        await apiURL1DetectionScope2(fid, defaultFilter, controller, spanDetect);
                    }
                } else {
                    if (settings.apiURL2DetectionScope === 'page') {
                        if (searchType) {
                            throw ['不支持搜索范围为全部收藏夹'];
                        }
                        await apiURL2DetectionScope1(fid, pageNumber);
                    } else {
                        await apiURL2DetectionScope2(fid, defaultFilter, controller, spanDetect);
                    }
                }

            } catch (error) {
                if (error instanceof Error) {
                    if (error.name === 'AbortError') {
                        return;
                    }
                    catchUnknownError(error);

                } else {
                    addMessage(error[0], false, 'red');
                    for (let i = 1; i < error.length; i++) {
                        addMessage(error[i], true);
                    }
                }

            } finally {
                activeControllers.delete(controller);
            }
        });
        divButtonDetect.appendChild(buttonDetect);

        const spanDetect = document.createElement('span');
        divButtonDetect.appendChild(spanDetect);

        const divLabelApiURL1 = document.createElement('div');
        divLabelApiURL1.classList.add('detect-div-first' + classAppendNewFreshSpace);
        divLabelApiURL1.setAttribute('title',
            '地址: https://api.bilibili.com/x/v3/fav/resource/ids?media_id={收藏夹fid}&pn={页码}&platform=web\n' +
            '数据: AV号, BV号 (一次请求最多可获取1000个视频, 按最近收藏排序)');
        divControls.appendChild(divLabelApiURL1);

        const labelApiURL1 = document.createElement('label');
        labelApiURL1.classList.add('detect-label' + classAppendNewFreshSpace);
        labelApiURL1.textContent = '从接口1获取数据';
        divLabelApiURL1.appendChild(labelApiURL1);

        const radioApiURL1 = document.createElement('input');
        radioApiURL1.type = 'radio';
        radioApiURL1.classList.add('detect-inputRadio' + classAppendNewFreshSpace);
        radioApiURL1.name = 'apiURL';
        radioApiURL1.value = 1;
        radioApiURL1.checked = settings.apiURL === 1;
        radioApiURL1.addEventListener('change', () => {
            try {
                settings.apiURL = 1;
                divLabelApiURL1DetectionScope1.classList.remove('detect-disabled' + classAppendNewFreshSpace);
                divLabelApiURL1DetectionScope2.classList.remove('detect-disabled' + classAppendNewFreshSpace);
                if (settings.apiURL1DetectionScope === 'favlist') {
                    divLabelApiURL1DetectionScope2Range.classList.remove('detect-disabled' + classAppendNewFreshSpace);
                }
                divLabelApiURL2DetectionScope1.classList.add('detect-disabled' + classAppendNewFreshSpace);
                divLabelApiURL2DetectionScope2.classList.add('detect-disabled' + classAppendNewFreshSpace);
                divLabelApiURL2DetectionScope2Range.classList.add('detect-disabled' + classAppendNewFreshSpace);
                GM_setValue('settings', settings);
            } catch (error) {
                catchUnknownError(error);
            }
        });
        labelApiURL1.insertAdjacentElement('afterbegin', radioApiURL1);

        const divLabelApiURL1DetectionScope1 = document.createElement('div');
        divLabelApiURL1DetectionScope1.classList.add('detect-div-second' + classAppendNewFreshSpace, 'detect-disabled' + classAppendNewFreshSpace);
        divLabelApiURL1DetectionScope1.setAttribute('title',
            '如果您刚刚在当前收藏夹添加或移除了视频, 请刷新页面后再使用此功能。\n' +
            '如果您正在使用的其他脚本或插件修改了视频封面和标题的链接地址, 请将其关闭后刷新页面再使用此功能。');
        divControls.appendChild(divLabelApiURL1DetectionScope1);

        const labelApiURL1DetectionScope1 = document.createElement('label');
        labelApiURL1DetectionScope1.classList.add('detect-label' + classAppendNewFreshSpace);
        labelApiURL1DetectionScope1.textContent = '检测当前页';
        divLabelApiURL1DetectionScope1.appendChild(labelApiURL1DetectionScope1);

        const radioApiURL1DetectionScope1 = document.createElement('input');
        radioApiURL1DetectionScope1.type = 'radio';
        radioApiURL1DetectionScope1.classList.add('detect-inputRadio' + classAppendNewFreshSpace);
        radioApiURL1DetectionScope1.name = 'apiURL1DetectionScope';
        radioApiURL1DetectionScope1.value = 'page';
        radioApiURL1DetectionScope1.checked = settings.apiURL1DetectionScope === 'page';
        radioApiURL1DetectionScope1.addEventListener('change', () => {
            try {
                settings.apiURL1DetectionScope = 'page';
                divLabelApiURL1DetectionScope2Range.classList.add('detect-disabled' + classAppendNewFreshSpace);
                GM_setValue('settings', settings);
            } catch (error) {
                catchUnknownError(error);
            }
        });
        labelApiURL1DetectionScope1.insertAdjacentElement('afterbegin', radioApiURL1DetectionScope1);

        const divLabelApiURL1DetectionScope2 = document.createElement('div');
        divLabelApiURL1DetectionScope2.classList.add('detect-div-second' + classAppendNewFreshSpace, 'detect-disabled' + classAppendNewFreshSpace);
        divControls.appendChild(divLabelApiURL1DetectionScope2);

        const labelApiURL1DetectionScope2 = document.createElement('label');
        labelApiURL1DetectionScope2.classList.add('detect-label' + classAppendNewFreshSpace);
        labelApiURL1DetectionScope2.textContent = '检测当前收藏夹';
        divLabelApiURL1DetectionScope2.appendChild(labelApiURL1DetectionScope2);

        const radioApiURL1DetectionScope2 = document.createElement('input');
        radioApiURL1DetectionScope2.type = 'radio';
        radioApiURL1DetectionScope2.classList.add('detect-inputRadio' + classAppendNewFreshSpace);
        radioApiURL1DetectionScope2.name = 'apiURL1DetectionScope';
        radioApiURL1DetectionScope2.value = 'favlist';
        radioApiURL1DetectionScope2.checked = settings.apiURL1DetectionScope === 'favlist';
        radioApiURL1DetectionScope2.addEventListener('change', () => {
            try {
                settings.apiURL1DetectionScope = 'favlist';
                divLabelApiURL1DetectionScope2Range.classList.remove('detect-disabled' + classAppendNewFreshSpace);
                GM_setValue('settings', settings);
            } catch (error) {
                catchUnknownError(error);
            }
        });
        labelApiURL1DetectionScope2.insertAdjacentElement('afterbegin', radioApiURL1DetectionScope2);

        const divLabelApiURL1DetectionScope2Range = document.createElement('div');
        divLabelApiURL1DetectionScope2Range.classList.add('detect-div-second' + classAppendNewFreshSpace, 'detect-disabled' + classAppendNewFreshSpace);
        divControls.appendChild(divLabelApiURL1DetectionScope2Range);

        const labelApiURL1DetectionScope2RangeStart = document.createElement('label');
        labelApiURL1DetectionScope2RangeStart.classList.add('detect-label' + classAppendNewFreshSpace);
        divLabelApiURL1DetectionScope2Range.appendChild(labelApiURL1DetectionScope2RangeStart);

        const inputTextApiURL1DetectionScope2RangeStart = document.createElement('input');
        inputTextApiURL1DetectionScope2RangeStart.type = 'number';
        inputTextApiURL1DetectionScope2RangeStart.classList.add('detect-inputText3' + classAppendNewFreshSpace);
        inputTextApiURL1DetectionScope2RangeStart.value = 1;
        inputTextApiURL1DetectionScope2RangeStart.min = 1;
        inputTextApiURL1DetectionScope2RangeStart.max = 49001;
        inputTextApiURL1DetectionScope2RangeStart.step = 1000;
        inputTextApiURL1DetectionScope2RangeStart.setAttribute('detect-def', 1);
        inputTextApiURL1DetectionScope2RangeStart.setAttribute('detect-min', 1);
        inputTextApiURL1DetectionScope2RangeStart.setAttribute('detect-max', 49001);
        inputTextApiURL1DetectionScope2RangeStart.setAttribute('detect-step', 1000);
        inputTextApiURL1DetectionScope2RangeStart.setAttribute('detect-setting', 'apiURL1DetectionScope2RangeStart');
        inputTextApiURL1DetectionScope2RangeStart.addEventListener('blur', validateInputTextRangeStart);

        labelApiURL1DetectionScope2RangeStart.appendChild(document.createTextNode('范围'));
        labelApiURL1DetectionScope2RangeStart.appendChild(inputTextApiURL1DetectionScope2RangeStart);

        const labelApiURL1DetectionScope2RangeEnd = document.createElement('label');
        labelApiURL1DetectionScope2RangeEnd.classList.add('detect-label' + classAppendNewFreshSpace);
        divLabelApiURL1DetectionScope2Range.appendChild(labelApiURL1DetectionScope2RangeEnd);

        const inputTextApiURL1DetectionScope2RangeEnd = document.createElement('input');
        inputTextApiURL1DetectionScope2RangeEnd.type = 'number';
        inputTextApiURL1DetectionScope2RangeEnd.classList.add('detect-inputText3' + classAppendNewFreshSpace);
        inputTextApiURL1DetectionScope2RangeEnd.value = 1000;
        inputTextApiURL1DetectionScope2RangeEnd.min = 1000;
        inputTextApiURL1DetectionScope2RangeEnd.max = 50000;
        inputTextApiURL1DetectionScope2RangeEnd.step = 1000;
        inputTextApiURL1DetectionScope2RangeEnd.setAttribute('detect-def', 1000);
        inputTextApiURL1DetectionScope2RangeEnd.setAttribute('detect-min', 1000);
        inputTextApiURL1DetectionScope2RangeEnd.setAttribute('detect-max', 50000);
        inputTextApiURL1DetectionScope2RangeEnd.setAttribute('detect-step', 1000);
        inputTextApiURL1DetectionScope2RangeEnd.setAttribute('detect-setting', 'apiURL1DetectionScope2RangeEnd');
        inputTextApiURL1DetectionScope2RangeEnd.addEventListener('blur', validateInputTextRangeEnd);

        labelApiURL1DetectionScope2RangeEnd.appendChild(document.createTextNode(' ~ '));
        labelApiURL1DetectionScope2RangeEnd.appendChild(inputTextApiURL1DetectionScope2RangeEnd);

        const divLabelApiURL2 = document.createElement('div');
        divLabelApiURL2.classList.add('detect-div-first' + classAppendNewFreshSpace);
        divLabelApiURL2.setAttribute('title',
            '地址: https://api.bilibili.com/medialist/gateway/base/spaceDetail?media_id={收藏夹fid}&pn={页码}&ps={每页展示视频数量}\n' +
            '数据: AV号, BV号, 简介, UP主, 上传时间, 发布时间, 收藏时间, 每个分集的标题, cid\n' +
            '极少数视频可以获取到标题和封面地址。\n' +
            '您需要将当前收藏夹设置为公开后才能获取到数据。如果收藏夹内第一个视频不是失效视频, 修改可见性会导致收藏夹的封面被固定为该视频的封面, 建议修改可见性之前先复制一个失效视频到当前收藏夹的首位。');
        divControls.appendChild(divLabelApiURL2);

        const labelApiURL2 = document.createElement('label');
        labelApiURL2.classList.add('detect-label' + classAppendNewFreshSpace);
        labelApiURL2.textContent = '从接口2获取数据';
        divLabelApiURL2.appendChild(labelApiURL2);

        const radioApiURL2 = document.createElement('input');
        radioApiURL2.type = 'radio';
        radioApiURL2.classList.add('detect-inputRadio' + classAppendNewFreshSpace);
        radioApiURL2.name = 'apiURL';
        radioApiURL2.value = 2;
        radioApiURL2.checked = settings.apiURL === 2;
        radioApiURL2.addEventListener('change', () => {
            try {
                settings.apiURL = 2;
                divLabelApiURL1DetectionScope1.classList.add('detect-disabled' + classAppendNewFreshSpace);
                divLabelApiURL1DetectionScope2.classList.add('detect-disabled' + classAppendNewFreshSpace);
                divLabelApiURL1DetectionScope2Range.classList.add('detect-disabled' + classAppendNewFreshSpace);
                divLabelApiURL2DetectionScope1.classList.remove('detect-disabled' + classAppendNewFreshSpace);
                divLabelApiURL2DetectionScope2.classList.remove('detect-disabled' + classAppendNewFreshSpace);
                if (settings.apiURL2DetectionScope === 'favlist') {
                    divLabelApiURL2DetectionScope2Range.classList.remove('detect-disabled' + classAppendNewFreshSpace);
                }
                GM_setValue('settings', settings);
            } catch (error) {
                catchUnknownError(error);
            }
        });
        labelApiURL2.insertAdjacentElement('afterbegin', radioApiURL2);

        const divLabelApiURL2DetectionScope1 = document.createElement('div');
        divLabelApiURL2DetectionScope1.classList.add('detect-div-second' + classAppendNewFreshSpace, 'detect-disabled' + classAppendNewFreshSpace);
        divLabelApiURL2DetectionScope1.setAttribute('title',
            '如果您刚刚在当前收藏夹添加或移除了视频, 请刷新页面后再使用此功能。\n' +
            '如果您正在使用的其他脚本或插件修改了视频封面和标题的链接地址, 请将其关闭后刷新页面再使用此功能。');
        divControls.appendChild(divLabelApiURL2DetectionScope1);

        const labelApiURL2DetectionScope1 = document.createElement('label');
        labelApiURL2DetectionScope1.classList.add('detect-label' + classAppendNewFreshSpace);
        labelApiURL2DetectionScope1.textContent = '检测当前页';
        divLabelApiURL2DetectionScope1.appendChild(labelApiURL2DetectionScope1);

        const radioApiURL2DetectionScope1 = document.createElement('input');
        radioApiURL2DetectionScope1.type = 'radio';
        radioApiURL2DetectionScope1.classList.add('detect-inputRadio' + classAppendNewFreshSpace);
        radioApiURL2DetectionScope1.name = 'apiURL2DetectionScope';
        radioApiURL2DetectionScope1.value = 'page';
        radioApiURL2DetectionScope1.checked = settings.apiURL2DetectionScope === 'page';
        radioApiURL2DetectionScope1.addEventListener('change', () => {
            try {
                settings.apiURL2DetectionScope = 'page';
                divLabelApiURL2DetectionScope2Range.classList.add('detect-disabled' + classAppendNewFreshSpace);
                GM_setValue('settings', settings);
            } catch (error) {
                catchUnknownError(error);
            }
        });
        labelApiURL2DetectionScope1.insertAdjacentElement('afterbegin', radioApiURL2DetectionScope1);

        const divLabelApiURL2DetectionScope2 = document.createElement('div');
        divLabelApiURL2DetectionScope2.classList.add('detect-div-second' + classAppendNewFreshSpace, 'detect-disabled' + classAppendNewFreshSpace);
        divControls.appendChild(divLabelApiURL2DetectionScope2);

        const labelApiURL2DetectionScope2 = document.createElement('label');
        labelApiURL2DetectionScope2.classList.add('detect-label' + classAppendNewFreshSpace);
        labelApiURL2DetectionScope2.textContent = '检测当前收藏夹';
        divLabelApiURL2DetectionScope2.appendChild(labelApiURL2DetectionScope2);

        const radioApiURL2DetectionScope2 = document.createElement('input');
        radioApiURL2DetectionScope2.type = 'radio';
        radioApiURL2DetectionScope2.classList.add('detect-inputRadio' + classAppendNewFreshSpace);
        radioApiURL2DetectionScope2.name = 'apiURL2DetectionScope';
        radioApiURL2DetectionScope2.value = 'favlist';
        radioApiURL2DetectionScope2.checked = settings.apiURL2DetectionScope === 'favlist';
        radioApiURL2DetectionScope2.addEventListener('change', () => {
            try {
                settings.apiURL2DetectionScope = 'favlist';
                divLabelApiURL2DetectionScope2Range.classList.remove('detect-disabled' + classAppendNewFreshSpace);
                GM_setValue('settings', settings);
            } catch (error) {
                catchUnknownError(error);
            }
        });
        labelApiURL2DetectionScope2.insertAdjacentElement('afterbegin', radioApiURL2DetectionScope2);

        const divLabelApiURL2DetectionScope2Range = document.createElement('div');
        divLabelApiURL2DetectionScope2Range.classList.add('detect-div-second' + classAppendNewFreshSpace, 'detect-disabled' + classAppendNewFreshSpace);
        divControls.appendChild(divLabelApiURL2DetectionScope2Range);

        const labelApiURL2DetectionScope2RangeStart = document.createElement('label');
        labelApiURL2DetectionScope2RangeStart.classList.add('detect-label' + classAppendNewFreshSpace);
        divLabelApiURL2DetectionScope2Range.appendChild(labelApiURL2DetectionScope2RangeStart);

        const inputTextApiURL2DetectionScope2RangeStart = document.createElement('input');
        inputTextApiURL2DetectionScope2RangeStart.type = 'number';
        inputTextApiURL2DetectionScope2RangeStart.classList.add('detect-inputText3' + classAppendNewFreshSpace);
        inputTextApiURL2DetectionScope2RangeStart.value = 1;
        inputTextApiURL2DetectionScope2RangeStart.min = 1;
        inputTextApiURL2DetectionScope2RangeStart.max = 49801;
        inputTextApiURL2DetectionScope2RangeStart.step = 200;
        inputTextApiURL2DetectionScope2RangeStart.setAttribute('detect-def', 1);
        inputTextApiURL2DetectionScope2RangeStart.setAttribute('detect-min', 1);
        inputTextApiURL2DetectionScope2RangeStart.setAttribute('detect-max', 49801);
        inputTextApiURL2DetectionScope2RangeStart.setAttribute('detect-step', 200);
        inputTextApiURL2DetectionScope2RangeStart.setAttribute('detect-setting', 'apiURL2DetectionScope2RangeStart');
        inputTextApiURL2DetectionScope2RangeStart.addEventListener('blur', validateInputTextRangeStart);

        labelApiURL2DetectionScope2RangeStart.appendChild(document.createTextNode('范围'));
        labelApiURL2DetectionScope2RangeStart.appendChild(inputTextApiURL2DetectionScope2RangeStart);

        const labelApiURL2DetectionScope2RangeEnd = document.createElement('label');
        labelApiURL2DetectionScope2RangeEnd.classList.add('detect-label' + classAppendNewFreshSpace);
        divLabelApiURL2DetectionScope2Range.appendChild(labelApiURL2DetectionScope2RangeEnd);

        const inputTextApiURL2DetectionScope2RangeEnd = document.createElement('input');
        inputTextApiURL2DetectionScope2RangeEnd.type = 'number';
        inputTextApiURL2DetectionScope2RangeEnd.classList.add('detect-inputText3' + classAppendNewFreshSpace);
        inputTextApiURL2DetectionScope2RangeEnd.value = 200;
        inputTextApiURL2DetectionScope2RangeEnd.min = 200;
        inputTextApiURL2DetectionScope2RangeEnd.max = 50000;
        inputTextApiURL2DetectionScope2RangeEnd.step = 200;
        inputTextApiURL2DetectionScope2RangeEnd.setAttribute('detect-def', 200);
        inputTextApiURL2DetectionScope2RangeEnd.setAttribute('detect-min', 200);
        inputTextApiURL2DetectionScope2RangeEnd.setAttribute('detect-max', 50000);
        inputTextApiURL2DetectionScope2RangeEnd.setAttribute('detect-step', 200);
        inputTextApiURL2DetectionScope2RangeEnd.setAttribute('detect-setting', 'apiURL2DetectionScope2RangeEnd');
        inputTextApiURL2DetectionScope2RangeEnd.addEventListener('blur', validateInputTextRangeEnd);

        labelApiURL2DetectionScope2RangeEnd.appendChild(document.createTextNode(' ~ '));
        labelApiURL2DetectionScope2RangeEnd.appendChild(inputTextApiURL2DetectionScope2RangeEnd);

        const divButtonJump = document.createElement('div');
        divButtonJump.classList.add('detect-div-first' + classAppendNewFreshSpace);
        divButtonJump.setAttribute('title',
            '在文本框内输入某个视频的AV号或BV号后, 点击此按钮, 将会跳转至该视频在各个第三方网站的页面。');
        divControls.appendChild(divButtonJump);

        const buttonJump = document.createElement('button');
        buttonJump.type = 'button';
        buttonJump.classList.add('detect-button' + classAppendNewFreshSpace);
        buttonJump.textContent = '查询视频信息';
        buttonJump.addEventListener('click', () => {
            try {
                let BV = inputTextAVBV.value;
                if (!BVRegex.test(BV)) {
                    if (startsWithAVRegex.test(BV)) {
                        BV = BV.slice(2);
                    }
                    if (AVRegex.test(BV)) {
                        BV = av2bv(BV);
                    } else {
                        throw ['请输入AV号或BV号'];
                    }
                }

                jump(BV);

            } catch (error) {
                if (error instanceof Error) {
                    catchUnknownError(error);
                } else {
                    addMessage(error[0], false, 'red');
                    for (let i = 1; i < error.length; i++) {
                        addMessage(error[i], true);
                    }
                }
            }
        });
        divButtonJump.appendChild(buttonJump);

        const divButtonRemove = document.createElement('div');
        divButtonRemove.classList.add('detect-div-first' + classAppendNewFreshSpace);
        divButtonRemove.setAttribute('title',
            '在文本框内输入某个视频的AV号或BV号后, 点击此按钮, 将会从当前收藏夹中移除该视频。');
        divControls.appendChild(divButtonRemove);

        const buttonRemove = document.createElement('button');
        buttonRemove.type = 'button';
        buttonRemove.classList.add('detect-button' + classAppendNewFreshSpace);
        buttonRemove.textContent = '取消收藏';
        buttonRemove.addEventListener('click', () => {
            try {
                GM_cookie.list({ name: 'bili_jct' }, async (cookies, error) => {
                    if (error) {
                        throw ['无法读取cookie, 更新Tampermonkey可能有帮助'];
                    }

                    try {
                        let AV = inputTextAVBV.value;
                        if (startsWithAVRegex.test(AV)) {
                            AV = AV.slice(2);
                        }
                        if (!AVRegex.test(AV)) {
                            if (BVRegex.test(AV)) {
                                AV = bv2av(AV);
                            } else {
                                throw ['请输入AV号或BV号'];
                            }
                        }

                        await remove(AV, cookies, spanRemove);

                    } catch (error) {
                        if (error instanceof Error) {
                            catchUnknownError(error);
                        } else {
                            addMessage(error[0], false, 'red');
                            for (let i = 1; i < error.length; i++) {
                                addMessage(error[i], true);
                            }
                        }
                    }
                });

            } catch (error) {
                catchUnknownError(error);
            }
        });
        divButtonRemove.appendChild(buttonRemove);

        const spanRemove = document.createElement('span');
        divButtonRemove.appendChild(spanRemove);

        const divButtonAdd = document.createElement('div');
        divButtonAdd.classList.add('detect-div-first' + classAppendNewFreshSpace);
        divButtonAdd.setAttribute('title',
            '在文本框内输入某个视频的AV号或BV号后, 点击此按钮, 将会添加该视频到当前收藏夹的首位。如果当前收藏夹是公开收藏夹, 将收藏夹分享到动态后, 就可以看到该视频的封面。');
        divControls.appendChild(divButtonAdd);

        const buttonAdd = document.createElement('button');
        buttonAdd.type = 'button';
        buttonAdd.classList.add('detect-button' + classAppendNewFreshSpace);
        buttonAdd.textContent = '添加收藏';
        buttonAdd.addEventListener('click', () => {
            try {
                GM_cookie.list({ name: 'bili_jct' }, async (cookies, error) => {
                    if (error) {
                        throw ['无法读取cookie, 更新Tampermonkey可能有帮助'];
                    }

                    try {
                        let AV = inputTextAVBV.value;
                        if (startsWithAVRegex.test(AV)) {
                            AV = AV.slice(2);
                        }
                        if (!AVRegex.test(AV)) {
                            if (BVRegex.test(AV)) {
                                AV = bv2av(AV);
                            } else {
                                throw ['请输入AV号或BV号'];
                            }
                        }

                        await add(AV, cookies, spanAdd);

                    } catch (error) {
                        if (error instanceof Error) {
                            catchUnknownError(error);
                        } else {
                            addMessage(error[0], false, 'red');
                            for (let i = 1; i < error.length; i++) {
                                addMessage(error[i], true);
                            }
                        }
                    }
                });

            } catch (error) {
                catchUnknownError(error);
            }
        });
        divButtonAdd.appendChild(buttonAdd);

        const spanAdd = document.createElement('span');
        divButtonAdd.appendChild(spanAdd);

        const divLabelDisplayAdvancedControls = document.createElement('div');
        divLabelDisplayAdvancedControls.classList.add('detect-div-first' + classAppendNewFreshSpace);
        divControls.appendChild(divLabelDisplayAdvancedControls);

        const labelDisplayAdvancedControls = document.createElement('label');
        labelDisplayAdvancedControls.classList.add('detect-label' + classAppendNewFreshSpace);
        labelDisplayAdvancedControls.textContent = '显示全部设置和功能';
        divLabelDisplayAdvancedControls.appendChild(labelDisplayAdvancedControls);

        const checkboxDisplayAdvancedControls = document.createElement('input');
        checkboxDisplayAdvancedControls.type = 'checkbox';
        checkboxDisplayAdvancedControls.classList.add('detect-inputCheckbox' + classAppendNewFreshSpace);
        checkboxDisplayAdvancedControls.checked = settings.displayAdvancedControls;
        checkboxDisplayAdvancedControls.addEventListener('change', () => {
            try {
                settings.displayAdvancedControls = checkboxDisplayAdvancedControls.checked;
                if (!settings.displayAdvancedControls) {
                    divControls.querySelectorAll('.detect-advanced').forEach(el => { el.classList.add('detect-hidden' + classAppendNewFreshSpace) });
                } else {
                    divControls.querySelectorAll('.detect-advanced').forEach(el => { el.classList.remove('detect-hidden' + classAppendNewFreshSpace) });
                }
                GM_setValue('settings', settings);
            } catch (error) {
                catchUnknownError(error);
            }
        });
        labelDisplayAdvancedControls.insertAdjacentElement('afterbegin', checkboxDisplayAdvancedControls);

        const divButtonAVBV = document.createElement('div');
        divButtonAVBV.classList.add('detect-div-first' + classAppendNewFreshSpace, 'detect-advanced');
        divButtonAVBV.setAttribute('title',
            '在文本框内输入某个视频的BV号后, 点击此按钮, 将会转换为其AV号, 反之亦然。');
        divControls.appendChild(divButtonAVBV);

        const buttonAVBV = document.createElement('button');
        buttonAVBV.type = 'button';
        buttonAVBV.classList.add('detect-button' + classAppendNewFreshSpace);
        buttonAVBV.textContent = 'AV号BV号互转';
        buttonAVBV.addEventListener('click', () => {
            try {
                let AVBV = inputTextAVBV.value;
                if (BVRegex.test(AVBV)) {
                    inputTextAVBV.value = bv2av(AVBV);
                    return;
                }
                if (startsWithAVRegex.test(AVBV)) {
                    AVBV = AVBV.slice(2);
                }
                if (AVRegex.test(AVBV)) {
                    inputTextAVBV.value = av2bv(AVBV);
                    return;
                }
                throw ['请输入AV号或BV号'];

            } catch (error) {
                if (error instanceof Error) {
                    catchUnknownError(error);
                } else {
                    addMessage(error[0], false, 'red');
                    for (let i = 1; i < error.length; i++) {
                        addMessage(error[i], true);
                    }
                }
            }
        });
        divButtonAVBV.appendChild(buttonAVBV);

        const divButtonPublic = document.createElement('div');
        divButtonPublic.classList.add('detect-div-first' + classAppendNewFreshSpace, 'detect-advanced');
        divButtonPublic.setAttribute('title',
            '请注意: 该功能会将当前收藏夹的封面恢复为默认 (总是显示第一个视频的封面)。');
        divControls.appendChild(divButtonPublic);

        const buttonPublic = document.createElement('button');
        buttonPublic.type = 'button';
        buttonPublic.classList.add('detect-button' + classAppendNewFreshSpace);
        buttonPublic.textContent = '将当前收藏夹设置为公开';
        buttonPublic.addEventListener('click', () => {
            try {
                GM_cookie.list({ name: 'bili_jct' }, async (cookies, error) => {
                    if (error) {
                        throw ['无法读取cookie, 更新Tampermonkey可能有帮助'];
                    }

                    try {
                        let fid;
                        if (newFreshSpace) {
                            const getFidFromURLMatch = location.href.match(getFidFromURLRegex);
                            if (getFidFromURLMatch) {
                                fid = parseInt(getFidFromURLMatch[1], 10);
                            } else {
                                throw ['无法获取当前收藏夹的fid, 刷新页面可能有帮助'];
                            }
                        } else {
                            fid = document.querySelector('.fav-item.cur').getAttribute('fid');
                        }

                        const response1 = await new Promise((resolve, reject) => {
                            GM_xmlhttpRequest({
                                method: 'GET',
                                url: `https://api.bilibili.com/x/v3/fav/folder/info?media_id=${fid}` + (newFreshSpace ? '&web_location=333.1387' : ''),
                                timeout: 5000,
                                responseType: 'json',
                                onload: (res) => resolve(res),
                                onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/folder/info', res.error]),
                                ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/folder/info'])
                            });
                        });
                        if (response1.status !== 200) {
                            throw ['请求失败', 'api.bilibili.com/x/v3/fav/folder/info', `${response1.status} ${response1.statusText}`];
                        }

                        const csrf = cookies[0].value;
                        const data = `media_id=${fid}&title=${encodeURIComponent(response1.response.data.title)}&intro=${encodeURIComponent(response1.response.data.intro)}&privacy=0&cover=&csrf=${csrf}`;
                        const response2 = await new Promise((resolve, reject) => {
                            GM_xmlhttpRequest({
                                method: 'POST',
                                url: 'https://api.bilibili.com/x/v3/fav/folder/edit',
                                data: data,
                                timeout: 5000,
                                responseType: 'json',
                                headers: {
                                    'Content-Length': data.length,
                                    'Content-Type': 'application/x-www-form-urlencoded'
                                },
                                onload: (res) => resolve(res),
                                onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/folder/edit', res.error]),
                                ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/folder/edit'])
                            });
                        });
                        if (response2.status !== 200) {
                            throw ['请求失败', 'api.bilibili.com/x/v3/fav/folder/edit', `${response2.status} ${response2.statusText}`];
                        }

                        console.log(response2.response);
                        spanPublic.textContent = ' ✔';
                        setTimeout(() => {
                            spanPublic.textContent = '';
                        }, 1000);

                    } catch (error) {
                        if (error instanceof Error) {
                            catchUnknownError(error);
                        } else {
                            addMessage(error[0], false, 'red');
                            for (let i = 1; i < error.length; i++) {
                                addMessage(error[i], true);
                            }
                        }
                    }
                });

            } catch (error) {
                catchUnknownError(error);
            }
        });
        divButtonPublic.appendChild(buttonPublic);

        const spanPublic = document.createElement('span');
        divButtonPublic.appendChild(spanPublic);

        const divButtonPrivate = document.createElement('div');
        divButtonPrivate.classList.add('detect-div-first' + classAppendNewFreshSpace, 'detect-advanced');
        divButtonPrivate.setAttribute('title',
            '请注意: 该功能会将当前收藏夹的封面恢复为默认 (总是显示第一个视频的封面)。');
        divControls.appendChild(divButtonPrivate);

        const buttonPrivate = document.createElement('button');
        buttonPrivate.type = 'button';
        buttonPrivate.classList.add('detect-button' + classAppendNewFreshSpace);
        buttonPrivate.textContent = '将当前收藏夹设置为私密';
        buttonPrivate.addEventListener('click', () => {
            try {
                GM_cookie.list({ name: 'bili_jct' }, async (cookies, error) => {
                    if (error) {
                        throw ['无法读取cookie, 更新Tampermonkey可能有帮助'];
                    }

                    try {
                        let fid;
                        if (newFreshSpace) {
                            const getFidFromURLMatch = location.href.match(getFidFromURLRegex);
                            if (getFidFromURLMatch) {
                                fid = parseInt(getFidFromURLMatch[1], 10);
                            } else {
                                throw ['无法获取当前收藏夹的fid, 刷新页面可能有帮助'];
                            }
                        } else {
                            fid = document.querySelector('.fav-item.cur').getAttribute('fid');
                        }

                        const response1 = await new Promise((resolve, reject) => {
                            GM_xmlhttpRequest({
                                method: 'GET',
                                url: `https://api.bilibili.com/x/v3/fav/folder/info?media_id=${fid}` + (newFreshSpace ? '&web_location=333.1387' : ''),
                                timeout: 5000,
                                responseType: 'json',
                                onload: (res) => resolve(res),
                                onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/folder/info', res.error]),
                                ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/folder/info'])
                            });
                        });
                        if (response1.status !== 200) {
                            throw ['请求失败', 'api.bilibili.com/x/v3/fav/folder/info', `${response1.status} ${response1.statusText}`];
                        }

                        const csrf = cookies[0].value;
                        const data = `media_id=${fid}&title=${encodeURIComponent(response1.response.data.title)}&intro=${encodeURIComponent(response1.response.data.intro)}&privacy=1&cover=&csrf=${csrf}`;
                        const response2 = await new Promise((resolve, reject) => {
                            GM_xmlhttpRequest({
                                method: 'POST',
                                url: 'https://api.bilibili.com/x/v3/fav/folder/edit',
                                data: data,
                                timeout: 5000,
                                responseType: 'json',
                                headers: {
                                    'Content-Length': data.length,
                                    'Content-Type': 'application/x-www-form-urlencoded'
                                },
                                onload: (res) => resolve(res),
                                onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/folder/edit', res.error]),
                                ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/folder/edit'])
                            });
                        });
                        if (response2.status !== 200) {
                            throw ['请求失败', 'api.bilibili.com/x/v3/fav/folder/edit', `${response2.status} ${response2.statusText}`];
                        }

                        console.log(response2.response);
                        spanPrivate.textContent = ' ✔';
                        setTimeout(() => {
                            spanPrivate.textContent = '';
                        }, 1000);

                    } catch (error) {
                        if (error instanceof Error) {
                            catchUnknownError(error);
                        } else {
                            addMessage(error[0], false, 'red');
                            for (let i = 1; i < error.length; i++) {
                                addMessage(error[i], true);
                            }
                        }
                    }
                });

            } catch (error) {
                catchUnknownError(error);
            }
        });
        divButtonPrivate.appendChild(buttonPrivate);

        const spanPrivate = document.createElement('span');
        divButtonPrivate.appendChild(spanPrivate);

        const divLabelApiDelay = document.createElement('div');
        divLabelApiDelay.classList.add('detect-div-first' + classAppendNewFreshSpace, 'detect-advanced');
        divLabelApiDelay.setAttribute('title',
            '默认: 500毫秒\n' +
            '请将其设置在400毫秒以上, 否则在短时间内频繁获取数据之后, 会出现请求失败的情况, 并且收藏夹无法使用, 需要若干分钟才能恢复。');
        divControls.appendChild(divLabelApiDelay);

        const labelApiDelay = document.createElement('label');
        labelApiDelay.classList.add('detect-label' + classAppendNewFreshSpace);
        divLabelApiDelay.appendChild(labelApiDelay);

        const inputTextApiDelay = document.createElement('input');
        inputTextApiDelay.type = 'text';
        inputTextApiDelay.classList.add('detect-inputText2' + classAppendNewFreshSpace);
        inputTextApiDelay.value = settings.apiDelay;
        inputTextApiDelay.setAttribute('detect-def', 500);
        inputTextApiDelay.setAttribute('detect-min', 0);
        inputTextApiDelay.setAttribute('detect-max', 1000);
        inputTextApiDelay.setAttribute('detect-setting', 'apiDelay');
        inputTextApiDelay.addEventListener('blur', validateInputText);

        labelApiDelay.appendChild(document.createTextNode('获取数据前等待'));
        labelApiDelay.appendChild(inputTextApiDelay);
        labelApiDelay.appendChild(document.createTextNode('毫秒'));

        const divLabelDivMessageHeight = document.createElement('div');
        divLabelDivMessageHeight.classList.add('detect-div-first' + classAppendNewFreshSpace, 'detect-advanced');
        divLabelDivMessageHeight.setAttribute('title',
            '默认: 25行');
        divControls.appendChild(divLabelDivMessageHeight);

        const labelDivMessageHeight = document.createElement('label');
        labelDivMessageHeight.classList.add('detect-label' + classAppendNewFreshSpace);
        divLabelDivMessageHeight.appendChild(labelDivMessageHeight);

        const inputTextDivMessageHeight = document.createElement('input');
        inputTextDivMessageHeight.type = 'text';
        inputTextDivMessageHeight.classList.add('detect-inputText2' + classAppendNewFreshSpace);
        inputTextDivMessageHeight.value = settings.divMessageHeight;
        inputTextDivMessageHeight.setAttribute('detect-def', 25);
        inputTextDivMessageHeight.setAttribute('detect-min', 10);
        inputTextDivMessageHeight.setAttribute('detect-max', 100);
        inputTextDivMessageHeight.setAttribute('detect-setting', 'divMessageHeight');
        inputTextDivMessageHeight.addEventListener('blur', validateInputTextDivMessageHeight);

        labelDivMessageHeight.appendChild(document.createTextNode('提示信息区域高度'));
        labelDivMessageHeight.appendChild(inputTextDivMessageHeight);
        labelDivMessageHeight.appendChild(document.createTextNode('行'));

        const divLabelAddMessageJumpRemoveAdd = document.createElement('div');
        divLabelAddMessageJumpRemoveAdd.classList.add('detect-div-first' + classAppendNewFreshSpace, 'detect-advanced');
        divLabelAddMessageJumpRemoveAdd.setAttribute('title',
            '开启后脚本将在检测结果信息中每个视频的下方添加查询视频信息, 取消收藏, 添加收藏的功能入口, 无需手动输入AV号或BV号。');
        divControls.appendChild(divLabelAddMessageJumpRemoveAdd);

        const labelAddMessageJumpRemoveAdd = document.createElement('label');
        labelAddMessageJumpRemoveAdd.classList.add('detect-label' + classAppendNewFreshSpace);
        labelAddMessageJumpRemoveAdd.textContent = '在检测结果信息中添加功能入口';
        divLabelAddMessageJumpRemoveAdd.appendChild(labelAddMessageJumpRemoveAdd);

        const checkboxAddMessageJumpRemoveAdd = document.createElement('input');
        checkboxAddMessageJumpRemoveAdd.type = 'checkbox';
        checkboxAddMessageJumpRemoveAdd.classList.add('detect-inputCheckbox' + classAppendNewFreshSpace);
        checkboxAddMessageJumpRemoveAdd.checked = settings.addMessageJumpRemoveAdd;
        checkboxAddMessageJumpRemoveAdd.addEventListener('change', () => {
            try {
                settings.addMessageJumpRemoveAdd = checkboxAddMessageJumpRemoveAdd.checked;
                GM_setValue('settings', settings);
            } catch (error) {
                catchUnknownError(error);
            }
        });
        labelAddMessageJumpRemoveAdd.insertAdjacentElement('afterbegin', checkboxAddMessageJumpRemoveAdd);

        const divLabelAutoClearMessage = document.createElement('div');
        divLabelAutoClearMessage.classList.add('detect-div-first' + classAppendNewFreshSpace, 'detect-advanced');
        divLabelAutoClearMessage.setAttribute('title',
            '开启后脚本将在每次检测隐藏视频之前清空提示信息。');
        divControls.appendChild(divLabelAutoClearMessage);

        const labelAutoClearMessage = document.createElement('label');
        labelAutoClearMessage.classList.add('detect-label' + classAppendNewFreshSpace);
        labelAutoClearMessage.textContent = '自动清空提示信息';
        divLabelAutoClearMessage.appendChild(labelAutoClearMessage);

        const checkboxAutoClearMessage = document.createElement('input');
        checkboxAutoClearMessage.type = 'checkbox';
        checkboxAutoClearMessage.classList.add('detect-inputCheckbox' + classAppendNewFreshSpace);
        checkboxAutoClearMessage.checked = settings.autoClearMessage;
        checkboxAutoClearMessage.addEventListener('change', () => {
            try {
                settings.autoClearMessage = checkboxAutoClearMessage.checked;
                GM_setValue('settings', settings);
            } catch (error) {
                catchUnknownError(error);
            }
        });
        labelAutoClearMessage.insertAdjacentElement('afterbegin', checkboxAutoClearMessage);

        divMessage = document.createElement('div');
        divMessage.classList.add('detect-divMessage' + classAppendNewFreshSpace);
        divControls.appendChild(divMessage);

        if (settings.apiURL === 1) {
            divLabelApiURL1DetectionScope1.classList.remove('detect-disabled' + classAppendNewFreshSpace);
            divLabelApiURL1DetectionScope2.classList.remove('detect-disabled' + classAppendNewFreshSpace);
            if (settings.apiURL1DetectionScope === 'favlist') {
                divLabelApiURL1DetectionScope2Range.classList.remove('detect-disabled' + classAppendNewFreshSpace);
            }
        } else {
            divLabelApiURL2DetectionScope1.classList.remove('detect-disabled' + classAppendNewFreshSpace);
            divLabelApiURL2DetectionScope2.classList.remove('detect-disabled' + classAppendNewFreshSpace);
            if (settings.apiURL2DetectionScope === 'favlist') {
                divLabelApiURL2DetectionScope2Range.classList.remove('detect-disabled' + classAppendNewFreshSpace);
            }
        }

        if (!settings.displayAdvancedControls) {
            divControls.querySelectorAll('.detect-advanced').forEach(el => {
                el.classList.add('detect-hidden' + classAppendNewFreshSpace);
            });
        }
    }

    async function apiURL1DetectionScope1(fid, pageNumber) {

        const newRequests = [];
        const oldPageStart = (pageNumber - 1) * pageSize;
        const oldPageEnd = oldPageStart + pageSize;
        let newPageNumber = Math.floor(oldPageStart / 1000) + 1;
        while ((newPageNumber - 1) * 1000 < oldPageEnd) {
            const newPageStart = (newPageNumber - 1) * 1000;
            const newPageEnd = newPageStart + 1000;
            const sliceStart = Math.max(oldPageStart, newPageStart);
            const sliceEnd = Math.min(oldPageEnd, newPageEnd);
            newRequests.push({ newPageNumber, sliceStart, sliceEnd });
            newPageNumber++;
        }

        const AVBVsPageAll = [];
        for (const newRequest of newRequests) {
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://api.bilibili.com/x/v3/fav/resource/ids?media_id=${fid}&pn=${newRequest.newPageNumber}&platform=web`,
                    timeout: 5000,
                    responseType: 'json',
                    onload: (res) => resolve(res),
                    onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/resource/ids', res.error]),
                    ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/resource/ids'])
                });
            });
            if (response.status !== 200) {
                throw ['请求失败', 'api.bilibili.com/x/v3/fav/resource/ids', `${response.status} ${response.statusText}`];
            }

            const sliceStart = newRequest.sliceStart - (newRequest.newPageNumber - 1) * 1000;
            const sliceEnd = newRequest.sliceEnd - (newRequest.newPageNumber - 1) * 1000;
            AVBVsPageAll.push(...response.response.data.slice(sliceStart, sliceEnd));
        }

        const videosPageVisable = document.querySelectorAll(newFreshSpace ? 'div.items__item' : 'li.small-item');
        let BVsPageVisable;
        if (newFreshSpace) {
            BVsPageVisable = Array.from(videosPageVisable).map(el => el.querySelector('a').getAttribute('href').match(getBVFromURLRegex)[1]);
        } else {
            BVsPageVisable = Array.from(videosPageVisable).map(el => el.getAttribute('data-aid'));
        }

        const AVBVsPageHidden = AVBVsPageAll.filter(el => !BVsPageVisable.includes(el.bvid) && el.type === 2);
        if (!AVBVsPageHidden.length) {
            addMessage('没有找到隐藏的视频', false, 'green');
            return;
        }
        AVBVsPageHidden.forEach(el => {
            addMessage(`在当前页的位置: ${AVBVsPageAll.findIndex(ele => ele.bvid === el.bvid) + 1}`, false, 'green');
            addMessage(`AV号: ${el.id}`);
            addMessage(`BV号: ${el.bvid}`);
            if (settings.addMessageJumpRemoveAdd) {
                addMessageJumpRemoveAdd(el.id, el.bvid, true);
            }
        });
    }

    async function apiURL1DetectionScope2(fid, defaultFilter, controller, spanDetect) {

        const AVBVsFavlistAll = [];
        const BVsFavlistVisable = [];
        let newPageNumber = (detectionScope2Range.apiURL1DetectionScope2RangeStart - 1) / 1000 + 1;

        while (newPageNumber * 1000 <= detectionScope2Range.apiURL1DetectionScope2RangeEnd) {
            if (controller.signal.aborted) {
                throw new DOMException('', 'AbortError');
            }

            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://api.bilibili.com/x/v3/fav/resource/ids?media_id=${fid}&pn=${newPageNumber}&platform=web`,
                    timeout: 5000,
                    responseType: 'json',
                    onload: (res) => resolve(res),
                    onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/resource/ids', res.error]),
                    ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/resource/ids'])
                });
            });
            if (response.status !== 200) {
                throw ['请求失败', 'api.bilibili.com/x/v3/fav/resource/ids', `${response.status} ${response.statusText}`];
            }

            if (!response.response.data.length) {
                break;
            }

            AVBVsFavlistAll.push(...response.response.data);

            let pn = (newPageNumber - 1) * 25 + 1;
            let finish = false;
            while (true) {
                if (controller.signal.aborted) {
                    throw new DOMException('', 'AbortError');
                }

                const response = await new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: `https://api.bilibili.com/x/v3/fav/resource/list?media_id=${fid}&pn=${pn}&ps=40&keyword=&order=mtime&type=0&tid=0&platform=web` + (newFreshSpace ? '&web_location=333.1387' : ''),
                        timeout: 5000,
                        responseType: 'json',
                        onload: (res) => resolve(res),
                        onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/resource/list', res.error]),
                        ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/resource/list'])
                    });
                });
                if (response.status !== 200) {
                    throw ['请求失败', 'api.bilibili.com/x/v3/fav/resource/list', `${response.status} ${response.statusText}`];
                }

                if (response.response.data.medias) {
                    BVsFavlistVisable.push(...response.response.data.medias.map(el => el.bvid));
                }
                spanDetect.textContent = ` (${BVsFavlistVisable.length} / ${detectionScope2Range.apiURL1DetectionScope2RangeEnd - detectionScope2Range.apiURL1DetectionScope2RangeStart + 1})`;
                if (!response.response.data.has_more) {
                    finish = true;
                    break;
                }
                if (pn === newPageNumber * 25) {
                    break;
                }
                pn++;

                await delay(settings.apiDelay);
            }

            if (finish) {
                break;
            }
            newPageNumber++;
        }

        setTimeout(() => {
            spanDetect.textContent = '';
        }, 1000);

        const AVBVsFavlistHidden = AVBVsFavlistAll.filter(el => !BVsFavlistVisable.includes(el.bvid) && el.type === 2);
        if (!AVBVsFavlistHidden.length) {
            addMessage('没有找到隐藏的视频', false, 'green');
            return;
        }
        let count = 1;
        AVBVsFavlistHidden.forEach(el => {
            if (defaultFilter) {
                addMessage(`第 ${Math.floor((AVBVsFavlistAll.findIndex(ele => ele.bvid === el.bvid) + detectionScope2Range.apiURL1DetectionScope2RangeStart - 1) / pageSize) + 1} 页:`, false, 'green');
            } else {
                addMessage(`第 ${count++} 个:`, false, 'green');
            }
            addMessage(`AV号: ${el.id}`);
            addMessage(`BV号: ${el.bvid}`);
            if (settings.addMessageJumpRemoveAdd) {
                addMessageJumpRemoveAdd(el.id, el.bvid, true);
            }
        });
    }

    async function apiURL2DetectionScope1(fid, pageNumber) {

        const newRequests = [];
        const oldPageStart = (pageNumber - 1) * pageSize;
        const oldPageEnd = oldPageStart + pageSize;
        let newPageNumber = Math.floor(oldPageStart / 20) + 1;
        while ((newPageNumber - 1) * 20 < oldPageEnd) {
            const newPageStart = (newPageNumber - 1) * 20;
            const newPageEnd = newPageStart + 20;
            const sliceStart = Math.max(oldPageStart, newPageStart);
            const sliceEnd = Math.min(oldPageEnd, newPageEnd);
            newRequests.push({ newPageNumber, sliceStart, sliceEnd });
            newPageNumber++;
        }

        const InfosPageAll = [];
        for (const newRequest of newRequests) {
            const urlWithParams = await appendParamsForApiURL2(fid, newRequest.newPageNumber, 20);
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: urlWithParams,
                    timeout: 5000,
                    responseType: 'json',
                    onload: (res) => resolve(res),
                    onerror: (res) => reject(['请求失败', 'api.bilibili.com/medialist/gateway/base/spaceDetail', res.error]),
                    ontimeout: () => reject(['请求超时', 'api.bilibili.com/medialist/gateway/base/spaceDetail'])
                });
            });
            if (response.status !== 200) {
                throw ['请求失败', 'api.bilibili.com/medialist/gateway/base/spaceDetail', `${response.status} ${response.statusText}`];
            }

            if (response.response.code === -403 || response.response.code === 7201004) {
                throw ['不支持处理私密收藏夹'];
            } else if (response.response.code) {
                throw ['发生未知错误, 请反馈该问题', JSON.stringify(response.response)];
            }

            if (!response.response.data.medias) {
                break;
            }

            const sliceStart = newRequest.sliceStart - (newRequest.newPageNumber - 1) * 20;
            const sliceEnd = newRequest.sliceEnd - (newRequest.newPageNumber - 1) * 20;
            InfosPageAll.push(...response.response.data.medias.slice(sliceStart, sliceEnd));
        }

        const videosPageVisable = document.querySelectorAll(newFreshSpace ? 'div.items__item' : 'li.small-item');
        let BVsPageVisable;
        if (newFreshSpace) {
            BVsPageVisable = Array.from(videosPageVisable).map(el => el.querySelector('a').getAttribute('href').match(getBVFromURLRegex)[1]);
        } else {
            BVsPageVisable = Array.from(videosPageVisable).map(el => el.getAttribute('data-aid'));
        }

        const InfosPageHidden = InfosPageAll.filter(el => !BVsPageVisable.includes(el.bvid));
        if (!InfosPageHidden.length) {
            addMessage('没有找到隐藏的视频', false, 'green');
            return;
        }
        InfosPageHidden.forEach(el => {
            addMessage(`在当前页的位置: ${InfosPageAll.findIndex(ele => ele.bvid === el.bvid) + 1}`, false, 'green');
            if (el.cover && !el.cover.includes('be27fd62c99036dce67efface486fb0a88ffed06')) {
                addMessage(`<img src="//${el.cover.replace(getHttpsFromURLRegex, '').replace(getAvifFromURLRegex, '')}@672w_378h_1c.avif" style="max-width: 100%; height: auto;">`);
            }
            if (el.title && el.title !== '已失效视频') {
                addMessage(`标题: ${el.title}`);
            }
            addMessage(`AV号: ${el.id}`);
            addMessage(`BV号: ${el.bvid}`);
            addMessage(`简介: ${el.intro}`);
            addMessage(`UP主: <a href="https://space.bilibili.com/${el.upper.mid}" target="_blank" style="text-decoration-line: underline;">${el.upper.name}</a>`, true);
            addMessage(`上传时间: ${new Date(1000 * el.ctime).toLocaleString()}`, true);
            addMessage(`发布时间: ${new Date(1000 * el.pubtime).toLocaleString()}`, true);
            addMessage(`收藏时间: ${new Date(1000 * el.fav_time).toLocaleString()}`, true);
            if (Array.isArray(el.pages)) {
                el.pages.forEach(ele => {
                    addMessage(`分集${ele.page}: cid: ${ele.id}`, true);
                    addMessage(`标题: ${ele.title}`);
                });
            }
            if (settings.addMessageJumpRemoveAdd) {
                addMessageJumpRemoveAdd(el.id, el.bvid, true);
            }
        });
    }

    async function apiURL2DetectionScope2(fid, defaultFilter, controller, spanDetect) {

        const InfosPageAll = [];
        const BVsFavlistVisable = [];
        let newPageNumber = (detectionScope2Range.apiURL2DetectionScope2RangeStart - 1) / 40 + 1;
        let finish = false;

        while (newPageNumber * 40 <= detectionScope2Range.apiURL2DetectionScope2RangeEnd) {
            if (controller.signal.aborted) {
                throw new DOMException('', 'AbortError');
            }

            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://api.bilibili.com/x/v3/fav/resource/list?media_id=${fid}&pn=${newPageNumber}&ps=40&keyword=&order=mtime&type=0&tid=0&platform=web` + (newFreshSpace ? '&web_location=333.1387' : ''),
                    timeout: 5000,
                    responseType: 'json',
                    onload: (res) => resolve(res),
                    onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/resource/list', res.error]),
                    ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/resource/list'])
                });
            });
            if (response.status !== 200) {
                throw ['请求失败', 'api.bilibili.com/x/v3/fav/resource/list', `${response.status} ${response.statusText}`];
            }

            if (response.response.data.medias) {
                BVsFavlistVisable.push(...response.response.data.medias.map(el => el.bvid));
            }
            if (!response.response.data.has_more) {
                finish = true;
            }

            if (controller.signal.aborted) {
                throw new DOMException('', 'AbortError');
            }

            const response1 = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://api.bilibili.com/medialist/gateway/base/spaceDetail?media_id=${fid}&pn=${newPageNumber * 2 - 1}&ps=20&keyword=&order=mtime&type=0&tid=0&jsonp=jsonp`,
                    timeout: 5000,
                    responseType: 'json',
                    onload: (res) => resolve(res),
                    onerror: (res) => reject(['请求失败', 'api.bilibili.com/medialist/gateway/base/spaceDetail', res.error]),
                    ontimeout: () => reject(['请求超时', 'api.bilibili.com/medialist/gateway/base/spaceDetail'])
                });
            });
            if (response1.status !== 200) {
                throw ['请求失败', 'api.bilibili.com/medialist/gateway/base/spaceDetail', `${response1.status} ${response1.statusText}`];
            }

            if (response1.response.code === -403 || response1.response.code === 7201004) {
                throw ['不支持处理私密收藏夹'];
            } else if (response1.response.code) {
                throw ['发生未知错误, 请反馈该问题', JSON.stringify(response1.response)];
            }

            if (!response1.response.data.medias) {
                break;
            }
            InfosPageAll.push(...response1.response.data.medias);

            if (controller.signal.aborted) {
                throw new DOMException('', 'AbortError');
            }

            const response2 = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://api.bilibili.com/medialist/gateway/base/spaceDetail?media_id=${fid}&pn=${newPageNumber * 2}&ps=20&keyword=&order=mtime&type=0&tid=0&jsonp=jsonp`,
                    timeout: 5000,
                    responseType: 'json',
                    onload: (res) => resolve(res),
                    onerror: (res) => reject(['请求失败', 'api.bilibili.com/medialist/gateway/base/spaceDetail', res.error]),
                    ontimeout: () => reject(['请求超时', 'api.bilibili.com/medialist/gateway/base/spaceDetail'])
                });
            });
            if (response2.status !== 200) {
                throw ['请求失败', 'api.bilibili.com/medialist/gateway/base/spaceDetail', `${response2.status} ${response2.statusText}`];
            }

            if (response2.response.code === -403 || response2.response.code === 7201004) {
                throw ['不支持处理私密收藏夹'];
            } else if (response2.response.code) {
                throw ['发生未知错误, 请反馈该问题', JSON.stringify(response2.response)];
            }

            spanDetect.textContent = ` (${BVsFavlistVisable.length} / ${detectionScope2Range.apiURL2DetectionScope2RangeEnd - detectionScope2Range.apiURL2DetectionScope2RangeStart + 1})`;

            if (!response2.response.data.medias) {
                break;
            }
            InfosPageAll.push(...response2.response.data.medias);

            if (finish) {
                break;
            }
            newPageNumber++;

            await delay(settings.apiDelay);
        }

        setTimeout(() => {
            spanDetect.textContent = '';
        }, 1000);

        const InfosPageHidden = InfosPageAll.filter(el => !BVsFavlistVisable.includes(el.bvid));
        if (!InfosPageHidden.length) {
            addMessage('没有找到隐藏的视频', false, 'green');
            return;
        }

        let count = 1;
        InfosPageHidden.forEach(el => {
            if (defaultFilter) {
                addMessage(`第 ${Math.floor((InfosPageAll.findIndex(ele => ele.bvid === el.bvid) + detectionScope2Range.apiURL2DetectionScope2RangeStart - 1) / pageSize) + 1} 页:`, false, 'green');
            } else {
                addMessage(`第 ${count++} 个:`, false, 'green');
            }

            if (el.cover && !el.cover.includes('be27fd62c99036dce67efface486fb0a88ffed06')) {
                addMessage(`<img src="//${el.cover.replace(getHttpsFromURLRegex, '').replace(getAvifFromURLRegex, '')}@672w_378h_1c.avif" style="max-width: 100%; height: auto;">`);
            }
            if (el.title && el.title !== '已失效视频') {
                addMessage(`标题: ${el.title}`);
            }
            addMessage(`AV号: ${el.id}`);
            addMessage(`BV号: ${el.bvid}`);
            addMessage(`简介: ${el.intro}`);
            addMessage(`UP主: <a href="https://space.bilibili.com/${el.upper.mid}" target="_blank" style="text-decoration-line: underline;">${el.upper.name}</a>`, true);
            addMessage(`上传时间: ${new Date(1000 * el.ctime).toLocaleString()}`, true);
            addMessage(`发布时间: ${new Date(1000 * el.pubtime).toLocaleString()}`, true);
            addMessage(`收藏时间: ${new Date(1000 * el.fav_time).toLocaleString()}`, true);
            if (Array.isArray(el.pages)) {
                el.pages.forEach(ele => {
                    addMessage(`分集${ele.page}: cid: ${ele.id}`, true);
                    addMessage(`标题: ${ele.title}`);
                });
            }
            if (settings.addMessageJumpRemoveAdd) {
                addMessageJumpRemoveAdd(el.id, el.bvid, true);
            }
        });
    }

    function jump(BV) {

        GM_openInTab(`https://www.biliplus.com/video/${BV}`, { active: true, insert: false, setParent: true });
        GM_openInTab(`https://xbeibeix.com/video/${BV}`, { insert: false, setParent: true });
        GM_openInTab(`https://www.jijidown.com/video/${BV}`, { insert: false, setParent: true });
    }

    async function remove(AV, cookies, spanRemove) {

        let fid;
        if (newFreshSpace) {
            const getFidFromURLMatch = location.href.match(getFidFromURLRegex);
            if (getFidFromURLMatch) {
                fid = parseInt(getFidFromURLMatch[1], 10);
            } else {
                throw ['无法获取当前收藏夹的fid, 刷新页面可能有帮助'];
            }
        } else {
            fid = document.querySelector('.fav-item.cur').getAttribute('fid');
        }

        const csrf = cookies[0].value;
        const data = `resources=${AV}:2&media_id=${fid}&platform=web&csrf=${csrf}`;
        const response = await new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: 'https://api.bilibili.com/x/v3/fav/resource/batch-del',
                data: data,
                timeout: 5000,
                responseType: 'json',
                headers: {
                    'Content-Length': data.length,
                    'Content-Type': 'application/x-www-form-urlencoded'
                },
                onload: (res) => resolve(res),
                onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/resource/batch-del', res.error]),
                ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/resource/batch-del'])
            });
        });
        if (response.status !== 200) {
            throw ['请求失败', 'api.bilibili.com/x/v3/fav/resource/batch-del', `${response.status} ${response.statusText}`];
        }

        console.log(response.response);
        spanRemove.textContent = ' ✔';
        setTimeout(() => {
            spanRemove.textContent = '';
        }, 1000);
    }

    async function add(AV, cookies, spanAdd) {

        let fid;
        if (newFreshSpace) {
            const getFidFromURLMatch = location.href.match(getFidFromURLRegex);
            if (getFidFromURLMatch) {
                fid = parseInt(getFidFromURLMatch[1], 10);
            } else {
                throw ['无法获取当前收藏夹的fid, 刷新页面可能有帮助'];
            }
        } else {
            fid = document.querySelector('.fav-item.cur').getAttribute('fid');
        }

        const csrf = cookies[0].value;
        const data = `rid=${AV}&type=2&add_media_ids=${fid}&platform=web&csrf=${csrf}`;
        const response = await new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: 'https://api.bilibili.com/x/v3/fav/resource/deal',
                data: data,
                timeout: 5000,
                responseType: 'json',
                headers: {
                    'Content-Length': data.length,
                    'Content-Type': 'application/x-www-form-urlencoded'
                },
                onload: (res) => resolve(res),
                onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/resource/deal', res.error]),
                ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/resource/deal'])
            });
        });
        if (response.status !== 200) {
            throw ['请求失败', 'api.bilibili.com/x/v3/fav/resource/deal', `${response.status} ${response.statusText}`];
        }

        console.log(response.response);
        spanAdd.textContent = ' ✔';
        setTimeout(() => {
            spanAdd.textContent = '';
        }, 1000);
    }

    function addMessage(msg, smallFontSize, border) {
        let px;
        if (smallFontSize) {
            px = newFreshSpace ? 11 : 10;
        } else {
            px = newFreshSpace ? 13 : 12;
        }
        const p = document.createElement('p');
        p.innerHTML = msg;
        p.style.fontSize = `${px}px`;
        if (border === 'red') {
            p.style.borderTop = '1px solid #800000';
        } else if (border === 'green') {
            p.style.borderTop = '1px solid #008000';
        }
        divMessage.appendChild(p);

        if (!divMessageHeightFixed && (divMessage.scrollHeight > settings.divMessageHeight * (newFreshSpace ? 20 : 18))) {
            divMessage.classList.add('detect-divMessage-heightFixed' + classAppendNewFreshSpace);
            divMessageHeightFixed = true;
        }

        p.scrollIntoView({ behavior: 'instant', block: 'nearest' });
    }

    function addMessageJumpRemoveAdd(AV, BV, smallFontSize, border) {
        let px;
        if (smallFontSize) {
            px = newFreshSpace ? 11 : 10;
        } else {
            px = newFreshSpace ? 13 : 12;
        }
        const p = document.createElement('p');
        p.style.fontSize = `${px}px`;
        if (border === 'red') {
            p.style.borderTop = '1px solid #800000';
        } else if (border === 'green') {
            p.style.borderTop = '1px solid #008000';
        }
        divMessage.appendChild(p);

        const spanButtonJump = document.createElement('span');
        spanButtonJump.textContent = '查询视频信息';
        spanButtonJump.style.textDecorationLine = 'underline';
        spanButtonJump.style.cursor = 'pointer';
        spanButtonJump.addEventListener('click', () => {
            try {
                jump(BV);
            } catch (error) {
                if (error instanceof Error) {
                    catchUnknownError(error);
                } else {
                    addMessage(error[0], false, 'red');
                    for (let i = 1; i < error.length; i++) {
                        addMessage(error[i], true);
                    }
                }
            }
        });
        p.appendChild(spanButtonJump);

        p.appendChild(document.createTextNode(' '));

        const spanButtonRemove = document.createElement('span');
        spanButtonRemove.textContent = '取消收藏';
        spanButtonRemove.style.textDecorationLine = 'underline';
        spanButtonRemove.style.cursor = 'pointer';
        spanButtonRemove.addEventListener('click', () => {
            try {
                GM_cookie.list({ name: 'bili_jct' }, async (cookies, error) => {
                    if (error) {
                        throw ['无法读取cookie, 更新Tampermonkey可能有帮助'];
                    }

                    try {
                        await remove(AV, cookies, spanRemove);
                    } catch (error) {
                        if (error instanceof Error) {
                            catchUnknownError(error);
                        } else {
                            addMessage(error[0], false, 'red');
                            for (let i = 1; i < error.length; i++) {
                                addMessage(error[i], true);
                            }
                        }
                    }
                });

            } catch (error) {
                catchUnknownError(error);
            }
        });
        p.appendChild(spanButtonRemove);

        const spanRemove = document.createElement('span');
        p.appendChild(spanRemove);

        p.appendChild(document.createTextNode(' '));

        const spanButtonAdd = document.createElement('span');
        spanButtonAdd.textContent = '添加收藏';
        spanButtonAdd.style.textDecorationLine = 'underline';
        spanButtonAdd.style.cursor = 'pointer';
        spanButtonAdd.addEventListener('click', () => {
            try {
                GM_cookie.list({ name: 'bili_jct' }, async (cookies, error) => {
                    if (error) {
                        throw ['无法读取cookie, 更新Tampermonkey可能有帮助'];
                    }

                    try {
                        await add(AV, cookies, spanAdd);
                    } catch (error) {
                        if (error instanceof Error) {
                            catchUnknownError(error);
                        } else {
                            addMessage(error[0], false, 'red');
                            for (let i = 1; i < error.length; i++) {
                                addMessage(error[i], true);
                            }
                        }
                    }
                });

            } catch (error) {
                catchUnknownError(error);
            }
        });
        p.appendChild(spanButtonAdd);

        const spanAdd = document.createElement('span');
        p.appendChild(spanAdd);

        if (!divMessageHeightFixed && (divMessage.scrollHeight > settings.divMessageHeight * (newFreshSpace ? 20 : 18))) {
            divMessage.classList.add('detect-divMessage-heightFixed' + classAppendNewFreshSpace);
            divMessageHeightFixed = true;
        }

        p.scrollIntoView({ behavior: 'instant', block: 'nearest' });
    }

    function clearMessage() {
        while (divMessage.firstChild) {
            divMessage.removeChild(divMessage.firstChild);
        }
        divMessage.classList.remove('detect-divMessage-heightFixed' + classAppendNewFreshSpace);
        divMessageHeightFixed = false;
    }

    function catchUnknownError(error) {
        addMessage('发生未知错误, 请反馈该问题', false, 'red');
        addMessage(error.stack, true);
        console.error(error);
    }

    function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    function abortActiveControllers() {
        for (const controller of activeControllers) {
            controller.abort();
        }
    }

    async function appendParamsForApiURL2(fid, pageNumber, pageSize) {
        const inputKeyword = document.querySelector(newFreshSpace ? 'input.fav-list-header-filter__search' : 'input.search-fav-input');
        let keyword = '';
        if (inputKeyword) {
            keyword = encodeURIComponent(inputKeyword.value);
        }

        let divFilterOrder;
        let divTid;

        if (!newFreshSpace) {
            const divDropdownFilterItems = document.querySelectorAll('div.fav-filters > div.be-dropdown.filter-item');
            if (divDropdownFilterItems.length === 2) {
                divFilterOrder = divDropdownFilterItems[1].querySelector('span');
                divTid = divDropdownFilterItems[0].querySelector('span');
            } else if (divDropdownFilterItems.length === 1) {
                divFilterOrder = divDropdownFilterItems[0].querySelector('span');
                divTid = null;
            } else {
                divFilterOrder = null;
                divTid = null;
            }
        }

        if (!newFreshSpace) {
            let orderText = '收藏';
            if (divFilterOrder) {
                orderText = divFilterOrder.textContent;
            }
            if (orderText.includes('收藏')) {
                order = 'mtime';
            } else if (orderText.includes('播放')) {
                order = 'view';
            } else if (orderText.includes('投稿')) {
                order = 'pubtime';
            } else {
                throw ['无法确定各个视频的排序方式, 请反馈该问题'];
            }
        }

        const divType = document.querySelector(newFreshSpace ? 'div.vui_input__prepend' : 'div.search-types > div');
        let typeText = '当前';
        if (divType) {
            typeText = divType.textContent;
        }
        if (!keyword) {
            typeText = '当前';
        }
        let type;
        if (typeText.includes('当前')) {
            type = 0;
        } else if (typeText.includes('全部')) {
            type = 1;
        } else {
            throw ['无法确定搜索的范围为当前收藏夹还是全部收藏夹, 请反馈该问题'];
        }

        if (newFreshSpace) {
            divTid = document.querySelector('div.fav-list-header-collapse div.radio-filter__item--active');
        }
        let tidText = '全部分区';
        if (divTid) {
            tidText = divTid.innerText;
        }
        let tid;
        if (tidText.includes('全部')) {
            tid = 0;
        } else {
            const UID = parseInt(location.href.match(getUIDFromURLRegex)[1], 10);
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://api.bilibili.com/x/v3/fav/resource/partition?up_mid=${UID}&media_id=${fid}` + (newFreshSpace ? '&web_location=333.1387' : ''),
                    timeout: 5000,
                    responseType: 'json',
                    onload: (res) => resolve(res),
                    onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/resource/partition', res.error]),
                    ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/resource/partition'])
                });
            });
            if (response.status !== 200) {
                throw ['请求失败', 'api.bilibili.com/x/v3/fav/resource/partition', `${response.status} ${response.statusText}`];
            }

            const found = response.response.data.find(el => tidText.includes(el.name));
            if (found) {
                tid = found.tid;
            } else {
                throw ['无法确定选择的分区, 请反馈该问题'];
            }
        }

        return (`https://api.bilibili.com/medialist/gateway/base/spaceDetail?media_id=${fid}&pn=${pageNumber}&ps=${pageSize}&keyword=${keyword}&order=${order}&type=${type}&tid=${tid}&jsonp=jsonp`);
    }

    function validateInputText(event) {
        try {
            const inputText = event.target;
            let value = inputText.value.trim();
            if (!value || isNaN(value)) {
                value = Number(inputText.getAttribute('detect-def'));
            } else {
                value = Math.floor(Number(value));
                if (value < Number(inputText.getAttribute('detect-min'))) {
                    value = Number(inputText.getAttribute('detect-min'));
                } else if (value > Number(inputText.getAttribute('detect-max'))) {
                    value = Number(inputText.getAttribute('detect-max'));
                }
            }
            inputText.value = value;
            settings[(inputText.getAttribute('detect-setting'))] = value;
            GM_setValue('settings', settings);
        } catch (error) {
            catchUnknownError(error);
        }
    }

    function validateInputTextRangeStart(event) {
        try {
            const inputTextRangeStart = event.target;
            const inputTextRangeEnd = inputTextRangeStart.parentNode.nextElementSibling.childNodes[1];
            const keyRangeStart = inputTextRangeStart.getAttribute('detect-setting');
            const keyRangeEnd = inputTextRangeEnd.getAttribute('detect-setting');
            const step = Number(inputTextRangeStart.getAttribute('detect-step'));
            let value = inputTextRangeStart.value.trim();
            if (!value || isNaN(value)) {
                value = Number(inputTextRangeStart.getAttribute('detect-def'));
                inputTextRangeEnd.value = detectionScope2Range[keyRangeEnd] = Number(inputTextRangeEnd.getAttribute('detect-def'));
            } else {
                value = Math.floor(Number(value));
                if (value < Number(inputTextRangeStart.getAttribute('detect-min'))) {
                    value = Number(inputTextRangeStart.getAttribute('detect-min'));
                } else if (value > Number(inputTextRangeStart.getAttribute('detect-max'))) {
                    value = Number(inputTextRangeStart.getAttribute('detect-max'));
                }
            }
            value -= ((value - 1) % step);
            if (value > detectionScope2Range[keyRangeEnd]) {
                if (value - detectionScope2Range[keyRangeEnd] === 1) {
                    inputTextRangeEnd.value = detectionScope2Range[keyRangeEnd] = Math.min(value + detectionScope2Range[keyRangeEnd] - detectionScope2Range[keyRangeStart], Number(inputTextRangeEnd.getAttribute('detect-max')));
                } else {
                    inputTextRangeEnd.value = detectionScope2Range[keyRangeEnd] = value + step - 1;
                }
            }
            inputTextRangeStart.value = detectionScope2Range[keyRangeStart] = value;
        } catch (error) {
            catchUnknownError(error);
        }
    }

    function validateInputTextRangeEnd(event) {
        try {
            const inputTextRangeEnd = event.target;
            const inputTextRangeStart = inputTextRangeEnd.parentNode.previousElementSibling.childNodes[1];
            const keyRangeEnd = inputTextRangeEnd.getAttribute('detect-setting');
            const keyRangeStart = inputTextRangeStart.getAttribute('detect-setting');
            const step = Number(inputTextRangeEnd.getAttribute('detect-step'));
            let value = inputTextRangeEnd.value.trim();
            if (!value || isNaN(value)) {
                value = Number(inputTextRangeEnd.getAttribute('detect-def'));
                inputTextRangeStart.value = detectionScope2Range[keyRangeStart] = Number(inputTextRangeStart.getAttribute('detect-def'));
            } else {
                value = Math.floor(Number(value));
                if (value < Number(inputTextRangeEnd.getAttribute('detect-min'))) {
                    value = Number(inputTextRangeEnd.getAttribute('detect-min'));
                } else if (value > Number(inputTextRangeEnd.getAttribute('detect-max'))) {
                    value = Number(inputTextRangeEnd.getAttribute('detect-max'));
                }
            }
            value = Math.ceil(value / step) * step;
            if (value < detectionScope2Range[keyRangeStart]) {
                if (detectionScope2Range[keyRangeStart] - value === 1) {
                    inputTextRangeStart.value = detectionScope2Range[keyRangeStart] = Math.max(value - detectionScope2Range[keyRangeEnd] + detectionScope2Range[keyRangeStart], Number(inputTextRangeStart.getAttribute('detect-min')));
                } else {
                    inputTextRangeStart.value = detectionScope2Range[keyRangeStart] = value - step + 1;
                }
            }
            inputTextRangeEnd.value = detectionScope2Range[keyRangeEnd] = value;
        } catch (error) {
            catchUnknownError(error);
        }
    }

    function validateInputTextDivMessageHeight(event) {
        try {
            const inputText = event.target;
            let value = inputText.value.trim();
            if (!value || isNaN(value)) {
                value = Number(inputText.getAttribute('detect-def'));
            } else {
                value = Math.floor(Number(value));
                if (value < Number(inputText.getAttribute('detect-min'))) {
                    value = Number(inputText.getAttribute('detect-min'));
                } else if (value > Number(inputText.getAttribute('detect-max'))) {
                    value = Number(inputText.getAttribute('detect-max'));
                }
            }
            inputText.value = value;
            settings[(inputText.getAttribute('detect-setting'))] = value;
            GM_setValue('settings', settings);
            document.querySelector('#detect-style-divMessage-heightFixed').textContent = `
                .detect-divMessage-heightFixed {
                    height: ${value * 18}px;
                }
                .detect-divMessage-heightFixed-newFreshSpace {
                    height: ${value * 20}px;
                }
            `;
            if (divMessage.scrollHeight > value * (newFreshSpace ? 20 : 18)) {
                divMessage.classList.add('detect-divMessage-heightFixed' + classAppendNewFreshSpace);
                divMessageHeightFixed = true;
            } else {
                divMessage.classList.remove('detect-divMessage-heightFixed' + classAppendNewFreshSpace);
                divMessageHeightFixed = false;
            }
        } catch (error) {
            catchUnknownError(error);
        }
    }

    const XOR_CODE = 23442827791579n;
    const MASK_CODE = 2251799813685247n;
    const MAX_AID = 1n << 51n;
    const BASE = 58n;
    const data = 'FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf';

    function av2bv(aid) {
        const bytes = ['B', 'V', '1', '0', '0', '0', '0', '0', '0', '0', '0', '0'];
        let bvIndex = bytes.length - 1;
        let tmp = (MAX_AID | BigInt(aid)) ^ XOR_CODE;
        while (tmp > 0) {
            bytes[bvIndex] = data[Number(tmp % BigInt(BASE))];
            tmp = tmp / BASE;
            bvIndex -= 1;
        }
        [bytes[3], bytes[9]] = [bytes[9], bytes[3]];
        [bytes[4], bytes[7]] = [bytes[7], bytes[4]];
        return bytes.join('');
    }

    function bv2av(bvid) {
        const bvidArr = Array.from(bvid);
        [bvidArr[3], bvidArr[9]] = [bvidArr[9], bvidArr[3]];
        [bvidArr[4], bvidArr[7]] = [bvidArr[7], bvidArr[4]];
        bvidArr.splice(0, 3);
        const tmp = bvidArr.reduce((pre, bvidChar) => pre * BASE + BigInt(data.indexOf(bvidChar)), 0n);
        return Number((tmp & MASK_CODE) ^ XOR_CODE);
    }
})();

QingJ © 2025

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