import { ipcMain, IpcMainEvent } from "electron"; import * as fs from "fs"; import * as path from "path"; import * as os from "os"; import { createLogger } from "../../logger"; import { JimakuApiResponse, JimakuDownloadQuery, JimakuDownloadResult, JimakuEntry, JimakuFileEntry, JimakuFilesQuery, JimakuMediaInfo, JimakuSearchQuery, KikuFieldGroupingChoice, KikuMergePreviewRequest, KikuMergePreviewResponse, } from "../../types"; const logger = createLogger("main:anki-jimaku-ipc"); export interface AnkiJimakuIpcDeps { setAnkiConnectEnabled: (enabled: boolean) => void; clearAnkiHistory: () => void; refreshKnownWords: () => Promise | void; respondFieldGrouping: (choice: KikuFieldGroupingChoice) => void; buildKikuMergePreview: ( request: KikuMergePreviewRequest, ) => Promise; getJimakuMediaInfo: () => JimakuMediaInfo; searchJimakuEntries: ( query: JimakuSearchQuery, ) => Promise>; listJimakuFiles: ( query: JimakuFilesQuery, ) => Promise>; resolveJimakuApiKey: () => Promise; getCurrentMediaPath: () => string | null; isRemoteMediaPath: (mediaPath: string) => boolean; downloadToFile: ( url: string, destPath: string, headers: Record, ) => Promise; onDownloadedSubtitle: (pathToSubtitle: string) => void; } export function registerAnkiJimakuIpcHandlers(deps: AnkiJimakuIpcDeps): void { ipcMain.on( "set-anki-connect-enabled", (_event: IpcMainEvent, enabled: boolean) => { deps.setAnkiConnectEnabled(enabled); }, ); ipcMain.on("clear-anki-connect-history", () => { deps.clearAnkiHistory(); }); ipcMain.on("anki:refresh-known-words", async () => { await deps.refreshKnownWords(); }); ipcMain.on( "kiku:field-grouping-respond", (_event: IpcMainEvent, choice: KikuFieldGroupingChoice) => { deps.respondFieldGrouping(choice); }, ); ipcMain.handle( "kiku:build-merge-preview", async ( _event, request: KikuMergePreviewRequest, ): Promise => { return deps.buildKikuMergePreview(request); }, ); ipcMain.handle("jimaku:get-media-info", (): JimakuMediaInfo => { return deps.getJimakuMediaInfo(); }); ipcMain.handle( "jimaku:search-entries", async ( _event, query: JimakuSearchQuery, ): Promise> => { return deps.searchJimakuEntries(query); }, ); ipcMain.handle( "jimaku:list-files", async ( _event, query: JimakuFilesQuery, ): Promise> => { return deps.listJimakuFiles(query); }, ); ipcMain.handle( "jimaku:download-file", async ( _event, query: JimakuDownloadQuery, ): Promise => { const apiKey = await deps.resolveJimakuApiKey(); if (!apiKey) { return { ok: false, error: { error: "Jimaku API key not set. Configure jimaku.apiKey or jimaku.apiKeyCommand.", code: 401, }, }; } const currentMediaPath = deps.getCurrentMediaPath(); if (!currentMediaPath) { return { ok: false, error: { error: "No media file loaded in MPV." } }; } const mediaDir = deps.isRemoteMediaPath(currentMediaPath) ? fs.mkdtempSync(path.join(os.tmpdir(), "subminer-jimaku-")) : path.dirname(path.resolve(currentMediaPath)); const safeName = path.basename(query.name); if (!safeName) { return { ok: false, error: { error: "Invalid subtitle filename." } }; } const ext = path.extname(safeName); const baseName = ext ? safeName.slice(0, -ext.length) : safeName; let targetPath = path.join(mediaDir, safeName); if (fs.existsSync(targetPath)) { targetPath = path.join( mediaDir, `${baseName} (jimaku-${query.entryId})${ext}`, ); let counter = 2; while (fs.existsSync(targetPath)) { targetPath = path.join( mediaDir, `${baseName} (jimaku-${query.entryId}-${counter})${ext}`, ); counter += 1; } } logger.info( `[jimaku] download-file name="${query.name}" entryId=${query.entryId}`, ); const result = await deps.downloadToFile(query.url, targetPath, { Authorization: apiKey, "User-Agent": "SubMiner", }); if (result.ok) { logger.info(`[jimaku] download-file saved to ${result.path}`); deps.onDownloadedSubtitle(result.path); } else { logger.error( `[jimaku] download-file failed: ${result.error?.error ?? "unknown error"}`, ); } return result; }, ); }