- // ==UserScript==
- // @name [Bilibili] 自动切P
- // @namespace ckylin-bilibili-auto-next-part
- // @version 0.1
- // @description 自动在多P分集中切换下一P或跳过进度
- // @author CKylinMC
- // @match https://*.bilibili.com/video/av*
- // @match https://*.bilibili.com/video/BV*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
- // @require https://greasyfork.org/scripts/429720-cktools/code/CKTools.js?version=1023553
- // @grant unsafeWindow
- // @run-at document-idle
- // @license MIT
- // ==/UserScript==
-
- (function () {
- 'use strict';
- class Logger {
- constructor(prefix = '[logUtil]') {
- this.prefix = prefix;
- }
- log(...args) {
- console.log(this.prefix, ...args);
- }
- info(...args) {
- console.info(this.prefix, ...args);
- }
- warn(...args) {
- console.warn(this.prefix, ...args);
- }
- error(...args) {
- console.error(this.prefix, ...args);
- }
- }
- const logger = new Logger("[AUTOP]");
- if (CKTools.ver < 1.2) {
- logger.warn("Library script 'CKTools' was loaded incompatible version " + CKTools.ver + ", so that SNI may couldn't work correctly. Please consider update your scripts.");
- }
- const { get, getAll, domHelper, wait, waitForDom, waitForPageVisible, addStyle, modal, bili } = CKTools;
-
- const getVideoID = () => {
- let id = new URL(location.href).pathname.replace('/video/', '')
- if (id.endsWith('/')) {
- id = id.substring(0, id.length - 2);
- }
- if (id.startsWith('av')) {
- return { type: 'aid', id };
- }
- if (id.startsWith('BV')) {
- return { type: 'bvid', id };
- }
- return { type: 'unknown', id };
- }
- const getCurrentTime = () => dataStore.vid?.currentTime??-1;
- const getCurrentPart = () => {
- let part = new URL(location.href).searchParams.get('p');
- if (!part) part = '1';
- return +part;
- }
- async function playerReady() {
- let i = 150;
- while (--i > 0) {
- await wait(100);
- if (unsafeWindow.player?.isInitialized() ?? false) break;
- }
- if (i < 0) return false;
- await waitForPageVisible();
- while (1) {
- await wait(200);
- if (document.querySelector(".bilibili-player-video-control-wrap, .bpx-player-control-wrap")) return true;
- }
- }
- const dataStore = unsafeWindow.autonextpart = {
- p: 0,
- id: null,
- vid: null,
- config: {
- autoNextAt: -1,//>0 to enable
- partsDefined: [
- null,
- /* 1: *//*{
- startsAt: -1,
- endsAt: -1,
- ignoreGlobal: false,
- skip: [
- {
- from: -1,
- to: -1
- }
- ]
- }*/
- ]
- },
- next: () => {
- unsafeWindow.dispatchEvent(new KeyboardEvent("keydown", {
- key: "]",
- keyCode: 221,
- code: "BracketRight",
- which: 221,
- shiftKey: false,
- ctrlKey: false,
- metaKey: false
- }));
- },
- hasNext: () => {
-
- }
- };
-
- function parseDesc(desc) {
- const rootRegex = /AP:=(GP!(?<GP>\d+)!GP;){0,1}(?<parts>.+)*=:AP/m;
- let rootResult = rootRegex.exec(desc);
- if (!rootRegex || !rootResult.groups) return false;
-
- const { GP, parts } = rootResult.groups;
- if (!isNaN(+GP)) dataStore.config.autoNextAt = +GP;
-
- if (parts.length) {
- let partsSplited = parts.split(';').filter(i => i.trim().length);
- for (let part of partsSplited) {
- let [partName, start, end, subs, ignoreGlobal] = part.split('!');
- let partId = +(partName.substring(1));
- if (isNaN(partId)) continue;
- let config = { startAt: -1, endsAt: -1, skip: [] };
- if (!isNaN(+start)) config.startAt = +start;
- if (!isNaN(+end)) config.endsAt = +end;
- let subsParts = subs.split("+").filter(i => i.trim().length);
- for (let sub of subsParts) try {
- const [from, to] = JSON.parse(sub);
- if (!isNaN(+from) && !isNaN(+to)) config.skip.push({ from, to });
- } catch (e) { continue; }
- if (ignoreGlobal) config.ignoreGlobal = true;
- logger.info("发现配置: 分P", partId, "设定", config);
- dataStore.config.partsDefined[+partId] = config;
- }
- }
- return true;
- }
-
- async function tryInject() {
- logger.log("注入开始");
- dataStore.vid = document.querySelector('.bpx-player-video-wrap>video');
- if (!dataStore.vid) {
- logger.error("未能找到播放器...");
- return false;
- }
- logger.info("已找到播放器:", dataStore.vid);
- dataStore.id = getVideoID();
- if (dataStore.id.type == "unknown") {
- logger.error("无法识别的视频ID:", dataStore.id.id);
- // return;
- } else {
- logger.log("视频ID", dataStore.id);
- }
- dataStore.p = getCurrentPart();
- if (isNaN(dataStore.p)) {
- logger.error("未知分P:", dataStore.p);
- return;
- } else {
- logger.log("视频分P", dataStore.p);
- }
- dataStore.vid.removeEventListener("timeupdate", onTimeUpdate);
- dataStore.vid.addEventListener("timeupdate", onTimeUpdate);
- logger.log("视频进度已hook");
- try {
- let desc = document.querySelector('.desc-info-text');
- if (!desc) throw "";
- let descTxt = desc.textContent;
- // let descTxt = `AP:=GP!5!GP;=:AP`;
- if (descTxt.includes("AP:=")) {
- let startIdx = descTxt.indexOf("AP:=");
- let endIdx = descTxt.indexOf("=:AP");
- if (startIdx === -1 || endIdx === -1) throw "";
- parseDesc(descTxt);
- } else throw "";
- unsafeWindow.player?.toast.create({text:"自动切P已启用"})
- } catch (e) {
- logger.log("没有在描述中发现信息", e);
- }
- logger.log("注入完成");
- }
-
- function onTimeUpdate(event) {
- let t = getCurrentTime();
- if (t == -1) return;
- if (dataStore.config.partsDefined[dataStore.p]) {
- let cfg = dataStore.config.partsDefined[dataStore.p];
- if (cfg.startsAt > -1 && t < cfg.startsAt) {
- if (unsafeWindow.player)
- unsafeWindow.player.seek?.(cfg.startsAt);
- else if (dataStore.vid)
- dataStore.vid.currentTime = cfg.startsAt;
- return;
- }
- if (cfg.endsAt > -1 && t >= cfg.endsAt) {
- if (unsafeWindow.player){
- unsafeWindow.player.pause();
- unsafeWindow.player.toast.create({text:"正在切换下一P"})
- }
- else if (dataStore.vid)
- dataStore.vid.pause();
- dataStore.next();
- dataStore.p++;
- return;
- }
- //skip
- }
- if (dataStore.config.autoNextAt > -1 && t >= dataStore.config.autoNextAt) {
- if (unsafeWindow.player){
- unsafeWindow.player.pause();
- unsafeWindow.player.toast.create({text:"正在切换下一P"})
- }
- else if (dataStore.vid)
- dataStore.vid.pause();
- dataStore.next();
- dataStore.p++;
-
- return;
- }
- }
-
- function run() {
- logger.log("等待播放器...");
- playerReady().then(tryInject);
- }
- run();
- })();