您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
自动备份视频信息至本地和第三方网站, 失效视频信息回显
当前为
// ==UserScript== // @name bilibili favlist backup // @name:zh-CN 哔哩哔哩(B站|Bilibili)收藏夹Fix (备份视频信息) // @name:zh-TW 哔哩哔哩(B站|Bilibili)收藏夹Fix (备份视频信息) // @namespace http://tampermonkey.net/ // @version 9 // @description automatically backup info of videos in favlist // @description:zh-CN 自动备份视频信息至本地和第三方网站, 失效视频信息回显 // @description:zh-TW 自动备份视频信息至本地和第三方网站, 失效视频信息回显 // @author YTB0710 // @match https://space.bilibili.com/* // @connect bbdownloader.com // @connect bilibili.com // @connect biliplus.com // @connect jiji.moe // @connect jijidown.com // @connect xbeibeix.com // @grant GM_openInTab // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_getValues // @grant GM_xmlhttpRequest // ==/UserScript== (function () { 'use strict'; const updates = '更新内容:<br>' + '新增: 导出本地备份数据的功能<br>' + '新增: 导入本地备份数据的功能<br>' + '新增: 停止处理当前页视频的功能<br>' + '修复: 本地备份数据中部分信息重复存储的问题'; const version = 9; const favlistURLRegex = /https:\/\/space\.bilibili\.com\/\d+\/favlist.*/; const localeTimeStringRegex = /^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/; const getFidFromURLRegex = /fid=(\d+)/; const getUIDFromURLRegex = /https:\/\/space\.bilibili\.com\/(\d+)/; const getBVFromURLRegex = /video\/(\w{12})/; const getHttpsFromURLRegex = /^https?:\/\//; const getJsonFromBiliplusRegex = /window\.addEventListener\('DOMContentLoaded',function\(\){view\((.+)\);}\);/; const getFilenameFromURLRegex = /[^/]+(?:\.[a-zA-Z0-9]+)$/; let AVBVs; let fidOfAVBVs; let onFavlistPage = false; let enableAutoNextPage = false; let newFreshSpace; let classAppendNewFreshSpace; let pageSize; // let mutations_count = 0; // let mutation_count = 0; let firstTimeMain = true; let divMessage; let divMessageHeightFixed = false; const activeControllers = new Set(); const sortedKeys = [ 'BV', 'AV', 'title', 'intro', 'cover', 'upperUID', 'upperName', 'upperAvatar', 'timeUpload', 'timePublish', 'timeFavorite', 'firstFrame', 'api', 'biliplus', 'jiji', 'bbdownloader', 'preferredTitle', 'preferredCover', ]; const settings = GM_getValue('settings', { version: 0, processNormal: true, processDisabled: true, enableGetFromApi: true, enableGetFromBiliplus: true, enableGetFromJiji: true, enableGetFromBbdownloader: true, enableDebug: false, defaultFavlistFid: null, defaultUID: null, }); /////////////////////////////////////////////////////////////////////////////////// if (settings.hasOwnProperty('enableGetFromJijidown')) { settings.enableGetFromJiji = settings.enableGetFromJijidown; delete settings.enableGetFromJijidown; GM_setValue('settings', settings); } if (settings.hasOwnProperty('enableGetFromXbeibeix')) { settings.enableGetFromBbdownloader = settings.enableGetFromXbeibeix; delete settings.enableGetFromXbeibeix; GM_setValue('settings', settings); } if (typeof settings.defaultFavlistFid === 'string') { settings.defaultFavlistFid = parseInt(settings.defaultFavlistFid, 10); GM_setValue('settings', settings); } /////////////////////////////////////////////////////////////////////////////////// const favlistObserver = new MutationObserver(async (_mutations, observer) => { if (settings.enableDebug) console.warn('callback favlistObserver'); if (document.querySelector('div.items')) { if (settings.enableDebug) console.warn('disconnect favlistObserver'); observer.disconnect(); newFreshSpace = true; classAppendNewFreshSpace = '-newFreshSpace'; pageSize = window.innerWidth < 1760 ? 40 : 36; addControls(); if (!firstTimeMain) { await delay(200); main(); } if (settings.enableDebug) console.warn('observe itemsObserver'); itemsObserver.observe(document.querySelector('div.items'), { childList: true, attributes: false, characterData: false }); if (settings.enableDebug) console.warn('observe bodyChildListObserver'); bodyChildListObserver.observe(document.body, { childList: true, attributes: false, characterData: false }); return; } if (document.querySelector('div.fav-content.section')) { if (settings.enableDebug) console.warn('disconnect favlistObserver'); observer.disconnect(); newFreshSpace = false; classAppendNewFreshSpace = ''; pageSize = 20; addControls(); if (settings.enableDebug) console.warn('observe favContentSectionObserver'); favContentSectionObserver.observe(document.querySelector('div.fav-content.section'), { characterData: false, attributeFilter: ['class'] }); return; } }); // const itemsObserver = new MutationObserver(async (mutations) => { const itemsObserver = new MutationObserver(async () => { abortActiveControllers(); if (settings.enableDebug) console.warn('callback itemsObserver'); await delay(200); // mainNewFreshSpace(); // mainNewFreshSpace(mutations); main(); }); const bodyChildListObserver = new MutationObserver(mutations => { if (settings.enableDebug) console.warn('callback bodyChildListObserver'); if (settings.enableDebug) console.log(mutations); for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { if (addedNode.nodeType === 1 && addedNode.classList.contains('bili-card-dropdown-popper')) { addDropdown(addedNode); // return; } if (addedNode.nodeType === 1 && addedNode.classList.contains('vui_toast--wrapper')) { abortActiveControllers(); if (settings.enableDebug) console.warn('disconncet itemsObserver'); itemsObserver.disconnect(); // return; } } for (const removedNode of mutation.removedNodes) { if (removedNode.nodeType === 1 && removedNode.classList.contains('vui_toast--wrapper')) { abortActiveControllers(); // mainNewFreshSpace(); main(); if (settings.enableDebug) console.warn('observe itemsObserver'); itemsObserver.observe(document.querySelector('div.items'), { childList: true, attributes: false, characterData: false }); // return; } } } }); const favContentSectionObserver = new MutationObserver(mutations => { if (settings.enableDebug) console.warn('callback favContentSectionObserver'); for (const mutation of mutations) { if (!mutation.target.classList.contains('loading')) { abortActiveControllers(); main(); return; } } }); 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 (settings.enableDebug) console.warn('checkURL'); if (favlistURLRegex.test(location.href)) { if (!onFavlistPage) { onFavlistPage = true; if (settings.enableDebug) console.warn('observe favlistObserver'); favlistObserver.observe(document.body, { subtree: true, childList: true, attributes: false, characterData: false }); } } else { if (onFavlistPage) { abortActiveControllers(); onFavlistPage = false; if (settings.enableDebug) console.warn('disconnect favlistObserver'); favlistObserver.disconnect(); if (settings.enableDebug) console.warn('disconncet itemsObserver'); itemsObserver.disconnect(); if (settings.enableDebug) console.warn('disconncet bodyChildListObserver'); bodyChildListObserver.disconnect(); if (settings.enableDebug) console.warn('disconncet favContentSectionObserver'); favContentSectionObserver.disconnect(); } } } async function main() { if (settings.enableDebug) console.warn('============main============'); let controller; firstTimeMain = false; try { controller = new AbortController(); activeControllers.add(controller); let fid; if (newFreshSpace) { const fidFromURLMatch = location.href.match(getFidFromURLRegex); if (fidFromURLMatch) { fid = parseInt(fidFromURLMatch[1], 10); if (!settings.defaultFavlistFid && !document.querySelector('div.vui_sidebar-item--active').parentNode.getAttribute('id')) { settings.defaultFavlistFid = fid; GM_setValue('settings', settings); } } else if (settings.defaultFavlistFid) { fid = settings.defaultFavlistFid; } else { throw ['无法获取当前收藏夹的fid, 刷新页面可能有帮助']; } } else { fid = parseInt(document.querySelector('.fav-item.cur').getAttribute('fid'), 10); } 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.defaultUID) { settings.defaultUID = parseInt(location.href.match(getUIDFromURLRegex)[1], 10); GM_setValue('settings', settings); } let videos; if (newFreshSpace) { videos = document.querySelectorAll('div.items__item'); } else { videos = document.querySelectorAll('li.small-item'); } if (fid !== fidOfAVBVs || !AVBVs) { 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}`, 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 (settings.enableDebug) console.warn(`获取新的AVBVs, fid: ${fid}`); fidOfAVBVs = fid; AVBVs = response.response.data; } const clonedAVBVs = structuredClone(AVBVs); const apiDetails = {}; for (const [index, video] of videos.entries()) { let as; let AV; let BV; let title; try { if (controller.signal.aborted) { throw new DOMException('', 'AbortError'); } let disabled = false; if (newFreshSpace) { if (!video.querySelector('.bili-cover-card__stats')) { disabled = true; } } else { if (video.classList.contains('disabled')) { disabled = true; } } if (!settings.processNormal && !disabled) { continue; } if (!settings.processDisabled && disabled) { continue; } as = video.querySelectorAll('a'); const divTitleNewFreshSpace = video.querySelector('.bili-video-card__title'); if (controller.signal.aborted) { throw new DOMException('', 'AbortError'); } if (newFreshSpace) { BV = as[0].getAttribute('href').match(getBVFromURLRegex)[1]; } else { BV = video.getAttribute('data-aid'); } AV = clonedAVBVs.find(clonedAVBV => clonedAVBV.bvid === BV).id; title = as[1].innerText; if (settings.enableDebug) console.warn('========video========'); if (settings.enableDebug) console.log(`收藏夹fid: ${fid}`); if (settings.enableDebug) console.log(`位置: 第${pageNumber}页的第${index + 1}个`); if (settings.enableDebug) consoleAVBVTitle('log', AV, BV, title); let spanFavTime; let divTCABJX; if (newFreshSpace) { const divSubtitle = document.createElement('div'); divSubtitle.classList.add('bili-video-card__subtitle'); video.querySelector('div.bili-video-card__details').appendChild(divSubtitle); spanFavTime = document.createElement('span'); spanFavTime.innerText = `收藏于:`; divSubtitle.appendChild(spanFavTime); divTCABJX = document.createElement('div'); divTCABJX.style.marginLeft = 'auto'; divTCABJX.style.display = 'block'; divSubtitle.appendChild(divTCABJX); } else { const divMetaPubdate = video.querySelector('div.meta.pubdate'); const metaText = divMetaPubdate.innerText; divMetaPubdate.innerHTML = null; spanFavTime = document.createElement('span'); spanFavTime.innerText = metaText.replace(': ', ':'); spanFavTime.style.width = 'auto'; if (!newFreshSpace) { spanFavTime.style.lineHeight = '16px'; } divMetaPubdate.appendChild(spanFavTime); // divTCABJX = document.createElement('div'); // divTCABJX.style.marginLeft = 'auto'; // // divTCABJX.style.display = 'block'; // divMetaPubdate.appendChild(divTCABJX); divTCABJX = divMetaPubdate; } const spanX = document.createElement('span'); spanX.classList.add('backup-spanTCABJX' + classAppendNewFreshSpace); spanX.innerText = 'X'; if (!newFreshSpace) { // spanX.style.marginRight = '11px'; spanX.style.marginRight = '9px'; spanX.style.lineHeight = '16px'; } spanX.addEventListener('click', () => { try { GM_openInTab(`https://bbdownloader.com/video/${BV}`, { active: true, insert: false, setParent: true }); } catch (error) { catchUnknownError(error); } }); divTCABJX.appendChild(spanX); const spanJ = document.createElement('span'); spanJ.classList.add('backup-spanTCABJX' + classAppendNewFreshSpace); spanJ.innerText = 'J'; spanJ.style.marginRight = '3px'; if (!newFreshSpace) { spanJ.style.lineHeight = '16px'; } spanJ.addEventListener('click', () => { try { GM_openInTab(`https://www.jiji.moe/video/${BV}`, { active: true, insert: false, setParent: true }); } catch (error) { catchUnknownError(error); } }); divTCABJX.appendChild(spanJ); const spanB = document.createElement('span'); spanB.classList.add('backup-spanTCABJX' + classAppendNewFreshSpace); spanB.innerText = 'B'; spanB.style.marginRight = '3px'; if (!newFreshSpace) { spanB.style.lineHeight = '16px'; } spanB.addEventListener('click', () => { try { GM_openInTab(`https://www.biliplus.com/video/${BV}`, { active: true, insert: false, setParent: true }); GM_openInTab(`https://www.biliplus.com/video/av${AV}`, { insert: false, setParent: true }); } catch (error) { catchUnknownError(error); } }); divTCABJX.appendChild(spanB); const spanA = document.createElement('span'); spanA.classList.add('backup-spanTCABJX' + classAppendNewFreshSpace); spanA.innerText = 'A'; spanA.style.marginRight = '3px'; if (!newFreshSpace) { spanA.style.lineHeight = '16px'; } spanA.addEventListener('click', () => { try { GM_openInTab(`https://api.bilibili.com/x/v3/fav/resource/infos?resources=${AV}%3A2&folder_id=${fid}`, { active: true, insert: false, setParent: true }); GM_openInTab(`https://api.bilibili.com/x/v3/fav/resource/list?media_id=${fid}&pn=${(pageNumber - 1) * pageSize + index + 1}&ps=1&order=mtime`, { insert: false, setParent: true }); } catch (error) { catchUnknownError(error); } }); divTCABJX.appendChild(spanA); const backup = GM_getValue(BV, { BV: null, AV: null, title: null, intro: null, cover: null, upperUID: null, upperName: null, upperAvatar: null, timeUpload: null, timePublish: null, timeFavorite: null, firstFrame: null, api: null, biliplus: null, jiji: null, bbdownloader: null, // preferredTitle: null, // preferredCover: null }); /////////////////////////////////////////////////////////////////////////////////// if (backup.hasOwnProperty('jijidown')) { backup.jiji = backup.jijidown; delete backup.jijidown; GM_setValue(BV, backup); } if (backup.hasOwnProperty('xbeibeix')) { backup.bbdownloader = backup.xbeibeix; delete backup.xbeibeix; GM_setValue(BV, backup); } if (backup.timeFavorite) { let modified = false; backup.timeFavorite.forEach(el => { if (typeof el.fid === 'string') { el.fid = parseInt(el.fid, 10); modified = true; } }); if (modified) { GM_setValue(BV, backup); } } if (backup.timeUpload && typeof backup.timeUpload === 'object') { if (backup.timeUpload.length === 1) { backup.timeUpload = backup.timeUpload[0].value; } else { backup.timeUpload = null; } } if (backup.timePublish && typeof backup.timePublish === 'object') { if (backup.timePublish.length === 1) { backup.timePublish = backup.timePublish[0].value; } else { backup.timePublish = null; } } ['cover', 'upperAvatar', 'firstFrame'].forEach(key => { if (backup[key] && backup[key].length > 1) { const tempBackup = {}; backup[key].forEach(el => { updateArrayDataInBackup(tempBackup, key, el.value, el.ts, el.from); }); backup[key] = tempBackup[key]; } }); /////////////////////////////////////////////////////////////////////////////////// const functions = []; try { if (backup.timeFavorite) { const target = backup.timeFavorite.find(el => el.fid === fid); if (target) { spanFavTime.innerText = `收藏于:${formatTsTimeFavorite(new Date(target.value * 1000))}`; spanFavTime.setAttribute('title', new Date(target.value * 1000).toLocaleString()); } } if (settings.enableGetFromApi) { if (!backup.api || (backup.api.value === false && getCurrentTs() - backup.api.ts > 3600 * 1) || (backup.api.value === true && getCurrentTs() - backup.api.ts > 3600 * 1)) { await getFromApi(AV, BV, title, backup, spanA, apiDetails, fid, pageNumber, disabled, spanFavTime); } else { spanA.style.color = backup.api.value ? '#00ff00' : '#ff0000'; } } if (settings.enableGetFromJiji) { if (!backup.jiji || (backup.jiji.value === false && getCurrentTs() - backup.jiji.ts > 3600 * 24 * 30) || (backup.jiji.value === true && getCurrentTs() - backup.jiji.ts > 3600 * 24 * 30)) { functions.push(getFromJiji(AV, BV, title, backup, spanJ)); } else { spanJ.style.color = backup.jiji.value ? '#00ff00' : '#ff0000'; } } if (settings.enableGetFromBbdownloader) { if (!backup.bbdownloader || (backup.bbdownloader.value === false && getCurrentTs() - backup.bbdownloader.ts > 3600 * 24 * 30) || (backup.bbdownloader.value === true && getCurrentTs() - backup.bbdownloader.ts > 3600 * 24 * 30)) { functions.push(getFromBbdownloader(AV, BV, title, backup, spanX)); } else { spanX.style.color = backup.bbdownloader.value ? '#00ff00' : '#ff0000'; } } if (settings.enableGetFromBiliplus) { if (!backup.biliplus || (backup.biliplus.value === false && getCurrentTs() - backup.biliplus.ts > 3600 * 24 * 30) || (backup.biliplus.value === true && getCurrentTs() - backup.biliplus.ts > 3600 * 24 * 30)) { functions.push(getFromBiliplus(AV, BV, title, backup, spanB)); } else { spanB.style.color = backup.biliplus.value ? '#00ff00' : '#ff0000'; } } if (functions.length) { if (controller.signal.aborted) { throw new DOMException('', 'AbortError'); } await Promise.all(functions); const sortedBackup = {}; for (const sortedKey of sortedKeys) { sortedBackup[sortedKey] = backup[sortedKey]; } GM_setValue(BV, sortedBackup); if (settings.enableDebug) console.warn('保存第三方网站的数据至本地'); if (settings.enableDebug) consoleAVBVTitle('warn', AV, BV, title); if (settings.enableDebug) console.warn(sortedBackup); } } catch (error) { if (error instanceof Error) { if (error.name === 'AbortError') { throw error; } addMessage('发生未知错误, 请反馈该问题', false, true); addMessage(`收藏夹fid: ${fid}`, true); addMessage(`位置: 第${pageNumber}页的第${index + 1}个`, true); addMessageAVBVTitle(AV, BV, title); addMessage(error.stack, true); console.error(`收藏夹fid: ${fid}`); console.error(`位置: 第${pageNumber}页的第${index + 1}个`); consoleAVBVTitle('error', AV, BV, title); console.error(error); if (as[1]) { as[1].style.color = '#ff0000'; } } else { addMessage(error[0], false, true); for (let i = 1; i < error.length; i++) { addMessage(error[i], true); } addMessage(`收藏夹fid: ${fid}`, true); addMessage(`位置: 第${pageNumber}页的第${index + 1}个`, true); addMessageAVBVTitle(AV, BV, title); if (as[1]) { as[1].style.color = '#ff0000'; } } } let picture; let sourceAvif; let sourceWebp; let img; if (newFreshSpace) { img = video.querySelector('img'); } else { picture = video.querySelector('picture'); sourceAvif = picture.querySelector('source[type="image/avif"]'); sourceWebp = picture.querySelector('source[type="image/webp"]'); img = picture.querySelector('img'); } if (disabled) { // video.style.opacity = '0.7'; if (newFreshSpace) { as[2].style.textDecoration = 'line-through'; as[2].style.opacity = '0.7'; if (backup.cover) { img.setAttribute('src', `//${backup.cover[backup.cover.length - 1].value}@672w_378h_1c.avif`); } } else { video.classList.remove('disabled'); as[0].classList.remove('disabled'); if (backup.cover) { sourceAvif.setAttribute('srcset', `//${backup.cover[backup.cover.length - 1].value}@320w_200h_1c_!web-space-favlist-video.avif`); sourceWebp.setAttribute('srcset', `//${backup.cover[backup.cover.length - 1].value}@320w_200h_1c_!web-space-favlist-video.webp`); img.setAttribute('src', `//${backup.cover[backup.cover.length - 1].value}@320w_200h_1c_!web-space-favlist-video.webp`); } } spanFavTime.style.textDecoration = 'line-through'; spanFavTime.style.opacity = '0.7'; as[1].style.textDecoration = 'line-through'; as[1].style.opacity = '0.5'; if (backup.title) { as[1].textContent = backup.title[backup.title.length - 1].value; if (newFreshSpace) { divTitleNewFreshSpace.setAttribute('title', backup.title[backup.title.length - 1].value); } else { as[1].setAttribute('title', backup.title[backup.title.length - 1].value); } } } if (backup.cover && backup.cover.length > 1) { const spanC = document.createElement('span'); spanC.classList.add('backup-spanTCABJX' + classAppendNewFreshSpace); spanC.innerText = 'C'; spanC.style.marginRight = '3px'; spanC.style.color = '#000000'; if (!newFreshSpace) { spanC.style.lineHeight = '16px'; } let i = backup.cover.length - 2; spanC.addEventListener('click', () => { try { if (i < 0) { i = backup.cover.length - 1; } if (newFreshSpace) { img.setAttribute('src', `//${backup.cover[i].value}@672w_378h_1c.avif`); } else { sourceAvif.setAttribute('srcset', `//${backup.cover[i].value}@320w_200h_1c_!web-space-favlist-video.avif`); sourceWebp.setAttribute('srcset', `//${backup.cover[i].value}@320w_200h_1c_!web-space-favlist-video.webp`); img.setAttribute('src', `//${backup.cover[i].value}@320w_200h_1c_!web-space-favlist-video.webp`); } if (i !== backup.cover.length - 1) { spanC.style.color = '#999999'; } else { spanC.style.color = '#000000'; } i--; } catch (error) { catchUnknownError(error); } }); divTCABJX.appendChild(spanC); } if (backup.title && backup.title.length > 1) { const spanT = document.createElement('span'); spanT.classList.add('backup-spanTCABJX' + classAppendNewFreshSpace); spanT.innerText = 'T'; spanT.style.marginRight = '3px'; spanT.style.color = '#000000'; if (!newFreshSpace) { spanT.style.lineHeight = '16px'; } let i = backup.title.length - 2; spanT.addEventListener('click', () => { try { if (i < 0) { i = backup.title.length - 1; } as[1].textContent = backup.title[i].value; if (newFreshSpace) { divTitleNewFreshSpace.setAttribute('title', backup.title[i].value); } else { as[1].setAttribute('title', backup.title[i].value); } if (i !== backup.title.length - 1) { spanT.style.color = '#999999'; } else { spanT.style.color = '#000000'; } i--; } catch (error) { catchUnknownError(error); } }); divTCABJX.appendChild(spanT); } if (controller.signal.aborted) { throw new DOMException('', 'AbortError'); } if (!newFreshSpace) { const ul = video.querySelector('ul.be-dropdown-menu'); addDropdown(ul, AV, BV); } if (functions.length === 1) { await delay(200); } } catch (error) { if (error instanceof Error) { if (error.name === 'AbortError') { throw error; } addMessage('发生未知错误, 请反馈该问题', false, true); addMessage(`收藏夹fid: ${fid}`, true); addMessage(`位置: 第${pageNumber}页的第${index + 1}个`, true); addMessageAVBVTitle(AV, BV, title); addMessage(error.stack, true); console.error(`收藏夹fid: ${fid}`); console.error(`位置: 第${pageNumber}页的第${index + 1}个`); consoleAVBVTitle('error', AV, BV, title); console.error(error); if (as[1]) { as[1].style.color = '#ff0000'; } } else { addMessage(error[0], false, true); for (let i = 1; i < error.length; i++) { addMessage(error[i], true); } addMessage(`收藏夹fid: ${fid}`, true); addMessage(`位置: 第${pageNumber}页的第${index + 1}个`, true); addMessageAVBVTitle(AV, BV, title); if (as[1]) { as[1].style.color = '#ff0000'; } } } } if (enableAutoNextPage) { if (newFreshSpace) { const pager = Array.from(document.querySelectorAll('button.vui_pagenation--btn-side')).find(b => b.innerText === '下一页'); if (pager && !pager.classList.contains('vui_button--disabled')) { await delay(5000); if (controller.signal.aborted) { throw new DOMException('', 'AbortError'); } if (enableAutoNextPage) { pager.click(); } } } else { const pager = document.querySelector('li.be-pager-next'); if (pager && !pager.classList.contains('be-pager-disabled')) { await delay(5000); if (controller.signal.aborted) { throw new DOMException('', 'AbortError'); } if (enableAutoNextPage) { pager.click(); } } } } } catch (error) { if (error instanceof Error) { if (error.name === 'AbortError') { return; } catchUnknownError(error); } else { addMessage(error[0], false, true); for (let i = 1; i < error.length; i++) { addMessage(error[i], true); } } } finally { activeControllers.delete(controller); } } function addControls() { let displayUpdate = false; if (settings.version !== version) { if (settings.version) { displayUpdate = true; } settings.version = version; GM_setValue('settings', settings); } const style = document.createElement('style'); style.textContent = ` .backup-div { padding: 2px; } .backup-div-newFreshSpace { padding: 2px 0; } .backup-label, .backup-label-newFreshSpace { line-height: 1; } .backup-disabled, .backup-disabled-newFreshSpace { opacity: 0.5; pointer-events: none; } .backup-button, .backup-button-newFreshSpace { border: 1px solid #cccccc; border-radius: 3px; line-height: 1; cursor: pointer; } .backup-button { padding: 3px; font-size: 14px; } .backup-button-newFreshSpace { padding: 4px; font-size: 16px; } .backup-divMessage, .backup-divMessage-newFreshSpace { overflow-y: auto; background-color: #eeeeee; line-height: 1.5; scrollbar-width: none; } .backup-divMessage { margin: 2px; } .backup-divMessage::-webkit-scrollbar { display: none; } .backup-divMessage-newFreshSpace { margin: 2px 0; } .backup-divMessage-newFreshSpace::-webkit-scrollbar { display: none; } .backup-spanTCABJX, .backup-spanTCABJX-newFreshSpace { float: right; font-weight: bold; cursor: pointer; } `; 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('backup-div' + classAppendNewFreshSpace); if (!newFreshSpace) { divControls.style.borderTop = '1px solid #e4e9f0'; } divSide.appendChild(divControls); let divLabelEnableGetFromApi; let divLabelEnableGetFromBiliplus; let divLabelEnableGetFromJiji; let divLabelEnableGetFromBbdownloader; const divLabelProcessNormal = document.createElement('div'); divLabelProcessNormal.classList.add('backup-div' + classAppendNewFreshSpace); divControls.appendChild(divLabelProcessNormal); const labelProcessNormal = document.createElement('label'); labelProcessNormal.classList.add('backup-label' + classAppendNewFreshSpace); labelProcessNormal.innerText = '处理正常视频'; divLabelProcessNormal.appendChild(labelProcessNormal); const checkboxProcessNormal = document.createElement('input'); checkboxProcessNormal.type = 'checkbox'; checkboxProcessNormal.checked = settings.processNormal; checkboxProcessNormal.addEventListener('change', () => { try { settings.processNormal = checkboxProcessNormal.checked; GM_setValue('settings', settings); if (!settings.processNormal && !settings.processDisabled) { divLabelEnableGetFromApi.classList.add('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromBiliplus.classList.add('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromJiji.classList.add('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromBbdownloader.classList.add('backup-disabled' + classAppendNewFreshSpace); } else { divLabelEnableGetFromApi.classList.remove('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromBiliplus.classList.remove('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromJiji.classList.remove('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromBbdownloader.classList.remove('backup-disabled' + classAppendNewFreshSpace); } } catch (error) { catchUnknownError(error); } }); labelProcessNormal.insertAdjacentElement('afterbegin', checkboxProcessNormal); const divLabelProcessDisabled = document.createElement('div'); divLabelProcessDisabled.classList.add('backup-div' + classAppendNewFreshSpace); divControls.appendChild(divLabelProcessDisabled); const labelProcessDisabled = document.createElement('label'); labelProcessDisabled.classList.add('backup-label' + classAppendNewFreshSpace); labelProcessDisabled.innerText = '处理失效视频'; divLabelProcessDisabled.appendChild(labelProcessDisabled); const checkboxProcessDisabled = document.createElement('input'); checkboxProcessDisabled.type = 'checkbox'; checkboxProcessDisabled.checked = settings.processDisabled; checkboxProcessDisabled.addEventListener('change', () => { try { settings.processDisabled = checkboxProcessDisabled.checked; GM_setValue('settings', settings); if (!settings.processNormal && !settings.processDisabled) { divLabelEnableGetFromApi.classList.add('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromBiliplus.classList.add('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromJiji.classList.add('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromBbdownloader.classList.add('backup-disabled' + classAppendNewFreshSpace); } else { divLabelEnableGetFromApi.classList.remove('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromBiliplus.classList.remove('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromJiji.classList.remove('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromBbdownloader.classList.remove('backup-disabled' + classAppendNewFreshSpace); } } catch (error) { catchUnknownError(error); } }); labelProcessDisabled.insertAdjacentElement('afterbegin', checkboxProcessDisabled); divLabelEnableGetFromApi = document.createElement('div'); divLabelEnableGetFromApi.classList.add('backup-div' + classAppendNewFreshSpace); divControls.appendChild(divLabelEnableGetFromApi); const labelEnableGetFromApi = document.createElement('label'); labelEnableGetFromApi.classList.add('backup-label' + classAppendNewFreshSpace); labelEnableGetFromApi.innerText = '从B站接口获取数据'; divLabelEnableGetFromApi.appendChild(labelEnableGetFromApi); const checkboxEnableGetFromApi = document.createElement('input'); checkboxEnableGetFromApi.type = 'checkbox'; checkboxEnableGetFromApi.checked = settings.enableGetFromApi; checkboxEnableGetFromApi.addEventListener('change', () => { try { settings.enableGetFromApi = checkboxEnableGetFromApi.checked; GM_setValue('settings', settings); } catch (error) { catchUnknownError(error); } }); labelEnableGetFromApi.insertAdjacentElement('afterbegin', checkboxEnableGetFromApi); divLabelEnableGetFromBiliplus = document.createElement('div'); divLabelEnableGetFromBiliplus.classList.add('backup-div' + classAppendNewFreshSpace); divControls.appendChild(divLabelEnableGetFromBiliplus); const labelEnableGetFromBiliplus = document.createElement('label'); labelEnableGetFromBiliplus.classList.add('backup-label' + classAppendNewFreshSpace); labelEnableGetFromBiliplus.innerText = '从BiliPlus获取数据'; divLabelEnableGetFromBiliplus.appendChild(labelEnableGetFromBiliplus); const checkboxEnableGetFromBiliplus = document.createElement('input'); checkboxEnableGetFromBiliplus.type = 'checkbox'; checkboxEnableGetFromBiliplus.checked = settings.enableGetFromBiliplus; checkboxEnableGetFromBiliplus.addEventListener('change', () => { try { settings.enableGetFromBiliplus = checkboxEnableGetFromBiliplus.checked; GM_setValue('settings', settings); } catch (error) { catchUnknownError(error); } }); labelEnableGetFromBiliplus.insertAdjacentElement('afterbegin', checkboxEnableGetFromBiliplus); divLabelEnableGetFromJiji = document.createElement('div'); divLabelEnableGetFromJiji.classList.add('backup-div' + classAppendNewFreshSpace); divControls.appendChild(divLabelEnableGetFromJiji); const labelEnableGetFromJiji = document.createElement('label'); labelEnableGetFromJiji.classList.add('backup-label' + classAppendNewFreshSpace); labelEnableGetFromJiji.innerText = '从唧唧获取数据'; divLabelEnableGetFromJiji.appendChild(labelEnableGetFromJiji); const checkboxEnableGetFromJiji = document.createElement('input'); checkboxEnableGetFromJiji.type = 'checkbox'; checkboxEnableGetFromJiji.checked = settings.enableGetFromJiji; checkboxEnableGetFromJiji.addEventListener('change', () => { try { settings.enableGetFromJiji = checkboxEnableGetFromJiji.checked; GM_setValue('settings', settings); } catch (error) { catchUnknownError(error); } }); labelEnableGetFromJiji.insertAdjacentElement('afterbegin', checkboxEnableGetFromJiji); divLabelEnableGetFromBbdownloader = document.createElement('div'); divLabelEnableGetFromBbdownloader.classList.add('backup-div' + classAppendNewFreshSpace); divControls.appendChild(divLabelEnableGetFromBbdownloader); const labelEnableGetFromBbdownloader = document.createElement('label'); labelEnableGetFromBbdownloader.classList.add('backup-label' + classAppendNewFreshSpace); labelEnableGetFromBbdownloader.innerText = '从贝贝工具站获取数据'; divLabelEnableGetFromBbdownloader.appendChild(labelEnableGetFromBbdownloader); const checkboxEnableGetFromBbdownloader = document.createElement('input'); checkboxEnableGetFromBbdownloader.type = 'checkbox'; checkboxEnableGetFromBbdownloader.checked = settings.enableGetFromBbdownloader; checkboxEnableGetFromBbdownloader.addEventListener('change', () => { try { settings.enableGetFromBbdownloader = checkboxEnableGetFromBbdownloader.checked; GM_setValue('settings', settings); } catch (error) { catchUnknownError(error); } }); labelEnableGetFromBbdownloader.insertAdjacentElement('afterbegin', checkboxEnableGetFromBbdownloader); if (!settings.processNormal && !settings.processDisabled) { divLabelEnableGetFromApi.classList.add('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromBiliplus.classList.add('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromJiji.classList.add('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromBbdownloader.classList.add('backup-disabled' + classAppendNewFreshSpace); } else { divLabelEnableGetFromApi.classList.remove('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromBiliplus.classList.remove('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromJiji.classList.remove('backup-disabled' + classAppendNewFreshSpace); divLabelEnableGetFromBbdownloader.classList.remove('backup-disabled' + classAppendNewFreshSpace); } const divLabelEnableAutoNextPage = document.createElement('div'); divLabelEnableAutoNextPage.classList.add('backup-div' + classAppendNewFreshSpace); divControls.appendChild(divLabelEnableAutoNextPage); const labelEnableAutoNextPage = document.createElement('label'); labelEnableAutoNextPage.classList.add('backup-label' + classAppendNewFreshSpace); labelEnableAutoNextPage.innerText = '自动点击下一页'; divLabelEnableAutoNextPage.appendChild(labelEnableAutoNextPage); const checkboxEnableAutoNextPage = document.createElement('input'); checkboxEnableAutoNextPage.type = 'checkbox'; checkboxEnableAutoNextPage.checked = enableAutoNextPage; checkboxEnableAutoNextPage.addEventListener('change', async () => { try { enableAutoNextPage = checkboxEnableAutoNextPage.checked; if (enableAutoNextPage && !activeControllers.size) { if (newFreshSpace) { const pager = Array.from(document.querySelectorAll('button.vui_pagenation--btn-side')).find(b => b.innerText === '下一页'); if (pager && !pager.classList.contains('vui_button--disabled')) { await delay(500); if (enableAutoNextPage) { pager.click(); } } } else { const pager = document.querySelector('li.be-pager-next'); if (pager && !pager.classList.contains('be-pager-disabled')) { await delay(500); if (enableAutoNextPage) { pager.click(); } } } } } catch (error) { catchUnknownError(error); } }); labelEnableAutoNextPage.insertAdjacentElement('afterbegin', checkboxEnableAutoNextPage); const divLabelEnableDebug = document.createElement('div'); divLabelEnableDebug.classList.add('backup-div' + classAppendNewFreshSpace); divControls.appendChild(divLabelEnableDebug); const labelEnableDebug = document.createElement('label'); labelEnableDebug.classList.add('backup-label' + classAppendNewFreshSpace); labelEnableDebug.innerText = '控制台输出日志'; divLabelEnableDebug.appendChild(labelEnableDebug); const checkboxEnableDebug = document.createElement('input'); checkboxEnableDebug.type = 'checkbox'; checkboxEnableDebug.checked = settings.enableDebug; checkboxEnableDebug.addEventListener('change', () => { try { settings.enableDebug = checkboxEnableDebug.checked; GM_setValue('settings', settings); } catch (error) { catchUnknownError(error); } }); labelEnableDebug.insertAdjacentElement('afterbegin', checkboxEnableDebug); const divButtonExportBackup = document.createElement('div'); divButtonExportBackup.classList.add('backup-div' + classAppendNewFreshSpace); divButtonExportBackup.setAttribute('title', '导入的备份数据会与脚本内已有的备份数据合并在一起。\n' + '请不要随意修改导出的备份数据文件中的内容, 否则导入时可能会出错。'); divControls.appendChild(divButtonExportBackup); const buttonExportBackup = document.createElement('button'); buttonExportBackup.type = 'button'; buttonExportBackup.classList.add('backup-button' + classAppendNewFreshSpace); buttonExportBackup.innerText = '导出本地备份数据'; buttonExportBackup.addEventListener('click', () => { try { // const BVs = GM_listValues(); // const backupsToExport = {}; // BVs.forEach(BV => { // backupsToExport[BV] = GM_getValue(BV, null); // }); const backupsToExport = GM_getValues(GM_listValues().sort()); delete backupsToExport.settings; // const backupData = JSON.stringify(backupToExport); const backupsData = JSON.stringify(backupsToExport, null, 4); const blob = new Blob([backupsData], { type: 'application/json' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `${formatTsYYMMDD_HHMMSS(getCurrentTs())}.json`; link.click(); URL.revokeObjectURL(link.href); } catch (error) { catchUnknownError(error); } }); divButtonExportBackup.appendChild(buttonExportBackup); const divButtonImportBackup = document.createElement('div'); divButtonImportBackup.classList.add('backup-div' + classAppendNewFreshSpace); divButtonImportBackup.setAttribute('title', '导入的备份数据会与脚本内已有的备份数据合并在一起。\n' + '请不要随意修改导出的备份数据文件中的内容, 否则导入时可能会出错。'); divControls.appendChild(divButtonImportBackup); const buttonImportBackup = document.createElement('button'); buttonImportBackup.type = 'button'; buttonImportBackup.classList.add('backup-button' + classAppendNewFreshSpace); buttonImportBackup.innerText = '导入本地备份数据'; divButtonImportBackup.appendChild(buttonImportBackup); const divInputFile = document.createElement('div'); divControls.appendChild(divInputFile); const inputFile = document.createElement('input'); inputFile.type = 'file'; inputFile.style.display = 'none'; divInputFile.appendChild(inputFile); buttonImportBackup.addEventListener('click', () => { try { inputFile.click(); } catch (error) { catchUnknownError(error); } }); inputFile.addEventListener('change', (event) => { try { const file = event.target.files[0]; if (!file) { return; } const reader = new FileReader(); reader.onload = (e) => { try { const backupsToImport = JSON.parse(e.target.result); if (typeof backupsToImport !== 'object') { addMessage('文件内容有误, 无法导入', false, true); return; } for (const BVOfBackupToImport in backupsToImport) { const backupToImport = backupsToImport[BVOfBackupToImport]; try { /////////////////////////////////////////////////////////////////////////////////// if (backupToImport.hasOwnProperty('jijidown')) { backupToImport.jiji = backupToImport.jijidown; delete backupToImport.jijidown; } if (backupToImport.hasOwnProperty('xbeibeix')) { backupToImport.bbdownloader = backupToImport.xbeibeix; delete backupToImport.xbeibeix; } if (backupToImport.timeFavorite) { backupToImport.timeFavorite.forEach(el => { if (typeof el.fid === 'string') { el.fid = parseInt(el.fid, 10); } }); } if (backupToImport.timeUpload && typeof backupToImport.timeUpload === 'object') { if (backupToImport.timeUpload.length === 1) { backupToImport.timeUpload = backupToImport.timeUpload[0].value; } else { backupToImport.timeUpload = null; } } if (backupToImport.timePublish && typeof backupToImport.timePublish === 'object') { if (backupToImport.timePublish.length === 1) { backupToImport.timePublish = backupToImport.timePublish[0].value; } else { backupToImport.timePublish = null; } } ['cover', 'upperAvatar', 'firstFrame'].forEach(key => { if (backupToImport[key] && backupToImport[key].length > 1) { const tempBackup = {}; backupToImport[key].forEach(el => { updateArrayDataInBackup(tempBackup, key, el.value, el.ts, el.from); }); backupToImport[key] = tempBackup[key]; } }); /////////////////////////////////////////////////////////////////////////////////// const backup = GM_getValue(backupToImport.BV, { BV: null, AV: null, title: null, intro: null, cover: null, upperUID: null, upperName: null, upperAvatar: null, timeUpload: null, timePublish: null, timeFavorite: null, firstFrame: null, api: null, biliplus: null, jiji: null, bbdownloader: null, }); /////////////////////////////////////////////////////////////////////////////////// if (backup.hasOwnProperty('jijidown')) { backup.jiji = backup.jijidown; delete backup.jijidown; } if (backup.hasOwnProperty('xbeibeix')) { backup.bbdownloader = backup.xbeibeix; delete backup.xbeibeix; } if (backup.timeFavorite) { backup.timeFavorite.forEach(el => { if (typeof el.fid === 'string') { el.fid = parseInt(el.fid, 10); } }); } if (backup.timeUpload && typeof backup.timeUpload === 'object') { if (backup.timeUpload.length === 1) { backup.timeUpload = backup.timeUpload[0].value; } else { backup.timeUpload = null; } } if (backup.timePublish && typeof backup.timePublish === 'object') { if (backup.timePublish.length === 1) { backup.timePublish = backup.timePublish[0].value; } else { backup.timePublish = null; } } ['cover', 'upperAvatar', 'firstFrame'].forEach(key => { if (backup[key] && backup[key].length > 1) { const tempBackup = {}; backup[key].forEach(el => { updateArrayDataInBackup(tempBackup, key, el.value, el.ts, el.from); }); backup[key] = tempBackup[key]; } }); /////////////////////////////////////////////////////////////////////////////////// backup.AV = backupToImport.AV; backup.BV = backupToImport.BV; if (backupToImport.title) { backupToImport.title.forEach(el => { updateArrayDataInBackup(backup, 'title', el.value, el.ts, el.from); }); } if (backupToImport.intro) { backupToImport.intro.forEach(el => { updateArrayDataInBackup(backup, 'intro', el.value, el.ts, el.from); }); } if (backupToImport.cover) { backupToImport.cover.forEach(el => { updateArrayDataInBackup(backup, 'cover', el.value, el.ts, el.from); }); } backup.upperUID = backupToImport.upperUID; if (backupToImport.upperName) { backupToImport.upperName.forEach(el => { updateArrayDataInBackup(backup, 'upperName', el.value, el.ts, el.from); }); } if (backupToImport.upperAvatar) { backupToImport.upperAvatar.forEach(el => { updateArrayDataInBackup(backup, 'upperAvatar', el.value, el.ts, el.from); }); } backup.timeUpload = backupToImport.timeUpload; backup.timePublish = backupToImport.timePublish; if (backupToImport.timeFavorite) { backupToImport.timeFavorite.forEach(el => { if (!backup.timeFavorite) { backup.timeFavorite = []; const data = { value: el.value, fid: el.fid }; backup.timeFavorite.push(data); } else { const target = backup.timeFavorite.find(ele => ele.fid === el.fid); if (target) { if (target.value !== el.value) { target.value = el.value; backup.timeFavorite.sort((a, b) => a.value - b.value); } } else { const data = { value: el.value, fid: el.fid }; backup.timeFavorite.push(data); backup.timeFavorite.sort((a, b) => a.value - b.value); } } }); } if (backupToImport.firstFrame) { backupToImport.firstFrame.forEach(el => { updateArrayDataInBackup(backup, 'firstFrame', el.value, el.ts, el.from); }); } ['api', 'biliplus', 'jiji', 'bbdownloader'].forEach(key => { if (backupToImport[key]) { if (!backup[key]) { backup[key] = { value: backupToImport[key].value, ts: backupToImport[key].ts }; } else { if (backup[key].ts < backupToImport[key].ts) { backup[key].value = backupToImport[key].value; backup[key].ts = backupToImport[key].ts; } } } }); const sortedBackup = {}; for (const sortedKey of sortedKeys) { sortedBackup[sortedKey] = backup[sortedKey]; } GM_setValue(backupToImport.BV, sortedBackup); } catch (error) { addMessage('导入该视频失败', false, true); if (backupToImport.BV) { addMessage(`BV号: ${backupToImport.BV}`, true); } addMessage(error.stack, true); console.error('需要导入的数据'); console.error(backupToImport); if (backupToImport.BV) { console.error('本地已保存的数据'); console.error(GM_getValue(backupToImport.BV, {})); } } } addMessage('导入完成'); } catch (error) { catchUnknownError(error); } }; reader.readAsText(file); } catch (error) { catchUnknownError(error); } }); const divButtonStopProcessing = document.createElement('div'); divButtonStopProcessing.classList.add('backup-div' + classAppendNewFreshSpace); divControls.appendChild(divButtonStopProcessing); const buttonStopProcessing = document.createElement('button'); buttonStopProcessing.type = 'button'; buttonStopProcessing.classList.add('backup-button' + classAppendNewFreshSpace); buttonStopProcessing.innerText = '停止处理当前页视频'; buttonStopProcessing.addEventListener('click', () => { try { abortActiveControllers(); } catch (error) { catchUnknownError(error); } }); divButtonStopProcessing.appendChild(buttonStopProcessing); divMessage = document.createElement('div'); divMessage.classList.add('backup-divMessage' + classAppendNewFreshSpace); divControls.appendChild(divMessage); if (displayUpdate) { setTimeout(() => { addMessage(updates); }, 300); } } function addDropdown(dropdownContainer, AV, BV) { try { if (newFreshSpace) { const biliCardDropdownVisible = document.querySelectorAll('.bili-card-dropdown--visible:not(.backup)'); if (biliCardDropdownVisible.length !== 1) { addMessage('同时发现了多个下拉列表开关, 无法确定下拉列表所对应的视频, 刷新页面可能有帮助', false, true); return; } let divTargetVideo; try { divTargetVideo = document.querySelector('div.items__item:has(.bili-card-dropdown--visible)'); } catch { const items = document.querySelectorAll('div.items__item'); for (const item of items) { if (item.contains(biliCardDropdownVisible[0])) { divTargetVideo = item; break; } } } if (!divTargetVideo) { addMessage('无法确定下拉列表所对应的视频, 请反馈该问题', false, true); return; } if (!settings.processNormal && divTargetVideo.querySelector('.bili-cover-card__stats')) { return; } if (!settings.processDisabled && !divTargetVideo.querySelector('.bili-cover-card__stats')) { return; } BV = biliCardDropdownVisible[0].parentNode.querySelector('a').getAttribute('href').match(getBVFromURLRegex)[1]; AV = AVBVs.find(AVBV => AVBV.bvid === BV).id; } else { if (dropdownContainer.lastElementChild.classList.contains('backup')) { return; } } if (!newFreshSpace) { dropdownContainer.lastElementChild.classList.add('be-dropdown-item-delimiter'); } const backup = GM_getValue(BV, {}); if (backup.cover) { const dropdownCover = document.createElement(newFreshSpace ? 'div' : 'li'); dropdownCover.classList.add(newFreshSpace ? 'bili-card-dropdown-popper__item' : 'be-dropdown-item'); if (!newFreshSpace) { dropdownCover.classList.add('backup'); } dropdownCover.textContent = '封面原图'; dropdownCover.addEventListener('click', () => { try { if (newFreshSpace) { dropdownContainer.classList.remove('visible'); } GM_openInTab(`https://${backup.cover[backup.cover.length - 1].value}`, { active: true, insert: true, setParent: true }); for (let i = backup.cover.length - 2; i >= 0; i--) { GM_openInTab(`https://${backup.cover[i].value}`, { insert: false, setParent: true }); } } catch (error) { catchUnknownError(error); } }); dropdownContainer.appendChild(dropdownCover); } const dropdownLocal = document.createElement('div'); dropdownLocal.classList.add(newFreshSpace ? 'bili-card-dropdown-popper__item' : 'be-dropdown-item'); if (!newFreshSpace) { dropdownLocal.classList.add('backup'); } dropdownLocal.textContent = '本地备份数据'; dropdownLocal.addEventListener('click', () => { try { if (newFreshSpace) { dropdownContainer.classList.remove('visible'); } const data = GM_getValue(BV, {}); const json = JSON.stringify(data); // GM_openInTab('data:text/plain;charset=utf-8,' + encodeURIComponent(json), { active: true, insert: false, setParent: true }); GM_openInTab('data:application/json;charset=utf-8,' + encodeURIComponent(json), { active: true, insert: false, setParent: true }); } catch (error) { catchUnknownError(error); } }); dropdownContainer.appendChild(dropdownLocal); const dropdownJump = document.createElement('div'); dropdownJump.classList.add(newFreshSpace ? 'bili-card-dropdown-popper__item' : 'be-dropdown-item'); if (!newFreshSpace) { dropdownJump.classList.add('backup'); } dropdownJump.textContent = '跳转至BJX'; dropdownJump.addEventListener('click', () => { try { if (newFreshSpace) { dropdownContainer.classList.remove('visible'); } GM_openInTab(`https://www.biliplus.com/video/${BV}`, { active: true, insert: false, setParent: true }); GM_openInTab(`https://www.biliplus.com/video/av${AV}`, { insert: false, setParent: true }); GM_openInTab(`https://www.jiji.moe/video/${BV}`, { insert: false, setParent: true }); GM_openInTab(`https://bbdownloader.com/video/${BV}`, { insert: false, setParent: true }); } catch (error) { catchUnknownError(error); } }); dropdownContainer.appendChild(dropdownJump); const dropdownReset = document.createElement('div'); dropdownReset.classList.add(newFreshSpace ? 'bili-card-dropdown-popper__item' : 'be-dropdown-item'); if (!newFreshSpace) { dropdownReset.classList.add('backup'); } dropdownReset.textContent = '重置备份数据'; dropdownReset.addEventListener('click', () => { try { if (newFreshSpace) { dropdownContainer.classList.remove('visible'); } GM_deleteValue(BV); } catch (error) { catchUnknownError(error); } }); dropdownContainer.appendChild(dropdownReset); } catch (error) { catchUnknownError(error); } } async function getFromApi(AV, BV, title, backup, spanA, apiDetails, fid, pageNumber, disabled, spanFavTime) { if (!backup.AV) { backup.AV = AV; } if (!backup.BV) { backup.BV = BV; } if (!apiDetails.value) { 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=${pageSize}&order=mtime`, 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']) }); }); apiDetails.value = response.response.data.medias; } const apiDetail = apiDetails.value.find(a => a.bvid === BV); if (!apiDetail) { throw ['从B站接口获取数据失败, 切换至按最近收藏排序可能有帮助']; } if (disabled) { backup.api = { value: false, ts: getCurrentTs() }; spanA.style.color = '#ff0000'; } else { backup.api = { value: true, ts: getCurrentTs() }; spanA.style.color = '#00ff00'; } if (settings.enableDebug) console.warn('从B站接口获取数据'); if (settings.enableDebug) consoleAVBVTitle('log', AV, BV, title); if (settings.enableDebug) console.log(apiDetail); if (!disabled) { updateArrayDataInBackup(backup, 'title', apiDetail.title, getCurrentTs(), 'api.bilibili.com/x/v3/fav/resource/list'); updateArrayDataInBackup(backup, 'cover', apiDetail.cover.replace(getHttpsFromURLRegex, ''), getCurrentTs(), 'api.bilibili.com/x/v3/fav/resource/list'); } updateArrayDataInBackup(backup, 'intro', apiDetail.intro, getCurrentTs(), 'api.bilibili.com/x/v3/fav/resource/list'); if (!disabled && apiDetail.intro.length >= 255) { const response = await new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'GET', url: `https://api.bilibili.com/x/web-interface/archive/desc?bvid=${BV}`, timeout: 5000, responseType: 'json', onload: (res) => resolve(res), onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/web-interface/archive/desc', res.error]), ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/web-interface/archive/desc']) }); }); updateArrayDataInBackup(backup, 'intro', response.response.data, getCurrentTs(), 'api.bilibili.com/x/web-interface/archive/desc'); } if (!backup.upperUID) { backup.upperUID = apiDetail.upper.mid; } updateArrayDataInBackup(backup, 'upperName', apiDetail.upper.name, getCurrentTs(), 'api.bilibili.com/x/v3/fav/resource/list'); updateArrayDataInBackup(backup, 'upperAvatar', apiDetail.upper.face.replace(getHttpsFromURLRegex, ''), getCurrentTs(), 'api.bilibili.com/x/v3/fav/resource/list'); backup.timeUpload = apiDetail.ctime; backup.timePublish = apiDetail.pubtime; if (!backup.timeFavorite) { backup.timeFavorite = []; const data = { value: apiDetail.fav_time, fid: fid }; backup.timeFavorite.push(data); if (settings.enableDebug) console.log('初始化timeFavorite'); if (settings.enableDebug) console.log(data); } else { const target = backup.timeFavorite.find(el => el.fid === fid); if (target) { if (target.value !== apiDetail.fav_time) { target.value = apiDetail.fav_time; backup.timeFavorite.sort((a, b) => a.value - b.value); if (settings.enableDebug) console.log('更新timeFavorite'); } } else { const data = { value: apiDetail.fav_time, fid: fid }; backup.timeFavorite.push(data); backup.timeFavorite.sort((a, b) => a.value - b.value); if (settings.enableDebug) console.log('新添加timeFavorite'); if (settings.enableDebug) console.log(data); } } spanFavTime.innerText = `收藏于:${formatTsTimeFavorite(new Date(apiDetail.fav_time * 1000))}`; spanFavTime.setAttribute('title', new Date(apiDetail.fav_time * 1000).toLocaleString()); const sortedBackup = {}; for (const sortedKey of sortedKeys) { sortedBackup[sortedKey] = backup[sortedKey]; } GM_setValue(BV, sortedBackup); if (settings.enableDebug) console.warn('保存B站接口的数据至本地'); if (settings.enableDebug) consoleAVBVTitle('warn', AV, BV, title); if (settings.enableDebug) console.warn(sortedBackup); } async function getFromBiliplus(AV, BV, title, backup, spanB) { const response = await new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'GET', url: `https://www.biliplus.com/video/av${AV}`, timeout: 5000, onload: (res) => resolve(res), onerror: (res) => reject(['请求失败', 'www.biliplus.com/video', res.error]), ontimeout: () => reject(['请求超时', 'www.biliplus.com/video']) }); }); const json = JSON.parse(response.response.match(getJsonFromBiliplusRegex)[1]); if (json.title) { backup.biliplus = { value: true, ts: getCurrentTs() }; spanB.style.color = '#00ff00'; if (settings.enableDebug) console.warn('从BiliPlus获取有效数据'); if (settings.enableDebug) consoleAVBVTitle('log', AV, BV, title); if (settings.enableDebug) console.log(json); if (!json.lastupdatets) { if (!json.lastupdate) { throw ['从BiliPlus获取的数据中无备份时间, 请反馈该问题']; } if (!localeTimeStringRegex.test(json.lastupdate)) { throw ['从BiliPlus获取的数据中备份时间不符合规范, 请反馈该问题']; } if (isNaN(new Date(json.lastupdate).getTime())) { throw ['从BiliPlus获取的数据中备份时间不符合规范, 请反馈该问题']; } json.lastupdatets = new Date(json.lastupdate).getTime() / 1000; } updateArrayDataInBackup(backup, 'title', json.title, json.lastupdatets, 'www.biliplus.com/video'); updateArrayDataInBackup(backup, 'cover', json.pic.replace(getHttpsFromURLRegex, ''), json.lastupdatets, 'www.biliplus.com/video'); updateArrayDataInBackup(backup, 'intro', json.description, json.lastupdatets, 'www.biliplus.com/video'); updateArrayDataInBackup(backup, 'upperName', json.author, json.lastupdatets, 'www.biliplus.com/video'); if (json.v2_app_api) { updateArrayDataInBackup(backup, 'title', json.v2_app_api.title, json.lastupdatets, 'www.biliplus.com/video'); updateArrayDataInBackup(backup, 'cover', json.v2_app_api.pic.replace(getHttpsFromURLRegex, ''), json.lastupdatets, 'www.biliplus.com/video'); updateArrayDataInBackup(backup, 'intro', json.v2_app_api.desc, json.lastupdatets, 'www.biliplus.com/video'); updateArrayDataInBackup(backup, 'upperName', json.v2_app_api.owner.name, json.lastupdatets, 'www.biliplus.com/video'); updateArrayDataInBackup(backup, 'upperAvatar', json.v2_app_api.owner.face.replace(getHttpsFromURLRegex, ''), json.lastupdatets, 'www.biliplus.com/video'); if (json.v2_app_api.first_frame) { updateArrayDataInBackup(backup, 'firstFrame', json.v2_app_api.first_frame.replace(getHttpsFromURLRegex, ''), json.lastupdatets, 'www.biliplus.com/video'); } } } else { backup.biliplus = { value: false, ts: getCurrentTs() }; spanB.style.color = '#ff0000'; if (settings.enableDebug) console.warn('从BiliPlus获取无效数据'); if (settings.enableDebug) consoleAVBVTitle('warn', AV, BV, title); if (settings.enableDebug) console.warn(json); } } async function getFromJiji(AV, BV, title, backup, spanJ) { let retryCount = 0; while (true) { const response = await new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'GET', url: `https://www.jiji.moe/api/v1/video_bv/get_info?id=${BV.slice(2)}`, timeout: 5000, responseType: 'json', onload: (res) => resolve(res), onerror: (res) => reject(['请求失败', 'www.jiji.moe/api/v1/video_bv/get_info', res.error]), ontimeout: () => reject(['请求超时', 'www.jiji.moe/api/v1/video_bv/get_info']) }); }); if (settings.enableDebug) console.log(response); if (response.status !== 200) { throw ['请求失败', 'www.jiji.moe/api/v1/video_bv/get_info', `${response.status} ${response.statusText}`]; } const json = response.response; if (json.upid > 0) { backup.jiji = { value: true, ts: getCurrentTs() }; spanJ.style.color = '#00ff00'; if (settings.enableDebug) console.warn('从唧唧获取有效数据'); if (settings.enableDebug) consoleAVBVTitle('log', AV, BV, title); if (settings.enableDebug) console.log(json); updateArrayDataInBackup(backup, 'title', json.title, json.ltime, 'www.jiji.moe/api/v1/video_bv/get_info'); updateArrayDataInBackup(backup, 'cover', json.img.replace(getHttpsFromURLRegex, ''), json.ltime, 'www.jiji.moe/api/v1/video_bv/get_info'); updateArrayDataInBackup(backup, 'intro', decodeHTMLEntities(json.desc.replaceAll('<br/>', '\n').replaceAll('\r', '\\r')).replaceAll('\\r', '\r'), json.ltime, 'www.jiji.moe/api/v1/video_bv/get_info'); if (json.up.id > 0) { updateArrayDataInBackup(backup, 'upperName', json.up.author, json.ltime, 'www.jiji.moe/api/v1/video_bv/get_info'); updateArrayDataInBackup(backup, 'upperAvatar', json.up.avatar.replace(getHttpsFromURLRegex, ''), json.ltime, 'www.jiji.moe/api/v1/video_bv/get_info'); } return; } else if (json.msg === 'loading') { retryCount++; if (settings.enableDebug) console.warn('从唧唧获取无效数据'); if (settings.enableDebug) consoleAVBVTitle('warn', AV, BV, title); if (settings.enableDebug) console.warn(`请求重试次数: ${retryCount}`); if (settings.enableDebug) console.warn(json); if (retryCount > 4) { throw ['请求重试次数过多', 'www.jiji.moe/api/v1/video_bv/get_info']; } } else { backup.jiji = { value: false, ts: getCurrentTs() }; spanJ.style.color = '#ff0000'; if (settings.enableDebug) console.warn('从唧唧获取无效数据'); if (settings.enableDebug) consoleAVBVTitle('warn', AV, BV, title); if (settings.enableDebug) console.warn(json); return; } await delay(600); } } async function getFromBbdownloader(AV, BV, title, backup, spanX) { const response = await new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'GET', url: `https://bbdownloader.com/video/${BV}`, timeout: 5000, onload: (res) => resolve(res), onerror: (res) => reject(['请求失败', 'bbdownloader.com/video', res.error]), ontimeout: () => reject(['请求超时', 'bbdownloader.com/video']) }); }); if (response.finalUrl !== 'https://bbdownloader.com/') { backup.bbdownloader = { value: true, ts: getCurrentTs() }; spanX.style.color = '#00ff00'; if (settings.enableDebug) console.warn('从贝贝工具站获取有效数据'); if (settings.enableDebug) consoleAVBVTitle('log', AV, BV, title); if (settings.enableDebug) console.log(response); updateArrayDataInBackup(backup, 'title', response.responseXML.querySelector('h5.fw-bold').innerText, undefined, 'bbdownloader.com/video'); updateArrayDataInBackup(backup, 'cover', response.responseXML.querySelector('div.col-4 > img').getAttribute('src').replace(getHttpsFromURLRegex, ''), undefined, 'bbdownloader.com/video'); updateArrayDataInBackup(backup, 'intro', decodeHTMLEntities(response.responseXML.querySelector('div.col-8 > textarea').innerText), undefined, 'bbdownloader.com/video'); updateArrayDataInBackup(backup, 'upperName', response.responseXML.querySelector('div.input-group.mb-2 > input').value, undefined, 'bbdownloader.com/video'); } else { backup.bbdownloader = { value: false, ts: getCurrentTs() }; spanX.style.color = '#ff0000'; if (settings.enableDebug) console.warn('从贝贝工具站获取无效数据'); if (settings.enableDebug) consoleAVBVTitle('warn', AV, BV, title); if (settings.enableDebug) console.warn(response); } } 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) { p.style.borderTop = '1px solid #ff0000'; } divMessage.appendChild(p); if (divMessageHeightFixed) { divMessage.scrollTop = divMessage.scrollHeight; } else { if (newFreshSpace) { if (divMessage.scrollHeight > 300) { divMessage.style.height = '300px'; divMessageHeightFixed = true; divMessage.scrollTop = divMessage.scrollHeight; } } else { if (divMessage.scrollHeight > 250) { divMessage.style.height = '250px'; divMessageHeightFixed = true; divMessage.scrollTop = divMessage.scrollHeight; } } } divMessage.scrollIntoView({ behavior: 'instant', block: 'nearest' }); } // function clearMessage() { // while (divMessage.firstChild) { // divMessage.removeChild(divMessage.firstChild); // } // } function addMessageAVBVTitle(AV, BV, title) { addMessage(`AV号: ${AV}`, true); addMessage(`BV号: ${BV}`, true); if (title) { addMessage(`标题: ${title.slice(0, 13)}`, true); } } function consoleAVBVTitle(type, AV, BV, title) { switch (type) { case 'log': console.log(`AV号: ${AV}`); console.log(`BV号: ${BV}`); console.log(`标题: ${title.slice(0, 13)}`); break; case 'warn': console.warn(`AV号: ${AV}`); console.warn(`BV号: ${BV}`); console.warn(`标题: ${title.slice(0, 13)}`); break; case 'error': console.error(`AV号: ${AV}`); console.error(`BV号: ${BV}`); if (title) { console.error(`标题: ${title.slice(0, 13)}`); } break; default: throw Error('invalid type'); } } function catchUnknownError(error) { addMessage('发生未知错误, 请反馈该问题', false, true); addMessage(error.stack, true); console.error(error); } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function getCurrentTs() { return Math.floor(Date.now() / 1000); } function abortActiveControllers() { for (const controller of activeControllers) { controller.abort(); } } function formatTsTimeFavorite(t) { const e = new Date(); const n = e.getTime(); const r = t.getTime(); const o = n - r; return o < 6e4 ? '刚刚' : o < 36e5 ? Math.floor(o / 6e4) + '分钟前' : o < 864e5 ? Math.floor(o / 36e5) + '小时前' : r >= new Date(e.getFullYear(), e.getMonth(), e.getDate() - 1).getTime() ? '昨天' : r >= new Date(e.getFullYear(), 0, 1).getTime() ? (t.getMonth() + 1) + '-' + t.getDate() // : o < 63072e6 // ? t.getFullYear() + '-' + (t.getMonth() + 1) + '-' + t.getDate() // : '2年前'; : t.getFullYear() + '-' + (t.getMonth() + 1) + '-' + t.getDate(); } function formatTsYYMMDD_HHMMSS(ts) { const date = new Date(ts * 1000); const year = String(date.getFullYear()).slice(2); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}${month}${day}_${hours}${minutes}${seconds}`; } function decodeHTMLEntities(str) { return new DOMParser().parseFromString(`<!doctype html><body>${str}`, 'text/html').body.textContent; } function updateArrayDataInBackup(backup, key, value, ts = 0, from) { if (!backup[key]) { backup[key] = []; const data = { value, ts, from }; backup[key].push(data); if (settings.enableDebug) console.log(`初始化${key}`); if (settings.enableDebug) console.log(data); return; } let target; target = backup[key].find(el => el.value === value); if (target) { if (settings.enableDebug) console.log(`已存在${key}`); if (target.ts <= ts) { target.ts = ts; target.from = from; backup[key].sort((a, b) => a.ts - b.ts); if (settings.enableDebug) console.warn(`重写${key}时间戳和数据来源`); } return; } if (key === 'cover' || key === 'upperAvatar' || key === 'firstFrame') { target = backup[key].find(el => el.value.match(getFilenameFromURLRegex)[0] === value.match(getFilenameFromURLRegex)[0]); if (target) { if (target.ts <= ts) { target.value = value; target.ts = ts; target.from = from; backup[key].sort((a, b) => a.ts - b.ts); } return; } } else if (key === 'intro') { target = backup.intro.find(el => el.value.replaceAll('\r', '') === value); if (target) { return; } target = backup.intro.find(el => el.value === value.replaceAll('\r', '')); if (target) { target.value = value; target.ts = ts; target.from = from; backup.intro.sort((a, b) => a.ts - b.ts); return; } if (value.length >= 255 || ((from.includes('xbeibeix') || from.includes('bbdownloader')) && value.length >= 200)) { target = backup.intro.find(el => el.value.replaceAll('\r', '').startsWith(value.replaceAll('\r', ''))); if (target) { return; } target = backup.intro.find(el => value.replaceAll('\r', '').startsWith(el.value.replaceAll('\r', ''))); if (target) { target.value = value; target.ts = ts; target.from = from; backup.intro.sort((a, b) => a.ts - b.ts); return; } } } const data = { value, ts, from }; backup[key].push(data); backup[key].sort((a, b) => a.ts - b.ts); if (settings.enableDebug) console.log(`新添加${key}`); if (settings.enableDebug) console.log(data); } // async function mainNewFreshSpace(mutations) { // async function mainNewFreshSpace() { // mutations_count++; // console.error('mutations_count' + mutations_count); // for (const mutation of mutations) { // mutation_count++; // console.warn('mutation_count' + mutation_count); // console.log(mutation); // } // if (firstTime) { // firstTime = false; // } else { // mutations = mutations.reverse(); // } // for (const mutation of mutations) { // if (mutation.addedNodes.length) { // const items = document.querySelectorAll('.items__item'); // for (const item of items) { // console.log(mutation.addedNodes[0].querySelectorAll('a')); // console.log(mutation.addedNodes[0].querySelectorAll('a')[1].innerText); // mutation.addedNodes[0].querySelectorAll('a')[1].style.color = 'red'; // } // } })();