Greasy Fork is available in English.

哔哩哔哩(B站|Bilibili)收藏夹Fix (cerenkov修改版)

修复 哔哩哔哩(www.bilibili.com) 失效的收藏。(可查看av号、简介、标题、封面、数据等)

  1. // ==UserScript==
  2. // @name 哔哩哔哩(B站|Bilibili)收藏夹Fix (cerenkov修改版)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3.1
  5. // @description 修复 哔哩哔哩(www.bilibili.com) 失效的收藏。(可查看av号、简介、标题、封面、数据等)
  6. // @author cerenkov
  7. // @license GPL-3.0
  8. // @match *://space.bilibili.com/*/favlist*
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.min.js
  10. // @resource iconError https://cdn.jsdelivr.net/gh/crnkv/bilibili-favorites-fix-cerenkov-mod/media/error.png
  11. // @resource iconSuccess https://cdn.jsdelivr.net/gh/crnkv/bilibili-favorites-fix-cerenkov-mod/media/success.png
  12. // @resource iconInfo https://cdn.jsdelivr.net/gh/crnkv/bilibili-favorites-fix-cerenkov-mod/media/info.png
  13. // @connect biliplus.com
  14. // @connect api.bilibili.com
  15. // @grant GM_xmlhttpRequest
  16. // @grant GM_notification
  17. // @grant GM_setClipboard
  18. // @grant GM_getResourceURL
  19. // @grant GM_openInTab
  20. // ==/UserScript==
  21.  
  22. /*jshint esversion: 8 */
  23. (function() {
  24. 'use strict';
  25.  
  26. /**
  27. * 失效收藏标题颜色(默认为灰色)。
  28. * @type {String}
  29. */
  30. const invalTitleColor = "#999";
  31.  
  32. /**
  33. * 是否启用调试模式。
  34. * 启用后,浏览器控制台会显示此脚本运行时的调试数据。
  35. * @type {Boolean}
  36. */
  37. const isDebug = false;
  38.  
  39. // 值为 true : 简化查询(新模式)。不再调用历史归档查询,更快出结果,且更不容易碰到“请求过快”警告。反正常规查询查不到的,历史归档查询基本上也查不到。适合有大量失效视频的收藏夹
  40. // 值为 false: 深度查询(旧模式)。即Mr.Po原脚本所用逻辑。常规查询失败时会调用历史归档查询,花费更多时间,且更容易碰到“请求过快”警告,但似乎得不到更多的结果。适合失效视频数量不多的情况
  41. let tryLess = true;
  42.  
  43. /**
  44. * 重试延迟[秒]。
  45. * @type {Number}
  46. */
  47. const retryDelay = 5;
  48.  
  49. /**
  50. * 每隔 interval [毫秒]检查一次,是否有新的收藏被加载出来。
  51. * 此值越小,检查越快;过小会造成浏览器卡顿。
  52. * @type {Number}
  53. */
  54. const interval = 2000;
  55.  
  56. let isFirefox = false;
  57. let isChromium = false;
  58. let uaData = GM_info.userAgentData;
  59. if (uaData && uaData.brands && uaData.brands.length > 0) {
  60. if (uaData.brands.some(x => (x.brand && x.brand.match(/firefox/i)))) {
  61. isFirefox = true;
  62. } else if (uaData.brands.some(x => (x.brand && x.brand.match(/chromium|chrome|edge/i)))) {
  63. isChromium = true;
  64. }
  65. }
  66. // 阿B是真丢人啊,Firefox下,一旦标题<a>内文字过长出现text-overflow,菜单按钮就无法在鼠标hover时显示
  67. // 这么基础的毛病,新UI铺开之前都测试不出来吗
  68. // 对于一般视频问题不大,但失效恢复视频的功能很需要这个功能菜单
  69. // 在阿B修好之前,只能我代为临时处理一下了
  70. function stripTitleFirefox(title) {
  71. if (isFirefox && title.length > 24) {
  72. return title.slice(0,24)+"..";
  73. } else {
  74. return title;
  75. }
  76. }
  77.  
  78. // 是否B站新网页界面,在首次(每次)运行handleFavorites()时会检测网页并记录在该变量中
  79. let isNewUI = false;
  80.  
  81. // 缓存已经查询过并且有结果的视频标题和封面(包括查到的和查不到的,不包括查询过程中请求过快、网络错误和解析错误的)
  82. let cache = {};
  83.  
  84. var XOR_CODE = 23442827791579n;
  85. var MASK_CODE = 2251799813685247n;
  86. var BASE = 58n;
  87. var CHAR_TABLE = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf";
  88.  
  89. function bv2av(bvid) {
  90. const bvidArr = Array.from(bvid);
  91. [bvidArr[3], bvidArr[9]] = [bvidArr[9], bvidArr[3]];
  92. [bvidArr[4], bvidArr[7]] = [bvidArr[7], bvidArr[4]];
  93. bvidArr.splice(0, 3);
  94. const tmp = bvidArr.reduce((pre, bvidChar) => pre * BASE + BigInt(CHAR_TABLE.indexOf(bvidChar)), 0n);
  95. return Number((tmp & MASK_CODE) ^ XOR_CODE);
  96. }
  97.  
  98. /**
  99. * 处理收藏
  100. */
  101. function handleFavorites() {
  102. isNewUI = $("div.fav-list-main div.items").length > 0;
  103. if (isDebug) console.log(`[bilibili-fav-fix] isNewUI: ${isNewUI}`);
  104.  
  105. // 失效收藏节点集
  106. let $targetItems = null;
  107. if (isNewUI) {
  108. $targetItems = $("div.fav-list-main div.items > div").filter(function (i, item) { return $(item).find(".bili-video-card__title a").first().text() == "已失效视频"; });
  109. } else if ($("ul.fav-video-list.content").length > 0) {
  110. $targetItems = $("ul.fav-video-list.content li.small-item.disabled");
  111. } else {
  112. console.error('[bilibili-fav-fix] B站网页样式无法识别');
  113. }
  114. if (isDebug) console.log(`[bilibili-fav-fix] $targetItems.length: ${$targetItems.length}`);
  115.  
  116. if ($targetItems.length > 0) {
  117. console.info(`[bilibili-fav-fix] ${$targetItems.length}个收藏待修复...`);
  118.  
  119. showDetail($targetItems);
  120.  
  121. $targetItems.each(function(i, item) {
  122. const $item = $(item);
  123. const bvid = getItemBVID($item);
  124. const avid = bv2av(bvid);
  125. if (isDebug) console.log(`[bilibili-fav-fix] BVID needed to fix: ${bvid}`);
  126.  
  127. // 更改封面图超链接和标题行超链接,跳过新UI的up主行的超链接
  128. const $aElems = $item.find("a:not(.bili-video-card__author)");
  129. $aElems.attr("href", `https://www.biliplus.com/video/av${avid}/`);
  130. $aElems.attr("target", "_blank");
  131.  
  132. addCopyAVIDButton($item, avid);
  133. addCopyBVIDButton($item, bvid);
  134.  
  135. // 移除禁用样式
  136. if (!isNewUI) {
  137. $item.removeClass("disabled");
  138. $aElems.removeClass("disabled");
  139. }
  140.  
  141. const $titleElem = $($aElems[1]);
  142. if (cache[avid]) {
  143. if (cache[avid].success) {
  144. // 从缓存中读出
  145. fixFavorites($item, $titleElem, avid, cache[avid].title, cache[avid].pic, cache[avid].history, cache[avid].parts);
  146. } else {
  147. fixFailed($item, $titleElem, avid);
  148. }
  149. } else {
  150. fixTitleAndPic($item, $titleElem, avid);
  151. }
  152. });
  153. }
  154. }
  155.  
  156. /**
  157. * 显示详细
  158. * @param {$节点} $targetItems 失效收藏节点集
  159. */
  160. function showDetail($targetItems) {
  161. const url = getBilibiliApiUrl();
  162. GM_xmlhttpRequest({
  163. method: 'GET',
  164. url: url,
  165. responseType: "json",
  166. onload: function(res) {
  167. const json = res.response;
  168. const medias = json.data.medias;
  169.  
  170. $targetItems.each(function(i, item) {
  171. const $item = $(item);
  172. const bvid = getItemBVID($item);
  173. if (isDebug) console.log(`[bilibili-fav-fix] showDetail: ${bvid}`);
  174.  
  175. let media = medias.filter((m) => (m.bvid == bvid));
  176. if (media.length > 0) {
  177. media = media[0];
  178. if (isDebug) console.log(media);
  179. } else {
  180. console.warn(`[bilibili-fav-fix] ${bvid} not found in Bilibili API JSON (wrong params?): ${url}`);
  181. return;
  182. }
  183.  
  184. let title = media.title;
  185. if (title == "已失效视频") {
  186. // 如果 biliplus 查询先有了结果并且被保存在节点上,则使用 biliplus 得来的数据
  187. if ($item.attr("_title")) title = $item.attr("_title");
  188. }
  189. let duration = new Date(media.duration * 1000).toISOString().slice(11, 19);
  190. if (duration.slice(0, 2) == "00") duration = duration.slice(3);
  191.  
  192. // 以前在 media.pages 里有子P标题,现在好像B站删了
  193. // 如果 biliplus 查询先有了结果并且被保存在节点上,则使用 biliplus 得来的数据
  194. let partTitles = null;
  195. if ($item.attr("_parts")) partTitles = $item.attr("_parts");
  196. if (media.pages) partTitles = media.pages.map((page, i, arry) => "* "+page.title).join("\n");
  197. const partsInfo = ( (media.page > 1) ? `分P数量:${media.page}\n` : "" ) + ( partTitles ? `子P标题:\n${partTitles}\n` : "" );
  198.  
  199. let reason;
  200. if (media.attr) {
  201. if (media.attr == 0) {
  202. reason = "未失效(0)";
  203. } else if (media.attr == 9) {
  204. reason = "UP主自己删除(9)";
  205. } else if (media.attr == 1) {
  206. reason = "其他原因删除(1)";
  207. } else {
  208. reason = `原因编号意义未明(${media.attr})`;
  209. }
  210. }
  211.  
  212. const content = `AV号:${media.id}
  213. BV号:${bvid}
  214. 标题:${title}
  215. UP主:${media.upper.name} https://space.bilibili.com/${media.upper.mid})
  216. 简介:${media.intro}
  217. 时长:${duration}
  218. 发布时间:${new Date(media.pubtime * 1000).toLocaleString()}
  219. 收藏时间:${new Date(media.fav_time * 1000).toLocaleString()}
  220. ${partsInfo}播放数:${media.cnt_info.play}
  221. 收藏数:${media.cnt_info.collect}
  222. 弹幕数:${media.cnt_info.danmaku}
  223. 失效原因:${reason}`;
  224. const $aElems = $item.find("a:not(.bili-video-card__author)");
  225. const $coverElem = $aElems.first();
  226. $coverElem.attr("title", content);
  227.  
  228. addCopyInfoButton($item);
  229. addOpenUpSpaceButton($item, media.upper.mid);
  230. addToggleModeButton($item);
  231. addSaveLoadCacheButton($item);
  232. });
  233. }
  234. });
  235. }
  236.  
  237. function getBilibiliApiUrl() {
  238. let fid = window.location.href.match(/fid=(\d+)/i);
  239. if (fid) {
  240. fid = fid[1];
  241. } else if (isNewUI) {
  242. fid = $("div.fav-sidebar-item:has(.vui_sidebar-item--active)").first().attr("id");
  243. } else {
  244. fid = $("li.fav-item.cur").first().attr("fid");
  245. }
  246. if (isDebug) console.log(`[bilibili-fav-fix] fid: ${fid}`);
  247.  
  248. let pn = 1;
  249. if (isNewUI) {
  250. pn = $("div.vui_pagenation--btns .vui_button.vui_button--active").text().trim();
  251. } else {
  252. pn = $("ul.be-pager li.be-pager-item.be-pager-item-active").text().trim();
  253. }
  254. if (isDebug) console.log(`[bilibili-fav-fix] pn: ${pn}`);
  255.  
  256. let order = "mtime";
  257. if (isNewUI) {
  258. order = $("div.fav-list-header-filter__left div.radio-filter__item--active").first().text().trim();
  259. } else {
  260. order = $($("div.fav-filters > div")[2]).find("span").first().text().trim();
  261. }
  262. order = new Map([["最近收藏", "mtime"], ["最多播放", "view"], ["最新投稿", "pubtime"], ["最近投稿", "pubtime"]]).get(order);
  263. if (order === undefined) order = "mtime"; // 执行收藏夹搜索时无从得知排序,只能手动指定成“最近收藏”,不保证结果正确
  264. if (isDebug) console.log(`[bilibili-fav-fix] order: ${order}`);
  265.  
  266. let tid = 0;
  267. if (isNewUI) {
  268. tid = $("div.fav-list-header-collapse div.radio-filter__item--active").first().text().trim().replace(/\s+\d+/, "");
  269. } else {
  270. tid = $($("div.fav-filters > div")[1]).find("span").first().text().trim();
  271. }
  272. tid = new Map([["全部分区", 0], ["动画", 1], ["音乐", 3], ["游戏", 4], ["娱乐", 5], ["电视剧", 11], ["番剧", 13], ["电影", 23], ["知识", 36], ["鬼畜", 119], ["舞蹈", 129], ["时尚", 155], ["生活", 160], ["国创", 167], ["纪录片", 177], ["影视", 181], ["资讯", 202], ["美食", 211], ["动物圈", 217], ["汽车", 223], ["运动", 234], ["科技", 188], ["版权内容", -24]]).get(tid);
  273. if (tid === undefined) tid = 0; // 一些被下线和撤除的分区,无从得知其名称和tid,只能手动指定成“全部分区”,返回的结果很大概率不包含目标视频的数据
  274. if (isDebug) console.log(`[bilibili-fav-fix] tid: ${tid}`);
  275.  
  276. let searchType = 0;
  277. let keyword = "";
  278. if (isNewUI) {
  279. if ($("div.fav-list-header-filter__desc").length > 0) {
  280. searchType = $("div.fav-list-header-filter__right button").first().text().trim();
  281. searchType = new Map([["当前", 0], ["全部", 1]]).get(searchType);
  282. keyword = encodeURIComponent($("div.fav-list-header-filter__right input").first().val());
  283. }
  284. } else {
  285. if ($("div.search-results-num").length > 0) {
  286. searchType = $("div.search-types > div > div").first().text().trim();
  287. searchType = new Map([["当前", 0], ["全部", 1]]).get(searchType);
  288. keyword = encodeURIComponent($("input.search-fav-input").first().val());
  289. }
  290. }
  291. if (isDebug) console.log(`[bilibili-fav-fix] searchType: ${searchType}\n[bilibili-fav-fix] keyword: ${keyword}`);
  292.  
  293. return `https://api.bilibili.com/x/v3/fav/resource/list?media_id=${fid}&pn=${pn}&ps=${isNewUI ? 40 : 20}&keyword=${keyword}&order=${order}&type=${searchType}&tid=${tid}&platform=web`;
  294. }
  295.  
  296. function getItemBVID($item) {
  297. if ($item.attr("bvid")) {
  298. return $item.attr("bvid");
  299. }
  300. let bvid = "";
  301. if (isNewUI) {
  302. bvid = $item.find(".bili-cover-card").first().attr("href").match(/bilibili\.com\/video\/(\w+)/i)[1];
  303. } else {
  304. bvid = $item.attr("data-aid");
  305. }
  306. $item.attr("bvid", bvid);
  307. return bvid;
  308. }
  309.  
  310. function addCopyAVIDButton($item, avid) {
  311. addButton($item, "复制AV号", function() {
  312. GM_setClipboard(`av${avid}`, "text");
  313. tipSuccess("AV号复制成功!");
  314. });
  315. }
  316.  
  317. function addCopyBVIDButton($item, bvid) {
  318. addButton($item, "复制BV号", function() {
  319. GM_setClipboard(bvid, "text");
  320. tipSuccess("BV号复制成功!");
  321. });
  322. }
  323.  
  324. function addCopyInfoButton($item) {
  325. addButton($item, "复制稿件信息", function() {
  326. const $aElems = $item.find("a:not(.bili-video-card__author)");
  327. const $coverElem = $aElems.first();
  328. GM_setClipboard($coverElem.attr("title"), "text");
  329. tipSuccess("稿件信息复制成功!");
  330. });
  331. }
  332.  
  333. function addOpenUpSpaceButton($item, mid) {
  334. addButton($item, "跳转UP主空间", function () {
  335. GM_openInTab(`https://space.bilibili.com/${mid}`, {active: true, insert: true, setParent: true});
  336. });
  337. }
  338.  
  339. function addToggleModeButton($item) {
  340. addButton($item, function () { return tryLess ? "切至深度查询" : "切至简化查询"; }, function () {
  341. if (tryLess) {
  342. tryLess = false;
  343. for (let k of Object.keys(cache)) {
  344. if (!cache[k].success) delete cache[k];
  345. }
  346. $(".bili-fav-fix-menu-item").each(function (i, item) {
  347. if ($(item).text() == "切至深度查询") $(item).text("切至简化查询");
  348. })
  349. tipSuccess("已切至深度查询(旧模式),更花时间,查询结果未必更多,且更容易碰到“请求过快”需手动加载,适合失效视频数量不多的情况");
  350. } else {
  351. tryLess = true;
  352. $(".bili-fav-fix-menu-item").each(function (i, item) {
  353. if ($(item).text() == "切至简化查询") $(item).text("切至深度查询");
  354. })
  355. tipSuccess("已切至简化查询(新模式),速度更快,查询结果或许有漏,但不容易碰到“请求过快”警告,适合有大量失效视频的收藏夹");
  356. }
  357. });
  358. }
  359.  
  360. function addSaveLoadCacheButton($item) {
  361. addButton($item, "导出/导入缓存", function () {
  362. if (unsafeWindow.confirm("【导出】点击确定,即可将当前标签页脚本运行期间查询到的标题/封面缓存数据导出至剪贴板(缓存将在网页刷新时消失)")) {
  363. GM_setClipboard(JSON.stringify(cache), "text");
  364. tipSuccess("缓存导出至剪贴板成功!");
  365. } else {
  366. let input = unsafeWindow.prompt("【导入】粘贴输入缓存数据,即可导入至当前标签页脚本中(缓存将在网页刷新时消失)");
  367. if (input) {
  368. try {
  369. cache = JSON.parse(input);
  370. tipSuccess("缓存导入成功!");
  371. } catch (e) {
  372. tipError("缓存导入失败!");
  373. }
  374. }
  375. }
  376. });
  377. }
  378.  
  379. function addButton($item, name, fun) {
  380. if (isNewUI) {
  381. const $dropdownTrigger = $item.find(".bili-card-dropdown").first();
  382. $dropdownTrigger.hover(
  383. function() {
  384. setTimeout(function() {
  385. if (typeof name == "function") name = name();
  386. // 延时获取dropdownMenu元素,因为B站新UI动态生成该元素
  387. const $dropdownMenu = $(".bili-card-dropdown-popper.visible").first();
  388. if (! $dropdownMenu.find(".bili-fav-fix-menu-item").text().includes(name) ) {
  389. const $menuItem = $(`<div class="bili-card-dropdown-popper__item bili-fav-fix-menu-item">${name}</div>`);
  390. $menuItem.click(fun);
  391. $dropdownMenu.append($menuItem);
  392. }
  393. }, 500);
  394. }, function() {}
  395. );
  396. } else {
  397. if (typeof name == "function") name = name();
  398. const $dropdownMenu = $item.find(".be-dropdown-menu").first();
  399. if (! ($dropdownMenu.find(".bili-fav-fix-menu-item").text().includes(name)) ) {
  400. const $lastChild = $dropdownMenu.children().last();
  401. // 未添加过扩展
  402. if (!$lastChild.hasClass('bili-fav-fix-menu-item')) {
  403. $lastChild.addClass("be-dropdown-item-delimiter");
  404. }
  405.  
  406. const $menuItem = $(`<li class="be-dropdown-item bili-fav-fix-menu-item">${name}</li>`);
  407. $menuItem.click(fun);
  408. $dropdownMenu.append($menuItem);
  409. }
  410. }
  411. }
  412.  
  413. function tipInfo(text) {
  414. tip(text, "iconInfo");
  415. }
  416.  
  417. function tipError(text) {
  418. tip(text, "iconError");
  419. }
  420.  
  421. function tipSuccess(text) {
  422. tip(text, "iconSuccess");
  423. }
  424.  
  425. function tip(text, iconName) {
  426. GM_notification({
  427. text: text,
  428. image: GM_getResourceURL(iconName)
  429. });
  430. }
  431.  
  432.  
  433.  
  434. /**
  435. * 修复标题和海报
  436. * @param {$节点} $item 当前收藏Item
  437. * @param {$节点} $titleElem 标题链接
  438. * @param {数字} avid av号
  439. */
  440. function fixTitleAndPic($item, $titleElem, avid) {
  441. $titleElem.text("Loading...");
  442. fixTitleAndPicEnhance3($item, $titleElem, avid); // 常规查询入口
  443. }
  444.  
  445. /**
  446. * 修复标题和海报 增强 - 3
  447. * 模拟常规查询
  448. * @param {$节点} $item 当前收藏Item
  449. * @param {$节点} $titleElem 标题链接
  450. * @param {数字} avid av号
  451. */
  452. function fixTitleAndPicEnhance3($item, $titleElem, avid) {
  453.  
  454. GM_xmlhttpRequest({
  455. method: 'GET',
  456. url: `https://www.biliplus.com/video/av${avid}/`,
  457. onload: function(response) {
  458. try {
  459. if (isDebug) {
  460. console.log("[bilibili-fav-fix] 3---->:");
  461. console.log(response.response);
  462. }
  463.  
  464. let jsonRegex = response.responseText.match(/window\.addEventListener\('DOMContentLoaded',function\(\){view\((.+)\);}\);/);
  465. if (isDebug) console.log(jsonRegex);
  466.  
  467. const jsonStr = jsonRegex[1];
  468. if (isDebug) console.log(jsonStr);
  469.  
  470. const res = $.parseJSON(jsonStr);
  471. if (res.title) { // 存在
  472. let partTitles = null;
  473. if (res.list && res.list.length > 1) {
  474. partTitles = res.list.map((part, i, arry) => part.part);
  475. }
  476. fixFavorites($item, $titleElem, avid, res.title, res.pic, null, partTitles);
  477. } else if (res.code == -503) { // 请求过快
  478. // 出现提示手动点击加载,转入API查询
  479. retryLoad($titleElem, avid, null, function() {
  480. fixTitleAndPicEnhance0($item, $titleElem, avid, true);
  481. });
  482. } else { // 常规查询无结果
  483. if (tryLess) { // 简化查询,常规查询失败就失败,不再尝试历史归档查询,反正大概率也查不到
  484. fixFailed($item, $titleElem, avid);
  485. } else {
  486. $titleElem.text("常规查询无结果,转入历史归档查询...");
  487. fixTitleAndPicEnhance1($item, $titleElem, avid);
  488. }
  489. }
  490. } catch (e) { // 网页内容解析错误(很可能是请求过快),出现提示手动点击加载,转入API查询
  491. console.error("[bilibili-fav-fix] 常规查询结果解析出错(很可能是请求过快)");
  492. retryLoad($titleElem, avid, null, function() {
  493. fixTitleAndPicEnhance0($item, $titleElem, avid, true);
  494. });
  495. }
  496. },
  497. onerror: function(e) {
  498. $titleElem.text("常规查询出错,请检查网络连接");
  499. }
  500. });
  501. }
  502.  
  503. /**
  504. * 修复标题和海报 增强 - 0
  505. * 使用公开的API
  506. * @param {$节点} $item 当前收藏Item
  507. * @param {$节点} $titleElem 标题链接
  508. * @param {数字} avid av号
  509. * @param {布尔} delayRetry 延迟重试
  510. */
  511. function fixTitleAndPicEnhance0($item, $titleElem, avid, delayRetry) {
  512. // 传入的delayRetry似乎只有true,即遇到503时永远需要强制延迟
  513. GM_xmlhttpRequest({
  514. method: 'GET',
  515. url: `https://www.biliplus.com/api/view?id=${avid}`,
  516. responseType: "json",
  517. onload: function(response) {
  518. const res = response.response;
  519. if (isDebug) {
  520. console.log("[bilibili-fav-fix] 0---->:");
  521. console.log(res);
  522. }
  523.  
  524. if (res.title) { // 找到了
  525. let partTitles = null;
  526. if (res.list && res.list.length > 1) {
  527. partTitles = res.list.map((part, i, arry) => part.part);
  528. }
  529. fixFavorites($item, $titleElem, avid, res.title, res.pic, null, partTitles);
  530. } else if (res.code == -503) { // 请求过快
  531. retryLoad($titleElem, avid, delayRetry, function() {
  532. fixTitleAndPicEnhance0($item, $titleElem, avid, true);
  533. });
  534. } else { // API查询无结果(或json解析格式出错)
  535. if (tryLess) { // 简化查询,API查询失败就失败,不再尝试历史归档查询,反正大概率也查不到
  536. fixFailed($item, $titleElem, avid);
  537. } else {
  538. $titleElem.text("API查询无结果,转入历史归档查询...");
  539. fixTitleAndPicEnhance1($item, $titleElem, avid);
  540. }
  541. }
  542. },
  543. onerror: function(e) {
  544. console.error("[bilibili-fav-fix] API查询出错");
  545. $titleElem.text("API查询出错,请检查网络连接");
  546. }
  547. });
  548. }
  549.  
  550. /**
  551. * 修复标题和海报 增强 - 1
  552. * 使用cache库 (历史归档查询)
  553. * @param {$节点} $item 当前收藏Item
  554. * @param {$节点} $titleElem 标题链接
  555. * @param {数字} avid av号
  556. */
  557. function fixTitleAndPicEnhance1($item, $titleElem, avid) {
  558.  
  559. GM_xmlhttpRequest({
  560. method: 'GET',
  561. url: `https://www.biliplus.com/all/video/av${avid}/`,
  562. onload: function(response) {
  563. try {
  564. if (isDebug) {
  565. console.log("[bilibili-fav-fix] 1---->:");
  566. console.log(response.response);
  567. }
  568.  
  569. const params = response.responseText.match(/getjson\('(\/api\/view_all.+)'/);
  570. fixTitleAndPicEnhance2($item, $titleElem, avid, params[1]); // 不传入delayRetry参数,第一次503时可立刻点击重载
  571. } catch (e) { // 网页内容解析错误
  572. console.error("[bilibili-fav-fix] 历史归档查询结果解析出错(1)或请求过快");
  573. $titleElem.text("历史归档查询结果解析出错(1)或请求过快");
  574. }
  575. },
  576. onerror: function(e) {
  577. $titleElem.text("历史归档查询出错(1),请检查网络连接");
  578. }
  579. });
  580. }
  581.  
  582. /**
  583. * 修复标题和海报 增强 - 2
  584. * 使用cache库,第一段,需与fixTitleAndPicEnhance1连用
  585. * @param {$节点} $item 当前收藏Item
  586. * @param {$节点} $titleElem 标题链接
  587. * @param {数字} avid av号
  588. * @param {字符串} param 待拼接参数
  589. * @param {布尔} delayRetry 延迟重试
  590. */
  591. function fixTitleAndPicEnhance2($item, $titleElem, avid, param, delayRetry) {
  592.  
  593. GM_xmlhttpRequest({
  594. method: 'GET',
  595. url: `https://www.biliplus.com${param}`,
  596. responseType: "json",
  597. onload: function(response) {
  598. try {
  599. const res = response.response;
  600. if (isDebug) {
  601. console.log("[bilibili-fav-fix] 2---->:");
  602. console.log(res);
  603. }
  604.  
  605. if (!res.code) throw "JSON格式不正确";
  606. if (res.code === 0) { // 找到了
  607. let partTitles = null;
  608. if (res.data.parts && res.data.parts.length > 1) {
  609. partTitles = res.data.parts.map((part, i, arry) => part.part);
  610. }
  611. fixFavorites($item, $titleElem, avid, res.data.info.title, res.data.info.pic, "all/", partTitles);
  612. } else if (res.code == -503) { // 请求过快
  613. retryLoad($titleElem, avid, delayRetry, function() {
  614. fixTitleAndPicEnhance2($item, $titleElem, avid, param, true);
  615. });
  616. } else { // 历史归档查询无结果
  617. fixFailed($item, $titleElem, avid);
  618. }
  619. } catch (e) { // JSON内容解析错误
  620. console.error("[bilibili-fav-fix] 历史归档查询结果解析出错(2)");
  621. $titleElem.text("历史归档查询结果解析出错(2)");
  622. }
  623. },
  624. onerror: function(e) {
  625. $titleElem.text("历史归档查询出错(2),请检查网络连接");
  626. }
  627. });
  628. }
  629.  
  630. /**
  631. * 修复收藏
  632. * @param {$节点} $item 当前收藏Item
  633. * @param {$节点} $titleElem 标题链接
  634. * @param {数字} avid av号
  635. * @param {字符串} title 标题
  636. * @param {字符串} pic 海报
  637. * @param {字符串} history 历史归档,若无时,使用 null
  638. * @param {字符串列表} parts 子P标题,默认为 null
  639. */
  640. function fixFavorites($item, $titleElem, avid, title, pic, history, parts) {
  641.  
  642. // 录入缓存
  643. if (!cache[avid] || !(cache[avid].success)) {
  644. cache[avid] = {success: true, title: title, pic: pic};
  645. if (history) cache[avid].history = history;
  646. if (parts) cache[avid].parts = parts;
  647. }
  648.  
  649. // 设置多个超链接跳转 biliplus
  650. const $aElems = $item.find("a:not(.bili-video-card__author)");
  651. $aElems.attr("href", `https://www.biliplus.com/${history ? history : ""}video/av${avid}/`);
  652.  
  653. // 设置标题文字
  654. $titleElem.text(stripTitleFirefox(title));
  655. $titleElem.attr("title", title);
  656.  
  657. // 保存标题和子P标题到节点上,以便让 showDetail 读取
  658. $item.attr("_title", title);
  659. if (parts) parts = "* "+parts.join("\n* ");
  660. if (parts) $item.attr("_parts", parts);
  661.  
  662. // 如果 showDetail 已经生成浮块,则替换浮块中的文本
  663. const $coverElem = $aElems.first();
  664. let content = $coverElem.attr("title");
  665. if (content) {
  666. content = content.replace(/\n标题:.*\n/, `\n标题:${title}\n`);
  667. if (parts) content = content.replace("播放数:", `子P标题:\n${parts}\n播放数:`);
  668. $coverElem.attr("title", content);
  669. }
  670.  
  671. // 设置标题样式
  672. setInvalItemStyle($item, $titleElem);
  673.  
  674. // 替换封面
  675. const $img = $item.find("img");
  676. $img.attr("src", pic);
  677. $item.find("source").remove();
  678. }
  679.  
  680. function fixFailed($item, $titleElem, avid) {
  681. $titleElem.text(`查不到标题/封面(${avid})`);
  682. $titleElem.attr("title", `查不到标题/封面(${avid})`);
  683. // 录入缓存
  684. if (!cache[avid]) cache[avid] = {success: false};
  685. }
  686.  
  687. /**
  688. * 标记失效的收藏
  689. * @param {$节点} $item 当前收藏Item
  690. * @param {$节点} $titleElem 标题链接
  691. */
  692. function setInvalItemStyle($item, $titleElem) {
  693. // 增加 删除线 + 置(灰)
  694. $titleElem.attr("style", `text-decoration:line-through;color:${invalTitleColor};`);
  695. // 收藏时间 + UP主(新UI)
  696. let $subtitle;
  697. if (isNewUI) {
  698. $subtitle = $item.find("div.bili-video-card__subtitle");
  699. } else {
  700. $subtitle = $item.find("div.meta.pubdate");
  701. }
  702. // 增加 删除线
  703. $subtitle.attr("style", "text-decoration:line-through");
  704. }
  705.  
  706. /**
  707. * 再次尝试加载
  708. * @param {$节点} $titleElem 标题链接
  709. * @param {数字} avid AV号
  710. * @param {布尔} delayRetry 延迟重试
  711. * @param {函数} fun 重试方法
  712. */
  713. function retryLoad($titleElem, avid, delayRetry, fun) {
  714.  
  715. console.warn(`[bilibili-fav-fix] 查询:av${avid},请求过快!`);
  716.  
  717. if (delayRetry) { // 延迟绑定
  718. $titleElem.text(`请求过快,${retryDelay}秒后再试!`);
  719. setTimeout(bindReload, retryDelay * 1000, $titleElem, fun);
  720. countdown($titleElem, retryDelay);
  721. } else { // 首次,立即绑定
  722. $titleElem.attr("href", "javascript:void(0);");
  723. $titleElem.attr("target", "_self");
  724. bindReload($titleElem, fun);
  725. }
  726. }
  727.  
  728. /**
  729. * 绑定重新加载
  730. * @param {$节点} $titleElem 标题链接
  731. * @param {函数} fun 重试方法
  732. */
  733. function bindReload($titleElem, fun) {
  734. $titleElem.text("->点击手动加载<-");
  735. $titleElem.click(function() {
  736. $(this).unbind("click");
  737. $titleElem.text("Loading...");
  738. fun();
  739. });
  740. }
  741.  
  742. /**
  743. * 重新绑定倒计时
  744. * @param {$节点} $titleElem 标题链接
  745. * @param {数字} second 秒
  746. */
  747. function countdown($titleElem, second) {
  748. if ($titleElem.text().indexOf("请求过快") === 0) {
  749. $titleElem.text(`请求过快,${second}秒后再试!`);
  750. if (second > 1) {
  751. setTimeout(countdown, 1000, $titleElem, second - 1);
  752. }
  753. }
  754. }
  755.  
  756. setInterval(handleFavorites, interval);
  757. })();