diff --git a/src/lib/server/db/migrations.ts b/src/lib/server/db/migrations.ts index 97121e5..8049dd6 100644 --- a/src/lib/server/db/migrations.ts +++ b/src/lib/server/db/migrations.ts @@ -26,6 +26,7 @@ import { migration as migration021 } from './migrations/021_create_parsed_releas import { migration as migration022 } from './migrations/022_add_next_run_at.ts'; import { migration as migration023 } from './migrations/023_create_pattern_match_cache.ts'; import { migration as migration024 } from './migrations/024_create_arr_rename_settings.ts'; +import { migration as migration025 } from './migrations/025_add_rename_notification_mode.ts'; export interface Migration { version: number; @@ -264,7 +265,8 @@ export function loadMigrations(): Migration[] { migration021, migration022, migration023, - migration024 + migration024, + migration025 ]; // Sort by version number diff --git a/src/lib/server/db/migrations/025_add_rename_notification_mode.ts b/src/lib/server/db/migrations/025_add_rename_notification_mode.ts new file mode 100644 index 0000000..b14e636 --- /dev/null +++ b/src/lib/server/db/migrations/025_add_rename_notification_mode.ts @@ -0,0 +1,24 @@ +import type { Migration } from '../migrations.ts'; + +/** + * Migration 025: Add summary_notifications to arr_rename_settings + * + * Adds a boolean column to control whether rename notifications are sent as + * summaries (default) or rich notifications with all file details. + * + * - 1 (default): Summary notification with total count and one sample item + * - 0: Rich notification with all renamed files listed + */ + +export const migration: Migration = { + version: 25, + name: 'Add summary_notifications to arr_rename_settings', + + up: ` + ALTER TABLE arr_rename_settings ADD COLUMN summary_notifications INTEGER NOT NULL DEFAULT 1; + `, + + down: ` + ALTER TABLE arr_rename_settings DROP COLUMN summary_notifications; + ` +}; diff --git a/src/lib/server/db/queries/arrRenameSettings.ts b/src/lib/server/db/queries/arrRenameSettings.ts index 4901cd5..4dd26ca 100644 --- a/src/lib/server/db/queries/arrRenameSettings.ts +++ b/src/lib/server/db/queries/arrRenameSettings.ts @@ -9,6 +9,7 @@ interface RenameSettingsRow { dry_run: number; rename_folders: number; ignore_tag: string | null; + summary_notifications: number; enabled: number; schedule: number; last_run_at: string | null; @@ -25,6 +26,7 @@ export interface RenameSettings { dryRun: boolean; renameFolders: boolean; ignoreTag: string | null; + summaryNotifications: boolean; enabled: boolean; schedule: number; lastRunAt: string | null; @@ -39,6 +41,7 @@ export interface RenameSettingsInput { dryRun?: boolean; renameFolders?: boolean; ignoreTag?: string | null; + summaryNotifications?: boolean; enabled?: boolean; schedule?: number; } @@ -53,6 +56,7 @@ function rowToSettings(row: RenameSettingsRow): RenameSettings { dryRun: row.dry_run === 1, renameFolders: row.rename_folders === 1, ignoreTag: row.ignore_tag, + summaryNotifications: row.summary_notifications === 1, enabled: row.enabled === 1, schedule: row.schedule, lastRunAt: row.last_run_at, @@ -110,17 +114,19 @@ export const arrRenameSettingsQueries = { const dryRun = input.dryRun !== undefined ? (input.dryRun ? 1 : 0) : 1; const renameFolders = input.renameFolders !== undefined ? (input.renameFolders ? 1 : 0) : 0; const ignoreTag = input.ignoreTag ?? null; + const summaryNotifications = input.summaryNotifications !== undefined ? (input.summaryNotifications ? 1 : 0) : 1; const enabled = input.enabled !== undefined ? (input.enabled ? 1 : 0) : 0; const schedule = input.schedule ?? 1440; db.execute( `INSERT INTO arr_rename_settings - (arr_instance_id, dry_run, rename_folders, ignore_tag, enabled, schedule) - VALUES (?, ?, ?, ?, ?, ?)`, + (arr_instance_id, dry_run, rename_folders, ignore_tag, summary_notifications, enabled, schedule) + VALUES (?, ?, ?, ?, ?, ?, ?)`, arrInstanceId, dryRun, renameFolders, ignoreTag, + summaryNotifications, enabled, schedule ); @@ -147,6 +153,10 @@ export const arrRenameSettingsQueries = { updates.push('ignore_tag = ?'); params.push(input.ignoreTag); } + if (input.summaryNotifications !== undefined) { + updates.push('summary_notifications = ?'); + params.push(input.summaryNotifications ? 1 : 0); + } if (input.enabled !== undefined) { updates.push('enabled = ?'); params.push(input.enabled ? 1 : 0); diff --git a/src/lib/server/db/schema.sql b/src/lib/server/db/schema.sql index f03f1c6..c3f2810 100644 --- a/src/lib/server/db/schema.sql +++ b/src/lib/server/db/schema.sql @@ -465,7 +465,7 @@ CREATE INDEX idx_pattern_match_cache_created_at ON pattern_match_cache(created_a -- ============================================================================== -- TABLE: arr_rename_settings -- Purpose: Store rename configuration per arr instance for bulk file/folder renaming --- Migration: 024_create_arr_rename_settings.ts +-- Migration: 024_create_arr_rename_settings.ts, 025_add_rename_notification_mode.ts -- ============================================================================== CREATE TABLE arr_rename_settings ( @@ -478,6 +478,7 @@ CREATE TABLE arr_rename_settings ( dry_run INTEGER NOT NULL DEFAULT 1, -- 1=preview only, 0=make changes rename_folders INTEGER NOT NULL DEFAULT 0, -- 1=rename folders too, 0=files only ignore_tag TEXT, -- Tag name to skip (items with tag won't be renamed) + summary_notifications INTEGER NOT NULL DEFAULT 1, -- 1=summary, 0=rich (Migration 025) -- Job scheduling enabled INTEGER NOT NULL DEFAULT 0, -- Master on/off switch for scheduled job diff --git a/src/lib/server/notifications/definitions/rename.ts b/src/lib/server/notifications/definitions/rename.ts index 3b50b9d..1a64bc2 100644 --- a/src/lib/server/notifications/definitions/rename.ts +++ b/src/lib/server/notifications/definitions/rename.ts @@ -8,6 +8,7 @@ import type { RenameJobLog } from '$lib/server/rename/types.ts'; interface RenameNotificationParams { log: RenameJobLog; config: { username?: string; avatar_url?: string }; + summaryNotifications?: boolean; } // Discord limits (https://birdie0.github.io/discord-webhooks-guide/other/field_limits.html) @@ -231,9 +232,62 @@ function startNewEmbed( } /** - * Notification for rename job completion + * Build a summary notification (compact, single embed) */ -export const rename = ({ log, config }: RenameNotificationParams) => { +function buildSummaryNotification( + log: RenameJobLog, + config: { username?: string; avatar_url?: string } +) { + const embed = createEmbed() + .author(config.username || 'Profilarr', config.avatar_url) + .title(`${getTitle(log)} - ${log.instanceName}`) + .color(Colors.INFO) + .timestamp() + .footer(`Type: rename.${log.status}`); + + // Stats fields + if (log.config.dryRun) { + embed.field('Mode', 'Dry Run', true); + embed.field('Files', String(log.results.filesNeedingRename), true); + } else { + embed.field('Files', `${log.results.filesRenamed}/${log.results.filesNeedingRename}`, true); + + if (log.config.renameFolders) { + embed.field('Folders', String(log.results.foldersRenamed), true); + } + } + + // Add sample if there are renamed items + if (log.renamedItems.length > 0) { + const sample = log.renamedItems[0]; + const sampleFile = sample.files[0]; + const othersCount = log.results.filesNeedingRename - 1; + const othersText = othersCount > 0 ? ` + ${othersCount} other${othersCount === 1 ? '' : 's'}` : ''; + + embed.field( + `Sample: ${truncateFieldName(sample.title)}${othersText}`, + '```\n' + formatFileEntry(sampleFile) + '\n```', + false + ); + } + + const genericMessage = + log.status === 'failed' + ? `Rename failed for ${log.instanceName}` + : `Renamed ${log.results.filesRenamed} files for ${log.instanceName}`; + + return notify(`rename.${log.status}`) + .generic(getTitle(log), genericMessage) + .discord((d) => d.embed(embed)); +} + +/** + * Build a rich notification (detailed, multiple embeds if needed) + */ +function buildRichNotification( + log: RenameJobLog, + config: { username?: string; avatar_url?: string } +) { const embeds: EmbedBuilder[] = []; // If no files to rename, single embed with just stats @@ -292,4 +346,31 @@ export const rename = ({ log, config }: RenameNotificationParams) => { for (const e of embeds) d.embed(e); return d; }); +} + +/** + * Notification for rename job completion + */ +export const rename = ({ log, config, summaryNotifications = true }: RenameNotificationParams) => { + // If no files to rename, always use a simple notification + if (log.renamedItems.length === 0) { + const embed = createEmbed() + .author(config.username || 'Profilarr', config.avatar_url) + .title(`${getTitle(log)} - ${log.instanceName}`) + .color(Colors.INFO) + .field('Status', 'No files needed renaming', false) + .timestamp() + .footer(`Type: rename.${log.status}`); + + return notify(`rename.${log.status}`) + .generic(getTitle(log), `No files needed renaming for ${log.instanceName}`) + .discord((d) => d.embed(embed)); + } + + // Use summary or rich notification based on setting + if (summaryNotifications) { + return buildSummaryNotification(log, config); + } + + return buildRichNotification(log, config); }; diff --git a/src/lib/server/notifications/types.ts b/src/lib/server/notifications/types.ts index 6da3ffd..88c6ab9 100644 --- a/src/lib/server/notifications/types.ts +++ b/src/lib/server/notifications/types.ts @@ -2,6 +2,8 @@ * Core notification types and interfaces */ +import type { DiscordEmbed } from './notifiers/discord/embed.ts'; + /** * Type-safe notification type constants */ @@ -40,7 +42,7 @@ export interface GenericNotification { * Discord-specific notification content */ export interface DiscordNotification { - embeds: unknown[]; + embeds: DiscordEmbed[]; } /** diff --git a/src/lib/server/rename/processor.ts b/src/lib/server/rename/processor.ts index 0fc6357..30f8de5 100644 --- a/src/lib/server/rename/processor.ts +++ b/src/lib/server/rename/processor.ts @@ -60,7 +60,7 @@ function createSkippedLog( /** * Send rename notification */ -async function sendRenameNotification(log: RenameJobLog): Promise { +async function sendRenameNotification(log: RenameJobLog, summaryNotifications: boolean): Promise { // Only notify if there were files to rename if (log.results.filesNeedingRename > 0) { const { DiscordNotifier } = await import('$lib/server/notifications/notifiers/discord/index.ts'); @@ -80,7 +80,7 @@ async function sendRenameNotification(log: RenameJobLog): Promise { if (service.service_type === 'discord') { const notifier = new DiscordNotifier(config); - const notification = notifications.rename({ log, config }).build(); + const notification = notifications.rename({ log, config, summaryNotifications }).build(); await notifier.notify(notification); } } catch { @@ -179,7 +179,7 @@ async function processRadarrRename( }; await logRenameRun(log); - sendRenameNotification(log); + sendRenameNotification(log, settings.summaryNotifications); return log; } @@ -280,7 +280,7 @@ async function processRadarrRename( }; await logRenameRun(log); - sendRenameNotification(log); + sendRenameNotification(log, settings.summaryNotifications); return log; } @@ -375,7 +375,7 @@ async function processSonarrRename( }; await logRenameRun(log); - sendRenameNotification(log); + sendRenameNotification(log, settings.summaryNotifications); return log; } @@ -490,7 +490,7 @@ async function processSonarrRename( }; await logRenameRun(log); - sendRenameNotification(log); + sendRenameNotification(log, settings.summaryNotifications); return log; } diff --git a/src/lib/server/utils/arr/clients/radarr.ts b/src/lib/server/utils/arr/clients/radarr.ts index 7618d09..328f495 100644 --- a/src/lib/server/utils/arr/clients/radarr.ts +++ b/src/lib/server/utils/arr/clients/radarr.ts @@ -28,7 +28,7 @@ export class RadarrClient extends BaseArrClient { /** * Get all quality profiles */ - getQualityProfiles(): Promise { + override getQualityProfiles(): Promise { return this.get(`/api/${this.apiVersion}/qualityprofile`); } diff --git a/src/routes/arr/[id]/rename/+page.server.ts b/src/routes/arr/[id]/rename/+page.server.ts index d68d89a..3a46541 100644 --- a/src/routes/arr/[id]/rename/+page.server.ts +++ b/src/routes/arr/[id]/rename/+page.server.ts @@ -47,13 +47,15 @@ export const actions: Actions = { const renameFolders = formData.get('renameFolders') === 'true'; const ignoreTag = (formData.get('ignoreTag') as string) || null; const schedule = parseInt(formData.get('schedule') as string, 10) || 1440; + const summaryNotifications = formData.get('summaryNotifications') === 'true'; const settingsData = { enabled, dryRun, renameFolders, ignoreTag, - schedule + schedule, + summaryNotifications }; arrRenameSettingsQueries.upsert(id, settingsData); @@ -106,13 +108,15 @@ export const actions: Actions = { const renameFolders = formData.get('renameFolders') === 'true'; const ignoreTag = (formData.get('ignoreTag') as string) || null; const schedule = parseInt(formData.get('schedule') as string, 10) || 1440; + const summaryNotifications = formData.get('summaryNotifications') === 'true'; const settingsData = { enabled, dryRun, renameFolders, ignoreTag, - schedule + schedule, + summaryNotifications }; arrRenameSettingsQueries.update(id, settingsData); diff --git a/src/routes/arr/[id]/rename/+page.svelte b/src/routes/arr/[id]/rename/+page.svelte index c683827..5d49cea 100644 --- a/src/routes/arr/[id]/rename/+page.svelte +++ b/src/routes/arr/[id]/rename/+page.svelte @@ -19,6 +19,7 @@ let renameFolders = data.settings?.renameFolders ?? false; let ignoreTag = data.settings?.ignoreTag ?? ''; let schedule = String(data.settings?.schedule ?? 1440); + let summaryNotifications = data.settings?.summaryNotifications ?? true; // Track if settings exist (determines save vs edit) $: isNewConfig = !data.settings; @@ -29,7 +30,7 @@ // Initialize dirty tracking onMount(() => { - const formData = { enabled, dryRun, renameFolders, ignoreTag, schedule }; + const formData = { enabled, dryRun, renameFolders, ignoreTag, schedule, summaryNotifications }; if (isNewConfig) { initCreate(formData); } else { @@ -44,6 +45,7 @@ $: update('renameFolders', renameFolders); $: update('ignoreTag', ignoreTag); $: update('schedule', schedule); + $: update('summaryNotifications', summaryNotifications); // Handle form response - use a processed flag to avoid re-running on field changes let lastFormId: unknown = null; @@ -51,7 +53,7 @@ lastFormId = form; if (form.success && !form.runResult) { alertStore.add('success', 'Configuration saved successfully'); - initEdit({ enabled, dryRun, renameFolders, ignoreTag, schedule }); + initEdit({ enabled, dryRun, renameFolders, ignoreTag, schedule, summaryNotifications }); } if (form.success && form.runResult) { const r = form.runResult; @@ -89,6 +91,7 @@ +
@@ -119,7 +122,7 @@ /> {/if} - +
diff --git a/src/routes/arr/[id]/rename/components/RenameSettings.svelte b/src/routes/arr/[id]/rename/components/RenameSettings.svelte index 4aa2a64..c98ce2e 100644 --- a/src/routes/arr/[id]/rename/components/RenameSettings.svelte +++ b/src/routes/arr/[id]/rename/components/RenameSettings.svelte @@ -4,6 +4,7 @@ export let renameFolders: boolean = false; export let ignoreTag: string = ''; export let schedule: string = '1440'; + export let summaryNotifications: boolean = true; const scheduleOptions = [ { value: '60', label: 'Every hour' }, @@ -67,7 +68,7 @@
-
+
+ + +
+
+ +

Compact notifications

+
+ +