feat: apply default delay profile to arrs when adding a new one

This commit is contained in:
Sam Chau
2026-01-22 09:05:30 +10:30
parent 76e51c9194
commit e6d16d76be
10 changed files with 342 additions and 2 deletions

View File

@@ -31,6 +31,7 @@ import { migration as migration026 } from './migrations/026_create_upgrade_runs.
import { migration as migration027 } from './migrations/027_create_rename_runs.ts'; import { migration as migration027 } from './migrations/027_create_rename_runs.ts';
import { migration as migration028 } from './migrations/028_simplify_delay_profile_sync.ts'; import { migration as migration028 } from './migrations/028_simplify_delay_profile_sync.ts';
import { migration as migration029 } from './migrations/029_add_database_id_foreign_keys.ts'; import { migration as migration029 } from './migrations/029_add_database_id_foreign_keys.ts';
import { migration as migration030 } from './migrations/030_create_general_settings.ts';
export interface Migration { export interface Migration {
version: number; version: number;
@@ -274,7 +275,8 @@ export function loadMigrations(): Migration[] {
migration026, migration026,
migration027, migration027,
migration028, migration028,
migration029 migration029,
migration030
]; ];
// Sort by version number // Sort by version number

View File

@@ -0,0 +1,31 @@
import type { Migration } from '../migrations.ts';
/**
* Create general_settings table for app-wide settings
* Initial setting: apply_default_delay_profiles (ON by default)
*/
export const migration: Migration = {
version: 30,
name: 'Create general_settings table',
up: `
-- General settings table (singleton pattern)
CREATE TABLE IF NOT EXISTS general_settings (
id INTEGER PRIMARY KEY CHECK (id = 1),
-- Default delay profile settings
apply_default_delay_profiles INTEGER NOT NULL DEFAULT 1, -- 1=apply defaults when adding arr, 0=don't
-- Metadata
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Insert default row
INSERT INTO general_settings (id) VALUES (1);
`,
down: `
DROP TABLE IF EXISTS general_settings;
`
};

View File

@@ -0,0 +1,64 @@
import { db } from '../db.ts';
/**
* Types for general_settings table
*/
export interface GeneralSettings {
id: number;
apply_default_delay_profiles: number; // 1=true, 0=false
created_at: string;
updated_at: string;
}
export interface UpdateGeneralSettingsInput {
applyDefaultDelayProfiles?: boolean;
}
/**
* All queries for general_settings table
* Singleton pattern - only one settings record exists
*/
export const generalSettingsQueries = {
/**
* Get the general settings (singleton)
*/
get(): GeneralSettings | undefined {
return db.queryFirst<GeneralSettings>('SELECT * FROM general_settings WHERE id = 1');
},
/**
* Check if default delay profiles should be applied when adding arr
*/
shouldApplyDefaultDelayProfiles(): boolean {
const settings = this.get();
return settings?.apply_default_delay_profiles === 1;
},
/**
* Update general settings
*/
update(input: UpdateGeneralSettingsInput): boolean {
const updates: string[] = [];
const params: (string | number)[] = [];
if (input.applyDefaultDelayProfiles !== undefined) {
updates.push('apply_default_delay_profiles = ?');
params.push(input.applyDefaultDelayProfiles ? 1 : 0);
}
if (updates.length === 0) {
return false;
}
// Add updated_at
updates.push('updated_at = CURRENT_TIMESTAMP');
params.push(1); // id is always 1
const affected = db.execute(
`UPDATE general_settings SET ${updates.join(', ')} WHERE id = ?`,
...params
);
return affected > 0;
}
};

View File

@@ -1,7 +1,7 @@
-- Profilarr Database Schema -- Profilarr Database Schema
-- This file documents the current database schema after all migrations -- This file documents the current database schema after all migrations
-- DO NOT execute this file directly - use migrations instead -- DO NOT execute this file directly - use migrations instead
-- Last updated: 2026-01-21 -- Last updated: 2026-01-22
-- ============================================================================== -- ==============================================================================
-- TABLE: migrations -- TABLE: migrations
@@ -603,3 +603,20 @@ CREATE TABLE rename_runs (
CREATE INDEX idx_rename_runs_instance ON rename_runs(instance_id); 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_started_at ON rename_runs(started_at DESC);
CREATE INDEX idx_rename_runs_status ON rename_runs(status); CREATE INDEX idx_rename_runs_status ON rename_runs(status);
-- ==============================================================================
-- TABLE: general_settings
-- Purpose: Store general app-wide settings (singleton pattern with id=1)
-- Migration: 030_create_general_settings.ts
-- ==============================================================================
CREATE TABLE general_settings (
id INTEGER PRIMARY KEY CHECK (id = 1),
-- Default delay profile settings
apply_default_delay_profiles INTEGER NOT NULL DEFAULT 1, -- 1=apply defaults when adding arr, 0=don't
-- Metadata
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

View File

@@ -0,0 +1,64 @@
/**
* Default delay profiles for Radarr and Sonarr
*
* These are applied when a new arr instance is added (if enabled in general settings).
* Values can be updated based on community feedback.
*
* Protocol configuration (maps to UI options):
* - Prefer Usenet: enableUsenet=true, enableTorrent=true, preferredProtocol='usenet'
* - Prefer Torrent: enableUsenet=true, enableTorrent=true, preferredProtocol='torrent'
* - Only Usenet: enableUsenet=true, enableTorrent=false, preferredProtocol='usenet'
* - Only Torrent: enableUsenet=false, enableTorrent=true, preferredProtocol='torrent'
*
* TODO: Get final values from Seraphys
*/
import type { ArrDelayProfile } from './types.ts';
/**
* Default delay profile for Radarr
* Applied to the default profile (id=1) when adding a new Radarr instance
*/
export const RADARR_DEFAULT_DELAY_PROFILE: Omit<ArrDelayProfile, 'id' | 'order'> = {
enableUsenet: true,
enableTorrent: true,
preferredProtocol: 'torrent',
usenetDelay: 600,
torrentDelay: 600,
bypassIfHighestQuality: false,
bypassIfAboveCustomFormatScore: false,
minimumCustomFormatScore: 0,
tags: []
};
/**
* Default delay profile for Sonarr
* Applied to the default profile (id=1) when adding a new Sonarr instance
*/
export const SONARR_DEFAULT_DELAY_PROFILE: Omit<ArrDelayProfile, 'id' | 'order'> = {
enableUsenet: true,
enableTorrent: true,
preferredProtocol: 'torrent',
usenetDelay: 600,
torrentDelay: 600,
bypassIfHighestQuality: false,
bypassIfAboveCustomFormatScore: false,
minimumCustomFormatScore: 0,
tags: []
};
/**
* Get the default delay profile for an arr type
*/
export function getDefaultDelayProfile(
arrType: 'radarr' | 'sonarr'
): Omit<ArrDelayProfile, 'id' | 'order'> {
switch (arrType) {
case 'radarr':
return RADARR_DEFAULT_DELAY_PROFILE;
case 'sonarr':
return SONARR_DEFAULT_DELAY_PROFILE;
default:
throw new Error(`No default delay profile for arr type: ${arrType}`);
}
}

View File

@@ -1,6 +1,10 @@
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from '@sveltejs/kit'; import type { Actions } from '@sveltejs/kit';
import { arrInstancesQueries } from '$db/queries/arrInstances.ts'; import { arrInstancesQueries } from '$db/queries/arrInstances.ts';
import { generalSettingsQueries } from '$db/queries/generalSettings.ts';
import { createArrClient } from '$arr/factory.ts';
import { getDefaultDelayProfile } from '$arr/defaults.ts';
import type { ArrType } from '$arr/types.ts';
import { logger } from '$logger/logger.ts'; import { logger } from '$logger/logger.ts';
const VALID_TYPES = ['radarr', 'sonarr']; const VALID_TYPES = ['radarr', 'sonarr'];
@@ -86,6 +90,35 @@ export const actions = {
source: 'arr/new', source: 'arr/new',
meta: { id, name, type, url } meta: { id, name, type, url }
}); });
// Apply default delay profile if enabled
if (
(type === 'radarr' || type === 'sonarr') &&
generalSettingsQueries.shouldApplyDefaultDelayProfiles()
) {
try {
const client = createArrClient(type as ArrType, url, apiKey);
const defaultProfile = getDefaultDelayProfile(type);
// Update the default delay profile (id=1)
await client.updateDelayProfile(1, {
...defaultProfile,
id: 1,
order: 2147483647 // Default profile order
});
await logger.info(`Applied default delay profile to ${name}`, {
source: 'arr/new',
meta: { id, type, profile: defaultProfile }
});
} catch (error) {
// Log but don't fail - the instance was created successfully
await logger.warn(`Failed to apply default delay profile to ${name}`, {
source: 'arr/new',
meta: { id, type, error: error instanceof Error ? error.message : error }
});
}
}
} catch (error) { } catch (error) {
await logger.error('Failed to create arr instance', { await logger.error('Failed to create arr instance', {
source: 'arr/new', source: 'arr/new',

View File

@@ -4,6 +4,7 @@ import { logSettingsQueries } from '$db/queries/logSettings.ts';
import { backupSettingsQueries } from '$db/queries/backupSettings.ts'; import { backupSettingsQueries } from '$db/queries/backupSettings.ts';
import { aiSettingsQueries } from '$db/queries/aiSettings.ts'; import { aiSettingsQueries } from '$db/queries/aiSettings.ts';
import { tmdbSettingsQueries } from '$db/queries/tmdbSettings.ts'; import { tmdbSettingsQueries } from '$db/queries/tmdbSettings.ts';
import { generalSettingsQueries } from '$db/queries/generalSettings.ts';
import { logSettings } from '$logger/settings.ts'; import { logSettings } from '$logger/settings.ts';
import { logger } from '$logger/logger.ts'; import { logger } from '$logger/logger.ts';
@@ -12,6 +13,7 @@ export const load = () => {
const backupSetting = backupSettingsQueries.get(); const backupSetting = backupSettingsQueries.get();
const aiSetting = aiSettingsQueries.get(); const aiSetting = aiSettingsQueries.get();
const tmdbSetting = tmdbSettingsQueries.get(); const tmdbSetting = tmdbSettingsQueries.get();
const generalSetting = generalSettingsQueries.get();
if (!logSetting) { if (!logSetting) {
throw new Error('Log settings not found in database'); throw new Error('Log settings not found in database');
@@ -29,6 +31,10 @@ export const load = () => {
throw new Error('TMDB settings not found in database'); throw new Error('TMDB settings not found in database');
} }
if (!generalSetting) {
throw new Error('General settings not found in database');
}
return { return {
logSettings: { logSettings: {
retention_days: logSetting.retention_days, retention_days: logSetting.retention_days,
@@ -52,6 +58,9 @@ export const load = () => {
}, },
tmdbSettings: { tmdbSettings: {
api_key: tmdbSetting.api_key api_key: tmdbSetting.api_key
},
generalSettings: {
apply_default_delay_profiles: generalSetting.apply_default_delay_profiles === 1
} }
}; };
}; };
@@ -243,6 +252,32 @@ export const actions: Actions = {
source: 'settings/general' source: 'settings/general'
}); });
return { success: true };
},
updateArrDefaults: async ({ request }: RequestEvent) => {
const formData = await request.formData();
// Parse form data
const applyDefaultDelayProfiles = formData.get('apply_default_delay_profiles') === 'on';
// Update settings
const updated = generalSettingsQueries.update({
applyDefaultDelayProfiles
});
if (!updated) {
await logger.error('Failed to update arr default settings', {
source: 'settings/general'
});
return fail(500, { error: 'Failed to update settings' });
}
await logger.info('Arr default settings updated', {
source: 'settings/general',
meta: { applyDefaultDelayProfiles }
});
return { success: true }; return { success: true };
} }
}; };

View File

@@ -4,6 +4,7 @@
import AISettings from './components/AISettings.svelte'; import AISettings from './components/AISettings.svelte';
import TMDBSettings from './components/TMDBSettings.svelte'; import TMDBSettings from './components/TMDBSettings.svelte';
import UISettings from './components/UISettings.svelte'; import UISettings from './components/UISettings.svelte';
import ArrDefaultsSettings from './components/ArrDefaultsSettings.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
export let data: PageData; export let data: PageData;
@@ -21,6 +22,9 @@
<!-- UI Preferences --> <!-- UI Preferences -->
<UISettings /> <UISettings />
<!-- Arr Instance Defaults -->
<ArrDefaultsSettings settings={data.generalSettings} />
<!-- Backup Configuration --> <!-- Backup Configuration -->
<BackupSettings settings={data.backupSettings} /> <BackupSettings settings={data.backupSettings} />

View File

@@ -0,0 +1,86 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { alertStore } from '$alerts/store';
import { Save, Check } from 'lucide-svelte';
import IconCheckbox from '$ui/form/IconCheckbox.svelte';
import type { GeneralSettings } from './types';
export let settings: GeneralSettings;
</script>
<div
class="rounded-lg border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900"
>
<!-- Header -->
<div class="border-b border-neutral-200 px-6 py-4 dark:border-neutral-800">
<h2 class="text-xl font-semibold text-neutral-900 dark:text-neutral-50">
Arr Instance Defaults
</h2>
<p class="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
Configure default settings applied when adding new Radarr/Sonarr instances
</p>
</div>
<!-- Form -->
<form
method="POST"
action="?/updateArrDefaults"
class="p-6"
use:enhance={() => {
return async ({ result, update }) => {
if (result.type === 'failure' && result.data) {
alertStore.add('error', (result.data as { error?: string }).error || 'Failed to save');
} else if (result.type === 'success') {
alertStore.add('success', 'Arr default settings saved successfully!');
}
await update();
};
}}
>
<div class="space-y-6">
<!-- Delay Profile Defaults -->
<div class="space-y-3">
<h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-50">Delay Profiles</h3>
<div class="space-y-2">
<div class="flex items-center gap-3">
<IconCheckbox
icon={Check}
checked={settings.apply_default_delay_profiles}
on:click={() =>
(settings.apply_default_delay_profiles = !settings.apply_default_delay_profiles)}
/>
<input
type="hidden"
name="apply_default_delay_profiles"
value={settings.apply_default_delay_profiles ? 'on' : ''}
/>
<button
type="button"
class="flex-1 text-left"
on:click={() =>
(settings.apply_default_delay_profiles = !settings.apply_default_delay_profiles)}
>
<span class="text-sm font-medium text-neutral-900 dark:text-neutral-50">
Apply Default Delay Profile
</span>
<p class="text-xs text-neutral-500 dark:text-neutral-400">
Automatically configure the default delay profile when adding new arr instances
</p>
</button>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex justify-end gap-3 border-t border-neutral-200 pt-6 dark:border-neutral-800">
<button
type="submit"
class="flex items-center gap-2 rounded-lg bg-accent-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-accent-700 dark:bg-accent-500 dark:hover:bg-accent-600"
>
<Save size={16} />
Save Changes
</button>
</div>
</div>
</form>
</div>

View File

@@ -28,3 +28,7 @@ export interface AISettings {
export interface TMDBSettings { export interface TMDBSettings {
api_key: string; api_key: string;
} }
export interface GeneralSettings {
apply_default_delay_profiles: boolean;
}