mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-02 18:22:42 -08:00
feat(core): add Electron runtime, services, and app composition
This commit is contained in:
185
src/core/services/anki-jimaku-ipc.ts
Normal file
185
src/core/services/anki-jimaku-ipc.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { createLogger } from '../../logger';
|
||||
import {
|
||||
JimakuApiResponse,
|
||||
JimakuDownloadResult,
|
||||
JimakuEntry,
|
||||
JimakuFileEntry,
|
||||
JimakuFilesQuery,
|
||||
JimakuMediaInfo,
|
||||
JimakuSearchQuery,
|
||||
KikuFieldGroupingChoice,
|
||||
KikuMergePreviewRequest,
|
||||
KikuMergePreviewResponse,
|
||||
} from '../../types';
|
||||
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
||||
import {
|
||||
parseJimakuDownloadQuery,
|
||||
parseJimakuFilesQuery,
|
||||
parseJimakuSearchQuery,
|
||||
parseKikuFieldGroupingChoice,
|
||||
parseKikuMergePreviewRequest,
|
||||
} from '../../shared/ipc/validators';
|
||||
|
||||
const logger = createLogger('main:anki-jimaku-ipc');
|
||||
|
||||
export interface AnkiJimakuIpcDeps {
|
||||
setAnkiConnectEnabled: (enabled: boolean) => void;
|
||||
clearAnkiHistory: () => void;
|
||||
refreshKnownWords: () => Promise<void> | void;
|
||||
respondFieldGrouping: (choice: KikuFieldGroupingChoice) => void;
|
||||
buildKikuMergePreview: (request: KikuMergePreviewRequest) => Promise<KikuMergePreviewResponse>;
|
||||
getJimakuMediaInfo: () => JimakuMediaInfo;
|
||||
searchJimakuEntries: (query: JimakuSearchQuery) => Promise<JimakuApiResponse<JimakuEntry[]>>;
|
||||
listJimakuFiles: (query: JimakuFilesQuery) => Promise<JimakuApiResponse<JimakuFileEntry[]>>;
|
||||
resolveJimakuApiKey: () => Promise<string | null>;
|
||||
getCurrentMediaPath: () => string | null;
|
||||
isRemoteMediaPath: (mediaPath: string) => boolean;
|
||||
downloadToFile: (
|
||||
url: string,
|
||||
destPath: string,
|
||||
headers: Record<string, string>,
|
||||
) => Promise<JimakuDownloadResult>;
|
||||
onDownloadedSubtitle: (pathToSubtitle: string) => void;
|
||||
}
|
||||
|
||||
interface IpcMainRegistrar {
|
||||
on: (channel: string, listener: (event: unknown, ...args: unknown[]) => void) => void;
|
||||
handle: (channel: string, listener: (event: unknown, ...args: unknown[]) => unknown) => void;
|
||||
}
|
||||
|
||||
export function registerAnkiJimakuIpcHandlers(
|
||||
deps: AnkiJimakuIpcDeps,
|
||||
ipc: IpcMainRegistrar = ipcMain,
|
||||
): void {
|
||||
ipc.on(IPC_CHANNELS.command.setAnkiConnectEnabled, (_event: unknown, enabled: unknown) => {
|
||||
if (typeof enabled !== 'boolean') return;
|
||||
deps.setAnkiConnectEnabled(enabled);
|
||||
});
|
||||
|
||||
ipc.on(IPC_CHANNELS.command.clearAnkiConnectHistory, () => {
|
||||
deps.clearAnkiHistory();
|
||||
});
|
||||
|
||||
ipc.on(IPC_CHANNELS.command.refreshKnownWords, async () => {
|
||||
await deps.refreshKnownWords();
|
||||
});
|
||||
|
||||
ipc.on(IPC_CHANNELS.command.kikuFieldGroupingRespond, (_event: unknown, choice: unknown) => {
|
||||
const parsedChoice = parseKikuFieldGroupingChoice(choice);
|
||||
if (!parsedChoice) return;
|
||||
deps.respondFieldGrouping(parsedChoice);
|
||||
});
|
||||
|
||||
ipc.handle(
|
||||
IPC_CHANNELS.request.kikuBuildMergePreview,
|
||||
async (_event, request: unknown): Promise<KikuMergePreviewResponse> => {
|
||||
const parsedRequest = parseKikuMergePreviewRequest(request);
|
||||
if (!parsedRequest) {
|
||||
return { ok: false, error: 'Invalid merge preview request payload' };
|
||||
}
|
||||
return deps.buildKikuMergePreview(parsedRequest);
|
||||
},
|
||||
);
|
||||
|
||||
ipc.handle(IPC_CHANNELS.request.jimakuGetMediaInfo, (): JimakuMediaInfo => {
|
||||
return deps.getJimakuMediaInfo();
|
||||
});
|
||||
|
||||
ipc.handle(
|
||||
IPC_CHANNELS.request.jimakuSearchEntries,
|
||||
async (_event, query: unknown): Promise<JimakuApiResponse<JimakuEntry[]>> => {
|
||||
const parsedQuery = parseJimakuSearchQuery(query);
|
||||
if (!parsedQuery) {
|
||||
return { ok: false, error: { error: 'Invalid Jimaku search query payload', code: 400 } };
|
||||
}
|
||||
return deps.searchJimakuEntries(parsedQuery);
|
||||
},
|
||||
);
|
||||
|
||||
ipc.handle(
|
||||
IPC_CHANNELS.request.jimakuListFiles,
|
||||
async (_event, query: unknown): Promise<JimakuApiResponse<JimakuFileEntry[]>> => {
|
||||
const parsedQuery = parseJimakuFilesQuery(query);
|
||||
if (!parsedQuery) {
|
||||
return { ok: false, error: { error: 'Invalid Jimaku files query payload', code: 400 } };
|
||||
}
|
||||
return deps.listJimakuFiles(parsedQuery);
|
||||
},
|
||||
);
|
||||
|
||||
ipc.handle(
|
||||
IPC_CHANNELS.request.jimakuDownloadFile,
|
||||
async (_event, query: unknown): Promise<JimakuDownloadResult> => {
|
||||
const parsedQuery = parseJimakuDownloadQuery(query);
|
||||
if (!parsedQuery) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
error: 'Invalid Jimaku download query payload',
|
||||
code: 400,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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(parsedQuery.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-${parsedQuery.entryId})${ext}`);
|
||||
let counter = 2;
|
||||
while (fs.existsSync(targetPath)) {
|
||||
targetPath = path.join(
|
||||
mediaDir,
|
||||
`${baseName} (jimaku-${parsedQuery.entryId}-${counter})${ext}`,
|
||||
);
|
||||
counter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[jimaku] download-file name="${parsedQuery.name}" entryId=${parsedQuery.entryId}`,
|
||||
);
|
||||
const result = await deps.downloadToFile(parsedQuery.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;
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user