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
@@ -1,6 +1,8 @@
import { MpvSubtitleRenderMetrics } from '../../types';
import { sanitizeMediaTitle } from '../../shared/media-identity';
import { widenHalfwidthKana } from '../utils/halfwidth-kana';
import { filterSubtitleText } from '../../shared/subtitle-text-filter';
import type { SubtitleFilterConfig } from '../../types/subtitle';
import type { PluginMpvEventMap } from '../../plugin-api';
import { toVideoClock } from './subtitle-clock';
@@ -68,6 +70,7 @@ export interface MpvProtocolParseResult {
export interface MpvProtocolHandleMessageDeps {
getResolvedConfig: () => {
secondarySub?: { secondarySubLanguages?: Array<string> };
subtitleFilter?: SubtitleFilterConfig;
};
getSubtitleMetrics: () => MpvSubtitleRenderMetrics;
isVisibleOverlayVisible: () => boolean;
@@ -263,13 +266,19 @@ export function splitMpvMessagesFromBuffer(
};
}
// Primary text enters the app here, so every consumer sees it widened and filtered.
function toPrimarySubText(data: unknown, deps: MpvProtocolHandleMessageDeps): string {
const text = widenHalfwidthKana(typeof data === 'string' ? data : '');
return filterSubtitleText(text, deps.getResolvedConfig().subtitleFilter);
}
export async function dispatchMpvProtocolMessage(
msg: MpvMessage,
deps: MpvProtocolHandleMessageDeps,
): Promise<void> {
if (msg.event === 'property-change') {
if (msg.name === 'sub-text') {
const nextSubText = widenHalfwidthKana((msg.data as string) || '');
const nextSubText = toPrimarySubText(msg.data, deps);
const overlayVisible = deps.isVisibleOverlayVisible();
deps.emitSubtitleChange({
text: nextSubText,
@@ -498,7 +507,7 @@ export async function dispatchMpvProtocolMessage(
}>,
);
} else if (msg.request_id === MPV_REQUEST_ID_SUBTEXT) {
const nextSubText = widenHalfwidthKana((msg.data as string) || '');
const nextSubText = toPrimarySubText(msg.data, deps);
deps.setCurrentSubText(nextSubText);
deps.emitSubtitleChange({
text: nextSubText,
+6
View File
@@ -3,6 +3,7 @@ import { EventEmitter } from 'events';
import { Config, MpvClient, MpvSubtitleRenderMetrics } from '../../types';
import {
dispatchMpvProtocolMessage,
MPV_REQUEST_ID_SUBTEXT,
MPV_REQUEST_ID_TRACK_LIST_AUDIO,
MPV_REQUEST_ID_TRACK_LIST_SECONDARY,
MpvMessage,
@@ -585,6 +586,11 @@ export class MpvIpcClient implements MpvClient {
});
}
/** Re-reads `sub-text` so the current line passes through the protocol handler again. */
refreshSubtitleText(): void {
this.send({ command: ['get_property', 'sub-text'], request_id: MPV_REQUEST_ID_SUBTEXT });
}
async requestProperty(name: string): Promise<unknown> {
const response = await this.request(['get_property', name]);
if (response.error && response.error !== 'success') {