mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-01 07:21:33 -07:00
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)
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { BridgePreference } from './types';
|
||||
|
||||
/**
|
||||
* Persists each source's preference array verbatim, keyed by bridge source id.
|
||||
*
|
||||
* Extensions keep credentials in here (the Jellyfin source stores a password),
|
||||
* so the file is written with owner-only permissions.
|
||||
*/
|
||||
export class PreferenceStore {
|
||||
private readonly file: string;
|
||||
private cache: Record<string, BridgePreference[]> | null = null;
|
||||
|
||||
constructor(file: string) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
private async load(): Promise<Record<string, BridgePreference[]>> {
|
||||
if (this.cache !== null) return this.cache;
|
||||
try {
|
||||
const parsed = JSON.parse(await readFile(this.file, 'utf8')) as unknown;
|
||||
this.cache =
|
||||
parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? (parsed as Record<string, BridgePreference[]>)
|
||||
: {};
|
||||
} catch {
|
||||
// Missing or corrupt file starts empty rather than blocking the browser.
|
||||
this.cache = {};
|
||||
}
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
async get(sourceId: string): Promise<BridgePreference[]> {
|
||||
const all = await this.load();
|
||||
return all[sourceId] ?? [];
|
||||
}
|
||||
|
||||
async set(sourceId: string, preferences: BridgePreference[]): Promise<void> {
|
||||
const all = await this.load();
|
||||
all[sourceId] = preferences;
|
||||
await this.persist(all);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop every saved value whose key starts with `prefix`.
|
||||
*
|
||||
* Removing an extension should not leave its credentials on disk, and a
|
||||
* source id is not knowable once the APK is gone — so callers pass the
|
||||
* package name and this clears anything recorded under it.
|
||||
*/
|
||||
async clear(prefix: string): Promise<void> {
|
||||
const all = await this.load();
|
||||
let changed = false;
|
||||
for (const key of Object.keys(all)) {
|
||||
if (key === prefix || key.startsWith(`${prefix}:`)) {
|
||||
delete all[key];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) await this.persist(all);
|
||||
}
|
||||
|
||||
private async persist(all: Record<string, BridgePreference[]>): Promise<void> {
|
||||
await mkdir(path.dirname(this.file), { recursive: true });
|
||||
await writeFile(this.file, JSON.stringify(all, null, 2), { mode: 0o600 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user