summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/subtitles/srt.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/lib/subtitles/srt.ts')
-rw-r--r--apps/web/src/lib/subtitles/srt.ts98
1 files changed, 98 insertions, 0 deletions
diff --git a/apps/web/src/lib/subtitles/srt.ts b/apps/web/src/lib/subtitles/srt.ts
new file mode 100644
index 00000000..197e96f9
--- /dev/null
+++ b/apps/web/src/lib/subtitles/srt.ts
@@ -0,0 +1,98 @@
+import type { ParseSubtitleResult, SubtitleCue } from "./types";
+
+const TIMESTAMP_SEPARATOR = /\s*-->\s*/;
+const TIMESTAMP_PATTERN =
+ /^(\d{2}:\d{2}:\d{2}[,.]\d{1,3})\s*-->\s*(\d{2}:\d{2}:\d{2}[,.]\d{1,3})/;
+
+export function parseSrt({ input }: { input: string }): ParseSubtitleResult {
+ const normalized = input.replace(/\r\n?/g, "\n").trim();
+ if (!normalized) {
+ return {
+ captions: [],
+ skippedCueCount: 0,
+ warnings: [],
+ };
+ }
+
+ const blocks = normalized.split(/\n{2,}/);
+ const cues: SubtitleCue[] = [];
+ let skippedCueCount = 0;
+
+ for (const block of blocks) {
+ const lines = block
+ .split("\n")
+ .map((line) => line.trim())
+ .filter((line) => line.length > 0);
+
+ if (lines.length < 2) {
+ skippedCueCount += 1;
+ continue;
+ }
+
+ const timestampIndex = TIMESTAMP_SEPARATOR.test(lines[0]) ? 0 : 1;
+ const timestampLine = lines[timestampIndex];
+ if (!timestampLine || !TIMESTAMP_PATTERN.test(timestampLine)) {
+ skippedCueCount += 1;
+ continue;
+ }
+
+ const textLines = lines.slice(timestampIndex + 1);
+ const text = textLines.join("\n").trim();
+ if (!text) {
+ skippedCueCount += 1;
+ continue;
+ }
+
+ const [rawStart, rawEnd] = timestampLine.split(TIMESTAMP_SEPARATOR);
+ if (!rawStart || !rawEnd) {
+ skippedCueCount += 1;
+ continue;
+ }
+
+ const startTime = parseSrtTimestamp({ input: rawStart });
+ const endTime = parseSrtTimestamp({ input: rawEnd });
+ const duration = endTime - startTime;
+
+ if (
+ !Number.isFinite(startTime) ||
+ !Number.isFinite(endTime) ||
+ duration <= 0
+ ) {
+ skippedCueCount += 1;
+ continue;
+ }
+
+ cues.push({
+ text,
+ startTime,
+ duration,
+ });
+ }
+
+ return {
+ captions: cues,
+ skippedCueCount,
+ warnings: [],
+ };
+}
+
+function parseSrtTimestamp({ input }: { input: string }): number {
+ const normalized = input.trim().replace(",", ".");
+ const match = normalized.match(/^(\d{2}):(\d{2}):(\d{2})\.(\d{1,3})$/);
+ if (!match) {
+ return Number.NaN;
+ }
+
+ const [, hours, minutes, seconds, milliseconds] = match;
+ const parsedHours = Number.parseInt(hours, 10);
+ const parsedMinutes = Number.parseInt(minutes, 10);
+ const parsedSeconds = Number.parseInt(seconds, 10);
+ const parsedMilliseconds = Number.parseInt(milliseconds.padEnd(3, "0"), 10);
+
+ return (
+ parsedHours * 3600 +
+ parsedMinutes * 60 +
+ parsedSeconds +
+ parsedMilliseconds / 1000
+ );
+}