mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-13 03:13:32 -07:00
Fix Windows mpv logging and add log export (#88)
This commit is contained in:
+108
-4
@@ -63,7 +63,6 @@ test('loads defaults when config is missing', () => {
|
||||
assert.deepEqual(config.ankiConnect.tags, ['SubMiner']);
|
||||
assert.equal(config.ankiConnect.media.audioPadding, 0);
|
||||
assert.equal(config.anilist.enabled, false);
|
||||
assert.equal(config.anilist.characterDictionary.enabled, false);
|
||||
assert.equal(config.subtitleStyle.nameMatchImagesEnabled, false);
|
||||
assert.equal(config.anilist.characterDictionary.refreshTtlHours, 168);
|
||||
assert.equal(config.anilist.characterDictionary.maxLoaded, 3);
|
||||
@@ -96,7 +95,6 @@ test('loads defaults when config is missing', () => {
|
||||
assert.equal(config.startupWarmups.subtitleDictionaries, true);
|
||||
assert.equal(config.startupWarmups.jellyfinRemoteSession, false);
|
||||
assert.equal(config.shortcuts.markAudioCard, 'CommandOrControl+Shift+A');
|
||||
assert.equal('openCharacterDictionary' in config.shortcuts, false);
|
||||
assert.equal(config.shortcuts.openCharacterDictionaryManager, 'CommandOrControl+D');
|
||||
assert.equal(config.shortcuts.toggleSubtitleSidebar, 'Backslash');
|
||||
assert.equal(config.discordPresence.enabled, true);
|
||||
@@ -825,7 +823,6 @@ test('parses anilist.characterDictionary config with clamping and enum validatio
|
||||
const config = service.getConfig();
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.equal(config.anilist.characterDictionary.enabled, true);
|
||||
assert.equal(config.anilist.characterDictionary.refreshTtlHours, 1);
|
||||
assert.equal(config.anilist.characterDictionary.maxLoaded, 20);
|
||||
assert.equal(config.anilist.characterDictionary.evictionPolicy, 'delete');
|
||||
@@ -1462,6 +1459,50 @@ test('accepts valid logging.level', () => {
|
||||
assert.equal(config.logging.level, 'warn');
|
||||
});
|
||||
|
||||
test('accepts valid logging.rotation', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"logging": {
|
||||
"rotation": 14
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.equal(config.logging.rotation, 14);
|
||||
});
|
||||
|
||||
test('accepts valid logging file toggles', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"logging": {
|
||||
"files": {
|
||||
"app": false,
|
||||
"launcher": true,
|
||||
"mpv": true
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.deepEqual(config.logging.files, {
|
||||
app: false,
|
||||
launcher: true,
|
||||
mpv: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('falls back for invalid logging.level and reports warning', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
@@ -1482,6 +1523,68 @@ test('falls back for invalid logging.level and reports warning', () => {
|
||||
assert.ok(warnings.some((warning) => warning.path === 'logging.level'));
|
||||
});
|
||||
|
||||
test('falls back for invalid logging.rotation and reports warning', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"logging": {
|
||||
"rotation": 0
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.equal(config.logging.rotation, DEFAULT_CONFIG.logging.rotation);
|
||||
assert.ok(warnings.some((warning) => warning.path === 'logging.rotation'));
|
||||
});
|
||||
|
||||
test('falls back for invalid logging file toggles and reports warning', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"logging": {
|
||||
"files": {
|
||||
"mpv": "yes"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.equal(config.logging.files.mpv, DEFAULT_CONFIG.logging.files.mpv);
|
||||
assert.ok(warnings.some((warning) => warning.path === 'logging.files.mpv'));
|
||||
});
|
||||
|
||||
test('falls back for invalid logging files object and reports warning', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"logging": {
|
||||
"files": false
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.deepEqual(config.logging.files, DEFAULT_CONFIG.logging.files);
|
||||
assert.ok(warnings.some((warning) => warning.path === 'logging.files'));
|
||||
});
|
||||
|
||||
test('warns and ignores unknown top-level config keys', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
@@ -2518,6 +2621,7 @@ test('template generator includes known keys', () => {
|
||||
assert.doesNotMatch(output, /"clientVersion":/);
|
||||
assert.doesNotMatch(output, /"youtubeSubgen":/);
|
||||
assert.match(output, /"characterDictionary":\s*\{/);
|
||||
assert.doesNotMatch(output, /"characterDictionary":\s*\{\s*"enabled":/);
|
||||
assert.match(output, /"preserveLineBreaks": false/);
|
||||
assert.match(output, /"knownWords"\s*:\s*\{/);
|
||||
assert.match(output, /"knownWordColor": "#a6da95"/);
|
||||
@@ -2527,7 +2631,7 @@ test('template generator includes known keys', () => {
|
||||
assert.match(output, /auto-generated from src\/config\/definitions.ts/);
|
||||
assert.match(
|
||||
output,
|
||||
/"level": "info",? \/\/ Minimum log level for runtime logging\. Values: debug \| info \| warn \| error/,
|
||||
/"level": "warn",? \/\/ Minimum log level for runtime logging\. Values: debug \| info \| warn \| error/,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
|
||||
@@ -28,7 +28,13 @@ export const CORE_DEFAULT_CONFIG: Pick<
|
||||
port: 6678,
|
||||
},
|
||||
logging: {
|
||||
level: 'info',
|
||||
level: 'warn',
|
||||
rotation: 7,
|
||||
files: {
|
||||
app: true,
|
||||
launcher: true,
|
||||
mpv: false,
|
||||
},
|
||||
},
|
||||
texthooker: {
|
||||
launchAtStartup: false,
|
||||
|
||||
@@ -110,7 +110,6 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
enabled: false,
|
||||
accessToken: '',
|
||||
characterDictionary: {
|
||||
enabled: false,
|
||||
refreshTtlHours: 168,
|
||||
maxLoaded: 3,
|
||||
evictionPolicy: 'delete',
|
||||
|
||||
@@ -92,6 +92,7 @@ test('config option registry includes critical paths and has unique entries', ()
|
||||
|
||||
for (const requiredPath of [
|
||||
'logging.level',
|
||||
'logging.files.mpv',
|
||||
'annotationWebsocket.enabled',
|
||||
'controller.enabled',
|
||||
'controller.scrollPixelsPerSecond',
|
||||
@@ -101,7 +102,7 @@ test('config option registry includes critical paths and has unique entries', ()
|
||||
'subtitleStyle.enableJlpt',
|
||||
'subtitleStyle.autoPauseVideoOnYomitanPopup',
|
||||
'ankiConnect.enabled',
|
||||
'anilist.characterDictionary.enabled',
|
||||
'subtitleStyle.nameMatchEnabled',
|
||||
'anilist.characterDictionary.collapsibleSections.description',
|
||||
'mpv.executablePath',
|
||||
'mpv.launchMode',
|
||||
|
||||
@@ -83,6 +83,30 @@ export function buildCoreConfigOptionRegistry(
|
||||
defaultValue: defaultConfig.logging.level,
|
||||
description: 'Minimum log level for runtime logging.',
|
||||
},
|
||||
{
|
||||
path: 'logging.rotation',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.logging.rotation,
|
||||
description: 'Number of days of app, launcher, and mpv logs to retain.',
|
||||
},
|
||||
{
|
||||
path: 'logging.files.app',
|
||||
kind: 'boolean',
|
||||
defaultValue: defaultConfig.logging.files.app,
|
||||
description: 'Write SubMiner app runtime logs.',
|
||||
},
|
||||
{
|
||||
path: 'logging.files.launcher',
|
||||
kind: 'boolean',
|
||||
defaultValue: defaultConfig.logging.files.launcher,
|
||||
description: 'Write launcher command logs.',
|
||||
},
|
||||
{
|
||||
path: 'logging.files.mpv',
|
||||
kind: 'boolean',
|
||||
defaultValue: defaultConfig.logging.files.mpv,
|
||||
description: 'Write mpv player logs. Enable temporarily when debugging mpv/plugin startup.',
|
||||
},
|
||||
{
|
||||
path: 'youtube.primarySubLanguages',
|
||||
kind: 'string',
|
||||
|
||||
@@ -392,13 +392,6 @@ export function buildIntegrationConfigOptionRegistry(
|
||||
description:
|
||||
'Optional explicit AniList access token override; leave empty to use locally stored token from setup.',
|
||||
},
|
||||
{
|
||||
path: 'anilist.characterDictionary.enabled',
|
||||
kind: 'boolean',
|
||||
defaultValue: defaultConfig.anilist.characterDictionary.enabled,
|
||||
description:
|
||||
'Enable automatic Yomitan character dictionary sync for currently watched AniList media.',
|
||||
},
|
||||
{
|
||||
path: 'anilist.characterDictionary.refreshTtlHours',
|
||||
kind: 'number',
|
||||
@@ -426,7 +419,7 @@ export function buildIntegrationConfigOptionRegistry(
|
||||
kind: 'enum',
|
||||
enumValues: ['all', 'active'],
|
||||
defaultValue: defaultConfig.anilist.characterDictionary.profileScope,
|
||||
description: 'Yomitan profile scope for dictionary enable/disable updates.',
|
||||
description: 'Yomitan profile scope for character dictionary settings updates.',
|
||||
},
|
||||
{
|
||||
path: 'anilist.characterDictionary.collapsibleSections.description',
|
||||
|
||||
@@ -74,7 +74,7 @@ export function buildSubtitleConfigOptionRegistry(
|
||||
kind: 'boolean',
|
||||
defaultValue: defaultConfig.subtitleStyle.nameMatchEnabled,
|
||||
description:
|
||||
'Enable subtitle token coloring for matches from the SubMiner character dictionary.',
|
||||
'Enable character dictionary sync and subtitle token coloring for character-name matches.',
|
||||
},
|
||||
{
|
||||
path: 'subtitleStyle.nameMatchImagesEnabled',
|
||||
|
||||
@@ -33,7 +33,7 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
{
|
||||
title: 'Logging',
|
||||
description: ['Controls logging verbosity.', 'Set to debug for full runtime diagnostics.'],
|
||||
notes: ['Hot-reload: logging.level applies live while SubMiner is running.'],
|
||||
notes: ['Hot-reload: logging.level and logging.files apply live while SubMiner is running.'],
|
||||
key: 'logging',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -100,6 +100,36 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
|
||||
'Expected debug, info, warn, or error.',
|
||||
);
|
||||
}
|
||||
|
||||
const logRotation = src.logging.rotation;
|
||||
if (typeof logRotation === 'number' && Number.isInteger(logRotation) && logRotation > 0) {
|
||||
resolved.logging.rotation = logRotation;
|
||||
} else if (src.logging.rotation !== undefined) {
|
||||
warn(
|
||||
'logging.rotation',
|
||||
src.logging.rotation,
|
||||
resolved.logging.rotation,
|
||||
'Expected a positive whole number of days.',
|
||||
);
|
||||
}
|
||||
|
||||
if (isObject(src.logging.files)) {
|
||||
for (const key of ['app', 'launcher', 'mpv'] as const) {
|
||||
const enabled = asBoolean(src.logging.files[key]);
|
||||
if (enabled !== undefined) {
|
||||
resolved.logging.files[key] = enabled;
|
||||
} else if (src.logging.files[key] !== undefined) {
|
||||
warn(
|
||||
`logging.files.${key}`,
|
||||
src.logging.files[key],
|
||||
resolved.logging.files[key],
|
||||
'Expected boolean.',
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (src.logging.files !== undefined) {
|
||||
warn('logging.files', src.logging.files, resolved.logging.files, 'Expected object.');
|
||||
}
|
||||
}
|
||||
|
||||
applyControllerConfig(context);
|
||||
|
||||
@@ -81,18 +81,6 @@ export function applyIntegrationConfig(context: ResolveContext): void {
|
||||
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)));
|
||||
|
||||
@@ -97,7 +97,6 @@ test('anilist character dictionary fields are parsed, clamped, and enum-validate
|
||||
const { context, warnings } = createResolveContext({
|
||||
anilist: {
|
||||
characterDictionary: {
|
||||
enabled: true,
|
||||
refreshTtlHours: 0,
|
||||
maxLoaded: 99,
|
||||
evictionPolicy: 'purge' as never,
|
||||
@@ -113,7 +112,6 @@ test('anilist character dictionary fields are parsed, clamped, and enum-validate
|
||||
|
||||
applyIntegrationConfig(context);
|
||||
|
||||
assert.equal(context.resolved.anilist.characterDictionary.enabled, true);
|
||||
assert.equal(context.resolved.anilist.characterDictionary.refreshTtlHours, 1);
|
||||
assert.equal(context.resolved.anilist.characterDictionary.maxLoaded, 20);
|
||||
assert.equal(context.resolved.anilist.characterDictionary.evictionPolicy, 'delete');
|
||||
|
||||
@@ -55,10 +55,6 @@ test('settings registry moves AniSkip button key into input shortcuts and hot re
|
||||
});
|
||||
|
||||
test('settings registry exposes character dictionary panel shortcuts dynamically', () => {
|
||||
assert.equal(
|
||||
fields.some((candidate) => candidate.configPath === 'shortcuts.openCharacterDictionary'),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
field('shortcuts.openCharacterDictionaryManager').label,
|
||||
'Open Character Dictionary Manager',
|
||||
@@ -69,7 +65,6 @@ test('settings registry exposes character dictionary panel shortcuts dynamically
|
||||
test('settings registry hides removed modal-only fields', () => {
|
||||
for (const path of [
|
||||
'shortcuts.multiCopyTimeoutMs',
|
||||
'shortcuts.openCharacterDictionary',
|
||||
'anilist.characterDictionary.profileScope',
|
||||
'jellyfin.directPlayContainers',
|
||||
]) {
|
||||
@@ -265,7 +260,7 @@ test('settings registry hides app-managed and inactive config surfaces', () => {
|
||||
]) {
|
||||
assert.equal(paths.has(hiddenPath), false, `${hiddenPath} should be hidden`);
|
||||
}
|
||||
assert.equal(field('anilist.characterDictionary.enabled').section, 'Character Dictionary');
|
||||
assert.equal(paths.has('anilist.characterDictionary.enabled'), false);
|
||||
});
|
||||
|
||||
test('settings registry marks safe live config paths as hot-reloadable', () => {
|
||||
@@ -274,6 +269,10 @@ test('settings registry marks safe live config paths as hot-reloadable', () => {
|
||||
'stats.toggleKey',
|
||||
'stats.markWatchedKey',
|
||||
'logging.level',
|
||||
'logging.rotation',
|
||||
'logging.files.app',
|
||||
'logging.files.launcher',
|
||||
'logging.files.mpv',
|
||||
'youtube.primarySubLanguages',
|
||||
'jimaku.apiBaseUrl',
|
||||
'jimaku.languagePreference',
|
||||
|
||||
@@ -63,6 +63,7 @@ export const LEGACY_HIDDEN_CONFIG_PATHS = [
|
||||
'controller.preferredGamepadLabel',
|
||||
'controller.profiles',
|
||||
'youtubeSubgen.primarySubLanguages',
|
||||
'anilist.characterDictionary.enabled',
|
||||
'anilist.characterDictionary.refreshTtlHours',
|
||||
'anilist.characterDictionary.evictionPolicy',
|
||||
'anilist.characterDictionary.profileScope',
|
||||
@@ -184,6 +185,11 @@ const PATH_ORDER = new Map<string, number>(
|
||||
'mpv.launchMode',
|
||||
'mpv.executablePath',
|
||||
'mpv.aniskipButtonKey',
|
||||
'logging.level',
|
||||
'logging.rotation',
|
||||
'logging.files.app',
|
||||
'logging.files.launcher',
|
||||
'logging.files.mpv',
|
||||
].map((path, index) => [path, index]),
|
||||
);
|
||||
|
||||
@@ -667,6 +673,8 @@ function restartBehaviorForPath(path: string): ConfigSettingsRestartBehavior {
|
||||
path === 'stats.toggleKey' ||
|
||||
path === 'stats.markWatchedKey' ||
|
||||
path === 'logging.level' ||
|
||||
path === 'logging.rotation' ||
|
||||
pathStartsWith(path, 'logging.files') ||
|
||||
path === 'youtube.primarySubLanguages' ||
|
||||
pathStartsWith(path, 'jimaku') ||
|
||||
pathStartsWith(path, 'subsync')
|
||||
|
||||
Reference in New Issue
Block a user