Merge pull request #162 from xFlawless11x/feature/on-grab-notification

feat: add On Grab notification event
This commit is contained in:
kikootwo
2026-05-14 15:47:17 -04:00
committed by GitHub
7 changed files with 77 additions and 8 deletions
+31 -2
View File
@@ -19,6 +19,7 @@ export type NotificationPriority = 'normal' | 'high';
* - `emoji`: Emoji prefix for notification titles
* - `severity`: Drives provider formatting (colors, Apprise types, ntfy tags)
* - `priority`: Drives notification urgency (Pushover/ntfy priority levels)
* - `messageLabel`: Optional label for the `message` payload field (defaults to "Error" if omitted)
*/
export const NOTIFICATION_EVENTS = {
request_pending_approval: {
@@ -35,6 +36,18 @@ export const NOTIFICATION_EVENTS = {
severity: 'success' as const,
priority: 'normal' as const,
},
request_grabbed: {
label: 'Request Grabbed',
title: 'Download Grabbed',
titleByRequestType: {
audiobook: 'Audiobook Grabbed',
ebook: 'Ebook Grabbed',
} as Record<string, string>,
emoji: '\u{1F4E5}',
severity: 'info' as const,
priority: 'normal' as const,
messageLabel: 'Details',
},
request_available: {
label: 'Request Available',
title: 'Request Available',
@@ -59,6 +72,7 @@ export const NOTIFICATION_EVENTS = {
emoji: '\u{1F6A9}',
severity: 'warning' as const,
priority: 'high' as const,
messageLabel: 'Reason',
},
} as const;
@@ -71,9 +85,24 @@ export const NOTIFICATION_EVENT_KEYS = Object.keys(NOTIFICATION_EVENTS) as [Noti
/** Metadata shape for a single notification event */
export type NotificationEventMeta = (typeof NOTIFICATION_EVENTS)[NotificationEvent];
/**
* Normalized interface for event metadata consumed by providers.
* Broadens the `as const` literal union to make optional fields accessible.
*/
export interface NotificationEventConfig {
label: string;
title: string;
titleByRequestType?: Record<string, string>;
emoji: string;
severity: NotificationSeverity;
priority: NotificationPriority;
/** Label for the `message` payload field. Defaults to "Error" in providers when absent. */
messageLabel?: string;
}
/** Helper: get event metadata by key */
export function getEventMeta(event: NotificationEvent) {
return NOTIFICATION_EVENTS[event];
export function getEventMeta(event: NotificationEvent): NotificationEventConfig {
return NOTIFICATION_EVENTS[event] as NotificationEventConfig;
}
/**
@@ -103,8 +103,32 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P
logger.info(`Created download history record: ${downloadHistory.id}`);
// Trigger monitor download job with initial delay
// Send grab notification
const requestWithUser = await prisma.request.findUnique({
where: { id: requestId },
include: {
user: { select: { plexUsername: true } },
},
});
const jobQueue = getJobQueueService();
if (requestWithUser) {
const grabMessage = `${torrent.title} via ${torrent.indexer} (${client.clientType})`;
await jobQueue.addNotificationJob(
'request_grabbed',
requestId,
audiobook.title,
audiobook.author,
requestWithUser.user.plexUsername || 'Unknown User',
grabMessage,
requestWithUser.type
).catch((error) => {
logger.error('Failed to queue grab notification', { error: error instanceof Error ? error.message : String(error) });
});
}
// Trigger monitor download job with initial delay
await jobQueue.addMonitorJob(
requestId,
downloadHistory.id,
@@ -127,6 +127,7 @@ export class AppriseProvider implements INotificationProvider {
private formatMessage(payload: NotificationPayload): { title: string; body: string } {
const { event, title, author, userName, message, requestType } = payload;
const meta = getEventMeta(event);
const isIssue = event === 'issue_reported';
const messageLines = [
@@ -136,7 +137,9 @@ export class AppriseProvider implements INotificationProvider {
];
if (message) {
messageLines.push(isIssue ? `\u{1F4DD} Reason: ${message}` : `\u26A0\uFE0F Error: ${message}`);
const messageLabel = meta.messageLabel ?? 'Error';
const msgEmoji = meta.severity === 'error' ? '\u26A0\uFE0F' : '\u{1F4DD}';
messageLines.push(`${msgEmoji} ${messageLabel}: ${message}`);
}
return {
@@ -71,7 +71,7 @@ export class DiscordProvider implements INotificationProvider {
];
if (message) {
fields.push({ name: isIssue ? 'Reason' : 'Error', value: message, inline: false });
fields.push({ name: meta.messageLabel ?? 'Error', value: message, inline: false });
}
return {
@@ -84,6 +84,7 @@ export class NtfyProvider implements INotificationProvider {
private formatMessage(payload: NotificationPayload): { title: string; message: string } {
const { event, title, author, userName, message, requestType } = payload;
const meta = getEventMeta(event);
const isIssue = event === 'issue_reported';
const messageLines = [
@@ -93,7 +94,9 @@ export class NtfyProvider implements INotificationProvider {
];
if (message) {
messageLines.push(isIssue ? `\u{1F4DD} Reason: ${message}` : `\u26A0\uFE0F Error: ${message}`);
const messageLabel = meta.messageLabel ?? 'Error';
const msgEmoji = meta.severity === 'error' ? '\u26A0\uFE0F' : '\u{1F4DD}';
messageLines.push(`${msgEmoji} ${messageLabel}: ${message}`);
}
return {
@@ -91,7 +91,9 @@ export class PushoverProvider implements INotificationProvider {
];
if (message) {
messageLines.push('', isIssue ? `\u{1F4DD} Reason: ${message}` : `\u26A0\uFE0F Error: ${message}`);
const messageLabel = meta.messageLabel ?? 'Error';
const msgEmoji = meta.severity === 'error' ? '\u26A0\uFE0F' : '\u{1F4DD}';
messageLines.push('', `${msgEmoji} ${messageLabel}: ${message}`);
}
return {