feat(subtitles): add regex subtitle filters with built-in presets (#318)

- Add `subtitleFilter` config with four presets: parentheses, brackets, speaker prefix and music symbols. Custom regex rules run after the presets.
- Filter primary text where it enters the app (live `sub-text` and parsed cues), so the overlay, sidebar, mining and stats all get the same filtered text.
- Changes hot-reload: the current line is read again and the active track is parsed again, using the filter config as the prefetch parse key.
- Add a custom rule editor in Settings under Behavior → Subtitle Filters that checks patterns as you type.
- Update the config example, the configuration docs and the architecture domains doc, and add a changelog fragment.
This commit is contained in:
sudacode authored and GitHub committed 2026-11-03 00:33:58 -08:00
1 parent 848a6c4021
commit fa3051f9cb
28 files changed
+740 -11

No files matched your search

+11 -2
View File
@@ -11,6 +11,8 @@ export interface SubtitlePrefetchInitControllerDeps {
setCurrentService: (service: SubtitlePrefetchService | null) => void;
loadSubtitleSourceText: (source: string) => Promise<string>;
parseSubtitleCues: (content: string, filename: string) => SubtitleCue[];
/** Changes whenever parseSubtitleCues would produce different cues for the same content. */
getParseKey?: () => string;
createSubtitlePrefetchService: (deps: SubtitlePrefetchServiceDeps) => SubtitlePrefetchService;
tokenizeSubtitle: (text: string) => Promise<SubtitleData | null>;
preCacheTokenization: (text: string, data: SubtitleData) => void;
@@ -37,7 +39,12 @@ export function createSubtitlePrefetchInitController(
// What the running service was built from. Startup fires several inits for one source
// (Jellyfin preload seed, sid change, each track-list update); restarting on each would
// discard the in-flight tokenization and re-queue the priority window behind it.
let activeSource: { key: string; content: string; cues: SubtitleCue[] } | null = null;
let activeSource: {
key: string;
content: string;
parseKey: string;
cues: SubtitleCue[];
} | null = null;
const stopCurrentService = (): void => {
deps.getCurrentService()?.stop();
@@ -67,9 +74,11 @@ export function createSubtitlePrefetchInitController(
if (revision !== initRevision) {
return;
}
const parseKey = deps.getParseKey?.() ?? '';
if (
activeSource?.key === sourceKey &&
activeSource.content === content &&
activeSource.parseKey === parseKey &&
deps.getCurrentService()
) {
// A track change clears the published cues synchronously, so republish them.
@@ -106,7 +115,7 @@ export function createSubtitlePrefetchInitController(
}
deps.setCurrentService(nextService);
activeSource = { key: sourceKey, content, cues };
activeSource = { key: sourceKey, content, parseKey, cues };
deps.onParsedSubtitleCuesChanged?.(cues, sourceKey);
nextService.start(currentTimePos);
deps.logInfo(