mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 18:22:42 -08:00
feat(jellyfin): add remote playback and config plumbing
This commit is contained in:
@@ -18,8 +18,22 @@ test("loads defaults when config is missing", () => {
|
||||
assert.equal(config.websocket.port, DEFAULT_CONFIG.websocket.port);
|
||||
assert.equal(config.ankiConnect.behavior.autoUpdateNewCards, true);
|
||||
assert.equal(config.anilist.enabled, false);
|
||||
assert.equal(config.jellyfin.remoteControlEnabled, true);
|
||||
assert.equal(config.jellyfin.remoteControlAutoConnect, true);
|
||||
assert.equal(config.jellyfin.autoAnnounce, false);
|
||||
assert.equal(config.jellyfin.remoteControlDeviceName, "SubMiner");
|
||||
assert.equal(config.immersionTracking.enabled, true);
|
||||
assert.equal(config.immersionTracking.dbPath, undefined);
|
||||
assert.equal(config.immersionTracking.dbPath, "");
|
||||
assert.equal(config.immersionTracking.batchSize, 25);
|
||||
assert.equal(config.immersionTracking.flushIntervalMs, 500);
|
||||
assert.equal(config.immersionTracking.queueCap, 1000);
|
||||
assert.equal(config.immersionTracking.payloadCapBytes, 256);
|
||||
assert.equal(config.immersionTracking.maintenanceIntervalMs, 86_400_000);
|
||||
assert.equal(config.immersionTracking.retention.eventsDays, 7);
|
||||
assert.equal(config.immersionTracking.retention.telemetryDays, 30);
|
||||
assert.equal(config.immersionTracking.retention.dailyRollupsDays, 365);
|
||||
assert.equal(config.immersionTracking.retention.monthlyRollupsDays, 1825);
|
||||
assert.equal(config.immersionTracking.retention.vacuumIntervalDays, 7);
|
||||
});
|
||||
|
||||
test("parses anilist.enabled and warns for invalid value", () => {
|
||||
@@ -45,6 +59,90 @@ test("parses anilist.enabled and warns for invalid value", () => {
|
||||
assert.equal(service.getConfig().anilist.enabled, true);
|
||||
});
|
||||
|
||||
test("parses jellyfin remote control fields", () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
`{
|
||||
"jellyfin": {
|
||||
"enabled": true,
|
||||
"serverUrl": "http://127.0.0.1:8096",
|
||||
"remoteControlEnabled": true,
|
||||
"remoteControlAutoConnect": true,
|
||||
"autoAnnounce": true,
|
||||
"remoteControlDeviceName": "SubMiner"
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.equal(config.jellyfin.enabled, true);
|
||||
assert.equal(config.jellyfin.serverUrl, "http://127.0.0.1:8096");
|
||||
assert.equal(config.jellyfin.remoteControlEnabled, true);
|
||||
assert.equal(config.jellyfin.remoteControlAutoConnect, true);
|
||||
assert.equal(config.jellyfin.autoAnnounce, true);
|
||||
assert.equal(config.jellyfin.remoteControlDeviceName, "SubMiner");
|
||||
});
|
||||
|
||||
test("parses jellyfin.enabled and remoteControlEnabled disabled combinations", () => {
|
||||
const disabledDir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(disabledDir, "config.jsonc"),
|
||||
`{
|
||||
"jellyfin": {
|
||||
"enabled": false,
|
||||
"remoteControlEnabled": false
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const disabledService = new ConfigService(disabledDir);
|
||||
const disabledConfig = disabledService.getConfig();
|
||||
assert.equal(disabledConfig.jellyfin.enabled, false);
|
||||
assert.equal(disabledConfig.jellyfin.remoteControlEnabled, false);
|
||||
assert.equal(
|
||||
disabledService
|
||||
.getWarnings()
|
||||
.some(
|
||||
(warning) =>
|
||||
warning.path === "jellyfin.enabled" ||
|
||||
warning.path === "jellyfin.remoteControlEnabled",
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
const mixedDir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(mixedDir, "config.jsonc"),
|
||||
`{
|
||||
"jellyfin": {
|
||||
"enabled": true,
|
||||
"remoteControlEnabled": false
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const mixedService = new ConfigService(mixedDir);
|
||||
const mixedConfig = mixedService.getConfig();
|
||||
assert.equal(mixedConfig.jellyfin.enabled, true);
|
||||
assert.equal(mixedConfig.jellyfin.remoteControlEnabled, false);
|
||||
assert.equal(
|
||||
mixedService
|
||||
.getWarnings()
|
||||
.some(
|
||||
(warning) =>
|
||||
warning.path === "jellyfin.enabled" ||
|
||||
warning.path === "jellyfin.remoteControlEnabled",
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts immersion tracking config values", () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
@@ -52,7 +150,19 @@ test("accepts immersion tracking config values", () => {
|
||||
`{
|
||||
"immersionTracking": {
|
||||
"enabled": false,
|
||||
"dbPath": "/tmp/immersions/custom.sqlite"
|
||||
"dbPath": "/tmp/immersions/custom.sqlite",
|
||||
"batchSize": 50,
|
||||
"flushIntervalMs": 750,
|
||||
"queueCap": 2000,
|
||||
"payloadCapBytes": 512,
|
||||
"maintenanceIntervalMs": 3600000,
|
||||
"retention": {
|
||||
"eventsDays": 14,
|
||||
"telemetryDays": 45,
|
||||
"dailyRollupsDays": 730,
|
||||
"monthlyRollupsDays": 3650,
|
||||
"vacuumIntervalDays": 14
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
@@ -62,7 +172,109 @@ test("accepts immersion tracking config values", () => {
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.equal(config.immersionTracking.enabled, false);
|
||||
assert.equal(config.immersionTracking.dbPath, "/tmp/immersions/custom.sqlite");
|
||||
assert.equal(
|
||||
config.immersionTracking.dbPath,
|
||||
"/tmp/immersions/custom.sqlite",
|
||||
);
|
||||
assert.equal(config.immersionTracking.batchSize, 50);
|
||||
assert.equal(config.immersionTracking.flushIntervalMs, 750);
|
||||
assert.equal(config.immersionTracking.queueCap, 2000);
|
||||
assert.equal(config.immersionTracking.payloadCapBytes, 512);
|
||||
assert.equal(config.immersionTracking.maintenanceIntervalMs, 3_600_000);
|
||||
assert.equal(config.immersionTracking.retention.eventsDays, 14);
|
||||
assert.equal(config.immersionTracking.retention.telemetryDays, 45);
|
||||
assert.equal(config.immersionTracking.retention.dailyRollupsDays, 730);
|
||||
assert.equal(config.immersionTracking.retention.monthlyRollupsDays, 3650);
|
||||
assert.equal(config.immersionTracking.retention.vacuumIntervalDays, 14);
|
||||
});
|
||||
|
||||
test("falls back for invalid immersion tracking tuning values", () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
`{
|
||||
"immersionTracking": {
|
||||
"batchSize": 0,
|
||||
"flushIntervalMs": 1,
|
||||
"queueCap": 5,
|
||||
"payloadCapBytes": 16,
|
||||
"maintenanceIntervalMs": 1000,
|
||||
"retention": {
|
||||
"eventsDays": 0,
|
||||
"telemetryDays": 99999,
|
||||
"dailyRollupsDays": 0,
|
||||
"monthlyRollupsDays": 999999,
|
||||
"vacuumIntervalDays": 0
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.equal(config.immersionTracking.batchSize, 25);
|
||||
assert.equal(config.immersionTracking.flushIntervalMs, 500);
|
||||
assert.equal(config.immersionTracking.queueCap, 1000);
|
||||
assert.equal(config.immersionTracking.payloadCapBytes, 256);
|
||||
assert.equal(config.immersionTracking.maintenanceIntervalMs, 86_400_000);
|
||||
assert.equal(config.immersionTracking.retention.eventsDays, 7);
|
||||
assert.equal(config.immersionTracking.retention.telemetryDays, 30);
|
||||
assert.equal(config.immersionTracking.retention.dailyRollupsDays, 365);
|
||||
assert.equal(config.immersionTracking.retention.monthlyRollupsDays, 1825);
|
||||
assert.equal(config.immersionTracking.retention.vacuumIntervalDays, 7);
|
||||
|
||||
assert.ok(
|
||||
warnings.some((warning) => warning.path === "immersionTracking.batchSize"),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) => warning.path === "immersionTracking.flushIntervalMs",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some((warning) => warning.path === "immersionTracking.queueCap"),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) => warning.path === "immersionTracking.payloadCapBytes",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) => warning.path === "immersionTracking.maintenanceIntervalMs",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) => warning.path === "immersionTracking.retention.eventsDays",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) => warning.path === "immersionTracking.retention.telemetryDays",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) =>
|
||||
warning.path === "immersionTracking.retention.dailyRollupsDays",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) =>
|
||||
warning.path === "immersionTracking.retention.monthlyRollupsDays",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) =>
|
||||
warning.path === "immersionTracking.retention.vacuumIntervalDays",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("parses jsonc and warns/falls back on invalid value", () => {
|
||||
@@ -117,9 +329,7 @@ test("falls back for invalid logging.level and reports warning", () => {
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.equal(config.logging.level, DEFAULT_CONFIG.logging.level);
|
||||
assert.ok(
|
||||
warnings.some((warning) => warning.path === "logging.level"),
|
||||
);
|
||||
assert.ok(warnings.some((warning) => warning.path === "logging.level"));
|
||||
});
|
||||
|
||||
test("parses invisible overlay config and new global shortcuts", () => {
|
||||
@@ -150,7 +360,11 @@ test("parses invisible overlay config and new global shortcuts", () => {
|
||||
assert.equal(config.shortcuts.openJimaku, "Ctrl+Alt+J");
|
||||
assert.equal(config.invisibleOverlay.startupVisibility, "hidden");
|
||||
assert.equal(config.bind_visible_overlay_to_mpv_sub_visibility, false);
|
||||
assert.deepEqual(config.youtubeSubgen.primarySubLanguages, ["ja", "jpn", "jp"]);
|
||||
assert.deepEqual(config.youtubeSubgen.primarySubLanguages, [
|
||||
"ja",
|
||||
"jpn",
|
||||
"jp",
|
||||
]);
|
||||
});
|
||||
|
||||
test("runtime options registry is centralized", () => {
|
||||
@@ -295,8 +509,8 @@ test("validates ankiConnect n+1 match mode values", () => {
|
||||
DEFAULT_CONFIG.ankiConnect.nPlusOne.matchMode,
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some((warning) =>
|
||||
warning.path === "ankiConnect.nPlusOne.matchMode",
|
||||
warnings.some(
|
||||
(warning) => warning.path === "ankiConnect.nPlusOne.matchMode",
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -349,10 +563,14 @@ test("validates ankiConnect n+1 color values", () => {
|
||||
DEFAULT_CONFIG.ankiConnect.nPlusOne.knownWord,
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some((warning) => warning.path === "ankiConnect.nPlusOne.nPlusOne"),
|
||||
warnings.some(
|
||||
(warning) => warning.path === "ankiConnect.nPlusOne.nPlusOne",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some((warning) => warning.path === "ankiConnect.nPlusOne.knownWord"),
|
||||
warnings.some(
|
||||
(warning) => warning.path === "ankiConnect.nPlusOne.knownWord",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -201,13 +201,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
||||
topX: 1000,
|
||||
mode: "single",
|
||||
singleColor: "#f5a97f",
|
||||
bandedColors: [
|
||||
"#ed8796",
|
||||
"#f5a97f",
|
||||
"#f9e2af",
|
||||
"#a6e3a1",
|
||||
"#8aadf4",
|
||||
],
|
||||
bandedColors: ["#ed8796", "#f5a97f", "#f9e2af", "#a6e3a1", "#8aadf4"],
|
||||
},
|
||||
secondary: {
|
||||
fontSize: 24,
|
||||
@@ -230,6 +224,26 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
||||
enabled: false,
|
||||
accessToken: "",
|
||||
},
|
||||
jellyfin: {
|
||||
enabled: false,
|
||||
serverUrl: "",
|
||||
username: "",
|
||||
accessToken: "",
|
||||
userId: "",
|
||||
deviceId: "subminer",
|
||||
clientName: "SubMiner",
|
||||
clientVersion: "0.1.0",
|
||||
defaultLibraryId: "",
|
||||
remoteControlEnabled: true,
|
||||
remoteControlAutoConnect: true,
|
||||
autoAnnounce: false,
|
||||
remoteControlDeviceName: "SubMiner",
|
||||
pullPictures: false,
|
||||
iconCacheDir: "/tmp/subminer-jellyfin-icons",
|
||||
directPlayPreferred: true,
|
||||
directPlayContainers: ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"],
|
||||
transcodeVideoCodec: "h264",
|
||||
},
|
||||
youtubeSubgen: {
|
||||
mode: "automatic",
|
||||
whisperBin: "",
|
||||
@@ -241,6 +255,19 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
||||
},
|
||||
immersionTracking: {
|
||||
enabled: true,
|
||||
dbPath: "",
|
||||
batchSize: 25,
|
||||
flushIntervalMs: 500,
|
||||
queueCap: 1000,
|
||||
payloadCapBytes: 256,
|
||||
maintenanceIntervalMs: 24 * 60 * 60 * 1000,
|
||||
retention: {
|
||||
eventsDays: 7,
|
||||
telemetryDays: 30,
|
||||
dailyRollupsDays: 365,
|
||||
monthlyRollupsDays: 5 * 365,
|
||||
vacuumIntervalDays: 7,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -324,8 +351,9 @@ export const CONFIG_OPTION_REGISTRY: ConfigOptionRegistryEntry[] = [
|
||||
path: "subtitleStyle.enableJlpt",
|
||||
kind: "boolean",
|
||||
defaultValue: DEFAULT_CONFIG.subtitleStyle.enableJlpt,
|
||||
description: "Enable JLPT vocabulary level underlines. "
|
||||
+ "When disabled, JLPT tagging lookup and underlines are skipped.",
|
||||
description:
|
||||
"Enable JLPT vocabulary level underlines. " +
|
||||
"When disabled, JLPT tagging lookup and underlines are skipped.",
|
||||
},
|
||||
{
|
||||
path: "subtitleStyle.frequencyDictionary.enabled",
|
||||
@@ -339,14 +367,15 @@ export const CONFIG_OPTION_REGISTRY: ConfigOptionRegistryEntry[] = [
|
||||
kind: "string",
|
||||
defaultValue: DEFAULT_CONFIG.subtitleStyle.frequencyDictionary.sourcePath,
|
||||
description:
|
||||
"Optional absolute path to a frequency dictionary directory."
|
||||
+ " If empty, built-in discovery search paths are used.",
|
||||
"Optional absolute path to a frequency dictionary directory." +
|
||||
" If empty, built-in discovery search paths are used.",
|
||||
},
|
||||
{
|
||||
path: "subtitleStyle.frequencyDictionary.topX",
|
||||
kind: "number",
|
||||
defaultValue: DEFAULT_CONFIG.subtitleStyle.frequencyDictionary.topX,
|
||||
description: "Only color tokens with frequency rank <= topX (default: 1000).",
|
||||
description:
|
||||
"Only color tokens with frequency rank <= topX (default: 1000).",
|
||||
},
|
||||
{
|
||||
path: "subtitleStyle.frequencyDictionary.mode",
|
||||
@@ -399,7 +428,8 @@ export const CONFIG_OPTION_REGISTRY: ConfigOptionRegistryEntry[] = [
|
||||
path: "ankiConnect.nPlusOne.highlightEnabled",
|
||||
kind: "boolean",
|
||||
defaultValue: DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled,
|
||||
description: "Enable fast local highlighting for words already known in Anki.",
|
||||
description:
|
||||
"Enable fast local highlighting for words already known in Anki.",
|
||||
},
|
||||
{
|
||||
path: "ankiConnect.nPlusOne.refreshMinutes",
|
||||
@@ -486,6 +516,89 @@ export const CONFIG_OPTION_REGISTRY: ConfigOptionRegistryEntry[] = [
|
||||
defaultValue: DEFAULT_CONFIG.anilist.accessToken,
|
||||
description: "AniList access token used for post-watch updates.",
|
||||
},
|
||||
{
|
||||
path: "jellyfin.enabled",
|
||||
kind: "boolean",
|
||||
defaultValue: DEFAULT_CONFIG.jellyfin.enabled,
|
||||
description:
|
||||
"Enable optional Jellyfin integration and CLI control commands.",
|
||||
},
|
||||
{
|
||||
path: "jellyfin.serverUrl",
|
||||
kind: "string",
|
||||
defaultValue: DEFAULT_CONFIG.jellyfin.serverUrl,
|
||||
description:
|
||||
"Base Jellyfin server URL (for example: http://localhost:8096).",
|
||||
},
|
||||
{
|
||||
path: "jellyfin.username",
|
||||
kind: "string",
|
||||
defaultValue: DEFAULT_CONFIG.jellyfin.username,
|
||||
description: "Default Jellyfin username used during CLI login.",
|
||||
},
|
||||
{
|
||||
path: "jellyfin.defaultLibraryId",
|
||||
kind: "string",
|
||||
defaultValue: DEFAULT_CONFIG.jellyfin.defaultLibraryId,
|
||||
description: "Optional default Jellyfin library ID for item listing.",
|
||||
},
|
||||
{
|
||||
path: "jellyfin.remoteControlEnabled",
|
||||
kind: "boolean",
|
||||
defaultValue: DEFAULT_CONFIG.jellyfin.remoteControlEnabled,
|
||||
description: "Enable Jellyfin remote cast control mode.",
|
||||
},
|
||||
{
|
||||
path: "jellyfin.remoteControlAutoConnect",
|
||||
kind: "boolean",
|
||||
defaultValue: DEFAULT_CONFIG.jellyfin.remoteControlAutoConnect,
|
||||
description: "Auto-connect to the configured remote control target.",
|
||||
},
|
||||
{
|
||||
path: "jellyfin.autoAnnounce",
|
||||
kind: "boolean",
|
||||
defaultValue: DEFAULT_CONFIG.jellyfin.autoAnnounce,
|
||||
description:
|
||||
"When enabled, automatically trigger remote announce/visibility check on websocket connect.",
|
||||
},
|
||||
{
|
||||
path: "jellyfin.remoteControlDeviceName",
|
||||
kind: "string",
|
||||
defaultValue: DEFAULT_CONFIG.jellyfin.remoteControlDeviceName,
|
||||
description: "Device name reported for Jellyfin remote control sessions.",
|
||||
},
|
||||
{
|
||||
path: "jellyfin.pullPictures",
|
||||
kind: "boolean",
|
||||
defaultValue: DEFAULT_CONFIG.jellyfin.pullPictures,
|
||||
description: "Enable Jellyfin poster/icon fetching for launcher menus.",
|
||||
},
|
||||
{
|
||||
path: "jellyfin.iconCacheDir",
|
||||
kind: "string",
|
||||
defaultValue: DEFAULT_CONFIG.jellyfin.iconCacheDir,
|
||||
description: "Directory used by launcher for cached Jellyfin poster icons.",
|
||||
},
|
||||
{
|
||||
path: "jellyfin.directPlayPreferred",
|
||||
kind: "boolean",
|
||||
defaultValue: DEFAULT_CONFIG.jellyfin.directPlayPreferred,
|
||||
description:
|
||||
"Try direct play before server-managed transcoding when possible.",
|
||||
},
|
||||
{
|
||||
path: "jellyfin.directPlayContainers",
|
||||
kind: "array",
|
||||
defaultValue: DEFAULT_CONFIG.jellyfin.directPlayContainers,
|
||||
description: "Container allowlist for direct play decisions.",
|
||||
},
|
||||
{
|
||||
path: "jellyfin.transcodeVideoCodec",
|
||||
kind: "string",
|
||||
defaultValue: DEFAULT_CONFIG.jellyfin.transcodeVideoCodec,
|
||||
description:
|
||||
"Preferred transcode video codec when direct play is unavailable.",
|
||||
},
|
||||
{
|
||||
path: "youtubeSubgen.mode",
|
||||
kind: "enum",
|
||||
@@ -497,7 +610,8 @@ export const CONFIG_OPTION_REGISTRY: ConfigOptionRegistryEntry[] = [
|
||||
path: "youtubeSubgen.whisperBin",
|
||||
kind: "string",
|
||||
defaultValue: DEFAULT_CONFIG.youtubeSubgen.whisperBin,
|
||||
description: "Path to whisper.cpp CLI used as fallback transcription engine.",
|
||||
description:
|
||||
"Path to whisper.cpp CLI used as fallback transcription engine.",
|
||||
},
|
||||
{
|
||||
path: "youtubeSubgen.whisperModel",
|
||||
@@ -525,6 +639,66 @@ export const CONFIG_OPTION_REGISTRY: ConfigOptionRegistryEntry[] = [
|
||||
description:
|
||||
"Optional SQLite database path for immersion tracking. Empty value uses the default app data path.",
|
||||
},
|
||||
{
|
||||
path: "immersionTracking.batchSize",
|
||||
kind: "number",
|
||||
defaultValue: DEFAULT_CONFIG.immersionTracking.batchSize,
|
||||
description: "Buffered telemetry/event writes per SQLite transaction.",
|
||||
},
|
||||
{
|
||||
path: "immersionTracking.flushIntervalMs",
|
||||
kind: "number",
|
||||
defaultValue: DEFAULT_CONFIG.immersionTracking.flushIntervalMs,
|
||||
description: "Max delay before queue flush in milliseconds.",
|
||||
},
|
||||
{
|
||||
path: "immersionTracking.queueCap",
|
||||
kind: "number",
|
||||
defaultValue: DEFAULT_CONFIG.immersionTracking.queueCap,
|
||||
description: "In-memory write queue cap before overflow policy applies.",
|
||||
},
|
||||
{
|
||||
path: "immersionTracking.payloadCapBytes",
|
||||
kind: "number",
|
||||
defaultValue: DEFAULT_CONFIG.immersionTracking.payloadCapBytes,
|
||||
description: "Max JSON payload size per event before truncation.",
|
||||
},
|
||||
{
|
||||
path: "immersionTracking.maintenanceIntervalMs",
|
||||
kind: "number",
|
||||
defaultValue: DEFAULT_CONFIG.immersionTracking.maintenanceIntervalMs,
|
||||
description: "Maintenance cadence (prune + rollup + vacuum checks).",
|
||||
},
|
||||
{
|
||||
path: "immersionTracking.retention.eventsDays",
|
||||
kind: "number",
|
||||
defaultValue: DEFAULT_CONFIG.immersionTracking.retention.eventsDays,
|
||||
description: "Raw event retention window in days.",
|
||||
},
|
||||
{
|
||||
path: "immersionTracking.retention.telemetryDays",
|
||||
kind: "number",
|
||||
defaultValue: DEFAULT_CONFIG.immersionTracking.retention.telemetryDays,
|
||||
description: "Telemetry retention window in days.",
|
||||
},
|
||||
{
|
||||
path: "immersionTracking.retention.dailyRollupsDays",
|
||||
kind: "number",
|
||||
defaultValue: DEFAULT_CONFIG.immersionTracking.retention.dailyRollupsDays,
|
||||
description: "Daily rollup retention window in days.",
|
||||
},
|
||||
{
|
||||
path: "immersionTracking.retention.monthlyRollupsDays",
|
||||
kind: "number",
|
||||
defaultValue: DEFAULT_CONFIG.immersionTracking.retention.monthlyRollupsDays,
|
||||
description: "Monthly rollup retention window in days.",
|
||||
},
|
||||
{
|
||||
path: "immersionTracking.retention.vacuumIntervalDays",
|
||||
kind: "number",
|
||||
defaultValue: DEFAULT_CONFIG.immersionTracking.retention.vacuumIntervalDays,
|
||||
description: "Minimum days between VACUUM runs.",
|
||||
},
|
||||
];
|
||||
|
||||
export const CONFIG_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
@@ -637,11 +811,20 @@ export const CONFIG_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
description: ["Anilist API credentials and update behavior."],
|
||||
key: "anilist",
|
||||
},
|
||||
{
|
||||
title: "Jellyfin",
|
||||
description: [
|
||||
"Optional Jellyfin integration for auth, browsing, and playback launch.",
|
||||
"Access token is stored in config and should be treated as a secret.",
|
||||
],
|
||||
key: "jellyfin",
|
||||
},
|
||||
{
|
||||
title: "Immersion Tracking",
|
||||
description: [
|
||||
"Enable/disable immersion tracking.",
|
||||
"Set dbPath to override the default sqlite database location.",
|
||||
"Policy tuning is available for queue, flush, and retention values.",
|
||||
],
|
||||
key: "immersionTracking",
|
||||
},
|
||||
|
||||
@@ -213,7 +213,12 @@ export class ConfigService {
|
||||
|
||||
if (isObject(src.logging)) {
|
||||
const logLevel = asString(src.logging.level);
|
||||
if (logLevel === "debug" || logLevel === "info" || logLevel === "warn" || logLevel === "error") {
|
||||
if (
|
||||
logLevel === "debug" ||
|
||||
logLevel === "info" ||
|
||||
logLevel === "warn" ||
|
||||
logLevel === "error"
|
||||
) {
|
||||
resolved.logging.level = logLevel;
|
||||
} else if (src.logging.level !== undefined) {
|
||||
warn(
|
||||
@@ -469,11 +474,90 @@ export class ConfigService {
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
"accessToken",
|
||||
"userId",
|
||||
"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 (asBoolean(src.auto_start_overlay) !== undefined) {
|
||||
resolved.auto_start_overlay = src.auto_start_overlay as boolean;
|
||||
}
|
||||
|
||||
if (asBoolean(src.bind_visible_overlay_to_mpv_sub_visibility) !== undefined) {
|
||||
if (
|
||||
asBoolean(src.bind_visible_overlay_to_mpv_sub_visibility) !== undefined
|
||||
) {
|
||||
resolved.bind_visible_overlay_to_mpv_sub_visibility =
|
||||
src.bind_visible_overlay_to_mpv_sub_visibility as boolean;
|
||||
} else if (src.bind_visible_overlay_to_mpv_sub_visibility !== undefined) {
|
||||
@@ -509,6 +593,191 @@ export class ConfigService {
|
||||
"Expected string.",
|
||||
);
|
||||
}
|
||||
|
||||
const batchSize = asNumber(src.immersionTracking.batchSize);
|
||||
if (batchSize !== undefined && batchSize >= 1 && batchSize <= 10_000) {
|
||||
resolved.immersionTracking.batchSize = Math.floor(batchSize);
|
||||
} else if (src.immersionTracking.batchSize !== undefined) {
|
||||
warn(
|
||||
"immersionTracking.batchSize",
|
||||
src.immersionTracking.batchSize,
|
||||
resolved.immersionTracking.batchSize,
|
||||
"Expected integer between 1 and 10000.",
|
||||
);
|
||||
}
|
||||
|
||||
const flushIntervalMs = asNumber(src.immersionTracking.flushIntervalMs);
|
||||
if (
|
||||
flushIntervalMs !== undefined &&
|
||||
flushIntervalMs >= 50 &&
|
||||
flushIntervalMs <= 60_000
|
||||
) {
|
||||
resolved.immersionTracking.flushIntervalMs = Math.floor(flushIntervalMs);
|
||||
} else if (src.immersionTracking.flushIntervalMs !== undefined) {
|
||||
warn(
|
||||
"immersionTracking.flushIntervalMs",
|
||||
src.immersionTracking.flushIntervalMs,
|
||||
resolved.immersionTracking.flushIntervalMs,
|
||||
"Expected integer between 50 and 60000.",
|
||||
);
|
||||
}
|
||||
|
||||
const queueCap = asNumber(src.immersionTracking.queueCap);
|
||||
if (queueCap !== undefined && queueCap >= 100 && queueCap <= 100_000) {
|
||||
resolved.immersionTracking.queueCap = Math.floor(queueCap);
|
||||
} else if (src.immersionTracking.queueCap !== undefined) {
|
||||
warn(
|
||||
"immersionTracking.queueCap",
|
||||
src.immersionTracking.queueCap,
|
||||
resolved.immersionTracking.queueCap,
|
||||
"Expected integer between 100 and 100000.",
|
||||
);
|
||||
}
|
||||
|
||||
const payloadCapBytes = asNumber(src.immersionTracking.payloadCapBytes);
|
||||
if (
|
||||
payloadCapBytes !== undefined &&
|
||||
payloadCapBytes >= 64 &&
|
||||
payloadCapBytes <= 8192
|
||||
) {
|
||||
resolved.immersionTracking.payloadCapBytes = Math.floor(payloadCapBytes);
|
||||
} else if (src.immersionTracking.payloadCapBytes !== undefined) {
|
||||
warn(
|
||||
"immersionTracking.payloadCapBytes",
|
||||
src.immersionTracking.payloadCapBytes,
|
||||
resolved.immersionTracking.payloadCapBytes,
|
||||
"Expected integer between 64 and 8192.",
|
||||
);
|
||||
}
|
||||
|
||||
const maintenanceIntervalMs = asNumber(
|
||||
src.immersionTracking.maintenanceIntervalMs,
|
||||
);
|
||||
if (
|
||||
maintenanceIntervalMs !== undefined &&
|
||||
maintenanceIntervalMs >= 60_000 &&
|
||||
maintenanceIntervalMs <= 7 * 24 * 60 * 60 * 1000
|
||||
) {
|
||||
resolved.immersionTracking.maintenanceIntervalMs = Math.floor(
|
||||
maintenanceIntervalMs,
|
||||
);
|
||||
} else if (src.immersionTracking.maintenanceIntervalMs !== undefined) {
|
||||
warn(
|
||||
"immersionTracking.maintenanceIntervalMs",
|
||||
src.immersionTracking.maintenanceIntervalMs,
|
||||
resolved.immersionTracking.maintenanceIntervalMs,
|
||||
"Expected integer between 60000 and 604800000.",
|
||||
);
|
||||
}
|
||||
|
||||
if (isObject(src.immersionTracking.retention)) {
|
||||
const eventsDays = asNumber(src.immersionTracking.retention.eventsDays);
|
||||
if (eventsDays !== undefined && eventsDays >= 1 && eventsDays <= 3650) {
|
||||
resolved.immersionTracking.retention.eventsDays =
|
||||
Math.floor(eventsDays);
|
||||
} else if (src.immersionTracking.retention.eventsDays !== undefined) {
|
||||
warn(
|
||||
"immersionTracking.retention.eventsDays",
|
||||
src.immersionTracking.retention.eventsDays,
|
||||
resolved.immersionTracking.retention.eventsDays,
|
||||
"Expected integer between 1 and 3650.",
|
||||
);
|
||||
}
|
||||
|
||||
const telemetryDays = asNumber(
|
||||
src.immersionTracking.retention.telemetryDays,
|
||||
);
|
||||
if (
|
||||
telemetryDays !== undefined &&
|
||||
telemetryDays >= 1 &&
|
||||
telemetryDays <= 3650
|
||||
) {
|
||||
resolved.immersionTracking.retention.telemetryDays =
|
||||
Math.floor(telemetryDays);
|
||||
} else if (
|
||||
src.immersionTracking.retention.telemetryDays !== undefined
|
||||
) {
|
||||
warn(
|
||||
"immersionTracking.retention.telemetryDays",
|
||||
src.immersionTracking.retention.telemetryDays,
|
||||
resolved.immersionTracking.retention.telemetryDays,
|
||||
"Expected integer between 1 and 3650.",
|
||||
);
|
||||
}
|
||||
|
||||
const dailyRollupsDays = asNumber(
|
||||
src.immersionTracking.retention.dailyRollupsDays,
|
||||
);
|
||||
if (
|
||||
dailyRollupsDays !== undefined &&
|
||||
dailyRollupsDays >= 1 &&
|
||||
dailyRollupsDays <= 36500
|
||||
) {
|
||||
resolved.immersionTracking.retention.dailyRollupsDays = Math.floor(
|
||||
dailyRollupsDays,
|
||||
);
|
||||
} else if (
|
||||
src.immersionTracking.retention.dailyRollupsDays !== undefined
|
||||
) {
|
||||
warn(
|
||||
"immersionTracking.retention.dailyRollupsDays",
|
||||
src.immersionTracking.retention.dailyRollupsDays,
|
||||
resolved.immersionTracking.retention.dailyRollupsDays,
|
||||
"Expected integer between 1 and 36500.",
|
||||
);
|
||||
}
|
||||
|
||||
const monthlyRollupsDays = asNumber(
|
||||
src.immersionTracking.retention.monthlyRollupsDays,
|
||||
);
|
||||
if (
|
||||
monthlyRollupsDays !== undefined &&
|
||||
monthlyRollupsDays >= 1 &&
|
||||
monthlyRollupsDays <= 36500
|
||||
) {
|
||||
resolved.immersionTracking.retention.monthlyRollupsDays = Math.floor(
|
||||
monthlyRollupsDays,
|
||||
);
|
||||
} else if (
|
||||
src.immersionTracking.retention.monthlyRollupsDays !== undefined
|
||||
) {
|
||||
warn(
|
||||
"immersionTracking.retention.monthlyRollupsDays",
|
||||
src.immersionTracking.retention.monthlyRollupsDays,
|
||||
resolved.immersionTracking.retention.monthlyRollupsDays,
|
||||
"Expected integer between 1 and 36500.",
|
||||
);
|
||||
}
|
||||
|
||||
const vacuumIntervalDays = asNumber(
|
||||
src.immersionTracking.retention.vacuumIntervalDays,
|
||||
);
|
||||
if (
|
||||
vacuumIntervalDays !== undefined &&
|
||||
vacuumIntervalDays >= 1 &&
|
||||
vacuumIntervalDays <= 3650
|
||||
) {
|
||||
resolved.immersionTracking.retention.vacuumIntervalDays = Math.floor(
|
||||
vacuumIntervalDays,
|
||||
);
|
||||
} else if (
|
||||
src.immersionTracking.retention.vacuumIntervalDays !== undefined
|
||||
) {
|
||||
warn(
|
||||
"immersionTracking.retention.vacuumIntervalDays",
|
||||
src.immersionTracking.retention.vacuumIntervalDays,
|
||||
resolved.immersionTracking.retention.vacuumIntervalDays,
|
||||
"Expected integer between 1 and 3650.",
|
||||
);
|
||||
}
|
||||
} else if (src.immersionTracking.retention !== undefined) {
|
||||
warn(
|
||||
"immersionTracking.retention",
|
||||
src.immersionTracking.retention,
|
||||
resolved.immersionTracking.retention,
|
||||
"Expected object.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isObject(src.subtitleStyle)) {
|
||||
@@ -524,10 +793,14 @@ export class ConfigService {
|
||||
},
|
||||
};
|
||||
|
||||
const enableJlpt = asBoolean((src.subtitleStyle as { enableJlpt?: unknown }).enableJlpt);
|
||||
const enableJlpt = asBoolean(
|
||||
(src.subtitleStyle as { enableJlpt?: unknown }).enableJlpt,
|
||||
);
|
||||
if (enableJlpt !== undefined) {
|
||||
resolved.subtitleStyle.enableJlpt = enableJlpt;
|
||||
} else if ((src.subtitleStyle as { enableJlpt?: unknown }).enableJlpt !== undefined) {
|
||||
} else if (
|
||||
(src.subtitleStyle as { enableJlpt?: unknown }).enableJlpt !== undefined
|
||||
) {
|
||||
warn(
|
||||
"subtitleStyle.enableJlpt",
|
||||
(src.subtitleStyle as { enableJlpt?: unknown }).enableJlpt,
|
||||
@@ -565,7 +838,8 @@ export class ConfigService {
|
||||
if (sourcePath !== undefined) {
|
||||
resolved.subtitleStyle.frequencyDictionary.sourcePath = sourcePath;
|
||||
} else if (
|
||||
(frequencyDictionary as { sourcePath?: unknown }).sourcePath !== undefined
|
||||
(frequencyDictionary as { sourcePath?: unknown }).sourcePath !==
|
||||
undefined
|
||||
) {
|
||||
warn(
|
||||
"subtitleStyle.frequencyDictionary.sourcePath",
|
||||
@@ -576,13 +850,11 @@ export class ConfigService {
|
||||
}
|
||||
|
||||
const topX = asNumber((frequencyDictionary as { topX?: unknown }).topX);
|
||||
if (
|
||||
topX !== undefined &&
|
||||
Number.isInteger(topX) &&
|
||||
topX > 0
|
||||
) {
|
||||
if (topX !== undefined && Number.isInteger(topX) && topX > 0) {
|
||||
resolved.subtitleStyle.frequencyDictionary.topX = Math.floor(topX);
|
||||
} else if ((frequencyDictionary as { topX?: unknown }).topX !== undefined) {
|
||||
} else if (
|
||||
(frequencyDictionary as { topX?: unknown }).topX !== undefined
|
||||
) {
|
||||
warn(
|
||||
"subtitleStyle.frequencyDictionary.topX",
|
||||
(frequencyDictionary as { topX?: unknown }).topX,
|
||||
@@ -592,10 +864,7 @@ export class ConfigService {
|
||||
}
|
||||
|
||||
const frequencyMode = frequencyDictionary.mode;
|
||||
if (
|
||||
frequencyMode === "single" ||
|
||||
frequencyMode === "banded"
|
||||
) {
|
||||
if (frequencyMode === "single" || frequencyMode === "banded") {
|
||||
resolved.subtitleStyle.frequencyDictionary.mode = frequencyMode;
|
||||
} else if (frequencyMode !== undefined) {
|
||||
warn(
|
||||
@@ -612,7 +881,8 @@ export class ConfigService {
|
||||
if (singleColor !== undefined) {
|
||||
resolved.subtitleStyle.frequencyDictionary.singleColor = singleColor;
|
||||
} else if (
|
||||
(frequencyDictionary as { singleColor?: unknown }).singleColor !== undefined
|
||||
(frequencyDictionary as { singleColor?: unknown }).singleColor !==
|
||||
undefined
|
||||
) {
|
||||
warn(
|
||||
"subtitleStyle.frequencyDictionary.singleColor",
|
||||
@@ -628,7 +898,8 @@ export class ConfigService {
|
||||
if (bandedColors !== undefined) {
|
||||
resolved.subtitleStyle.frequencyDictionary.bandedColors = bandedColors;
|
||||
} else if (
|
||||
(frequencyDictionary as { bandedColors?: unknown }).bandedColors !== undefined
|
||||
(frequencyDictionary as { bandedColors?: unknown }).bandedColors !==
|
||||
undefined
|
||||
) {
|
||||
warn(
|
||||
"subtitleStyle.frequencyDictionary.bandedColors",
|
||||
@@ -649,13 +920,17 @@ export class ConfigService {
|
||||
: isObject(ac.openRouter)
|
||||
? ac.openRouter
|
||||
: {};
|
||||
const { nPlusOne: _nPlusOneConfigFromAnkiConnect, ...ankiConnectWithoutNPlusOne } =
|
||||
ac as Record<string, unknown>;
|
||||
const {
|
||||
nPlusOne: _nPlusOneConfigFromAnkiConnect,
|
||||
...ankiConnectWithoutNPlusOne
|
||||
} = ac as Record<string, unknown>;
|
||||
|
||||
resolved.ankiConnect = {
|
||||
...resolved.ankiConnect,
|
||||
...(isObject(ankiConnectWithoutNPlusOne)
|
||||
? (ankiConnectWithoutNPlusOne as Partial<ResolvedConfig["ankiConnect"]>)
|
||||
? (ankiConnectWithoutNPlusOne as Partial<
|
||||
ResolvedConfig["ankiConnect"]
|
||||
>)
|
||||
: {}),
|
||||
fields: {
|
||||
...resolved.ankiConnect.fields,
|
||||
@@ -837,8 +1112,7 @@ export class ConfigService {
|
||||
nPlusOneRefreshMinutes > 0;
|
||||
if (nPlusOneRefreshMinutes !== undefined) {
|
||||
if (hasValidNPlusOneRefreshMinutes) {
|
||||
resolved.ankiConnect.nPlusOne.refreshMinutes =
|
||||
nPlusOneRefreshMinutes;
|
||||
resolved.ankiConnect.nPlusOne.refreshMinutes = nPlusOneRefreshMinutes;
|
||||
} else {
|
||||
warn(
|
||||
"ankiConnect.nPlusOne.refreshMinutes",
|
||||
@@ -927,8 +1201,7 @@ export class ConfigService {
|
||||
DEFAULT_CONFIG.ankiConnect.nPlusOne.matchMode;
|
||||
} else if (legacyNPlusOneMatchMode !== undefined) {
|
||||
if (hasValidLegacyMatchMode) {
|
||||
resolved.ankiConnect.nPlusOne.matchMode =
|
||||
legacyNPlusOneMatchMode;
|
||||
resolved.ankiConnect.nPlusOne.matchMode = legacyNPlusOneMatchMode;
|
||||
warn(
|
||||
"ankiConnect.behavior.nPlusOneMatchMode",
|
||||
behavior.nPlusOneMatchMode,
|
||||
@@ -958,9 +1231,7 @@ export class ConfigService {
|
||||
.filter((entry) => entry.length > 0);
|
||||
|
||||
if (normalizedDecks.length === nPlusOneDecks.length) {
|
||||
resolved.ankiConnect.nPlusOne.decks = [
|
||||
...new Set(normalizedDecks),
|
||||
];
|
||||
resolved.ankiConnect.nPlusOne.decks = [...new Set(normalizedDecks)];
|
||||
} else if (nPlusOneDecks.length > 0) {
|
||||
warn(
|
||||
"ankiConnect.nPlusOne.decks",
|
||||
|
||||
@@ -11,11 +11,14 @@ function renderValue(value: unknown, indent = 0): string {
|
||||
|
||||
if (value === null) return "null";
|
||||
if (typeof value === "string") return JSON.stringify(value);
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "number" || typeof value === "boolean")
|
||||
return String(value);
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) return "[]";
|
||||
const items = value.map((item) => `${nextPad}${renderValue(item, indent + 2)}`);
|
||||
const items = value.map(
|
||||
(item) => `${nextPad}${renderValue(item, indent + 2)}`,
|
||||
);
|
||||
return `\n${items.join(",\n")}\n${pad}`.replace(/^/, "[").concat("]");
|
||||
}
|
||||
|
||||
@@ -25,7 +28,8 @@ function renderValue(value: unknown, indent = 0): string {
|
||||
);
|
||||
if (entries.length === 0) return "{}";
|
||||
const lines = entries.map(
|
||||
([key, child]) => `${nextPad}${JSON.stringify(key)}: ${renderValue(child, indent + 2)}`,
|
||||
([key, child]) =>
|
||||
`${nextPad}${JSON.stringify(key)}: ${renderValue(child, indent + 2)}`,
|
||||
);
|
||||
return `\n${lines.join(",\n")}\n${pad}`.replace(/^/, "{").concat("}");
|
||||
}
|
||||
@@ -45,23 +49,33 @@ function renderSection(
|
||||
lines.push(` // ${comment}`);
|
||||
}
|
||||
lines.push(" // ==========================================");
|
||||
lines.push(` ${JSON.stringify(key)}: ${renderValue(value, 2)}${isLast ? "" : ","}`);
|
||||
lines.push(
|
||||
` ${JSON.stringify(key)}: ${renderValue(value, 2)}${isLast ? "" : ","}`,
|
||||
);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function generateConfigTemplate(config: ResolvedConfig = deepCloneConfig(DEFAULT_CONFIG)): string {
|
||||
export function generateConfigTemplate(
|
||||
config: ResolvedConfig = deepCloneConfig(DEFAULT_CONFIG),
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
lines.push("/**");
|
||||
lines.push(" * SubMiner Example Configuration File");
|
||||
lines.push(" *");
|
||||
lines.push(" * This file is auto-generated from src/config/definitions.ts.");
|
||||
lines.push(" * Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.");
|
||||
lines.push(
|
||||
" * Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.",
|
||||
);
|
||||
lines.push(" */");
|
||||
lines.push("{");
|
||||
|
||||
CONFIG_TEMPLATE_SECTIONS.forEach((section, index) => {
|
||||
lines.push("");
|
||||
const comments = [section.title, ...section.description, ...(section.notes ?? [])];
|
||||
const comments = [
|
||||
section.title,
|
||||
...section.description,
|
||||
...(section.notes ?? []),
|
||||
];
|
||||
lines.push(
|
||||
renderSection(
|
||||
section.key,
|
||||
|
||||
Reference in New Issue
Block a user