diff --git a/public/_locales/en/en.messages.json b/public/_locales/en/en.messages.json
index 3a25fb76..3433cf21 100644
--- a/public/_locales/en/en.messages.json
+++ b/public/_locales/en/en.messages.json
@@ -809,6 +809,30 @@
"autoSkipOnMusicVideos": {
"message": "Auto skip all segments when there is a non-music segment"
},
+ "danmakuSkip": {
+ "message": "Match parachute time from the danmaku"
+ },
+ "autoSkipDanmakuSkip": {
+ "message": "Automatically skip after matching successfully"
+ },
+ "menuDanmakuSkip": {
+ "message": "Open the submission window after matching successfully"
+ },
+ "checkTimeDanmakuSkip": {
+ "message": "Check if there is already uploaded skip info near the segment, if so, this feature will not be triggered"
+ },
+ "danmakuRegexPattern": {
+ "message": "Match Regex: "
+ },
+ "danmakuRegexPatternPlaceholder": {
+ "message": "Regex requires capturing 2 to 3 sets of numbers"
+ },
+ "danmakuRegexTitle": {
+ "message": "Please enter a regex that matches the time format, which should capture 2 to 3 sets of numbers. If you're not sure how to write a regex, do not modify this option"
+ },
+ "danmakuRegexPatternDescription": {
+ "message": "Enable the skip function based on danmaku, currently in experimental status"
+ },
"muteSegments": {
"message": "Allow segments that mute audio instead of skip"
},
diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json
index 4797e486..3406a261 100644
--- a/public/_locales/en/messages.json
+++ b/public/_locales/en/messages.json
@@ -754,6 +754,30 @@
"autoSkipOnMusicVideos": {
"message": "自动跳过所有音乐视频中的非音乐片段"
},
+ "danmakuSkip": {
+ "message": "从弹幕中匹配空降时间"
+ },
+ "autoSkipDanmakuSkip": {
+ "message": "匹配成功后自动跳过"
+ },
+ "menuDanmakuSkip": {
+ "message": "匹配成功后打开提交窗口"
+ },
+ "checkTimeDanmakuSkip": {
+ "message": "检查片段附近是否已有上传的跳过信息,已有则不触发此功能"
+ },
+ "danmakuRegexPattern": {
+ "message": "匹配正则: "
+ },
+ "danmakuRegexPatternPlaceholder": {
+ "message": "正则要求能捕获2到3组数字"
+ },
+ "danmakuRegexTitle": {
+ "message": "请输入一个匹配时间格式的正则表达式,要求能捕获2到3组数字。如果不清楚如何写正则表达式,请勿修改此项"
+ },
+ "danmakuRegexPatternDescription": {
+ "message": "启用基于弹幕的跳过功能,目前处于实验状态"
+ },
"muteSegments": {
"message": "允许片段静音而不是跳过"
},
diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json
index 4797e486..3406a261 100644
--- a/public/_locales/zh_CN/messages.json
+++ b/public/_locales/zh_CN/messages.json
@@ -754,6 +754,30 @@
"autoSkipOnMusicVideos": {
"message": "自动跳过所有音乐视频中的非音乐片段"
},
+ "danmakuSkip": {
+ "message": "从弹幕中匹配空降时间"
+ },
+ "autoSkipDanmakuSkip": {
+ "message": "匹配成功后自动跳过"
+ },
+ "menuDanmakuSkip": {
+ "message": "匹配成功后打开提交窗口"
+ },
+ "checkTimeDanmakuSkip": {
+ "message": "检查片段附近是否已有上传的跳过信息,已有则不触发此功能"
+ },
+ "danmakuRegexPattern": {
+ "message": "匹配正则: "
+ },
+ "danmakuRegexPatternPlaceholder": {
+ "message": "正则要求能捕获2到3组数字"
+ },
+ "danmakuRegexTitle": {
+ "message": "请输入一个匹配时间格式的正则表达式,要求能捕获2到3组数字。如果不清楚如何写正则表达式,请勿修改此项"
+ },
+ "danmakuRegexPatternDescription": {
+ "message": "启用基于弹幕的跳过功能,目前处于实验状态"
+ },
"muteSegments": {
"message": "允许片段静音而不是跳过"
},
diff --git a/public/_locales/zh_TW/messages.json b/public/_locales/zh_TW/messages.json
index c9696c30..d7663498 100644
--- a/public/_locales/zh_TW/messages.json
+++ b/public/_locales/zh_TW/messages.json
@@ -754,6 +754,30 @@
"autoSkipOnMusicVideos": {
"message": "自動跳過所有音樂影片中的非音樂片段"
},
+ "danmakuSkip": {
+ "message": "從彈幕中匹配空降時間"
+ },
+ "autoSkipDanmakuSkip": {
+ "message": "匹配成功後自動跳過"
+ },
+ "menuDanmakuSkip": {
+ "message": "匹配成功後打開提交窗口"
+ },
+ "checkTimeDanmakuSkip": {
+ "message": "檢查片段附近是否已有上傳的跳過信息,已有則不觸發此功能"
+ },
+ "danmakuRegexPattern": {
+ "message": "匹配正則: "
+ },
+ "danmakuRegexPatternPlaceholder": {
+ "message": "正則要求能捕獲2到3組數字"
+ },
+ "danmakuRegexTitle": {
+ "message": "請輸入一個匹配時間格式的正則表達式,要求能捕獲2到3組數字。如果不清楚如何寫正則表達式,請勿修改此項"
+ },
+ "danmakuRegexPatternDescription": {
+ "message": "啟用基於彈幕的跳過功能,目前處於實驗狀態"
+ },
"muteSegments": {
"message": "允許片段靜音而不是跳過"
},
diff --git a/public/options/options.html b/public/options/options.html
index eb2b71ea..cffb8719 100644
--- a/public/options/options.html
+++ b/public/options/options.html
@@ -65,6 +65,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
__MSG_danmakuRegexPatternDescription__
+
diff --git a/src/config.ts b/src/config.ts
index 7fa8873c..18ebdb54 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -30,6 +30,11 @@ interface SBConfig {
submissionCountSinceCategories: number; // New count used to show the "Read The Guidelines!!" message
showTimeWithSkips: boolean;
disableSkipping: boolean;
+ enableDanmakuSkip: boolean;
+ enableAutoSkipDanmakuSkip: boolean;
+ enableMenuDanmakuSkip: boolean;
+ danmakuRegexPattern: string;
+ checkTimeDanmakuSkip: boolean;
muteSegments: boolean;
fullVideoSegments: boolean;
fullVideoLabelsOnThumbnails: boolean;
@@ -197,6 +202,11 @@ const syncDefaults = {
submissionCountSinceCategories: 0,
showTimeWithSkips: true,
disableSkipping: false,
+ enableDanmakuSkip: false,
+ enableAutoSkipDanmakuSkip: false,
+ enableMenuDanmakuSkip: false,
+ danmakuRegexPattern: "(?:空降\\s*)?(\\d{1,2}):(\\d{1,2})(?::(\\d{1,2}))?",
+ checkTimeDanmakuSkip: true,
muteSegments: true,
fullVideoSegments: true,
fullVideoLabelsOnThumbnails: true,
diff --git a/src/content.ts b/src/content.ts
index b530ee39..911d717a 100644
--- a/src/content.ts
+++ b/src/content.ts
@@ -762,6 +762,101 @@ async function startSponsorSchedule(
}
}
+function checkDanmaku(text: string, offset: number) {
+ const regex = new RegExp(Config.config.danmakuRegexPattern)
+ const match = regex.exec(text);
+
+ if (match) {
+ const timeComponents = match.slice(1).filter(Boolean).map((value) => parseInt(value, 10));
+ let hours = 0, minutes = 0, seconds = 0;
+
+ if (timeComponents.length === 2) {
+ minutes = timeComponents[0];
+ seconds = timeComponents[1];
+ } else if (timeComponents.length === 3) {
+ hours = timeComponents[0];
+ minutes = timeComponents[1];
+ seconds = timeComponents[2];
+ }
+
+ const time = hours * 3600 + minutes * 60 + seconds;
+ const currentTime = getVirtualTime() + offset;
+
+ if (Config.config.checkTimeDanmakuSkip && isSegmentMarkedNearCurrentTime(currentTime))
+ return;
+
+ const skippingSegments: SponsorTime[] =
+ [{ actionType: ActionType.Skip, segment: [currentTime, time],
+ source: SponsorSourceType.Local, UUID: generateUserID() as SegmentUUID,
+ category: "sponsor" as Category}];
+ setTimeout(() => {
+ skipToTime({
+ v: getVideo(),
+ skipTime: [currentTime, time],
+ skippingSegments,
+ openNotice: true,
+ forceAutoSkip: Config.config.enableAutoSkipDanmakuSkip,
+ unskipTime: currentTime
+ });
+ if (Config.config.enableMenuDanmakuSkip) {
+ setTimeout(() => {
+ if (!sponsorTimesSubmitting?.some((s) => s.segment[1] === skippingSegments[0].segment[1])) {
+ sponsorTimesSubmitting.push(skippingSegments[0]);
+ }
+ openSubmissionMenu();
+ }, Config.config.skipNoticeDuration * 1000 + 500);
+ }
+ }, offset * 1000 - 100);
+ }
+}
+
+let observer: MutationObserver = null;
+const processedDanmaku = new Set();
+
+function danmakuForSkip(clean: boolean = false) {
+ if (observer)
+ return;
+ if (clean) {
+ observer.disconnect();
+ observer = null;
+ processedDanmaku.clear();
+ return;
+ }
+ const targetNode = document.querySelector('.bpx-player-row-dm-wrap'); // 选择父节点
+ const config = { attributes: true, subtree: true }; // 观察属性变化
+ const callback = (mutationsList: MutationRecord[]) => {
+ if (!Config.config.enableDanmakuSkip || Config.config.disableSkipping)
+ return;
+ if (targetNode.classList.contains('bili-danmaku-x-paused'))
+ return;
+ for (const mutation of mutationsList) {
+ const target = mutation.target as HTMLElement;
+ if (mutation.type === 'attributes' && target.classList.contains('bili-danmaku-x-dm')) {
+ const content = mutation.target.textContent;
+ if (target.classList.contains('bili-danmaku-x-show')) {
+ if (!content || processedDanmaku.has(content)) {
+ continue;
+ }
+ processedDanmaku.add(content);
+ const offset = target.classList.contains("bili-danmaku-x-center") ? 0.3 : 1;
+ checkDanmaku(content, offset);
+ } else {
+ if (!content || processedDanmaku.has(content)) {
+ processedDanmaku.delete(content);
+ }
+ }
+ }
+ }
+ };
+ observer = new MutationObserver(callback);
+ observer.observe(targetNode, config);
+ addCleanupListener(() => {
+ if (observer) {
+ danmakuForSkip(true);
+ }
+ });
+}
+
/**
* Used on Firefox only, waits for the next animation frame until
* the video time has changed
@@ -802,6 +897,16 @@ function inMuteSegment(currentTime: number, includeOverlap: boolean): boolean {
return sponsorTimes?.some(checkFunction) || sponsorTimesSubmitting.some(checkFunction);
}
+function isSegmentMarkedNearCurrentTime(currentTime: number, range: number = 5): boolean {
+ const lowerBound = currentTime - range;
+ const upperBound = currentTime + range;
+
+ return sponsorTimes?.some(sponsorTime => {
+ const { segment: [startTime, endTime] } = sponsorTime;
+ return startTime <= upperBound && endTime >= lowerBound;
+ });
+}
+
/**
* This makes sure the videoID is still correct and if the sponsorTime is included
*/
@@ -858,6 +963,7 @@ function setupVideoListeners() {
}
if (!Config.config.disableSkipping) {
+ danmakuForSkip();
switchingVideos = false;
let startedWaiting = false;