mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 12:55:16 -07:00
6ba91780c1
- Replace subminer.conf plugin config with mpv.* fields in config.jsonc - Add socketPath, backend, autoStartSubMiner, pauseUntilOverlayReady, aniskipEnabled/buttonKey, subminerBinaryPath to mpv config - Add subtitleSidebar.css field; migrate legacy sidebar appearance fields - Add paintOrder and WebkitTextStroke to subtitle style options - Update default subtitle/sidebar fontFamily to CJK-first stack - Fix overlay visible state surviving mpv y-r restart - Fix live config saves applying subtitle CSS immediately to open overlays - Migrate legacy primary/secondary subtitle appearance into subtitleStyle.css on load - Switch AniSkip button key setting to click-to-learn key capture
296 lines
8.8 KiB
TypeScript
296 lines
8.8 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import type { MpvInstallPaths } from '../../shared/setup-state';
|
|
|
|
export interface InstalledFirstRunPluginCandidate {
|
|
path: string;
|
|
kind: 'directory' | 'file';
|
|
}
|
|
|
|
export type InstalledMpvPluginSource =
|
|
| 'default-config'
|
|
| 'xdg-config'
|
|
| 'portable-config'
|
|
| 'legacy-file';
|
|
|
|
export interface InstalledMpvPluginDetection {
|
|
installed: boolean;
|
|
path: string | null;
|
|
version: string | null;
|
|
source: InstalledMpvPluginSource | null;
|
|
message: string | null;
|
|
}
|
|
|
|
export interface LegacyMpvPluginRemovalResult {
|
|
ok: boolean;
|
|
removedPaths: string[];
|
|
failedPaths: Array<{ path: string; message: string }>;
|
|
}
|
|
|
|
export function resolvePackagedFirstRunPluginAssets(deps: {
|
|
dirname: string;
|
|
appPath: string;
|
|
resourcesPath: string;
|
|
joinPath?: (...parts: string[]) => string;
|
|
existsSync?: (candidate: string) => boolean;
|
|
}): { pluginDirSource: string; pluginConfigSource: string } | null {
|
|
const joinPath = deps.joinPath ?? path.join;
|
|
const existsSync = deps.existsSync ?? fs.existsSync;
|
|
const roots = [
|
|
joinPath(deps.resourcesPath, 'plugin'),
|
|
joinPath(deps.resourcesPath, 'app.asar', 'plugin'),
|
|
joinPath(deps.appPath, 'plugin'),
|
|
joinPath(deps.dirname, '..', 'plugin'),
|
|
joinPath(deps.dirname, '..', '..', 'plugin'),
|
|
];
|
|
|
|
for (const root of roots) {
|
|
const pluginDirSource = joinPath(root, 'subminer');
|
|
const pluginConfigSource = joinPath(root, 'subminer.conf');
|
|
if (
|
|
existsSync(pluginDirSource) &&
|
|
existsSync(pluginConfigSource) &&
|
|
existsSync(joinPath(pluginDirSource, 'main.lua'))
|
|
) {
|
|
return { pluginDirSource, pluginConfigSource };
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function resolvePackagedRuntimePluginPath(deps: {
|
|
dirname: string;
|
|
appPath: string;
|
|
resourcesPath: string;
|
|
joinPath?: (...parts: string[]) => string;
|
|
existsSync?: (candidate: string) => boolean;
|
|
}): string | null {
|
|
const joinPath = deps.joinPath ?? path.join;
|
|
const existsSync = deps.existsSync ?? fs.existsSync;
|
|
const assets = resolvePackagedFirstRunPluginAssets({
|
|
dirname: deps.dirname,
|
|
appPath: deps.appPath,
|
|
resourcesPath: deps.resourcesPath,
|
|
joinPath,
|
|
existsSync,
|
|
});
|
|
if (!assets) {
|
|
return null;
|
|
}
|
|
|
|
const entrypoint = joinPath(assets.pluginDirSource, 'main.lua');
|
|
return existsSync(entrypoint) ? entrypoint : null;
|
|
}
|
|
|
|
export function detectInstalledFirstRunPlugin(
|
|
installPaths: MpvInstallPaths,
|
|
deps?: {
|
|
existsSync?: (candidate: string) => boolean;
|
|
},
|
|
): boolean {
|
|
const existsSync = deps?.existsSync ?? fs.existsSync;
|
|
const pluginEntrypointPath = path.join(installPaths.scriptsDir, 'subminer', 'main.lua');
|
|
return existsSync(pluginEntrypointPath);
|
|
}
|
|
|
|
function getPlatformPath(platform: NodeJS.Platform): typeof path.posix | typeof path.win32 {
|
|
return platform === 'win32' ? path.win32 : path.posix;
|
|
}
|
|
|
|
interface MpvConfigRootCandidate {
|
|
root: string;
|
|
source: Exclude<InstalledMpvPluginSource, 'legacy-file'>;
|
|
}
|
|
|
|
function collectMpvConfigRootCandidates(options: {
|
|
platform: NodeJS.Platform;
|
|
homeDir: string;
|
|
xdgConfigHome?: string;
|
|
appDataDir?: string;
|
|
mpvExecutablePath?: string;
|
|
}): MpvConfigRootCandidate[] {
|
|
const platformPath = getPlatformPath(options.platform);
|
|
if (options.platform === 'win32') {
|
|
const roots: MpvConfigRootCandidate[] = [];
|
|
if (options.mpvExecutablePath?.trim()) {
|
|
roots.push({
|
|
root: platformPath.join(
|
|
platformPath.dirname(options.mpvExecutablePath.trim()),
|
|
'portable_config',
|
|
),
|
|
source: 'portable-config',
|
|
});
|
|
}
|
|
roots.push({
|
|
root: platformPath.join(
|
|
options.appDataDir?.trim() || platformPath.join(options.homeDir, 'AppData', 'Roaming'),
|
|
'mpv',
|
|
),
|
|
source: 'default-config',
|
|
});
|
|
return roots;
|
|
}
|
|
|
|
const xdgRoot = options.xdgConfigHome?.trim()
|
|
? platformPath.join(options.xdgConfigHome.trim(), 'mpv')
|
|
: null;
|
|
const homeRoot = platformPath.join(options.homeDir, '.config', 'mpv');
|
|
const roots: MpvConfigRootCandidate[] = [];
|
|
if (xdgRoot) {
|
|
roots.push({ root: xdgRoot, source: 'xdg-config' });
|
|
}
|
|
if (!xdgRoot || xdgRoot !== homeRoot) {
|
|
roots.push({ root: homeRoot, source: 'default-config' });
|
|
}
|
|
return roots;
|
|
}
|
|
|
|
export function detectInstalledFirstRunPluginCandidates(options: {
|
|
platform: NodeJS.Platform;
|
|
homeDir: string;
|
|
xdgConfigHome?: string;
|
|
appDataDir?: string;
|
|
mpvExecutablePath?: string;
|
|
existsSync?: (candidate: string) => boolean;
|
|
}): InstalledFirstRunPluginCandidate[] {
|
|
const platformPath = getPlatformPath(options.platform);
|
|
const existsSync = options.existsSync ?? fs.existsSync;
|
|
const roots = collectMpvConfigRootCandidates(options);
|
|
|
|
const candidates: InstalledFirstRunPluginCandidate[] = [];
|
|
const seen = new Set<string>();
|
|
const pushIfExists = (
|
|
candidate: InstalledFirstRunPluginCandidate,
|
|
verifyPath = candidate.path,
|
|
) => {
|
|
if (seen.has(candidate.path) || !existsSync(verifyPath)) return;
|
|
seen.add(candidate.path);
|
|
candidates.push(candidate);
|
|
};
|
|
|
|
for (const root of roots) {
|
|
const scriptsDir = platformPath.join(root.root, 'scripts');
|
|
const pluginDir = platformPath.join(scriptsDir, 'subminer');
|
|
pushIfExists({ path: pluginDir, kind: 'directory' });
|
|
pushIfExists({ path: platformPath.join(scriptsDir, 'subminer.lua'), kind: 'file' });
|
|
pushIfExists({ path: platformPath.join(scriptsDir, 'subminer-loader.lua'), kind: 'file' });
|
|
}
|
|
|
|
return candidates;
|
|
}
|
|
|
|
function parseInstalledPluginVersion(content: string): string | null {
|
|
const match = content.match(/\bversion\s*=\s*["']([^"']+)["']/);
|
|
return match?.[1] ?? null;
|
|
}
|
|
|
|
function readInstalledPluginVersion(options: {
|
|
pluginEntrypointPath: string;
|
|
platformPath: typeof path.posix | typeof path.win32;
|
|
existsSync: (candidate: string) => boolean;
|
|
readFileSync: (candidate: string, encoding: BufferEncoding) => string;
|
|
}): string | null {
|
|
const versionPath = options.platformPath.join(
|
|
options.platformPath.dirname(options.pluginEntrypointPath),
|
|
'version.lua',
|
|
);
|
|
if (!options.existsSync(versionPath)) {
|
|
return null;
|
|
}
|
|
try {
|
|
return parseInstalledPluginVersion(options.readFileSync(versionPath, 'utf8'));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function detectInstalledMpvPlugin(options: {
|
|
platform: NodeJS.Platform;
|
|
homeDir: string;
|
|
xdgConfigHome?: string;
|
|
appDataDir?: string;
|
|
mpvExecutablePath?: string;
|
|
existsSync?: (candidate: string) => boolean;
|
|
readFileSync?: (candidate: string, encoding: BufferEncoding) => string;
|
|
}): InstalledMpvPluginDetection {
|
|
const platformPath = getPlatformPath(options.platform);
|
|
const existsSync = options.existsSync ?? fs.existsSync;
|
|
const readFileSync =
|
|
options.readFileSync ?? ((candidate, encoding) => fs.readFileSync(candidate, encoding));
|
|
const roots = collectMpvConfigRootCandidates(options);
|
|
|
|
for (const root of roots) {
|
|
const scriptsDir = platformPath.join(root.root, 'scripts');
|
|
const directoryEntrypoint = platformPath.join(scriptsDir, 'subminer', 'main.lua');
|
|
if (existsSync(directoryEntrypoint)) {
|
|
const version = readInstalledPluginVersion({
|
|
pluginEntrypointPath: directoryEntrypoint,
|
|
platformPath,
|
|
existsSync,
|
|
readFileSync,
|
|
});
|
|
return {
|
|
installed: true,
|
|
path: directoryEntrypoint,
|
|
version,
|
|
source: root.source,
|
|
message: `SubMiner detected an installed mpv plugin at: ${directoryEntrypoint}`,
|
|
};
|
|
}
|
|
|
|
for (const legacyPath of [
|
|
platformPath.join(scriptsDir, 'subminer.lua'),
|
|
platformPath.join(scriptsDir, 'subminer-loader.lua'),
|
|
]) {
|
|
if (existsSync(legacyPath)) {
|
|
return {
|
|
installed: true,
|
|
path: legacyPath,
|
|
version: null,
|
|
source: root.source === 'portable-config' ? 'portable-config' : 'legacy-file',
|
|
message: `SubMiner detected an installed mpv plugin at: ${legacyPath}`,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
installed: false,
|
|
path: null,
|
|
version: null,
|
|
source: null,
|
|
message: null,
|
|
};
|
|
}
|
|
|
|
function errorMessage(error: unknown): string {
|
|
return error instanceof Error ? error.message : String(error);
|
|
}
|
|
|
|
export async function removeLegacyMpvPluginCandidates(options: {
|
|
candidates: InstalledFirstRunPluginCandidate[];
|
|
trashItem: (path: string) => Promise<void>;
|
|
}): Promise<LegacyMpvPluginRemovalResult> {
|
|
const removedPaths: string[] = [];
|
|
const failedPaths: Array<{ path: string; message: string }> = [];
|
|
const seen = new Set<string>();
|
|
|
|
for (const candidate of options.candidates) {
|
|
if (seen.has(candidate.path)) continue;
|
|
seen.add(candidate.path);
|
|
try {
|
|
await options.trashItem(candidate.path);
|
|
removedPaths.push(candidate.path);
|
|
} catch (error) {
|
|
failedPaths.push({ path: candidate.path, message: errorMessage(error) });
|
|
}
|
|
}
|
|
|
|
return {
|
|
ok: failedPaths.length === 0,
|
|
removedPaths,
|
|
failedPaths,
|
|
};
|
|
}
|