From c31e55398d99b5a15322891e07faacc3bffe91ad Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 3 Apr 2026 14:04:07 -0700 Subject: [PATCH] Run prettier across source files --- launcher/config/cli-parser-builder.ts | 5 +- launcher/mpv.test.ts | 289 ++++++++++-------- launcher/mpv.ts | 8 +- scripts/run-coverage-lane.ts | 21 +- src/anki-integration.ts | 8 +- src/anki-integration/anki-connect-proxy.ts | 17 +- src/anki-integration/card-creation.ts | 14 +- src/anki-integration/duplicate.test.ts | 34 ++- src/anki-integration/duplicate.ts | 8 +- src/config/definitions.ts | 13 +- src/config/definitions/template-sections.ts | 4 +- src/core/services/discord-presence.ts | 13 +- .../services/immersion-tracker-service.ts | 30 +- .../immersion-tracker/__tests__/query.test.ts | 117 ++++--- .../services/immersion-tracker/lifetime.ts | 11 +- .../immersion-tracker/maintenance.test.ts | 6 +- .../services/immersion-tracker/maintenance.ts | 13 +- .../immersion-tracker/query-lexical.ts | 8 +- .../immersion-tracker/query-maintenance.ts | 4 +- .../immersion-tracker/query-sessions.ts | 28 +- .../immersion-tracker/query-shared.ts | 23 +- .../immersion-tracker/query-trends.ts | 27 +- src/core/services/mpv-protocol.ts | 16 +- src/core/services/stats-server.ts | 13 +- src/core/services/subsync.ts | 6 +- .../tokenizer/yomitan-parser-runtime.ts | 3 +- .../services/youtube/metadata-probe.test.ts | 7 +- .../services/youtube/playback-resolve.test.ts | 7 +- src/main-entry-runtime.test.ts | 7 +- src/main.ts | 43 +-- src/main/boot/services.test.ts | 22 +- src/main/boot/services.ts | 13 +- .../character-dictionary-runtime/zip.test.ts | 15 +- src/main/character-dictionary-runtime/zip.ts | 6 +- src/main/runtime/autoplay-ready-gate.test.ts | 28 +- src/main/runtime/autoplay-ready-gate.ts | 9 +- .../composers/anilist-setup-composer.test.ts | 4 +- .../composers/cli-startup-composer.test.ts | 9 +- .../runtime/composers/cli-startup-composer.ts | 4 +- .../composers/headless-startup-composer.ts | 36 +-- .../jellyfin-remote-composer.test.ts | 8 +- .../composers/mpv-runtime-composer.test.ts | 5 +- .../overlay-visibility-runtime-composer.ts | 3 +- src/main/runtime/discord-rpc-client.ts | 12 +- .../runtime/first-run-setup-plugin.test.ts | 5 +- src/main/runtime/first-run-setup-plugin.ts | 5 +- .../runtime/first-run-setup-window.test.ts | 10 +- src/main/runtime/local-subtitle-selection.ts | 18 +- src/main/runtime/playlist-browser-ipc.ts | 3 +- .../runtime/playlist-browser-runtime.test.ts | 32 +- src/main/runtime/playlist-browser-runtime.ts | 5 +- src/main/runtime/windows-mpv-launch.ts | 9 +- src/main/runtime/youtube-playback-runtime.ts | 4 +- src/renderer/handlers/keyboard.ts | 10 +- .../modals/playlist-browser-renderer.ts | 7 +- src/renderer/modals/playlist-browser.test.ts | 26 +- src/renderer/modals/playlist-browser.ts | 8 +- src/renderer/style.css | 11 +- src/renderer/utils/dom.ts | 8 +- src/runtime-options.test.ts | 11 +- 60 files changed, 615 insertions(+), 534 deletions(-) diff --git a/launcher/config/cli-parser-builder.ts b/launcher/config/cli-parser-builder.ts index 6993e9bc..eb6de93f 100644 --- a/launcher/config/cli-parser-builder.ts +++ b/launcher/config/cli-parser-builder.ts @@ -43,7 +43,10 @@ export interface CliInvocations { function applyRootOptions(program: Command): void { program - .option('-b, --backend ', 'Display backend (auto, hyprland, sway, x11, macos, windows)') + .option( + '-b, --backend ', + 'Display backend (auto, hyprland, sway, x11, macos, windows)', + ) .option('-d, --directory ', 'Directory to browse') .option('-a, --args ', 'Pass arguments to MPV') .option('-r, --recursive', 'Search directories recursively') diff --git a/launcher/mpv.test.ts b/launcher/mpv.test.ts index 8f8eb983..9ca35aaa 100644 --- a/launcher/mpv.test.ts +++ b/launcher/mpv.test.ts @@ -457,7 +457,9 @@ function withFindAppBinaryPlatformSandbox( const originalPlatform = process.platform; try { Object.defineProperty(process, 'platform', { value: platform, configurable: true }); - withFindAppBinaryEnvSandbox(() => run(platform === 'win32' ? (path.win32 as typeof path) : path)); + withFindAppBinaryEnvSandbox(() => + run(platform === 'win32' ? (path.win32 as typeof path) : path), + ); } finally { Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }); } @@ -495,110 +497,126 @@ function withRealpathSyncStub(resolvePath: (filePath: string) => string, run: () } } -test('findAppBinary resolves ~/.local/bin/SubMiner.AppImage when it exists', { concurrency: false }, () => { - const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-home-')); - const originalHomedir = os.homedir; - try { - os.homedir = () => baseDir; - const appImage = path.join(baseDir, '.local/bin/SubMiner.AppImage'); - makeExecutable(appImage); +test( + 'findAppBinary resolves ~/.local/bin/SubMiner.AppImage when it exists', + { concurrency: false }, + () => { + const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-home-')); + const originalHomedir = os.homedir; + try { + os.homedir = () => baseDir; + const appImage = path.join(baseDir, '.local/bin/SubMiner.AppImage'); + makeExecutable(appImage); - withFindAppBinaryPlatformSandbox('linux', (pathModule) => { - const result = findAppBinary('/some/other/path/subminer', pathModule); - assert.equal(result, appImage); - }); - } finally { - os.homedir = originalHomedir; - fs.rmSync(baseDir, { recursive: true, force: true }); - } -}); + withFindAppBinaryPlatformSandbox('linux', (pathModule) => { + const result = findAppBinary('/some/other/path/subminer', pathModule); + assert.equal(result, appImage); + }); + } finally { + os.homedir = originalHomedir; + fs.rmSync(baseDir, { recursive: true, force: true }); + } + }, +); -test('findAppBinary resolves /opt/SubMiner/SubMiner.AppImage when ~/.local/bin candidate does not exist', { concurrency: false }, () => { - const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-home-')); - const originalHomedir = os.homedir; - try { - os.homedir = () => baseDir; - withFindAppBinaryPlatformSandbox('linux', (pathModule) => { - withAccessSyncStub( - (filePath) => filePath === '/opt/SubMiner/SubMiner.AppImage', - () => { - const result = findAppBinary('/some/other/path/subminer', pathModule); - assert.equal(result, '/opt/SubMiner/SubMiner.AppImage'); - }, - ); - }); - } finally { - os.homedir = originalHomedir; - fs.rmSync(baseDir, { recursive: true, force: true }); - } -}); +test( + 'findAppBinary resolves /opt/SubMiner/SubMiner.AppImage when ~/.local/bin candidate does not exist', + { concurrency: false }, + () => { + const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-home-')); + const originalHomedir = os.homedir; + try { + os.homedir = () => baseDir; + withFindAppBinaryPlatformSandbox('linux', (pathModule) => { + withAccessSyncStub( + (filePath) => filePath === '/opt/SubMiner/SubMiner.AppImage', + () => { + const result = findAppBinary('/some/other/path/subminer', pathModule); + assert.equal(result, '/opt/SubMiner/SubMiner.AppImage'); + }, + ); + }); + } finally { + os.homedir = originalHomedir; + fs.rmSync(baseDir, { recursive: true, force: true }); + } + }, +); -test('findAppBinary finds subminer on PATH when AppImage candidates do not exist', { concurrency: false }, () => { - const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-path-')); - const originalHomedir = os.homedir; - const originalPath = process.env.PATH; - try { - os.homedir = () => baseDir; - // No AppImage candidates in empty home dir; place subminer wrapper on PATH - const binDir = path.join(baseDir, 'bin'); - const wrapperPath = path.join(binDir, 'subminer'); - makeExecutable(wrapperPath); - process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ''}`; +test( + 'findAppBinary finds subminer on PATH when AppImage candidates do not exist', + { concurrency: false }, + () => { + const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-path-')); + const originalHomedir = os.homedir; + const originalPath = process.env.PATH; + try { + os.homedir = () => baseDir; + // No AppImage candidates in empty home dir; place subminer wrapper on PATH + const binDir = path.join(baseDir, 'bin'); + const wrapperPath = path.join(binDir, 'subminer'); + makeExecutable(wrapperPath); + process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ''}`; - withFindAppBinaryPlatformSandbox('linux', (pathModule) => { - withAccessSyncStub( - (filePath) => filePath === wrapperPath, - () => { - // selfPath must differ from wrapperPath so the self-check does not exclude it - const result = findAppBinary(path.join(baseDir, 'launcher', 'subminer'), pathModule); - assert.equal(result, wrapperPath); - }, - ); - }); - } finally { - os.homedir = originalHomedir; - process.env.PATH = originalPath; - fs.rmSync(baseDir, { recursive: true, force: true }); - } -}); + withFindAppBinaryPlatformSandbox('linux', (pathModule) => { + withAccessSyncStub( + (filePath) => filePath === wrapperPath, + () => { + // selfPath must differ from wrapperPath so the self-check does not exclude it + const result = findAppBinary(path.join(baseDir, 'launcher', 'subminer'), pathModule); + assert.equal(result, wrapperPath); + }, + ); + }); + } finally { + os.homedir = originalHomedir; + process.env.PATH = originalPath; + fs.rmSync(baseDir, { recursive: true, force: true }); + } + }, +); -test('findAppBinary excludes PATH matches that canonicalize to the launcher path', { concurrency: false }, () => { - const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-realpath-')); - const originalHomedir = os.homedir; - const originalPath = process.env.PATH; - try { - os.homedir = () => baseDir; - const binDir = path.join(baseDir, 'bin'); - const wrapperPath = path.join(binDir, 'subminer'); - const canonicalPath = path.join(baseDir, 'launch', 'subminer'); - makeExecutable(wrapperPath); - process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ''}`; +test( + 'findAppBinary excludes PATH matches that canonicalize to the launcher path', + { concurrency: false }, + () => { + const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-realpath-')); + const originalHomedir = os.homedir; + const originalPath = process.env.PATH; + try { + os.homedir = () => baseDir; + const binDir = path.join(baseDir, 'bin'); + const wrapperPath = path.join(binDir, 'subminer'); + const canonicalPath = path.join(baseDir, 'launch', 'subminer'); + makeExecutable(wrapperPath); + process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ''}`; - withFindAppBinaryPlatformSandbox('linux', (pathModule) => { - withAccessSyncStub( - (filePath) => filePath === wrapperPath, - () => { - withRealpathSyncStub( - (filePath) => { - if (filePath === canonicalPath || filePath === wrapperPath) { - return canonicalPath; - } - return filePath; - }, - () => { - const result = findAppBinary(canonicalPath, pathModule); - assert.equal(result, null); - }, - ); - }, - ); - }); - } finally { - os.homedir = originalHomedir; - process.env.PATH = originalPath; - fs.rmSync(baseDir, { recursive: true, force: true }); - } -}); + withFindAppBinaryPlatformSandbox('linux', (pathModule) => { + withAccessSyncStub( + (filePath) => filePath === wrapperPath, + () => { + withRealpathSyncStub( + (filePath) => { + if (filePath === canonicalPath || filePath === wrapperPath) { + return canonicalPath; + } + return filePath; + }, + () => { + const result = findAppBinary(canonicalPath, pathModule); + assert.equal(result, null); + }, + ); + }, + ); + }); + } finally { + os.homedir = originalHomedir; + process.env.PATH = originalPath; + fs.rmSync(baseDir, { recursive: true, force: true }); + } + }, +); test('findAppBinary resolves Windows install paths when present', { concurrency: false }, () => { const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-win-')); @@ -620,7 +638,10 @@ test('findAppBinary resolves Windows install paths when present', { concurrency: withAccessSyncStub( (filePath) => filePath === appExe, () => { - const result = findAppBinary(pathModule.join(baseDir, 'launcher', 'SubMiner.exe'), pathModule); + const result = findAppBinary( + pathModule.join(baseDir, 'launcher', 'SubMiner.exe'), + pathModule, + ); assert.equal(result, appExe); }, ); @@ -651,7 +672,10 @@ test('findAppBinary resolves SubMiner.exe on PATH on Windows', { concurrency: fa withAccessSyncStub( (filePath) => filePath === wrapperPath, () => { - const result = findAppBinary(pathModule.join(baseDir, 'launcher', 'SubMiner.exe'), pathModule); + const result = findAppBinary( + pathModule.join(baseDir, 'launcher', 'SubMiner.exe'), + pathModule, + ); assert.equal(result, wrapperPath); }, ); @@ -663,34 +687,41 @@ test('findAppBinary resolves SubMiner.exe on PATH on Windows', { concurrency: fa } }); -test('findAppBinary resolves a Windows install directory to SubMiner.exe', { concurrency: false }, () => { - const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-win-dir-')); - const originalHomedir = os.homedir; - const originalSubminerBinaryPath = process.env.SUBMINER_BINARY_PATH; - try { - os.homedir = () => baseDir; - const installDir = path.win32.join(baseDir, 'Programs', 'SubMiner'); - const appExe = path.win32.join(installDir, 'SubMiner.exe'); - process.env.SUBMINER_BINARY_PATH = installDir; - fs.mkdirSync(installDir, { recursive: true }); - fs.writeFileSync(appExe, '#!/bin/sh\nexit 0\n'); - fs.chmodSync(appExe, 0o755); - - const originalPlatform = process.platform; +test( + 'findAppBinary resolves a Windows install directory to SubMiner.exe', + { concurrency: false }, + () => { + const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-win-dir-')); + const originalHomedir = os.homedir; + const originalSubminerBinaryPath = process.env.SUBMINER_BINARY_PATH; try { - Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); - const result = findAppBinary(path.win32.join(baseDir, 'launcher', 'SubMiner.exe'), path.win32); - assert.equal(result, appExe); + os.homedir = () => baseDir; + const installDir = path.win32.join(baseDir, 'Programs', 'SubMiner'); + const appExe = path.win32.join(installDir, 'SubMiner.exe'); + process.env.SUBMINER_BINARY_PATH = installDir; + fs.mkdirSync(installDir, { recursive: true }); + fs.writeFileSync(appExe, '#!/bin/sh\nexit 0\n'); + fs.chmodSync(appExe, 0o755); + + const originalPlatform = process.platform; + try { + Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); + const result = findAppBinary( + path.win32.join(baseDir, 'launcher', 'SubMiner.exe'), + path.win32, + ); + assert.equal(result, appExe); + } finally { + Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }); + } } finally { - Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }); + os.homedir = originalHomedir; + if (originalSubminerBinaryPath === undefined) { + delete process.env.SUBMINER_BINARY_PATH; + } else { + process.env.SUBMINER_BINARY_PATH = originalSubminerBinaryPath; + } + fs.rmSync(baseDir, { recursive: true, force: true }); } - } finally { - os.homedir = originalHomedir; - if (originalSubminerBinaryPath === undefined) { - delete process.env.SUBMINER_BINARY_PATH; - } else { - process.env.SUBMINER_BINARY_PATH = originalSubminerBinaryPath; - } - fs.rmSync(baseDir, { recursive: true, force: true }); - } -}); + }, +); diff --git a/launcher/mpv.ts b/launcher/mpv.ts index 1d7cfe9c..a366b9f5 100644 --- a/launcher/mpv.ts +++ b/launcher/mpv.ts @@ -364,8 +364,12 @@ export function findAppBinary(selfPath: string, pathModule: PathModule = path): } else if (process.platform === 'darwin') { candidates.push('/Applications/SubMiner.app/Contents/MacOS/SubMiner'); candidates.push('/Applications/SubMiner.app/Contents/MacOS/subminer'); - candidates.push(pathModule.join(os.homedir(), 'Applications/SubMiner.app/Contents/MacOS/SubMiner')); - candidates.push(pathModule.join(os.homedir(), 'Applications/SubMiner.app/Contents/MacOS/subminer')); + candidates.push( + pathModule.join(os.homedir(), 'Applications/SubMiner.app/Contents/MacOS/SubMiner'), + ); + candidates.push( + pathModule.join(os.homedir(), 'Applications/SubMiner.app/Contents/MacOS/subminer'), + ); } else { candidates.push(pathModule.join(os.homedir(), '.local/bin/SubMiner.AppImage')); candidates.push('/opt/SubMiner/SubMiner.AppImage'); diff --git a/scripts/run-coverage-lane.ts b/scripts/run-coverage-lane.ts index 964962e5..b333e495 100644 --- a/scripts/run-coverage-lane.ts +++ b/scripts/run-coverage-lane.ts @@ -38,7 +38,11 @@ const lanes: Record = { }, }; -function collectFiles(rootDir: string, includeSuffixes: string[], excludeSet: Set): string[] { +function collectFiles( + rootDir: string, + includeSuffixes: string[], + excludeSet: Set, +): string[] { const out: string[] = []; const visit = (currentDir: string) => { for (const entry of readdirSync(currentDir, { withFileTypes: true })) { @@ -145,7 +149,12 @@ function parseLcovReport(report: string): LcovRecord[] { } if (line.startsWith('BRDA:')) { const [lineNumber, block, branch, hits] = line.slice(5).split(','); - if (lineNumber === undefined || block === undefined || branch === undefined || hits === undefined) { + if ( + lineNumber === undefined || + block === undefined || + branch === undefined || + hits === undefined + ) { continue; } ensureCurrent().branches.set(`${lineNumber}:${block}:${branch}`, { @@ -224,7 +233,9 @@ export function mergeLcovReports(reports: string[]): string { chunks.push(`FNDA:${record.functionHits.get(name) ?? 0},${name}`); } chunks.push(`FNF:${functions.length}`); - chunks.push(`FNH:${functions.filter(([name]) => (record.functionHits.get(name) ?? 0) > 0).length}`); + chunks.push( + `FNH:${functions.filter(([name]) => (record.functionHits.get(name) ?? 0) > 0).length}`, + ); const branches = [...record.branches.values()].sort((a, b) => a.line === b.line @@ -298,7 +309,9 @@ function runCoverageLane(): number { } writeFileSync(join(coverageDir, 'lcov.info'), mergeLcovReports(reports), 'utf8'); - process.stdout.write(`Merged LCOV written to ${relative(repoRoot, join(coverageDir, 'lcov.info'))}\n`); + process.stdout.write( + `Merged LCOV written to ${relative(repoRoot, join(coverageDir, 'lcov.info'))}\n`, + ); return 0; } finally { rmSync(shardRoot, { recursive: true, force: true }); diff --git a/src/anki-integration.ts b/src/anki-integration.ts index 74157e16..01b2ab17 100644 --- a/src/anki-integration.ts +++ b/src/anki-integration.ts @@ -369,7 +369,8 @@ export class AnkiIntegration { trackLastAddedDuplicateNoteIds: (noteId, duplicateNoteIds) => { this.trackedDuplicateNoteIds.set(noteId, [...duplicateNoteIds]); }, - findDuplicateNoteIds: (expression, noteInfo) => this.findDuplicateNoteIds(expression, noteInfo), + findDuplicateNoteIds: (expression, noteInfo) => + this.findDuplicateNoteIds(expression, noteInfo), recordCardsMinedCallback: (count, noteIds) => { this.recordCardsMinedSafely(count, noteIds, 'card creation'); }, @@ -1082,10 +1083,7 @@ export class AnkiIntegration { }); } - private async findDuplicateNoteIds( - expression: string, - noteInfo: NoteInfo, - ): Promise { + private async findDuplicateNoteIds(expression: string, noteInfo: NoteInfo): Promise { return findDuplicateNoteIdsForAnkiIntegration(expression, -1, noteInfo, { findNotes: async (query, options) => (await this.client.findNotes(query, options)) as unknown, notesInfo: async (noteIds) => (await this.client.notesInfo(noteIds)) as unknown, diff --git a/src/anki-integration/anki-connect-proxy.ts b/src/anki-integration/anki-connect-proxy.ts index 3c5fb44d..9a8be34a 100644 --- a/src/anki-integration/anki-connect-proxy.ts +++ b/src/anki-integration/anki-connect-proxy.ts @@ -162,7 +162,8 @@ export class AnkiConnectProxyServer { } try { - const forwardedBody = req.method === 'POST' ? this.getForwardRequestBody(rawBody, requestJson) : rawBody; + const forwardedBody = + req.method === 'POST' ? this.getForwardRequestBody(rawBody, requestJson) : rawBody; const targetUrl = new URL(req.url || '/', upstreamUrl).toString(); const contentType = typeof req.headers['content-type'] === 'string' @@ -272,7 +273,9 @@ export class AnkiConnectProxyServer { private sanitizeRequestJson(requestJson: Record): Record { const action = - typeof requestJson.action === 'string' ? requestJson.action : String(requestJson.action ?? ''); + typeof requestJson.action === 'string' + ? requestJson.action + : String(requestJson.action ?? ''); if (action !== 'addNote') { return requestJson; } @@ -301,9 +304,13 @@ export class AnkiConnectProxyServer { const rawNoteIds = Array.isArray(params?.subminerDuplicateNoteIds) ? params.subminerDuplicateNoteIds : []; - return [...new Set(rawNoteIds.filter((entry): entry is number => { - return typeof entry === 'number' && Number.isInteger(entry) && entry > 0; - }))].sort((left, right) => left - right); + return [ + ...new Set( + rawNoteIds.filter((entry): entry is number => { + return typeof entry === 'number' && Number.isInteger(entry) && entry > 0; + }), + ), + ].sort((left, right) => left - right); } private requestIncludesAddAction(action: string, requestJson: Record): boolean { diff --git a/src/anki-integration/card-creation.ts b/src/anki-integration/card-creation.ts index 36d7a582..375a6773 100644 --- a/src/anki-integration/card-creation.ts +++ b/src/anki-integration/card-creation.ts @@ -113,10 +113,7 @@ interface CardCreationDeps { setUpdateInProgress: (value: boolean) => void; trackLastAddedNoteId?: (noteId: number) => void; trackLastAddedDuplicateNoteIds?: (noteId: number, duplicateNoteIds: number[]) => void; - findDuplicateNoteIds?: ( - expression: string, - noteInfo: CardCreationNoteInfo, - ) => Promise; + findDuplicateNoteIds?: (expression: string, noteInfo: CardCreationNoteInfo) => Promise; recordCardsMinedCallback?: (count: number, noteIds?: number[]) => void; } @@ -573,10 +570,7 @@ export class CardCreationService { await this.deps.findDuplicateNoteIds(pendingExpressionText, pendingNoteInfo), ); } catch (error) { - log.warn( - 'Failed to capture pre-add duplicate note ids:', - (error as Error).message, - ); + log.warn('Failed to capture pre-add duplicate note ids:', (error as Error).message); } } @@ -728,9 +722,7 @@ export class CardCreationService { private createPendingNoteInfo(fields: Record): CardCreationNoteInfo { return { noteId: -1, - fields: Object.fromEntries( - Object.entries(fields).map(([name, value]) => [name, { value }]), - ), + fields: Object.fromEntries(Object.entries(fields).map(([name, value]) => [name, { value }])), }; } diff --git a/src/anki-integration/duplicate.test.ts b/src/anki-integration/duplicate.test.ts index 19d58646..b21e3d4e 100644 --- a/src/anki-integration/duplicate.test.ts +++ b/src/anki-integration/duplicate.test.ts @@ -307,21 +307,27 @@ test('findDuplicateNoteIds returns no matches when maxMatches is zero', async () }; let notesInfoCalls = 0; - const duplicateIds = await findDuplicateNoteIds('貴様', 100, currentNote, { - findNotes: async () => [200], - notesInfo: async (noteIds) => { - notesInfoCalls += 1; - return noteIds.map((noteId) => ({ - noteId, - fields: { - Expression: { value: '貴様' }, - }, - })); + const duplicateIds = await findDuplicateNoteIds( + '貴様', + 100, + currentNote, + { + findNotes: async () => [200], + notesInfo: async (noteIds) => { + notesInfoCalls += 1; + return noteIds.map((noteId) => ({ + noteId, + fields: { + Expression: { value: '貴様' }, + }, + })); + }, + getDeck: () => 'Japanese::Mining', + resolveFieldName: (noteInfo, preferredName) => createFieldResolver(noteInfo, preferredName), + logWarn: () => {}, }, - getDeck: () => 'Japanese::Mining', - resolveFieldName: (noteInfo, preferredName) => createFieldResolver(noteInfo, preferredName), - logWarn: () => {}, - }, 0); + 0, + ); assert.deepEqual(duplicateIds, []); assert.equal(notesInfoCalls, 0); diff --git a/src/anki-integration/duplicate.ts b/src/anki-integration/duplicate.ts index d3cc70d4..48c12589 100644 --- a/src/anki-integration/duplicate.ts +++ b/src/anki-integration/duplicate.ts @@ -24,13 +24,7 @@ export async function findDuplicateNote( noteInfo: NoteInfo, deps: DuplicateDetectionDeps, ): Promise { - const duplicateNoteIds = await findDuplicateNoteIds( - expression, - excludeNoteId, - noteInfo, - deps, - 1, - ); + const duplicateNoteIds = await findDuplicateNoteIds(expression, excludeNoteId, noteInfo, deps, 1); return duplicateNoteIds[0] ?? null; } diff --git a/src/config/definitions.ts b/src/config/definitions.ts index 77949b05..e6000ebb 100644 --- a/src/config/definitions.ts +++ b/src/config/definitions.ts @@ -35,17 +35,8 @@ const { startupWarmups, auto_start_overlay, } = CORE_DEFAULT_CONFIG; -const { - ankiConnect, - jimaku, - anilist, - mpv, - yomitan, - jellyfin, - discordPresence, - ai, - youtubeSubgen, -} = INTEGRATIONS_DEFAULT_CONFIG; +const { ankiConnect, jimaku, anilist, mpv, yomitan, jellyfin, discordPresence, ai, youtubeSubgen } = + INTEGRATIONS_DEFAULT_CONFIG; const { subtitleStyle, subtitleSidebar } = SUBTITLE_DEFAULT_CONFIG; const { immersionTracking } = IMMERSION_DEFAULT_CONFIG; const { stats } = STATS_DEFAULT_CONFIG; diff --git a/src/config/definitions/template-sections.ts b/src/config/definitions/template-sections.ts index 43d9992b..4132c0e8 100644 --- a/src/config/definitions/template-sections.ts +++ b/src/config/definitions/template-sections.ts @@ -131,7 +131,9 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [ }, { title: 'YouTube Playback Settings', - description: ['Defaults for managed subtitle language preferences and YouTube subtitle loading.'], + description: [ + 'Defaults for managed subtitle language preferences and YouTube subtitle loading.', + ], key: 'youtube', }, { diff --git a/src/core/services/discord-presence.ts b/src/core/services/discord-presence.ts index 420a8e99..9b211a4f 100644 --- a/src/core/services/discord-presence.ts +++ b/src/core/services/discord-presence.ts @@ -83,7 +83,9 @@ const PRESENCE_STYLES: Record 0) { activity.smallImageText = trimField(style.smallImageText.trim()); } - if ( - style.buttonLabel.trim().length > 0 && - /^https?:\/\//.test(style.buttonUrl.trim()) - ) { + if (style.buttonLabel.trim().length > 0 && /^https?:\/\//.test(style.buttonUrl.trim())) { activity.buttons = [ { label: trimField(style.buttonLabel.trim(), 32), diff --git a/src/core/services/immersion-tracker-service.ts b/src/core/services/immersion-tracker-service.ts index 276cd3d6..6cbf5841 100644 --- a/src/core/services/immersion-tracker-service.ts +++ b/src/core/services/immersion-tracker-service.ts @@ -380,42 +380,22 @@ export class ImmersionTrackerService { }; }; - const eventsRetention = daysToRetentionWindow( - retention.eventsDays, - 7, - 3650, - ); - const telemetryRetention = daysToRetentionWindow( - retention.telemetryDays, - 30, - 3650, - ); - const sessionsRetention = daysToRetentionWindow( - retention.sessionsDays, - 30, - 3650, - ); + const eventsRetention = daysToRetentionWindow(retention.eventsDays, 7, 3650); + const telemetryRetention = daysToRetentionWindow(retention.telemetryDays, 30, 3650); + const sessionsRetention = daysToRetentionWindow(retention.sessionsDays, 30, 3650); this.eventsRetentionMs = eventsRetention.ms; this.eventsRetentionDays = eventsRetention.days; this.telemetryRetentionMs = telemetryRetention.ms; this.telemetryRetentionDays = telemetryRetention.days; this.sessionsRetentionMs = sessionsRetention.ms; this.sessionsRetentionDays = sessionsRetention.days; - this.dailyRollupRetentionMs = daysToRetentionWindow( - retention.dailyRollupsDays, - 365, - 36500, - ).ms; + this.dailyRollupRetentionMs = daysToRetentionWindow(retention.dailyRollupsDays, 365, 36500).ms; this.monthlyRollupRetentionMs = daysToRetentionWindow( retention.monthlyRollupsDays, 5 * 365, 36500, ).ms; - this.vacuumIntervalMs = daysToRetentionWindow( - retention.vacuumIntervalDays, - 7, - 3650, - ).ms; + this.vacuumIntervalMs = daysToRetentionWindow(retention.vacuumIntervalDays, 7, 3650).ms; this.db = new Database(this.dbPath); applyPragmas(this.db); ensureSchema(this.db); diff --git a/src/core/services/immersion-tracker/__tests__/query.test.ts b/src/core/services/immersion-tracker/__tests__/query.test.ts index dc8bc45e..f131048e 100644 --- a/src/core/services/immersion-tracker/__tests__/query.test.ts +++ b/src/core/services/immersion-tracker/__tests__/query.test.ts @@ -975,79 +975,79 @@ test('getTrendsDashboard month grouping spans every touched calendar month and k ); } - const insertDailyRollup = db.prepare( - ` + const insertDailyRollup = db.prepare( + ` INSERT INTO imm_daily_rollups ( rollup_day, video_id, total_sessions, total_active_min, total_lines_seen, total_tokens_seen, total_cards, CREATED_DATE, LAST_UPDATE_DATE ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, - ); - const insertMonthlyRollup = db.prepare( - ` + ); + const insertMonthlyRollup = db.prepare( + ` INSERT INTO imm_monthly_rollups ( rollup_month, video_id, total_sessions, total_active_min, total_lines_seen, total_tokens_seen, total_cards, CREATED_DATE, LAST_UPDATE_DATE ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, - ); - insertDailyRollup.run(20500, febVideoId, 1, 30, 4, 100, 2, febStartedAtMs, febStartedAtMs); - insertDailyRollup.run(20513, marVideoId, 1, 30, 4, 120, 4, marStartedAtMs, marStartedAtMs); - insertMonthlyRollup.run(202602, febVideoId, 1, 30, 4, 100, 2, febStartedAtMs, febStartedAtMs); - insertMonthlyRollup.run(202603, marVideoId, 1, 30, 4, 120, 4, marStartedAtMs, marStartedAtMs); + ); + insertDailyRollup.run(20500, febVideoId, 1, 30, 4, 100, 2, febStartedAtMs, febStartedAtMs); + insertDailyRollup.run(20513, marVideoId, 1, 30, 4, 120, 4, marStartedAtMs, marStartedAtMs); + insertMonthlyRollup.run(202602, febVideoId, 1, 30, 4, 100, 2, febStartedAtMs, febStartedAtMs); + insertMonthlyRollup.run(202603, marVideoId, 1, 30, 4, 120, 4, marStartedAtMs, marStartedAtMs); - db.prepare( - ` + db.prepare( + ` INSERT INTO imm_words ( headword, word, reading, part_of_speech, pos1, pos2, pos3, first_seen, last_seen, frequency ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, - ).run( - '二月', - '二月', - 'にがつ', - 'noun', - '名詞', - '', - '', - (BigInt(febStartedAtMs) / 1000n).toString(), - (BigInt(febStartedAtMs) / 1000n).toString(), - 1, - ); - db.prepare( - ` + ).run( + '二月', + '二月', + 'にがつ', + 'noun', + '名詞', + '', + '', + (BigInt(febStartedAtMs) / 1000n).toString(), + (BigInt(febStartedAtMs) / 1000n).toString(), + 1, + ); + db.prepare( + ` INSERT INTO imm_words ( headword, word, reading, part_of_speech, pos1, pos2, pos3, first_seen, last_seen, frequency ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, - ).run( - '三月', - '三月', - 'さんがつ', - 'noun', - '名詞', - '', - '', - (BigInt(marStartedAtMs) / 1000n).toString(), - (BigInt(marStartedAtMs) / 1000n).toString(), - 1, - ); + ).run( + '三月', + '三月', + 'さんがつ', + 'noun', + '名詞', + '', + '', + (BigInt(marStartedAtMs) / 1000n).toString(), + (BigInt(marStartedAtMs) / 1000n).toString(), + 1, + ); - const dashboard = getTrendsDashboard(db, '30d', 'month'); + const dashboard = getTrendsDashboard(db, '30d', 'month'); - assert.equal(dashboard.activity.watchTime.length, 2); - assert.deepEqual( - dashboard.progress.newWords.map((point) => point.label), - dashboard.activity.watchTime.map((point) => point.label), - ); - assert.deepEqual( - dashboard.progress.episodes.map((point) => point.label), - dashboard.activity.watchTime.map((point) => point.label), - ); - assert.deepEqual( - dashboard.progress.lookups.map((point) => point.label), - dashboard.activity.watchTime.map((point) => point.label), - ); + assert.equal(dashboard.activity.watchTime.length, 2); + assert.deepEqual( + dashboard.progress.newWords.map((point) => point.label), + dashboard.activity.watchTime.map((point) => point.label), + ); + assert.deepEqual( + dashboard.progress.episodes.map((point) => point.label), + dashboard.activity.watchTime.map((point) => point.label), + ); + assert.deepEqual( + dashboard.progress.lookups.map((point) => point.label), + dashboard.activity.watchTime.map((point) => point.label), + ); } finally { db.close(); cleanupDbPath(dbPath); @@ -1230,18 +1230,7 @@ test('getQueryHints counts new words by distinct headword first-seen time', () = headword, word, reading, part_of_speech, pos1, pos2, pos3, first_seen, last_seen, frequency ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, - ).run( - '猫', - '猫', - 'ねこ', - 'noun', - '名詞', - '', - '', - String(twoDaysAgo), - String(twoDaysAgo), - 1, - ); + ).run('猫', '猫', 'ねこ', 'noun', '名詞', '', '', String(twoDaysAgo), String(twoDaysAgo), 1); const hints = getQueryHints(db); assert.equal(hints.newWordsToday, 1); diff --git a/src/core/services/immersion-tracker/lifetime.ts b/src/core/services/immersion-tracker/lifetime.ts index e0aac328..5d422ec2 100644 --- a/src/core/services/immersion-tracker/lifetime.ts +++ b/src/core/services/immersion-tracker/lifetime.ts @@ -82,12 +82,9 @@ function hasRetainedPriorSession( LIMIT 1 `, ) - .get( - videoId, - toDbTimestamp(startedAtMs), - toDbTimestamp(startedAtMs), - currentSessionId, - ) as { found: number } | null; + .get(videoId, toDbTimestamp(startedAtMs), toDbTimestamp(startedAtMs), currentSessionId) as { + found: number; + } | null; return Boolean(row); } @@ -150,7 +147,7 @@ function resetLifetimeSummaries(db: DatabaseSync, nowMs: number): void { LAST_UPDATE_DATE = ? WHERE global_id = 1 `, - ).run(toDbTimestamp(nowMs), toDbTimestamp(nowMs)); + ).run(toDbTimestamp(nowMs), toDbTimestamp(nowMs)); } function rebuildLifetimeSummariesInternal( diff --git a/src/core/services/immersion-tracker/maintenance.test.ts b/src/core/services/immersion-tracker/maintenance.test.ts index ab9af881..5ab59f34 100644 --- a/src/core/services/immersion-tracker/maintenance.test.ts +++ b/src/core/services/immersion-tracker/maintenance.test.ts @@ -126,9 +126,9 @@ test('pruneRawRetention skips disabled retention windows', () => { const remainingTelemetry = db .prepare('SELECT COUNT(*) AS count FROM imm_session_telemetry') .get() as { count: number }; - const remainingSessions = db - .prepare('SELECT COUNT(*) AS count FROM imm_sessions') - .get() as { count: number }; + const remainingSessions = db.prepare('SELECT COUNT(*) AS count FROM imm_sessions').get() as { + count: number; + }; assert.equal(result.deletedSessionEvents, 0); assert.equal(result.deletedTelemetryRows, 0); diff --git a/src/core/services/immersion-tracker/maintenance.ts b/src/core/services/immersion-tracker/maintenance.ts index d225f0d2..e723750c 100644 --- a/src/core/services/immersion-tracker/maintenance.ts +++ b/src/core/services/immersion-tracker/maintenance.ts @@ -56,10 +56,7 @@ export function pruneRawRetention( sessionsRetentionDays?: number; }, ): RawRetentionResult { - const resolveCutoff = ( - retentionMs: number, - retentionDays: number | undefined, - ): string => { + const resolveCutoff = (retentionMs: number, retentionDays: number | undefined): string => { if (retentionDays !== undefined) { return subtractDbTimestamp(currentMs, BigInt(retentionDays) * 86_400_000n); } @@ -68,9 +65,11 @@ export function pruneRawRetention( const deletedSessionEvents = Number.isFinite(policy.eventsRetentionMs) ? ( - db.prepare(`DELETE FROM imm_session_events WHERE ts_ms < ?`).run( - resolveCutoff(policy.eventsRetentionMs, policy.eventsRetentionDays), - ) as { changes: number } + db + .prepare(`DELETE FROM imm_session_events WHERE ts_ms < ?`) + .run(resolveCutoff(policy.eventsRetentionMs, policy.eventsRetentionDays)) as { + changes: number; + } ).changes : 0; const deletedTelemetryRows = Number.isFinite(policy.telemetryRetentionMs) diff --git a/src/core/services/immersion-tracker/query-lexical.ts b/src/core/services/immersion-tracker/query-lexical.ts index 5e6ac68d..a2143a2e 100644 --- a/src/core/services/immersion-tracker/query-lexical.ts +++ b/src/core/services/immersion-tracker/query-lexical.ts @@ -150,9 +150,11 @@ export function getSessionEvents( ORDER BY ts_ms ASC LIMIT ? `); - const rows = stmt.all(sessionId, ...eventTypes, limit) as Array; + const rows = stmt.all(sessionId, ...eventTypes, limit) as Array< + SessionEventRow & { + tsMs: number | string; + } + >; return rows.map((row) => ({ ...row, tsMs: fromDbTimestamp(row.tsMs) ?? 0, diff --git a/src/core/services/immersion-tracker/query-maintenance.ts b/src/core/services/immersion-tracker/query-maintenance.ts index 00b687c2..e94aeeb5 100644 --- a/src/core/services/immersion-tracker/query-maintenance.ts +++ b/src/core/services/immersion-tracker/query-maintenance.ts @@ -355,9 +355,7 @@ export function upsertCoverArt( const fetchedAtMs = toDbTimestamp(nowMs()); const coverBlob = normalizeCoverBlobBytes(art.coverBlob); const computedCoverBlobHash = - coverBlob && coverBlob.length > 0 - ? createHash('sha256').update(coverBlob).digest('hex') - : null; + coverBlob && coverBlob.length > 0 ? createHash('sha256').update(coverBlob).digest('hex') : null; let coverBlobHash = computedCoverBlobHash ?? sharedCoverBlobHash ?? null; if (!coverBlobHash && (!coverBlob || coverBlob.length === 0)) { coverBlobHash = existing?.coverBlobHash ?? null; diff --git a/src/core/services/immersion-tracker/query-sessions.ts b/src/core/services/immersion-tracker/query-sessions.ts index 2d068656..d81fb0bc 100644 --- a/src/core/services/immersion-tracker/query-sessions.ts +++ b/src/core/services/immersion-tracker/query-sessions.ts @@ -39,10 +39,12 @@ export function getSessionSummaries(db: DatabaseSync, limit = 50): SessionSummar ORDER BY s.started_at_ms DESC LIMIT ? `); - const rows = prepared.all(limit) as Array; + const rows = prepared.all(limit) as Array< + SessionSummaryQueryRow & { + startedAtMs: number | string; + endedAtMs: number | string | null; + } + >; return rows.map((row) => ({ ...row, startedAtMs: fromDbTimestamp(row.startedAtMs) ?? 0, @@ -69,19 +71,21 @@ export function getSessionTimeline( `; if (limit === undefined) { - const rows = db.prepare(select).all(sessionId) as Array; + const rows = db.prepare(select).all(sessionId) as Array< + SessionTimelineRow & { + sampleMs: number | string; + } + >; return rows.map((row) => ({ ...row, sampleMs: fromDbTimestamp(row.sampleMs) ?? 0, })); } - const rows = db - .prepare(`${select}\n LIMIT ?`) - .all(sessionId, limit) as Array; + const rows = db.prepare(`${select}\n LIMIT ?`).all(sessionId, limit) as Array< + SessionTimelineRow & { + sampleMs: number | string; + } + >; return rows.map((row) => ({ ...row, sampleMs: fromDbTimestamp(row.sampleMs) ?? 0, diff --git a/src/core/services/immersion-tracker/query-shared.ts b/src/core/services/immersion-tracker/query-shared.ts index 2634ce6e..38a17189 100644 --- a/src/core/services/immersion-tracker/query-shared.ts +++ b/src/core/services/immersion-tracker/query-shared.ts @@ -359,10 +359,7 @@ function getNumericCalendarValue( return Number(row?.value ?? 0); } -export function getLocalEpochDay( - db: DatabaseSync, - timestampMs: number | bigint | string, -): number { +export function getLocalEpochDay(db: DatabaseSync, timestampMs: number | bigint | string): number { return getNumericCalendarValue( db, ` @@ -375,10 +372,7 @@ export function getLocalEpochDay( ); } -export function getLocalMonthKey( - db: DatabaseSync, - timestampMs: number | bigint | string, -): number { +export function getLocalMonthKey(db: DatabaseSync, timestampMs: number | bigint | string): number { return getNumericCalendarValue( db, ` @@ -391,10 +385,7 @@ export function getLocalMonthKey( ); } -export function getLocalDayOfWeek( - db: DatabaseSync, - timestampMs: number | bigint | string, -): number { +export function getLocalDayOfWeek(db: DatabaseSync, timestampMs: number | bigint | string): number { return getNumericCalendarValue( db, ` @@ -407,10 +398,7 @@ export function getLocalDayOfWeek( ); } -export function getLocalHourOfDay( - db: DatabaseSync, - timestampMs: number | bigint | string, -): number { +export function getLocalHourOfDay(db: DatabaseSync, timestampMs: number | bigint | string): number { return getNumericCalendarValue( db, ` @@ -458,7 +446,8 @@ export function getShiftedLocalDayTimestamp( dayOffset: number, ): string { const normalizedDayOffset = Math.trunc(dayOffset); - const modifier = normalizedDayOffset >= 0 ? `+${normalizedDayOffset} days` : `${normalizedDayOffset} days`; + const modifier = + normalizedDayOffset >= 0 ? `+${normalizedDayOffset} days` : `${normalizedDayOffset} days`; const row = db .prepare( ` diff --git a/src/core/services/immersion-tracker/query-trends.ts b/src/core/services/immersion-tracker/query-trends.ts index 4e7d2dcb..2a0f3eb2 100644 --- a/src/core/services/immersion-tracker/query-trends.ts +++ b/src/core/services/immersion-tracker/query-trends.ts @@ -87,7 +87,20 @@ const TREND_DAY_LIMITS: Record, number> = { '90d': 90, }; -const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; +const MONTH_NAMES = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', +]; const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; @@ -101,7 +114,11 @@ function getTrendMonthlyLimit(db: DatabaseSync, range: TrendRange): number { } const currentTimestamp = currentDbTimestamp(); const todayStartMs = getShiftedLocalDayTimestamp(db, currentTimestamp, 0); - const cutoffMs = getShiftedLocalDayTimestamp(db, currentTimestamp, -(TREND_DAY_LIMITS[range] - 1)); + const cutoffMs = getShiftedLocalDayTimestamp( + db, + currentTimestamp, + -(TREND_DAY_LIMITS[range] - 1), + ); const currentMonthKey = getLocalMonthKey(db, todayStartMs); const cutoffMonthKey = getLocalMonthKey(db, cutoffMs); const currentYear = Math.floor(currentMonthKey / 100); @@ -630,8 +647,10 @@ export function getTrendsDashboard( const animePerDay = { episodes: buildEpisodesPerAnimeFromDailyRollups(dailyRollups, titlesByVideoId), - watchTime: buildPerAnimeFromDailyRollups(dailyRollups, titlesByVideoId, (rollup) => - rollup.totalActiveMin, + watchTime: buildPerAnimeFromDailyRollups( + dailyRollups, + titlesByVideoId, + (rollup) => rollup.totalActiveMin, ), cards: buildPerAnimeFromDailyRollups( dailyRollups, diff --git a/src/core/services/mpv-protocol.ts b/src/core/services/mpv-protocol.ts index 13680aa6..6a8ac979 100644 --- a/src/core/services/mpv-protocol.ts +++ b/src/core/services/mpv-protocol.ts @@ -102,7 +102,9 @@ type SubtitleTrackCandidate = { externalFilename: string | null; }; -function normalizeSubtitleTrackCandidate(track: Record): SubtitleTrackCandidate | null { +function normalizeSubtitleTrackCandidate( + track: Record, +): SubtitleTrackCandidate | null { const id = typeof track.id === 'number' ? track.id @@ -122,8 +124,12 @@ function normalizeSubtitleTrackCandidate(track: Record): Subtit return { id, - lang: String(track.lang || '').trim().toLowerCase(), - title: String(track.title || '').trim().toLowerCase(), + lang: String(track.lang || '') + .trim() + .toLowerCase(), + title: String(track.title || '') + .trim() + .toLowerCase(), selected: track.selected === true, external: track.external === true, externalFilename, @@ -168,9 +174,7 @@ function pickSecondarySubtitleTrackId( const uniqueTracks = [...dedupedTracks.values()]; for (const language of normalizedLanguages) { - const selectedMatch = uniqueTracks.find( - (track) => track.selected && track.lang === language, - ); + const selectedMatch = uniqueTracks.find((track) => track.selected && track.lang === language); if (selectedMatch) { return selectedMatch.id; } diff --git a/src/core/services/stats-server.ts b/src/core/services/stats-server.ts index 986a9518..52185877 100644 --- a/src/core/services/stats-server.ts +++ b/src/core/services/stats-server.ts @@ -102,10 +102,7 @@ async function writeFetchResponse(res: ServerResponse, response: Response): Prom res.end(Buffer.from(body)); } -function startNodeHttpServer( - app: Hono, - config: StatsServerConfig, -): { close: () => void } { +function startNodeHttpServer(app: Hono, config: StatsServerConfig): { close: () => void } { const server = http.createServer((req, res) => { void (async () => { try { @@ -1075,11 +1072,9 @@ export function startStatsServer(config: StatsServerConfig): { close: () => void const bunRuntime = globalThis as typeof globalThis & { Bun?: { - serve?: (options: { - fetch: (typeof app)['fetch']; - port: number; - hostname: string; - }) => { stop: () => void }; + serve?: (options: { fetch: (typeof app)['fetch']; port: number; hostname: string }) => { + stop: () => void; + }; }; }; diff --git a/src/core/services/subsync.ts b/src/core/services/subsync.ts index 4d525ea1..e01061b1 100644 --- a/src/core/services/subsync.ts +++ b/src/core/services/subsync.ts @@ -77,7 +77,11 @@ function normalizeTrackIds(tracks: unknown[]): MpvTrack[] { } function getSourceTrackIdentity(track: MpvTrack): string { - if (track.external && typeof track['external-filename'] === 'string' && track['external-filename'].length > 0) { + if ( + track.external && + typeof track['external-filename'] === 'string' && + track['external-filename'].length > 0 + ) { return `external:${track['external-filename'].toLowerCase()}`; } if (typeof track.id === 'number') { diff --git a/src/core/services/tokenizer/yomitan-parser-runtime.ts b/src/core/services/tokenizer/yomitan-parser-runtime.ts index ffac7cc9..568dc053 100644 --- a/src/core/services/tokenizer/yomitan-parser-runtime.ts +++ b/src/core/services/tokenizer/yomitan-parser-runtime.ts @@ -2029,7 +2029,8 @@ export async function addYomitanNoteViaSearch( : null, duplicateNoteIds: Array.isArray(envelope.duplicateNoteIds) ? envelope.duplicateNoteIds.filter( - (entry): entry is number => typeof entry === 'number' && Number.isInteger(entry) && entry > 0, + (entry): entry is number => + typeof entry === 'number' && Number.isInteger(entry) && entry > 0, ) : [], }; diff --git a/src/core/services/youtube/metadata-probe.test.ts b/src/core/services/youtube/metadata-probe.test.ts index e0f58add..4ba6d0b0 100644 --- a/src/core/services/youtube/metadata-probe.test.ts +++ b/src/core/services/youtube/metadata-probe.test.ts @@ -16,11 +16,12 @@ async function withTempDir(fn: (dir: string) => Promise): Promise { function makeFakeYtDlpScript(dir: string, payload: string): void { const scriptPath = path.join(dir, 'yt-dlp'); - const script = process.platform === 'win32' - ? `#!/usr/bin/env bun + const script = + process.platform === 'win32' + ? `#!/usr/bin/env bun process.stdout.write(${JSON.stringify(payload)}); ` - : `#!/usr/bin/env sh + : `#!/usr/bin/env sh cat <<'EOF' | base64 -d ${Buffer.from(payload).toString('base64')} EOF diff --git a/src/core/services/youtube/playback-resolve.test.ts b/src/core/services/youtube/playback-resolve.test.ts index 708ea6d2..eb60f8c8 100644 --- a/src/core/services/youtube/playback-resolve.test.ts +++ b/src/core/services/youtube/playback-resolve.test.ts @@ -16,11 +16,12 @@ async function withTempDir(fn: (dir: string) => Promise): Promise { function makeFakeYtDlpScript(dir: string, payload: string): void { const scriptPath = path.join(dir, 'yt-dlp'); - const script = process.platform === 'win32' - ? `#!/usr/bin/env bun + const script = + process.platform === 'win32' + ? `#!/usr/bin/env bun process.stdout.write(${JSON.stringify(payload)}); ` - : `#!/usr/bin/env sh + : `#!/usr/bin/env sh cat <<'EOF' | base64 -d ${Buffer.from(payload).toString('base64')} EOF diff --git a/src/main-entry-runtime.test.ts b/src/main-entry-runtime.test.ts index 202913fc..54cb79e7 100644 --- a/src/main-entry-runtime.test.ts +++ b/src/main-entry-runtime.test.ts @@ -114,12 +114,7 @@ test('launch-mpv entry helpers detect and normalize targets', () => { ['--input-ipc-server', '\\\\.\\pipe\\custom-subminer-socket', '--alang', 'ja,jpn'], ); assert.deepEqual( - normalizeLaunchMpvExtraArgs([ - 'SubMiner.exe', - '--launch-mpv', - '--fullscreen', - 'C:\\a.mkv', - ]), + normalizeLaunchMpvExtraArgs(['SubMiner.exe', '--launch-mpv', '--fullscreen', 'C:\\a.mkv']), ['--fullscreen'], ); assert.deepEqual( diff --git a/src/main.ts b/src/main.ts index b0427669..de310ab5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -77,15 +77,15 @@ function getStartupModeFlags(initialArgs: CliArgs | null | undefined): { return { shouldUseMinimalStartup: Boolean( (initialArgs && isStandaloneTexthookerCommand(initialArgs)) || - (initialArgs?.stats && - (initialArgs.statsCleanup || initialArgs.statsBackground || initialArgs.statsStop)), + (initialArgs?.stats && + (initialArgs.statsCleanup || initialArgs.statsBackground || initialArgs.statsStop)), ), shouldSkipHeavyStartup: Boolean( initialArgs && - (shouldRunSettingsOnlyStartup(initialArgs) || - initialArgs.stats || - initialArgs.dictionary || - initialArgs.setup), + (shouldRunSettingsOnlyStartup(initialArgs) || + initialArgs.stats || + initialArgs.dictionary || + initialArgs.setup), ), }; } @@ -123,7 +123,12 @@ import { AnkiIntegration } from './anki-integration'; import { SubtitleTimingTracker } from './subtitle-timing-tracker'; import { RuntimeOptionsManager } from './runtime-options'; import { downloadToFile, isRemoteMediaPath, parseMediaInfo } from './jimaku/utils'; -import { createLogger, setLogLevel, resolveDefaultLogFilePath, type LogLevelSource } from './logger'; +import { + createLogger, + setLogLevel, + resolveDefaultLogFilePath, + type LogLevelSource, +} from './logger'; import { createWindowTracker as createWindowTrackerCore } from './window-trackers'; import { commandNeedsOverlayStartupPrereqs, @@ -496,7 +501,10 @@ import { } from './config'; import { resolveConfigDir } from './config/path-resolution'; import { parseSubtitleCues } from './core/services/subtitle-cue-parser'; -import { createSubtitlePrefetchService, type SubtitlePrefetchService } from './core/services/subtitle-prefetch'; +import { + createSubtitlePrefetchService, + type SubtitlePrefetchService, +} from './core/services/subtitle-prefetch'; import { buildSubtitleSidebarSourceKey, resolveSubtitleSourcePath, @@ -1412,8 +1420,7 @@ const refreshSubtitlePrefetchFromActiveTrackHandler = getMpvClient: () => appState.mpvClient, getLastObservedTimePos: () => lastObservedTimePos, subtitlePrefetchInitController, - resolveActiveSubtitleSidebarSource: (input) => - resolveActiveSubtitleSidebarSourceHandler(input), + resolveActiveSubtitleSidebarSource: (input) => resolveActiveSubtitleSidebarSourceHandler(input), }); function scheduleSubtitlePrefetchRefresh(delayMs = 0): void { @@ -1426,7 +1433,8 @@ function scheduleSubtitlePrefetchRefresh(delayMs = 0): void { const subtitlePrefetchRuntime = { cancelPendingInit: () => subtitlePrefetchInitController.cancelPendingInit(), initSubtitlePrefetch: subtitlePrefetchInitController.initSubtitlePrefetch, - refreshSubtitleSidebarFromSource: (sourcePath: string) => refreshSubtitleSidebarFromSource(sourcePath), + refreshSubtitleSidebarFromSource: (sourcePath: string) => + refreshSubtitleSidebarFromSource(sourcePath), refreshSubtitlePrefetchFromActiveTrack: () => refreshSubtitlePrefetchFromActiveTrackHandler(), scheduleSubtitlePrefetchRefresh: (delayMs?: number) => scheduleSubtitlePrefetchRefresh(delayMs), clearScheduledSubtitlePrefetchRefresh: () => clearScheduledSubtitlePrefetchRefresh(), @@ -1861,10 +1869,11 @@ const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService( }, })(), ); -const buildGetRuntimeOptionsStateMainDepsHandler = - createBuildGetRuntimeOptionsStateMainDepsHandler({ +const buildGetRuntimeOptionsStateMainDepsHandler = createBuildGetRuntimeOptionsStateMainDepsHandler( + { getRuntimeOptionsManager: () => appState.runtimeOptionsManager, - }); + }, +); const getRuntimeOptionsStateMainDeps = buildGetRuntimeOptionsStateMainDepsHandler(); const getRuntimeOptionsStateHandler = createGetRuntimeOptionsStateHandler( getRuntimeOptionsStateMainDeps, @@ -3042,7 +3051,8 @@ const runStatsCliCommand = createRunStatsCliCommandHandler({ }, getImmersionTracker: () => appState.immersionTracker, ensureStatsServerStarted: () => statsStartupRuntime.ensureStatsServerStarted(), - ensureBackgroundStatsServerStarted: () => statsStartupRuntime.ensureBackgroundStatsServerStarted(), + ensureBackgroundStatsServerStarted: () => + statsStartupRuntime.ensureBackgroundStatsServerStarted(), stopBackgroundStatsServer: () => statsStartupRuntime.stopBackgroundStatsServer(), openExternal: (url: string) => shell.openExternal(url), writeResponse: (responsePath, payload) => { @@ -3258,8 +3268,7 @@ const { appReadyRuntimeRunner } = composeAppReadyRuntime({ Boolean(appState.initialArgs && isHeadlessInitialCommand(appState.initialArgs)), shouldUseMinimalStartup: () => getStartupModeFlags(appState.initialArgs).shouldUseMinimalStartup, - shouldSkipHeavyStartup: () => - getStartupModeFlags(appState.initialArgs).shouldSkipHeavyStartup, + shouldSkipHeavyStartup: () => getStartupModeFlags(appState.initialArgs).shouldSkipHeavyStartup, createImmersionTracker: () => { ensureImmersionTrackerStarted(); }, diff --git a/src/main/boot/services.test.ts b/src/main/boot/services.test.ts index c09e69bb..c469c8b4 100644 --- a/src/main/boot/services.test.ts +++ b/src/main/boot/services.test.ts @@ -24,7 +24,11 @@ test('createMainBootServices builds boot-phase service bundle', () => { { scope: string; warn: () => void; info: () => void; error: () => void }, { registry: boolean }, { getModalWindow: () => null }, - { inputState: boolean; getModalInputExclusive: () => boolean; handleModalInputStateChange: (isActive: boolean) => void }, + { + inputState: boolean; + getModalInputExclusive: () => boolean; + handleModalInputStateChange: (isActive: boolean) => void; + }, { measurementStore: boolean }, { modalRuntime: boolean }, { mpvSocketPath: string; texthookerPort: number }, @@ -80,7 +84,11 @@ test('createMainBootServices builds boot-phase service bundle', () => { createOverlayManager: () => ({ getModalWindow: () => null, }), - createOverlayModalInputState: () => ({ inputState: true, getModalInputExclusive: () => false, handleModalInputStateChange: () => {} }), + createOverlayModalInputState: () => ({ + inputState: true, + getModalInputExclusive: () => false, + handleModalInputStateChange: () => {}, + }), createOverlayContentMeasurementStore: () => ({ measurementStore: true }), getSyncOverlayShortcutsForModal: () => () => {}, getSyncOverlayVisibilityForModal: () => () => {}, @@ -106,8 +114,14 @@ test('createMainBootServices builds boot-phase service bundle', () => { mpvSocketPath: '/tmp/subminer.sock', texthookerPort: 5174, }); - assert.equal(services.appLifecycleApp.on('ready', () => {}), services.appLifecycleApp); - assert.equal(services.appLifecycleApp.on('second-instance', () => {}), services.appLifecycleApp); + assert.equal( + services.appLifecycleApp.on('ready', () => {}), + services.appLifecycleApp, + ); + assert.equal( + services.appLifecycleApp.on('second-instance', () => {}), + services.appLifecycleApp, + ); assert.deepEqual(appOnCalls, ['ready']); assert.equal(secondInstanceHandlerRegistered, true); assert.deepEqual(calls, ['mkdir:/tmp/subminer-config']); diff --git a/src/main/boot/services.ts b/src/main/boot/services.ts index 862c1fa3..51c4f74f 100644 --- a/src/main/boot/services.ts +++ b/src/main/boot/services.ts @@ -56,9 +56,7 @@ export interface MainBootServicesParams< }; shouldBypassSingleInstanceLock: () => boolean; requestSingleInstanceLockEarly: () => boolean; - registerSecondInstanceHandlerEarly: ( - listener: (_event: unknown, argv: string[]) => void, - ) => void; + registerSecondInstanceHandlerEarly: (listener: (_event: unknown, argv: string[]) => void) => void; onConfigStartupParseError: (error: ConfigStartupParseError) => void; createConfigService: (configDir: string) => TConfigService; createAnilistTokenStore: (targetPath: string) => TAnilistTokenStore; @@ -87,10 +85,7 @@ export interface MainBootServicesParams< overlayModalInputState: TOverlayModalInputState; onModalStateChange: (isActive: boolean) => void; }) => TOverlayModalRuntime; - createAppState: (input: { - mpvSocketPath: string; - texthookerPort: number; - }) => TAppState; + createAppState: (input: { mpvSocketPath: string; texthookerPort: number }) => TAppState; } export interface MainBootServicesResult< @@ -239,9 +234,7 @@ export function createMainBootServices< const appLifecycleApp = { requestSingleInstanceLock: () => - params.shouldBypassSingleInstanceLock() - ? true - : params.requestSingleInstanceLockEarly(), + params.shouldBypassSingleInstanceLock() ? true : params.requestSingleInstanceLockEarly(), quit: () => params.app.quit(), on: (event: string, listener: (...args: unknown[]) => void) => { if (event === 'second-instance') { diff --git a/src/main/character-dictionary-runtime/zip.test.ts b/src/main/character-dictionary-runtime/zip.test.ts index a17f77a5..b7f8ab40 100644 --- a/src/main/character-dictionary-runtime/zip.test.ts +++ b/src/main/character-dictionary-runtime/zip.test.ts @@ -31,9 +31,9 @@ function readStoredZipEntries(zipPath: string): Map { const extraLength = archive.readUInt16LE(cursor + 28); const fileNameStart = cursor + 30; const dataStart = fileNameStart + fileNameLength + extraLength; - const fileName = archive.subarray(fileNameStart, fileNameStart + fileNameLength).toString( - 'utf8', - ); + const fileName = archive + .subarray(fileNameStart, fileNameStart + fileNameLength) + .toString('utf8'); const data = archive.subarray(dataStart, dataStart + compressedSize); entries.set(fileName, Buffer.from(data)); cursor = dataStart + compressedSize; @@ -57,7 +57,9 @@ test('buildDictionaryZip writes a valid stored zip without fs.writeFileSync', () }) as typeof fs.writeFileSync; Buffer.concat = ((...args: Parameters) => { - throw new Error(`buildDictionaryZip should not Buffer.concat the full archive (${args[0].length} chunks)`); + throw new Error( + `buildDictionaryZip should not Buffer.concat the full archive (${args[0].length} chunks)`, + ); }) as typeof Buffer.concat; const result = buildDictionaryZip( @@ -91,8 +93,9 @@ test('buildDictionaryZip writes a valid stored zip without fs.writeFileSync', () assert.equal(indexJson.revision, '2026-03-27'); assert.equal(indexJson.format, 3); - const termBank = JSON.parse(entries.get('term_bank_1.json')!.toString('utf8')) as - CharacterDictionaryTermEntry[]; + const termBank = JSON.parse( + entries.get('term_bank_1.json')!.toString('utf8'), + ) as CharacterDictionaryTermEntry[]; assert.equal(termBank.length, 1); assert.equal(termBank[0]?.[0], 'アルファ'); assert.deepEqual(entries.get('images/alpha.bin'), Buffer.from([1, 2, 3])); diff --git a/src/main/character-dictionary-runtime/zip.ts b/src/main/character-dictionary-runtime/zip.ts index 85bf34b1..9ec09904 100644 --- a/src/main/character-dictionary-runtime/zip.ts +++ b/src/main/character-dictionary-runtime/zip.ts @@ -138,7 +138,11 @@ function createCentralDirectoryHeader(entry: ZipEntry): Buffer { return central; } -function createEndOfCentralDirectory(entriesLength: number, centralSize: number, centralStart: number): Buffer { +function createEndOfCentralDirectory( + entriesLength: number, + centralSize: number, + centralStart: number, +): Buffer { const end = Buffer.alloc(22); let cursor = 0; writeUint32LE(end, 0x06054b50, cursor); diff --git a/src/main/runtime/autoplay-ready-gate.test.ts b/src/main/runtime/autoplay-ready-gate.test.ts index ac8f8b1f..17c053a9 100644 --- a/src/main/runtime/autoplay-ready-gate.test.ts +++ b/src/main/runtime/autoplay-ready-gate.test.ts @@ -37,13 +37,13 @@ test('autoplay ready gate suppresses duplicate media signals for the same media' firstScheduled?.(); await new Promise((resolve) => setTimeout(resolve, 0)); - assert.deepEqual(commands.filter((command) => command[0] === 'script-message'), [ - ['script-message', 'subminer-autoplay-ready'], - ]); + assert.deepEqual( + commands.filter((command) => command[0] === 'script-message'), + [['script-message', 'subminer-autoplay-ready']], + ); assert.ok( commands.some( - (command) => - command[0] === 'set_property' && command[1] === 'pause' && command[2] === false, + (command) => command[0] === 'set_property' && command[1] === 'pause' && command[2] === false, ), ); assert.equal(scheduled.length > 0, true); @@ -84,13 +84,13 @@ test('autoplay ready gate retry loop does not re-signal plugin readiness', async await new Promise((resolve) => setTimeout(resolve, 0)); } - assert.deepEqual(commands.filter((command) => command[0] === 'script-message'), [ - ['script-message', 'subminer-autoplay-ready'], - ]); + assert.deepEqual( + commands.filter((command) => command[0] === 'script-message'), + [['script-message', 'subminer-autoplay-ready']], + ); assert.equal( commands.filter( - (command) => - command[0] === 'set_property' && command[1] === 'pause' && command[2] === false, + (command) => command[0] === 'set_property' && command[1] === 'pause' && command[2] === false, ).length > 0, true, ); @@ -130,13 +130,15 @@ test('autoplay ready gate does not unpause again after a later manual pause on t await new Promise((resolve) => setTimeout(resolve, 0)); playbackPaused = true; - gate.maybeSignalPluginAutoplayReady({ text: '字幕その2', tokens: null }, { forceWhilePaused: true }); + gate.maybeSignalPluginAutoplayReady( + { text: '字幕その2', tokens: null }, + { forceWhilePaused: true }, + ); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal( commands.filter( - (command) => - command[0] === 'set_property' && command[1] === 'pause' && command[2] === false, + (command) => command[0] === 'set_property' && command[1] === 'pause' && command[2] === false, ).length, 1, ); diff --git a/src/main/runtime/autoplay-ready-gate.ts b/src/main/runtime/autoplay-ready-gate.ts index 1bc749cd..89c7d2ed 100644 --- a/src/main/runtime/autoplay-ready-gate.ts +++ b/src/main/runtime/autoplay-ready-gate.ts @@ -40,9 +40,7 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) { } const mediaPath = - deps.getCurrentMediaPath()?.trim() || - deps.getCurrentVideoPath()?.trim() || - '__unknown__'; + deps.getCurrentMediaPath()?.trim() || deps.getCurrentVideoPath()?.trim() || '__unknown__'; const duplicateMediaSignal = autoPlayReadySignalMediaPath === mediaPath; const releaseRetryDelayMs = 200; const maxReleaseAttempts = resolveAutoplayReadyMaxReleaseAttempts({ @@ -85,7 +83,10 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) { const mpvClient = deps.getMpvClient(); if (!mpvClient?.connected) { if (attempt < maxReleaseAttempts) { - deps.schedule(() => attemptRelease(playbackGeneration, attempt + 1), releaseRetryDelayMs); + deps.schedule( + () => attemptRelease(playbackGeneration, attempt + 1), + releaseRetryDelayMs, + ); } return; } diff --git a/src/main/runtime/composers/anilist-setup-composer.test.ts b/src/main/runtime/composers/anilist-setup-composer.test.ts index 3e81cacc..8a0a1dd7 100644 --- a/src/main/runtime/composers/anilist-setup-composer.test.ts +++ b/src/main/runtime/composers/anilist-setup-composer.test.ts @@ -50,6 +50,8 @@ test('composeAnilistSetupHandlers returns callable setup handlers', () => { assert.equal(handled, false); // handleAnilistSetupProtocolUrl returns true for subminer:// URLs - const handledProtocol = composed.handleAnilistSetupProtocolUrl('subminer://anilist-setup?code=abc'); + const handledProtocol = composed.handleAnilistSetupProtocolUrl( + 'subminer://anilist-setup?code=abc', + ); assert.equal(handledProtocol, true); }); diff --git a/src/main/runtime/composers/cli-startup-composer.test.ts b/src/main/runtime/composers/cli-startup-composer.test.ts index 72e68cb4..04fa7e49 100644 --- a/src/main/runtime/composers/cli-startup-composer.test.ts +++ b/src/main/runtime/composers/cli-startup-composer.test.ts @@ -36,8 +36,13 @@ test('composeCliStartupHandlers returns callable CLI startup handlers', () => { openJellyfinSetupWindow: () => {}, getAnilistQueueStatus: () => ({}) as never, processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'done' }), - generateCharacterDictionary: async () => - ({ zipPath: '/tmp/test.zip', fromCache: false, mediaId: 1, mediaTitle: 'Test', entryCount: 1 }), + generateCharacterDictionary: async () => ({ + zipPath: '/tmp/test.zip', + fromCache: false, + mediaId: 1, + mediaTitle: 'Test', + entryCount: 1, + }), runJellyfinCommand: async () => {}, runStatsCommand: async () => {}, runYoutubePlaybackFlow: async () => {}, diff --git a/src/main/runtime/composers/cli-startup-composer.ts b/src/main/runtime/composers/cli-startup-composer.ts index a473c0aa..bffacdb6 100644 --- a/src/main/runtime/composers/cli-startup-composer.ts +++ b/src/main/runtime/composers/cli-startup-composer.ts @@ -30,9 +30,7 @@ export type CliStartupComposerResult = ComposerOutputs<{ export function composeCliStartupHandlers( options: CliStartupComposerOptions, ): CliStartupComposerResult { - const createCliCommandContext = createCliCommandContextFactory( - options.cliCommandContextMainDeps, - ); + const createCliCommandContext = createCliCommandContextFactory(options.cliCommandContextMainDeps); const handleCliCommand = createCliCommandRuntimeHandler({ ...options.cliCommandRuntimeHandlerMainDeps, createCliCommandContext: () => createCliCommandContext(), diff --git a/src/main/runtime/composers/headless-startup-composer.ts b/src/main/runtime/composers/headless-startup-composer.ts index 033c37f4..a9dca997 100644 --- a/src/main/runtime/composers/headless-startup-composer.ts +++ b/src/main/runtime/composers/headless-startup-composer.ts @@ -8,28 +8,22 @@ type StartupRuntimeHandlers >; -export type HeadlessStartupComposerOptions< - TCliArgs, - TStartupState, - TStartupBootstrapRuntimeDeps, -> = ComposerInputs<{ - startupRuntimeHandlersDeps: StartupRuntimeHandlersDeps< - TCliArgs, - TStartupState, - TStartupBootstrapRuntimeDeps - >; -}>; +export type HeadlessStartupComposerOptions = + ComposerInputs<{ + startupRuntimeHandlersDeps: StartupRuntimeHandlersDeps< + TCliArgs, + TStartupState, + TStartupBootstrapRuntimeDeps + >; + }>; -export type HeadlessStartupComposerResult< - TCliArgs, - TStartupState, - TStartupBootstrapRuntimeDeps, -> = ComposerOutputs< - Pick< - StartupRuntimeHandlers, - 'appLifecycleRuntimeRunner' | 'runAndApplyStartupState' - > ->; +export type HeadlessStartupComposerResult = + ComposerOutputs< + Pick< + StartupRuntimeHandlers, + 'appLifecycleRuntimeRunner' | 'runAndApplyStartupState' + > + >; export function composeHeadlessStartupHandlers< TCliArgs, diff --git a/src/main/runtime/composers/jellyfin-remote-composer.test.ts b/src/main/runtime/composers/jellyfin-remote-composer.test.ts index ec6525f8..0ecce276 100644 --- a/src/main/runtime/composers/jellyfin-remote-composer.test.ts +++ b/src/main/runtime/composers/jellyfin-remote-composer.test.ts @@ -4,7 +4,13 @@ import { composeJellyfinRemoteHandlers } from './jellyfin-remote-composer'; test('composeJellyfinRemoteHandlers returns callable jellyfin remote handlers', async () => { let lastProgressAt = 0; - let activePlayback: unknown = { itemId: 'item-1', mediaSourceId: 'src-1', playMethod: 'DirectPlay', audioStreamIndex: null, subtitleStreamIndex: null }; + let activePlayback: unknown = { + itemId: 'item-1', + mediaSourceId: 'src-1', + playMethod: 'DirectPlay', + audioStreamIndex: null, + subtitleStreamIndex: null, + }; const calls: string[] = []; const composed = composeJellyfinRemoteHandlers({ diff --git a/src/main/runtime/composers/mpv-runtime-composer.test.ts b/src/main/runtime/composers/mpv-runtime-composer.test.ts index dcbd375f..ec1b8038 100644 --- a/src/main/runtime/composers/mpv-runtime-composer.test.ts +++ b/src/main/runtime/composers/mpv-runtime-composer.test.ts @@ -85,7 +85,10 @@ function createDefaultMpvFixture() { updateMpvSubtitleRenderMetricsMainDeps: { getCurrentMetrics: () => BASE_METRICS, setCurrentMetrics: () => {}, - applyPatch: (current: MpvSubtitleRenderMetrics, patch: Partial) => ({ + applyPatch: ( + current: MpvSubtitleRenderMetrics, + patch: Partial, + ) => ({ next: { ...current, ...patch }, changed: true, }), diff --git a/src/main/runtime/composers/overlay-visibility-runtime-composer.ts b/src/main/runtime/composers/overlay-visibility-runtime-composer.ts index b2daecac..2d971963 100644 --- a/src/main/runtime/composers/overlay-visibility-runtime-composer.ts +++ b/src/main/runtime/composers/overlay-visibility-runtime-composer.ts @@ -58,7 +58,8 @@ export function composeOverlayVisibilityRuntime( options: OverlayVisibilityRuntimeComposerOptions, ): OverlayVisibilityRuntimeComposerResult { return { - updateVisibleOverlayVisibility: () => options.overlayVisibilityRuntime.updateVisibleOverlayVisibility(), + updateVisibleOverlayVisibility: () => + options.overlayVisibilityRuntime.updateVisibleOverlayVisibility(), restorePreviousSecondarySubVisibility: createRestorePreviousSecondarySubVisibilityHandler( createBuildRestorePreviousSecondarySubVisibilityMainDepsHandler( options.restorePreviousSecondarySubVisibilityMainDeps, diff --git a/src/main/runtime/discord-rpc-client.ts b/src/main/runtime/discord-rpc-client.ts index b50aa08d..bcd945da 100644 --- a/src/main/runtime/discord-rpc-client.ts +++ b/src/main/runtime/discord-rpc-client.ts @@ -31,7 +31,10 @@ function requireUser(client: DiscordRpcRawClient): DiscordRpcClientUserLike { export function wrapDiscordRpcClient(client: DiscordRpcRawClient): DiscordRpcClient { return { login: () => client.login(), - setActivity: (activity) => requireUser(client).setActivity(activity).then(() => undefined), + setActivity: (activity) => + requireUser(client) + .setActivity(activity) + .then(() => undefined), clearActivity: () => requireUser(client).clearActivity(), destroy: () => client.destroy(), }; @@ -39,7 +42,12 @@ export function wrapDiscordRpcClient(client: DiscordRpcRawClient): DiscordRpcCli export function createDiscordRpcClient( clientId: string, - deps?: { createClient?: (options: { clientId: string; transport: { type: 'ipc' } }) => DiscordRpcRawClient }, + deps?: { + createClient?: (options: { + clientId: string; + transport: { type: 'ipc' }; + }) => DiscordRpcRawClient; + }, ): DiscordRpcClient { const client = deps?.createClient?.({ clientId, transport: { type: 'ipc' } }) ?? diff --git a/src/main/runtime/first-run-setup-plugin.test.ts b/src/main/runtime/first-run-setup-plugin.test.ts index 1eebb02f..e43c13b0 100644 --- a/src/main/runtime/first-run-setup-plugin.test.ts +++ b/src/main/runtime/first-run-setup-plugin.test.ts @@ -173,7 +173,10 @@ test('syncInstalledFirstRunPluginBinaryPath fills blank binary_path for existing const installPaths = resolveDefaultMpvInstallPaths('linux', homeDir, xdgConfigHome); fs.mkdirSync(path.dirname(installPaths.pluginConfigPath), { recursive: true }); - fs.writeFileSync(installPaths.pluginConfigPath, 'binary_path=\nsocket_path=/tmp/subminer-socket\n'); + fs.writeFileSync( + installPaths.pluginConfigPath, + 'binary_path=\nsocket_path=/tmp/subminer-socket\n', + ); const result = syncInstalledFirstRunPluginBinaryPath({ platform: 'linux', diff --git a/src/main/runtime/first-run-setup-plugin.ts b/src/main/runtime/first-run-setup-plugin.ts index 8a24ca24..0cda63a1 100644 --- a/src/main/runtime/first-run-setup-plugin.ts +++ b/src/main/runtime/first-run-setup-plugin.ts @@ -187,7 +187,10 @@ export function syncInstalledFirstRunPluginBinaryPath(options: { return { updated: false, configPath: installPaths.pluginConfigPath }; } - const updated = rewriteInstalledPluginBinaryPath(installPaths.pluginConfigPath, options.binaryPath); + const updated = rewriteInstalledPluginBinaryPath( + installPaths.pluginConfigPath, + options.binaryPath, + ); if (options.platform === 'win32') { rewriteInstalledWindowsPluginConfig(installPaths.pluginConfigPath); } diff --git a/src/main/runtime/first-run-setup-window.test.ts b/src/main/runtime/first-run-setup-window.test.ts index 5382e7f7..c0edfc4f 100644 --- a/src/main/runtime/first-run-setup-window.test.ts +++ b/src/main/runtime/first-run-setup-window.test.ts @@ -139,7 +139,10 @@ test('buildFirstRunSetupHtml explains external yomitan mode and keeps finish ena }); assert.match(html, /External profile configured/); - assert.match(html, /Finish stays unlocked while SubMiner is reusing an external Yomitan profile\./); + assert.match( + html, + /Finish stays unlocked while SubMiner is reusing an external Yomitan profile\./, + ); }); test('parseFirstRunSetupSubmissionUrl parses supported custom actions', () => { @@ -155,7 +158,10 @@ test('parseFirstRunSetupSubmissionUrl parses supported custom actions', () => { assert.deepEqual(parseFirstRunSetupSubmissionUrl('subminer://first-run-setup?action=refresh'), { action: 'refresh', }); - assert.equal(parseFirstRunSetupSubmissionUrl('subminer://first-run-setup?action=skip-plugin'), null); + assert.equal( + parseFirstRunSetupSubmissionUrl('subminer://first-run-setup?action=skip-plugin'), + null, + ); assert.equal(parseFirstRunSetupSubmissionUrl('https://example.com'), null); }); diff --git a/src/main/runtime/local-subtitle-selection.ts b/src/main/runtime/local-subtitle-selection.ts index 41597b62..f929cb2d 100644 --- a/src/main/runtime/local-subtitle-selection.ts +++ b/src/main/runtime/local-subtitle-selection.ts @@ -109,7 +109,9 @@ function pickBestTrackId( if (left.track.external !== right.track.external) { return left.track.external ? -1 : 1; } - if (isLikelyHearingImpaired(left.track.title) !== isLikelyHearingImpaired(right.track.title)) { + if ( + isLikelyHearingImpaired(left.track.title) !== isLikelyHearingImpaired(right.track.title) + ) { return isLikelyHearingImpaired(left.track.title) ? 1 : -1; } if (/\bdefault\b/i.test(left.track.title) !== /\bdefault\b/i.test(right.track.title)) { @@ -130,7 +132,9 @@ export function resolveManagedLocalSubtitleSelection(input: { secondaryLanguages: string[]; }): ManagedLocalSubtitleSelection { const tracks = Array.isArray(input.trackList) - ? input.trackList.map(normalizeTrack).filter((track): track is NormalizedSubtitleTrack => track !== null) + ? input.trackList + .map(normalizeTrack) + .filter((track): track is NormalizedSubtitleTrack => track !== null) : []; const preferredPrimaryLanguages = normalizeLanguageList( input.primaryLanguages, @@ -165,12 +169,10 @@ function normalizeLocalMediaPath(mediaPath: string | null | undefined): string | export function createManagedLocalSubtitleSelectionRuntime(deps: { getCurrentMediaPath: () => string | null; - getMpvClient: () => - | { - connected?: boolean; - requestProperty?: (name: string) => Promise; - } - | null; + getMpvClient: () => { + connected?: boolean; + requestProperty?: (name: string) => Promise; + } | null; getPrimarySubtitleLanguages: () => string[]; getSecondarySubtitleLanguages: () => string[]; sendMpvCommand: (command: ['set_property', 'sid' | 'secondary-sid', number]) => void; diff --git a/src/main/runtime/playlist-browser-ipc.ts b/src/main/runtime/playlist-browser-ipc.ts index 598e6693..990e2566 100644 --- a/src/main/runtime/playlist-browser-ipc.ts +++ b/src/main/runtime/playlist-browser-ipc.ts @@ -38,7 +38,8 @@ export function createPlaylistBrowserIpcRuntime( return { playlistBrowserRuntimeDeps, playlistBrowserMainDeps: { - getPlaylistBrowserSnapshot: () => getPlaylistBrowserSnapshotRuntime(playlistBrowserRuntimeDeps), + getPlaylistBrowserSnapshot: () => + getPlaylistBrowserSnapshotRuntime(playlistBrowserRuntimeDeps), appendPlaylistBrowserFile: (filePath: string) => appendPlaylistBrowserFileRuntime(playlistBrowserRuntimeDeps, filePath), playPlaylistBrowserIndex: (index: number) => diff --git a/src/main/runtime/playlist-browser-runtime.test.ts b/src/main/runtime/playlist-browser-runtime.test.ts index cce07043..b25b8821 100644 --- a/src/main/runtime/playlist-browser-runtime.test.ts +++ b/src/main/runtime/playlist-browser-runtime.test.ts @@ -102,7 +102,8 @@ function createFakeMpvClient(options: { if (removingCurrent) { syncFlags(); this.currentVideoPath = - playlist.find((item) => item.current || item.playing)?.filename ?? this.currentVideoPath; + playlist.find((item) => item.current || item.playing)?.filename ?? + this.currentVideoPath; } return true; } @@ -276,7 +277,10 @@ test('playlist-browser mutation runtimes mutate queue and return refreshed snaps ['set_property', 'sub-auto', 'fuzzy'], ['playlist-play-index', 1], ]); - assert.deepEqual(scheduled.map((entry) => entry.delayMs), [400]); + assert.deepEqual( + scheduled.map((entry) => entry.delayMs), + [400], + ); scheduled[0]?.callback(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(mpvClient.getCommands().slice(-2), [ @@ -382,10 +386,7 @@ test('movePlaylistBrowserIndexRuntime rejects top and bottom boundary moves', as const mpvClient = createFakeMpvClient({ currentVideoPath: episode1, - playlist: [ - { filename: episode1, current: true }, - { filename: episode2 }, - ], + playlist: [{ filename: episode1, current: true }, { filename: episode2 }], }); const deps = { @@ -486,17 +487,14 @@ test('playPlaylistBrowserIndexRuntime ignores superseded local subtitle rearm ca scheduled[1]?.(); await new Promise((resolve) => setTimeout(resolve, 0)); - assert.deepEqual( - mpvClient.getCommands().slice(-6), - [ - ['set_property', 'sub-auto', 'fuzzy'], - ['playlist-play-index', 1], - ['set_property', 'sub-auto', 'fuzzy'], - ['playlist-play-index', 2], - ['set_property', 'sid', 'auto'], - ['set_property', 'secondary-sid', 'auto'], - ], - ); + assert.deepEqual(mpvClient.getCommands().slice(-6), [ + ['set_property', 'sub-auto', 'fuzzy'], + ['playlist-play-index', 1], + ['set_property', 'sub-auto', 'fuzzy'], + ['playlist-play-index', 2], + ['set_property', 'sid', 'auto'], + ['set_property', 'secondary-sid', 'auto'], + ]); }); test('playPlaylistBrowserIndexRuntime aborts stale async subtitle rearm work', async (t) => { diff --git a/src/main/runtime/playlist-browser-runtime.ts b/src/main/runtime/playlist-browser-runtime.ts index 109f6589..8a2442bd 100644 --- a/src/main/runtime/playlist-browser-runtime.ts +++ b/src/main/runtime/playlist-browser-runtime.ts @@ -63,7 +63,10 @@ async function resolveCurrentFilePath( function resolveDirectorySnapshot( currentFilePath: string | null, -): Pick { +): Pick< + PlaylistBrowserSnapshot, + 'directoryAvailable' | 'directoryItems' | 'directoryPath' | 'directoryStatus' +> { if (!currentFilePath) { return { directoryAvailable: false, diff --git a/src/main/runtime/windows-mpv-launch.ts b/src/main/runtime/windows-mpv-launch.ts index 31b8418b..d50a6ab2 100644 --- a/src/main/runtime/windows-mpv-launch.ts +++ b/src/main/runtime/windows-mpv-launch.ts @@ -34,10 +34,7 @@ export function getConfiguredWindowsMpvPathStatus( return fileExists(configPath) ? 'configured' : 'invalid'; } -export function resolveWindowsMpvPath( - deps: WindowsMpvLaunchDeps, - configuredMpvPath = '', -): string { +export function resolveWindowsMpvPath(deps: WindowsMpvLaunchDeps, configuredMpvPath = ''): string { const configPath = normalizeCandidate(configuredMpvPath); const configuredPathStatus = getConfiguredWindowsMpvPathStatus(configPath, deps.fileExists); if (configuredPathStatus === 'configured') { @@ -178,9 +175,7 @@ export function createWindowsMpvLaunchDeps(options: { error: result.error ?? undefined, }; }, - fileExists: - options.fileExists ?? - defaultWindowsMpvFileExists, + fileExists: options.fileExists ?? defaultWindowsMpvFileExists, spawnDetached: (command, args) => new Promise((resolve, reject) => { try { diff --git a/src/main/runtime/youtube-playback-runtime.ts b/src/main/runtime/youtube-playback-runtime.ts index f3000742..3d2554fb 100644 --- a/src/main/runtime/youtube-playback-runtime.ts +++ b/src/main/runtime/youtube-playback-runtime.ts @@ -92,7 +92,9 @@ export function createYoutubePlaybackRuntime(deps: YoutubePlaybackRuntimeDeps) { ]); launchedWindowsMpv = launchResult.ok; if (launchResult.ok && launchResult.mpvPath) { - deps.logInfo(`Bootstrapping Windows mpv for YouTube playback via ${launchResult.mpvPath}`); + deps.logInfo( + `Bootstrapping Windows mpv for YouTube playback via ${launchResult.mpvPath}`, + ); } if (!launchResult.ok) { deps.logWarn('Unable to bootstrap Windows mpv for YouTube playback.'); diff --git a/src/renderer/handlers/keyboard.ts b/src/renderer/handlers/keyboard.ts index 8bb1e22a..bd97e213 100644 --- a/src/renderer/handlers/keyboard.ts +++ b/src/renderer/handlers/keyboard.ts @@ -358,12 +358,10 @@ export function createKeyboardHandlers( }); } - function isSubtitleSeekCommand(command: (string | number)[] | undefined): command is [string, number] { - return ( - Array.isArray(command) && - command[0] === 'sub-seek' && - typeof command[1] === 'number' - ); + function isSubtitleSeekCommand( + command: (string | number)[] | undefined, + ): command is [string, number] { + return Array.isArray(command) && command[0] === 'sub-seek' && typeof command[1] === 'number'; } function dispatchConfiguredMpvCommand(command: (string | number)[]): void { diff --git a/src/renderer/modals/playlist-browser-renderer.ts b/src/renderer/modals/playlist-browser-renderer.ts index d3e9d24d..4faef959 100644 --- a/src/renderer/modals/playlist-browser-renderer.ts +++ b/src/renderer/modals/playlist-browser-renderer.ts @@ -1,7 +1,4 @@ -import type { - PlaylistBrowserDirectoryItem, - PlaylistBrowserQueueItem, -} from '../../types'; +import type { PlaylistBrowserDirectoryItem, PlaylistBrowserQueueItem } from '../../types'; import type { RendererContext } from '../context'; type PlaylistBrowserRowRenderActions = { @@ -55,7 +52,7 @@ export function renderPlaylistBrowserDirectoryRow( ? item.episodeLabel ? `${item.episodeLabel} · Current file` : 'Current file' - : item.episodeLabel ?? 'Video file'; + : (item.episodeLabel ?? 'Video file'); main.append(label, meta); const trailing = document.createElement('div'); diff --git a/src/renderer/modals/playlist-browser.test.ts b/src/renderer/modals/playlist-browser.test.ts index 28623001..c15fd7bc 100644 --- a/src/renderer/modals/playlist-browser.test.ts +++ b/src/renderer/modals/playlist-browser.test.ts @@ -236,9 +236,17 @@ function createPlaylistBrowserElectronApi(overrides?: Partial): Ele notifyOverlayModalClosed: () => {}, focusMainWindow: async () => {}, setIgnoreMouseEvents: () => {}, - appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }), + appendPlaylistBrowserFile: async () => ({ + ok: true, + message: 'ok', + snapshot: createSnapshot(), + }), playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }), - removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }), + removePlaylistBrowserIndex: async () => ({ + ok: true, + message: 'ok', + snapshot: createSnapshot(), + }), movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }), ...overrides, } as ElectronAPI; @@ -348,15 +356,13 @@ test('playlist browser modal action buttons stop double-click propagation', asyn await modal.openPlaylistBrowserModal(); - const row = - env.dom.playlistBrowserDirectoryList.children[0] as - | ReturnType - | undefined; + const row = env.dom.playlistBrowserDirectoryList.children[0] as + | ReturnType + | undefined; const trailing = row?.children?.[1] as ReturnType | undefined; - const button = - trailing?.children?.at(-1) as - | { listeners?: Map void>> } - | undefined; + const button = trailing?.children?.at(-1) as + | { listeners?: Map void>> } + | undefined; const dblclickHandler = button?.listeners?.get('dblclick')?.[0]; assert.equal(typeof dblclickHandler, 'function'); diff --git a/src/renderer/modals/playlist-browser.ts b/src/renderer/modals/playlist-browser.ts index 0fc3a81b..63308ca2 100644 --- a/src/renderer/modals/playlist-browser.ts +++ b/src/renderer/modals/playlist-browser.ts @@ -31,7 +31,8 @@ function getDefaultDirectorySelectionIndex(snapshot: PlaylistBrowserSnapshot): n function getDefaultPlaylistSelectionIndex(snapshot: PlaylistBrowserSnapshot): number { const playlistIndex = - snapshot.playingIndex ?? snapshot.playlistItems.findIndex((item) => item.current || item.playing); + snapshot.playingIndex ?? + snapshot.playlistItems.findIndex((item) => item.current || item.playing); return clampIndex(playlistIndex >= 0 ? playlistIndex : 0, snapshot.playlistItems.length); } @@ -225,7 +226,10 @@ export function createPlaylistBrowserModal( } async function removePlaylistItem(index: number): Promise { - await handleMutation(window.electronAPI.removePlaylistBrowserIndex(index), 'Removed queue item'); + await handleMutation( + window.electronAPI.removePlaylistBrowserIndex(index), + 'Removed queue item', + ); } async function movePlaylistItem(index: number, direction: 1 | -1): Promise { diff --git a/src/renderer/style.css b/src/renderer/style.css index 0e6fb3b1..5ca4db41 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -453,8 +453,7 @@ body { padding: 14px; border-radius: 16px; border: 1px solid rgba(110, 115, 141, 0.16); - background: - linear-gradient(180deg, rgba(54, 58, 79, 0.55), rgba(36, 39, 58, 0.6)); + background: linear-gradient(180deg, rgba(54, 58, 79, 0.55), rgba(36, 39, 58, 0.6)); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); } @@ -496,8 +495,12 @@ body { } .playlist-browser-row.current { - background: - linear-gradient(90deg, rgba(138, 173, 244, 0.12), rgba(138, 173, 244, 0.03) 28%, transparent); + background: linear-gradient( + 90deg, + rgba(138, 173, 244, 0.12), + rgba(138, 173, 244, 0.03) 28%, + transparent + ); box-shadow: inset 3px 0 0 #8aadf4; } diff --git a/src/renderer/utils/dom.ts b/src/renderer/utils/dom.ts index 7ae03319..8f2084c4 100644 --- a/src/renderer/utils/dom.ts +++ b/src/renderer/utils/dom.ts @@ -222,8 +222,12 @@ export function resolveRendererDom(): RendererDom { playlistBrowserModal: getRequiredElement('playlistBrowserModal'), playlistBrowserTitle: getRequiredElement('playlistBrowserTitle'), playlistBrowserStatus: getRequiredElement('playlistBrowserStatus'), - playlistBrowserDirectoryList: getRequiredElement('playlistBrowserDirectoryList'), - playlistBrowserPlaylistList: getRequiredElement('playlistBrowserPlaylistList'), + playlistBrowserDirectoryList: getRequiredElement( + 'playlistBrowserDirectoryList', + ), + playlistBrowserPlaylistList: getRequiredElement( + 'playlistBrowserPlaylistList', + ), playlistBrowserClose: getRequiredElement('playlistBrowserClose'), }; } diff --git a/src/runtime-options.test.ts b/src/runtime-options.test.ts index 6c1e8480..4199dc79 100644 --- a/src/runtime-options.test.ts +++ b/src/runtime-options.test.ts @@ -47,13 +47,10 @@ test('RuntimeOptionsManager returns detached effective Anki config copies', () = }, }; - const manager = new RuntimeOptionsManager( - () => structuredClone(baseConfig), - { - applyAnkiPatch: () => undefined, - onOptionsChanged: () => undefined, - }, - ); + const manager = new RuntimeOptionsManager(() => structuredClone(baseConfig), { + applyAnkiPatch: () => undefined, + onOptionsChanged: () => undefined, + }); const effective = manager.getEffectiveAnkiConnectConfig(); effective.tags!.push('mutated');