From de19c401187a63246636466e971429b46bd2123f Mon Sep 17 00:00:00 2001 From: sudacode Date: Sat, 25 Apr 2026 20:03:38 -0700 Subject: [PATCH] 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 --- src/main.ts | 31 ++++++++++++------- .../character-dictionary-selection.test.ts | 27 ++++++++++++++++ src/main/character-dictionary-selection.ts | 29 +++++++++++++++++ 3 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 src/main/character-dictionary-selection.test.ts create mode 100644 src/main/character-dictionary-selection.ts diff --git a/src/main.ts b/src/main.ts index 2e115b7d..582c5c4a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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), diff --git a/src/main/character-dictionary-selection.test.ts b/src/main/character-dictionary-selection.test.ts new file mode 100644 index 00000000..aa511c49 --- /dev/null +++ b/src/main/character-dictionary-selection.test.ts @@ -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); +}); diff --git a/src/main/character-dictionary-selection.ts b/src/main/character-dictionary-selection.ts new file mode 100644 index 00000000..94b5c55a --- /dev/null +++ b/src/main/character-dictionary-selection.ts @@ -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; + resetAnilistMediaGuessState: () => void; + runSyncNow: () => Promise; + warn: (message: string, error?: unknown) => void; +}; + +export async function applyCharacterDictionarySelection( + request: CharacterDictionarySelectionRequest, + deps: CharacterDictionarySelectionDeps, +): Promise { + 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; +}