mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
feat: add summary notifs for rename jobs, rename original to rich notifs
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
`
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,7 +60,7 @@ function createSkippedLog(
|
||||
/**
|
||||
* Send rename notification
|
||||
*/
|
||||
async function sendRenameNotification(log: RenameJobLog): Promise<void> {
|
||||
async function sendRenameNotification(log: RenameJobLog, summaryNotifications: boolean): Promise<void> {
|
||||
// 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<void> {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export class RadarrClient extends BaseArrClient {
|
||||
/**
|
||||
* Get all quality profiles
|
||||
*/
|
||||
getQualityProfiles(): Promise<RadarrQualityProfile[]> {
|
||||
override getQualityProfiles(): Promise<RadarrQualityProfile[]> {
|
||||
return this.get<RadarrQualityProfile[]>(`/api/${this.apiVersion}/qualityprofile`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 @@
|
||||
<input type="hidden" name="renameFolders" value={renameFolders} />
|
||||
<input type="hidden" name="ignoreTag" value={ignoreTag} />
|
||||
<input type="hidden" name="schedule" value={schedule} />
|
||||
<input type="hidden" name="summaryNotifications" value={summaryNotifications} />
|
||||
|
||||
<div class="mt-6 space-y-6">
|
||||
<!-- Header -->
|
||||
@@ -119,7 +122,7 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<RenameSettings bind:enabled bind:dryRun bind:renameFolders bind:ignoreTag bind:schedule />
|
||||
<RenameSettings bind:enabled bind:dryRun bind:renameFolders bind:ignoreTag bind:schedule bind:summaryNotifications />
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-end gap-3">
|
||||
|
||||
@@ -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 @@
|
||||
</div>
|
||||
|
||||
<!-- Toggles -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<!-- Enabled Toggle -->
|
||||
<div
|
||||
class="flex items-center justify-between rounded-lg border border-neutral-200 p-3 dark:border-neutral-700"
|
||||
@@ -160,6 +161,37 @@
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Summary Notifications Toggle -->
|
||||
<div
|
||||
class="flex items-center justify-between rounded-lg border border-neutral-200 p-3 dark:border-neutral-700"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
for="summaryNotifications"
|
||||
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Summary Notifications
|
||||
</label>
|
||||
<p class="text-xs text-neutral-500 dark:text-neutral-400">Compact notifications</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={summaryNotifications}
|
||||
aria-label="Toggle summary notifications"
|
||||
on:click={() => (summaryNotifications = !summaryNotifications)}
|
||||
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 {summaryNotifications
|
||||
? 'bg-blue-600 dark:bg-blue-500'
|
||||
: 'bg-neutral-200 dark:bg-neutral-700'}"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out {summaryNotifications
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'}"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user