Greasy Fork is available in English.

[Bilibili] 自动切P

自动在多P分集中切换下一P或跳过进度

  1. // ==UserScript==
  2. // @name [Bilibili] 自动切P
  3. // @namespace ckylin-bilibili-auto-next-part
  4. // @version 0.1
  5. // @description 自动在多P分集中切换下一P或跳过进度
  6. // @author CKylinMC
  7. // @match https://*.bilibili.com/video/av*
  8. // @match https://*.bilibili.com/video/BV*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
  10. // @require https://greasyfork.org/scripts/429720-cktools/code/CKTools.js?version=1023553
  11. // @grant unsafeWindow
  12. // @run-at document-idle
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict';
  18. class Logger {
  19. constructor(prefix = '[logUtil]') {
  20. this.prefix = prefix;
  21. }
  22. log(...args) {
  23. console.log(this.prefix, ...args);
  24. }
  25. info(...args) {
  26. console.info(this.prefix, ...args);
  27. }
  28. warn(...args) {
  29. console.warn(this.prefix, ...args);
  30. }
  31. error(...args) {
  32. console.error(this.prefix, ...args);
  33. }
  34. }
  35. const logger = new Logger("[AUTOP]");
  36. if (CKTools.ver < 1.2) {
  37. logger.warn("Library script 'CKTools' was loaded incompatible version " + CKTools.ver + ", so that SNI may couldn't work correctly. Please consider update your scripts.");
  38. }
  39. const { get, getAll, domHelper, wait, waitForDom, waitForPageVisible, addStyle, modal, bili } = CKTools;
  40.  
  41. const getVideoID = () => {
  42. let id = new URL(location.href).pathname.replace('/video/', '')
  43. if (id.endsWith('/')) {
  44. id = id.substring(0, id.length - 2);
  45. }
  46. if (id.startsWith('av')) {
  47. return { type: 'aid', id };
  48. }
  49. if (id.startsWith('BV')) {
  50. return { type: 'bvid', id };
  51. }
  52. return { type: 'unknown', id };
  53. }
  54. const getCurrentTime = () => dataStore.vid?.currentTime??-1;
  55. const getCurrentPart = () => {
  56. let part = new URL(location.href).searchParams.get('p');
  57. if (!part) part = '1';
  58. return +part;
  59. }
  60. async function playerReady() {
  61. let i = 150;
  62. while (--i > 0) {
  63. await wait(100);
  64. if (unsafeWindow.player?.isInitialized() ?? false) break;
  65. }
  66. if (i < 0) return false;
  67. await waitForPageVisible();
  68. while (1) {
  69. await wait(200);
  70. if (document.querySelector(".bilibili-player-video-control-wrap, .bpx-player-control-wrap")) return true;
  71. }
  72. }
  73. const dataStore = unsafeWindow.autonextpart = {
  74. p: 0,
  75. id: null,
  76. vid: null,
  77. config: {
  78. autoNextAt: -1,//>0 to enable
  79. partsDefined: [
  80. null,
  81. /* 1: *//*{
  82. startsAt: -1,
  83. endsAt: -1,
  84. ignoreGlobal: false,
  85. skip: [
  86. {
  87. from: -1,
  88. to: -1
  89. }
  90. ]
  91. }*/
  92. ]
  93. },
  94. next: () => {
  95. unsafeWindow.dispatchEvent(new KeyboardEvent("keydown", {
  96. key: "]",
  97. keyCode: 221,
  98. code: "BracketRight",
  99. which: 221,
  100. shiftKey: false,
  101. ctrlKey: false,
  102. metaKey: false
  103. }));
  104. },
  105. hasNext: () => {
  106.  
  107. }
  108. };
  109.  
  110. function parseDesc(desc) {
  111. const rootRegex = /AP:=(GP!(?<GP>\d+)!GP;){0,1}(?<parts>.+)*=:AP/m;
  112. let rootResult = rootRegex.exec(desc);
  113. if (!rootRegex || !rootResult.groups) return false;
  114.  
  115. const { GP, parts } = rootResult.groups;
  116. if (!isNaN(+GP)) dataStore.config.autoNextAt = +GP;
  117.  
  118. if (parts.length) {
  119. let partsSplited = parts.split(';').filter(i => i.trim().length);
  120. for (let part of partsSplited) {
  121. let [partName, start, end, subs, ignoreGlobal] = part.split('!');
  122. let partId = +(partName.substring(1));
  123. if (isNaN(partId)) continue;
  124. let config = { startAt: -1, endsAt: -1, skip: [] };
  125. if (!isNaN(+start)) config.startAt = +start;
  126. if (!isNaN(+end)) config.endsAt = +end;
  127. let subsParts = subs.split("+").filter(i => i.trim().length);
  128. for (let sub of subsParts) try {
  129. const [from, to] = JSON.parse(sub);
  130. if (!isNaN(+from) && !isNaN(+to)) config.skip.push({ from, to });
  131. } catch (e) { continue; }
  132. if (ignoreGlobal) config.ignoreGlobal = true;
  133. logger.info("发现配置: 分P", partId, "设定", config);
  134. dataStore.config.partsDefined[+partId] = config;
  135. }
  136. }
  137. return true;
  138. }
  139.  
  140. async function tryInject() {
  141. logger.log("注入开始");
  142. dataStore.vid = document.querySelector('.bpx-player-video-wrap>video');
  143. if (!dataStore.vid) {
  144. logger.error("未能找到播放器...");
  145. return false;
  146. }
  147. logger.info("已找到播放器:", dataStore.vid);
  148. dataStore.id = getVideoID();
  149. if (dataStore.id.type == "unknown") {
  150. logger.error("无法识别的视频ID:", dataStore.id.id);
  151. // return;
  152. } else {
  153. logger.log("视频ID", dataStore.id);
  154. }
  155. dataStore.p = getCurrentPart();
  156. if (isNaN(dataStore.p)) {
  157. logger.error("未知分P:", dataStore.p);
  158. return;
  159. } else {
  160. logger.log("视频分P", dataStore.p);
  161. }
  162. dataStore.vid.removeEventListener("timeupdate", onTimeUpdate);
  163. dataStore.vid.addEventListener("timeupdate", onTimeUpdate);
  164. logger.log("视频进度已hook");
  165. try {
  166. let desc = document.querySelector('.desc-info-text');
  167. if (!desc) throw "";
  168. let descTxt = desc.textContent;
  169. // let descTxt = `AP:=GP!5!GP;=:AP`;
  170. if (descTxt.includes("AP:=")) {
  171. let startIdx = descTxt.indexOf("AP:=");
  172. let endIdx = descTxt.indexOf("=:AP");
  173. if (startIdx === -1 || endIdx === -1) throw "";
  174. parseDesc(descTxt);
  175. } else throw "";
  176. unsafeWindow.player?.toast.create({text:"自动切P已启用"})
  177. } catch (e) {
  178. logger.log("没有在描述中发现信息", e);
  179. }
  180. logger.log("注入完成");
  181. }
  182.  
  183. function onTimeUpdate(event) {
  184. let t = getCurrentTime();
  185. if (t == -1) return;
  186. if (dataStore.config.partsDefined[dataStore.p]) {
  187. let cfg = dataStore.config.partsDefined[dataStore.p];
  188. if (cfg.startsAt > -1 && t < cfg.startsAt) {
  189. if (unsafeWindow.player)
  190. unsafeWindow.player.seek?.(cfg.startsAt);
  191. else if (dataStore.vid)
  192. dataStore.vid.currentTime = cfg.startsAt;
  193. return;
  194. }
  195. if (cfg.endsAt > -1 && t >= cfg.endsAt) {
  196. if (unsafeWindow.player){
  197. unsafeWindow.player.pause();
  198. unsafeWindow.player.toast.create({text:"正在切换下一P"})
  199. }
  200. else if (dataStore.vid)
  201. dataStore.vid.pause();
  202. dataStore.next();
  203. dataStore.p++;
  204. return;
  205. }
  206. //skip
  207. }
  208. if (dataStore.config.autoNextAt > -1 && t >= dataStore.config.autoNextAt) {
  209. if (unsafeWindow.player){
  210. unsafeWindow.player.pause();
  211. unsafeWindow.player.toast.create({text:"正在切换下一P"})
  212. }
  213. else if (dataStore.vid)
  214. dataStore.vid.pause();
  215. dataStore.next();
  216. dataStore.p++;
  217. return;
  218. }
  219. }
  220.  
  221. function run() {
  222. logger.log("等待播放器...");
  223. playerReady().then(tryInject);
  224. }
  225. run();
  226. })();