Skip to content

Commit f7a430a

Browse files
feat: add scheduled arr cleanup with per-instance settings
1 parent 20e2789 commit f7a430a

File tree

16 files changed

+575
-65
lines changed

16 files changed

+575
-65
lines changed

src/lib/server/db/migrations.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { migration as migration048 } from './migrations/048_fix_sync_database_fo
5353
import { migration as migration049 } from './migrations/049_create_job_queue.ts';
5454
import { migration as migration050 } from './migrations/050_remove_dry_run_columns.ts';
5555
import { migration as migration051 } from './migrations/051_cron_scheduling.ts';
56+
import { migration as migration052 } from './migrations/052_create_arr_cleanup_settings.ts';
5657

5758
export interface Migration {
5859
version: number;
@@ -324,7 +325,8 @@ export function loadMigrations(): Migration[] {
324325
migration048,
325326
migration049,
326327
migration050,
327-
migration051
328+
migration051,
329+
migration052
328330
];
329331

330332
// Sort by version number
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { Migration } from '../migrations.ts';
2+
3+
/**
4+
* Migration 052: Create arr_cleanup_settings table
5+
*
6+
* Stores per-instance cleanup scheduling config.
7+
* Cleanup removes stale synced configs and media flagged as removed from TMDB/TVDB.
8+
*/
9+
10+
export const migration: Migration = {
11+
version: 52,
12+
name: 'Create arr_cleanup_settings table',
13+
14+
up: `
15+
CREATE TABLE arr_cleanup_settings (
16+
id INTEGER PRIMARY KEY AUTOINCREMENT,
17+
arr_instance_id INTEGER NOT NULL UNIQUE,
18+
enabled INTEGER NOT NULL DEFAULT 0,
19+
cron TEXT NOT NULL DEFAULT '0 0 * * 0',
20+
next_run_at TEXT,
21+
last_run_at TEXT,
22+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
23+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
24+
FOREIGN KEY (arr_instance_id) REFERENCES arr_instances(id) ON DELETE CASCADE
25+
);
26+
27+
CREATE INDEX idx_arr_cleanup_settings_instance ON arr_cleanup_settings(arr_instance_id);
28+
`,
29+
30+
down: `
31+
DROP INDEX IF EXISTS idx_arr_cleanup_settings_instance;
32+
DROP TABLE IF EXISTS arr_cleanup_settings;
33+
`
34+
};
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { db } from '../db.ts';
2+
3+
interface CleanupSettingsRow {
4+
id: number;
5+
arr_instance_id: number;
6+
enabled: number;
7+
cron: string;
8+
next_run_at: string | null;
9+
last_run_at: string | null;
10+
created_at: string;
11+
updated_at: string;
12+
}
13+
14+
export interface CleanupSettings {
15+
id: number;
16+
arrInstanceId: number;
17+
enabled: boolean;
18+
cron: string;
19+
nextRunAt: string | null;
20+
lastRunAt: string | null;
21+
createdAt: string;
22+
updatedAt: string;
23+
}
24+
25+
export interface CleanupSettingsInput {
26+
enabled?: boolean;
27+
cron?: string;
28+
}
29+
30+
function rowToSettings(row: CleanupSettingsRow): CleanupSettings {
31+
return {
32+
id: row.id,
33+
arrInstanceId: row.arr_instance_id,
34+
enabled: row.enabled === 1,
35+
cron: row.cron,
36+
nextRunAt: row.next_run_at,
37+
lastRunAt: row.last_run_at,
38+
createdAt: row.created_at,
39+
updatedAt: row.updated_at
40+
};
41+
}
42+
43+
export const arrCleanupSettingsQueries = {
44+
getByInstanceId(arrInstanceId: number): CleanupSettings | undefined {
45+
const row = db.queryFirst<CleanupSettingsRow>(
46+
'SELECT * FROM arr_cleanup_settings WHERE arr_instance_id = ?',
47+
arrInstanceId
48+
);
49+
return row ? rowToSettings(row) : undefined;
50+
},
51+
52+
getAll(): CleanupSettings[] {
53+
const rows = db.query<CleanupSettingsRow>('SELECT * FROM arr_cleanup_settings');
54+
return rows.map(rowToSettings);
55+
},
56+
57+
getEnabled(): CleanupSettings[] {
58+
const rows = db.query<CleanupSettingsRow>(
59+
'SELECT * FROM arr_cleanup_settings WHERE enabled = 1'
60+
);
61+
return rows.map(rowToSettings);
62+
},
63+
64+
upsert(arrInstanceId: number, input: CleanupSettingsInput): CleanupSettings {
65+
const existing = this.getByInstanceId(arrInstanceId);
66+
67+
if (existing) {
68+
this.update(arrInstanceId, input);
69+
return this.getByInstanceId(arrInstanceId)!;
70+
}
71+
72+
const enabled = input.enabled !== undefined ? (input.enabled ? 1 : 0) : 0;
73+
const cron = input.cron ?? '0 0 * * 0';
74+
75+
db.execute(
76+
`INSERT INTO arr_cleanup_settings (arr_instance_id, enabled, cron) VALUES (?, ?, ?)`,
77+
arrInstanceId,
78+
enabled,
79+
cron
80+
);
81+
82+
return this.getByInstanceId(arrInstanceId)!;
83+
},
84+
85+
update(arrInstanceId: number, input: CleanupSettingsInput): boolean {
86+
const updates: string[] = [];
87+
const params: (string | number | null)[] = [];
88+
89+
if (input.enabled !== undefined) {
90+
updates.push('enabled = ?');
91+
params.push(input.enabled ? 1 : 0);
92+
}
93+
if (input.cron !== undefined) {
94+
updates.push('cron = ?');
95+
params.push(input.cron);
96+
}
97+
98+
if (updates.length === 0) {
99+
return false;
100+
}
101+
102+
updates.push('updated_at = CURRENT_TIMESTAMP');
103+
params.push(arrInstanceId);
104+
105+
const affected = db.execute(
106+
`UPDATE arr_cleanup_settings SET ${updates.join(', ')} WHERE arr_instance_id = ?`,
107+
...params
108+
);
109+
110+
return affected > 0;
111+
},
112+
113+
delete(arrInstanceId: number): boolean {
114+
const affected = db.execute(
115+
'DELETE FROM arr_cleanup_settings WHERE arr_instance_id = ?',
116+
arrInstanceId
117+
);
118+
return affected > 0;
119+
},
120+
121+
updateLastRun(arrInstanceId: number): void {
122+
db.execute(
123+
'UPDATE arr_cleanup_settings SET last_run_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE arr_instance_id = ?',
124+
arrInstanceId
125+
);
126+
},
127+
128+
updateNextRunAt(arrInstanceId: number, nextRunAt: string | null): void {
129+
db.execute(
130+
'UPDATE arr_cleanup_settings SET next_run_at = ?, updated_at = CURRENT_TIMESTAMP WHERE arr_instance_id = ?',
131+
nextRunAt,
132+
arrInstanceId
133+
);
134+
},
135+
136+
getDueConfigs(): CleanupSettings[] {
137+
const rows = db.query<CleanupSettingsRow>(`
138+
SELECT * FROM arr_cleanup_settings
139+
WHERE enabled = 1
140+
AND (
141+
next_run_at IS NULL
142+
OR datetime('now') >= datetime(replace(replace(next_run_at, 'T', ' '), 'Z', ''))
143+
)
144+
`);
145+
return rows.map(rowToSettings);
146+
}
147+
};

src/lib/server/db/schema.sql

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-- Profilarr Database Schema
22
-- This file documents the current database schema after all migrations
33
-- DO NOT execute this file directly - use migrations instead
4-
-- Last updated: 2026-01-28
4+
-- Last updated: 2026-02-20
55

66
-- ==============================================================================
77
-- TABLE: migrations
@@ -276,7 +276,7 @@ CREATE TABLE database_instances (
276276
-- ==============================================================================
277277
-- TABLE: upgrade_configs
278278
-- Purpose: Store upgrade configuration per arr instance for automated quality upgrades
279-
-- Migration: 011_create_upgrade_configs.ts, 012_add_upgrade_last_run.ts, 013_add_upgrade_dry_run.ts
279+
-- Migration: 011_create_upgrade_configs.ts, 012_add_upgrade_last_run.ts, 013_add_upgrade_dry_run.ts, 051_cron_scheduling.ts
280280
-- ==============================================================================
281281

282282
CREATE TABLE upgrade_configs (
@@ -288,7 +288,7 @@ CREATE TABLE upgrade_configs (
288288
-- Core settings
289289
enabled INTEGER NOT NULL DEFAULT 0, -- Master on/off switch
290290
dry_run INTEGER NOT NULL DEFAULT 0, -- 1=dry run mode, 0=normal (Migration 013)
291-
schedule INTEGER NOT NULL DEFAULT 360, -- Run interval in minutes (default 6 hours)
291+
cron TEXT NOT NULL DEFAULT '0 */6 * * *', -- Cron expression (Migration 051, replaced schedule)
292292
filter_mode TEXT NOT NULL DEFAULT 'round_robin', -- 'round_robin' or 'random'
293293

294294
-- Filters (stored as JSON array of FilterConfig objects)
@@ -297,6 +297,7 @@ CREATE TABLE upgrade_configs (
297297
-- State tracking
298298
current_filter_index INTEGER NOT NULL DEFAULT 0, -- For round-robin mode
299299
last_run_at DATETIME, -- When upgrade job last ran (Migration 012)
300+
next_run_at TEXT, -- Next scheduled run (Migration 051)
300301

301302
-- Metadata
302303
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -614,7 +615,7 @@ CREATE INDEX idx_pattern_match_cache_created_at ON pattern_match_cache(created_a
614615
-- ==============================================================================
615616
-- TABLE: arr_rename_settings
616617
-- Purpose: Store rename configuration per arr instance for bulk file/folder renaming
617-
-- Migration: 024_create_arr_rename_settings.ts, 025_add_rename_notification_mode.ts
618+
-- Migration: 024_create_arr_rename_settings.ts, 025_add_rename_notification_mode.ts, 051_cron_scheduling.ts
618619
-- ==============================================================================
619620

620621
CREATE TABLE arr_rename_settings (
@@ -631,7 +632,8 @@ CREATE TABLE arr_rename_settings (
631632

632633
-- Job scheduling
633634
enabled INTEGER NOT NULL DEFAULT 0, -- Master on/off switch for scheduled job
634-
schedule INTEGER NOT NULL DEFAULT 1440, -- Run interval in minutes (default 24 hours)
635+
cron TEXT NOT NULL DEFAULT '0 0 * * *', -- Cron expression (Migration 051, replaced schedule)
636+
next_run_at TEXT, -- Next scheduled run (Migration 051)
635637
last_run_at DATETIME, -- When rename job last ran
636638

637639
-- Metadata
@@ -644,6 +646,36 @@ CREATE TABLE arr_rename_settings (
644646
-- Arr rename settings indexes (Migration: 024_create_arr_rename_settings.ts)
645647
CREATE INDEX idx_arr_rename_settings_arr_instance ON arr_rename_settings(arr_instance_id);
646648

649+
-- ==============================================================================
650+
-- TABLE: arr_cleanup_settings
651+
-- Purpose: Store cleanup scheduling configuration per arr instance
652+
-- Migration: 052_create_arr_cleanup_settings.ts
653+
-- ==============================================================================
654+
655+
CREATE TABLE arr_cleanup_settings (
656+
id INTEGER PRIMARY KEY AUTOINCREMENT,
657+
658+
-- Relationship (one config per arr instance)
659+
arr_instance_id INTEGER NOT NULL UNIQUE,
660+
661+
-- Settings
662+
enabled INTEGER NOT NULL DEFAULT 0, -- Master on/off switch
663+
cron TEXT NOT NULL DEFAULT '0 0 * * 0', -- Cron expression (default: weekly Sunday midnight)
664+
665+
-- State tracking
666+
next_run_at TEXT, -- Next scheduled run
667+
last_run_at TEXT, -- When cleanup job last ran
668+
669+
-- Metadata
670+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
671+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
672+
673+
FOREIGN KEY (arr_instance_id) REFERENCES arr_instances(id) ON DELETE CASCADE
674+
);
675+
676+
-- Arr cleanup settings indexes (Migration: 052_create_arr_cleanup_settings.ts)
677+
CREATE INDEX idx_arr_cleanup_settings_instance ON arr_cleanup_settings(arr_instance_id);
678+
647679
-- ==============================================================================
648680
-- TABLE: upgrade_runs
649681
-- Purpose: Store upgrade run history for each arr instance
@@ -665,7 +697,7 @@ CREATE TABLE upgrade_runs (
665697
dry_run INTEGER NOT NULL DEFAULT 0, -- 1=dry run, 0=live
666698

667699
-- Config snapshot (flat for queryability)
668-
schedule INTEGER NOT NULL, -- Schedule interval in minutes
700+
cron TEXT NOT NULL, -- Cron expression (Migration 051, renamed from schedule)
669701
filter_mode TEXT NOT NULL, -- 'round_robin' or 'random'
670702
filter_name TEXT NOT NULL, -- Name of the filter used
671703

src/lib/server/jobs/cleanup.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export function cleanupJobsForArrInstance(instanceId: number): number {
2121
'arr.sync',
2222
'arr.sync.qualityProfiles',
2323
'arr.sync.delayProfiles',
24-
'arr.sync.mediaManagement'
24+
'arr.sync.mediaManagement',
25+
'arr.cleanup'
2526
];
2627

2728
const jobs = jobQueueQueries.listByJobTypes(jobTypes);

src/lib/server/jobs/display.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export function formatJobTypeLabel(jobType: JobType): string {
2121
return 'Arr Rename';
2222
case 'arr.upgrade':
2323
return 'Arr Upgrade';
24+
case 'arr.cleanup':
25+
return 'Arr Cleanup';
2426
case 'pcd.sync':
2527
return 'PCD Sync';
2628
case 'backup.create':
@@ -63,7 +65,7 @@ export function buildJobDisplayName(
6365
}
6466

6567
const isArrSync = jobType === 'arr.sync' || jobType.startsWith('arr.sync.');
66-
if ((isArrSync || jobType === 'arr.rename' || jobType === 'arr.upgrade') && instanceId !== null) {
68+
if ((isArrSync || jobType === 'arr.rename' || jobType === 'arr.upgrade' || jobType === 'arr.cleanup') && instanceId !== null) {
6769
const name =
6870
lookups?.arrNameById?.get(instanceId) ?? arrInstancesQueries.getById(instanceId)?.name;
6971
return name ? `${base} - ${name}` : base;

0 commit comments

Comments
 (0)