mirror of
https://github.com/sct/overseerr.git
synced 2025-12-25 08:14:29 +01:00
feat: notification framework
This commit is contained in:
@@ -20,6 +20,7 @@ import RadarrAPI from '../api/radarr';
|
||||
import logger from '../logger';
|
||||
import SeasonRequest from './SeasonRequest';
|
||||
import SonarrAPI from '../api/sonarr';
|
||||
import notificationManager, { Notification } from '../lib/notifications';
|
||||
|
||||
@Entity()
|
||||
export class MediaRequest {
|
||||
@@ -60,6 +61,22 @@ export class MediaRequest {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
@AfterInsert()
|
||||
private async notifyNewRequest() {
|
||||
if (this.status === MediaRequestStatus.PENDING) {
|
||||
const tmdb = new TheMovieDb();
|
||||
if (this.media.mediaType === MediaType.MOVIE) {
|
||||
const movie = await tmdb.getMovie({ movieId: this.media.tmdbId });
|
||||
notificationManager.sendNotification(Notification.MEDIA_ADDED, {
|
||||
subject: `New Request: ${movie.title}`,
|
||||
message: movie.overview,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||
username: this.requestedBy.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AfterUpdate()
|
||||
@AfterInsert()
|
||||
private async updateParentStatus() {
|
||||
|
||||
@@ -14,6 +14,8 @@ import { Session } from './entity/Session';
|
||||
import { getSettings } from './lib/settings';
|
||||
import logger from './logger';
|
||||
import { startJobs } from './job/schedule';
|
||||
import notificationManager from './lib/notifications';
|
||||
import DiscordAgent from './lib/notifications/agents/discord';
|
||||
|
||||
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
||||
|
||||
@@ -28,6 +30,9 @@ app
|
||||
// Load Settings
|
||||
getSettings().load();
|
||||
|
||||
// Register Notification Agents
|
||||
notificationManager.registerAgents([new DiscordAgent()]);
|
||||
|
||||
// Start Jobs
|
||||
startJobs();
|
||||
|
||||
|
||||
13
server/lib/notifications/agents/agent.ts
Normal file
13
server/lib/notifications/agents/agent.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Notification } from '..';
|
||||
|
||||
export interface NotificationPayload {
|
||||
subject: string;
|
||||
username?: string;
|
||||
image?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface NotificationAgent {
|
||||
shouldSend(type: Notification): boolean;
|
||||
send(type: Notification, payload: NotificationPayload): Promise<boolean>;
|
||||
}
|
||||
157
server/lib/notifications/agents/discord.ts
Normal file
157
server/lib/notifications/agents/discord.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import axios from 'axios';
|
||||
import { Notification } from '..';
|
||||
import logger from '../../../logger';
|
||||
import { getSettings } from '../../settings';
|
||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
enum EmbedColors {
|
||||
DEFAULT = 0,
|
||||
AQUA = 1752220,
|
||||
GREEN = 3066993,
|
||||
BLUE = 3447003,
|
||||
PURPLE = 10181046,
|
||||
GOLD = 15844367,
|
||||
ORANGE = 15105570,
|
||||
RED = 15158332,
|
||||
GREY = 9807270,
|
||||
DARKER_GREY = 8359053,
|
||||
NAVY = 3426654,
|
||||
DARK_AQUA = 1146986,
|
||||
DARK_GREEN = 2067276,
|
||||
DARK_BLUE = 2123412,
|
||||
DARK_PURPLE = 7419530,
|
||||
DARK_GOLD = 12745742,
|
||||
DARK_ORANGE = 11027200,
|
||||
DARK_RED = 10038562,
|
||||
DARK_GREY = 9936031,
|
||||
LIGHT_GREY = 12370112,
|
||||
DARK_NAVY = 2899536,
|
||||
LUMINOUS_VIVID_PINK = 16580705,
|
||||
DARK_VIVID_PINK = 12320855,
|
||||
}
|
||||
|
||||
interface DiscordImageEmbed {
|
||||
url?: string;
|
||||
proxy_url?: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
interface DiscordRichEmbed {
|
||||
title?: string;
|
||||
type?: 'rich'; // Always rich for webhooks
|
||||
description?: string;
|
||||
url?: string;
|
||||
timestamp?: string;
|
||||
color?: number;
|
||||
footer?: {
|
||||
text: string;
|
||||
icon_url?: string;
|
||||
proxy_icon_url?: string;
|
||||
};
|
||||
image?: DiscordImageEmbed;
|
||||
thumbnail?: DiscordImageEmbed;
|
||||
provider?: {
|
||||
name?: string;
|
||||
url?: string;
|
||||
};
|
||||
author?: {
|
||||
name?: string;
|
||||
url?: string;
|
||||
icon_url?: string;
|
||||
proxy_icon_url?: string;
|
||||
};
|
||||
fields?: {
|
||||
name: string;
|
||||
value: string;
|
||||
inline?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface DiscordWebhookPayload {
|
||||
embeds: DiscordRichEmbed[];
|
||||
username: string;
|
||||
avatar_url?: string;
|
||||
tts: boolean;
|
||||
}
|
||||
|
||||
class DiscordAgent implements NotificationAgent {
|
||||
public buildEmbed(
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): DiscordRichEmbed {
|
||||
let color = EmbedColors.DEFAULT;
|
||||
|
||||
switch (type) {
|
||||
case Notification.MEDIA_ADDED:
|
||||
color = EmbedColors.ORANGE;
|
||||
}
|
||||
|
||||
return {
|
||||
title: payload.subject,
|
||||
description: payload.message,
|
||||
color,
|
||||
timestamp: new Date().toISOString(),
|
||||
author: { name: 'Overseerr' },
|
||||
fields: [
|
||||
{
|
||||
name: 'Requested By',
|
||||
value: payload.username ?? '',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Status',
|
||||
value: 'Pending Approval',
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
thumbnail: {
|
||||
url: payload.image,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public shouldSend(type: Notification): boolean {
|
||||
const settings = getSettings();
|
||||
|
||||
if (
|
||||
settings.notifications.agents.discord?.enabled &&
|
||||
settings.notifications.agents.discord?.options?.webhookUrl
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async send(
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
const settings = getSettings();
|
||||
logger.debug('Sending discord notification', { label: 'Notifications' });
|
||||
try {
|
||||
const webhookUrl = settings.notifications.agents.discord?.options
|
||||
?.webhookUrl as string;
|
||||
|
||||
if (!webhookUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl, {
|
||||
username: 'Overseerr',
|
||||
embeds: [this.buildEmbed(type, payload)],
|
||||
} as DiscordWebhookPayload);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.error('Error sending Discord notification', {
|
||||
label: 'Notifications',
|
||||
message: e.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DiscordAgent;
|
||||
33
server/lib/notifications/index.ts
Normal file
33
server/lib/notifications/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import logger from '../../logger';
|
||||
import type { NotificationAgent, NotificationPayload } from './agents/agent';
|
||||
|
||||
export enum Notification {
|
||||
MEDIA_ADDED = 2,
|
||||
}
|
||||
|
||||
class NotificationManager {
|
||||
private activeAgents: NotificationAgent[] = [];
|
||||
|
||||
public registerAgents = (agents: NotificationAgent[]): void => {
|
||||
this.activeAgents = [...this.activeAgents, ...agents];
|
||||
logger.info('Registered Notification Agents', { label: 'Notifications' });
|
||||
};
|
||||
|
||||
public sendNotification(
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): void {
|
||||
logger.info(`Sending notification for ${Notification[type]}`, {
|
||||
label: 'Notifications',
|
||||
});
|
||||
this.activeAgents.forEach((agent) => {
|
||||
if (agent.shouldSend(type)) {
|
||||
agent.send(type, payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const notificationManager = new NotificationManager();
|
||||
|
||||
export default notificationManager;
|
||||
@@ -50,6 +50,16 @@ interface PublicSettings {
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
interface NotificationAgent {
|
||||
enabled: boolean;
|
||||
types: number;
|
||||
options: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface NotificationSettings {
|
||||
agents: Record<string, NotificationAgent>;
|
||||
}
|
||||
|
||||
interface AllSettings {
|
||||
clientId?: string;
|
||||
main: MainSettings;
|
||||
@@ -57,6 +67,7 @@ interface AllSettings {
|
||||
radarr: RadarrSettings[];
|
||||
sonarr: SonarrSettings[];
|
||||
public: PublicSettings;
|
||||
notifications: NotificationSettings;
|
||||
}
|
||||
|
||||
const SETTINGS_PATH = path.join(__dirname, '../../config/settings.json');
|
||||
@@ -80,6 +91,17 @@ class Settings {
|
||||
public: {
|
||||
initialized: false,
|
||||
},
|
||||
notifications: {
|
||||
agents: {
|
||||
discord: {
|
||||
enabled: false,
|
||||
types: 0,
|
||||
options: {
|
||||
webhookUrl: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
if (initialSettings) {
|
||||
Object.assign<AllSettings, AllSettings>(this.data, initialSettings);
|
||||
@@ -126,6 +148,14 @@ class Settings {
|
||||
this.data.public = data;
|
||||
}
|
||||
|
||||
get notifications(): NotificationSettings {
|
||||
return this.data.notifications;
|
||||
}
|
||||
|
||||
set notifications(data: NotificationSettings) {
|
||||
this.data.notifications = data;
|
||||
}
|
||||
|
||||
get clientId(): string {
|
||||
if (!this.data.clientId) {
|
||||
this.data.clientId = uuidv4();
|
||||
@@ -156,6 +186,7 @@ class Settings {
|
||||
|
||||
if (data) {
|
||||
this.data = Object.assign(this.data, JSON.parse(data));
|
||||
this.save();
|
||||
}
|
||||
return this.data;
|
||||
}
|
||||
|
||||
@@ -349,4 +349,19 @@ settingsRoutes.get(
|
||||
}
|
||||
);
|
||||
|
||||
settingsRoutes.get('/notifications/discord', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.discord);
|
||||
});
|
||||
|
||||
settingsRoutes.post('/notifications/discord', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.discord = req.body;
|
||||
settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.discord);
|
||||
});
|
||||
|
||||
export default settingsRoutes;
|
||||
|
||||
Reference in New Issue
Block a user