// ==UserScript== // @name 哔哩哔哩(B站|Bilibili)收藏夹Fix (cerenkov修改版) // @namespace http://tampermonkey.net/ // @version 1.4.3 // @description 修复 哔哩哔哩(www.bilibili.com) 失效的视频收藏、和被up主隐藏的视频。(可查看av号、简介、标题、封面、数据等) // @note 1.4.* 版本主要更新: // @note 支持恢复被隐藏(up主设置为“仅自己可见”)的视频信息,让收藏夹不再“缺一角” // @note 多个数据来源: biliplus jijidown xbeibeix B站官方鉴权API、 B站官方非鉴权API,最大程度恢复信息 // @note 建议将收藏夹设为公开,可以通过 B站官方非鉴权API 查询并恢复很多视频信息,脚本缓存后再改回私密 // @note 查询结果缓存在篡改猴(Tampermonkey)的本地存储,免除对同一个失效视频的反复查询,刷新标签页、关闭浏览器也不再丢失数据 // @author cerenkov // @license GPL-3.0 // @match *://space.bilibili.com/*/favlist* // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.min.js // @resource iconError https://cdn.jsdelivr.net/gh/crnkv/bilibili-favorites-fix-cerenkov-mod/media/error.png // @resource iconSuccess https://cdn.jsdelivr.net/gh/crnkv/bilibili-favorites-fix-cerenkov-mod/media/success.png // @resource iconInfo https://cdn.jsdelivr.net/gh/crnkv/bilibili-favorites-fix-cerenkov-mod/media/info.png // @connect api.bilibili.com // @connect biliplus.com // @connect jijidown.com // @connect xbeibeix.com // @grant unsafeWindow // @grant GM.xmlHttpRequest // @grant GM_notification // @grant GM_setClipboard // @grant GM_getResourceURL // @grant GM_openInTab // @grant GM_listValues // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_setValues // @grant GM_getValues // @grant GM_deleteValues // @downloadURL none // ==/UserScript== /*jshint esversion: 8 */ (function() { 'use strict'; // 改成true可以启用调试模式 // 注意:如果代码被修改,脚本的自动更新会停止,需要在Tampermonkey编辑器中将脚本重置到出厂,才能恢复自动更新(重置之前请记得先导出缓存备份) const isDebug = false; // 从监测到网页节点变动,到执行脚本修复之间的延迟(秒),延迟太短可能会导致收藏夹翻页的网页渲染未完成而脚本已经开始干预,造成意外后果 const delay = 2.0; // 失效收藏的标题文字颜色(默认为灰色)。 const invalTitleColor = "#999"; // 被恢复的隐藏视频的背景颜色 const recoveredItemColor = "#fa2"; // 从分P视频的第一P获取到标题时加以后缀标注 const titleGuessSuffix = " (视频投稿上传时的标题)"; // 全局变量,用于保存是否B站新网页界面,脚本自动检测 let isNewUI; // 全局变量,用于保存B新/旧界面各自的视频根节点,脚本自动检测 let $rootItem; // 全局变量,用于保存由B站API端口查询得到的本应展示的视频总数(当中包含被隐藏的视频,即被up主设为“仅自己可见”的视频) let NTotalItems = undefined; // 缓存已经查询过并且有结果的视频标题和封面(包括查到的和查不到的,不包括查询过程中请求过快、网络错误和解析错误的) let cache = { clear: function() { GM_deleteValues(GM_listValues()); }, delete: function(avid, key) { if (key == undefined) { GM_deleteValue(avid); // cache.delete(avid) 删除一条avid缓存对象 } else { let c = GM_getValue(avid, {}); delete c[key]; if (Object.keys(c).length == 0) { this.delete(avid); // 如果所删除的key是avid缓存对象唯一的key属性,那么将整条avid缓存对象删除 } else { GM_setValue(avid, c); // cache.delete(avid, key) 删除avid缓存对象的对应key属性 } } }, set: function(avid, key, value) { if (key == undefined) { value = avid; if (typeof value !== "object") throw "格式不正确"; GM_setValues(value); // cache.set(value) 把 value 对象内的所有avid项都覆盖保存到所有缓存 } else if (value == undefined) { // 注意永远不要故意传入value=undefined,应该要用delete方法 value = key; if (typeof value !== "object") throw "格式不正确"; if (Object.keys(value).length == 0) { this.delete(avid); // cache.set(avid, {}) 删除对应avid缓存对象 } else { GM_setValue(avid, value); // cache.set(avid, value) 把 value 对象覆盖保存到对应avid缓存对象 } } else { if (key == "title") value = $("
").html(value).text(); // decode HTML entities let c = GM_getValue(avid, {}); c[key] = value; GM_setValue(avid, c); // cache.set(avid, key, value) 把 value 值覆盖保存到对应avid缓存的对应key属性 } }, get: function(avid, key, defaultValue) { if (defaultValue == undefined) { // 注意永远不要故意传入defaultValue=undefined switch (key) { case "archive": defaultValue = undefined; break; case "title": case "pic": case "ff": case "author": defaultValue = ""; break; case "parts": defaultValue = []; break; case "tid": case "thumb_up": case "coin": case "reply": defaultValue = 0; break; default: // do nothing }; } if (avid == undefined) { return GM_getValues(GM_listValues()); // cache.get() 返回所有缓存 } else if (key == undefined) { return GM_getValue(avid, undefined); // cache.get(avid) 返回一条avid缓存对象(未命中时返回 undefined ) } else { let v = GM_getValue(avid, {})[key]; // cache.get(avid, key) 返回对应avid缓存的对应key属性(未命中时返回 switch-case 指定的 defaultValue ) return v == undefined ? defaultValue : v; // cache.get(avid, key, defaultValue) 返回对应avid缓存的对应key属性(未命中时返回 defaultValue ) } }, update: function(avid, key, newValue) { let oldValue = this.get(avid, key); if (!newValue || newValue === oldValue) return oldValue; let isBetter = false; switch (key) { case "archive": isBetter = (oldValue == undefined && newValue !== undefined) || (oldValue == "nohit" && (newValue == "bp" || newValue == "jj")) || (oldValue == "jj" && newValue == "bp"); break; case "title": newValue = $("").html(newValue).text().trim(); // decode HTML entities if (newValue == "" || newValue == "已失效视频" || newValue == avid) break; isBetter = (oldValue == "" || oldValue == "已失效视频") || (oldValue.includes(titleGuessSuffix) && !newValue.includes(titleGuessSuffix)); break; case "pic": if (/bfs\/archive\/be27fd62c99036dce67efface486fb0a88ffed06/i.test(newValue)) break; if (newValue.includes("@")) newValue = newValue.match(/([^@]*)@/)[1]; isBetter = (oldValue == "" && newValue !== "") || (!/bfs\/archive/i.test(oldValue) && /bfs\/archive/i.test(newValue)); break; case "ff": isBetter = oldValue == "" && newValue !== ""; break; case "author": isBetter = oldValue == "" && newValue !== "" && newValue !== "账号已注销"; case "parts": isBetter = oldValue.length < 2 && newValue.length > 1; break; case "tid": isBetter = (!oldValue) && (!!newValue); break; case "thumb_up": case "coin": case "reply": isBetter = (!!newValue) && newValue > oldValue; break; default: // do nothing } if (isBetter) { this.set(avid, key, newValue); return newValue; } else { return oldValue; } }, export: function() { return JSON.stringify(this.get()); }, import: function(str) { let incremental = str[0] == "+"; if (incremental) str = str.slice(1); let json = JSON.parse(str); if (typeof json !== "object") { throw "JSON格式不正确"; } else { for (let avid in json) { if (typeof json[avid] !== "object") throw "JSON格式不正确"; if (json[avid].success == true) { json[avid].archive = "bp"; } delete json[avid].success; } } if (incremental) { for (let avid in json) { this.set(avid, json[avid]); } } else { this.clear(); this.set(json); } } }; const categoriesArray = [["全部分区", 0], ["动画", 1], ["音乐", 3], ["游戏", 4], ["娱乐", 5], ["电视剧", 11], ["番剧", 13], ["单机游戏", 17], ["Mugen", 19], ["宅舞", 20], ["日常", 21], ["鬼畜调教", 22], ["电影", 23], ["MAD·AMV", 24], ["MMD·3D", 25], ["音MAD", 26], ["综合", 27], ["原创音乐", 28], ["音乐现场", 29], ["VOCALOID·UTAU", 30], ["翻唱", 31], ["完结动画", 32], ["连载动画", 33], ["完结剧集", 34], ["知识", 36], ["人文·历史", 37], ["演讲·公开课", 39], ["短片·手书", 47], ["资讯", 51], ["演奏", 59], ["网络游戏", 65], ["综艺", 71], ["动物综合", 75], ["美食制作", 76], ["其他国家", 83], ["小剧场", 85], ["特摄", 86], ["数码", 95], ["星海", 96], ["机械", 98], ["鬼畜", 119], ["GMV", 121], ["野生技术协会", 122], ["社科·法律·心理", 124], ["人力VOCALOID", 126], ["教程演示", 127], ["舞蹈", 129], ["音乐综合", 130], ["Korea相关", 131], ["音游", 136], ["明星综合", 137], ["搞笑", 138], ["欧美电影", 145], ["日本电影", 146], ["华语电影", 147], ["官方延伸", 152], ["国产动画", 153], ["舞蹈综合", 154], ["时尚", 155], ["舞蹈教程", 156], ["美妆护肤", 157], ["穿搭", 158], ["时尚潮流", 159], ["生活", 160], ["手工", 161], ["绘画", 162], ["运动", 163], ["健身", 164], ["广告", 165], ["国创", 167], ["国产原创相关", 168], ["布袋戏", 169], ["资讯", 170], ["电子竞技", 171], ["手机游戏", 172], ["桌游棋牌", 173], ["其他", 174], ["汽车生活", 176], ["纪录片", 177], ["科学·探索·自然", 178], ["军事", 179], ["社会·美食·旅行", 180], ["影视", 181], ["影视杂谈", 182], ["影视剪辑", 183], ["预告·资讯", 184], ["国产剧", 185], ["海外剧", 187], ["科技", 188], ["电脑装机", 189], ["摄影摄像", 190], ["影音智能", 191], ["风尚标", 192], ["MV", 193], ["电音", 194], ["动态漫·广播剧", 195], ["街舞", 198], ["明星舞蹈", 199], ["国风舞蹈", 200], ["科学科普", 201], ["资讯", 202], ["热点", 203], ["环球", 204], ["社会", 205], ["综合", 206], ["财经商业", 207], ["校园学习", 208], ["职业职场", 209], ["手办·模玩", 210], ["美食", 211], ["美食侦探", 212], ["美食测评", 213], ["田园美食", 214], ["美食记录", 215], ["鬼畜剧场", 216], ["动物圈", 217], ["喵星人", 218], ["汪星人", 219], ["动物二创", 220], ["野生动物", 221], ["小宠异宠", 222], ["汽车", 223], ["汽车文化", 224], ["汽车极客", 225], ["智能出行", 226], ["购车攻略", 227], ["人文历史", 228], ["设计·创意", 229], ["软件应用", 230], ["计算机技术", 231], ["科工机械", 232], ["极客DIY", 233], ["运动", 234], ["篮球", 235], ["竞技体育", 236], ["运动文化", 237], ["运动综合", 238], ["家居房产", 239], ["摩托车", 240], ["娱乐杂谈", 241], ["粉丝创作", 242], ["乐评盘点", 243], ["音乐教学", 244], ["赛车", 245], ["改装玩车", 246], ["新能源车", 247], ["房车", 248], ["足球", 249], ["出行", 250], ["三农", 251], ["仿妆cos", 252], ["动漫杂谈", 253], ["亲子", 254], ["手势·网红舞", 255], ["短片", 256], ["配音", 257], ["汽车知识科普", 258], ["版权内容", -24]]; const categoriesDictReversed = Object.fromEntries(categoriesArray); const categoriesDict = Object.fromEntries(categoriesArray.map(x => x.reverse())); var XOR_CODE = 23442827791579n; var MASK_CODE = 2251799813685247n; var BASE = 58n; var CHAR_TABLE = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf"; function bv2av(bvid) { const bvidArr = Array.from(bvid); [bvidArr[3], bvidArr[9]] = [bvidArr[9], bvidArr[3]]; [bvidArr[4], bvidArr[7]] = [bvidArr[7], bvidArr[4]]; bvidArr.splice(0, 3); const tmp = bvidArr.reduce((pre, bvidChar) => pre * BASE + BigInt(CHAR_TABLE.indexOf(bvidChar)), 0n); return Number((tmp & MASK_CODE) ^ XOR_CODE); } async function fetchJSON(url) { if (isDebug) console.log(`[bilibili-fav-fix] fetchJSON for ${url}`); let res = await GM.xmlHttpRequest({ method: 'GET', url: url, responseType: "json" }).catch(e => { console.error(e); // e里含有网络请求对象,res里不含 }); if (isDebug) console.log(res); if (!res) { tipError(`收藏夹修复错误:${url} 网站无法访问,可能需要检查网络连接并手动刷新重试`); return null; } else if (res.status == 502 || res.status == 503) { // jijidown 和 biliplus 的常见错误,应该是请求过于频繁 console.error(`[bilibili-fav-fix] network connection with status code ${res.status}: ${url}`); if (url.includes("biliplus")) { // 让返回值可以被优雅地解读为请求过快并处理 return {code: -503}; } else if (url.includes("jijidown")) { return {code: 0}; } } else if (res.status !== 200) { console.error(`[bilibili-fav-fix] network connection with status code ${res.status}: ${url}`); // tipError(`收藏夹修复错误:${url} 网站无法访问(错误代码${res.status}),可能需要检查网络连接并手动刷新重试`); return null; } else if (res.response == undefined) { console.error(`[bilibili-fav-fix] website not responding in a valid JSON format: ${url}`); tipError(`收藏夹修复错误:${url} 网站返回的数据格式不正确`); return null; } else { return res.response; } } async function fetchHTMLDoc(url) { if (isDebug) console.log(`[bilibili-fav-fix] fetchHTML for ${url}`); let res = await GM.xmlHttpRequest({ method: 'GET', url: url, redirect: "manual", cookiePartition: { topLevelSite: 'https://xbeibeix.com' } }).catch(e => { console.error(e); // e里含有网络请求对象,res里不含 }); if (isDebug) console.log(res); if (!res) { tipError(`收藏夹修复错误:${url} 网站无法访问,可能需要检查网络连接并手动刷新重试`); return null; } else if (res.status == 403) { if (isDebug) console.log(`[bilibili-fav-fix] network connection with status code ${res.status}: ${url}`); return "verifyHuman"; } else if ((res.status == 301 || res.status == 302) && res.responseHeaders.match(/\nlocation:\/\r?\n/i)) { if (isDebug) console.log(`[bilibili-fav-fix] network connection redirected to homepage: ${url}`); return "redirected"; } else if (res.status !== 200) { console.error(`[bilibili-fav-fix] network connection with status code ${res.status}: ${url}`); // tipError(`收藏夹修复错误:${url} 网站无法访问(错误代码${res.status}),可能需要检查网络连接并手动刷新重试`); return null; } else { try { let doc = new DOMParser().parseFromString(res.responseText, 'text/html'); if (isDebug) console.log(doc); return doc; } catch (e) { console.error(`[bilibili-fav-fix] website not responding in a valid HTML format: ${url}`); tipError(`收藏夹修复错误:${url} 网站返回的数据格式不正确`); return null; } } } // 脚本主入口 // 函数调用逻辑: // - handleFavorites 调用 startBilibiliApiQuery startBiliplusQuery 和 queryCached (如果命中缓存) // - startBiliplusQuery 调用 queryHit (如果查询有结果)和 startJijidownQuery (如果查询无结果) // - startJijidownQuery 调用 queryHit (如果查询有结果)和 startXbeibeixQuery (如果查询无结果) // - startXbeibeixQuery 调用 queryHit (如果查询有结果)和 queryFailed(如果查询无结果) // - queryHit 条件性调用 refineBiliplusQuery (如果有 refine 需求) // - queryFailed 调用 queryCached // - queryCached 条件性调用 refineBiliplusQuery (如果有 refine 需求) // - startBilibiliApiQuery 设定是否需要 refine 的条件,且条件性调用 recoverHiddenItems // - 最终负责执行的是 setTitleLink setTitleText setCoverPic setTooltip replaceTooltip replaceAuthorText 等等 function handleFavorites() { if (isDebug) console.log(`[bilibili-fav-fix] isNewUI: ${isNewUI}`); // 失效收藏节点集 let $allItems = getAllItems().toArray().map(item => $(item)); let $targetItems = []; if (isNewUI) { $targetItems = $allItems.filter($item => $item.find(".bili-video-card__title a").first().text() == "已失效视频"); } else if ($("ul.fav-video-list.content").length > 0) { $targetItems = $allItems.filter($item => $item.hasClass("disabled")); } else { console.error('[bilibili-fav-fix] B站网页样式无法识别'); } if ($targetItems.length > 0 || NTotalItems == undefined || $allItems.length < NTotalItems) { if ($targetItems.length > 0) console.log(`[bilibili-fav-fix] ${$targetItems.length}个失效收藏待修复...`); if (NTotalItems == undefined) { if (isDebug) console.log(`[bilibili-fav-fix] 潜在可能有被隐藏的收藏待修复...`); } else if ($allItems.length < NTotalItems) { console.log(`[bilibili-fav-fix] ${NTotalItems - $allItems.length}个被隐藏的收藏待修复...`); } // 预处理 $allItems $targetItems 移除多余元素和样式 添加功能菜单 $allItems.forEach(function($item) { setupItem($item); }); $targetItems.forEach(function($item) { if (isDebug) console.log(`[bilibili-fav-fix] item needed to fix: ${$item.attr("bvid")} ( ${$item.attr("avid")} )`); // 移除无效的备用封面(有一版B站UI用过这种设计) $item.find("source").remove(); // 移除旧UI的禁用样式 if (!isNewUI) { $item.removeClass("disabled"); getCoverElem($item).removeClass("disabled") getTitleElem($item).removeClass("disabled"); } // 添加功能菜单 addCopyAVIDButton($item); addCopyBVIDButton($item); addCopyInfoButton($item); addOpenPicButton($item); addSaveLoadCacheButton($item); // addDeleteThisButton($item); }); startBilibiliApiQuery($targetItems, $allItems); // 分离已缓存条目和待查询条目 let $queryItems = {}; $targetItems.forEach(function($item) { const avid = $item.attr("avid"); let c = cache.get(avid); if (c && c.archive !== undefined) { // c.archive 无论是 bp jj 还是 nohit ,都表明biliplus或jijidown的查询结果都已保存在cache中 queryCached($item, avid, c); } else { // 完全没查询过,或者只保存了biliAPI的查询结果,未查清、缓存biliplus或jijidown的查询结果 $queryItems[avid] = $item; } }); if (Object.keys($queryItems).length > 0) startBiliplusQuery($queryItems); } } async function startBiliplusQuery($queryItems) { let avids = Object.keys($queryItems); if (isDebug) console.log(`[bilibili-fav-fix] startBiliplusQuery for ${avids.length} items: ${avids.join(', ')}`); for (let $item of Object.values($queryItems)) { setTitleText($item, "正在查询 biliplus ..."); } const json = await fetchJSON(`https://www.biliplus.com/api/aidinfo?aid=${avids.join(',')}`); if (!json) { // 由于网络请求遇到故障中断、网站暂时下线等,重试也无益,姑且尝试jijidown,不代表biliplus上真的没数据 startJijidownQuery($queryItems, false); } else if (json.code == -503) { // 请求过快,手动点击重试 if (isDebug) console.log(`[bilibili-fav-fix] biliplus 请求过快 for ${avids.length} items: ${avids.join(', ')}`); for (let $item of Object.values($queryItems)) { setTitleRetry($item, {items: $queryItems}, function(event) { event.preventDefault(); let $queryIts = event.data.items; // 取出成局部变量,以便.off()时销毁event.data,函数结束后亦销毁局部变量 for (let $it of Object.values($queryIts)) { getTitleElem($it).off("click"); } startBiliplusQuery($queryIts); }); } } else if (json.code !== 0) { // json.code == -404 全无记录 -403 访问权限不足(up主隐藏) if (isDebug) console.log(`[bilibili-fav-fix] biliplus no results for ${avids.length} items: ${avids.join(', ')}`); startJijidownQuery($queryItems); } else { // 至少部分avid有记录 if (isDebug) console.log(`[bilibili-fav-fix] biliplus has ${Object.keys(json.data).length} hits`); for (let avid in json.data) { if (isDebug) console.log(`[bilibili-fav-fix] biliplus retrieved info for ${avid}`); let info = json.data[avid]; let $item = $queryItems[avid]; let pic = cache.update(avid, "pic", info.pic); queryHit($item, avid, info.title, pic, info.author, "bp"); if (!/bfs\/archive/i.test(pic)) { // 极大概率是失效的旧图片链接 if (isDebug) console.log(`[bilibili-fav-fix] query for better pic for ${avid}`); startJijidownQuery(Object.fromEntries([[avid, $item]]), false, true); } delete $queryItems[avid]; } if (Object.keys($queryItems).length > 0) startJijidownQuery($queryItems); } } function startJijidownQuery($queryItems, isDecisive = true, isForPic = false, silent = true) { if (isDebug) console.log(`[bilibili-fav-fix] startJijidownQuery for ${Object.keys($queryItems).length} items`); for (let [avid, $item] of Object.entries($queryItems)) { // 并发网络请求 for 循环 if (isDebug) console.log(`[bilibili-fav-fix] startJijidownQuery for ${avid}`); if (!isForPic) { setTitleText($item, "正在查询 jijidown ..."); } else if (!silent) { setTitleText($item, "正从 jijidown 查询封面图 ..."); // 若仅查找封面图则意味着标题已找到,默认不改动显示标题,除非需要重置已修改的标题 } fetchJSON(`https://www.jijidown.com/api/v1/video/get_info?id=${avid}`) .then(json => { if (!json) { // 由于网络请求遇到故障中断、网站暂时下线等,重试也无益 startXbeibeixQuery($item, avid, false, isForPic, silent); } else if (json.code == 0 || json.upid == undefined) { // 大概率是请求过快 if (isDebug) console.log(`[bilibili-fav-fix] jijidown 请求过快 ${avid}`); setTitleRetry($item, {item: $item}, function(event) { event.preventDefault(); let $it = event.data.item; // 取出成局部变量,以便.off()时销毁event.data,函数结束后亦销毁局部变量 $(this).off("click"); startJijidownQuery(Object.fromEntries([[$it.attr("avid"), $it]]), isDecisive, isForPic, false); }); } else if (json.upid == -1 || json.upid == 0 || json.title == "视频去哪了呢?" || json.title == "该视频或许已经被删除了" || (json.title == avid && json.img == "")) { if (isDebug) console.log(`[bilibili-fav-fix] jijidown failed for ${avid}`); startXbeibeixQuery($item, avid, isDecisive, isForPic, silent); } else { if (isDebug) console.log(`[bilibili-fav-fix] jijidown retrieved info for ${avid}`); let pic = cache.update(avid, "pic", json.img); if (!isForPic) { queryHit($item, avid, json.title, pic, json.up.author, isDecisive ? "jj" : undefined); } else { if (!silent) setTitleText($item, cache.get(avid, "title"), true); setCoverPic($item, pic, cache.get(avid, "ff")); if (/bfs\/archive/i.test(pic)) if (isDebug) console.log(`[bilibili-fav-fix] jijidown got better pic for ${avid}`); } if (!/bfs\/archive/i.test(pic)) { // 极大概率是失效的旧图片链接 if (isDebug) console.log(`[bilibili-fav-fix] query for better pic for ${avid}`); startXbeibeixQuery($item, avid, false, true); } } }); } } async function startXbeibeixQuery($item, avid, isDecisive = true, isForPic = false, silent = true) { if (isDebug) console.log(`[bilibili-fav-fix] startXbeibeixQuery for ${avid}`); if (!isForPic) { setTitleText($item, "正在查询 xbeibeix ..."); } else if (!silent) { setTitleText($item, "正从 xbeibeix 查询封面图 ..."); // 若仅查找封面图则意味着标题已找到,默认不改动显示标题,除非需要重置已修改的标题 } const doc = await fetchHTMLDoc(`https://xbeibeix.com/video/${$item.attr("bvid")}`); if (!doc) { if (!isForPic) { queryFailed($item, avid); } else { if (!silent) setTitleText($item, cache.get(avid, "title"), true); } } else if (doc == "redirected") { if (isDebug) console.log(`[bilibili-fav-fix] xbeibeix failed for ${avid}`); if (isDecisive) cache.update(avid, "archive", "nohit"); if (!isForPic) { queryFailed($item, avid); } else { if (!silent) setTitleText($item, cache.get(avid, "title"), true); } } else if (doc == "verifyHuman" || doc.querySelector('meta[name="robots"]')) { if (isDebug) console.log(`[bilibili-fav-fix] xbeibeix need to verify human for ${$item.attr("bvid")} ( ${avid} )`); getTitleElem($item).addClass("verifyHuman"); setTitleText($item, "请点击验证真实人类,而后关闭该标签页"); getTitleElem($item).on("click", {item: $item}, function(event) { event.preventDefault(); let $it = event.data.item; if ($(this).hasClass("verifyHuman")) { const tab = GM_openInTab(`https://xbeibeix.com/video/${$it.attr("bvid")}`, {active: true, insert: true, setParent: true}); tab.onclose = function() { if (isDebug) console.log(`[bilibili-fav-fix] human verification completed for ${$it.attr("bvid")} ( ${avid} )`); let $titleElems = $(".verifyHuman"); $titleElems.removeClass("verifyHuman"); $titleElems.trigger("click"); }; } else { $(this).off("click"); startXbeibeixQuery($it, avid, isDecisive, isForPic, false); } }); } else { if (isDebug) console.log(`[bilibili-fav-fix] xbeibeix retrieved info for ${avid}`); let title = doc.querySelector(".fw-bold").textContent; let pic = cache.update(avid, "pic", doc.querySelector("img.img-thumbnail").getAttribute("src")); let author = doc.querySelector("input").getAttribute("value"); if (!isForPic) { queryHit($item, avid, title, pic, author, isDecisive ? "bb" : undefined); } else { if (!silent) setTitleText($item, cache.get(avid, "title"), true); setCoverPic($item, pic, cache.get(avid, "ff")); if (/bfs\/archive/i.test(pic)) if (isDebug) console.log(`[bilibili-fav-fix] xbeibeix got better pic for ${avid}`); } if (!/bfs\/archive/i.test(pic)) { // 极大概率是失效的旧图片链接 if (isDebug) console.log(`[bilibili-fav-fix] xbeibeix failed for valid pic for ${avid}`); // 终止封面图查询 } } } function queryHit($item, avid, title, pic, author, archive) { if (isDebug) console.log(`[bilibili-fav-fix] queryHit for ${avid}`); // 检查refine需求 if (archive == "bp") { if ($item.attr("_refineParts") == "needRefine") { $item.attr("_refineParts", ""); refineBiliplusQuery($item, avid); } else { $item.attr("_refineParts", "canRefine"); } } // 仅在hit函数时才更新archive,failed或cached时在调用函数前更新 cache.update(avid, "archive", archive); // 取值bp jj 或undefined // 设置超链接 if (archive == "bp") { setTitleLink($item, `https://www.biliplus.com/video/av${avid}/`); } else if (archive == "jj") { setTitleLink($item, `https://www.jijidown.com/api/v1/video/get_info?id=${avid}`); } else if (archive == "bb") { setTitleLink($item, `https://xbeibeix.com/video/${$item.attr("bvid")}`); } // 明明是queryHit 但却archive==undefined以至于没有coverLink的情况存在,就是biliplus网络故障中断,暂由jijidown得到hit的时候 // 设置标题 cache.update(avid, "title", title); setTitleText($item, title, true); getTitleElem($item).attr("_handover", "true"); replaceTooltip($item, /\n标题:.*\n/, `\n标题:${title}\n`); // 总是替换标题,不管浮块是否生成 // 设置封面图 pic = cache.update(avid, "pic", pic); setCoverPic($item, pic, cache.get(avid, "ff")); // 设置up主名称 if ($item.attr("_author") == "needAuthor") { // 仅当biliAPI获取不到up名称时替换up名称 cache.update(avid, "author", author); replaceTooltip($item, /\nUP主:.* (https:\/\/space\.bilibili\.com/, `\nUP主:${author} (https://space.bilibili.com`); replaceAuthorText($item, author); } else { $item.attr("_author", author); // 默认不进入缓存,因为不知道biliAPI是否返回“账号已注销” } } function queryFailed($item, avid) { if (isDebug) console.log(`[bilibili-fav-fix] queryFailed for ${avid}`); let c = cache.get(avid) || {}; queryCached($item, avid, c); } function queryCached($item, avid, c) { if (isDebug) console.log(`[bilibili-fav-fix] queryCached for ${avid}`); // 检查refine需求 if (c.archive == "bp") { if ($item.attr("_refineParts") == "needRefine") { $item.attr("_refineParts", ""); refineBiliplusQuery($item, avid); } else { $item.attr("_refineParts", "canRefine"); } } // 设置超链接 if (c.archive == "bp") { setTitleLink($item, `https://www.biliplus.com/video/av${avid}/`); } else if (c.archive == "jj") { setTitleLink($item, `https://www.jijidown.com/api/v1/video/get_info?id=${avid}`); } else if (c.archive == "bb") { setTitleLink($item, `https://xbeibeix.com/video/${$item.attr("bvid")}`); } // 设置标题 if (c.title) { // 有缓存title则先显示,可能会被biliAPI之后修改 setTitleText($item, c.title, true); // 仅当成功恢复时修改样式 } else if (getTitleElem($item).attr("_noguesses") == "true") { // 没有缓存,biliAPI也没有 setTitleText($item, `查不到标题(${avid})【鼠标悬停查看简介】`); } else { // 没有缓存,但biliAPI之后可能有 setTitleText($item, `正在查询 bilibili API ...`); } getTitleElem($item).attr("_handover", "true"); // 设置封面图 setCoverPic($item, c.pic, c.ff); } async function refineBiliplusQuery($item, avid, retry = 0) { if (isDebug) console.log(`[bilibili-fav-fix] refineBiliplusQuery for ${avid}`); const json = await fetchJSON(`https://www.biliplus.com/api/view?id=${avid}`); if (!json) { // 网络连接故障中断或JSON格式错误 return; } else if (json.code == -503) { // 请求过快 if (retry < 2) { // 仅自动重试两次,但由于needRefine始终存在,以后还会有重试的机会 if (isDebug) console.log(`[bilibili-fav-fix] refineBiliplusQuery 请求过快,10秒后重试`); setTimeout(refineBiliplusQuery, 10000, $item, avid, retry + 1); } return; } else if (json.code == -404) { // 查询无结果 return; } else if (json.code == -403) { // 访问权限不足(up主隐藏) return; } if (json.list && json.list.length > 1) { if (isDebug) console.log(`[bilibili-fav-fix] refined biliplus got ${json.list.length} parts for ${avid}`); let parts = json.list.map(x => x.part); parts = cache.update(avid, "parts", parts); let partsStr = parts.map(part => `* ${part}\n`).join(''); replaceTooltip($item, "\n播放数:", `\n子P标题:\n${partsStr}播放数:`); } if (Number(json.tid)) { let tid = cache.update(avid, "tid", Number(json.tid)); replaceTooltip($item, /\n分区:.*\n/, `\n分区:${categoriesDict[tid]}\n`); } if (Number(json.coins)) { let coin = cache.update(avid, "coin", Number(json.coins)); let content = getCoverElem($item).attr("title"); if (content && content.includes("\n投币数:")) { replaceTooltip($item, /\n投币数:.*\n/, `\n投币数:${coin}\n`); } else { replaceTooltip($item, "\n失效原因:", `\n投币数:${coin}\n失效原因:`) } } if (Number(json.review)) { let reply = cache.update(avid, "reply", Number(json.review)); let content = getCoverElem($item).attr("title"); if (content && content.includes("\n回复数:")) { replaceTooltip($item, /\n回复数:.*\n/, `\n回复数:${reply}\n`); } else { replaceTooltip($item, "\n失效原因:", `\n回复数:${reply}\n失效原因:`) } } // 将first_frame更新成备用的封面图,但其实没多大用。如果biliplus和jijidown都返回无效的pic那么基本找不到ff,如果有ff那么基本上pic都是有效的 if (json.v2_app_api && json.v2_app_api.first_frame) { let ff = json.v2_app_api.first_frame; if (isDebug) console.log(`[bilibili-fav-fix] refined biliplus got first_frame pic for ${avid}: ${ff}`); ff = cache.update(avid, "ff", ff); const $imgElem = getImgElem($item); if ($imgElem.attr("alt") == "图片链接失效") { // pic已经被替换进img元素,且执行替换时无ff $imgElem.attr("alt", " "); $(`视频已失效