diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index ed033741..8ac66e73 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -769,17 +769,29 @@ "checkTimeDanmakuSkip": { "message": "检查片段附近是否已有上传的跳过信息,已有则不触发此功能" }, - "danmakuRegexPattern": { - "message": "匹配正则: " + "danmakuTimeMatchingRegexPattern": { + "message": "空降时间匹配正则: " }, "danmakuRegexPatternPlaceholder": { - "message": "正则要求能捕获2到3组数字" + "message": "能匹配2~3组数字的正则表达式" }, "danmakuRegexTitle": { - "message": "请输入一个匹配时间格式的正则表达式,要求能捕获2到3组数字。如果不清楚如何写正则表达式,请勿修改此项" + "message": "请输入一个匹配时间格式的正则表达式,要求能捕获2~3组数字。如果不清楚如何写正则表达式,请勿修改此项" }, - "danmakuRegexPatternDescription": { - "message": "启用基于弹幕的跳过功能,目前处于实验状态" + "danmakuTimeMatchingRegexPatternDescription": { + "message": "空降时间匹配类似“01:23”“四分五十六秒”“1分2秒”等格式的弹幕。" + }, + "danmakuOffsetMatchingRegexPattern": { + "message": "时间偏移匹配正则: " + }, + "danmakuOffsetRegexPatternPlaceholder": { + "message": "能匹配时间偏移指令的正则表达式" + }, + "danmakuOffsetRegexTitle": { + "message": "请输入一个匹配时间偏移指令的正则表达式。如果不清楚如何写正则表达式,请勿修改此项" + }, + "danmakuOffsetRegexPatternDescription": { + "message": "时间偏移匹配类似“向右12下”“右方向34次”等格式的弹幕,并以此计算空降时间。" }, "muteSegments": { "message": "允许片段静音而不是跳过" diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json index ed033741..8ac66e73 100644 --- a/public/_locales/zh_CN/messages.json +++ b/public/_locales/zh_CN/messages.json @@ -769,17 +769,29 @@ "checkTimeDanmakuSkip": { "message": "检查片段附近是否已有上传的跳过信息,已有则不触发此功能" }, - "danmakuRegexPattern": { - "message": "匹配正则: " + "danmakuTimeMatchingRegexPattern": { + "message": "空降时间匹配正则: " }, "danmakuRegexPatternPlaceholder": { - "message": "正则要求能捕获2到3组数字" + "message": "能匹配2~3组数字的正则表达式" }, "danmakuRegexTitle": { - "message": "请输入一个匹配时间格式的正则表达式,要求能捕获2到3组数字。如果不清楚如何写正则表达式,请勿修改此项" + "message": "请输入一个匹配时间格式的正则表达式,要求能捕获2~3组数字。如果不清楚如何写正则表达式,请勿修改此项" }, - "danmakuRegexPatternDescription": { - "message": "启用基于弹幕的跳过功能,目前处于实验状态" + "danmakuTimeMatchingRegexPatternDescription": { + "message": "空降时间匹配类似“01:23”“四分五十六秒”“1分2秒”等格式的弹幕。" + }, + "danmakuOffsetMatchingRegexPattern": { + "message": "时间偏移匹配正则: " + }, + "danmakuOffsetRegexPatternPlaceholder": { + "message": "能匹配时间偏移指令的正则表达式" + }, + "danmakuOffsetRegexTitle": { + "message": "请输入一个匹配时间偏移指令的正则表达式。如果不清楚如何写正则表达式,请勿修改此项" + }, + "danmakuOffsetRegexPatternDescription": { + "message": "时间偏移匹配类似“向右12下”“右方向34次”等格式的弹幕,并以此计算空降时间。" }, "muteSegments": { "message": "允许片段静音而不是跳过" diff --git a/public/_locales/zh_TW/messages.json b/public/_locales/zh_TW/messages.json index 69fcb567..34da1866 100644 --- a/public/_locales/zh_TW/messages.json +++ b/public/_locales/zh_TW/messages.json @@ -769,18 +769,30 @@ "checkTimeDanmakuSkip": { "message": "檢查片段附近是否已有上傳的跳過資訊,已有則不觸發此功能" }, - "danmakuRegexPattern": { - "message": "匹配正則: " + "danmakuTimeMatchingRegexPattern": { + "message": "空降時間匹配正則: " }, "danmakuRegexPatternPlaceholder": { - "message": "正則要求能捕獲2到3組數字" + "message": "能匹配2~3組數字的正則表達式" }, "danmakuRegexTitle": { - "message": "請輸入一個匹配時間格式的正則表達式,要求能捕獲2到3組數字。如果不清楚如何寫正則表達式,請勿修改此項" + "message": "請輸入一個匹配時間格式的正則表達式,要求能捕獲2~3組數字。如果不清楚如何寫正則表達式,請勿修改此項" }, - "danmakuRegexPatternDescription": { - "message": "啟用基於彈幕的跳過功能,目前處於實驗狀態" + "danmakuTimeMatchingRegexPatternDescription": { + "message": "空降時間匹配類似“01:23”“四分五十六秒”“1分2秒”等格式的彈幕。" }, + "danmakuOffsetMatchingRegexPattern": { + "message": "時間偏移匹配正則: " + }, + "danmakuOffsetRegexPatternPlaceholder": { + "message": "能匹配時間偏移指令的正則表達式" + }, + "danmakuOffsetRegexTitle": { + "message": "請輸入一個匹配時間偏移指令的正則表達式。如果不清楚如何寫正則表達式,請勿修改此項" + }, + "danmakuOffsetRegexPatternDescription": { + "message": "時間偏移匹配類似“向右12下”“右方向34次”等格式的彈幕,並以此計算空降時間。" + }, "muteSegments": { "message": "允許片段靜音而不是跳過" }, diff --git a/public/options/options.html b/public/options/options.html index fc7f22f8..1313f8ee 100644 --- a/public/options/options.html +++ b/public/options/options.html @@ -751,16 +751,27 @@

__MSG_exportOtherData__

- -
+ +
- - + +
-
__MSG_danmakuRegexPatternDescription__
+
__MSG_danmakuTimeMatchingRegexPatternDescription__
+
+ +
+
+ + + + +
+
__MSG_danmakuOffsetRegexPatternDescription__
diff --git a/src/config.ts b/src/config.ts index f22826f6..dd7456de 100644 --- a/src/config.ts +++ b/src/config.ts @@ -33,7 +33,8 @@ interface SBConfig { enableDanmakuSkip: boolean; enableAutoSkipDanmakuSkip: boolean; enableMenuDanmakuSkip: boolean; - danmakuRegexPattern: string; + danmakuTimeMatchingRegexPattern: string; + danmakuOffsetMatchingRegexPattern: string; checkTimeDanmakuSkip: boolean; muteSegments: boolean; fullVideoSegments: boolean; @@ -189,13 +190,8 @@ function migrateOldSyncFormats(config: SBConfig) { config["serverAddress"] = CompileConfig.serverAddress; } - // danmaku regex update since 0.5.0 - const oldDanmakuRegexPatterns = [ - "(?:空降\\s*)?(\\d{1,2}):(\\d{1,2})(?::(\\d{1,2}))?", // 0.5.0 - ]; - if (oldDanmakuRegexPatterns.includes(config["danmakuRegexPattern"])) { - config["danmakuRegexPattern"] = syncDefaults.danmakuRegexPattern; - } + // "danmakuRegexPattern" 参数在 0.5.9 版本(预计)中被移除,取而代之的是 "danmakuTimeMatchingRegexPattern" 和 "danmakuOffsetMatchingRegexPattern" 参数 + delete config["danmakuRegexPattern"]; } const syncDefaults = { @@ -217,7 +213,9 @@ const syncDefaults = { enableDanmakuSkip: false, enableAutoSkipDanmakuSkip: false, enableMenuDanmakuSkip: false, - danmakuRegexPattern: "(?:空降\\s*)?(\\d{1,2})[::](\\d{1,2})(?:[::](\\d{1,2}))?$", + danmakuTimeMatchingRegexPattern: + "(?:(\\d{1,2})\\s*(?:小时|h|H|:|:|;|;|\\.|-|—)\\s*)?(?:(\\d{1,2})\\s*(?:分钟|分|:|:|;|;|\\.|-|—|m|M)\\s*)?(?:(\\d{1,2})\\s*(秒|s|S)?)", + danmakuOffsetMatchingRegexPattern: "(?:^|(右|右滑|按|右下|右向|右方向|→|⇒|⇢|⇨|⮕|🡆|🠺|🠾|🢒|👉))(\\d+)(下|次)?$", checkTimeDanmakuSkip: true, muteSegments: true, diff --git a/src/content.ts b/src/content.ts index c6fd11b5..fa80ff47 100644 --- a/src/content.ts +++ b/src/content.ts @@ -42,6 +42,7 @@ import { isFirefox, isFirefoxOrSafari, isSafari, sleep, waitFor } from "./utils/ import { AnimationUtils } from "./utils/animationUtils"; import { addCleanupListener, cleanPage } from "./utils/cleanup"; import { defaultPreviewTime } from "./utils/constants"; +import { parseTargetTimeFromDanmaku } from "./utils/danmakusUtils"; import { findValidElement } from "./utils/dom"; import { importTimes } from "./utils/exporter"; import { getErrorMessage, getFormattedTime } from "./utils/formating"; @@ -778,29 +779,12 @@ async function startSponsorSchedule( } function checkDanmaku(text: string, offset: number) { - const match = new RegExp(Config.config.danmakuRegexPattern).exec(text); - if (!match) { - return; - } + const targetTime = parseTargetTimeFromDanmaku(text, getVirtualTime()); + if (targetTime === null) return; - 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]; - } + // [DEBUG] + // console.debug("检测到空降弹幕: ", text, "请求跳转到: ", targetTime); - const targetTime = hours * 3600 + minutes * 60 + seconds; const startTime = getVirtualTime() + offset; // ignore if the time is in the past diff --git a/src/utils/danmakusUtils.ts b/src/utils/danmakusUtils.ts new file mode 100644 index 00000000..62adea0e --- /dev/null +++ b/src/utils/danmakusUtils.ts @@ -0,0 +1,138 @@ +import Config from "../config"; + +/** + * 解析弹幕文本中的目标时间 + * + * @param text 输入需要解析的弹幕文本 + * @param currentTime 弹幕出现的时间 + * @returns 返回弹幕指向目标时间。若无法解析,则会返回null。 + */ +export function parseTargetTimeFromDanmaku(text: string, currentTime: number) { + /** + * 解析时间字符串并将其转换为总秒数。 + * + * @param text - 包含需要解析的时间的输入字符串。 + * @returns 由时间字符串表示的总秒数,如果时间字符串无效则返回null。 + * + * 该函数使用在 `Config.config.danmakuTimeMatchingRegexPattern` 中定义的正则表达式模式 + * 来匹配和提取输入字符串中的小时、分钟和秒。如果匹配成功且有效, + * 它通过将小时转换为秒、分钟转换为秒并将它们加到解析的秒数中来计算总秒数。 + */ + function parseTime(text: string) { + const regex = new RegExp(Config.config.danmakuTimeMatchingRegexPattern, "g"); + + let match: RegExpExecArray | null; + while ((match = regex.exec(text)) !== null) { + const [, , minutes, seconds, secondsSuffix] = match; + + if (seconds && (secondsSuffix || minutes)) { + const hours = parseInt(match[1] || "0"); + const minutes = parseInt(match[2] || "0"); + const seconds = parseInt(match[3] || "0"); + return hours * 3600 + minutes * 60 + seconds; + } + } + + return null; + } + + /** + * 解析弹幕文本中的偏移时间。 + * + * @param text - 包含偏移时间的弹幕文本。 + * @returns 如果找到匹配的偏移时间,返回偏移时间(以秒为单位);否则返回 null。 + * + * @remarks + * 该函数使用配置中的正则表达式模式来匹配偏移时间。匹配的偏移时间格式类似于“向右x下”, + * 其中 x 是一个整数,表示偏移的时间单位。偏移时间等价于当前时间加上 5 倍的 x 秒。 + */ + function parseOffsetTime(text: string) { + const regex = new RegExp(Config.config.danmakuOffsetMatchingRegexPattern, "g"); + + let match: RegExpExecArray | null; + while ((match = regex.exec(text)) !== null) { + const [, direction, offset, suffix] = match; + + if (offset && (direction || suffix)) { + // “向右x下”等价于当前时间 + 5x秒 + return parseInt(offset) * 5; + } + } + + return null; + } + + text = text.replace(/[零一二三四五六七八九两壹贰叁肆伍陆柒捌玖十百千万]+/g, (cnNum) => parseChineseNumber(cnNum)); + + const directParsedTime = parseTime(text); + if (directParsedTime) return directParsedTime; + else { + const offsetParsedTime = parseOffsetTime(text); + if (offsetParsedTime) return offsetParsedTime + currentTime; + } + return null; +} + +/** + * 将中文数字字符串转换为阿拉伯数字字符串。 + * + * @param inputText - 包含中文数字的字符串。 + * @returns 转换后的阿拉伯数字字符串。如果输入包含无效字符,则返回 null。 + * + * @example + * ```typescript + * parseChineseNumber("一"); // 返回 "1" + * parseChineseNumber("十二"); // 返回 "12" + * parseChineseNumber("二十"); // 返回 "20" + * parseChineseNumber("二十一"); // 返回 "21" + * parseChineseNumber("一百"); // 返回 null + * ``` + */ +export function parseChineseNumber(inputText: string) { + const cnChrMap: { [key: string]: number } = { + 零: 0, + 一: 1, + 二: 2, + 三: 3, + 四: 4, + 五: 5, + 六: 6, + 七: 7, + 八: 8, + 九: 9, + 两: 2, + 壹: 1, + 贰: 2, + 叁: 3, + 肆: 4, + 伍: 5, + 陆: 6, + 柒: 7, + 捌: 8, + 玖: 9, + }; + + const cnUnitMap: { [key: string]: number } = { + 十: 10, + }; + + let num = 0; + let unit = 1; + for (let i = 0; i < inputText.length; i++) { + const chr = inputText[i]; + if (chr in cnChrMap) { + num += cnChrMap[chr] * unit; + } else if (chr in cnUnitMap) { + unit = cnUnitMap[chr]; + if (num === 0 && unit === 10) { + num = 1; + } + num = num * unit; + unit = 1; + } else { + return null; + } + } + + return num.toString(); +}