From cc517758914ab22a5f0f100230d4ab21ceb2fbe7 Mon Sep 17 00:00:00 2001 From: JackW <53652452+JackW6809@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:33:57 +0000 Subject: [PATCH 1/9] feat(notifications): added Play On Media Server in notifications when media is available In the email notification users receive when their media request is now available, I have added a Play on media server (e.g. Play on Plex) button that takes them directly to the media on their media server. feat(notifications): play On Media Server in Discord & Slack Notifications In the Discord & Slack notification users receive when their media request is now available, I have added a Play on media server (e.g. Play on Plex) button that takes them directly to the media on their media server. --- server/lib/notifications/agents/email.ts | 31 ++++++++++++- server/lib/notifications/agents/slack.ts | 45 ++++++++++++++++++- server/lib/notifications/agents/telegram.ts | 32 ++++++++++++- server/templates/email/media-request/html.pug | 6 +++ 4 files changed, 111 insertions(+), 3 deletions(-) diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 0e373c855f..6dcc668576 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -1,5 +1,6 @@ import { IssueType, IssueTypeName } from '@server/constants/issue'; import { MediaType } from '@server/constants/media'; +import { MediaServerType } from '@server/constants/server'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; import PreparedEmail from '@server/lib/email'; @@ -49,9 +50,33 @@ class EmailAgent recipientName?: string ): EmailOptions | undefined { const settings = getSettings(); - const { applicationUrl, applicationTitle } = settings.main; + const { applicationUrl, applicationTitle, mediaServerType } = settings.main; const { embedPoster } = settings.notifications.agents.email; + function getAvailableMediaServerName() { + if (mediaServerType === MediaServerType.EMBY) { + return 'Emby'; + } + + if (mediaServerType === MediaServerType.PLEX) { + return 'Plex'; + } + + return 'Jellyfin'; + } + + const mediaServerName = getAvailableMediaServerName(); + + function getAvailableMediaServerUrl(): string | undefined { + const wants4k = payload.request?.is4k; + const url4k = (payload.media as any)?.mediaUrl4k as string | undefined; + const url = (payload.media as any)?.mediaUrl as string | undefined; + + return (wants4k ? (url4k ?? url) : (url ?? url4k)) || undefined; + } + + const mediaServerUrl = getAvailableMediaServerUrl(); + if (type === Notification.TEST_NOTIFICATION) { return { template: path.join(__dirname, '../../../templates/email/test-email'), @@ -141,6 +166,10 @@ class EmailAgent applicationTitle, recipientName, recipientEmail, + mediaServerActionUrl: mediaServerUrl + ? `${mediaServerUrl}` + : undefined, + mediaServerName, }, }; } else if (payload.issue) { diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index 71360d63ab..9a378633c1 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -1,4 +1,5 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue'; +import { MediaServerType } from '@server/constants/server'; import type { NotificationAgentSlack } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; @@ -64,9 +65,29 @@ class SlackAgent payload: NotificationPayload ): SlackBlockEmbed { const settings = getSettings(); - const { applicationUrl, applicationTitle } = settings.main; + const { applicationUrl, applicationTitle, mediaServerType } = settings.main; const { embedPoster } = settings.notifications.agents.slack; + function getAvailableMediaServerName() { + if (mediaServerType === MediaServerType.EMBY) { + return 'Emby'; + } + + if (mediaServerType === MediaServerType.PLEX) { + return 'Plex'; + } + + return 'Jellyfin'; + } + + function getAvailableMediaServerUrl(): string | undefined { + const wants4k = payload.request?.is4k; + const url4k = (payload.media as any)?.mediaUrl4k as string | undefined; + const url = (payload.media as any)?.mediaUrl as string | undefined; + + return (wants4k ? (url4k ?? url) : (url ?? url4k)) || undefined; + } + const fields: EmbedField[] = []; if (payload.request) { @@ -206,6 +227,28 @@ class SlackAgent }); } + if (!payload.issue) { + const mediaServerName = getAvailableMediaServerName(); + const mediaServerUrl = getAvailableMediaServerUrl(); + + if (mediaServerUrl) { + blocks.push({ + type: 'actions', + elements: [ + { + action_id: 'open-in-mediaServer', + type: 'button', + url: mediaServerUrl, + text: { + type: 'plain_text', + text: `Play on ${mediaServerName}`, + }, + }, + ], + }); + } + } + return { text: payload.event ?? payload.subject, blocks, diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index cd54a86d63..d85409707b 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -1,5 +1,6 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue'; import { MediaStatus } from '@server/constants/media'; +import { MediaServerType } from '@server/constants/server'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; import type { NotificationAgentTelegram } from '@server/lib/settings'; @@ -66,9 +67,29 @@ class TelegramAgent payload: NotificationPayload ): Partial { const settings = getSettings(); - const { applicationUrl, applicationTitle } = settings.main; + const { applicationUrl, applicationTitle, mediaServerType } = settings.main; const { embedPoster } = settings.notifications.agents.telegram; + function getAvailableMediaServerName() { + if (mediaServerType === MediaServerType.EMBY) { + return 'Emby'; + } + + if (mediaServerType === MediaServerType.PLEX) { + return 'Plex'; + } + + return 'Jellyfin'; + } + + function getAvailableMediaServerUrl(): string | undefined { + const wants4k = payload.request?.is4k; + const url4k = (payload.media as any)?.mediaUrl4k as string | undefined; + const url = (payload.media as any)?.mediaUrl as string | undefined; + + return (wants4k ? (url4k ?? url) : (url ?? url4k)) || undefined; + } + /* eslint-disable no-useless-escape */ let message = `\*${this.escapeText( payload.event ? `${payload.event} - ${payload.subject}` : payload.subject @@ -142,6 +163,15 @@ class TelegramAgent payload.issue ? 'Issue' : 'Media' } in ${this.escapeText(applicationTitle)}\]\(${url}\)`; } + + if (!payload.issue) { + const mediaServerName = getAvailableMediaServerName(); + const mediaServerUrl = getAvailableMediaServerUrl(); + + if (mediaServerUrl) { + message += `\n\[Play on ${this.escapeText(mediaServerName)}\]\(${mediaServerUrl}\)`; + } + } /* eslint-enable */ return embedPoster && payload.image diff --git a/server/templates/email/media-request/html.pug b/server/templates/email/media-request/html.pug index bd7c9aa61e..57b3b91ba4 100644 --- a/server/templates/email/media-request/html.pug +++ b/server/templates/email/media-request/html.pug @@ -67,3 +67,9 @@ div(style='display: block; background-color: #111827; padding: 2.5rem 0;') a(href=actionUrl style='display: block; margin: 1.5rem 3rem 0; text-decoration: none; font-size: 1.0em; line-height: 2.25em;') span(style='padding: 0.2rem; font-weight: 500; text-align: center; border-radius: 10px; background-color: rgb(99,102,241); color: #fff; display: block; border: 1px solid rgba(255,255,255,0.2);') | View Media in #{applicationTitle} + if mediaServerActionUrl + tr + td + a(href=mediaServerActionUrl style='display: block; margin: 1.5rem 3rem 0; text-decoration: none; font-size: 1.0em; line-height: 2.25em;') + span(style='padding: 0.2rem; font-weight: 500; text-align: center; border-radius: 10px; background-color: rgb(99,102,241); color: #fff; display: block; border: 1px solid rgba(255,255,255,0.2);') + | Play on #{mediaServerName} From cdd66ba6cad5bc8be7bb055e7897912ac7de862d Mon Sep 17 00:00:00 2001 From: JackW <53652452+JackW6809@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:10:32 +0000 Subject: [PATCH 2/9] feat(notifications): created a Helper to place functions in a single location Created a helper file, this makes it easier to make changes to the function that multiple different agents use in the same fashion. BREAKING CHANGE: Adding extra content to notifications closes #2104 --- server/lib/notifications/agents/email.ts | 31 +++------------- server/lib/notifications/agents/slack.ts | 29 +++------------ server/lib/notifications/agents/telegram.ts | 29 +++------------ server/utils/mediaServerHelper.ts | 41 +++++++++++++++++++++ 4 files changed, 59 insertions(+), 71 deletions(-) create mode 100644 server/utils/mediaServerHelper.ts diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 6dcc668576..71bf1dfb7a 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -1,12 +1,15 @@ import { IssueType, IssueTypeName } from '@server/constants/issue'; import { MediaType } from '@server/constants/media'; -import { MediaServerType } from '@server/constants/server'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; import PreparedEmail from '@server/lib/email'; import type { NotificationAgentEmail } from '@server/lib/settings'; import { NotificationAgentKey, getSettings } from '@server/lib/settings'; import logger from '@server/logger'; +import { + getAvailableMediaServerName, + getAvailableMediaServerUrl, +} from '@server/utils/mediaServerHelper'; import type { EmailOptions } from 'email-templates'; import path from 'path'; import validator from 'validator'; @@ -52,30 +55,8 @@ class EmailAgent const settings = getSettings(); const { applicationUrl, applicationTitle, mediaServerType } = settings.main; const { embedPoster } = settings.notifications.agents.email; - - function getAvailableMediaServerName() { - if (mediaServerType === MediaServerType.EMBY) { - return 'Emby'; - } - - if (mediaServerType === MediaServerType.PLEX) { - return 'Plex'; - } - - return 'Jellyfin'; - } - - const mediaServerName = getAvailableMediaServerName(); - - function getAvailableMediaServerUrl(): string | undefined { - const wants4k = payload.request?.is4k; - const url4k = (payload.media as any)?.mediaUrl4k as string | undefined; - const url = (payload.media as any)?.mediaUrl as string | undefined; - - return (wants4k ? (url4k ?? url) : (url ?? url4k)) || undefined; - } - - const mediaServerUrl = getAvailableMediaServerUrl(); + const mediaServerName = getAvailableMediaServerName(mediaServerType); + const mediaServerUrl = getAvailableMediaServerUrl(payload); if (type === Notification.TEST_NOTIFICATION) { return { diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index 9a378633c1..dcea9aed4b 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -1,8 +1,11 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue'; -import { MediaServerType } from '@server/constants/server'; import type { NotificationAgentSlack } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; +import { + getAvailableMediaServerName, + getAvailableMediaServerUrl, +} from '@server/utils/mediaServerHelper'; import axios from 'axios'; import { Notification, hasNotificationType } from '..'; import type { NotificationAgent, NotificationPayload } from './agent'; @@ -68,26 +71,6 @@ class SlackAgent const { applicationUrl, applicationTitle, mediaServerType } = settings.main; const { embedPoster } = settings.notifications.agents.slack; - function getAvailableMediaServerName() { - if (mediaServerType === MediaServerType.EMBY) { - return 'Emby'; - } - - if (mediaServerType === MediaServerType.PLEX) { - return 'Plex'; - } - - return 'Jellyfin'; - } - - function getAvailableMediaServerUrl(): string | undefined { - const wants4k = payload.request?.is4k; - const url4k = (payload.media as any)?.mediaUrl4k as string | undefined; - const url = (payload.media as any)?.mediaUrl as string | undefined; - - return (wants4k ? (url4k ?? url) : (url ?? url4k)) || undefined; - } - const fields: EmbedField[] = []; if (payload.request) { @@ -228,8 +211,8 @@ class SlackAgent } if (!payload.issue) { - const mediaServerName = getAvailableMediaServerName(); - const mediaServerUrl = getAvailableMediaServerUrl(); + const mediaServerName = getAvailableMediaServerName(mediaServerType); + const mediaServerUrl = getAvailableMediaServerUrl(payload); if (mediaServerUrl) { blocks.push({ diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index d85409707b..25109b3653 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -1,11 +1,14 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue'; import { MediaStatus } from '@server/constants/media'; -import { MediaServerType } from '@server/constants/server'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; import type { NotificationAgentTelegram } from '@server/lib/settings'; import { NotificationAgentKey, getSettings } from '@server/lib/settings'; import logger from '@server/logger'; +import { + getAvailableMediaServerName, + getAvailableMediaServerUrl, +} from '@server/utils/mediaServerHelper'; import axios from 'axios'; import { Notification, @@ -70,26 +73,6 @@ class TelegramAgent const { applicationUrl, applicationTitle, mediaServerType } = settings.main; const { embedPoster } = settings.notifications.agents.telegram; - function getAvailableMediaServerName() { - if (mediaServerType === MediaServerType.EMBY) { - return 'Emby'; - } - - if (mediaServerType === MediaServerType.PLEX) { - return 'Plex'; - } - - return 'Jellyfin'; - } - - function getAvailableMediaServerUrl(): string | undefined { - const wants4k = payload.request?.is4k; - const url4k = (payload.media as any)?.mediaUrl4k as string | undefined; - const url = (payload.media as any)?.mediaUrl as string | undefined; - - return (wants4k ? (url4k ?? url) : (url ?? url4k)) || undefined; - } - /* eslint-disable no-useless-escape */ let message = `\*${this.escapeText( payload.event ? `${payload.event} - ${payload.subject}` : payload.subject @@ -165,8 +148,8 @@ class TelegramAgent } if (!payload.issue) { - const mediaServerName = getAvailableMediaServerName(); - const mediaServerUrl = getAvailableMediaServerUrl(); + const mediaServerName = getAvailableMediaServerName(mediaServerType); + const mediaServerUrl = getAvailableMediaServerUrl(payload); if (mediaServerUrl) { message += `\n\[Play on ${this.escapeText(mediaServerName)}\]\(${mediaServerUrl}\)`; diff --git a/server/utils/mediaServerHelper.ts b/server/utils/mediaServerHelper.ts new file mode 100644 index 0000000000..bcd0016d32 --- /dev/null +++ b/server/utils/mediaServerHelper.ts @@ -0,0 +1,41 @@ +import { MediaServerType } from '@server/constants/server'; + +/** + * Returns the display name of the media server being used in the seerr installation + * If neither Emby nor Plex is detected it falls back to Jellyfin + * @param currentMediaServer The currently configured media server type from settings + * @returns Human readable media server name + */ +export function getAvailableMediaServerName( + currentMediaServer: MediaServerType +): string { + if (currentMediaServer === MediaServerType.EMBY) { + return 'Emby'; + } + + if (currentMediaServer === MediaServerType.PLEX) { + return 'Plex'; + } + + return 'Jellyfin'; +} + +/** + * This function returns the URL of the media directly in the media server + * Used later on in the email as a button to send directly to + * @param payload Notification payload containing media and request information + * @returns Media server URL or undefined if it's unavailable + */ +export function getAvailableMediaServerUrl(payload: { + request?: { is4k?: boolean }; + media?: { + mediaUrl?: string; + mediaUrl4k?: string; + }; +}): string | undefined { + const wants4k = payload.request?.is4k; + const url4k = (payload.media as any)?.mediaUrl4k as string | undefined; + const url = (payload.media as any)?.mediaUrl as string | undefined; + + return (wants4k ? (url4k ?? url) : (url ?? url4k)) || undefined; +} From 0f69b572010c18ec050ca261ec59aef10f5eb120 Mon Sep 17 00:00:00 2001 From: JackW <53652452+JackW6809@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:19:45 +0000 Subject: [PATCH 3/9] feat(notifications): updated casts as they were unnecessary Removed some casts from the getAvailableMediaServerUrl as they were unnecessary BREAKING CHANGE: Function updated closes #2104 --- server/utils/mediaServerHelper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/utils/mediaServerHelper.ts b/server/utils/mediaServerHelper.ts index bcd0016d32..8a0b379093 100644 --- a/server/utils/mediaServerHelper.ts +++ b/server/utils/mediaServerHelper.ts @@ -34,8 +34,8 @@ export function getAvailableMediaServerUrl(payload: { }; }): string | undefined { const wants4k = payload.request?.is4k; - const url4k = (payload.media as any)?.mediaUrl4k as string | undefined; - const url = (payload.media as any)?.mediaUrl as string | undefined; + const url4k = payload.media?.mediaUrl4k; + const url = payload.media?.mediaUrl; return (wants4k ? (url4k ?? url) : (url ?? url4k)) || undefined; } From e32541b0f3edc9de18133d8ec6018dfdc7b27385 Mon Sep 17 00:00:00 2001 From: JackW <53652452+JackW6809@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:45:43 +0000 Subject: [PATCH 4/9] feat(notifications): updated slack button behaviour In this commit, I updated the slack button behaviour based on the suggestion made by coderabbitai. This means that now the View Media in Seerr and Play on Plex is on the same line and handled in one variable. BREAKING CHANGE: Changing the way buttons are sent in Slack notifications Closes #2104 --- server/lib/notifications/agents/slack.ts | 55 ++++++++++++------------ 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index dcea9aed4b..afb63e44cb 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -191,22 +191,19 @@ class SlackAgent : undefined : undefined; + const actionElements: Element[] = []; + if (url) { - blocks.push({ - type: 'actions', - elements: [ - { - action_id: 'open-in-seerr', - type: 'button', - url, - text: { - type: 'plain_text', - text: `View ${ - payload.issue ? 'Issue' : 'Media' - } in ${applicationTitle}`, - }, - }, - ], + actionElements.push({ + action_id: 'open-in-seerr', + type: 'button', + url, + text: { + type: 'plain_text', + text: `View ${ + payload.issue ? 'Issue' : 'Media' + } in ${applicationTitle}`, + }, }); } @@ -215,23 +212,25 @@ class SlackAgent const mediaServerUrl = getAvailableMediaServerUrl(payload); if (mediaServerUrl) { - blocks.push({ - type: 'actions', - elements: [ - { - action_id: 'open-in-mediaServer', - type: 'button', - url: mediaServerUrl, - text: { - type: 'plain_text', - text: `Play on ${mediaServerName}`, - }, - }, - ], + actionElements.push({ + action_id: 'open-in-mediaServer', + type: 'button', + url: mediaServerUrl, + text: { + type: 'plain_text', + text: `Play on ${mediaServerName}`, + }, }); } } + if (actionElements.length > 0) { + blocks.push({ + type: 'actions', + elements: actionElements, + }); + } + return { text: payload.event ?? payload.subject, blocks, From a8d2b0ba64b3b404bfda0ab4ec19647f94eeaff2 Mon Sep 17 00:00:00 2001 From: JackW <53652452+JackW6809@users.noreply.github.com> Date: Sun, 15 Feb 2026 14:04:48 +0000 Subject: [PATCH 5/9] feat(notifications): added jsdoc info to the functions I am using --- server/lib/notifications/agents/email.ts | 15 +++++++++++++++ server/lib/notifications/agents/slack.ts | 12 ++++++++++++ server/lib/notifications/agents/telegram.ts | 13 +++++++++++++ 3 files changed, 40 insertions(+) diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 71bf1dfb7a..140072e778 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -46,6 +46,15 @@ class EmailAgent return false; } + /** + * + * @param type Type of notification being sent + * @param payload Notification context + * @param recipientEmail The recipient's email address + * @param recipientName The recipient's name + * @returns Email options to be used by the email creator, or undefined if it's not applicable + * @private + */ private buildMessage( type: Notification, payload: NotificationPayload, @@ -204,6 +213,12 @@ class EmailAgent return undefined; } + /** + * + * @param type The type of notification being sent + * @param payload Notification context + * @returns True if the notification has successfully sent, else returns False + */ public async send( type: Notification, payload: NotificationPayload diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index afb63e44cb..42c6751dd1 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -63,6 +63,12 @@ class SlackAgent return settings.notifications.agents.slack; } + /** + * + * @param type The type of notification being sent + * @param payload Notification context + * @returns The Slack embed payload + */ public buildEmbed( type: Notification, payload: NotificationPayload @@ -247,6 +253,12 @@ class SlackAgent return false; } + /** + * + * @param type The type of notification being sent + * @param payload Notification context + * @returns True if the notification was sent successfully, else returns False + */ public async send( type: Notification, payload: NotificationPayload diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index 25109b3653..ff780d9804 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -65,6 +65,13 @@ class TelegramAgent return text ? text.replace(/[_*[\]()~>#+=|{}.!-]/gi, (x) => '\\' + x) : ''; } + /** + * + * @param type The type of notification being sent + * @param payload Notification context + * @returns Telegram Notification Payload + * @private + */ private getNotificationPayload( type: Notification, payload: NotificationPayload @@ -169,6 +176,12 @@ class TelegramAgent }; } + /** + * + * @param type The type of notification being sent + * @param payload Notification payload + * @returns True if the notification was sent successfully, else returns false + */ public async send( type: Notification, payload: NotificationPayload From 70ad89ad5707f864a90e7d9a1634fc9327d78bb8 Mon Sep 17 00:00:00 2001 From: JackW <53652452+JackW6809@users.noreply.github.com> Date: Sun, 15 Feb 2026 14:47:39 +0000 Subject: [PATCH 6/9] docs(notifications): added docstrings for email, slack, and telegram agents --- server/lib/notifications/agents/email.ts | 16 +++++++++++++- server/lib/notifications/agents/slack.ts | 18 +++++++++++++++- server/lib/notifications/agents/telegram.ts | 24 +++++++++++++++++++-- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 140072e778..0a40b31e2c 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -21,6 +21,11 @@ class EmailAgent extends BaseAgent implements NotificationAgent { + /** + * Returns this agents notification settings + * Uses a cached copy when available + * @protected + */ protected getSettings(): NotificationAgentEmail { if (this.settings) { return this.settings; @@ -31,6 +36,10 @@ class EmailAgent return settings.notifications.agents.email; } + /** + * Determines whether this agent is able to send email notifications + * Returns true only if the agent is enabled and the required SMTP settings are configured + */ public shouldSend(): boolean { const settings = this.getSettings(); @@ -47,12 +56,15 @@ class EmailAgent } /** + * Builds the email payload for an email notification * + * For all notifications it has a button to take you to the media in Seerr + * For request notifications it includes a button that links to the media in the chosen media server * @param type Type of notification being sent * @param payload Notification context * @param recipientEmail The recipient's email address * @param recipientName The recipient's name - * @returns Email options to be used by the email creator, or undefined if it's not applicable + * @returns Email notification payload * @private */ private buildMessage( @@ -214,7 +226,9 @@ class EmailAgent } /** + * Sends an email notification to the recipients in the payload. * + * Respects per-user notification settings, and it validates the email address before sending the notification * @param type The type of notification being sent * @param payload Notification context * @returns True if the notification has successfully sent, else returns False diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index 42c6751dd1..2d62af76f3 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -53,6 +53,11 @@ class SlackAgent extends BaseAgent implements NotificationAgent { + /** + * Returns this agent's notification settings + * Uses a cached copy when available + * @protected + */ protected getSettings(): NotificationAgentSlack { if (this.settings) { return this.settings; @@ -64,7 +69,10 @@ class SlackAgent } /** + * Builds the Slack Embed for the Slack notification that will be sent * + * For all notifications it has a button to take you to the media in Seerr + * For request notifications it includes a button that links to the media in the chosen media server * @param type The type of notification being sent * @param payload Notification context * @returns The Slack embed payload @@ -243,6 +251,10 @@ class SlackAgent }; } + /** + * Determines whether this agent is able to send Slack notifications + * Returns true only if the agent is enabled and a webhook URL is configured + */ public shouldSend(): boolean { const settings = this.getSettings(); @@ -254,10 +266,14 @@ class SlackAgent } /** + * Sends a Slack notification to the configured Slack webhook + * + * Returns true when notifications are skipped, disabled/type not enabled, or when the webhook call succeeds + * Returns false only when the webhook call fails * * @param type The type of notification being sent * @param payload Notification context - * @returns True if the notification was sent successfully, else returns False + * @returns True if the notification was sent successfully, otherwise False */ public async send( type: Notification, diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index ff780d9804..71fc4d3051 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -41,6 +41,11 @@ class TelegramAgent { private baseUrl = 'https://api.telegram.org/'; + /** + * Returns this agent's notification settings. + * Uses a cached copy when available. + * @protected + */ protected getSettings(): NotificationAgentTelegram { if (this.settings) { return this.settings; @@ -51,6 +56,10 @@ class TelegramAgent return settings.notifications.agents.telegram; } + /** + * Determines whether this agent is able to send Telegram notifications + * Returns True only if Telegram notifications are enabled and the bot API token is filled in, else returns False + */ public shouldSend(): boolean { const settings = this.getSettings(); @@ -61,15 +70,25 @@ class TelegramAgent return false; } + /** + * Escapes text for Telegram MarkdownV2 + * Telegram requires some characters to be escaped to prevent any formatting issues + * @param text The unescaped input + * @returns Escaped text that is safe for MarkdownV2 + * @private + */ private escapeText(text: string | undefined): string { return text ? text.replace(/[_*[\]()~>#+=|{}.!-]/gi, (x) => '\\' + x) : ''; } /** + * Builds the Telegram notification payload that will be sent * + * For all notifications it has a button to take you to the media in Seerr + * For request notifications it includes a button that links to the media in the chosen media server * @param type The type of notification being sent * @param payload Notification context - * @returns Telegram Notification Payload + * @returns Telegram notification payload * @private */ private getNotificationPayload( @@ -177,10 +196,11 @@ class TelegramAgent } /** + * Sends the Telegram notification for the provided type using the payload * * @param type The type of notification being sent * @param payload Notification payload - * @returns True if the notification was sent successfully, else returns false + * @returns True if the notification was sent successfully, otherwise False */ public async send( type: Notification, From e2882177bfcefa386292f59dc3534ecfe9d8d300 Mon Sep 17 00:00:00 2001 From: JackW <53652452+JackW6809@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:41:10 +0000 Subject: [PATCH 7/9] docs(notifications): removed double space, added more info to jsdoc Removed a double space from JSDoc, added more into to JSDoc --- server/lib/notifications/agents/telegram.ts | 2 +- server/utils/mediaServerHelper.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index 71fc4d3051..599c82b750 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -42,7 +42,7 @@ class TelegramAgent private baseUrl = 'https://api.telegram.org/'; /** - * Returns this agent's notification settings. + * Returns this agent's notification settings. * Uses a cached copy when available. * @protected */ diff --git a/server/utils/mediaServerHelper.ts b/server/utils/mediaServerHelper.ts index 8a0b379093..0622332cc6 100644 --- a/server/utils/mediaServerHelper.ts +++ b/server/utils/mediaServerHelper.ts @@ -23,6 +23,12 @@ export function getAvailableMediaServerName( /** * This function returns the URL of the media directly in the media server * Used later on in the email as a button to send directly to + * + * Prefers the 4K URL when the request is marked as 4K and prefers the + * standard URL otherwise. If the preferred URL is unavailable, it falls + * back to the alternative resolution URL to ensure a playable link is + * returned wherever possible. + * * @param payload Notification payload containing media and request information * @returns Media server URL or undefined if it's unavailable */ From d313d7f767d58bcedd5c491daf3201aad0be9157 Mon Sep 17 00:00:00 2001 From: JackW <53652452+JackW6809@users.noreply.github.com> Date: Sun, 15 Feb 2026 16:31:55 +0000 Subject: [PATCH 8/9] feat(notifications): escaped ` and \ in telegram notifications Telegram requires escaping of ` and \ characters, currently does not. Coderabbit found this and I am implementing this change BREAKING CHANGE: Changes the regex formula for escaping characters for telegram --- server/lib/notifications/agents/email.ts | 4 +--- server/lib/notifications/agents/telegram.ts | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 0a40b31e2c..79fb10790a 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -168,9 +168,7 @@ class EmailAgent applicationTitle, recipientName, recipientEmail, - mediaServerActionUrl: mediaServerUrl - ? `${mediaServerUrl}` - : undefined, + mediaServerActionUrl: mediaServerUrl, mediaServerName, }, }; diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index 599c82b750..8fa1c2f418 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -78,7 +78,9 @@ class TelegramAgent * @private */ private escapeText(text: string | undefined): string { - return text ? text.replace(/[_*[\]()~>#+=|{}.!-]/gi, (x) => '\\' + x) : ''; + return text + ? text.replace(/[_*[\]()~`\\>#+=|{}.!-]/gi, (x) => '\\' + x) + : ''; } /** From bf0c4a3f786d9197543aa94befaf535ce4be2f5e Mon Sep 17 00:00:00 2001 From: JackW <53652452+JackW6809@users.noreply.github.com> Date: Sun, 1 Mar 2026 11:21:24 +0000 Subject: [PATCH 9/9] fix(email.ts, telegram.ts): updated escaping of chars for Telegram, update var location in emails In Telegram I have added more character escpaing to the message sent, in emails I have moved down the variables so they only get called and set when required --- server/lib/notifications/agents/email.ts | 8 +++++--- server/lib/notifications/agents/telegram.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 79fb10790a..63c2a98d93 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -74,10 +74,8 @@ class EmailAgent recipientName?: string ): EmailOptions | undefined { const settings = getSettings(); - const { applicationUrl, applicationTitle, mediaServerType } = settings.main; + const { applicationUrl, applicationTitle } = settings.main; const { embedPoster } = settings.notifications.agents.email; - const mediaServerName = getAvailableMediaServerName(mediaServerType); - const mediaServerUrl = getAvailableMediaServerUrl(payload); if (type === Notification.TEST_NOTIFICATION) { return { @@ -145,6 +143,10 @@ class EmailAgent break; } + const { mediaServerType } = settings.main; + const mediaServerName = getAvailableMediaServerName(mediaServerType); + const mediaServerUrl = getAvailableMediaServerUrl(payload); + return { template: path.join( __dirname, diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index 8fa1c2f418..dcba4b7b8e 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -180,7 +180,7 @@ class TelegramAgent const mediaServerUrl = getAvailableMediaServerUrl(payload); if (mediaServerUrl) { - message += `\n\[Play on ${this.escapeText(mediaServerName)}\]\(${mediaServerUrl}\)`; + message += `\n\[Play on ${this.escapeText(mediaServerName)}\]\(${mediaServerUrl.replace(/\)/g, '\\)')}\)`; } } /* eslint-enable */