Files
SubMiner/src/main/runtime/anime-browser-ipc-handlers.ts
T
sudacode 2512e0f3af feat(anime): add anime browser powered by Aniyomi extensions
- Add `subminer anime` / `--anime` and a tray entry to open a browser that searches installed Aniyomi extension sources, shows cover art and episodes, and plays into mpv with overlay/mining attached
- Add an Extensions tab to add repos and install/update/remove sources, and per-source settings for sources needing config
- Support searching all sources at once with streaming, per-source results and status
- Prefer Japanese audio/subtitle tracks from the source and keep the primary subtitle slot reserved for Japanese
- Fix window/tray/Dock handling so the browser and mpv can be switched between without quitting the app or losing the Dock icon
- Add anime.repos, anime.extensionsDir, anime.preferredQuality config keys (no bundled repos or discovery)
2026-08-14 22:38:31 -07:00

76 lines
3.2 KiB
TypeScript

import { IPC_CHANNELS } from '../../shared/ipc/contracts';
import type { AnimeBrowserRuntime } from './anime-browser-runtime';
import type { AnimeBrowserPlayRequest } from '../../types/anime-browser';
export interface AnimeBrowserIpcDeps {
// Structurally typed so tests can pass a fake without importing Electron.
ipcMain: {
handle(channel: string, listener: (event: unknown, ...args: unknown[]) => unknown): unknown;
};
runtime: AnimeBrowserRuntime;
}
/**
* Bridge the renderer to the anime runtime. Page arguments arrive as `unknown`
* from the renderer, so they are coerced here rather than trusted.
*/
export function registerAnimeBrowserIpcHandlers(deps: AnimeBrowserIpcDeps): void {
const channels = IPC_CHANNELS.request;
const { runtime } = deps;
const handle = (
channel: string,
listener: (event: unknown, ...args: unknown[]) => unknown,
): void => {
deps.ipcMain.handle(channel, listener);
};
handle(channels.animeBrowserGetSnapshot, () => runtime.getSnapshot());
handle(channels.animeBrowserEnsureBridge, () => runtime.ensureBridge());
handle(channels.animeBrowserSelectSource, (_event, sourceId) =>
runtime.selectSource(String(sourceId)),
);
handle(channels.animeBrowserSearch, (_event, query, page) =>
runtime.search(String(query ?? ''), toPage(page)),
);
handle(channels.animeBrowserGetPopular, (_event, page) => runtime.getPopular(toPage(page)));
handle(channels.animeBrowserGetDetails, (_event, animeUrl, sourceId) =>
runtime.getDetails(String(animeUrl), toOptionalId(sourceId)),
);
handle(channels.animeBrowserGetEpisodes, (_event, animeUrl, sourceId) =>
runtime.getEpisodes(String(animeUrl), toOptionalId(sourceId)),
);
handle(channels.animeBrowserListAvailableExtensions, () => runtime.listAvailableExtensions());
handle(channels.animeBrowserInstallExtension, (_event, pkg) =>
runtime.installExtension(String(pkg)),
);
handle(channels.animeBrowserRemoveExtension, (_event, pkg) =>
runtime.removeExtension(String(pkg)),
);
handle(channels.animeBrowserRescanExtensions, () => runtime.rescanExtensions());
handle(channels.animeBrowserAddRepo, (_event, url) => runtime.addRepo(String(url)));
handle(channels.animeBrowserRemoveRepo, (_event, url) => runtime.removeRepo(String(url)));
handle(channels.animeBrowserPlayEpisode, (_event, request) =>
runtime.playEpisode(request as AnimeBrowserPlayRequest),
);
handle(channels.animeBrowserGetPreferences, (_event, sourceId) =>
runtime.getPreferences(String(sourceId)),
);
handle(channels.animeBrowserSetPreference, (_event, sourceId, key, value) =>
runtime.setPreference(String(sourceId), String(key), value as string | string[] | boolean),
);
}
/**
* A source id the renderer may omit. Absent means "use the current selection",
* so an empty value must stay undefined rather than becoming the string "".
*/
function toOptionalId(value: unknown): string | undefined {
return typeof value === 'string' && value.length > 0 ? value : undefined;
}
/** Bridge pages are 1-based; anything unusable falls back to the first page. */
function toPage(value: unknown): number {
const page = Number(value);
return Number.isFinite(page) && page >= 1 ? Math.floor(page) : 1;
}