mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00: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
472 lines
16 KiB
TypeScript
472 lines
16 KiB
TypeScript
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { MPV_LAUNCH_MODE_VALUES, parseMpvLaunchMode } from '../../shared/mpv-launch-mode';
|
|
import { ResolveContext } from './context';
|
|
import { asBoolean, asNumber, asString, isObject } from './shared';
|
|
|
|
function normalizeExternalProfilePath(value: string): string {
|
|
const trimmed = value.trim();
|
|
if (trimmed === '~') {
|
|
return os.homedir();
|
|
}
|
|
if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
|
|
return path.join(os.homedir(), trimmed.slice(2));
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
export function applyIntegrationConfig(context: ResolveContext): void {
|
|
const { src, resolved, warn } = context;
|
|
|
|
if (isObject(src.ai)) {
|
|
const booleanKeys = ['enabled'] as const;
|
|
for (const key of booleanKeys) {
|
|
const value = asBoolean(src.ai[key]);
|
|
if (value !== undefined) {
|
|
resolved.ai[key] = value;
|
|
} else if (src.ai[key] !== undefined) {
|
|
warn(`ai.${key}`, src.ai[key], resolved.ai[key], 'Expected boolean.');
|
|
}
|
|
}
|
|
|
|
const stringKeys = ['apiKey', 'apiKeyCommand', 'baseUrl', 'model', 'systemPrompt'] as const;
|
|
for (const key of stringKeys) {
|
|
const value = asString(src.ai[key]);
|
|
if (value !== undefined) {
|
|
resolved.ai[key] = value;
|
|
} else if (src.ai[key] !== undefined) {
|
|
warn(`ai.${key}`, src.ai[key], resolved.ai[key], 'Expected string.');
|
|
}
|
|
}
|
|
|
|
const requestTimeoutMs = asNumber(src.ai.requestTimeoutMs);
|
|
if (
|
|
requestTimeoutMs !== undefined &&
|
|
Number.isInteger(requestTimeoutMs) &&
|
|
requestTimeoutMs > 0
|
|
) {
|
|
resolved.ai.requestTimeoutMs = requestTimeoutMs;
|
|
} else if (src.ai.requestTimeoutMs !== undefined) {
|
|
warn(
|
|
'ai.requestTimeoutMs',
|
|
src.ai.requestTimeoutMs,
|
|
resolved.ai.requestTimeoutMs,
|
|
'Expected positive integer.',
|
|
);
|
|
}
|
|
} else if (src.ai !== undefined) {
|
|
warn('ai', src.ai, resolved.ai, 'Expected object.');
|
|
}
|
|
|
|
if (isObject(src.anilist)) {
|
|
const enabled = asBoolean(src.anilist.enabled);
|
|
if (enabled !== undefined) {
|
|
resolved.anilist.enabled = enabled;
|
|
} else if (src.anilist.enabled !== undefined) {
|
|
warn('anilist.enabled', src.anilist.enabled, resolved.anilist.enabled, 'Expected boolean.');
|
|
}
|
|
|
|
const accessToken = asString(src.anilist.accessToken);
|
|
if (accessToken !== undefined) {
|
|
resolved.anilist.accessToken = accessToken;
|
|
} else if (src.anilist.accessToken !== undefined) {
|
|
warn(
|
|
'anilist.accessToken',
|
|
src.anilist.accessToken,
|
|
resolved.anilist.accessToken,
|
|
'Expected string.',
|
|
);
|
|
}
|
|
|
|
if (isObject(src.anilist.characterDictionary)) {
|
|
const characterDictionary = src.anilist.characterDictionary;
|
|
|
|
const dictionaryEnabled = asBoolean(characterDictionary.enabled);
|
|
if (dictionaryEnabled !== undefined) {
|
|
resolved.anilist.characterDictionary.enabled = dictionaryEnabled;
|
|
} else if (characterDictionary.enabled !== undefined) {
|
|
warn(
|
|
'anilist.characterDictionary.enabled',
|
|
characterDictionary.enabled,
|
|
resolved.anilist.characterDictionary.enabled,
|
|
'Expected boolean.',
|
|
);
|
|
}
|
|
|
|
const refreshTtlHours = asNumber(characterDictionary.refreshTtlHours);
|
|
if (refreshTtlHours !== undefined) {
|
|
const normalized = Math.min(24 * 365, Math.max(1, Math.floor(refreshTtlHours)));
|
|
if (normalized !== refreshTtlHours) {
|
|
warn(
|
|
'anilist.characterDictionary.refreshTtlHours',
|
|
characterDictionary.refreshTtlHours,
|
|
normalized,
|
|
'Out of range; clamped to 1..8760 hours.',
|
|
);
|
|
}
|
|
resolved.anilist.characterDictionary.refreshTtlHours = normalized;
|
|
} else if (characterDictionary.refreshTtlHours !== undefined) {
|
|
warn(
|
|
'anilist.characterDictionary.refreshTtlHours',
|
|
characterDictionary.refreshTtlHours,
|
|
resolved.anilist.characterDictionary.refreshTtlHours,
|
|
'Expected number.',
|
|
);
|
|
}
|
|
|
|
const maxLoaded = asNumber(characterDictionary.maxLoaded);
|
|
if (maxLoaded !== undefined) {
|
|
const normalized = Math.min(20, Math.max(1, Math.floor(maxLoaded)));
|
|
if (normalized !== maxLoaded) {
|
|
warn(
|
|
'anilist.characterDictionary.maxLoaded',
|
|
characterDictionary.maxLoaded,
|
|
normalized,
|
|
'Out of range; clamped to 1..20.',
|
|
);
|
|
}
|
|
resolved.anilist.characterDictionary.maxLoaded = normalized;
|
|
} else if (characterDictionary.maxLoaded !== undefined) {
|
|
warn(
|
|
'anilist.characterDictionary.maxLoaded',
|
|
characterDictionary.maxLoaded,
|
|
resolved.anilist.characterDictionary.maxLoaded,
|
|
'Expected number.',
|
|
);
|
|
}
|
|
|
|
const evictionPolicyRaw = asString(characterDictionary.evictionPolicy);
|
|
if (evictionPolicyRaw !== undefined) {
|
|
const evictionPolicy = evictionPolicyRaw.trim().toLowerCase();
|
|
if (evictionPolicy === 'disable' || evictionPolicy === 'delete') {
|
|
resolved.anilist.characterDictionary.evictionPolicy = evictionPolicy;
|
|
} else {
|
|
warn(
|
|
'anilist.characterDictionary.evictionPolicy',
|
|
characterDictionary.evictionPolicy,
|
|
resolved.anilist.characterDictionary.evictionPolicy,
|
|
"Expected one of: 'disable', 'delete'.",
|
|
);
|
|
}
|
|
} else if (characterDictionary.evictionPolicy !== undefined) {
|
|
warn(
|
|
'anilist.characterDictionary.evictionPolicy',
|
|
characterDictionary.evictionPolicy,
|
|
resolved.anilist.characterDictionary.evictionPolicy,
|
|
'Expected string.',
|
|
);
|
|
}
|
|
|
|
const profileScopeRaw = asString(characterDictionary.profileScope);
|
|
if (profileScopeRaw !== undefined) {
|
|
const profileScope = profileScopeRaw.trim().toLowerCase();
|
|
if (profileScope === 'all' || profileScope === 'active') {
|
|
resolved.anilist.characterDictionary.profileScope = profileScope;
|
|
} else {
|
|
warn(
|
|
'anilist.characterDictionary.profileScope',
|
|
characterDictionary.profileScope,
|
|
resolved.anilist.characterDictionary.profileScope,
|
|
"Expected one of: 'all', 'active'.",
|
|
);
|
|
}
|
|
} else if (characterDictionary.profileScope !== undefined) {
|
|
warn(
|
|
'anilist.characterDictionary.profileScope',
|
|
characterDictionary.profileScope,
|
|
resolved.anilist.characterDictionary.profileScope,
|
|
'Expected string.',
|
|
);
|
|
}
|
|
|
|
if (isObject(characterDictionary.collapsibleSections)) {
|
|
const collapsibleSections = characterDictionary.collapsibleSections;
|
|
const keys = ['description', 'characterInformation', 'voicedBy'] as const;
|
|
for (const key of keys) {
|
|
const value = asBoolean(collapsibleSections[key]);
|
|
if (value !== undefined) {
|
|
resolved.anilist.characterDictionary.collapsibleSections[key] = value;
|
|
} else if (collapsibleSections[key] !== undefined) {
|
|
warn(
|
|
`anilist.characterDictionary.collapsibleSections.${key}`,
|
|
collapsibleSections[key],
|
|
resolved.anilist.characterDictionary.collapsibleSections[key],
|
|
'Expected boolean.',
|
|
);
|
|
}
|
|
}
|
|
} else if (characterDictionary.collapsibleSections !== undefined) {
|
|
warn(
|
|
'anilist.characterDictionary.collapsibleSections',
|
|
characterDictionary.collapsibleSections,
|
|
resolved.anilist.characterDictionary.collapsibleSections,
|
|
'Expected object.',
|
|
);
|
|
}
|
|
} else if (src.anilist.characterDictionary !== undefined) {
|
|
warn(
|
|
'anilist.characterDictionary',
|
|
src.anilist.characterDictionary,
|
|
resolved.anilist.characterDictionary,
|
|
'Expected object.',
|
|
);
|
|
}
|
|
}
|
|
|
|
if (isObject(src.yomitan)) {
|
|
const externalProfilePath = asString(src.yomitan.externalProfilePath);
|
|
if (externalProfilePath !== undefined) {
|
|
resolved.yomitan.externalProfilePath = normalizeExternalProfilePath(externalProfilePath);
|
|
} else if (src.yomitan.externalProfilePath !== undefined) {
|
|
warn(
|
|
'yomitan.externalProfilePath',
|
|
src.yomitan.externalProfilePath,
|
|
resolved.yomitan.externalProfilePath,
|
|
'Expected string.',
|
|
);
|
|
}
|
|
} else if (src.yomitan !== undefined) {
|
|
warn('yomitan', src.yomitan, resolved.yomitan, 'Expected object.');
|
|
}
|
|
|
|
if (isObject(src.mpv)) {
|
|
const executablePath = asString(src.mpv.executablePath);
|
|
if (executablePath !== undefined) {
|
|
resolved.mpv.executablePath = executablePath.trim();
|
|
} else if (src.mpv.executablePath !== undefined) {
|
|
warn(
|
|
'mpv.executablePath',
|
|
src.mpv.executablePath,
|
|
resolved.mpv.executablePath,
|
|
'Expected string.',
|
|
);
|
|
}
|
|
|
|
const launchMode = parseMpvLaunchMode(src.mpv.launchMode);
|
|
if (launchMode !== undefined) {
|
|
resolved.mpv.launchMode = launchMode;
|
|
} else if (src.mpv.launchMode !== undefined) {
|
|
warn(
|
|
'mpv.launchMode',
|
|
src.mpv.launchMode,
|
|
resolved.mpv.launchMode,
|
|
`Expected one of: ${MPV_LAUNCH_MODE_VALUES.map((value) => `'${value}'`).join(', ')}.`,
|
|
);
|
|
}
|
|
|
|
const socketPath = asString(src.mpv.socketPath);
|
|
if (socketPath !== undefined && socketPath.trim().length > 0) {
|
|
resolved.mpv.socketPath = socketPath.trim();
|
|
} else if (src.mpv.socketPath !== undefined) {
|
|
warn(
|
|
'mpv.socketPath',
|
|
src.mpv.socketPath,
|
|
resolved.mpv.socketPath,
|
|
'Expected non-empty string.',
|
|
);
|
|
}
|
|
|
|
const backend = asString(src.mpv.backend);
|
|
if (
|
|
backend === 'auto' ||
|
|
backend === 'hyprland' ||
|
|
backend === 'sway' ||
|
|
backend === 'x11' ||
|
|
backend === 'macos' ||
|
|
backend === 'windows'
|
|
) {
|
|
resolved.mpv.backend = backend;
|
|
} else if (src.mpv.backend !== undefined) {
|
|
warn(
|
|
'mpv.backend',
|
|
src.mpv.backend,
|
|
resolved.mpv.backend,
|
|
'Expected auto, hyprland, sway, x11, macos, or windows.',
|
|
);
|
|
}
|
|
|
|
const autoStartSubMiner = asBoolean(src.mpv.autoStartSubMiner);
|
|
if (autoStartSubMiner !== undefined) {
|
|
resolved.mpv.autoStartSubMiner = autoStartSubMiner;
|
|
} else if (src.mpv.autoStartSubMiner !== undefined) {
|
|
warn(
|
|
'mpv.autoStartSubMiner',
|
|
src.mpv.autoStartSubMiner,
|
|
resolved.mpv.autoStartSubMiner,
|
|
'Expected boolean.',
|
|
);
|
|
}
|
|
|
|
const pauseUntilOverlayReady = asBoolean(src.mpv.pauseUntilOverlayReady);
|
|
if (pauseUntilOverlayReady !== undefined) {
|
|
resolved.mpv.pauseUntilOverlayReady = pauseUntilOverlayReady;
|
|
} else if (src.mpv.pauseUntilOverlayReady !== undefined) {
|
|
warn(
|
|
'mpv.pauseUntilOverlayReady',
|
|
src.mpv.pauseUntilOverlayReady,
|
|
resolved.mpv.pauseUntilOverlayReady,
|
|
'Expected boolean.',
|
|
);
|
|
}
|
|
|
|
const subminerBinaryPath = asString(src.mpv.subminerBinaryPath);
|
|
if (subminerBinaryPath !== undefined) {
|
|
resolved.mpv.subminerBinaryPath = subminerBinaryPath.trim();
|
|
} else if (src.mpv.subminerBinaryPath !== undefined) {
|
|
warn(
|
|
'mpv.subminerBinaryPath',
|
|
src.mpv.subminerBinaryPath,
|
|
resolved.mpv.subminerBinaryPath,
|
|
'Expected string.',
|
|
);
|
|
}
|
|
|
|
const aniskipEnabled = asBoolean(src.mpv.aniskipEnabled);
|
|
if (aniskipEnabled !== undefined) {
|
|
resolved.mpv.aniskipEnabled = aniskipEnabled;
|
|
} else if (src.mpv.aniskipEnabled !== undefined) {
|
|
warn(
|
|
'mpv.aniskipEnabled',
|
|
src.mpv.aniskipEnabled,
|
|
resolved.mpv.aniskipEnabled,
|
|
'Expected boolean.',
|
|
);
|
|
}
|
|
|
|
const aniskipButtonKey = asString(src.mpv.aniskipButtonKey);
|
|
if (aniskipButtonKey !== undefined && aniskipButtonKey.trim().length > 0) {
|
|
resolved.mpv.aniskipButtonKey = aniskipButtonKey.trim();
|
|
} else if (src.mpv.aniskipButtonKey !== undefined) {
|
|
warn(
|
|
'mpv.aniskipButtonKey',
|
|
src.mpv.aniskipButtonKey,
|
|
resolved.mpv.aniskipButtonKey,
|
|
'Expected non-empty string.',
|
|
);
|
|
}
|
|
} else if (src.mpv !== undefined) {
|
|
warn('mpv', src.mpv, resolved.mpv, 'Expected object.');
|
|
}
|
|
|
|
if (isObject(src.jellyfin)) {
|
|
const enabled = asBoolean(src.jellyfin.enabled);
|
|
if (enabled !== undefined) {
|
|
resolved.jellyfin.enabled = enabled;
|
|
} else if (src.jellyfin.enabled !== undefined) {
|
|
warn(
|
|
'jellyfin.enabled',
|
|
src.jellyfin.enabled,
|
|
resolved.jellyfin.enabled,
|
|
'Expected boolean.',
|
|
);
|
|
}
|
|
|
|
const stringKeys = [
|
|
'serverUrl',
|
|
'username',
|
|
'deviceId',
|
|
'clientName',
|
|
'clientVersion',
|
|
'defaultLibraryId',
|
|
'iconCacheDir',
|
|
'transcodeVideoCodec',
|
|
] as const;
|
|
for (const key of stringKeys) {
|
|
const value = asString(src.jellyfin[key]);
|
|
if (value !== undefined) {
|
|
resolved.jellyfin[key] = value as (typeof resolved.jellyfin)[typeof key];
|
|
} else if (src.jellyfin[key] !== undefined) {
|
|
warn(`jellyfin.${key}`, src.jellyfin[key], resolved.jellyfin[key], 'Expected string.');
|
|
}
|
|
}
|
|
|
|
const booleanKeys = [
|
|
'remoteControlEnabled',
|
|
'remoteControlAutoConnect',
|
|
'autoAnnounce',
|
|
'directPlayPreferred',
|
|
'pullPictures',
|
|
] as const;
|
|
for (const key of booleanKeys) {
|
|
const value = asBoolean(src.jellyfin[key]);
|
|
if (value !== undefined) {
|
|
resolved.jellyfin[key] = value as (typeof resolved.jellyfin)[typeof key];
|
|
} else if (src.jellyfin[key] !== undefined) {
|
|
warn(`jellyfin.${key}`, src.jellyfin[key], resolved.jellyfin[key], 'Expected boolean.');
|
|
}
|
|
}
|
|
|
|
if (Array.isArray(src.jellyfin.directPlayContainers)) {
|
|
resolved.jellyfin.directPlayContainers = src.jellyfin.directPlayContainers
|
|
.filter((item): item is string => typeof item === 'string')
|
|
.map((item) => item.trim().toLowerCase())
|
|
.filter((item) => item.length > 0);
|
|
} else if (src.jellyfin.directPlayContainers !== undefined) {
|
|
warn(
|
|
'jellyfin.directPlayContainers',
|
|
src.jellyfin.directPlayContainers,
|
|
resolved.jellyfin.directPlayContainers,
|
|
'Expected string array.',
|
|
);
|
|
}
|
|
|
|
if (Array.isArray(src.jellyfin.recentServers)) {
|
|
const seenRecentServers = new Set<string>();
|
|
resolved.jellyfin.recentServers = src.jellyfin.recentServers
|
|
.filter((item): item is string => typeof item === 'string')
|
|
.map((item) => item.trim().replace(/\/+$/, ''))
|
|
.filter((item) => {
|
|
if (!item || seenRecentServers.has(item)) return false;
|
|
seenRecentServers.add(item);
|
|
return true;
|
|
})
|
|
.slice(0, 5);
|
|
} else if (src.jellyfin.recentServers !== undefined) {
|
|
warn(
|
|
'jellyfin.recentServers',
|
|
src.jellyfin.recentServers,
|
|
resolved.jellyfin.recentServers,
|
|
'Expected string array.',
|
|
);
|
|
}
|
|
}
|
|
|
|
if (isObject(src.discordPresence)) {
|
|
const enabled = asBoolean(src.discordPresence.enabled);
|
|
if (enabled !== undefined) {
|
|
resolved.discordPresence.enabled = enabled;
|
|
} else if (src.discordPresence.enabled !== undefined) {
|
|
warn(
|
|
'discordPresence.enabled',
|
|
src.discordPresence.enabled,
|
|
resolved.discordPresence.enabled,
|
|
'Expected boolean.',
|
|
);
|
|
}
|
|
|
|
const updateIntervalMs = asNumber(src.discordPresence.updateIntervalMs);
|
|
if (updateIntervalMs !== undefined) {
|
|
resolved.discordPresence.updateIntervalMs = Math.max(1_000, Math.floor(updateIntervalMs));
|
|
} else if (src.discordPresence.updateIntervalMs !== undefined) {
|
|
warn(
|
|
'discordPresence.updateIntervalMs',
|
|
src.discordPresence.updateIntervalMs,
|
|
resolved.discordPresence.updateIntervalMs,
|
|
'Expected number.',
|
|
);
|
|
}
|
|
|
|
const debounceMs = asNumber(src.discordPresence.debounceMs);
|
|
if (debounceMs !== undefined) {
|
|
resolved.discordPresence.debounceMs = Math.max(0, Math.floor(debounceMs));
|
|
} else if (src.discordPresence.debounceMs !== undefined) {
|
|
warn(
|
|
'discordPresence.debounceMs',
|
|
src.discordPresence.debounceMs,
|
|
resolved.discordPresence.debounceMs,
|
|
'Expected number.',
|
|
);
|
|
}
|
|
}
|
|
}
|