mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-31 19: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,150 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readdir, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
/**
|
||||
* Locates the M-Extension-Server release bundle for the host platform. Each
|
||||
* bundle ships a matching JRE alongside the server JAR, so no system JDK is
|
||||
* required.
|
||||
*/
|
||||
|
||||
export const BUNDLE_RELEASES_URL =
|
||||
'https://api.github.com/repos/1Selxo/M-Extension-Server/releases?page=1&per_page=10';
|
||||
|
||||
/**
|
||||
* The bridge release this integration was verified against.
|
||||
*
|
||||
* Upstream publishes no checksums for the desktop bundles, so we pin a tag and
|
||||
* a hash we computed ourselves rather than tracking "latest". Bumping this
|
||||
* means downloading the new asset, verifying it starts and reports the
|
||||
* capabilities in `AnimeBridgeClient.isReady`, then updating both fields.
|
||||
*/
|
||||
export const PINNED_BUNDLE_TAG = 'v1.0.6.0';
|
||||
|
||||
/** SHA-256 of each pinned asset, keyed by release asset name. */
|
||||
export const PINNED_BUNDLE_SHA256: Readonly<Record<string, string>> = {
|
||||
'macOS-arm64-bundle.zip': '5f4fb03abfe88bc46ddf5f4d8221156ee2d66b9cbad7c4bc3ade4baf3a4266e6',
|
||||
};
|
||||
|
||||
/**
|
||||
* Check a downloaded asset against its pin. Assets we have not verified
|
||||
* ourselves are rejected rather than trusted, so an unpinned platform fails
|
||||
* loudly instead of silently running an unchecked binary.
|
||||
*/
|
||||
export function verifyPinnedBundle(
|
||||
assetName: string,
|
||||
bytes: Uint8Array,
|
||||
): { ok: true } | { ok: false; reason: string } {
|
||||
const expected = PINNED_BUNDLE_SHA256[assetName];
|
||||
if (expected === undefined) {
|
||||
return { ok: false, reason: `No pinned checksum for ${assetName}; refusing to run it.` };
|
||||
}
|
||||
const actual = sha256(bytes);
|
||||
if (actual !== expected) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: `Checksum mismatch for ${assetName}: expected ${expected}, got ${actual}.`,
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/** Release asset name for a platform/arch pair, or null when unsupported. */
|
||||
export function resolveBundleAssetName(platform: string, arch: string): string | null {
|
||||
if (platform === 'linux') return arch === 'x64' ? 'linux-x64-bundle.zip' : null;
|
||||
if (platform === 'win32') return arch === 'x64' ? 'windows-x64-bundle.zip' : null;
|
||||
if (platform === 'darwin') {
|
||||
if (arch === 'arm64') return 'macOS-arm64-bundle.zip';
|
||||
if (arch === 'x64') return 'macOS-x64-bundle.zip';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface BundleBinaries {
|
||||
javaPath: string;
|
||||
jarPath: string;
|
||||
}
|
||||
|
||||
async function walk(dir: string, depth: number, onFile: (file: string) => void): Promise<void> {
|
||||
if (depth < 0) return;
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) await walk(full, depth - 1, onFile);
|
||||
else onFile(full);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the bundled `java` executable and `MExtensionServer-*.jar` inside an
|
||||
* extracted bundle. The archive nests them a few levels deep and the exact
|
||||
* layout differs per platform, so this searches rather than assuming a path.
|
||||
*/
|
||||
export async function findBundleBinaries(rootDir: string): Promise<BundleBinaries | null> {
|
||||
const javaCandidates: string[] = [];
|
||||
const jarCandidates: string[] = [];
|
||||
|
||||
await walk(rootDir, 6, (file) => {
|
||||
const base = path.basename(file);
|
||||
if (base === 'java' || base === 'java.exe') javaCandidates.push(file);
|
||||
else if (/^MExtensionServer.*\.jar$/.test(base)) jarCandidates.push(file);
|
||||
});
|
||||
|
||||
// Prefer the shallowest match so a nested duplicate never shadows the real one.
|
||||
const byDepth = (a: string, b: string) => a.split(path.sep).length - b.split(path.sep).length;
|
||||
const javaPath = javaCandidates.sort(byDepth)[0];
|
||||
const jarPath = jarCandidates.sort(byDepth)[0];
|
||||
if (!javaPath || !jarPath) return null;
|
||||
return { javaPath, jarPath };
|
||||
}
|
||||
|
||||
/** Verify a downloaded archive against a pinned SHA-256, as Mangatan does. */
|
||||
export function sha256(bytes: Uint8Array): string {
|
||||
return createHash('sha256').update(bytes).digest('hex');
|
||||
}
|
||||
|
||||
export async function isExecutableFile(file: string): Promise<boolean> {
|
||||
try {
|
||||
const info = await stat(file);
|
||||
return info.isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface BundleAsset {
|
||||
tagName: string;
|
||||
assetName: string;
|
||||
downloadUrl: string;
|
||||
sizeBytes: number;
|
||||
}
|
||||
|
||||
interface GithubRelease {
|
||||
tag_name?: string;
|
||||
assets?: Array<{ name?: string; browser_download_url?: string; size?: number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the newest release carrying an asset for this platform. Releases are
|
||||
* returned newest-first, and some (the iOS runtime) carry no desktop bundle.
|
||||
*/
|
||||
export function selectBundleAsset(releases: unknown, assetName: string): BundleAsset | null {
|
||||
if (!Array.isArray(releases)) return null;
|
||||
for (const release of releases as GithubRelease[]) {
|
||||
const asset = release.assets?.find((candidate) => candidate.name === assetName);
|
||||
if (asset?.browser_download_url && release.tag_name) {
|
||||
return {
|
||||
tagName: release.tag_name,
|
||||
assetName,
|
||||
downloadUrl: asset.browser_download_url,
|
||||
sizeBytes: asset.size ?? 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user