diff --git a/src/lib/server/db/migrations.ts b/src/lib/server/db/migrations.ts index ca6e5f2..5675ec1 100644 --- a/src/lib/server/db/migrations.ts +++ b/src/lib/server/db/migrations.ts @@ -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 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 migration030 } from './migrations/030_create_general_settings.ts'; export interface Migration { version: number; @@ -274,7 +275,8 @@ export function loadMigrations(): Migration[] { migration026, migration027, migration028, - migration029 + migration029, + migration030 ]; // Sort by version number diff --git a/src/lib/server/db/migrations/030_create_general_settings.ts b/src/lib/server/db/migrations/030_create_general_settings.ts new file mode 100644 index 0000000..45b17b8 --- /dev/null +++ b/src/lib/server/db/migrations/030_create_general_settings.ts @@ -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; + ` +}; diff --git a/src/lib/server/db/queries/generalSettings.ts b/src/lib/server/db/queries/generalSettings.ts new file mode 100644 index 0000000..26cc320 --- /dev/null +++ b/src/lib/server/db/queries/generalSettings.ts @@ -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('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; + } +}; diff --git a/src/lib/server/db/schema.sql b/src/lib/server/db/schema.sql index 14f6036..214ca59 100644 --- a/src/lib/server/db/schema.sql +++ b/src/lib/server/db/schema.sql @@ -1,7 +1,7 @@ -- Profilarr Database Schema -- This file documents the current database schema after all migrations -- DO NOT execute this file directly - use migrations instead --- Last updated: 2026-01-21 +-- Last updated: 2026-01-22 -- ============================================================================== -- 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_started_at ON rename_runs(started_at DESC); 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 +); diff --git a/src/lib/server/utils/arr/defaults.ts b/src/lib/server/utils/arr/defaults.ts new file mode 100644 index 0000000..1c0db04 --- /dev/null +++ b/src/lib/server/utils/arr/defaults.ts @@ -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 = { + 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 = { + 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 { + 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}`); + } +} diff --git a/src/routes/arr/new/+page.server.ts b/src/routes/arr/new/+page.server.ts index 50f1361..19175f1 100644 --- a/src/routes/arr/new/+page.server.ts +++ b/src/routes/arr/new/+page.server.ts @@ -1,6 +1,10 @@ import { fail, redirect } from '@sveltejs/kit'; import type { Actions } from '@sveltejs/kit'; 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'; const VALID_TYPES = ['radarr', 'sonarr']; @@ -86,6 +90,35 @@ export const actions = { source: 'arr/new', 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) { await logger.error('Failed to create arr instance', { source: 'arr/new', diff --git a/src/routes/settings/general/+page.server.ts b/src/routes/settings/general/+page.server.ts index a32f081..a7dbecf 100644 --- a/src/routes/settings/general/+page.server.ts +++ b/src/routes/settings/general/+page.server.ts @@ -4,6 +4,7 @@ import { logSettingsQueries } from '$db/queries/logSettings.ts'; import { backupSettingsQueries } from '$db/queries/backupSettings.ts'; import { aiSettingsQueries } from '$db/queries/aiSettings.ts'; import { tmdbSettingsQueries } from '$db/queries/tmdbSettings.ts'; +import { generalSettingsQueries } from '$db/queries/generalSettings.ts'; import { logSettings } from '$logger/settings.ts'; import { logger } from '$logger/logger.ts'; @@ -12,6 +13,7 @@ export const load = () => { const backupSetting = backupSettingsQueries.get(); const aiSetting = aiSettingsQueries.get(); const tmdbSetting = tmdbSettingsQueries.get(); + const generalSetting = generalSettingsQueries.get(); if (!logSetting) { 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'); } + if (!generalSetting) { + throw new Error('General settings not found in database'); + } + return { logSettings: { retention_days: logSetting.retention_days, @@ -52,6 +58,9 @@ export const load = () => { }, tmdbSettings: { 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' }); + 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 }; } }; diff --git a/src/routes/settings/general/+page.svelte b/src/routes/settings/general/+page.svelte index 9223782..ca3c75b 100644 --- a/src/routes/settings/general/+page.svelte +++ b/src/routes/settings/general/+page.svelte @@ -4,6 +4,7 @@ import AISettings from './components/AISettings.svelte'; import TMDBSettings from './components/TMDBSettings.svelte'; import UISettings from './components/UISettings.svelte'; + import ArrDefaultsSettings from './components/ArrDefaultsSettings.svelte'; import type { PageData } from './$types'; export let data: PageData; @@ -21,6 +22,9 @@ + + + diff --git a/src/routes/settings/general/components/ArrDefaultsSettings.svelte b/src/routes/settings/general/components/ArrDefaultsSettings.svelte new file mode 100644 index 0000000..717da47 --- /dev/null +++ b/src/routes/settings/general/components/ArrDefaultsSettings.svelte @@ -0,0 +1,86 @@ + + +
+ +
+

+ Arr Instance Defaults +

+

+ Configure default settings applied when adding new Radarr/Sonarr instances +

+
+ + +
{ + 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(); + }; + }} + > +
+ +
+

Delay Profiles

+
+
+ + (settings.apply_default_delay_profiles = !settings.apply_default_delay_profiles)} + /> + + +
+
+
+ + +
+ +
+
+
+
diff --git a/src/routes/settings/general/components/types.ts b/src/routes/settings/general/components/types.ts index a980499..e1f616f 100644 --- a/src/routes/settings/general/components/types.ts +++ b/src/routes/settings/general/components/types.ts @@ -28,3 +28,7 @@ export interface AISettings { export interface TMDBSettings { api_key: string; } + +export interface GeneralSettings { + apply_default_delay_profiles: boolean; +}