哔哩哔哩(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           11
// @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+)/;

    let onFavlistPage = false;
    let newFreshSpace;
    let classAppendNewFreshSpace;
    let pageSize;
    let divMessageHeightFixed = false;
    let order = 'mtime';

    const settings = GM_getValue('settings', {
        apiURL: 1,
        apiURL1DetectionScope: 'page',
        autoClearMessage: true,
    });

    if (GM_getValue('detectionScope', '')) {
        settings.apiURL1DetectionScope = GM_getValue('detectionScope', '');
        GM_setValue('detectionScope', '');
        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;
            main();
            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;
            main();
            return;
        }
    });

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

    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) {
                onFavlistPage = false;
                sideObserver.disconnect();
            }
        }
    }

    function main() {

        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-inputText, .detect-inputText-newFreshSpace {
                box-sizing: content-box;
                border: 1px solid #cccccc;
                line-height: 1;
            }
            .detect-inputText {
                width: 140px;
                height: 14px;
                border-radius: 2px;
                padding: 2px;
                font-size: 14px;
            }
            .detect-inputText-newFreshSpace {
                width: 160px;
                height: 16px;
                border-radius: 3px;
                padding: 3px;
                font-size: 16px;
            }
            .detect-button, .detect-button-newFreshSpace {
                border: 1px solid #cccccc;
                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-disabled, .detect-disabled-newFreshSpace {
                opacity: 0.5;
                pointer-events: none;
            }
            .detect-divMessage, .detect-divMessage-newFreshSpace {
                overflow-y: auto;
                background-color: #eeeeee;
                line-height: 1.5;
                scrollbar-width: none;
            }
            .detect-divMessage {
                margin: 2px;
            }
            .detect-divMessage-heightFixed {
                height: 350px;
            }
            .detect-divMessage::-webkit-scrollbar {
                display: none;
            }
            .detect-divMessage-newFreshSpace {
                margin: 2px 0;
            }
            .detect-divMessage-heightFixed-newFreshSpace {
                height: 400px;
            }
            .detect-divMessage-newFreshSpace::-webkit-scrollbar {
                display: none;
            }
        `;
        document.head.appendChild(style);

        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);
        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-inputText' + 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 () => {
            try {
                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.innerText, 10);
                    }
                } else {
                    pageNumber = parseInt(document.querySelector('li.be-pager-item-active > a').innerText, 10);
                }

                if (settings.apiURL === 1) {
                    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.apiURL1DetectionScope === 'page') {
                        if (!defaultFilter) {
                            throw ['请刷新页面, 恢复默认的排序方式和筛选条件'];
                        }
                        if ((pageNumber - 1) * pageSize > 999) {
                            throw ['仅能获取按最近收藏排序的前1000个视频'];
                        }

                        const response = await new Promise((resolve, reject) => {
                            GM.xmlHttpRequest({
                                method: 'GET',
                                url: `https://api.bilibili.com/x/v3/fav/resource/ids?media_id=${fid}&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'])
                            });
                        });
                        const AVBVsFavlistAll = response.response.data;

                        const sliceStart = (pageNumber - 1) * pageSize;
                        const AVBVsPageAll = AVBVsFavlistAll.slice(sliceStart, sliceStart + pageSize);

                        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}`);
                        });

                    } else {
                        const response = await new Promise((resolve, reject) => {
                            GM.xmlHttpRequest({
                                method: 'GET',
                                url: `https://api.bilibili.com/x/v3/fav/resource/ids?media_id=${fid}&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'])
                            });
                        });
                        const AVBVsFavlistAll = response.response.data;

                        let pageNumber = 1;
                        let BVsFavlistVisable = [];
                        while (true) {
                            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=${pageNumber}&ps=40&platform=web`,
                                    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.response.data.info.media_count) {
                                addMessage('没有找到隐藏的视频', false, 'green');
                                return;
                            }
                            if (response.response.data.medias) {
                                BVsFavlistVisable.push(...response.response.data.medias.map(el => el.bvid));
                            }
                            addMessage(`已获取可见视频个数: ${BVsFavlistVisable.length}`, true);
                            if (!response.response.data.has_more) {
                                break;
                            }
                            if (pageNumber === 25) {
                                addMessage('仅能获取按最近收藏排序的前1000个视频', false, 'red');
                                break;
                            }
                            pageNumber++;
                        }

                        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) / pageSize) + 1} 页:`, false, 'green');
                            } else {
                                addMessage(`第 ${count++} 个:`, false, 'green');
                            }
                            addMessage(`AV号: ${el.id}`);
                            addMessage(`BV号: ${el.bvid}`);
                        });
                    }

                } else {
                    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(async (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.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');
                        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}`);
                            });
                        }
                    });
                }

            } 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);
                    }
                }
            }
        });
        divButtonDetect.appendChild(buttonDetect);

        const divSwitchApiURL1 = document.createElement('div');
        divSwitchApiURL1.classList.add('detect-div-first' + classAppendNewFreshSpace);
        divSwitchApiURL1.setAttribute('title',
            '地址: https://api.bilibili.com/x/v3/fav/resource/ids?media_id={收藏夹fid}&platform=web\n' +
            '数据: 当前收藏夹内按最近收藏排序的前1000个视频的AV号和BV号');
        divControls.appendChild(divSwitchApiURL1);

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

        const radioApiURL1 = document.createElement('input');
        radioApiURL1.type = 'radio';
        radioApiURL1.name = 'apiURL';
        radioApiURL1.value = 1;
        radioApiURL1.checked = settings.apiURL === 1;
        radioApiURL1.addEventListener('change', () => {
            try {
                settings.apiURL = 1;
                divSwitchApiURL1DetectionScope1.classList.remove('detect-disabled' + classAppendNewFreshSpace);
                divSwitchApiURL1DetectionScope2.classList.remove('detect-disabled' + classAppendNewFreshSpace);
                divSwitchApiURL2DetectionScope1.classList.add('detect-disabled' + classAppendNewFreshSpace);
                GM_setValue('settings', settings);
            } catch (error) {
                catchUnknownError(error);
            }
        });
        labelApiURL1.insertAdjacentElement('afterbegin', radioApiURL1);

        const divSwitchApiURL1DetectionScope1 = document.createElement('div');
        divSwitchApiURL1DetectionScope1.classList.add('detect-div-second' + classAppendNewFreshSpace);
        divSwitchApiURL1DetectionScope1.setAttribute('title',
            '请确保当前是默认的排序方式和筛选条件。\n' +
            '如果您刚刚在当前收藏夹添加或移除了视频, 请刷新页面后再使用此功能。\n' +
            '仅能获取按最近收藏排序的前1000个视频。如果某个隐藏视频在这个范围之外, 请将该视频之前的一些视频移出该收藏夹。\n' +
            '如果您正在使用的其他脚本或插件修改了视频封面和标题的链接地址, 请将其关闭后刷新页面再使用此功能。');
        divControls.appendChild(divSwitchApiURL1DetectionScope1);

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

        const radioApiURL1DetectionScope1 = document.createElement('input');
        radioApiURL1DetectionScope1.type = 'radio';
        radioApiURL1DetectionScope1.name = 'apiURL1DetectionScope';
        radioApiURL1DetectionScope1.value = 'page';
        radioApiURL1DetectionScope1.checked = settings.apiURL1DetectionScope === 'page';
        radioApiURL1DetectionScope1.addEventListener('change', () => {
            try {
                settings.apiURL1DetectionScope = 'page';
                GM_setValue('settings', settings);
            } catch (error) {
                catchUnknownError(error);
            }
        });
        labelApiURL1DetectionScope1.insertAdjacentElement('afterbegin', radioApiURL1DetectionScope1);

        const divSwitchApiURL1DetectionScope2 = document.createElement('div');
        divSwitchApiURL1DetectionScope2.classList.add('detect-div-second' + classAppendNewFreshSpace);
        divSwitchApiURL1DetectionScope2.setAttribute('title',
            '仅能获取按最近收藏排序的前1000个视频。如果某个隐藏视频在这个范围之外, 请将该视频之前的一些视频移出该收藏夹。');
        divControls.appendChild(divSwitchApiURL1DetectionScope2);

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

        const radioApiURL1DetectionScope2 = document.createElement('input');
        radioApiURL1DetectionScope2.type = 'radio';
        radioApiURL1DetectionScope2.name = 'apiURL1DetectionScope';
        radioApiURL1DetectionScope2.value = 'favlist';
        radioApiURL1DetectionScope2.checked = settings.apiURL1DetectionScope === 'favlist';
        radioApiURL1DetectionScope2.addEventListener('change', () => {
            try {
                settings.apiURL1DetectionScope = 'favlist';
                GM_setValue('settings', settings);
            } catch (error) {
                catchUnknownError(error);
            }
        });
        labelApiURL1DetectionScope2.insertAdjacentElement('afterbegin', radioApiURL1DetectionScope2);

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

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

        const radioApiURL2 = document.createElement('input');
        radioApiURL2.type = 'radio';
        radioApiURL2.name = 'apiURL';
        radioApiURL2.value = 2;
        radioApiURL2.checked = settings.apiURL === 2;
        radioApiURL2.addEventListener('change', () => {
            try {
                settings.apiURL = 2;
                divSwitchApiURL2DetectionScope1.classList.remove('detect-disabled' + classAppendNewFreshSpace);
                divSwitchApiURL1DetectionScope1.classList.add('detect-disabled' + classAppendNewFreshSpace);
                divSwitchApiURL1DetectionScope2.classList.add('detect-disabled' + classAppendNewFreshSpace);
                GM_setValue('settings', settings);
            } catch (error) {
                catchUnknownError(error);
            }
        });
        labelApiURL2.insertAdjacentElement('afterbegin', radioApiURL2);

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

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

        const radioApiURL2DetectionScope1 = document.createElement('input');
        radioApiURL2DetectionScope1.type = 'radio';
        radioApiURL2DetectionScope1.name = 'apiURL2DetectionScope';
        radioApiURL2DetectionScope1.value = 'page';
        radioApiURL2DetectionScope1.checked = true;
        labelApiURL2DetectionScope1.insertAdjacentElement('afterbegin', radioApiURL2DetectionScope1);

        if (settings.apiURL === 1) {
            divSwitchApiURL2DetectionScope1.classList.add('detect-disabled' + classAppendNewFreshSpace);
        } else {
            divSwitchApiURL1DetectionScope1.classList.add('detect-disabled' + classAppendNewFreshSpace);
            divSwitchApiURL1DetectionScope2.classList.add('detect-disabled' + classAppendNewFreshSpace);
        }

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

        const buttonJump = document.createElement('button');
        buttonJump.type = 'button';
        buttonJump.classList.add('detect-button' + classAppendNewFreshSpace);
        buttonJump.textContent = '查询视频信息';
        buttonJump.addEventListener('click', () => {
            try {
                const BV = inputTextAVBV.value;
                if (!BVRegex.test(BV)) {
                    throw ['请输入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 });

            } 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号后, 点击此按钮, 将会从当前收藏夹中移除该视频。');
        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)) {
                            throw ['请输入AV号'];
                        }

                        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}%3A2&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,
                                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'])
                            });
                        });
                        addMessage('B站接口响应内容:');
                        addMessage(response.response, true);

                    } 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 divButtonAdd = document.createElement('div');
        divButtonAdd.classList.add('detect-div-first' + classAppendNewFreshSpace);
        divButtonAdd.setAttribute('title',
            '在文本框内输入某个视频的AV号后, 点击此按钮, 将会添加该视频到当前收藏夹的首位。如果当前收藏夹是公开收藏夹, 将收藏夹分享到动态后, 就可以看到该视频的封面。');
        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)) {
                            throw ['请输入AV号'];
                        }

                        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}&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,
                                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'])
                            });
                        });
                        addMessage('B站接口响应内容:');
                        addMessage(response.response, true);

                    } 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 divLabelAutoClearMessage = document.createElement('div');
        divLabelAutoClearMessage.classList.add('detect-div-first' + classAppendNewFreshSpace);
        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.checked = settings.autoClearMessage;
        checkboxAutoClearMessage.addEventListener('change', () => {
            try {
                settings.autoClearMessage = checkboxAutoClearMessage.checked;
                GM_setValue('settings', settings);
            } catch (error) {
                catchUnknownError(error);
            }
        });
        labelAutoClearMessage.insertAdjacentElement('afterbegin', checkboxAutoClearMessage);

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

        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.scrollTop = divMessage.scrollHeight;
            } else {
                if (newFreshSpace) {
                    if (divMessage.scrollHeight > 400) {
                        divMessage.classList.add('detect-divMessage-heightFixed-newFreshSpace');
                        divMessageHeightFixed = true;
                        divMessage.scrollTop = divMessage.scrollHeight;
                    }
                } else {
                    if (divMessage.scrollHeight > 350) {
                        divMessage.classList.add('detect-divMessage-heightFixed');
                        divMessageHeightFixed = true;
                        divMessage.scrollTop = divMessage.scrollHeight;
                    }
                }
            }

            divMessage.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);
        }

        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.innerText;
                }
                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');
            let typeText = '当前';
            if (divType) {
                typeText = divType.innerText;
            }
            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'])
                    });
                });

                const target = response.response.data.find(el => tidText.includes(el.name));
                if (target) {
                    tid = target.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`);
        }
    }
})();

QingJ © 2025

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