mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor: extract anki and jimaku ipc handlers
This commit is contained in:
169
src/core/services/anki-jimaku-ipc-service.ts
Normal file
169
src/core/services/anki-jimaku-ipc-service.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { ipcMain, IpcMainEvent } from "electron";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import {
|
||||||
|
JimakuApiResponse,
|
||||||
|
JimakuDownloadQuery,
|
||||||
|
JimakuDownloadResult,
|
||||||
|
JimakuEntry,
|
||||||
|
JimakuFileEntry,
|
||||||
|
JimakuFilesQuery,
|
||||||
|
JimakuMediaInfo,
|
||||||
|
JimakuSearchQuery,
|
||||||
|
KikuFieldGroupingChoice,
|
||||||
|
KikuMergePreviewRequest,
|
||||||
|
KikuMergePreviewResponse,
|
||||||
|
} from "../../types";
|
||||||
|
|
||||||
|
export interface AnkiJimakuIpcDeps {
|
||||||
|
setAnkiConnectEnabled: (enabled: boolean) => void;
|
||||||
|
clearAnkiHistory: () => 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
"kiku:field-grouping-respond",
|
||||||
|
(_event: IpcMainEvent, choice: KikuFieldGroupingChoice) => {
|
||||||
|
deps.respondFieldGrouping(choice);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
"kiku:build-merge-preview",
|
||||||
|
async (
|
||||||
|
_event,
|
||||||
|
request: KikuMergePreviewRequest,
|
||||||
|
): Promise<KikuMergePreviewResponse> => {
|
||||||
|
return deps.buildKikuMergePreview(request);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle("jimaku:get-media-info", (): JimakuMediaInfo => {
|
||||||
|
return deps.getJimakuMediaInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
"jimaku:search-entries",
|
||||||
|
async (
|
||||||
|
_event,
|
||||||
|
query: JimakuSearchQuery,
|
||||||
|
): Promise<JimakuApiResponse<JimakuEntry[]>> => {
|
||||||
|
return deps.searchJimakuEntries(query);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
"jimaku:list-files",
|
||||||
|
async (
|
||||||
|
_event,
|
||||||
|
query: JimakuFilesQuery,
|
||||||
|
): Promise<JimakuApiResponse<JimakuFileEntry[]>> => {
|
||||||
|
return deps.listJimakuFiles(query);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
"jimaku:download-file",
|
||||||
|
async (_event, query: JimakuDownloadQuery): Promise<JimakuDownloadResult> => {
|
||||||
|
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." } };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deps.isRemoteMediaPath(currentMediaPath)) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: { error: "Cannot download subtitles for remote media paths." },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaDir = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[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) {
|
||||||
|
console.log(`[jimaku] download-file saved to ${result.path}`);
|
||||||
|
deps.onDownloadedSubtitle(result.path);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`[jimaku] download-file failed: ${result.error?.error ?? "unknown error"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
132
src/main.ts
132
src/main.ts
@@ -19,13 +19,11 @@ import {
|
|||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
session,
|
session,
|
||||||
ipcMain,
|
|
||||||
globalShortcut,
|
globalShortcut,
|
||||||
clipboard,
|
clipboard,
|
||||||
shell,
|
shell,
|
||||||
protocol,
|
protocol,
|
||||||
screen,
|
screen,
|
||||||
IpcMainEvent,
|
|
||||||
Extension,
|
Extension,
|
||||||
} from "electron";
|
} from "electron";
|
||||||
|
|
||||||
@@ -60,10 +58,7 @@ import {
|
|||||||
JimakuDownloadResult,
|
JimakuDownloadResult,
|
||||||
JimakuEntry,
|
JimakuEntry,
|
||||||
JimakuFileEntry,
|
JimakuFileEntry,
|
||||||
JimakuFilesQuery,
|
|
||||||
JimakuMediaInfo,
|
JimakuMediaInfo,
|
||||||
JimakuSearchQuery,
|
|
||||||
JimakuDownloadQuery,
|
|
||||||
JimakuConfig,
|
JimakuConfig,
|
||||||
JimakuLanguagePreference,
|
JimakuLanguagePreference,
|
||||||
SubtitleData,
|
SubtitleData,
|
||||||
@@ -153,6 +148,7 @@ import {
|
|||||||
updateInvisibleOverlayVisibilityService,
|
updateInvisibleOverlayVisibilityService,
|
||||||
updateVisibleOverlayVisibilityService,
|
updateVisibleOverlayVisibilityService,
|
||||||
} from "./core/services/overlay-visibility-service";
|
} from "./core/services/overlay-visibility-service";
|
||||||
|
import { registerAnkiJimakuIpcHandlers } from "./core/services/anki-jimaku-ipc-service";
|
||||||
import {
|
import {
|
||||||
ConfigService,
|
ConfigService,
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
@@ -3298,9 +3294,8 @@ function sendToVisibleOverlay(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.on(
|
registerAnkiJimakuIpcHandlers({
|
||||||
"set-anki-connect-enabled",
|
setAnkiConnectEnabled: (enabled) => {
|
||||||
(_event: IpcMainEvent, enabled: boolean) => {
|
|
||||||
configService.patchRawConfig({
|
configService.patchRawConfig({
|
||||||
ankiConnect: {
|
ankiConnect: {
|
||||||
enabled,
|
enabled,
|
||||||
@@ -3338,31 +3333,19 @@ ipcMain.on(
|
|||||||
|
|
||||||
broadcastRuntimeOptionsChanged();
|
broadcastRuntimeOptionsChanged();
|
||||||
},
|
},
|
||||||
);
|
clearAnkiHistory: () => {
|
||||||
|
|
||||||
ipcMain.on("clear-anki-connect-history", () => {
|
|
||||||
if (subtitleTimingTracker) {
|
if (subtitleTimingTracker) {
|
||||||
subtitleTimingTracker.cleanup();
|
subtitleTimingTracker.cleanup();
|
||||||
console.log("AnkiConnect subtitle timing history cleared");
|
console.log("AnkiConnect subtitle timing history cleared");
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
respondFieldGrouping: (choice) => {
|
||||||
ipcMain.on(
|
|
||||||
"kiku:field-grouping-respond",
|
|
||||||
(_event: IpcMainEvent, choice: KikuFieldGroupingChoice) => {
|
|
||||||
if (fieldGroupingResolver) {
|
if (fieldGroupingResolver) {
|
||||||
fieldGroupingResolver(choice);
|
fieldGroupingResolver(choice);
|
||||||
fieldGroupingResolver = null;
|
fieldGroupingResolver = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
buildKikuMergePreview: async (request) => {
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
"kiku:build-merge-preview",
|
|
||||||
async (
|
|
||||||
_event,
|
|
||||||
request: KikuMergePreviewRequest,
|
|
||||||
): Promise<KikuMergePreviewResponse> => {
|
|
||||||
if (!ankiIntegration) {
|
if (!ankiIntegration) {
|
||||||
return { ok: false, error: "AnkiConnect integration not enabled" };
|
return { ok: false, error: "AnkiConnect integration not enabled" };
|
||||||
}
|
}
|
||||||
@@ -3372,18 +3355,8 @@ ipcMain.handle(
|
|||||||
request.deleteDuplicate,
|
request.deleteDuplicate,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
getJimakuMediaInfo: () => parseMediaInfo(currentMediaPath),
|
||||||
|
searchJimakuEntries: async (query) => {
|
||||||
ipcMain.handle("jimaku:get-media-info", (): JimakuMediaInfo => {
|
|
||||||
return parseMediaInfo(currentMediaPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
"jimaku:search-entries",
|
|
||||||
async (
|
|
||||||
_event,
|
|
||||||
query: JimakuSearchQuery,
|
|
||||||
): Promise<JimakuApiResponse<JimakuEntry[]>> => {
|
|
||||||
console.log(`[jimaku] search-entries query: "${query.query}"`);
|
console.log(`[jimaku] search-entries query: "${query.query}"`);
|
||||||
const response = await jimakuFetchJson<JimakuEntry[]>(
|
const response = await jimakuFetchJson<JimakuEntry[]>(
|
||||||
"/api/entries/search",
|
"/api/entries/search",
|
||||||
@@ -3399,14 +3372,7 @@ ipcMain.handle(
|
|||||||
);
|
);
|
||||||
return { ok: true, data: response.data.slice(0, maxResults) };
|
return { ok: true, data: response.data.slice(0, maxResults) };
|
||||||
},
|
},
|
||||||
);
|
listJimakuFiles: async (query) => {
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
"jimaku:list-files",
|
|
||||||
async (
|
|
||||||
_event,
|
|
||||||
query: JimakuFilesQuery,
|
|
||||||
): Promise<JimakuApiResponse<JimakuFileEntry[]>> => {
|
|
||||||
console.log(
|
console.log(
|
||||||
`[jimaku] list-files entryId=${query.entryId} episode=${query.episode ?? "all"}`,
|
`[jimaku] list-files entryId=${query.entryId} episode=${query.episode ?? "all"}`,
|
||||||
);
|
);
|
||||||
@@ -3424,77 +3390,13 @@ ipcMain.handle(
|
|||||||
console.log(`[jimaku] list-files returned ${sorted.length} files`);
|
console.log(`[jimaku] list-files returned ${sorted.length} files`);
|
||||||
return { ok: true, data: sorted };
|
return { ok: true, data: sorted };
|
||||||
},
|
},
|
||||||
);
|
resolveJimakuApiKey: () => resolveJimakuApiKey(),
|
||||||
|
getCurrentMediaPath: () => currentMediaPath,
|
||||||
ipcMain.handle(
|
isRemoteMediaPath: (mediaPath) => isRemoteMediaPath(mediaPath),
|
||||||
"jimaku:download-file",
|
downloadToFile: (url, destPath, headers) => downloadToFile(url, destPath, headers),
|
||||||
async (_event, query: JimakuDownloadQuery): Promise<JimakuDownloadResult> => {
|
onDownloadedSubtitle: (pathToSubtitle) => {
|
||||||
const apiKey = await resolveJimakuApiKey();
|
|
||||||
if (!apiKey) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
error: {
|
|
||||||
error:
|
|
||||||
"Jimaku API key not set. Configure jimaku.apiKey or jimaku.apiKeyCommand.",
|
|
||||||
code: 401,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentMediaPath) {
|
|
||||||
return { ok: false, error: { error: "No media file loaded in MPV." } };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRemoteMediaPath(currentMediaPath)) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
error: { error: "Cannot download subtitles for remote media paths." },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mediaDir = 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[jimaku] download-file name="${query.name}" entryId=${query.entryId}`,
|
|
||||||
);
|
|
||||||
const result = await downloadToFile(query.url, targetPath, {
|
|
||||||
Authorization: apiKey,
|
|
||||||
"User-Agent": "SubMiner",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.ok) {
|
|
||||||
console.log(`[jimaku] download-file saved to ${result.path}`);
|
|
||||||
if (mpvClient && mpvClient.connected) {
|
if (mpvClient && mpvClient.connected) {
|
||||||
mpvClient.send({ command: ["sub-add", result.path, "select"] });
|
mpvClient.send({ command: ["sub-add", pathToSubtitle, "select"] });
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
`[jimaku] download-file failed: ${result.error?.error ?? "unknown error"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user