mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 18:22:42 -08:00
feat: improve background startup and launcher control
Detach --background launches from terminals with quieter runtime output, make wrapper/plugin overlay start explicit, and allow trailing commas in JSONC configs for safer hot-reload edits. Includes pending Anki/docs/backlog updates in this unreleased batch.
This commit is contained in:
@@ -17,6 +17,7 @@ test('loads defaults when config is missing', () => {
|
||||
const config = service.getConfig();
|
||||
assert.equal(config.websocket.port, DEFAULT_CONFIG.websocket.port);
|
||||
assert.equal(config.ankiConnect.behavior.autoUpdateNewCards, true);
|
||||
assert.deepEqual(config.ankiConnect.tags, ['SubMiner']);
|
||||
assert.equal(config.anilist.enabled, false);
|
||||
assert.equal(config.jellyfin.remoteControlEnabled, true);
|
||||
assert.equal(config.jellyfin.remoteControlAutoConnect, true);
|
||||
@@ -258,6 +259,28 @@ test('parses jsonc and warns/falls back on invalid value', () => {
|
||||
assert.ok(service.getWarnings().some((w) => w.path === 'websocket.port'));
|
||||
});
|
||||
|
||||
test('accepts trailing commas in jsonc', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"websocket": {
|
||||
"enabled": "auto",
|
||||
"port": 7788,
|
||||
},
|
||||
"youtubeSubgen": {
|
||||
"primarySubLanguages": ["ja", "en",],
|
||||
},
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
assert.equal(config.websocket.port, 7788);
|
||||
assert.deepEqual(config.youtubeSubgen.primarySubLanguages, ['ja', 'en']);
|
||||
});
|
||||
|
||||
test('reloadConfigStrict rejects invalid jsonc and preserves previous config', () => {
|
||||
const dir = makeTempDir();
|
||||
const configPath = path.join(dir, 'config.jsonc');
|
||||
@@ -631,6 +654,44 @@ test('accepts valid ankiConnect n+1 deck list', () => {
|
||||
assert.deepEqual(config.ankiConnect.nPlusOne.decks, ['Deck One', 'Deck Two']);
|
||||
});
|
||||
|
||||
test('accepts valid ankiConnect tags list', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"tags": ["SubMiner", "Mining"]
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.deepEqual(config.ankiConnect.tags, ['SubMiner', 'Mining']);
|
||||
});
|
||||
|
||||
test('falls back to default when ankiConnect tags list is invalid', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"tags": ["SubMiner", 123]
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.deepEqual(config.ankiConnect.tags, ['SubMiner']);
|
||||
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.tags'));
|
||||
});
|
||||
|
||||
test('falls back to default when ankiConnect n+1 deck list is invalid', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -79,6 +79,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
||||
enabled: false,
|
||||
url: 'http://127.0.0.1:8765',
|
||||
pollingRate: 3000,
|
||||
tags: ['SubMiner'],
|
||||
fields: {
|
||||
audio: 'ExpressionAudio',
|
||||
image: 'Picture',
|
||||
@@ -397,6 +398,13 @@ export const CONFIG_OPTION_REGISTRY: ConfigOptionRegistryEntry[] = [
|
||||
defaultValue: DEFAULT_CONFIG.ankiConnect.pollingRate,
|
||||
description: 'Polling interval in milliseconds.',
|
||||
},
|
||||
{
|
||||
path: 'ankiConnect.tags',
|
||||
kind: 'array',
|
||||
defaultValue: DEFAULT_CONFIG.ankiConnect.tags,
|
||||
description:
|
||||
'Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.',
|
||||
},
|
||||
{
|
||||
path: 'ankiConnect.behavior.autoUpdateNewCards',
|
||||
kind: 'boolean',
|
||||
|
||||
@@ -174,7 +174,10 @@ export class ConfigService {
|
||||
const parsed = configPath.endsWith('.jsonc')
|
||||
? (() => {
|
||||
const errors: ParseError[] = [];
|
||||
const result = parseJsonc(data, errors);
|
||||
const result = parseJsonc(data, errors, {
|
||||
allowTrailingComma: true,
|
||||
disallowComments: false,
|
||||
});
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Invalid JSONC (${errors[0]?.error ?? 'unknown'})`);
|
||||
}
|
||||
@@ -889,6 +892,32 @@ export class ConfigService {
|
||||
},
|
||||
};
|
||||
|
||||
if (Array.isArray(ac.tags)) {
|
||||
const normalizedTags = ac.tags
|
||||
.filter((entry): entry is string => typeof entry === 'string')
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0);
|
||||
if (normalizedTags.length === ac.tags.length) {
|
||||
resolved.ankiConnect.tags = [...new Set(normalizedTags)];
|
||||
} else {
|
||||
resolved.ankiConnect.tags = DEFAULT_CONFIG.ankiConnect.tags;
|
||||
warn(
|
||||
'ankiConnect.tags',
|
||||
ac.tags,
|
||||
resolved.ankiConnect.tags,
|
||||
'Expected an array of non-empty strings.',
|
||||
);
|
||||
}
|
||||
} else if (ac.tags !== undefined) {
|
||||
resolved.ankiConnect.tags = DEFAULT_CONFIG.ankiConnect.tags;
|
||||
warn(
|
||||
'ankiConnect.tags',
|
||||
ac.tags,
|
||||
resolved.ankiConnect.tags,
|
||||
'Expected an array of strings.',
|
||||
);
|
||||
}
|
||||
|
||||
const legacy = ac as Record<string, unknown>;
|
||||
const mapLegacy = (key: string, apply: (value: unknown) => void): void => {
|
||||
if (legacy[key] !== undefined) apply(legacy[key]);
|
||||
|
||||
Reference in New Issue
Block a user