refactor(character-dictionary): extract applyCharacterDictionarySelection helper

- Add `applyCharacterDictionarySelection` in its own module with injected deps
- Catches sync errors and emits a warning instead of propagating
- Remove duplicated inline logic from IPC and CLI startup handlers in main.ts
- Add unit test covering sync-failure resilience
This commit is contained in:
2026-04-25 20:03:38 -07:00
parent a05a698774
commit de19c40118
3 changed files with 75 additions and 12 deletions

View File

@@ -534,6 +534,7 @@ import {
resolveSubtitleSourcePath,
} from './main/runtime/subtitle-prefetch-source';
import { createSubtitlePrefetchInitController } from './main/runtime/subtitle-prefetch-init';
import { applyCharacterDictionarySelection } from './main/character-dictionary-selection';
import { codecToExtension, getSubsyncConfig } from './subsync/utils';
if (process.platform === 'linux') {
@@ -4861,12 +4862,16 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
retryAnilistQueueNow: () => processNextAnilistRetryUpdate(),
getCharacterDictionarySelection: () =>
characterDictionaryRuntime.getManualSelectionSnapshot(),
setCharacterDictionarySelection: async (mediaId: number) => {
const result = await characterDictionaryRuntime.setManualSelection({ mediaId });
resetAnilistMediaGuessState();
await characterDictionaryAutoSyncRuntime.runSyncNow();
return result;
},
setCharacterDictionarySelection: async (mediaId: number) =>
applyCharacterDictionarySelection(
{ mediaId },
{
setManualSelection: (request) => characterDictionaryRuntime.setManualSelection(request),
resetAnilistMediaGuessState,
runSyncNow: () => characterDictionaryAutoSyncRuntime.runSyncNow(),
warn: (message, error) => logger.warn(message, error),
},
),
appendClipboardVideoToQueue: () => appendClipboardVideoToQueue(),
...playlistBrowserMainDeps,
getImmersionTracker: () => appState.immersionTracker,
@@ -4951,12 +4956,14 @@ const { handleCliCommand, handleInitialArgs } = composeCliStartupHandlers({
},
getCharacterDictionarySelection: async (targetPath?: string) =>
characterDictionaryRuntime.getManualSelectionSnapshot(targetPath),
setCharacterDictionarySelection: async (request) => {
const result = await characterDictionaryRuntime.setManualSelection(request);
resetAnilistMediaGuessState();
await characterDictionaryAutoSyncRuntime.runSyncNow();
return result;
},
setCharacterDictionarySelection: async (request) =>
applyCharacterDictionarySelection(request, {
setManualSelection: (selectionRequest) =>
characterDictionaryRuntime.setManualSelection(selectionRequest),
resetAnilistMediaGuessState,
runSyncNow: () => characterDictionaryAutoSyncRuntime.runSyncNow(),
warn: (message, error) => logger.warn(message, error),
}),
runJellyfinCommand: (argsFromCommand: CliArgs) => runJellyfinCommand(argsFromCommand),
runStatsCommand: (argsFromCommand: CliArgs, source: CliCommandSource) =>
runStatsCliCommand(argsFromCommand, source),

View File

@@ -0,0 +1,27 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyCharacterDictionarySelection } from './character-dictionary-selection';
test('applyCharacterDictionarySelection returns saved override when post-save sync fails', async () => {
const warnings: unknown[] = [];
const result = await applyCharacterDictionarySelection(
{ mediaId: 21355 },
{
setManualSelection: async (request) => ({
ok: true,
seriesKey: `series-${request.mediaId}`,
selected: { id: request.mediaId, title: 'Re:ZERO', episodes: 25 },
staleMediaIds: [10607],
}),
resetAnilistMediaGuessState: () => {},
runSyncNow: async () => {
throw new Error('sync failed');
},
warn: (...args) => warnings.push(args),
},
);
assert.equal(result.selected.id, 21355);
assert.equal(warnings.length, 1);
});

View File

@@ -0,0 +1,29 @@
import type { CharacterDictionaryManualSelectionResult } from './character-dictionary-runtime/types';
export type CharacterDictionarySelectionRequest = {
targetPath?: string;
mediaId: number;
};
export type CharacterDictionarySelectionDeps = {
setManualSelection: (
request: CharacterDictionarySelectionRequest,
) => Promise<CharacterDictionaryManualSelectionResult>;
resetAnilistMediaGuessState: () => void;
runSyncNow: () => Promise<void>;
warn: (message: string, error?: unknown) => void;
};
export async function applyCharacterDictionarySelection(
request: CharacterDictionarySelectionRequest,
deps: CharacterDictionarySelectionDeps,
): Promise<CharacterDictionaryManualSelectionResult> {
const result = await deps.setManualSelection(request);
deps.resetAnilistMediaGuessState();
try {
await deps.runSyncNow();
} catch (error) {
deps.warn('Character dictionary auto-sync failed after manual selection', error);
}
return result;
}