feat(dictionary): add Hachidori backend support (#266)

This commit is contained in:
sudacode authored and GitHub committed 2026-10-24 21:08:18 -07:00
1 parent e690ffba07
commit cbaa4626e1
143 files changed
+5564 -305

No files matched your search

+35 -5
View File
@@ -4,6 +4,7 @@ import type {
} from '../../core/services/subtitle-prefetch';
import type { SubtitleData } from '../../types';
import type { SubtitleCue } from '../../types';
import { buildOverlapPrefetchCues } from './subtitle-prefetch-overlaps';
export interface SubtitlePrefetchInitControllerDeps {
getCurrentService: () => SubtitlePrefetchService | null;
@@ -14,6 +15,7 @@ export interface SubtitlePrefetchInitControllerDeps {
tokenizeSubtitle: (text: string) => Promise<SubtitleData | null>;
preCacheTokenization: (text: string, data: SubtitleData) => void;
hasCachedTokenization?: (text: string) => boolean;
getCacheGeneration?: () => number;
logInfo: (message: string) => void;
logWarn: (message: string) => void;
onParsedSubtitleCuesChanged?: (cues: SubtitleCue[] | null, sourceKey: string | null) => void;
@@ -32,11 +34,20 @@ export function createSubtitlePrefetchInitController(
deps: SubtitlePrefetchInitControllerDeps,
): SubtitlePrefetchInitController {
let initRevision = 0;
// 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;
const stopCurrentService = (): void => {
deps.getCurrentService()?.stop();
deps.setCurrentService(null);
activeSource = null;
};
const cancelPendingInit = (): void => {
initRevision += 1;
deps.getCurrentService()?.stop();
deps.setCurrentService(null);
stopCurrentService();
deps.onParsedSubtitleCuesChanged?.(null, null);
};
@@ -46,14 +57,26 @@ export function createSubtitlePrefetchInitController(
sourceKey = sourcePath,
): Promise<void> => {
const revision = ++initRevision;
deps.getCurrentService()?.stop();
deps.setCurrentService(null);
// The same source may still be current once loaded; keep its service running until then.
if (activeSource?.key !== sourceKey) {
stopCurrentService();
}
try {
const content = await deps.loadSubtitleSourceText(sourcePath);
if (revision !== initRevision) {
return;
}
if (
activeSource?.key === sourceKey &&
activeSource.content === content &&
deps.getCurrentService()
) {
// A track change clears the published cues synchronously, so republish them.
deps.onParsedSubtitleCuesChanged?.(activeSource.cues, sourceKey);
return;
}
stopCurrentService();
const cues = deps.parseSubtitleCues(content, sourcePath);
if (revision !== initRevision || cues.length === 0) {
@@ -66,11 +89,16 @@ export function createSubtitlePrefetchInitController(
return;
}
// Overlap cues only feed the cache; the published cue list stays as authored.
const prefetchCues = [...cues, ...buildOverlapPrefetchCues(cues)].sort(
(a, b) => a.startTime - b.startTime,
);
const nextService = deps.createSubtitlePrefetchService({
cues,
cues: prefetchCues,
tokenizeSubtitle: (text) => deps.tokenizeSubtitle(text),
preCacheTokenization: (text, data) => deps.preCacheTokenization(text, data),
hasCachedTokenization: (text) => deps.hasCachedTokenization?.(text) ?? false,
getCacheGeneration: deps.getCacheGeneration,
});
if (revision !== initRevision) {
@@ -78,6 +106,7 @@ export function createSubtitlePrefetchInitController(
}
deps.setCurrentService(nextService);
activeSource = { key: sourceKey, content, cues };
deps.onParsedSubtitleCuesChanged?.(cues, sourceKey);
nextService.start(currentTimePos);
deps.logInfo(
@@ -85,6 +114,7 @@ export function createSubtitlePrefetchInitController(
);
} catch (error) {
if (revision === initRevision) {
stopCurrentService();
deps.onParsedSubtitleCuesChanged?.(null, null);
deps.logWarn(`[subtitle-prefetch] failed to initialize: ${(error as Error).message}`);
}