0.12.0-1 Plugins und diverses

This commit is contained in:
2026-03-19 18:57:05 +00:00
parent 0a9cf6969f
commit 24955a956d
33 changed files with 3978 additions and 479 deletions
+221 -19
View File
@@ -530,6 +530,7 @@ async function openAndPrepareDatabase() {
await removeDeprecatedSettings(dbInstance);
await migrateSettingsSchemaMetadata(dbInstance);
await ensurePipelineStateRow(dbInstance);
await backfillJobOutputFolders(dbInstance);
const syncedLogRoot = await configureRuntimeLogRootFromSettings(dbInstance, { ensure: true });
logger.info('log-root:synced', {
configured: syncedLogRoot.configured || null,
@@ -974,51 +975,214 @@ async function migrateSettingsSchemaMetadata(db) {
);
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('auto_eject_after_rip', 'false')`);
// Plugin-Architektur Toggle (Phase 2)
await db.run(
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
VALUES ('coverart_recovery_enabled', 'Metadaten', 'Coverart-Nachladen aktiv', 'boolean', 1, 'Wenn aktiv, werden fehlende Coverarts automatisch in Intervallen geprüft und nachgeladen.', 'true', '[]', '{}', 430)`
);
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('coverart_recovery_enabled', 'true')`);
await db.run(
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
VALUES ('coverart_recovery_interval_hours', 'Metadaten', 'Coverart-Prüfintervall (Stunden)', 'number', 1, 'Intervall für automatische Prüfung fehlender Coverarts in Stunden.', '6', '[]', '{"min":1,"max":168}', 431)`
);
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('coverart_recovery_interval_hours', '6')`);
// depends_on-Spalte zu settings_schema hinzufügen (falls noch nicht vorhanden)
{
const cols = await db.all(`PRAGMA table_info(settings_schema)`);
if (!cols.some((c) => c.name === 'depends_on')) {
await db.run(`ALTER TABLE settings_schema ADD COLUMN depends_on TEXT DEFAULT NULL`);
logger.info('migrate:settings-schema-add-depends_on');
}
}
// Plugin-Architektur Toggle (Phase 2) — hierarchisch mit depends_on
// Struktur: global → medium (depends_on: global) → phase (depends_on: medium)
//
// Implementierungsstand:
// ✅ = plugin-pfad aktiv ⏳ = noch nicht implementiert (readonly, immer aus)
//
// BluRay / DVD: Analyse ✅ Rip ✅ Review ⏳ Encode ⏳
// CD: Analyse ✅ Rip ⏳
// Audiobook: Analyse ✅ Encode ⏳
const pluginArchitectureSettings = [
// ── Global ──────────────────────────────────────────────────────────────
{
key: 'use_plugin_architecture',
label: 'Neue Plugin-Architektur (Beta)',
description: 'Aktiviert die neue modulare Plugin-Architektur (Phase 2). Legacy-Code bleibt aktiv solange dieser Toggle deaktiviert ist. Nur für Testzwecke aktivieren.',
description: 'Masterschalter: Aktiviert die neue modulare Plugin-Architektur (Phase 2). Alle Medium-Schalter darunter sind nur wirksam wenn dieser aktiv ist.',
defaultValue: 'false',
orderIndex: 9999
orderIndex: 9999,
dependsOn: null,
validationJson: '{}'
},
// ── Blu-ray ──────────────────────────────────────────────────────────────
{
key: 'use_plugin_architecture_bluray',
label: 'Beta: Blu-ray Plugin aktiv',
description: 'Aktiviert das Blu-ray Plugin der neuen Architektur. Wirksam nur wenn der globale Beta-Schalter aktiviert ist. Deaktiviert = Legacy-Pfad.',
label: 'Blu-ray Plugin',
description: 'Aktiviert das Blu-ray Plugin. Deaktiviert = Legacy-Pfad für alle Blu-ray-Phasen.',
defaultValue: 'true',
orderIndex: 10000
orderIndex: 10000,
dependsOn: 'use_plugin_architecture',
validationJson: '{}'
},
{
key: 'use_plugin_architecture_bluray_analyze',
label: 'Analyse (Blu-ray)',
description: 'Disc-Erkennung und OMDB-Suche laufen über das Plugin.',
defaultValue: 'true',
orderIndex: 10001,
dependsOn: 'use_plugin_architecture_bluray',
validationJson: '{}'
},
{
key: 'use_plugin_architecture_bluray_rip',
label: 'Rip (Blu-ray)',
description: 'MakeMKV-Rip läuft über das Plugin.',
defaultValue: 'true',
orderIndex: 10002,
dependsOn: 'use_plugin_architecture_bluray',
validationJson: '{}'
},
{
key: 'use_plugin_architecture_bluray_review',
label: 'Review / Mediainfo-Prüfung (Blu-ray)',
description: 'Noch nicht implementiert — kommt in einem späteren Sprint.',
defaultValue: 'false',
orderIndex: 10003,
dependsOn: 'use_plugin_architecture_bluray',
validationJson: '{"readonly":true}'
},
{
key: 'use_plugin_architecture_bluray_encode',
label: 'Encoding (Blu-ray)',
description: 'Noch nicht implementiert — kommt in einem späteren Sprint.',
defaultValue: 'false',
orderIndex: 10004,
dependsOn: 'use_plugin_architecture_bluray',
validationJson: '{"readonly":true}'
},
// ── DVD ──────────────────────────────────────────────────────────────────
{
key: 'use_plugin_architecture_dvd',
label: 'Beta: DVD Plugin aktiv',
description: 'Aktiviert das DVD Plugin der neuen Architektur. Wirksam nur wenn der globale Beta-Schalter aktiviert ist. Deaktiviert = Legacy-Pfad.',
label: 'DVD Plugin',
description: 'Aktiviert das DVD Plugin. Deaktiviert = Legacy-Pfad für alle DVD-Phasen.',
defaultValue: 'true',
orderIndex: 10001
orderIndex: 10010,
dependsOn: 'use_plugin_architecture',
validationJson: '{}'
},
{
key: 'use_plugin_architecture_dvd_analyze',
label: 'Analyse (DVD)',
description: 'Disc-Erkennung und OMDB-Suche laufen über das Plugin.',
defaultValue: 'true',
orderIndex: 10011,
dependsOn: 'use_plugin_architecture_dvd',
validationJson: '{}'
},
{
key: 'use_plugin_architecture_dvd_rip',
label: 'Rip (DVD)',
description: 'MakeMKV-Rip läuft über das Plugin.',
defaultValue: 'true',
orderIndex: 10012,
dependsOn: 'use_plugin_architecture_dvd',
validationJson: '{}'
},
{
key: 'use_plugin_architecture_dvd_review',
label: 'Review / Mediainfo-Prüfung (DVD)',
description: 'Noch nicht implementiert — kommt in einem späteren Sprint.',
defaultValue: 'false',
orderIndex: 10013,
dependsOn: 'use_plugin_architecture_dvd',
validationJson: '{"readonly":true}'
},
{
key: 'use_plugin_architecture_dvd_encode',
label: 'Encoding (DVD)',
description: 'Noch nicht implementiert — kommt in einem späteren Sprint.',
defaultValue: 'false',
orderIndex: 10014,
dependsOn: 'use_plugin_architecture_dvd',
validationJson: '{"readonly":true}'
},
// ── Audio-CD ─────────────────────────────────────────────────────────────
{
key: 'use_plugin_architecture_cd',
label: 'Beta: Audio-CD Plugin aktiv',
description: 'Aktiviert das Audio-CD Plugin der neuen Architektur. Wirksam nur wenn der globale Beta-Schalter aktiviert ist. Deaktiviert = Legacy-Pfad.',
label: 'Audio-CD Plugin',
description: 'Aktiviert das Audio-CD Plugin. Deaktiviert = Legacy-Pfad für alle CD-Phasen.',
defaultValue: 'true',
orderIndex: 10002
orderIndex: 10020,
dependsOn: 'use_plugin_architecture',
validationJson: '{}'
},
{
key: 'use_plugin_architecture_audiobook',
label: 'Beta: Audiobook Plugin aktiv',
description: 'Aktiviert das Audiobook Plugin der neuen Architektur. Wirksam nur wenn der globale Beta-Schalter aktiviert ist. Deaktiviert = Legacy-Pfad.',
key: 'use_plugin_architecture_cd_analyze',
label: 'Analyse (Audio-CD)',
description: 'TOC-Auslesung läuft über das Plugin.',
defaultValue: 'true',
orderIndex: 10003
orderIndex: 10021,
dependsOn: 'use_plugin_architecture_cd',
validationJson: '{}'
},
{
key: 'use_plugin_architecture_cd_rip',
label: 'Rip + Encode (Audio-CD)',
description: 'Noch nicht implementiert — cdparanoia-Rip läuft noch über Legacy.',
defaultValue: 'false',
orderIndex: 10022,
dependsOn: 'use_plugin_architecture_cd',
validationJson: '{"readonly":true}'
},
// ── Audiobook ────────────────────────────────────────────────────────────
{
key: 'use_plugin_architecture_audiobook',
label: 'Audiobook Plugin',
description: 'Aktiviert das Audiobook Plugin. Deaktiviert = Legacy-Pfad für alle Audiobook-Phasen.',
defaultValue: 'true',
orderIndex: 10030,
dependsOn: 'use_plugin_architecture',
validationJson: '{}'
},
{
key: 'use_plugin_architecture_audiobook_analyze',
label: 'Analyse (Audiobook)',
description: 'ffprobe-Metadatenanalyse läuft über das Plugin.',
defaultValue: 'true',
orderIndex: 10031,
dependsOn: 'use_plugin_architecture_audiobook',
validationJson: '{}'
},
{
key: 'use_plugin_architecture_audiobook_encode',
label: 'Encoding (Audiobook)',
description: 'Noch nicht implementiert — Encoding läuft noch über Legacy.',
defaultValue: 'false',
orderIndex: 10032,
dependsOn: 'use_plugin_architecture_audiobook',
validationJson: '{"readonly":true}'
}
];
for (const setting of pluginArchitectureSettings) {
await db.run(
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
VALUES (?, 'Erweitert', ?, 'boolean', 1, ?, ?, '[]', '{}', ?)`,
[setting.key, setting.label, setting.description, setting.defaultValue, setting.orderIndex]
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index, depends_on)
VALUES (?, 'Erweitert', ?, 'boolean', 1, ?, ?, '[]', ?, ?, ?)`,
[setting.key, setting.label, setting.description, setting.defaultValue, setting.validationJson, setting.orderIndex, setting.dependsOn ?? null]
);
// depends_on und order_index für bereits existierende Zeilen aktualisieren
await db.run(
`UPDATE settings_schema SET depends_on = ?, order_index = ?, validation_json = ?, updated_at = CURRENT_TIMESTAMP
WHERE key = ? AND (depends_on IS NOT ? OR order_index != ? OR validation_json != ?)`,
[setting.dependsOn ?? null, setting.orderIndex, setting.validationJson, setting.key, setting.dependsOn ?? null, setting.orderIndex, setting.validationJson]
);
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES (?, ?)`, [setting.key, setting.defaultValue]);
// Readonly-Toggles immer auf 'false' zurücksetzen (nie aktivierbar)
if (setting.validationJson === '{"readonly":true}') {
await db.run(`UPDATE settings_values SET value = 'false' WHERE key = ?`, [setting.key]);
}
}
await db.run(`
@@ -1030,6 +1194,44 @@ async function migrateSettingsSchemaMetadata(db) {
`);
}
async function backfillJobOutputFolders(db) {
// Remove duplicate (job_id, output_path) rows before unique index is enforced.
await db.run(`
DELETE FROM job_output_folders
WHERE id NOT IN (
SELECT MIN(id) FROM job_output_folders GROUP BY job_id, output_path
)
`);
// Populate job_output_folders from jobs.output_path for pre-feature jobs.
// Only inserts rows where no entry exists for the job yet.
const rows = await db.all(`
SELECT j.id AS job_id, j.output_path
FROM jobs j
WHERE j.output_path IS NOT NULL
AND j.output_path != ''
AND NOT EXISTS (
SELECT 1 FROM job_output_folders f WHERE f.job_id = j.id
)
`);
if (!Array.isArray(rows) || rows.length === 0) return;
let inserted = 0;
for (const row of rows) {
try {
await db.run(
'INSERT OR IGNORE INTO job_output_folders (job_id, output_path) VALUES (?, ?)',
[row.job_id, row.output_path]
);
inserted += 1;
} catch (err) {
logger.warn('backfill:job-output-folders:insert-failed', { jobId: row.job_id, error: err?.message });
}
}
if (inserted > 0) {
logger.info('backfill:job-output-folders:done', { inserted });
}
}
async function getDb() {
return initDatabase();
}