mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-30 06:10:56 +01:00
feat: run history for rename jobs
This commit is contained in:
@@ -28,6 +28,7 @@ import { migration as migration023 } from './migrations/023_create_pattern_match
|
||||
import { migration as migration024 } from './migrations/024_create_arr_rename_settings.ts';
|
||||
import { migration as migration025 } from './migrations/025_add_rename_notification_mode.ts';
|
||||
import { migration as migration026 } from './migrations/026_create_upgrade_runs.ts';
|
||||
import { migration as migration027 } from './migrations/027_create_rename_runs.ts';
|
||||
|
||||
export interface Migration {
|
||||
version: number;
|
||||
@@ -268,7 +269,8 @@ export function loadMigrations(): Migration[] {
|
||||
migration023,
|
||||
migration024,
|
||||
migration025,
|
||||
migration026
|
||||
migration026,
|
||||
migration027
|
||||
];
|
||||
|
||||
// Sort by version number
|
||||
|
||||
91
src/lib/server/db/migrations/027_create_rename_runs.ts
Normal file
91
src/lib/server/db/migrations/027_create_rename_runs.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { Migration } from '../migrations.ts';
|
||||
|
||||
/**
|
||||
* Migration 027: Create rename_runs table
|
||||
*
|
||||
* Creates the table for storing rename run history.
|
||||
* Similar to upgrade_runs but with rename-specific fields.
|
||||
*
|
||||
* Fields:
|
||||
* - id: UUID primary key
|
||||
* - instance_id: Foreign key to arr_instances
|
||||
* - started_at, completed_at: Timestamps for the run
|
||||
* - status: success, partial, failed, skipped
|
||||
* - dry_run: Whether this was a dry run
|
||||
* - manual: Whether this was manually triggered
|
||||
*
|
||||
* Config snapshot:
|
||||
* - rename_folders, ignore_tag
|
||||
*
|
||||
* Stats (flat for easy querying):
|
||||
* - library_total, after_ignore_tag, skipped_by_tag
|
||||
* - files_needing_rename, files_renamed, folders_renamed
|
||||
* - commands_triggered, commands_completed, commands_failed
|
||||
*
|
||||
* Complex data as JSON:
|
||||
* - items: Array of renamed items with file paths
|
||||
* - errors: Array of error strings
|
||||
*/
|
||||
|
||||
export const migration: Migration = {
|
||||
version: 27,
|
||||
name: 'Create rename_runs table',
|
||||
|
||||
up: `
|
||||
CREATE TABLE rename_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
|
||||
-- Relationship
|
||||
instance_id INTEGER NOT NULL,
|
||||
|
||||
-- Timing
|
||||
started_at TEXT NOT NULL,
|
||||
completed_at TEXT NOT NULL,
|
||||
|
||||
-- Status
|
||||
status TEXT NOT NULL CHECK (status IN ('success', 'partial', 'failed', 'skipped')),
|
||||
dry_run INTEGER NOT NULL DEFAULT 1,
|
||||
manual INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Config snapshot
|
||||
rename_folders INTEGER NOT NULL DEFAULT 0,
|
||||
ignore_tag TEXT,
|
||||
|
||||
-- Library stats
|
||||
library_total INTEGER NOT NULL,
|
||||
library_fetch_ms INTEGER NOT NULL,
|
||||
|
||||
-- Filtering stats
|
||||
after_ignore_tag INTEGER NOT NULL,
|
||||
skipped_by_tag INTEGER NOT NULL,
|
||||
|
||||
-- Results stats
|
||||
files_needing_rename INTEGER NOT NULL,
|
||||
files_renamed INTEGER NOT NULL,
|
||||
folders_renamed INTEGER NOT NULL,
|
||||
commands_triggered INTEGER NOT NULL,
|
||||
commands_completed INTEGER NOT NULL,
|
||||
commands_failed INTEGER NOT NULL,
|
||||
|
||||
-- Complex data as JSON
|
||||
items TEXT NOT NULL DEFAULT '[]',
|
||||
errors TEXT NOT NULL DEFAULT '[]',
|
||||
|
||||
-- Metadata
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (instance_id) REFERENCES arr_instances(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_rename_runs_instance ON rename_runs(instance_id);
|
||||
CREATE INDEX idx_rename_runs_started_at ON rename_runs(started_at DESC);
|
||||
CREATE INDEX idx_rename_runs_status ON rename_runs(status);
|
||||
`,
|
||||
|
||||
down: `
|
||||
DROP INDEX IF EXISTS idx_rename_runs_status;
|
||||
DROP INDEX IF EXISTS idx_rename_runs_started_at;
|
||||
DROP INDEX IF EXISTS idx_rename_runs_instance;
|
||||
DROP TABLE IF EXISTS rename_runs;
|
||||
`
|
||||
};
|
||||
251
src/lib/server/db/queries/renameRuns.ts
Normal file
251
src/lib/server/db/queries/renameRuns.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { db } from '../db.ts';
|
||||
import type { RenameJobLog } from '$lib/server/rename/types.ts';
|
||||
|
||||
/**
|
||||
* Database row type for rename_runs table
|
||||
*/
|
||||
interface RenameRunRow {
|
||||
id: string;
|
||||
instance_id: number;
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
status: string;
|
||||
dry_run: number;
|
||||
manual: number;
|
||||
rename_folders: number;
|
||||
ignore_tag: string | null;
|
||||
library_total: number;
|
||||
library_fetch_ms: number;
|
||||
after_ignore_tag: number;
|
||||
skipped_by_tag: number;
|
||||
files_needing_rename: number;
|
||||
files_renamed: number;
|
||||
folders_renamed: number;
|
||||
commands_triggered: number;
|
||||
commands_completed: number;
|
||||
commands_failed: number;
|
||||
items: string;
|
||||
errors: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renamed item stored in the database
|
||||
*/
|
||||
interface RenamedItemRow {
|
||||
id: number;
|
||||
title: string;
|
||||
files: { existingPath: string; newPath: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert database row to RenameJobLog format
|
||||
*/
|
||||
function rowToLog(row: RenameRunRow): RenameJobLog {
|
||||
return {
|
||||
id: row.id,
|
||||
instanceId: row.instance_id,
|
||||
instanceName: '', // Not stored, can be joined if needed
|
||||
instanceType: 'radarr', // Not stored, default
|
||||
startedAt: row.started_at,
|
||||
completedAt: row.completed_at,
|
||||
status: row.status as 'success' | 'partial' | 'failed' | 'skipped',
|
||||
|
||||
config: {
|
||||
dryRun: row.dry_run === 1,
|
||||
renameFolders: row.rename_folders === 1,
|
||||
ignoreTag: row.ignore_tag,
|
||||
manual: row.manual === 1
|
||||
},
|
||||
|
||||
library: {
|
||||
totalItems: row.library_total,
|
||||
fetchDurationMs: row.library_fetch_ms
|
||||
},
|
||||
|
||||
filtering: {
|
||||
afterIgnoreTag: row.after_ignore_tag,
|
||||
skippedByTag: row.skipped_by_tag
|
||||
},
|
||||
|
||||
results: {
|
||||
filesNeedingRename: row.files_needing_rename,
|
||||
filesRenamed: row.files_renamed,
|
||||
foldersRenamed: row.folders_renamed,
|
||||
commandsTriggered: row.commands_triggered,
|
||||
commandsCompleted: row.commands_completed,
|
||||
commandsFailed: row.commands_failed,
|
||||
errors: JSON.parse(row.errors) as string[]
|
||||
},
|
||||
|
||||
renamedItems: JSON.parse(row.items) as RenamedItemRow[]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* All queries for rename_runs table
|
||||
*/
|
||||
export const renameRunsQueries = {
|
||||
/**
|
||||
* Insert a new rename run
|
||||
*/
|
||||
insert(log: RenameJobLog): void {
|
||||
db.execute(
|
||||
`INSERT INTO rename_runs (
|
||||
id, instance_id, started_at, completed_at, status, dry_run, manual,
|
||||
rename_folders, ignore_tag,
|
||||
library_total, library_fetch_ms,
|
||||
after_ignore_tag, skipped_by_tag,
|
||||
files_needing_rename, files_renamed, folders_renamed,
|
||||
commands_triggered, commands_completed, commands_failed,
|
||||
items, errors
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
log.id,
|
||||
log.instanceId,
|
||||
log.startedAt,
|
||||
log.completedAt,
|
||||
log.status,
|
||||
log.config.dryRun ? 1 : 0,
|
||||
log.config.manual ? 1 : 0,
|
||||
log.config.renameFolders ? 1 : 0,
|
||||
log.config.ignoreTag,
|
||||
log.library.totalItems,
|
||||
log.library.fetchDurationMs,
|
||||
log.filtering.afterIgnoreTag,
|
||||
log.filtering.skippedByTag,
|
||||
log.results.filesNeedingRename,
|
||||
log.results.filesRenamed,
|
||||
log.results.foldersRenamed,
|
||||
log.results.commandsTriggered,
|
||||
log.results.commandsCompleted,
|
||||
log.results.commandsFailed,
|
||||
JSON.stringify(log.renamedItems),
|
||||
JSON.stringify(log.results.errors)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all rename runs for an instance, newest first
|
||||
*/
|
||||
getByInstanceId(instanceId: number, limit = 100): RenameJobLog[] {
|
||||
const rows = db.query<RenameRunRow>(
|
||||
`SELECT * FROM rename_runs
|
||||
WHERE instance_id = ?
|
||||
ORDER BY started_at DESC
|
||||
LIMIT ?`,
|
||||
instanceId,
|
||||
limit
|
||||
);
|
||||
return rows.map(rowToLog);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single rename run by ID
|
||||
*/
|
||||
getById(id: string): RenameJobLog | undefined {
|
||||
const row = db.queryFirst<RenameRunRow>(
|
||||
'SELECT * FROM rename_runs WHERE id = ?',
|
||||
id
|
||||
);
|
||||
return row ? rowToLog(row) : undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get recent runs across all instances
|
||||
*/
|
||||
getRecent(limit = 50): RenameJobLog[] {
|
||||
const rows = db.query<RenameRunRow>(
|
||||
`SELECT * FROM rename_runs
|
||||
ORDER BY started_at DESC
|
||||
LIMIT ?`,
|
||||
limit
|
||||
);
|
||||
return rows.map(rowToLog);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get runs by status for an instance
|
||||
*/
|
||||
getByStatus(instanceId: number, status: string, limit = 50): RenameJobLog[] {
|
||||
const rows = db.query<RenameRunRow>(
|
||||
`SELECT * FROM rename_runs
|
||||
WHERE instance_id = ? AND status = ?
|
||||
ORDER BY started_at DESC
|
||||
LIMIT ?`,
|
||||
instanceId,
|
||||
status,
|
||||
limit
|
||||
);
|
||||
return rows.map(rowToLog);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete old runs (for retention policy)
|
||||
* Returns number of rows deleted
|
||||
*/
|
||||
deleteOlderThan(days: number): number {
|
||||
return db.execute(
|
||||
`DELETE FROM rename_runs
|
||||
WHERE datetime(started_at) < datetime('now', '-' || ? || ' days')`,
|
||||
days
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete all runs for an instance
|
||||
*/
|
||||
deleteByInstanceId(instanceId: number): number {
|
||||
return db.execute(
|
||||
'DELETE FROM rename_runs WHERE instance_id = ?',
|
||||
instanceId
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get count of runs for an instance
|
||||
*/
|
||||
getCount(instanceId: number): number {
|
||||
const result = db.queryFirst<{ count: number }>(
|
||||
'SELECT COUNT(*) as count FROM rename_runs WHERE instance_id = ?',
|
||||
instanceId
|
||||
);
|
||||
return result?.count ?? 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get stats summary for an instance
|
||||
*/
|
||||
getStats(instanceId: number): {
|
||||
totalRuns: number;
|
||||
successfulRuns: number;
|
||||
failedRuns: number;
|
||||
totalFilesRenamed: number;
|
||||
totalFoldersRenamed: number;
|
||||
} {
|
||||
const result = db.queryFirst<{
|
||||
total_runs: number;
|
||||
successful_runs: number;
|
||||
failed_runs: number;
|
||||
total_files_renamed: number;
|
||||
total_folders_renamed: number;
|
||||
}>(
|
||||
`SELECT
|
||||
COUNT(*) as total_runs,
|
||||
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successful_runs,
|
||||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_runs,
|
||||
SUM(files_renamed) as total_files_renamed,
|
||||
SUM(folders_renamed) as total_folders_renamed
|
||||
FROM rename_runs
|
||||
WHERE instance_id = ?`,
|
||||
instanceId
|
||||
);
|
||||
|
||||
return {
|
||||
totalRuns: result?.total_runs ?? 0,
|
||||
successfulRuns: result?.successful_runs ?? 0,
|
||||
failedRuns: result?.failed_runs ?? 0,
|
||||
totalFilesRenamed: result?.total_files_renamed ?? 0,
|
||||
totalFoldersRenamed: result?.total_folders_renamed ?? 0
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -555,3 +555,59 @@ CREATE TABLE upgrade_runs (
|
||||
CREATE INDEX idx_upgrade_runs_instance ON upgrade_runs(instance_id);
|
||||
CREATE INDEX idx_upgrade_runs_started_at ON upgrade_runs(started_at DESC);
|
||||
CREATE INDEX idx_upgrade_runs_status ON upgrade_runs(status);
|
||||
|
||||
-- ==============================================================================
|
||||
-- TABLE: rename_runs
|
||||
-- Purpose: Store rename run history for each arr instance
|
||||
-- Migration: 027_create_rename_runs.ts
|
||||
-- ==============================================================================
|
||||
|
||||
CREATE TABLE rename_runs (
|
||||
id TEXT PRIMARY KEY, -- UUID
|
||||
|
||||
-- Relationship
|
||||
instance_id INTEGER NOT NULL, -- Foreign key to arr_instances
|
||||
|
||||
-- Timing
|
||||
started_at TEXT NOT NULL, -- ISO timestamp when run started
|
||||
completed_at TEXT NOT NULL, -- ISO timestamp when run completed
|
||||
|
||||
-- Status
|
||||
status TEXT NOT NULL CHECK (status IN ('success', 'partial', 'failed', 'skipped')),
|
||||
dry_run INTEGER NOT NULL DEFAULT 1, -- 1=dry run, 0=live
|
||||
manual INTEGER NOT NULL DEFAULT 0, -- 1=manually triggered, 0=scheduled
|
||||
|
||||
-- Config snapshot
|
||||
rename_folders INTEGER NOT NULL DEFAULT 0, -- 1=rename folders too
|
||||
ignore_tag TEXT, -- Tag name to skip
|
||||
|
||||
-- Library stats
|
||||
library_total INTEGER NOT NULL, -- Total items in library
|
||||
library_fetch_ms INTEGER NOT NULL, -- Time to fetch library in ms
|
||||
|
||||
-- Filtering stats
|
||||
after_ignore_tag INTEGER NOT NULL, -- Items after ignore tag filter
|
||||
skipped_by_tag INTEGER NOT NULL, -- Items skipped due to tag
|
||||
|
||||
-- Results stats
|
||||
files_needing_rename INTEGER NOT NULL, -- Files that need renaming
|
||||
files_renamed INTEGER NOT NULL, -- Files actually renamed
|
||||
folders_renamed INTEGER NOT NULL, -- Folders renamed
|
||||
commands_triggered INTEGER NOT NULL, -- Rename commands triggered
|
||||
commands_completed INTEGER NOT NULL, -- Commands completed successfully
|
||||
commands_failed INTEGER NOT NULL, -- Commands that failed
|
||||
|
||||
-- Complex data as JSON
|
||||
items TEXT NOT NULL DEFAULT '[]', -- JSON array of renamed items
|
||||
errors TEXT NOT NULL DEFAULT '[]', -- JSON array of error strings
|
||||
|
||||
-- Metadata
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (instance_id) REFERENCES arr_instances(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Rename runs indexes (Migration: 027_create_rename_runs.ts)
|
||||
CREATE INDEX idx_rename_runs_instance ON rename_runs(instance_id);
|
||||
CREATE INDEX idx_rename_runs_started_at ON rename_runs(started_at DESC);
|
||||
CREATE INDEX idx_rename_runs_status ON rename_runs(status);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
/**
|
||||
* Structured logging for rename jobs
|
||||
* Uses the shared logger with source 'RenameJob'
|
||||
* Stores run history in the database
|
||||
*/
|
||||
|
||||
import { logger } from '$logger/logger.ts';
|
||||
import { renameRunsQueries } from '$db/queries/renameRuns.ts';
|
||||
import type { RenameJobLog } from './types.ts';
|
||||
|
||||
const SOURCE = 'RenameJob';
|
||||
@@ -41,6 +43,16 @@ export async function logRenameRun(log: RenameJobLog): Promise<void> {
|
||||
items
|
||||
}
|
||||
});
|
||||
|
||||
// Save full structured data to database
|
||||
try {
|
||||
renameRunsQueries.insert(log);
|
||||
} catch (err) {
|
||||
await logger.error(`Failed to save rename run to database: ${err}`, {
|
||||
source: SOURCE,
|
||||
meta: { runId: log.id, error: err }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user