run prettier

This commit is contained in:
2026-02-28 21:15:22 -08:00
parent e4038127cb
commit cbff3f9ad9
146 changed files with 891 additions and 584 deletions

View File

@@ -1,4 +1,3 @@
<!-- BACKLOG.MD MCP GUIDELINES START --> <!-- BACKLOG.MD MCP GUIDELINES START -->
<CRITICAL_INSTRUCTION> <CRITICAL_INSTRUCTION>
@@ -17,6 +16,7 @@ This project uses Backlog.md MCP for all task and project management activities.
- **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work - **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work
These guides cover: These guides cover:
- Decision framework for when to create tasks - Decision framework for when to create tasks
- Search-first workflow to avoid duplicates - Search-first workflow to avoid duplicates
- Links to detailed guides for task creation, execution, and finalization - Links to detailed guides for task creation, execution, and finalization

View File

@@ -65,7 +65,6 @@ cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/
mkdir -p ~/.config/SubMiner && cp /tmp/config.example.jsonc ~/.config/SubMiner/config.jsonc mkdir -p ~/.config/SubMiner && cp /tmp/config.example.jsonc ~/.config/SubMiner/config.jsonc
``` ```
### 3. Set up Yomitan Dictionaries ### 3. Set up Yomitan Dictionaries
```bash ```bash

View File

@@ -5,7 +5,6 @@
* Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed. * Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
*/ */
{ {
// ========================================== // ==========================================
// Overlay Auto-Start // Overlay Auto-Start
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles. // When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
@@ -17,7 +16,7 @@
// Control whether browser opens automatically for texthooker. // Control whether browser opens automatically for texthooker.
// ========================================== // ==========================================
"texthooker": { "texthooker": {
"openBrowser": true // Open browser setting. Values: true | false "openBrowser": true, // Open browser setting. Values: true | false
}, // Control whether browser opens automatically for texthooker. }, // Control whether browser opens automatically for texthooker.
// ========================================== // ==========================================
@@ -27,7 +26,7 @@
// ========================================== // ==========================================
"websocket": { "websocket": {
"enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false "enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false
"port": 6677 // Built-in subtitle websocket server port. "port": 6677, // Built-in subtitle websocket server port.
}, // Built-in WebSocket server broadcasts subtitle text to connected clients. }, // Built-in WebSocket server broadcasts subtitle text to connected clients.
// ========================================== // ==========================================
@@ -36,7 +35,7 @@
// Set to debug for full runtime diagnostics. // Set to debug for full runtime diagnostics.
// ========================================== // ==========================================
"logging": { "logging": {
"level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error "level": "info", // Minimum log level for runtime logging. Values: debug | info | warn | error
}, // Controls logging verbosity. Keep this as an object; do not replace with a bare string. }, // Controls logging verbosity. Keep this as an object; do not replace with a bare string.
// ========================================== // ==========================================
@@ -57,7 +56,7 @@
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting. "toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting. "markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting. "openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
"openJimaku": "Ctrl+Shift+J" // Open jimaku setting. "openJimaku": "Ctrl+Shift+J", // Open jimaku setting.
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable. }, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
// ========================================== // ==========================================
@@ -77,7 +76,7 @@
"secondarySub": { "secondarySub": {
"secondarySubLanguages": [], // Secondary sub languages setting. "secondarySubLanguages": [], // Secondary sub languages setting.
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false "autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
"defaultMode": "hover" // Default mode setting. "defaultMode": "hover", // Default mode setting.
}, // Dual subtitle track options. }, // Dual subtitle track options.
// ========================================== // ==========================================
@@ -88,7 +87,7 @@
"defaultMode": "auto", // Subsync default mode. Values: auto | manual "defaultMode": "auto", // Subsync default mode. Values: auto | manual
"alass_path": "", // Alass path setting. "alass_path": "", // Alass path setting.
"ffsubsync_path": "", // Ffsubsync path setting. "ffsubsync_path": "", // Ffsubsync path setting.
"ffmpeg_path": "" // Ffmpeg path setting. "ffmpeg_path": "", // Ffmpeg path setting.
}, // Subsync engine and executable paths. }, // Subsync engine and executable paths.
// ========================================== // ==========================================
@@ -96,7 +95,7 @@
// Initial vertical subtitle position from the bottom. // Initial vertical subtitle position from the bottom.
// ========================================== // ==========================================
"subtitlePosition": { "subtitlePosition": {
"yPercent": 10 // Y percent setting. "yPercent": 10, // Y percent setting.
}, // Initial vertical subtitle position from the bottom. }, // Initial vertical subtitle position from the bottom.
// ========================================== // ==========================================
@@ -129,7 +128,7 @@
"N2": "#f5a97f", // N2 setting. "N2": "#f5a97f", // N2 setting.
"N3": "#f9e2af", // N3 setting. "N3": "#f9e2af", // N3 setting.
"N4": "#a6e3a1", // N4 setting. "N4": "#a6e3a1", // N4 setting.
"N5": "#8aadf4" // N5 setting. "N5": "#8aadf4", // N5 setting.
}, // Jlpt colors setting. }, // Jlpt colors setting.
"frequencyDictionary": { "frequencyDictionary": {
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false "enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
@@ -138,13 +137,7 @@
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded "mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
"matchMode": "headword", // Frequency lookup text selection mode. Values: headword | surface "matchMode": "headword", // Frequency lookup text selection mode. Values: headword | surface
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`. "singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
"bandedColors": [ "bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#a6e3a1", "#8aadf4"], // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
"#ed8796",
"#f5a97f",
"#f9e2af",
"#a6e3a1",
"#8aadf4"
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
}, // Frequency dictionary setting. }, // Frequency dictionary setting.
"secondary": { "secondary": {
"fontFamily": "Manrope, Inter", // Font family setting. "fontFamily": "Manrope, Inter", // Font family setting.
@@ -159,8 +152,8 @@
"backgroundColor": "transparent", // Background color setting. "backgroundColor": "transparent", // Background color setting.
"backdropFilter": "blur(6px)", // Backdrop filter setting. "backdropFilter": "blur(6px)", // Backdrop filter setting.
"fontWeight": "normal", // Font weight setting. "fontWeight": "normal", // Font weight setting.
"fontStyle": "normal" // Font style setting. "fontStyle": "normal", // Font style setting.
} // Secondary setting. }, // Secondary setting.
}, // Primary and secondary subtitle styling. }, // Primary and secondary subtitle styling.
// ========================================== // ==========================================
@@ -177,17 +170,15 @@
"enabled": false, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false "enabled": false, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
"host": "127.0.0.1", // Bind host for local AnkiConnect proxy. "host": "127.0.0.1", // Bind host for local AnkiConnect proxy.
"port": 8766, // Bind port for local AnkiConnect proxy. "port": 8766, // Bind port for local AnkiConnect proxy.
"upstreamUrl": "http://127.0.0.1:8765" // Upstream AnkiConnect URL proxied by local AnkiConnect proxy. "upstreamUrl": "http://127.0.0.1:8765", // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
}, // Proxy setting. }, // Proxy setting.
"tags": [ "tags": ["SubMiner"], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
"SubMiner"
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
"fields": { "fields": {
"audio": "ExpressionAudio", // Audio setting. "audio": "ExpressionAudio", // Audio setting.
"image": "Picture", // Image setting. "image": "Picture", // Image setting.
"sentence": "Sentence", // Sentence setting. "sentence": "Sentence", // Sentence setting.
"miscInfo": "MiscInfo", // Misc info setting. "miscInfo": "MiscInfo", // Misc info setting.
"translation": "SelectionText" // Translation setting. "translation": "SelectionText", // Translation setting.
}, // Fields setting. }, // Fields setting.
"ai": { "ai": {
"enabled": false, // Enabled setting. Values: true | false "enabled": false, // Enabled setting. Values: true | false
@@ -196,7 +187,7 @@
"model": "openai/gpt-4o-mini", // Model setting. "model": "openai/gpt-4o-mini", // Model setting.
"baseUrl": "https://openrouter.ai/api", // Base url setting. "baseUrl": "https://openrouter.ai/api", // Base url setting.
"targetLanguage": "English", // Target language setting. "targetLanguage": "English", // Target language setting.
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations." // System prompt setting. "systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting.
}, // Ai setting. }, // Ai setting.
"media": { "media": {
"generateAudio": true, // Generate audio setting. Values: true | false "generateAudio": true, // Generate audio setting. Values: true | false
@@ -209,7 +200,7 @@
"animatedCrf": 35, // Animated crf setting. "animatedCrf": 35, // Animated crf setting.
"audioPadding": 0.5, // Audio padding setting. "audioPadding": 0.5, // Audio padding setting.
"fallbackDuration": 3, // Fallback duration setting. "fallbackDuration": 3, // Fallback duration setting.
"maxMediaDuration": 30 // Max media duration setting. "maxMediaDuration": 30, // Max media duration setting.
}, // Media setting. }, // Media setting.
"behavior": { "behavior": {
"overwriteAudio": true, // Overwrite audio setting. Values: true | false "overwriteAudio": true, // Overwrite audio setting. Values: true | false
@@ -217,7 +208,7 @@
"mediaInsertMode": "append", // Media insert mode setting. "mediaInsertMode": "append", // Media insert mode setting.
"highlightWord": true, // Highlight word setting. Values: true | false "highlightWord": true, // Highlight word setting. Values: true | false
"notificationType": "osd", // Notification type setting. "notificationType": "osd", // Notification type setting.
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false "autoUpdateNewCards": true, // Automatically update newly added cards. Values: true | false
}, // Behavior setting. }, // Behavior setting.
"nPlusOne": { "nPlusOne": {
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false "highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
@@ -226,20 +217,20 @@
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names. "decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3). "minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight. "nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
"knownWord": "#a6da95" // Color used for legacy known-word highlights. "knownWord": "#a6da95", // Color used for legacy known-word highlights.
}, // N plus one setting. }, // N plus one setting.
"metadata": { "metadata": {
"pattern": "[SubMiner] %f (%t)" // Pattern setting. "pattern": "[SubMiner] %f (%t)", // Pattern setting.
}, // Metadata setting. }, // Metadata setting.
"isLapis": { "isLapis": {
"enabled": false, // Enabled setting. Values: true | false "enabled": false, // Enabled setting. Values: true | false
"sentenceCardModel": "Japanese sentences" // Sentence card model setting. "sentenceCardModel": "Japanese sentences", // Sentence card model setting.
}, // Is lapis setting. }, // Is lapis setting.
"isKiku": { "isKiku": {
"enabled": false, // Enabled setting. Values: true | false "enabled": false, // Enabled setting. Values: true | false
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled "fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
"deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false "deleteDuplicateInAuto": true, // Delete duplicate in auto setting. Values: true | false
} // Is kiku setting. }, // Is kiku setting.
}, // Automatic Anki updates and media generation options. }, // Automatic Anki updates and media generation options.
// ========================================== // ==========================================
@@ -249,7 +240,7 @@
"jimaku": { "jimaku": {
"apiBaseUrl": "https://jimaku.cc", // Api base url setting. "apiBaseUrl": "https://jimaku.cc", // Api base url setting.
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none "languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
"maxEntryResults": 10 // Maximum Jimaku search results returned. "maxEntryResults": 10, // Maximum Jimaku search results returned.
}, // Jimaku API configuration and defaults. }, // Jimaku API configuration and defaults.
// ========================================== // ==========================================
@@ -260,10 +251,7 @@
"mode": "automatic", // YouTube subtitle generation mode for the launcher script. Values: automatic | preprocess | off "mode": "automatic", // YouTube subtitle generation mode for the launcher script. Values: automatic | preprocess | off
"whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine. "whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine.
"whisperModel": "", // Path to whisper model used for fallback transcription. "whisperModel": "", // Path to whisper model used for fallback transcription.
"primarySubLanguages": [ "primarySubLanguages": ["ja", "jpn"], // Comma-separated primary subtitle language priority used by the launcher.
"ja",
"jpn"
] // Comma-separated primary subtitle language priority used by the launcher.
}, // Defaults for subminer YouTube subtitle extraction/transcription mode. }, // Defaults for subminer YouTube subtitle extraction/transcription mode.
// ========================================== // ==========================================
@@ -272,7 +260,7 @@
// ========================================== // ==========================================
"anilist": { "anilist": {
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false "enabled": false, // Enable AniList post-watch progress updates. Values: true | false
"accessToken": "" // Optional explicit AniList access token override; leave empty to use locally stored token from setup. "accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
}, // Anilist API credentials and update behavior. }, // Anilist API credentials and update behavior.
// ========================================== // ==========================================
@@ -296,16 +284,8 @@
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false "pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons. "iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false "directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
"directPlayContainers": [ "directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"], // Container allowlist for direct play decisions.
"mkv", "transcodeVideoCodec": "h264", // Preferred transcode video codec when direct play is unavailable.
"mp4",
"webm",
"mov",
"flac",
"mp3",
"aac"
], // Container allowlist for direct play decisions.
"transcodeVideoCodec": "h264" // Preferred transcode video codec when direct play is unavailable.
}, // Optional Jellyfin integration for auth, browsing, and playback launch. }, // Optional Jellyfin integration for auth, browsing, and playback launch.
// ========================================== // ==========================================
@@ -316,7 +296,7 @@
"discordPresence": { "discordPresence": {
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false "enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
"updateIntervalMs": 3000, // Minimum interval between presence payload updates. "updateIntervalMs": 3000, // Minimum interval between presence payload updates.
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates. "debounceMs": 750, // Debounce delay used to collapse bursty presence updates.
}, // Optional Discord Rich Presence activity card updates for current playback/study session. }, // Optional Discord Rich Presence activity card updates for current playback/study session.
// ========================================== // ==========================================
@@ -338,7 +318,7 @@
"telemetryDays": 30, // Telemetry retention window in days. "telemetryDays": 30, // Telemetry retention window in days.
"dailyRollupsDays": 365, // Daily rollup retention window in days. "dailyRollupsDays": 365, // Daily rollup retention window in days.
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days. "monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
"vacuumIntervalDays": 7 // Minimum days between VACUUM runs. "vacuumIntervalDays": 7, // Minimum days between VACUUM runs.
} // Retention setting. }, // Retention setting.
} // Enable/disable immersion tracking. }, // Enable/disable immersion tracking.
} }

View File

@@ -62,7 +62,7 @@ test('inferAniSkipMetadataForFile falls back to anime directory title when filen
test('buildSubminerScriptOpts includes aniskip metadata fields', () => { test('buildSubminerScriptOpts includes aniskip metadata fields', () => {
const opts = buildSubminerScriptOpts('/tmp/SubMiner.AppImage', '/tmp/subminer.sock', { const opts = buildSubminerScriptOpts('/tmp/SubMiner.AppImage', '/tmp/subminer.sock', {
title: 'Frieren: Beyond Journey\'s End', title: "Frieren: Beyond Journey's End",
season: 1, season: 1,
episode: 5, episode: 5,
source: 'guessit', source: 'guessit',

View File

@@ -28,7 +28,11 @@ function toPositiveInt(value: unknown): number | null {
} }
function detectEpisodeFromName(baseName: string): number | null { function detectEpisodeFromName(baseName: string): number | null {
const patterns = [/[Ss]\d+[Ee](\d{1,3})/, /(?:^|[\s._-])[Ee][Pp]?[\s._-]*(\d{1,3})(?:$|[\s._-])/, /[-\s](\d{1,3})$/]; const patterns = [
/[Ss]\d+[Ee](\d{1,3})/,
/(?:^|[\s._-])[Ee][Pp]?[\s._-]*(\d{1,3})(?:$|[\s._-])/,
/[-\s](\d{1,3})$/,
];
for (const pattern of patterns) { for (const pattern of patterns) {
const match = baseName.match(pattern); const match = baseName.match(pattern);
if (!match || !match[1]) continue; if (!match || !match[1]) continue;
@@ -171,7 +175,11 @@ export function inferAniSkipMetadataForFile(
} }
function sanitizeScriptOptValue(value: string): string { function sanitizeScriptOptValue(value: string): string {
return value.replace(/,/g, ' ').replace(/[\r\n]/g, ' ').replace(/\s+/g, ' ').trim(); return value
.replace(/,/g, ' ')
.replace(/[\r\n]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
} }
export function buildSubminerScriptOpts( export function buildSubminerScriptOpts(

View File

@@ -4,11 +4,9 @@ import { execFileSync } from 'node:child_process';
import path from 'node:path'; import path from 'node:path';
test('launcher root help lists subcommands', () => { test('launcher root help lists subcommands', () => {
const output = execFileSync( const output = execFileSync('bun', ['run', path.join(process.cwd(), 'launcher/main.ts'), '-h'], {
'bun', encoding: 'utf8',
['run', path.join(process.cwd(), 'launcher/main.ts'), '-h'], });
{ encoding: 'utf8' },
);
assert.match(output, /Commands:/); assert.match(output, /Commands:/);
assert.match(output, /jellyfin\|jf/); assert.match(output, /jellyfin\|jf/);

View File

@@ -182,7 +182,8 @@ export function parseCliPrograms(
server: typeof options.server === 'string' ? options.server : undefined, server: typeof options.server === 'string' ? options.server : undefined,
username: typeof options.username === 'string' ? options.username : undefined, username: typeof options.username === 'string' ? options.username : undefined,
password: typeof options.password === 'string' ? options.password : undefined, password: typeof options.password === 'string' ? options.password : undefined,
passwordStore: typeof options.passwordStore === 'string' ? options.passwordStore : undefined, passwordStore:
typeof options.passwordStore === 'string' ? options.passwordStore : undefined,
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined, logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
}; };
}); });

View File

@@ -22,10 +22,14 @@ function withTempDir<T>(fn: (dir: string) => T): T {
} }
function runLauncher(argv: string[], env: NodeJS.ProcessEnv): RunResult { function runLauncher(argv: string[], env: NodeJS.ProcessEnv): RunResult {
const result = spawnSync(process.execPath, ['run', path.join(process.cwd(), 'launcher/main.ts'), ...argv], { const result = spawnSync(
process.execPath,
['run', path.join(process.cwd(), 'launcher/main.ts'), ...argv],
{
env, env,
encoding: 'utf8', encoding: 'utf8',
}); },
);
return { return {
status: result.status, status: result.status,
stdout: result.stdout || '', stdout: result.stdout || '',
@@ -225,10 +229,7 @@ test('jellyfin setup forwards password-store to app command', () => {
SUBMINER_APPIMAGE_PATH: appPath, SUBMINER_APPIMAGE_PATH: appPath,
SUBMINER_TEST_CAPTURE: capturePath, SUBMINER_TEST_CAPTURE: capturePath,
}; };
const result = runLauncher( const result = runLauncher(['jf', 'setup', '--password-store', 'gnome-libsecret'], env);
['jf', 'setup', '--password-store', 'gnome-libsecret'],
env,
);
assert.equal(result.status, 0); assert.equal(result.status, 0);
assert.equal( assert.equal(

View File

@@ -475,8 +475,7 @@ export function startMpv(
if (preloadedSubtitles?.secondaryPath) { if (preloadedSubtitles?.secondaryPath) {
mpvArgs.push(`--sub-file=${preloadedSubtitles.secondaryPath}`); mpvArgs.push(`--sub-file=${preloadedSubtitles.secondaryPath}`);
} }
const aniSkipMetadata = const aniSkipMetadata = targetKind === 'file' ? inferAniSkipMetadataForFile(target) : null;
targetKind === 'file' ? inferAniSkipMetadataForFile(target) : null;
const scriptOpts = buildSubminerScriptOpts(appPath, socketPath, aniSkipMetadata); const scriptOpts = buildSubminerScriptOpts(appPath, socketPath, aniSkipMetadata);
if (aniSkipMetadata) { if (aniSkipMetadata) {
log( log(

View File

@@ -31,11 +31,7 @@ test('parseArgs maps jellyfin play action and log-level override', () => {
}); });
test('parseArgs forwards jellyfin password-store option', () => { test('parseArgs forwards jellyfin password-store option', () => {
const parsed = parseArgs( const parsed = parseArgs(['jf', 'setup', '--password-store', 'gnome-libsecret'], 'subminer', {});
['jf', 'setup', '--password-store', 'gnome-libsecret'],
'subminer',
{},
);
assert.equal(parsed.jellyfin, true); assert.equal(parsed.jellyfin, true);
assert.equal(parsed.passwordStore, 'gnome-libsecret'); assert.equal(parsed.passwordStore, 'gnome-libsecret');

View File

@@ -239,7 +239,8 @@ export class AnkiIntegration {
} }
private createProxyServer(): AnkiConnectProxyServer { private createProxyServer(): AnkiConnectProxyServer {
const { AnkiConnectProxyServer } = require('./anki-integration/anki-connect-proxy') as typeof import('./anki-integration/anki-connect-proxy'); const { AnkiConnectProxyServer } =
require('./anki-integration/anki-connect-proxy') as typeof import('./anki-integration/anki-connect-proxy');
return new AnkiConnectProxyServer({ return new AnkiConnectProxyServer({
shouldAutoUpdateNewCards: () => this.config.behavior?.autoUpdateNewCards !== false, shouldAutoUpdateNewCards: () => this.config.behavior?.autoUpdateNewCards !== false,
processNewCard: (noteId: number) => this.processNewCard(noteId), processNewCard: (noteId: number) => this.processNewCard(noteId),

View File

@@ -27,9 +27,11 @@ test('proxy enqueues addNote result for enrichment', async () => {
logError: () => undefined, logError: () => undefined,
}); });
(proxy as unknown as { (
proxy as unknown as {
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void; maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
}).maybeEnqueueFromRequest( }
).maybeEnqueueFromRequest(
{ action: 'addNote' }, { action: 'addNote' },
Buffer.from(JSON.stringify({ result: 42, error: null }), 'utf8'), Buffer.from(JSON.stringify({ result: 42, error: null }), 'utf8'),
); );
@@ -50,9 +52,11 @@ test('proxy enqueues addNote bare numeric response for enrichment', async () =>
logError: () => undefined, logError: () => undefined,
}); });
(proxy as unknown as { (
proxy as unknown as {
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void; maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
}).maybeEnqueueFromRequest({ action: 'addNote' }, Buffer.from('42', 'utf8')); }
).maybeEnqueueFromRequest({ action: 'addNote' }, Buffer.from('42', 'utf8'));
await waitForCondition(() => processed.length === 1); await waitForCondition(() => processed.length === 1);
assert.deepEqual(processed, [42]); assert.deepEqual(processed, [42]);
@@ -71,9 +75,11 @@ test('proxy de-duplicates addNotes IDs within the same response', async () => {
logError: () => undefined, logError: () => undefined,
}); });
(proxy as unknown as { (
proxy as unknown as {
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void; maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
}).maybeEnqueueFromRequest( }
).maybeEnqueueFromRequest(
{ action: 'addNotes' }, { action: 'addNotes' },
Buffer.from(JSON.stringify({ result: [101, 102, 101, null], error: null }), 'utf8'), Buffer.from(JSON.stringify({ result: [101, 102, 101, null], error: null }), 'utf8'),
); );
@@ -94,17 +100,15 @@ test('proxy enqueues note IDs from multi action addNote/addNotes results', async
logError: () => undefined, logError: () => undefined,
}); });
(proxy as unknown as { (
proxy as unknown as {
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void; maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
}).maybeEnqueueFromRequest( }
).maybeEnqueueFromRequest(
{ {
action: 'multi', action: 'multi',
params: { params: {
actions: [ actions: [{ action: 'version' }, { action: 'addNote' }, { action: 'addNotes' }],
{ action: 'version' },
{ action: 'addNote' },
{ action: 'addNotes' },
],
}, },
}, },
Buffer.from(JSON.stringify({ result: [6, 777, [888, 777, null]], error: null }), 'utf8'), Buffer.from(JSON.stringify({ result: [6, 777, [888, 777, null]], error: null }), 'utf8'),
@@ -126,9 +130,11 @@ test('proxy enqueues note IDs from bare multi action results', async () => {
logError: () => undefined, logError: () => undefined,
}); });
(proxy as unknown as { (
proxy as unknown as {
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void; maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
}).maybeEnqueueFromRequest( }
).maybeEnqueueFromRequest(
{ {
action: 'multi', action: 'multi',
params: { params: {
@@ -154,17 +160,15 @@ test('proxy enqueues note IDs from multi action envelope results', async () => {
logError: () => undefined, logError: () => undefined,
}); });
(proxy as unknown as { (
proxy as unknown as {
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void; maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
}).maybeEnqueueFromRequest( }
).maybeEnqueueFromRequest(
{ {
action: 'multi', action: 'multi',
params: { params: {
actions: [ actions: [{ action: 'version' }, { action: 'addNote' }, { action: 'addNotes' }],
{ action: 'version' },
{ action: 'addNote' },
{ action: 'addNotes' },
],
}, },
}, },
Buffer.from( Buffer.from(
@@ -196,9 +200,11 @@ test('proxy skips auto-enrichment when auto-update is disabled', async () => {
logError: () => undefined, logError: () => undefined,
}); });
(proxy as unknown as { (
proxy as unknown as {
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void; maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
}).maybeEnqueueFromRequest( }
).maybeEnqueueFromRequest(
{ action: 'addNote' }, { action: 'addNote' },
Buffer.from(JSON.stringify({ result: 303, error: null }), 'utf8'), Buffer.from(JSON.stringify({ result: 303, error: null }), 'utf8'),
); );
@@ -219,9 +225,11 @@ test('proxy ignores addNote when upstream response reports error', async () => {
logError: () => undefined, logError: () => undefined,
}); });
(proxy as unknown as { (
proxy as unknown as {
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void; maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
}).maybeEnqueueFromRequest( }
).maybeEnqueueFromRequest(
{ action: 'addNote' }, { action: 'addNote' },
Buffer.from(JSON.stringify({ result: 123, error: 'duplicate' }), 'utf8'), Buffer.from(JSON.stringify({ result: 123, error: 'duplicate' }), 'utf8'),
); );
@@ -248,9 +256,11 @@ test('proxy does not fallback-enqueue latest note for multi requests without add
logError: () => undefined, logError: () => undefined,
}); });
(proxy as unknown as { (
proxy as unknown as {
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void; maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
}).maybeEnqueueFromRequest( }
).maybeEnqueueFromRequest(
{ {
action: 'multi', action: 'multi',
params: { params: {
@@ -283,9 +293,11 @@ test('proxy fallback-enqueues latest note for addNote responses without note IDs
logError: () => undefined, logError: () => undefined,
}); });
(proxy as unknown as { (
proxy as unknown as {
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void; maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
}).maybeEnqueueFromRequest( }
).maybeEnqueueFromRequest(
{ action: 'addNote' }, { action: 'addNote' },
Buffer.from(JSON.stringify({ result: 0, error: null }), 'utf8'), Buffer.from(JSON.stringify({ result: 0, error: null }), 'utf8'),
); );
@@ -304,13 +316,15 @@ test('proxy detects self-referential loop configuration', () => {
logError: () => undefined, logError: () => undefined,
}); });
const result = (proxy as unknown as { const result = (
proxy as unknown as {
isSelfReferentialProxy: (options: { isSelfReferentialProxy: (options: {
host: string; host: string;
port: number; port: number;
upstreamUrl: string; upstreamUrl: string;
}) => boolean; }) => boolean;
}).isSelfReferentialProxy({ }
).isSelfReferentialProxy({
host: '127.0.0.1', host: '127.0.0.1',
port: 8766, port: 8766,
upstreamUrl: 'http://localhost:8766', upstreamUrl: 'http://localhost:8766',

View File

@@ -175,7 +175,9 @@ export class AnkiConnectProxyServer {
} }
const action = const action =
typeof requestJson.action === 'string' ? requestJson.action : String(requestJson.action ?? ''); typeof requestJson.action === 'string'
? requestJson.action
: String(requestJson.action ?? '');
if (action !== 'addNote' && action !== 'addNotes' && action !== 'multi') { if (action !== 'addNote' && action !== 'addNotes' && action !== 'multi') {
return; return;
} }

View File

@@ -89,7 +89,11 @@ test('findDuplicateNote checks both source expression/word values when both fiel
if (query.includes('昨日は雨だった。')) { if (query.includes('昨日は雨だった。')) {
return []; return [];
} }
if (query.includes('"Word:雨"') || query.includes('"word:雨"') || query.includes('"Expression:雨"')) { if (
query.includes('"Word:雨"') ||
query.includes('"word:雨"') ||
query.includes('"Expression:雨"')
) {
return [200]; return [200];
} }
return []; return [];

View File

@@ -32,9 +32,7 @@ export async function findDuplicateNote(
); );
const deckValue = deps.getDeck(); const deckValue = deps.getDeck();
const queryPrefixes = deckValue const queryPrefixes = deckValue ? [`"deck:${escapeAnkiSearchValue(deckValue)}" `, ''] : [''];
? [`"deck:${escapeAnkiSearchValue(deckValue)}" `, '']
: [''];
try { try {
const noteIds = new Set<number>(); const noteIds = new Set<number>();

View File

@@ -112,12 +112,7 @@ export class FieldGroupingWorkflow {
const keepNoteId = choice.keepNoteId; const keepNoteId = choice.keepNoteId;
const deleteNoteId = choice.deleteNoteId; const deleteNoteId = choice.deleteNoteId;
await this.performMerge( await this.performMerge(keepNoteId, deleteNoteId, expression, choice.deleteDuplicate);
keepNoteId,
deleteNoteId,
expression,
choice.deleteDuplicate,
);
return true; return true;
} catch (error) { } catch (error) {
this.deps.logError('Field grouping manual merge failed:', (error as Error).message); this.deps.logError('Field grouping manual merge failed:', (error as Error).message);

View File

@@ -51,18 +51,10 @@ function createWorkflowHarness() {
return out; return out;
}, },
findDuplicateNote: async (_expression, _excludeNoteId, _noteInfo) => null, findDuplicateNote: async (_expression, _excludeNoteId, _noteInfo) => null,
handleFieldGroupingAuto: async ( handleFieldGroupingAuto: async (_originalNoteId, _newNoteId, _newNoteInfo, _expression) =>
_originalNoteId, undefined,
_newNoteId, handleFieldGroupingManual: async (_originalNoteId, _newNoteId, _newNoteInfo, _expression) =>
_newNoteInfo, false,
_expression,
) => undefined,
handleFieldGroupingManual: async (
_originalNoteId,
_newNoteId,
_newNoteInfo,
_expression,
) => false,
processSentence: (text: string, _noteFields: Record<string, string>) => text, processSentence: (text: string, _noteFields: Record<string, string>) => text,
resolveConfiguredFieldName: (noteInfo: NoteUpdateWorkflowNoteInfo, preferred?: string) => { resolveConfiguredFieldName: (noteInfo: NoteUpdateWorkflowNoteInfo, preferred?: string) => {
if (!preferred) return null; if (!preferred) return null;

View File

@@ -748,6 +748,9 @@ test('runtime options registry is centralized', () => {
const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id); const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id);
assert.deepEqual(ids, [ assert.deepEqual(ids, [
'anki.autoUpdateNewCards', 'anki.autoUpdateNewCards',
'subtitle.annotation.nPlusOne',
'subtitle.annotation.jlpt',
'subtitle.annotation.frequency',
'anki.nPlusOneMatchMode', 'anki.nPlusOneMatchMode',
'anki.kikuFieldGrouping', 'anki.kikuFieldGrouping',
]); ]);

View File

@@ -62,7 +62,9 @@ test('domain registry builders each contribute entries to composed registry', ()
}); });
test('default keybindings include primary and secondary subtitle track cycling on J keys', () => { test('default keybindings include primary and secondary subtitle track cycling on J keys', () => {
const keybindingMap = new Map(DEFAULT_KEYBINDINGS.map((binding) => [binding.key, binding.command])); const keybindingMap = new Map(
DEFAULT_KEYBINDINGS.map((binding) => [binding.key, binding.command]),
);
assert.deepEqual(keybindingMap.get('KeyJ'), ['cycle', 'sid']); assert.deepEqual(keybindingMap.get('KeyJ'), ['cycle', 'sid']);
assert.deepEqual(keybindingMap.get('Shift+KeyJ'), ['cycle', 'secondary-sid']); assert.deepEqual(keybindingMap.get('Shift+KeyJ'), ['cycle', 'secondary-sid']);
}); });

View File

@@ -5,6 +5,8 @@ export function buildIntegrationConfigOptionRegistry(
defaultConfig: ResolvedConfig, defaultConfig: ResolvedConfig,
runtimeOptionRegistry: RuntimeOptionRegistryEntry[], runtimeOptionRegistry: RuntimeOptionRegistryEntry[],
): ConfigOptionRegistryEntry[] { ): ConfigOptionRegistryEntry[] {
const runtimeOptionById = new Map(runtimeOptionRegistry.map((entry) => [entry.id, entry]));
return [ return [
{ {
path: 'ankiConnect.enabled', path: 'ankiConnect.enabled',
@@ -54,7 +56,7 @@ export function buildIntegrationConfigOptionRegistry(
kind: 'boolean', kind: 'boolean',
defaultValue: defaultConfig.ankiConnect.behavior.autoUpdateNewCards, defaultValue: defaultConfig.ankiConnect.behavior.autoUpdateNewCards,
description: 'Automatically update newly added cards.', description: 'Automatically update newly added cards.',
runtime: runtimeOptionRegistry[0], runtime: runtimeOptionById.get('anki.autoUpdateNewCards'),
}, },
{ {
path: 'ankiConnect.nPlusOne.matchMode', path: 'ankiConnect.nPlusOne.matchMode',
@@ -105,7 +107,7 @@ export function buildIntegrationConfigOptionRegistry(
enumValues: ['auto', 'manual', 'disabled'], enumValues: ['auto', 'manual', 'disabled'],
defaultValue: defaultConfig.ankiConnect.isKiku.fieldGrouping, defaultValue: defaultConfig.ankiConnect.isKiku.fieldGrouping,
description: 'Kiku duplicate-card field grouping mode.', description: 'Kiku duplicate-card field grouping mode.',
runtime: runtimeOptionRegistry[1], runtime: runtimeOptionById.get('anki.kikuFieldGrouping'),
}, },
{ {
path: 'jimaku.languagePreference', path: 'jimaku.languagePreference',

View File

@@ -19,6 +19,42 @@ export function buildRuntimeOptionRegistry(
behavior: { autoUpdateNewCards: value === true }, behavior: { autoUpdateNewCards: value === true },
}), }),
}, },
{
id: 'subtitle.annotation.nPlusOne',
path: 'ankiConnect.nPlusOne.highlightEnabled',
label: 'N+1 Annotation',
scope: 'subtitle',
valueType: 'boolean',
allowedValues: [true, false],
defaultValue: defaultConfig.ankiConnect.nPlusOne.highlightEnabled,
requiresRestart: false,
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
toAnkiPatch: () => ({}),
},
{
id: 'subtitle.annotation.jlpt',
path: 'subtitleStyle.enableJlpt',
label: 'JLPT Annotation',
scope: 'subtitle',
valueType: 'boolean',
allowedValues: [true, false],
defaultValue: defaultConfig.subtitleStyle.enableJlpt,
requiresRestart: false,
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
toAnkiPatch: () => ({}),
},
{
id: 'subtitle.annotation.frequency',
path: 'subtitleStyle.frequencyDictionary.enabled',
label: 'Frequency Annotation',
scope: 'subtitle',
valueType: 'boolean',
allowedValues: [true, false],
defaultValue: defaultConfig.subtitleStyle.frequencyDictionary.enabled,
requiresRestart: false,
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
toAnkiPatch: () => ({}),
},
{ {
id: 'anki.nPlusOneMatchMode', id: 'anki.nPlusOneMatchMode',
path: 'ankiConnect.nPlusOne.matchMode', path: 'ankiConnect.nPlusOne.matchMode',

View File

@@ -111,7 +111,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
frequencyDictionary: { frequencyDictionary: {
...resolved.subtitleStyle.frequencyDictionary, ...resolved.subtitleStyle.frequencyDictionary,
...(isObject((src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary) ...(isObject((src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary)
? ((src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary as ResolvedConfig['subtitleStyle']['frequencyDictionary']) ? ((src.subtitleStyle as { frequencyDictionary?: unknown })
.frequencyDictionary as ResolvedConfig['subtitleStyle']['frequencyDictionary'])
: {}), : {}),
}, },
secondary: { secondary: {
@@ -152,7 +153,9 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
); );
} }
const hoverTokenColor = asColor((src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor); const hoverTokenColor = asColor(
(src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor,
);
if (hoverTokenColor !== undefined) { if (hoverTokenColor !== undefined) {
resolved.subtitleStyle.hoverTokenColor = hoverTokenColor; resolved.subtitleStyle.hoverTokenColor = hoverTokenColor;
} else if ((src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor !== undefined) { } else if ((src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor !== undefined) {
@@ -174,7 +177,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
(src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor !== (src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor !==
undefined undefined
) { ) {
resolved.subtitleStyle.hoverTokenBackgroundColor = fallbackSubtitleStyleHoverTokenBackgroundColor; resolved.subtitleStyle.hoverTokenBackgroundColor =
fallbackSubtitleStyleHoverTokenBackgroundColor;
warn( warn(
'subtitleStyle.hoverTokenBackgroundColor', 'subtitleStyle.hoverTokenBackgroundColor',
(src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor, (src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor,
@@ -208,7 +212,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
if (sourcePath !== undefined) { if (sourcePath !== undefined) {
resolved.subtitleStyle.frequencyDictionary.sourcePath = sourcePath; resolved.subtitleStyle.frequencyDictionary.sourcePath = sourcePath;
} else if ((frequencyDictionary as { sourcePath?: unknown }).sourcePath !== undefined) { } else if ((frequencyDictionary as { sourcePath?: unknown }).sourcePath !== undefined) {
resolved.subtitleStyle.frequencyDictionary.sourcePath = fallbackFrequencyDictionary.sourcePath; resolved.subtitleStyle.frequencyDictionary.sourcePath =
fallbackFrequencyDictionary.sourcePath;
warn( warn(
'subtitleStyle.frequencyDictionary.sourcePath', 'subtitleStyle.frequencyDictionary.sourcePath',
(frequencyDictionary as { sourcePath?: unknown }).sourcePath, (frequencyDictionary as { sourcePath?: unknown }).sourcePath,
@@ -260,7 +265,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
if (singleColor !== undefined) { if (singleColor !== undefined) {
resolved.subtitleStyle.frequencyDictionary.singleColor = singleColor; resolved.subtitleStyle.frequencyDictionary.singleColor = singleColor;
} else if ((frequencyDictionary as { singleColor?: unknown }).singleColor !== undefined) { } else if ((frequencyDictionary as { singleColor?: unknown }).singleColor !== undefined) {
resolved.subtitleStyle.frequencyDictionary.singleColor = fallbackFrequencyDictionary.singleColor; resolved.subtitleStyle.frequencyDictionary.singleColor =
fallbackFrequencyDictionary.singleColor;
warn( warn(
'subtitleStyle.frequencyDictionary.singleColor', 'subtitleStyle.frequencyDictionary.singleColor',
(frequencyDictionary as { singleColor?: unknown }).singleColor, (frequencyDictionary as { singleColor?: unknown }).singleColor,

View File

@@ -25,7 +25,8 @@ function humanizeKey(key: string): string {
function buildInlineOptionComment(path: string, value: unknown): string { function buildInlineOptionComment(path: string, value: unknown): string {
const registryEntry = OPTION_REGISTRY_BY_PATH.get(path); const registryEntry = OPTION_REGISTRY_BY_PATH.get(path);
const baseDescription = registryEntry?.description ?? TOP_LEVEL_SECTION_DESCRIPTION_BY_KEY.get(path); const baseDescription =
registryEntry?.description ?? TOP_LEVEL_SECTION_DESCRIPTION_BY_KEY.get(path);
const description = const description =
baseDescription && baseDescription.trim().length > 0 baseDescription && baseDescription.trim().length > 0
? normalizeCommentText(baseDescription) ? normalizeCommentText(baseDescription)

View File

@@ -132,16 +132,15 @@ export function createAnilistTokenStore(
} }
return decrypted; return decrypted;
} }
if ( if (typeof parsed.plaintextToken === 'string' && parsed.plaintextToken.trim().length > 0) {
typeof parsed.plaintextToken === 'string' &&
parsed.plaintextToken.trim().length > 0
) {
if (storage.isEncryptionAvailable()) { if (storage.isEncryptionAvailable()) {
if (!isSafeStorageUsable()) { if (!isSafeStorageUsable()) {
return null; return null;
} }
const plaintext = parsed.plaintextToken.trim(); const plaintext = parsed.plaintextToken.trim();
notifyUser('AniList token plaintext fallback payload found. Migrating to encrypted storage.'); notifyUser(
'AniList token plaintext fallback payload found. Migrating to encrypted storage.',
);
this.saveToken(plaintext); this.saveToken(plaintext);
return plaintext; return plaintext;
} }

View File

@@ -283,8 +283,7 @@ export function handleCliCommand(
return; return;
} }
const shouldStart = const shouldStart = args.start || args.toggle || args.toggleVisibleOverlay;
args.start || args.toggle || args.toggleVisibleOverlay;
const needsOverlayRuntime = commandNeedsOverlayRuntime(args); const needsOverlayRuntime = commandNeedsOverlayRuntime(args);
const shouldInitializeOverlayRuntime = needsOverlayRuntime || args.start; const shouldInitializeOverlayRuntime = needsOverlayRuntime || args.start;

View File

@@ -71,7 +71,8 @@ test('createFrequencyDictionaryLookup aggregates duplicate-term logs into a sing
assert.equal(lookup('猫'), 100); assert.equal(lookup('猫'), 100);
assert.equal( assert.equal(
logs.filter((entry) => entry.includes('Frequency dictionary ignored 2 duplicate term entries')).length, logs.filter((entry) => entry.includes('Frequency dictionary ignored 2 duplicate term entries'))
.length,
1, 1,
); );
assert.equal( assert.equal(

View File

@@ -32,7 +32,7 @@ function parsePositiveFrequencyString(value: string): number | null {
const chunks = numericPrefix.split(','); const chunks = numericPrefix.split(',');
const normalizedNumber = const normalizedNumber =
chunks.length <= 1 chunks.length <= 1
? chunks[0] ?? '' ? (chunks[0] ?? '')
: chunks.slice(1).every((chunk) => /^\d{3}$/.test(chunk)) : chunks.slice(1).every((chunk) => /^\d{3}$/.test(chunk))
? chunks.join('') ? chunks.join('')
: (chunks[0] ?? ''); : (chunks[0] ?? '');

View File

@@ -62,10 +62,7 @@ export {
updateOverlayWindowBounds, updateOverlayWindowBounds,
} from './overlay-window'; } from './overlay-window';
export { initializeOverlayRuntime } from './overlay-runtime-init'; export { initializeOverlayRuntime } from './overlay-runtime-init';
export { export { setVisibleOverlayVisible, updateVisibleOverlayVisibility } from './overlay-visibility';
setVisibleOverlayVisible,
updateVisibleOverlayVisibility,
} from './overlay-visibility';
export { export {
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY, MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
MpvIpcClient, MpvIpcClient,

View File

@@ -150,7 +150,12 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () =
}); });
const validResult = await setHandler!({}, 'anki.autoUpdateNewCards', true); const validResult = await setHandler!({}, 'anki.autoUpdateNewCards', true);
assert.deepEqual(validResult, { ok: true }); assert.deepEqual(validResult, { ok: true });
assert.deepEqual(calls, [{ id: 'anki.autoUpdateNewCards', value: true }]); const validSubtitleAnnotationResult = await setHandler!({}, 'subtitle.annotation.jlpt', false);
assert.deepEqual(validSubtitleAnnotationResult, { ok: true });
assert.deepEqual(calls, [
{ id: 'anki.autoUpdateNewCards', value: true },
{ id: 'subtitle.annotation.jlpt', value: false },
]);
const cycleHandler = handlers.handle.get(IPC_CHANNELS.request.cycleRuntimeOption); const cycleHandler = handlers.handle.get(IPC_CHANNELS.request.cycleRuntimeOption);
assert.ok(cycleHandler); assert.ok(cycleHandler);
@@ -215,9 +220,7 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 'bad' }); handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 'bad' });
handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 42 }); handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 42 });
assert.deepEqual(saves, [ assert.deepEqual(saves, [{ yPercent: 42 }]);
{ yPercent: 42 },
]);
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'not-a-modal'); handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'not-a-modal');
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'subsync'); handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'subsync');

View File

@@ -62,7 +62,8 @@ export function createJellyfinTokenStore(
} }
const decrypted = safeStorage.decryptString(encrypted).trim(); const decrypted = safeStorage.decryptString(encrypted).trim();
const session = JSON.parse(decrypted) as Partial<JellyfinStoredSession>; const session = JSON.parse(decrypted) as Partial<JellyfinStoredSession>;
const accessToken = typeof session.accessToken === 'string' ? session.accessToken.trim() : ''; const accessToken =
typeof session.accessToken === 'string' ? session.accessToken.trim() : '';
const userId = typeof session.userId === 'string' ? session.userId.trim() : ''; const userId = typeof session.userId === 'string' ? session.userId.trim() : '';
if (!accessToken || !userId) return null; if (!accessToken || !userId) return null;
return { accessToken, userId }; return { accessToken, userId };
@@ -88,7 +89,9 @@ export function createJellyfinTokenStore(
(typeof parsed.encryptedToken === 'string' && parsed.encryptedToken.length > 0) || (typeof parsed.encryptedToken === 'string' && parsed.encryptedToken.length > 0) ||
(typeof parsed.plaintextToken === 'string' && parsed.plaintextToken.trim().length > 0) (typeof parsed.plaintextToken === 'string' && parsed.plaintextToken.trim().length > 0)
) { ) {
logger.warn('Ignoring legacy Jellyfin token-only store payload because userId is missing.'); logger.warn(
'Ignoring legacy Jellyfin token-only store payload because userId is missing.',
);
} }
} catch (error) { } catch (error) {
logger.error('Failed to read Jellyfin session store.', error); logger.error('Failed to read Jellyfin session store.', error);

View File

@@ -34,10 +34,7 @@ export function sendToVisibleOverlayRuntime<T extends string>(options: {
const isLoading = typeof webContents.isLoading === 'function' ? webContents.isLoading() : false; const isLoading = typeof webContents.isLoading === 'function' ? webContents.isLoading() : false;
const currentURL = typeof webContents.getURL === 'function' ? webContents.getURL() : ''; const currentURL = typeof webContents.getURL === 'function' ? webContents.getURL() : '';
const isReady = const isReady = !isLoading && currentURL !== '' && currentURL !== 'about:blank';
!isLoading &&
currentURL !== '' &&
currentURL !== 'about:blank';
if (!isReady) { if (!isReady) {
if (typeof webContents.once !== 'function') { if (typeof webContents.once !== 'function') {

View File

@@ -85,7 +85,9 @@ export function parseClipboardVideoPath(text: string): string | null {
return isSupportedVideoPath(unquoted) ? unquoted : null; return isSupportedVideoPath(unquoted) ? unquoted : null;
} }
export function collectDroppedVideoPaths(dataTransfer: DropDataTransferLike | null | undefined): string[] { export function collectDroppedVideoPaths(
dataTransfer: DropDataTransferLike | null | undefined,
): string[] {
if (!dataTransfer) return []; if (!dataTransfer) return [];
const out: string[] = []; const out: string[] = [];

View File

@@ -117,13 +117,9 @@ test('runtime-option broadcast still uses expected channel', () => {
}, },
); );
let state = false; let state = false;
const changed = setOverlayDebugVisualizationEnabledRuntime( const changed = setOverlayDebugVisualizationEnabledRuntime(state, true, (enabled) => {
state,
true,
(enabled) => {
state = enabled; state = enabled;
}, });
);
assert.equal(changed, true); assert.equal(changed, true);
assert.equal(state, true); assert.equal(state, true);
assert.deepEqual(broadcasts, [['runtime-options:changed', []]]); assert.deepEqual(broadcasts, [['runtime-options:changed', []]]);

View File

@@ -41,8 +41,7 @@ test('initializeOverlayRuntime skips Anki integration when ankiConnect.enabled i
setIntegrationCalls += 1; setIntegrationCalls += 1;
}, },
showDesktopNotification: () => {}, showDesktopNotification: () => {},
createFieldGroupingCallback: () => createFieldGroupingCallback: () => async () => ({
async () => ({
keepNoteId: 1, keepNoteId: 1,
deleteNoteId: 2, deleteNoteId: 2,
deleteDuplicate: false, deleteDuplicate: false,
@@ -96,8 +95,7 @@ test('initializeOverlayRuntime starts Anki integration when ankiConnect.enabled
setIntegrationCalls += 1; setIntegrationCalls += 1;
}, },
showDesktopNotification: () => {}, showDesktopNotification: () => {},
createFieldGroupingCallback: () => createFieldGroupingCallback: () => async () => ({
async () => ({
keepNoteId: 3, keepNoteId: 3,
deleteNoteId: 4, deleteNoteId: 4,
deleteDuplicate: false, deleteDuplicate: false,

View File

@@ -23,7 +23,8 @@ type CreateAnkiIntegrationArgs = {
}; };
function createDefaultAnkiIntegration(args: CreateAnkiIntegrationArgs): AnkiIntegrationLike { function createDefaultAnkiIntegration(args: CreateAnkiIntegrationArgs): AnkiIntegrationLike {
const { AnkiIntegration } = require('../../anki-integration') as typeof import('../../anki-integration'); const { AnkiIntegration } =
require('../../anki-integration') as typeof import('../../anki-integration');
return new AnkiIntegration( return new AnkiIntegration(
args.config, args.config,
args.subtitleTimingTracker as never, args.subtitleTimingTracker as never,
@@ -76,7 +77,10 @@ export function initializeOverlayRuntime(options: {
options.registerGlobalShortcuts(); options.registerGlobalShortcuts();
const createWindowTrackerHandler = options.createWindowTracker ?? createWindowTracker; const createWindowTrackerHandler = options.createWindowTracker ?? createWindowTracker;
const windowTracker = createWindowTrackerHandler(options.backendOverride, options.getMpvSocketPath()); const windowTracker = createWindowTrackerHandler(
options.backendOverride,
options.getMpvSocketPath(),
);
options.setWindowTracker(windowTracker); options.setWindowTracker(windowTracker);
if (windowTracker) { if (windowTracker) {
windowTracker.onGeometryChange = (geometry: WindowGeometry) => { windowTracker.onGeometryChange = (geometry: WindowGeometry) => {

View File

@@ -138,3 +138,35 @@ test('subtitle processing refresh can use explicit text override', async () => {
assert.deepEqual(emitted, [{ text: 'initial', tokens: [] }]); assert.deepEqual(emitted, [{ text: 'initial', tokens: [] }]);
}); });
test('subtitle processing cache invalidation only affects future subtitle events', async () => {
const emitted: SubtitleData[] = [];
const callsByText = new Map<string, number>();
const controller = createSubtitleProcessingController({
tokenizeSubtitle: async (text) => {
callsByText.set(text, (callsByText.get(text) ?? 0) + 1);
return { text, tokens: [] };
},
emitSubtitle: (payload) => emitted.push(payload),
});
controller.onSubtitleChange('same');
await flushMicrotasks();
controller.onSubtitleChange('other');
await flushMicrotasks();
controller.onSubtitleChange('same');
await flushMicrotasks();
assert.equal(callsByText.get('same'), 1);
assert.equal(emitted.length, 3);
controller.invalidateTokenizationCache();
assert.equal(emitted.length, 3);
controller.onSubtitleChange('different');
await flushMicrotasks();
controller.onSubtitleChange('same');
await flushMicrotasks();
assert.equal(callsByText.get('same'), 2);
});

View File

@@ -10,6 +10,7 @@ export interface SubtitleProcessingControllerDeps {
export interface SubtitleProcessingController { export interface SubtitleProcessingController {
onSubtitleChange: (text: string) => void; onSubtitleChange: (text: string) => void;
refreshCurrentSubtitle: (textOverride?: string) => void; refreshCurrentSubtitle: (textOverride?: string) => void;
invalidateTokenizationCache: () => void;
} }
export function createSubtitleProcessingController( export function createSubtitleProcessingController(
@@ -126,5 +127,8 @@ export function createSubtitleProcessingController(
refreshRequested = true; refreshRequested = true;
processLatest(); processLatest();
}, },
invalidateTokenizationCache: () => {
tokenizationCache.clear();
},
}; };
} }

View File

@@ -52,7 +52,12 @@ test('annotateTokens known-word match mode uses headword vs surface', () => {
test('annotateTokens excludes frequency for particle/bound_auxiliary and pos1 exclusions', () => { test('annotateTokens excludes frequency for particle/bound_auxiliary and pos1 exclusions', () => {
const tokens = [ const tokens = [
makeToken({ surface: 'は', headword: 'は', partOfSpeech: PartOfSpeech.particle, frequencyRank: 3 }), makeToken({
surface: 'は',
headword: 'は',
partOfSpeech: PartOfSpeech.particle,
frequencyRank: 3,
}),
makeToken({ makeToken({
surface: 'です', surface: 'です',
headword: 'です', headword: 'です',

View File

@@ -7,12 +7,7 @@ import {
DEFAULT_ANNOTATION_POS2_EXCLUSION_CONFIG, DEFAULT_ANNOTATION_POS2_EXCLUSION_CONFIG,
resolveAnnotationPos2ExclusionSet, resolveAnnotationPos2ExclusionSet,
} from '../../../token-pos2-exclusions'; } from '../../../token-pos2-exclusions';
import { import { JlptLevel, MergedToken, NPlusOneMatchMode, PartOfSpeech } from '../../../types';
JlptLevel,
MergedToken,
NPlusOneMatchMode,
PartOfSpeech,
} from '../../../types';
import { shouldIgnoreJlptByTerm, shouldIgnoreJlptForMecabPos1 } from '../jlpt-token-filter'; import { shouldIgnoreJlptByTerm, shouldIgnoreJlptForMecabPos1 } from '../jlpt-token-filter';
const KATAKANA_TO_HIRAGANA_OFFSET = 0x60; const KATAKANA_TO_HIRAGANA_OFFSET = 0x60;
@@ -67,10 +62,7 @@ function normalizePos1Tag(pos1: string | undefined): string {
return typeof pos1 === 'string' ? pos1.trim() : ''; return typeof pos1 === 'string' ? pos1.trim() : '';
} }
function isExcludedByTagSet( function isExcludedByTagSet(normalizedTag: string, exclusions: ReadonlySet<string>): boolean {
normalizedTag: string,
exclusions: ReadonlySet<string>,
): boolean {
if (!normalizedTag) { if (!normalizedTag) {
return false; return false;
} }
@@ -350,10 +342,7 @@ function isLikelyFrequencyNoiseToken(token: MergedToken): boolean {
continue; continue;
} }
if ( if (shouldIgnoreJlptByTerm(trimmedCandidate) || shouldIgnoreJlptByTerm(normalizedCandidate)) {
shouldIgnoreJlptByTerm(trimmedCandidate) ||
shouldIgnoreJlptByTerm(normalizedCandidate)
) {
return true; return true;
} }
@@ -442,8 +431,7 @@ export function annotateTokens(
})); }));
const frequencyEnabled = options.frequencyEnabled !== false; const frequencyEnabled = options.frequencyEnabled !== false;
const frequencyMarkedTokens = const frequencyMarkedTokens = frequencyEnabled
frequencyEnabled
? applyFrequencyMarking(knownMarkedTokens, pos1Exclusions, pos2Exclusions) ? applyFrequencyMarking(knownMarkedTokens, pos1Exclusions, pos2Exclusions)
: knownMarkedTokens.map((token) => ({ : knownMarkedTokens.map((token) => ({
...token, ...token,

View File

@@ -116,7 +116,9 @@ class ParserEnrichmentWorkerRuntime {
} }
private handleWorkerFailure(error: Error): void { private handleWorkerFailure(error: Error): void {
logger.debug(`Parser enrichment worker unavailable, falling back to main thread: ${error.message}`); logger.debug(
`Parser enrichment worker unavailable, falling back to main thread: ${error.message}`,
);
for (const pending of this.pending.values()) { for (const pending of this.pending.values()) {
pending.reject(error); pending.reject(error);
} }

View File

@@ -1256,10 +1256,7 @@ function getResolvedConfig() {
} }
function getRuntimeBooleanOption( function getRuntimeBooleanOption(
id: id: 'subtitle.annotation.nPlusOne' | 'subtitle.annotation.jlpt' | 'subtitle.annotation.frequency',
| 'subtitle.annotation.nPlusOne'
| 'subtitle.annotation.jlpt'
| 'subtitle.annotation.frequency',
fallback: boolean, fallback: boolean,
): boolean { ): boolean {
const value = appState.runtimeOptionsManager?.getOptionValue(id); const value = appState.runtimeOptionsManager?.getOptionValue(id);
@@ -2318,7 +2315,10 @@ const {
getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords, getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords,
getJlptLevel: (text) => appState.jlptLevelLookup(text), getJlptLevel: (text) => appState.jlptLevelLookup(text),
getJlptEnabled: () => getJlptEnabled: () =>
getRuntimeBooleanOption('subtitle.annotation.jlpt', getResolvedConfig().subtitleStyle.enableJlpt), getRuntimeBooleanOption(
'subtitle.annotation.jlpt',
getResolvedConfig().subtitleStyle.enableJlpt,
),
getFrequencyDictionaryEnabled: () => getFrequencyDictionaryEnabled: () =>
getRuntimeBooleanOption( getRuntimeBooleanOption(
'subtitle.annotation.frequency', 'subtitle.annotation.frequency',

View File

@@ -214,9 +214,13 @@ test('handleOverlayModalClosed hides modal window only after all pending modals
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, { runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
restoreOnModalClose: 'runtime-options', restoreOnModalClose: 'runtime-options',
}); });
runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, { runtime.sendToActiveOverlayWindow(
'subsync:open-manual',
{ sourceTracks: [] },
{
restoreOnModalClose: 'subsync', restoreOnModalClose: 'subsync',
}); },
);
runtime.handleOverlayModalClosed('runtime-options'); runtime.handleOverlayModalClosed('runtime-options');
assert.equal(window.getHideCount(), 0); assert.equal(window.getHideCount(), 0);
@@ -267,9 +271,13 @@ test('modal runtime notifies callers when modal input state becomes active/inact
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, { runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
restoreOnModalClose: 'runtime-options', restoreOnModalClose: 'runtime-options',
}); });
runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, { runtime.sendToActiveOverlayWindow(
'subsync:open-manual',
{ sourceTracks: [] },
{
restoreOnModalClose: 'subsync', restoreOnModalClose: 'subsync',
}); },
);
assert.deepEqual(state, []); assert.deepEqual(state, []);
runtime.notifyOverlayModalOpened('runtime-options'); runtime.notifyOverlayModalOpened('runtime-options');
assert.deepEqual(state, [true]); assert.deepEqual(state, [true]);
@@ -352,9 +360,13 @@ test('handleOverlayModalClosed hides modal window for single kiku modal', () =>
setModalWindowBounds: () => {}, setModalWindowBounds: () => {},
}); });
runtime.sendToActiveOverlayWindow('kiku:field-grouping-open', { test: true }, { runtime.sendToActiveOverlayWindow(
'kiku:field-grouping-open',
{ test: true },
{
restoreOnModalClose: 'kiku', restoreOnModalClose: 'kiku',
}); },
);
runtime.handleOverlayModalClosed('kiku'); runtime.handleOverlayModalClosed('kiku');
assert.equal(window.getHideCount(), 1); assert.equal(window.getHideCount(), 1);

View File

@@ -3,7 +3,9 @@ import type {
createMaybeProbeAnilistDurationHandler, createMaybeProbeAnilistDurationHandler,
} from './anilist-media-guess'; } from './anilist-media-guess';
type MaybeProbeAnilistDurationMainDeps = Parameters<typeof createMaybeProbeAnilistDurationHandler>[0]; type MaybeProbeAnilistDurationMainDeps = Parameters<
typeof createMaybeProbeAnilistDurationHandler
>[0];
type EnsureAnilistMediaGuessMainDeps = Parameters<typeof createEnsureAnilistMediaGuessHandler>[0]; type EnsureAnilistMediaGuessMainDeps = Parameters<typeof createEnsureAnilistMediaGuessHandler>[0];
export function createBuildMaybeProbeAnilistDurationMainDepsHandler( export function createBuildMaybeProbeAnilistDurationMainDepsHandler(
@@ -19,13 +21,17 @@ export function createBuildMaybeProbeAnilistDurationMainDepsHandler(
}); });
} }
export function createBuildEnsureAnilistMediaGuessMainDepsHandler(deps: EnsureAnilistMediaGuessMainDeps) { export function createBuildEnsureAnilistMediaGuessMainDepsHandler(
deps: EnsureAnilistMediaGuessMainDeps,
) {
return (): EnsureAnilistMediaGuessMainDeps => ({ return (): EnsureAnilistMediaGuessMainDeps => ({
getState: () => deps.getState(), getState: () => deps.getState(),
setState: (state) => deps.setState(state), setState: (state) => deps.setState(state),
resolveMediaPathForJimaku: (currentMediaPath) => deps.resolveMediaPathForJimaku(currentMediaPath), resolveMediaPathForJimaku: (currentMediaPath) =>
deps.resolveMediaPathForJimaku(currentMediaPath),
getCurrentMediaPath: () => deps.getCurrentMediaPath(), getCurrentMediaPath: () => deps.getCurrentMediaPath(),
getCurrentMediaTitle: () => deps.getCurrentMediaTitle(), getCurrentMediaTitle: () => deps.getCurrentMediaTitle(),
guessAnilistMediaInfo: (mediaPath, mediaTitle) => deps.guessAnilistMediaInfo(mediaPath, mediaTitle), guessAnilistMediaInfo: (mediaPath, mediaTitle) =>
deps.guessAnilistMediaInfo(mediaPath, mediaTitle),
}); });
} }

View File

@@ -6,8 +6,12 @@ import type {
createSetAnilistMediaGuessRuntimeStateHandler, createSetAnilistMediaGuessRuntimeStateHandler,
} from './anilist-media-state'; } from './anilist-media-state';
type GetCurrentAnilistMediaKeyMainDeps = Parameters<typeof createGetCurrentAnilistMediaKeyHandler>[0]; type GetCurrentAnilistMediaKeyMainDeps = Parameters<
type ResetAnilistMediaTrackingMainDeps = Parameters<typeof createResetAnilistMediaTrackingHandler>[0]; typeof createGetCurrentAnilistMediaKeyHandler
>[0];
type ResetAnilistMediaTrackingMainDeps = Parameters<
typeof createResetAnilistMediaTrackingHandler
>[0];
type GetAnilistMediaGuessRuntimeStateMainDeps = Parameters< type GetAnilistMediaGuessRuntimeStateMainDeps = Parameters<
typeof createGetAnilistMediaGuessRuntimeStateHandler typeof createGetAnilistMediaGuessRuntimeStateHandler
>[0]; >[0];

View File

@@ -47,7 +47,8 @@ export function createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler(
hasAttemptedUpdateKey: (key: string) => deps.hasAttemptedUpdateKey(key), hasAttemptedUpdateKey: (key: string) => deps.hasAttemptedUpdateKey(key),
processNextAnilistRetryUpdate: () => deps.processNextAnilistRetryUpdate(), processNextAnilistRetryUpdate: () => deps.processNextAnilistRetryUpdate(),
refreshAnilistClientSecretState: () => deps.refreshAnilistClientSecretState(), refreshAnilistClientSecretState: () => deps.refreshAnilistClientSecretState(),
enqueueRetry: (key: string, title: string, episode: number) => deps.enqueueRetry(key, title, episode), enqueueRetry: (key: string, title: string, episode: number) =>
deps.enqueueRetry(key, title, episode),
markRetryFailure: (key: string, message: string) => deps.markRetryFailure(key, message), markRetryFailure: (key: string, message: string) => deps.markRetryFailure(key, message),
markRetrySuccess: (key: string) => deps.markRetrySuccess(key), markRetrySuccess: (key: string) => deps.markRetrySuccess(key),
refreshRetryQueueState: () => deps.refreshRetryQueueState(), refreshRetryQueueState: () => deps.refreshRetryQueueState(),

View File

@@ -64,7 +64,11 @@ export function createProcessNextAnilistRetryUpdateHandler(deps: {
return { ok: false, message: 'AniList token unavailable for queued retry.' }; return { ok: false, message: 'AniList token unavailable for queued retry.' };
} }
const result = await deps.updateAnilistPostWatchProgress(accessToken, queued.title, queued.episode); const result = await deps.updateAnilistPostWatchProgress(
accessToken,
queued.title,
queued.episode,
);
if (result.status === 'updated' || result.status === 'skipped') { if (result.status === 'updated' || result.status === 'skipped') {
deps.markSuccess(queued.key); deps.markSuccess(queued.key);
deps.rememberAttemptedUpdateKey(queued.key); deps.rememberAttemptedUpdateKey(queued.key);
@@ -166,7 +170,11 @@ export function createMaybeRunAnilistPostWatchUpdateHandler(deps: {
return; return;
} }
const result = await deps.updateAnilistPostWatchProgress(accessToken, guess.title, guess.episode); const result = await deps.updateAnilistPostWatchProgress(
accessToken,
guess.title,
guess.episode,
);
if (result.status === 'updated') { if (result.status === 'updated') {
deps.rememberAttemptedUpdateKey(attemptKey); deps.rememberAttemptedUpdateKey(attemptKey);
deps.markRetrySuccess(attemptKey); deps.markRetrySuccess(attemptKey);

View File

@@ -44,7 +44,8 @@ export function createBuildHandleAnilistSetupProtocolUrlMainDepsHandler(
deps: HandleAnilistSetupProtocolUrlMainDeps, deps: HandleAnilistSetupProtocolUrlMainDeps,
) { ) {
return (): HandleAnilistSetupProtocolUrlMainDeps => ({ return (): HandleAnilistSetupProtocolUrlMainDeps => ({
consumeAnilistSetupTokenFromUrl: (rawUrl: string) => deps.consumeAnilistSetupTokenFromUrl(rawUrl), consumeAnilistSetupTokenFromUrl: (rawUrl: string) =>
deps.consumeAnilistSetupTokenFromUrl(rawUrl),
logWarn: (message: string, details: unknown) => deps.logWarn(message, details), logWarn: (message: string, details: unknown) => deps.logWarn(message, details),
}); });
} }

View File

@@ -66,11 +66,7 @@ export function createRegisterSubminerProtocolClientHandler(deps: {
getArgv: () => string[]; getArgv: () => string[];
execPath: string; execPath: string;
resolvePath: (value: string) => string; resolvePath: (value: string) => string;
setAsDefaultProtocolClient: ( setAsDefaultProtocolClient: (scheme: string, path?: string, args?: string[]) => boolean;
scheme: string,
path?: string,
args?: string[],
) => boolean;
logWarn: (message: string, details?: unknown) => void; logWarn: (message: string, details?: unknown) => void;
}) { }) {
return (): void => { return (): void => {

View File

@@ -259,7 +259,8 @@ test('open anilist setup handler no-ops when existing setup window focused', ()
test('open anilist setup handler wires navigation, fallback, and lifecycle', () => { test('open anilist setup handler wires navigation, fallback, and lifecycle', () => {
let openHandler: ((params: { url: string }) => { action: 'deny' }) | null = null; let openHandler: ((params: { url: string }) => { action: 'deny' }) | null = null;
let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null = null; let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null =
null;
let didNavigateHandler: ((event: unknown, url: string) => void) | null = null; let didNavigateHandler: ((event: unknown, url: string) => void) | null = null;
let didFinishLoadHandler: (() => void) | null = null; let didFinishLoadHandler: (() => void) | null = null;
let didFailLoadHandler: let didFailLoadHandler:
@@ -276,7 +277,12 @@ test('open anilist setup handler wires navigation, fallback, and lifecycle', ()
openHandler = handler; openHandler = handler;
}, },
on: ( on: (
event: 'will-navigate' | 'will-redirect' | 'did-navigate' | 'did-fail-load' | 'did-finish-load', event:
| 'will-navigate'
| 'will-redirect'
| 'did-navigate'
| 'did-fail-load'
| 'did-finish-load',
handler: (...args: any[]) => void, handler: (...args: any[]) => void,
) => { ) => {
if (event === 'will-navigate') willNavigateHandler = handler as never; if (event === 'will-navigate') willNavigateHandler = handler as never;

View File

@@ -126,7 +126,11 @@ export function createAnilistSetupDidNavigateHandler(deps: {
} }
export function createAnilistSetupDidFailLoadHandler(deps: { export function createAnilistSetupDidFailLoadHandler(deps: {
onLoadFailure: (details: { errorCode: number; errorDescription: string; validatedURL: string }) => void; onLoadFailure: (details: {
errorCode: number;
errorDescription: string;
validatedURL: string;
}) => void;
}) { }) {
return (details: { errorCode: number; errorDescription: string; validatedURL: string }): void => { return (details: { errorCode: number; errorDescription: string; validatedURL: string }): void => {
deps.onLoadFailure(details); deps.onLoadFailure(details);
@@ -175,7 +179,11 @@ export function createAnilistSetupFallbackHandler(deps: {
logWarn: (message: string) => void; logWarn: (message: string) => void;
}) { }) {
return { return {
onLoadFailure: (details: { errorCode: number; errorDescription: string; validatedURL: string }) => { onLoadFailure: (details: {
errorCode: number;
errorDescription: string;
validatedURL: string;
}) => {
deps.logError('AniList setup window failed to load', details); deps.logError('AniList setup window failed to load', details);
deps.openSetupInBrowser(); deps.openSetupInBrowser();
if (!deps.setupWindow.isDestroyed()) { if (!deps.setupWindow.isDestroyed()) {
@@ -298,12 +306,7 @@ export function createOpenAnilistSetupWindowHandler<TWindow extends AnilistSetup
}); });
setupWindow.webContents.on( setupWindow.webContents.on(
'did-fail-load', 'did-fail-load',
( (_event: unknown, errorCode: number, errorDescription: string, validatedURL: string) => {
_event: unknown,
errorCode: number,
errorDescription: string,
validatedURL: string,
) => {
handleDidFailLoad({ handleDidFailLoad({
errorCode, errorCode,
errorDescription, errorDescription,

View File

@@ -65,9 +65,7 @@ export function findAnilistSetupDeepLinkArgvUrl(argv: readonly string[]): string
return null; return null;
} }
export function consumeAnilistSetupCallbackUrl( export function consumeAnilistSetupCallbackUrl(deps: ConsumeAnilistSetupCallbackUrlDeps): boolean {
deps: ConsumeAnilistSetupCallbackUrlDeps,
): boolean {
const token = extractAnilistAccessTokenFromUrl(deps.rawUrl); const token = extractAnilistAccessTokenFromUrl(deps.rawUrl);
if (!token) { if (!token) {
return false; return false;

View File

@@ -12,7 +12,9 @@ type ConfigWithAnilistToken = {
}; };
}; };
export function createRefreshAnilistClientSecretStateHandler<TConfig extends ConfigWithAnilistToken>(deps: { export function createRefreshAnilistClientSecretStateHandler<
TConfig extends ConfigWithAnilistToken,
>(deps: {
getResolvedConfig: () => TConfig; getResolvedConfig: () => TConfig;
isAnilistTrackingEnabled: (config: TConfig) => boolean; isAnilistTrackingEnabled: (config: TConfig) => boolean;
getCachedAccessToken: () => string | null; getCachedAccessToken: () => string | null;

View File

@@ -24,7 +24,9 @@ export function createBuildUpdateLastCardFromClipboardMainDepsHandler<TAnki>(dep
}); });
} }
export function createBuildRefreshKnownWordCacheMainDepsHandler(deps: RefreshKnownWordCacheMainDeps) { export function createBuildRefreshKnownWordCacheMainDepsHandler(
deps: RefreshKnownWordCacheMainDeps,
) {
return (): RefreshKnownWordCacheMainDeps => ({ return (): RefreshKnownWordCacheMainDeps => ({
getAnkiIntegration: () => deps.getAnkiIntegration(), getAnkiIntegration: () => deps.getAnkiIntegration(),
missingIntegrationMessage: deps.missingIntegrationMessage, missingIntegrationMessage: deps.missingIntegrationMessage,
@@ -42,8 +44,10 @@ export function createBuildTriggerFieldGroupingMainDepsHandler<TAnki>(deps: {
return () => ({ return () => ({
getAnkiIntegration: () => deps.getAnkiIntegration(), getAnkiIntegration: () => deps.getAnkiIntegration(),
showMpvOsd: (text: string) => deps.showMpvOsd(text), showMpvOsd: (text: string) => deps.showMpvOsd(text),
triggerFieldGroupingCore: (options: { ankiIntegration: TAnki; showMpvOsd: (text: string) => void }) => triggerFieldGroupingCore: (options: {
deps.triggerFieldGroupingCore(options), ankiIntegration: TAnki;
showMpvOsd: (text: string) => void;
}) => deps.triggerFieldGroupingCore(options),
}); });
} }
@@ -58,8 +62,10 @@ export function createBuildMarkLastCardAsAudioCardMainDepsHandler<TAnki>(deps: {
return () => ({ return () => ({
getAnkiIntegration: () => deps.getAnkiIntegration(), getAnkiIntegration: () => deps.getAnkiIntegration(),
showMpvOsd: (text: string) => deps.showMpvOsd(text), showMpvOsd: (text: string) => deps.showMpvOsd(text),
markLastCardAsAudioCardCore: (options: { ankiIntegration: TAnki; showMpvOsd: (text: string) => void }) => markLastCardAsAudioCardCore: (options: {
deps.markLastCardAsAudioCardCore(options), ankiIntegration: TAnki;
showMpvOsd: (text: string) => void;
}) => deps.markLastCardAsAudioCardCore(options),
}); });
} }

View File

@@ -52,8 +52,7 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
destroyTray: () => deps.destroyTray(), destroyTray: () => deps.destroyTray(),
stopConfigHotReload: () => deps.stopConfigHotReload(), stopConfigHotReload: () => deps.stopConfigHotReload(),
restorePreviousSecondarySubVisibility: () => deps.restorePreviousSecondarySubVisibility(), restorePreviousSecondarySubVisibility: () => deps.restorePreviousSecondarySubVisibility(),
restoreMpvSubVisibility: () => restoreMpvSubVisibility: () => deps.restoreMpvSubVisibility(),
deps.restoreMpvSubVisibility(),
unregisterAllGlobalShortcuts: () => deps.unregisterAllGlobalShortcuts(), unregisterAllGlobalShortcuts: () => deps.unregisterAllGlobalShortcuts(),
stopSubtitleWebsocket: () => deps.stopSubtitleWebsocket(), stopSubtitleWebsocket: () => deps.stopSubtitleWebsocket(),
stopTexthookerService: () => deps.stopTexthookerService(), stopTexthookerService: () => deps.stopTexthookerService(),

View File

@@ -1,8 +1,6 @@
import type { AppReadyRuntimeDepsFactoryInput } from '../app-lifecycle'; import type { AppReadyRuntimeDepsFactoryInput } from '../app-lifecycle';
export function createBuildAppReadyRuntimeMainDepsHandler( export function createBuildAppReadyRuntimeMainDepsHandler(deps: AppReadyRuntimeDepsFactoryInput) {
deps: AppReadyRuntimeDepsFactoryInput,
) {
return (): AppReadyRuntimeDepsFactoryInput => ({ return (): AppReadyRuntimeDepsFactoryInput => ({
loadSubtitlePosition: deps.loadSubtitlePosition, loadSubtitlePosition: deps.loadSubtitlePosition,
resolveKeybindings: deps.resolveKeybindings, resolveKeybindings: deps.resolveKeybindings,
@@ -27,8 +25,7 @@ export function createBuildAppReadyRuntimeMainDepsHandler(
prewarmSubtitleDictionaries: deps.prewarmSubtitleDictionaries, prewarmSubtitleDictionaries: deps.prewarmSubtitleDictionaries,
startBackgroundWarmups: deps.startBackgroundWarmups, startBackgroundWarmups: deps.startBackgroundWarmups,
texthookerOnlyMode: deps.texthookerOnlyMode, texthookerOnlyMode: deps.texthookerOnlyMode,
shouldAutoInitializeOverlayRuntimeFromConfig: shouldAutoInitializeOverlayRuntimeFromConfig: deps.shouldAutoInitializeOverlayRuntimeFromConfig,
deps.shouldAutoInitializeOverlayRuntimeFromConfig,
initializeOverlayRuntime: deps.initializeOverlayRuntime, initializeOverlayRuntime: deps.initializeOverlayRuntime,
handleInitialArgs: deps.handleInitialArgs, handleInitialArgs: deps.handleInitialArgs,
onCriticalConfigErrors: deps.onCriticalConfigErrors, onCriticalConfigErrors: deps.onCriticalConfigErrors,

View File

@@ -70,7 +70,8 @@ test('open yomitan settings main deps map async open callbacks', async () => {
const extension = { id: 'ext' }; const extension = { id: 'ext' };
const deps = createBuildOpenYomitanSettingsMainDepsHandler({ const deps = createBuildOpenYomitanSettingsMainDepsHandler({
ensureYomitanExtensionLoaded: async () => extension, ensureYomitanExtensionLoaded: async () => extension,
openYomitanSettingsWindow: ({ yomitanExt }) => calls.push(`open:${(yomitanExt as { id: string }).id}`), openYomitanSettingsWindow: ({ yomitanExt }) =>
calls.push(`open:${(yomitanExt as { id: string }).id}`),
getExistingWindow: () => currentWindow, getExistingWindow: () => currentWindow,
setWindow: (window) => { setWindow: (window) => {
currentWindow = window; currentWindow = window;

View File

@@ -2,9 +2,7 @@ import { createCliCommandContext } from './cli-command-context';
import { createBuildCliCommandContextDepsHandler } from './cli-command-context-deps'; import { createBuildCliCommandContextDepsHandler } from './cli-command-context-deps';
import { createBuildCliCommandContextMainDepsHandler } from './cli-command-context-main-deps'; import { createBuildCliCommandContextMainDepsHandler } from './cli-command-context-main-deps';
type CliCommandContextMainDeps = Parameters< type CliCommandContextMainDeps = Parameters<typeof createBuildCliCommandContextMainDepsHandler>[0];
typeof createBuildCliCommandContextMainDepsHandler
>[0];
export function createCliCommandContextFactory(deps: CliCommandContextMainDeps) { export function createCliCommandContextFactory(deps: CliCommandContextMainDeps) {
const buildCliCommandContextMainDepsHandler = createBuildCliCommandContextMainDepsHandler(deps); const buildCliCommandContextMainDepsHandler = createBuildCliCommandContextMainDepsHandler(deps);

View File

@@ -34,11 +34,11 @@ function createDeps() {
triggerFieldGrouping: async () => {}, triggerFieldGrouping: async () => {},
triggerSubsyncFromConfig: async () => {}, triggerSubsyncFromConfig: async () => {},
markLastCardAsAudioCard: async () => {}, markLastCardAsAudioCard: async () => {},
getAnilistStatus: () => ({} as never), getAnilistStatus: () => ({}) as never,
clearAnilistToken: () => {}, clearAnilistToken: () => {},
openAnilistSetup: () => {}, openAnilistSetup: () => {},
openJellyfinSetup: () => {}, openJellyfinSetup: () => {},
getAnilistQueueStatus: () => ({} as never), getAnilistQueueStatus: () => ({}) as never,
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }), retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
runJellyfinCommand: async () => {}, runJellyfinCommand: async () => {},
openYomitanSettings: () => {}, openYomitanSettings: () => {},

View File

@@ -15,8 +15,7 @@ export function createCliCommandRuntimeHandler<TCliContext>(deps: {
cliContext: TCliContext, cliContext: TCliContext,
) => void; ) => void;
}) { }) {
const handleTexthookerOnlyModeTransitionHandler = const handleTexthookerOnlyModeTransitionHandler = createHandleTexthookerOnlyModeTransitionHandler(
createHandleTexthookerOnlyModeTransitionHandler(
createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler( createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler(
deps.handleTexthookerOnlyModeTransitionMainDeps, deps.handleTexthookerOnlyModeTransitionMainDeps,
)(), )(),

View File

@@ -13,9 +13,10 @@ export type AppendClipboardVideoToQueueRuntimeDeps = {
sendMpvCommand: (command: (string | number)[]) => void; sendMpvCommand: (command: (string | number)[]) => void;
}; };
export function appendClipboardVideoToQueueRuntime( export function appendClipboardVideoToQueueRuntime(deps: AppendClipboardVideoToQueueRuntimeDeps): {
deps: AppendClipboardVideoToQueueRuntimeDeps, ok: boolean;
): { ok: boolean; message: string } { message: string;
} {
const mpvClient = deps.getMpvClient(); const mpvClient = deps.getMpvClient();
if (!mpvClient || !mpvClient.connected) { if (!mpvClient || !mpvClient.connected) {
return { ok: false, message: 'MPV is not connected.' }; return { ok: false, message: 'MPV is not connected.' };

View File

@@ -6,7 +6,8 @@ test('composeJellyfinRemoteHandlers returns callable jellyfin remote handlers',
let lastProgressAt = 0; let lastProgressAt = 0;
const composed = composeJellyfinRemoteHandlers({ const composed = composeJellyfinRemoteHandlers({
getConfiguredSession: () => null, getConfiguredSession: () => null,
getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: 'test', deviceId: 'dev' }) as never, getClientInfo: () =>
({ clientName: 'SubMiner', clientVersion: 'test', deviceId: 'dev' }) as never,
getJellyfinConfig: () => ({ enabled: false }) as never, getJellyfinConfig: () => ({ enabled: false }) as never,
playJellyfinItem: async () => {}, playJellyfinItem: async () => {},
logWarn: () => {}, logWarn: () => {},

View File

@@ -39,7 +39,10 @@ export function createConfigDerivedRuntime(deps: ConfigDerivedRuntimeDeps): {
deps.defaultJimakuLanguagePreference, deps.defaultJimakuLanguagePreference,
), ),
getJimakuMaxEntryResults: () => getJimakuMaxEntryResults: () =>
getJimakuMaxEntryResultsCore(() => deps.getResolvedConfig(), deps.defaultJimakuMaxEntryResults), getJimakuMaxEntryResultsCore(
() => deps.getResolvedConfig(),
deps.defaultJimakuMaxEntryResults,
),
resolveJimakuApiKey: () => resolveJimakuApiKeyCore(() => deps.getResolvedConfig()), resolveJimakuApiKey: () => resolveJimakuApiKeyCore(() => deps.getResolvedConfig()),
jimakuFetchJson: <T>( jimakuFetchJson: <T>(
endpoint: string, endpoint: string,

View File

@@ -111,7 +111,7 @@ test('config hot reload applied main deps builder maps callbacks', () => {
test('config hot reload runtime main deps builder maps runtime callbacks', () => { test('config hot reload runtime main deps builder maps runtime callbacks', () => {
const calls: string[] = []; const calls: string[] = [];
const deps = createBuildConfigHotReloadRuntimeMainDepsHandler({ const deps = createBuildConfigHotReloadRuntimeMainDepsHandler({
getCurrentConfig: () => ({ id: 1 } as never as ResolvedConfig), getCurrentConfig: () => ({ id: 1 }) as never as ResolvedConfig,
reloadConfigStrict: () => reloadConfigStrict: () =>
({ ({
ok: true, ok: true,
@@ -144,5 +144,10 @@ test('config hot reload runtime main deps builder maps runtime callbacks', () =>
deps.onRestartRequired([]); deps.onRestartRequired([]);
deps.onInvalidConfig('bad'); deps.onInvalidConfig('bad');
deps.onValidationWarnings('/tmp/config.jsonc', []); deps.onValidationWarnings('/tmp/config.jsonc', []);
assert.deepEqual(calls, ['hot-reload', 'restart-required', 'invalid-config', 'validation-warnings']); assert.deepEqual(calls, [
'hot-reload',
'restart-required',
'invalid-config',
'validation-warnings',
]);
}); });

View File

@@ -3,7 +3,12 @@ import type {
ConfigHotReloadRuntimeDeps, ConfigHotReloadRuntimeDeps,
} from '../../core/services/config-hot-reload'; } from '../../core/services/config-hot-reload';
import type { ReloadConfigStrictResult } from '../../config'; import type { ReloadConfigStrictResult } from '../../config';
import type { ConfigHotReloadPayload, ConfigValidationWarning, ResolvedConfig, SecondarySubMode } from '../../types'; import type {
ConfigHotReloadPayload,
ConfigValidationWarning,
ResolvedConfig,
SecondarySubMode,
} from '../../types';
import type { createConfigHotReloadMessageHandler } from './config-hot-reload-handlers'; import type { createConfigHotReloadMessageHandler } from './config-hot-reload-handlers';
type ConfigWatchListener = (eventType: string, filename: string | null) => void; type ConfigWatchListener = (eventType: string, filename: string | null) => void;

View File

@@ -29,7 +29,8 @@ test('jlpt dictionary runtime main deps builder maps search paths and log prefix
const deps = createBuildJlptDictionaryRuntimeMainDepsHandler({ const deps = createBuildJlptDictionaryRuntimeMainDepsHandler({
isJlptEnabled: () => true, isJlptEnabled: () => true,
getDictionaryRoots: () => ['/root/a'], getDictionaryRoots: () => ['/root/a'],
getJlptDictionarySearchPaths: ({ getDictionaryRoots }) => getDictionaryRoots().map((path) => `${path}/jlpt`), getJlptDictionarySearchPaths: ({ getDictionaryRoots }) =>
getDictionaryRoots().map((path) => `${path}/jlpt`),
setJlptLevelLookup: () => calls.push('set-lookup'), setJlptLevelLookup: () => calls.push('set-lookup'),
logInfo: (message) => calls.push(`log:${message}`), logInfo: (message) => calls.push(`log:${message}`),
})(); })();

View File

@@ -12,20 +12,24 @@ test('get configured shortcuts main deps map config resolver inputs', () => {
const build = createBuildGetConfiguredShortcutsMainDepsHandler({ const build = createBuildGetConfiguredShortcutsMainDepsHandler({
getResolvedConfig: () => config, getResolvedConfig: () => config,
defaultConfig: defaults, defaultConfig: defaults,
resolveConfiguredShortcuts: (nextConfig, nextDefaults) => ({ nextConfig, nextDefaults }) as never, resolveConfiguredShortcuts: (nextConfig, nextDefaults) =>
({ nextConfig, nextDefaults }) as never,
}); });
const deps = build(); const deps = build();
assert.equal(deps.getResolvedConfig(), config); assert.equal(deps.getResolvedConfig(), config);
assert.equal(deps.defaultConfig, defaults); assert.equal(deps.defaultConfig, defaults);
assert.deepEqual(deps.resolveConfiguredShortcuts(config, defaults), { nextConfig: config, nextDefaults: defaults }); assert.deepEqual(deps.resolveConfiguredShortcuts(config, defaults), {
nextConfig: config,
nextDefaults: defaults,
});
}); });
test('register global shortcuts main deps map callbacks and flags', () => { test('register global shortcuts main deps map callbacks and flags', () => {
const calls: string[] = []; const calls: string[] = [];
const mainWindow = { id: 'main' }; const mainWindow = { id: 'main' };
const build = createBuildRegisterGlobalShortcutsMainDepsHandler({ const build = createBuildRegisterGlobalShortcutsMainDepsHandler({
getConfiguredShortcuts: () => ({ copySubtitle: 's' } as never), getConfiguredShortcuts: () => ({ copySubtitle: 's' }) as never,
registerGlobalShortcutsCore: () => calls.push('register'), registerGlobalShortcutsCore: () => calls.push('register'),
toggleVisibleOverlay: () => calls.push('toggle-visible'), toggleVisibleOverlay: () => calls.push('toggle-visible'),
openYomitanSettings: () => calls.push('open-yomitan'), openYomitanSettings: () => calls.push('open-yomitan'),

View File

@@ -32,15 +32,17 @@ export function createGlobalShortcutsRuntimeHandlers(deps: {
const getConfiguredShortcutsMainDeps = createBuildGetConfiguredShortcutsMainDepsHandler( const getConfiguredShortcutsMainDeps = createBuildGetConfiguredShortcutsMainDepsHandler(
deps.getConfiguredShortcutsMainDeps, deps.getConfiguredShortcutsMainDeps,
)(); )();
const getConfiguredShortcutsHandler = const getConfiguredShortcutsHandler = createGetConfiguredShortcutsHandler(
createGetConfiguredShortcutsHandler(getConfiguredShortcutsMainDeps); getConfiguredShortcutsMainDeps,
);
const getConfiguredShortcuts = () => getConfiguredShortcutsHandler(); const getConfiguredShortcuts = () => getConfiguredShortcutsHandler();
const registerGlobalShortcutsMainDeps = createBuildRegisterGlobalShortcutsMainDepsHandler( const registerGlobalShortcutsMainDeps = createBuildRegisterGlobalShortcutsMainDepsHandler(
deps.buildRegisterGlobalShortcutsMainDeps(getConfiguredShortcuts), deps.buildRegisterGlobalShortcutsMainDeps(getConfiguredShortcuts),
)(); )();
const registerGlobalShortcutsHandler = const registerGlobalShortcutsHandler = createRegisterGlobalShortcutsHandler(
createRegisterGlobalShortcutsHandler(registerGlobalShortcutsMainDeps); registerGlobalShortcutsMainDeps,
);
const registerGlobalShortcuts = () => registerGlobalShortcutsHandler(); const registerGlobalShortcuts = () => registerGlobalShortcutsHandler();
const refreshGlobalAndOverlayShortcutsMainDeps = const refreshGlobalAndOverlayShortcutsMainDeps =

View File

@@ -5,10 +5,7 @@ import type { RegisterGlobalShortcutsServiceOptions } from '../../core/services/
export function createGetConfiguredShortcutsHandler(deps: { export function createGetConfiguredShortcutsHandler(deps: {
getResolvedConfig: () => Config; getResolvedConfig: () => Config;
defaultConfig: Config; defaultConfig: Config;
resolveConfiguredShortcuts: ( resolveConfiguredShortcuts: (config: Config, defaultConfig: Config) => ConfiguredShortcuts;
config: Config,
defaultConfig: Config,
) => ConfiguredShortcuts;
}) { }) {
return (): ConfiguredShortcuts => return (): ConfiguredShortcuts =>
deps.resolveConfiguredShortcuts(deps.getResolvedConfig(), deps.defaultConfig); deps.resolveConfiguredShortcuts(deps.getResolvedConfig(), deps.defaultConfig);

View File

@@ -5,7 +5,7 @@ import { createBuildImmersionTrackerStartupMainDepsHandler } from './immersion-s
test('immersion tracker startup main deps builder maps callbacks', () => { test('immersion tracker startup main deps builder maps callbacks', () => {
const calls: string[] = []; const calls: string[] = [];
const deps = createBuildImmersionTrackerStartupMainDepsHandler({ const deps = createBuildImmersionTrackerStartupMainDepsHandler({
getResolvedConfig: () => ({ immersionTracking: { enabled: true } } as never), getResolvedConfig: () => ({ immersionTracking: { enabled: true } }) as never,
getConfiguredDbPath: () => '/tmp/immersion.db', getConfiguredDbPath: () => '/tmp/immersion.db',
createTrackerService: () => { createTrackerService: () => {
calls.push('create'); calls.push('create');
@@ -21,9 +21,12 @@ test('immersion tracker startup main deps builder maps callbacks', () => {
assert.deepEqual(deps.getResolvedConfig(), { immersionTracking: { enabled: true } }); assert.deepEqual(deps.getResolvedConfig(), { immersionTracking: { enabled: true } });
assert.equal(deps.getConfiguredDbPath(), '/tmp/immersion.db'); assert.equal(deps.getConfiguredDbPath(), '/tmp/immersion.db');
assert.deepEqual(deps.createTrackerService({ dbPath: '/tmp/immersion.db', policy: {} as never }), { assert.deepEqual(
deps.createTrackerService({ dbPath: '/tmp/immersion.db', policy: {} as never }),
{
id: 'tracker', id: 'tracker',
}); },
);
deps.setTracker(null); deps.setTracker(null);
assert.equal(deps.getMpvClient()?.connected, true); assert.equal(deps.getMpvClient()?.connected, true);
deps.seedTrackerFromCurrentMedia(); deps.seedTrackerFromCurrentMedia();

View File

@@ -24,7 +24,7 @@ test('initial args handler no-ops without initial args', () => {
test('initial args handler ensures tray in background mode', () => { test('initial args handler ensures tray in background mode', () => {
let ensuredTray = false; let ensuredTray = false;
const handleInitialArgs = createHandleInitialArgsHandler({ const handleInitialArgs = createHandleInitialArgsHandler({
getInitialArgs: () => ({ start: true } as never), getInitialArgs: () => ({ start: true }) as never,
isBackgroundMode: () => true, isBackgroundMode: () => true,
ensureTray: () => { ensureTray: () => {
ensuredTray = true; ensuredTray = true;
@@ -44,7 +44,7 @@ test('initial args handler auto-connects mpv when needed', () => {
let connectCalls = 0; let connectCalls = 0;
let logged = false; let logged = false;
const handleInitialArgs = createHandleInitialArgsHandler({ const handleInitialArgs = createHandleInitialArgsHandler({
getInitialArgs: () => ({ start: true } as never), getInitialArgs: () => ({ start: true }) as never,
isBackgroundMode: () => false, isBackgroundMode: () => false,
ensureTray: () => {}, ensureTray: () => {},
isTexthookerOnlyMode: () => false, isTexthookerOnlyMode: () => false,
@@ -69,7 +69,7 @@ test('initial args handler auto-connects mpv when needed', () => {
test('initial args handler forwards args to cli handler', () => { test('initial args handler forwards args to cli handler', () => {
const seenSources: string[] = []; const seenSources: string[] = [];
const handleInitialArgs = createHandleInitialArgsHandler({ const handleInitialArgs = createHandleInitialArgsHandler({
getInitialArgs: () => ({ start: true } as never), getInitialArgs: () => ({ start: true }) as never,
isBackgroundMode: () => false, isBackgroundMode: () => false,
ensureTray: () => {}, ensureTray: () => {},
isTexthookerOnlyMode: () => false, isTexthookerOnlyMode: () => false,

View File

@@ -5,7 +5,7 @@ import { createInitialArgsRuntimeHandler } from './initial-args-runtime-handler'
test('initial args runtime handler composes main deps and runs initial command flow', () => { test('initial args runtime handler composes main deps and runs initial command flow', () => {
const calls: string[] = []; const calls: string[] = [];
const handleInitialArgs = createInitialArgsRuntimeHandler({ const handleInitialArgs = createInitialArgsRuntimeHandler({
getInitialArgs: () => ({ start: true } as never), getInitialArgs: () => ({ start: true }) as never,
isBackgroundMode: () => true, isBackgroundMode: () => true,
ensureTray: () => calls.push('tray'), ensureTray: () => calls.push('tray'),
isTexthookerOnlyMode: () => false, isTexthookerOnlyMode: () => false,
@@ -20,5 +20,10 @@ test('initial args runtime handler composes main deps and runs initial command f
handleInitialArgs(); handleInitialArgs();
assert.deepEqual(calls, ['tray', 'log:Auto-connecting MPV client for immersion tracking', 'connect', 'cli:initial']); assert.deepEqual(calls, [
'tray',
'log:Auto-connecting MPV client for immersion tracking',
'connect',
'cli:initial',
]);
}); });

View File

@@ -1,6 +1,8 @@
import type { MpvCommandFromIpcRuntimeDeps } from '../ipc-mpv-command'; import type { MpvCommandFromIpcRuntimeDeps } from '../ipc-mpv-command';
export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler(deps: MpvCommandFromIpcRuntimeDeps) { export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler(
deps: MpvCommandFromIpcRuntimeDeps,
) {
return (): MpvCommandFromIpcRuntimeDeps => ({ return (): MpvCommandFromIpcRuntimeDeps => ({
triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(), triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(),
openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(), openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(),

View File

@@ -1,4 +1,7 @@
import { createHandleMpvCommandFromIpcHandler, createRunSubsyncManualFromIpcHandler } from './ipc-bridge-actions'; import {
createHandleMpvCommandFromIpcHandler,
createRunSubsyncManualFromIpcHandler,
} from './ipc-bridge-actions';
import { import {
createBuildHandleMpvCommandFromIpcMainDepsHandler, createBuildHandleMpvCommandFromIpcMainDepsHandler,
createBuildRunSubsyncManualFromIpcMainDepsHandler, createBuildRunSubsyncManualFromIpcMainDepsHandler,
@@ -22,10 +25,10 @@ export function createIpcRuntimeHandlers<TRequest, TResult>(deps: {
handleMpvCommandFromIpcMainDeps, handleMpvCommandFromIpcMainDeps,
); );
const runSubsyncManualFromIpcMainDeps = const runSubsyncManualFromIpcMainDeps = createBuildRunSubsyncManualFromIpcMainDepsHandler<
createBuildRunSubsyncManualFromIpcMainDepsHandler<TRequest, TResult>( TRequest,
deps.runSubsyncManualFromIpcDeps, TResult
)(); >(deps.runSubsyncManualFromIpcDeps)();
const runSubsyncManualFromIpc = createRunSubsyncManualFromIpcHandler<TRequest, TResult>( const runSubsyncManualFromIpc = createRunSubsyncManualFromIpcHandler<TRequest, TResult>(
runSubsyncManualFromIpcMainDeps, runSubsyncManualFromIpcMainDeps,
); );

View File

@@ -94,7 +94,11 @@ export function createHandleJellyfinListCommands(deps: {
if (!args.jellyfinItemId) { if (!args.jellyfinItemId) {
throw new Error('Missing --jellyfin-item-id for --jellyfin-subtitles.'); throw new Error('Missing --jellyfin-item-id for --jellyfin-subtitles.');
} }
const tracks = await deps.listJellyfinSubtitleTracks(session, clientInfo, args.jellyfinItemId); const tracks = await deps.listJellyfinSubtitleTracks(
session,
clientInfo,
args.jellyfinItemId,
);
if (tracks.length === 0) { if (tracks.length === 0) {
deps.logInfo('No Jellyfin subtitle tracks found for item.'); deps.logInfo('No Jellyfin subtitle tracks found for item.');
return true; return true;

View File

@@ -1,15 +1,7 @@
import type { import type { createHandleJellyfinAuthCommands } from './jellyfin-cli-auth';
createHandleJellyfinAuthCommands, import type { createHandleJellyfinListCommands } from './jellyfin-cli-list';
} from './jellyfin-cli-auth'; import type { createHandleJellyfinPlayCommand } from './jellyfin-cli-play';
import type { import type { createHandleJellyfinRemoteAnnounceCommand } from './jellyfin-cli-remote-announce';
createHandleJellyfinListCommands,
} from './jellyfin-cli-list';
import type {
createHandleJellyfinPlayCommand,
} from './jellyfin-cli-play';
import type {
createHandleJellyfinRemoteAnnounceCommand,
} from './jellyfin-cli-remote-announce';
type HandleJellyfinAuthCommandsMainDeps = Parameters<typeof createHandleJellyfinAuthCommands>[0]; type HandleJellyfinAuthCommandsMainDeps = Parameters<typeof createHandleJellyfinAuthCommands>[0];
type HandleJellyfinListCommandsMainDeps = Parameters<typeof createHandleJellyfinListCommands>[0]; type HandleJellyfinListCommandsMainDeps = Parameters<typeof createHandleJellyfinListCommands>[0];

View File

@@ -3,7 +3,9 @@ import type {
createGetResolvedJellyfinConfigHandler, createGetResolvedJellyfinConfigHandler,
} from './jellyfin-client-info'; } from './jellyfin-client-info';
type GetResolvedJellyfinConfigMainDeps = Parameters<typeof createGetResolvedJellyfinConfigHandler>[0]; type GetResolvedJellyfinConfigMainDeps = Parameters<
typeof createGetResolvedJellyfinConfigHandler
>[0];
type GetJellyfinClientInfoMainDeps = Parameters<typeof createGetJellyfinClientInfoHandler>[0]; type GetJellyfinClientInfoMainDeps = Parameters<typeof createGetJellyfinClientInfoHandler>[0];
export function createBuildGetResolvedJellyfinConfigMainDepsHandler( export function createBuildGetResolvedJellyfinConfigMainDepsHandler(

View File

@@ -8,7 +8,7 @@ import {
test('get resolved jellyfin config returns jellyfin section from resolved config', () => { test('get resolved jellyfin config returns jellyfin section from resolved config', () => {
const jellyfin = { url: 'https://jellyfin.local' } as never; const jellyfin = { url: 'https://jellyfin.local' } as never;
const getConfig = createGetResolvedJellyfinConfigHandler({ const getConfig = createGetResolvedJellyfinConfigHandler({
getResolvedConfig: () => ({ jellyfin } as never), getResolvedConfig: () => ({ jellyfin }) as never,
loadStoredSession: () => null, loadStoredSession: () => null,
getEnv: () => undefined, getEnv: () => undefined,
}); });
@@ -68,8 +68,7 @@ test('get resolved jellyfin config uses stored user id when env token set withou
}, },
}) as never, }) as never,
loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'stored-user' }), loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'stored-user' }),
getEnv: (key: string) => getEnv: (key: string) => (key === 'SUBMINER_JELLYFIN_ACCESS_TOKEN' ? 'env-token' : undefined),
key === 'SUBMINER_JELLYFIN_ACCESS_TOKEN' ? 'env-token' : undefined,
}); });
assert.deepEqual(getConfig(), { assert.deepEqual(getConfig(), {
@@ -81,7 +80,7 @@ test('get resolved jellyfin config uses stored user id when env token set withou
test('jellyfin client info resolves defaults when fields are missing', () => { test('jellyfin client info resolves defaults when fields are missing', () => {
const getClientInfo = createGetJellyfinClientInfoHandler({ const getClientInfo = createGetJellyfinClientInfoHandler({
getResolvedJellyfinConfig: () => ({ clientName: '', clientVersion: '', deviceId: '' } as never), getResolvedJellyfinConfig: () => ({ clientName: '', clientVersion: '', deviceId: '' }) as never,
getDefaultJellyfinConfig: () => getDefaultJellyfinConfig: () =>
({ ({
clientName: 'SubMiner', clientName: 'SubMiner',

View File

@@ -2,7 +2,9 @@ import type { createPlayJellyfinItemInMpvHandler } from './jellyfin-playback-lau
type PlayJellyfinItemInMpvMainDeps = Parameters<typeof createPlayJellyfinItemInMpvHandler>[0]; type PlayJellyfinItemInMpvMainDeps = Parameters<typeof createPlayJellyfinItemInMpvHandler>[0];
export function createBuildPlayJellyfinItemInMpvMainDepsHandler(deps: PlayJellyfinItemInMpvMainDeps) { export function createBuildPlayJellyfinItemInMpvMainDepsHandler(
deps: PlayJellyfinItemInMpvMainDeps,
) {
return (): PlayJellyfinItemInMpvMainDeps => ({ return (): PlayJellyfinItemInMpvMainDeps => ({
ensureMpvConnectedForPlayback: () => deps.ensureMpvConnectedForPlayback(), ensureMpvConnectedForPlayback: () => deps.ensureMpvConnectedForPlayback(),
getMpvClient: () => deps.getMpvClient(), getMpvClient: () => deps.getMpvClient(),

View File

@@ -136,6 +136,8 @@ test('createHandleJellyfinRemoteGeneralCommand mutates active playback indices',
assert.equal(playback.subtitleStreamIndex, null); assert.equal(playback.subtitleStreamIndex, null);
assert.ok(calls.includes('progress:true')); assert.ok(calls.includes('progress:true'));
assert.ok( assert.ok(
calls.some((entry) => entry.includes('Ignoring unsupported Jellyfin GeneralCommand: UnsupportedCommand')), calls.some((entry) =>
entry.includes('Ignoring unsupported Jellyfin GeneralCommand: UnsupportedCommand'),
),
); );
}); });

View File

@@ -44,7 +44,9 @@ export type JellyfinRemoteProgressReporterDeps = {
logDebug: (message: string, error: unknown) => void; logDebug: (message: string, error: unknown) => void;
}; };
export function createReportJellyfinRemoteProgressHandler(deps: JellyfinRemoteProgressReporterDeps) { export function createReportJellyfinRemoteProgressHandler(
deps: JellyfinRemoteProgressReporterDeps,
) {
return async (force = false): Promise<void> => { return async (force = false): Promise<void> => {
const playback = deps.getActivePlayback(); const playback = deps.getActivePlayback();
if (!playback) return; if (!playback) return;

View File

@@ -3,8 +3,12 @@ import type {
createStopJellyfinRemoteSessionHandler, createStopJellyfinRemoteSessionHandler,
} from './jellyfin-remote-session-lifecycle'; } from './jellyfin-remote-session-lifecycle';
type StartJellyfinRemoteSessionMainDeps = Parameters<typeof createStartJellyfinRemoteSessionHandler>[0]; type StartJellyfinRemoteSessionMainDeps = Parameters<
type StopJellyfinRemoteSessionMainDeps = Parameters<typeof createStopJellyfinRemoteSessionHandler>[0]; typeof createStartJellyfinRemoteSessionHandler
>[0];
type StopJellyfinRemoteSessionMainDeps = Parameters<
typeof createStopJellyfinRemoteSessionHandler
>[0];
export function createBuildStartJellyfinRemoteSessionMainDepsHandler( export function createBuildStartJellyfinRemoteSessionMainDepsHandler(
deps: StartJellyfinRemoteSessionMainDeps, deps: StartJellyfinRemoteSessionMainDeps,

View File

@@ -16,7 +16,11 @@ test('open jellyfin setup window main deps builder maps callbacks', async () =>
accessToken: 'token', accessToken: 'token',
userId: 'uid', userId: 'uid',
}), }),
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'dev' }), getJellyfinClientInfo: () => ({
clientName: 'SubMiner',
clientVersion: '1.0',
deviceId: 'dev',
}),
saveStoredSession: () => calls.push('save'), saveStoredSession: () => calls.push('save'),
patchJellyfinConfig: () => calls.push('patch'), patchJellyfinConfig: () => calls.push('patch'),
logInfo: (message) => calls.push(`info:${message}`), logInfo: (message) => calls.push(`info:${message}`),
@@ -38,12 +42,15 @@ test('open jellyfin setup window main deps builder maps callbacks', async () =>
username: 'u', username: 'u',
password: 'p', password: 'p',
}); });
assert.deepEqual(await deps.authenticateWithPassword('s', 'u', 'p', deps.getJellyfinClientInfo()), { assert.deepEqual(
await deps.authenticateWithPassword('s', 'u', 'p', deps.getJellyfinClientInfo()),
{
serverUrl: 'http://127.0.0.1:8096', serverUrl: 'http://127.0.0.1:8096',
username: 'alice', username: 'alice',
accessToken: 'token', accessToken: 'token',
userId: 'uid', userId: 'uid',
}); },
);
deps.saveStoredSession({ accessToken: 'token', userId: 'uid' }); deps.saveStoredSession({ accessToken: 'token', userId: 'uid' });
deps.patchJellyfinConfig({ deps.patchJellyfinConfig({
serverUrl: 'http://127.0.0.1:8096', serverUrl: 'http://127.0.0.1:8096',
@@ -57,5 +64,13 @@ test('open jellyfin setup window main deps builder maps callbacks', async () =>
deps.clearSetupWindow(); deps.clearSetupWindow();
deps.setSetupWindow({} as never); deps.setSetupWindow({} as never);
assert.equal(deps.encodeURIComponent('a b'), 'a%20b'); assert.equal(deps.encodeURIComponent('a b'), 'a%20b');
assert.deepEqual(calls, ['save', 'patch', 'info:ok', 'error:bad', 'osd:toast', 'clear', 'set-window']); assert.deepEqual(calls, [
'save',
'patch',
'info:ok',
'error:bad',
'osd:toast',
'clear',
'set-window',
]);
}); });

View File

@@ -50,7 +50,11 @@ test('createHandleJellyfinSetupSubmissionHandler applies successful login', asyn
accessToken: 'token', accessToken: 'token',
userId: 'uid', userId: 'uid',
}), }),
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }), getJellyfinClientInfo: () => ({
clientName: 'SubMiner',
clientVersion: '1.0',
deviceId: 'did',
}),
saveStoredSession: (session) => { saveStoredSession: (session) => {
savedSession = session; savedSession = session;
calls.push('save'); calls.push('save');
@@ -86,7 +90,11 @@ test('createHandleJellyfinSetupSubmissionHandler reports failure to OSD', async
authenticateWithPassword: async () => { authenticateWithPassword: async () => {
throw new Error('bad credentials'); throw new Error('bad credentials');
}, },
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }), getJellyfinClientInfo: () => ({
clientName: 'SubMiner',
clientVersion: '1.0',
deviceId: 'did',
}),
saveStoredSession: () => calls.push('save'), saveStoredSession: () => calls.push('save'),
patchJellyfinConfig: () => calls.push('patch'), patchJellyfinConfig: () => calls.push('patch'),
logInfo: () => calls.push('info'), logInfo: () => calls.push('info'),
@@ -180,7 +188,11 @@ test('createOpenJellyfinSetupWindowHandler no-ops when existing setup window is
authenticateWithPassword: async () => { authenticateWithPassword: async () => {
throw new Error('should not auth'); throw new Error('should not auth');
}, },
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }), getJellyfinClientInfo: () => ({
clientName: 'SubMiner',
clientVersion: '1.0',
deviceId: 'did',
}),
saveStoredSession: () => {}, saveStoredSession: () => {},
patchJellyfinConfig: () => {}, patchJellyfinConfig: () => {},
logInfo: () => {}, logInfo: () => {},
@@ -196,14 +208,18 @@ test('createOpenJellyfinSetupWindowHandler no-ops when existing setup window is
}); });
test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window lifecycle', async () => { test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window lifecycle', async () => {
let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null = null; let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null =
null;
let closedHandler: (() => void) | null = null; let closedHandler: (() => void) | null = null;
let prevented = false; let prevented = false;
const calls: string[] = []; const calls: string[] = [];
const fakeWindow = { const fakeWindow = {
focus: () => {}, focus: () => {},
webContents: { webContents: {
on: (event: 'will-navigate', handler: (event: { preventDefault: () => void }, url: string) => void) => { on: (
event: 'will-navigate',
handler: (event: { preventDefault: () => void }, url: string) => void,
) => {
if (event === 'will-navigate') { if (event === 'will-navigate') {
willNavigateHandler = handler; willNavigateHandler = handler;
} }
@@ -233,7 +249,11 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li
accessToken: 'token', accessToken: 'token',
userId: 'uid', userId: 'uid',
}), }),
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }), getJellyfinClientInfo: () => ({
clientName: 'SubMiner',
clientVersion: '1.0',
deviceId: 'did',
}),
saveStoredSession: () => calls.push('save'), saveStoredSession: () => calls.push('save'),
patchJellyfinConfig: () => calls.push('patch'), patchJellyfinConfig: () => calls.push('patch'),
logInfo: () => calls.push('info'), logInfo: () => calls.push('info'),
@@ -249,7 +269,9 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li
assert.ok(closedHandler); assert.ok(closedHandler);
assert.deepEqual(calls.slice(0, 2), ['load:data-url', 'set-window']); assert.deepEqual(calls.slice(0, 2), ['load:data-url', 'set-window']);
const navHandler = willNavigateHandler as ((event: { preventDefault: () => void }, url: string) => void) | null; const navHandler = willNavigateHandler as
| ((event: { preventDefault: () => void }, url: string) => void)
| null;
if (!navHandler) { if (!navHandler) {
throw new Error('missing will-navigate handler'); throw new Error('missing will-navigate handler');
} }

View File

@@ -109,7 +109,9 @@ export function parseJellyfinSetupSubmissionUrl(rawUrl: string): {
} }
export function createHandleJellyfinSetupSubmissionHandler(deps: { export function createHandleJellyfinSetupSubmissionHandler(deps: {
parseSubmissionUrl: (rawUrl: string) => { server: string; username: string; password: string } | null; parseSubmissionUrl: (
rawUrl: string,
) => { server: string; username: string; password: string } | null;
authenticateWithPassword: ( authenticateWithPassword: (
server: string, server: string,
username: string, username: string,
@@ -179,20 +181,22 @@ export function createHandleJellyfinSetupWindowClosedHandler(deps: {
}; };
} }
export function createHandleJellyfinSetupWindowOpenedHandler(deps: { export function createHandleJellyfinSetupWindowOpenedHandler(deps: { setSetupWindow: () => void }) {
setSetupWindow: () => void;
}) {
return (): void => { return (): void => {
deps.setSetupWindow(); deps.setSetupWindow();
}; };
} }
export function createOpenJellyfinSetupWindowHandler<TWindow extends JellyfinSetupWindowLike>(deps: { export function createOpenJellyfinSetupWindowHandler<
TWindow extends JellyfinSetupWindowLike,
>(deps: {
maybeFocusExistingSetupWindow: () => boolean; maybeFocusExistingSetupWindow: () => boolean;
createSetupWindow: () => TWindow; createSetupWindow: () => TWindow;
getResolvedJellyfinConfig: () => { serverUrl?: string | null; username?: string | null }; getResolvedJellyfinConfig: () => { serverUrl?: string | null; username?: string | null };
buildSetupFormHtml: (defaultServer: string, defaultUser: string) => string; buildSetupFormHtml: (defaultServer: string, defaultUser: string) => string;
parseSubmissionUrl: (rawUrl: string) => { server: string; username: string; password: string } | null; parseSubmissionUrl: (
rawUrl: string,
) => { server: string; username: string; password: string } | null;
authenticateWithPassword: ( authenticateWithPassword: (
server: string, server: string,
username: string, username: string,
@@ -258,9 +262,7 @@ export function createOpenJellyfinSetupWindowHandler<TWindow extends JellyfinSet
}, },
}); });
}); });
void setupWindow.loadURL( void setupWindow.loadURL(`data:text/html;charset=utf-8,${deps.encodeURIComponent(formHtml)}`);
`data:text/html;charset=utf-8,${deps.encodeURIComponent(formHtml)}`,
);
setupWindow.on('closed', () => { setupWindow.on('closed', () => {
handleWindowClosed(); handleWindowClosed();
}); });

View File

@@ -117,13 +117,7 @@ export function createPreloadJellyfinExternalSubtitlesHandler(deps: {
seenUrls.add(track.deliveryUrl); seenUrls.add(track.deliveryUrl);
const labelBase = (track.title || track.language || '').trim(); const labelBase = (track.title || track.language || '').trim();
const label = labelBase || `Jellyfin Subtitle ${track.index}`; const label = labelBase || `Jellyfin Subtitle ${track.index}`;
deps.sendMpvCommand([ deps.sendMpvCommand(['sub-add', track.deliveryUrl, 'cached', label, track.language || '']);
'sub-add',
track.deliveryUrl,
'cached',
label,
track.language || '',
]);
} }
await deps.wait(250); await deps.wait(250);

View File

@@ -39,8 +39,10 @@ export function createCopyCurrentSubtitleHandler<TSubtitleTimingTracker>(deps: {
}; };
} }
export function createHandleMineSentenceDigitHandler<TSubtitleTimingTracker, TAnkiIntegration>( export function createHandleMineSentenceDigitHandler<
deps: { TSubtitleTimingTracker,
TAnkiIntegration,
>(deps: {
getSubtitleTimingTracker: () => TSubtitleTimingTracker; getSubtitleTimingTracker: () => TSubtitleTimingTracker;
getAnkiIntegration: () => TAnkiIntegration; getAnkiIntegration: () => TAnkiIntegration;
getCurrentSecondarySubText: () => string | undefined; getCurrentSecondarySubText: () => string | undefined;
@@ -58,8 +60,7 @@ export function createHandleMineSentenceDigitHandler<TSubtitleTimingTracker, TAn
onCardsMined: (count: number) => void; onCardsMined: (count: number) => void;
}, },
) => void; ) => void;
}, }) {
) {
return (count: number): void => { return (count: number): void => {
deps.handleMineSentenceDigitCore(count, { deps.handleMineSentenceDigitCore(count, {
subtitleTimingTracker: deps.getSubtitleTimingTracker(), subtitleTimingTracker: deps.getSubtitleTimingTracker(),

View File

@@ -7,7 +7,10 @@ test('mpv runtime service main deps builder maps state and callbacks', () => {
let reconnectTimer: ReturnType<typeof setTimeout> | null = null; let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
class FakeClient { class FakeClient {
constructor(public socketPath: string, public options: unknown) {} constructor(
public socketPath: string,
public options: unknown,
) {}
} }
const build = createBuildMpvClientRuntimeServiceFactoryDepsHandler({ const build = createBuildMpvClientRuntimeServiceFactoryDepsHandler({

View File

@@ -22,7 +22,8 @@ export function createBuildMpvClientRuntimeServiceFactoryDepsHandler<
setOverlayVisible: (visible: boolean) => deps.setOverlayVisible(visible), setOverlayVisible: (visible: boolean) => deps.setOverlayVisible(visible),
isVisibleOverlayVisible: () => deps.isVisibleOverlayVisible(), isVisibleOverlayVisible: () => deps.isVisibleOverlayVisible(),
getReconnectTimer: () => deps.getReconnectTimer(), getReconnectTimer: () => deps.getReconnectTimer(),
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => deps.setReconnectTimer(timer), setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) =>
deps.setReconnectTimer(timer),
}, },
bindEventHandlers: (client: TClient) => deps.bindEventHandlers(client), bindEventHandlers: (client: TClient) => deps.bindEventHandlers(client),
}); });

View File

@@ -15,9 +15,7 @@ export function createBuildApplyJellyfinMpvDefaultsMainDepsHandler(
}); });
} }
export function createBuildGetDefaultSocketPathMainDepsHandler( export function createBuildGetDefaultSocketPathMainDepsHandler(deps: GetDefaultSocketPathMainDeps) {
deps: GetDefaultSocketPathMainDeps,
) {
return (): GetDefaultSocketPathMainDeps => ({ return (): GetDefaultSocketPathMainDeps => ({
platform: deps.platform, platform: deps.platform,
}); });

View File

@@ -97,8 +97,7 @@ export function createBindMpvMainEventHandlersHandler(deps: {
const handleMpvMediaPathChange = createHandleMpvMediaPathChangeHandler({ const handleMpvMediaPathChange = createHandleMpvMediaPathChangeHandler({
updateCurrentMediaPath: (path) => deps.updateCurrentMediaPath(path), updateCurrentMediaPath: (path) => deps.updateCurrentMediaPath(path),
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(), reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
restoreMpvSubVisibility: () => restoreMpvSubVisibility: () => deps.restoreMpvSubVisibility(),
deps.restoreMpvSubVisibility(),
getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(), getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(),
resetAnilistMediaTracking: (mediaKey) => deps.resetAnilistMediaTracking(mediaKey), resetAnilistMediaTracking: (mediaKey) => deps.resetAnilistMediaTracking(mediaKey),
maybeProbeAnilistDuration: (mediaKey) => deps.maybeProbeAnilistDuration(mediaKey), maybeProbeAnilistDuration: (mediaKey) => deps.maybeProbeAnilistDuration(mediaKey),

View File

@@ -1,6 +1,8 @@
import type { createUpdateMpvSubtitleRenderMetricsHandler } from './mpv-subtitle-render-metrics'; import type { createUpdateMpvSubtitleRenderMetricsHandler } from './mpv-subtitle-render-metrics';
type UpdateMpvSubtitleRenderMetricsMainDeps = Parameters<typeof createUpdateMpvSubtitleRenderMetricsHandler>[0]; type UpdateMpvSubtitleRenderMetricsMainDeps = Parameters<
typeof createUpdateMpvSubtitleRenderMetricsHandler
>[0];
export function createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler( export function createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler(
deps: UpdateMpvSubtitleRenderMetricsMainDeps, deps: UpdateMpvSubtitleRenderMetricsMainDeps,

View File

@@ -22,7 +22,10 @@ test('numeric shortcut runtime main deps builder maps callbacks', () => {
}, },
})(); })();
assert.equal(deps.globalShortcut.register('1', () => {}), true); assert.equal(
deps.globalShortcut.register('1', () => {}),
true,
);
deps.globalShortcut.unregister('1'); deps.globalShortcut.unregister('1');
deps.showMpvOsd('x'); deps.showMpvOsd('x');
deps.setTimer(() => calls.push('tick'), 1000); deps.setTimer(() => calls.push('tick'), 1000);

View File

@@ -1,6 +1,8 @@
import type { NumericShortcutRuntimeOptions } from '../../core/services/numeric-shortcut'; import type { NumericShortcutRuntimeOptions } from '../../core/services/numeric-shortcut';
export function createBuildNumericShortcutRuntimeMainDepsHandler(deps: NumericShortcutRuntimeOptions) { export function createBuildNumericShortcutRuntimeMainDepsHandler(
deps: NumericShortcutRuntimeOptions,
) {
return (): NumericShortcutRuntimeOptions => ({ return (): NumericShortcutRuntimeOptions => ({
globalShortcut: deps.globalShortcut, globalShortcut: deps.globalShortcut,
showMpvOsd: (text: string) => deps.showMpvOsd(text), showMpvOsd: (text: string) => deps.showMpvOsd(text),

View File

@@ -3,8 +3,12 @@ import type {
createStartNumericShortcutSessionHandler, createStartNumericShortcutSessionHandler,
} from './numeric-shortcut-session-handlers'; } from './numeric-shortcut-session-handlers';
type CancelNumericShortcutSessionMainDeps = Parameters<typeof createCancelNumericShortcutSessionHandler>[0]; type CancelNumericShortcutSessionMainDeps = Parameters<
type StartNumericShortcutSessionMainDeps = Parameters<typeof createStartNumericShortcutSessionHandler>[0]; typeof createCancelNumericShortcutSessionHandler
>[0];
type StartNumericShortcutSessionMainDeps = Parameters<
typeof createStartNumericShortcutSessionHandler
>[0];
export function createBuildCancelNumericShortcutSessionMainDepsHandler( export function createBuildCancelNumericShortcutSessionMainDepsHandler(
deps: CancelNumericShortcutSessionMainDeps, deps: CancelNumericShortcutSessionMainDeps,

View File

@@ -20,8 +20,9 @@ export function createNumericShortcutSessionRuntimeHandlers(deps: {
const cancelPendingMultiCopyMainDeps = createBuildCancelNumericShortcutSessionMainDepsHandler({ const cancelPendingMultiCopyMainDeps = createBuildCancelNumericShortcutSessionMainDepsHandler({
session: deps.multiCopySession, session: deps.multiCopySession,
})(); })();
const cancelPendingMultiCopyHandler = const cancelPendingMultiCopyHandler = createCancelNumericShortcutSessionHandler(
createCancelNumericShortcutSessionHandler(cancelPendingMultiCopyMainDeps); cancelPendingMultiCopyMainDeps,
);
const startPendingMultiCopyMainDeps = createBuildStartNumericShortcutSessionMainDepsHandler({ const startPendingMultiCopyMainDeps = createBuildStartNumericShortcutSessionMainDepsHandler({
session: deps.multiCopySession, session: deps.multiCopySession,
@@ -32,8 +33,9 @@ export function createNumericShortcutSessionRuntimeHandlers(deps: {
cancelled: 'Cancelled', cancelled: 'Cancelled',
}, },
})(); })();
const startPendingMultiCopyHandler = const startPendingMultiCopyHandler = createStartNumericShortcutSessionHandler(
createStartNumericShortcutSessionHandler(startPendingMultiCopyMainDeps); startPendingMultiCopyMainDeps,
);
const cancelPendingMineSentenceMultipleMainDeps = const cancelPendingMineSentenceMultipleMainDeps =
createBuildCancelNumericShortcutSessionMainDepsHandler({ createBuildCancelNumericShortcutSessionMainDepsHandler({

View File

@@ -14,9 +14,7 @@ export function createBuildOverlayContentMeasurementStoreMainDepsHandler(
}); });
} }
export function createBuildOverlayModalRuntimeMainDepsHandler( export function createBuildOverlayModalRuntimeMainDepsHandler(deps: OverlayWindowResolver) {
deps: OverlayWindowResolver,
) {
return (): OverlayWindowResolver => ({ return (): OverlayWindowResolver => ({
getMainWindow: () => deps.getMainWindow(), getMainWindow: () => deps.getMainWindow(),
getModalWindow: () => deps.getModalWindow(), getModalWindow: () => deps.getModalWindow(),

View File

@@ -35,12 +35,15 @@ test('overlay main action main deps builders map callbacks', () => {
showMpvOsd: (text) => calls.push(`osd:${text}`), showMpvOsd: (text) => calls.push(`osd:${text}`),
sendMpvCommand: (command) => calls.push(`cmd:${command.join(':')}`), sendMpvCommand: (command) => calls.push(`cmd:${command.join(':')}`),
})(); })();
assert.deepEqual(append.appendClipboardVideoToQueueRuntime({ assert.deepEqual(
append.appendClipboardVideoToQueueRuntime({
getMpvClient: () => ({ connected: true }), getMpvClient: () => ({ connected: true }),
readClipboardText: () => '/tmp/v.mkv', readClipboardText: () => '/tmp/v.mkv',
showMpvOsd: () => {}, showMpvOsd: () => {},
sendMpvCommand: () => {}, sendMpvCommand: () => {},
}), { ok: true, message: 'ok' }); }),
{ ok: true, message: 'ok' },
);
assert.equal(append.readClipboardText(), '/tmp/v.mkv'); assert.equal(append.readClipboardText(), '/tmp/v.mkv');
assert.equal(typeof append.getMpvClient(), 'object'); assert.equal(typeof append.getMpvClient(), 'object');
append.showMpvOsd('queued'); append.showMpvOsd('queued');

View File

@@ -8,7 +8,9 @@ import type {
type SetOverlayVisibleMainDeps = Parameters<typeof createSetOverlayVisibleHandler>[0]; type SetOverlayVisibleMainDeps = Parameters<typeof createSetOverlayVisibleHandler>[0];
type ToggleOverlayMainDeps = Parameters<typeof createToggleOverlayHandler>[0]; type ToggleOverlayMainDeps = Parameters<typeof createToggleOverlayHandler>[0];
type HandleOverlayModalClosedMainDeps = Parameters<typeof createHandleOverlayModalClosedHandler>[0]; type HandleOverlayModalClosedMainDeps = Parameters<typeof createHandleOverlayModalClosedHandler>[0];
type AppendClipboardVideoToQueueMainDeps = Parameters<typeof createAppendClipboardVideoToQueueHandler>[0]; type AppendClipboardVideoToQueueMainDeps = Parameters<
typeof createAppendClipboardVideoToQueueHandler
>[0];
export function createBuildSetOverlayVisibleMainDepsHandler(deps: SetOverlayVisibleMainDeps) { export function createBuildSetOverlayVisibleMainDepsHandler(deps: SetOverlayVisibleMainDeps) {
return (): SetOverlayVisibleMainDeps => ({ return (): SetOverlayVisibleMainDeps => ({
@@ -34,7 +36,8 @@ export function createBuildAppendClipboardVideoToQueueMainDepsHandler(
deps: AppendClipboardVideoToQueueMainDeps, deps: AppendClipboardVideoToQueueMainDeps,
) { ) {
return (): AppendClipboardVideoToQueueMainDeps => ({ return (): AppendClipboardVideoToQueueMainDeps => ({
appendClipboardVideoToQueueRuntime: (options) => deps.appendClipboardVideoToQueueRuntime(options), appendClipboardVideoToQueueRuntime: (options) =>
deps.appendClipboardVideoToQueueRuntime(options),
getMpvClient: () => deps.getMpvClient(), getMpvClient: () => deps.getMpvClient(),
readClipboardText: () => deps.readClipboardText(), readClipboardText: () => deps.readClipboardText(),
showMpvOsd: (text: string) => deps.showMpvOsd(text), showMpvOsd: (text: string) => deps.showMpvOsd(text),

View File

@@ -9,9 +9,7 @@ export function createSetOverlayVisibleHandler(deps: {
}; };
} }
export function createToggleOverlayHandler(deps: { export function createToggleOverlayHandler(deps: { toggleVisibleOverlay: () => void }) {
toggleVisibleOverlay: () => void;
}) {
return (): void => { return (): void => {
deps.toggleVisibleOverlay(); deps.toggleVisibleOverlay();
}; };
@@ -26,9 +24,10 @@ export function createHandleOverlayModalClosedHandler(deps: {
} }
export function createAppendClipboardVideoToQueueHandler(deps: { export function createAppendClipboardVideoToQueueHandler(deps: {
appendClipboardVideoToQueueRuntime: ( appendClipboardVideoToQueueRuntime: (options: AppendClipboardVideoToQueueRuntimeDeps) => {
options: AppendClipboardVideoToQueueRuntimeDeps, ok: boolean;
) => { ok: boolean; message: string }; message: string;
};
getMpvClient: () => AppendClipboardVideoToQueueRuntimeDeps['getMpvClient'] extends () => infer T getMpvClient: () => AppendClipboardVideoToQueueRuntimeDeps['getMpvClient'] extends () => infer T
? T ? T
: never; : never;

View File

@@ -25,7 +25,6 @@ function parseSubVisibility(value: unknown): boolean {
return true; return true;
} }
export function createEnsureOverlayMpvSubtitlesHiddenHandler(deps: { export function createEnsureOverlayMpvSubtitlesHiddenHandler(deps: {
getMpvClient: () => MpvVisibilityClient | null; getMpvClient: () => MpvVisibilityClient | null;
getSavedSubVisibility: () => boolean | null; getSavedSubVisibility: () => boolean | null;

Some files were not shown because too many files have changed in this diff Show More