0.12.0-1 Plugins und diverses
This commit is contained in:
+221
-19
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user