Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add notification support #1381

Draft
wants to merge 53 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
b779129
add initial notification support to the server
jorenn92 Sep 30, 2024
0e0dcd4
intermediate commit
jorenn92 Oct 1, 2024
6f411ed
intermediate commit
jorenn92 Oct 7, 2024
a53e05f
add initial notification support to the server
jorenn92 Sep 30, 2024
1a3f16b
intermediate commit
jorenn92 Oct 1, 2024
be5165a
intermediate commit
jorenn92 Oct 7, 2024
c558af0
intermediate commit
jorenn92 Oct 11, 2024
d78bc8f
Merge branch 'notification-support' of github.com:jorenn92/Maintainer…
jorenn92 Oct 11, 2024
175d91e
intermediate commit
jorenn92 Oct 15, 2024
b44d815
Merge branch 'main' into notification-support
jorenn92 Nov 18, 2024
d23958b
Intermediate commit
jorenn92 Nov 18, 2024
b60b273
Merge branch 'main' into notification-support
jorenn92 Nov 18, 2024
dc23da3
chore: fixed openPgpEncrypt import
jorenn92 Nov 18, 2024
aea53a6
chore: Fix linter errors
jorenn92 Nov 18, 2024
781583c
chore: Added missing job & ui changes for the 'about to be removed' n…
jorenn92 Nov 19, 2024
01bf021
Merge branch 'notification-support' of github.com:jorenn92/Maintainer…
jorenn92 Nov 19, 2024
8dedd8a
Merge branch 'main' of github.com:jorenn92/Maintainerr into notificat…
jorenn92 Nov 25, 2024
36e1b78
chore: updated yarn.lock
jorenn92 Nov 25, 2024
502d280
chore: Removed base64 decode from webhook payloadString
jorenn92 Dec 5, 2024
76de2b6
feat: add initial notification support to the server
jorenn92 Sep 30, 2024
87bdc5e
intermediate commit
jorenn92 Oct 1, 2024
4f47715
intermediate commit
jorenn92 Oct 7, 2024
6785dc4
intermediate commit
jorenn92 Oct 11, 2024
e6760bc
add initial notification support to the server
jorenn92 Sep 30, 2024
05a47ea
intermediate commit
jorenn92 Oct 1, 2024
c980fa1
intermediate commit
jorenn92 Oct 7, 2024
c8a1734
intermediate commit
jorenn92 Oct 15, 2024
3e048ca
Intermediate commit
jorenn92 Nov 18, 2024
b9dc26c
chore: Added missing job & ui changes for the 'about to be removed' n…
jorenn92 Nov 19, 2024
56afb8e
feat: Add base path support (#1373)
benscobie Nov 18, 2024
7316f88
fix: Docker startup
benscobie Nov 18, 2024
79b60e4
chore: Fix release PR workflow condition
benscobie Nov 18, 2024
082b224
chore: fixed openPgpEncrypt import
jorenn92 Nov 18, 2024
81d27a6
chore: Fix linter errors
jorenn92 Nov 18, 2024
a7a7b96
chore(deps): bump @types/node from 22.8.7 to 22.9.0
dependabot[bot] Nov 18, 2024
1a5ed72
chore(deps): bump @nestjs/cli from 10.4.5 to 10.4.7
dependabot[bot] Nov 18, 2024
98d73f9
chore(deps-dev): bump tailwindcss from 3.4.14 to 3.4.15
dependabot[bot] Nov 18, 2024
66bb7c2
chore(deps-dev): bump eslint-config-next from 15.0.0 to 15.0.3
dependabot[bot] Nov 18, 2024
c5022f6
chore(deps-dev): bump monaco-editor from 0.51.0 to 0.52.0
dependabot[bot] Nov 18, 2024
08ab490
chore(deps-dev): bump @eslint/eslintrc from 3.1.0 to 3.2.0
dependabot[bot] Nov 18, 2024
9ef6757
chore(deps-dev): bump eslint from 9.12.0 to 9.15.0
dependabot[bot] Nov 18, 2024
e5c542b
chore: Ensure .sh files always have LF line endings
benscobie Nov 20, 2024
5ad757f
fix: Handling collections failure after multi arr
benscobie Nov 20, 2024
04c8411
fix: Sonarr media existence check in collection handling
benscobie Nov 20, 2024
50ef770
fix: __PATH_PREFIX__ not replaced when using user directive (#1394)
benscobie Nov 25, 2024
8334aeb
Merge branch 'notification-support' of github.com:jorenn92/Maintainer…
jorenn92 Jan 24, 2025
0ba1cdc
Merge branch 'main' of github.com:jorenn92/Maintainerr into notificat…
jorenn92 Jan 24, 2025
e784377
chore: finalize merge from main
jorenn92 Jan 24, 2025
fbb02d0
chore: fix formatting after merge
jorenn92 Jan 24, 2025
c4e1394
chore: Apply suggestions from code review
jorenn92 Jan 24, 2025
b4e5120
chore: Suggestions after code review
jorenn92 Jan 24, 2025
ef13134
Merge branch 'main' of github.com:jorenn92/Maintainerr into notificat…
jorenn92 Jan 24, 2025
fe406a6
Merge branch 'main' of github.com:jorenn92/Maintainerr into notificat…
jorenn92 Jan 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@
"axios": "^1.7.9",
"chalk": "^4.1.2",
"cron-validator": "^1.3.1",
"email-templates": "9.0.0",
"lodash": "^4.17.21",
"nest-winston": "^1.10.0",
"node-cache": "^5.1.2",
"nodemailer": "6.9.1",
"openpgp": "^5.7.0",
"plex-api": "^5.3.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
Expand All @@ -57,6 +60,7 @@
"@nestjs/cli": "^10.4.9",
"@nestjs/schematics": "^10.2.3",
"@nestjs/testing": "^10.4.15",
"@types/email-templates": "^10.0.4",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.14",
"@types/node": "^22",
Expand Down
9 changes: 8 additions & 1 deletion server/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { OverseerrApiService } from '../modules/api/overseerr-api/overseerr-api.
import ormConfig from './config/typeOrmConfig';
import { TautulliApiModule } from '../modules/api/tautulli-api/tautulli-api.module';
import { TautulliApiService } from '../modules/api/tautulli-api/tautulli-api.service';
import { NotificationsModule } from '../modules/notifications/notifications.module';
import { NotificationService } from '../modules/notifications/notifications.service';

@Module({
imports: [
Expand All @@ -29,6 +31,7 @@ import { TautulliApiService } from '../modules/api/tautulli-api/tautulli-api.ser
TautulliApiModule,
RulesModule,
CollectionsModule,
NotificationsModule,
],
controllers: [AppController],
providers: [AppService],
Expand All @@ -39,12 +42,16 @@ export class AppModule implements OnModuleInit {
private readonly plexApi: PlexApiService,
private readonly overseerApi: OverseerrApiService,
private readonly tautulliApi: TautulliApiService,
private readonly notificationService: NotificationService,
) {}
async onModuleInit() {
// Initialize stuff needing settings here.. Otherwise problems
// Initialize modules requiring settings
await this.settings.init();
await this.plexApi.initialize({});
await this.overseerApi.init();
await this.tautulliApi.init();

// intialize notification agents
await this.notificationService.registerConfiguredAgents();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddNotificationSettings1727693832830
implements MigrationInterface
{
name = 'AddNotificationSettings1727693832830';

public async up(queryRunner: QueryRunner): Promise<void> {
// Create notification table
await queryRunner.query(`
CREATE TABLE "notification" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"name" VARCHAR NOT NULL,
"agent" VARCHAR NOT NULL,
"enabled" BOOLEAN DEFAULT false,
"types" TEXT,
"options" TEXT NOT NULL
);
`);

// Create notification_rulegroup table with foreign key constraints directly
await queryRunner.query(`
CREATE TABLE "notification_rulegroup" (
"notificationId" INTEGER NOT NULL,
"rulegroupId" INTEGER NOT NULL,
PRIMARY KEY ("notificationId", "rulegroupId"),
FOREIGN KEY ("notificationId") REFERENCES "notification"("id") ON DELETE CASCADE,
FOREIGN KEY ("rulegroupId") REFERENCES "rule_group"("id") ON DELETE CASCADE
);
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
// Drop the tables in reverse order
await queryRunner.query(`DROP TABLE "notification_rulegroup"`);
await queryRunner.query(`DROP TABLE "notification"`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class NotificationSettingsAboutScale1732008945000
implements MigrationInterface
{
name = 'NotificationSettingsAboutScale1732008945000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "notification" ADD COLUMN "aboutScale" INTEGER NOT NULL DEFAULT 3;
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "notification" DROP COLUMN "aboutScale";
`);
}
}
142 changes: 87 additions & 55 deletions server/src/modules/collections/collection-worker.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { PlexMetadata } from '../api/plex-api/interfaces/media.interface';
import { EPlexDataType } from '../api/plex-api/enums/plex-data-type-enum';
import { TmdbIdService } from '../api/tmdb-api/tmdb-id.service';
import { TaskBase } from '../tasks/task.base';
import { NotificationService } from '../notifications/notifications.service';
import { NotificationType } from '../notifications/notifications-interfaces';

@Injectable()
export class CollectionWorkerService extends TaskBase {
Expand All @@ -37,6 +39,7 @@ export class CollectionWorkerService extends TaskBase {
private readonly settings: SettingsService,
private readonly tmdbIdService: TmdbIdService,
private readonly tmdbIdHelper: TmdbIdService,
private readonly notificationService: NotificationService,
) {
super(taskService);
}
Expand All @@ -46,76 +49,105 @@ export class CollectionWorkerService extends TaskBase {
}

public async execute() {
// check if another instance of this task is already running
if (await this.isRunning()) {
this.logger.log(
`Another instance of the ${this.name} task is currently running. Skipping this execution`,
);
return;
}

await super.execute();
try {
// check if another instance of this task is already running
if (await this.isRunning()) {
this.logger.log(
`Another instance of the ${this.name} task is currently running. Skipping this execution`,
);
return;
}

// wait 5 seconds to make sure we're not executing together with the rule handler
await new Promise((resolve) => setTimeout(resolve, 5000));
await this.notificationService.registerConfiguredAgents(true); // re-register notification agents, to avoid flukes
await super.execute();

// if we are, then wait..
await this.taskService.waitUntilTaskIsFinished('Rule Handler', this.name);
// wait 5 seconds to make sure we're not executing together with the rule handler
await new Promise((resolve) => setTimeout(resolve, 5000));

// Start actual task
const appStatus = await this.settings.testConnections();
// if we are, then wait..
await this.taskService.waitUntilTaskIsFinished('Rule Handler', this.name);

this.logger.log('Start handling all collections');
let handledCollections = 0;
if (appStatus) {
// loop over all active collections
const collections = await this.collectionRepo.find({
where: { isActive: true },
});
for (const collection of collections) {
this.infoLogger(`Handling collection '${collection.title}'`);
// Start actual task
const appStatus = await this.settings.testConnections();

const collectionMedia = await this.collectionMediaRepo.find({
where: {
collectionId: collection.id,
},
this.logger.log('Start handling all collections');
let handledCollections = 0;
if (appStatus) {
// loop over all active collections
const collections = await this.collectionRepo.find({
where: { isActive: true },
});
for (const collection of collections) {
this.infoLogger(`Handling collection '${collection.title}'`);

const dangerDate = new Date(
new Date().getTime() - +collection.deleteAfterDays * 86400000,
);
const collectionMedia = await this.collectionMediaRepo.find({
where: {
collectionId: collection.id,
},
});

for (const media of collectionMedia) {
// handle media addate <= due date
if (new Date(media.addDate) <= dangerDate) {
await this.handleMedia(collection, media);
handledCollections++;
const dangerDate = new Date(
new Date().getTime() - +collection.deleteAfterDays * 86400000,
);

const handledMediaForNotification = [];
for (const media of collectionMedia) {
// handle media addate <= due date
if (new Date(media.addDate) <= dangerDate) {
await this.handleMedia(collection, media);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I brought up in Discord here about a possible handleMedia improvement. We may want to notify on individual media handling failures at some point once we've figured out what to do there.

handledCollections++;
handledMediaForNotification.push({ plexId: media.plexId });
}
}
}

this.infoLogger(`Handling collection '${collection.title}' finished`);
}
if (handledCollections > 0) {
if (this.settings.overseerrConfigured()) {
setTimeout(() => {
this.overseerrApi.api
.post('/settings/jobs/availability-sync/run')
.then(() => {
this.infoLogger(
`All collections handled. Triggered Overseerr's availability-sync because media was altered`,
);
});
}, 7000);
// handle notification
if (handledMediaForNotification.length > 0) {
await this.notificationService.handleNotification(
NotificationType.MEDIA_HANDLED,
handledMediaForNotification,
collection.title,
);
}

this.infoLogger(`Handling collection '${collection.title}' finished`);
}
if (handledCollections > 0) {
if (this.settings.overseerrConfigured()) {
setTimeout(() => {
this.overseerrApi.api
.post('/settings/jobs/availability-sync/run')
.then(() => {
this.infoLogger(
`All collections handled. Triggered Overseerr's availability-sync because media was altered`,
);
});
}, 7000);
}
} else {
this.infoLogger(`All collections handled. No data was altered`);
}
} else {
this.infoLogger(`All collections handled. No data was altered`);
this.infoLogger(
'Not all applications are reachable.. Skipping collection handling',
);

// notify
await this.notificationService.handleNotification(
NotificationType.COLLECTION_HANDLING_FAILED,
undefined,
);
}
} else {
this.infoLogger(
'Not all applications are reachable.. Skipping collection handling',
await this.finish();
} catch (e) {
this.logger.error('An error occurred where handling collections');
this.logger.debug(e);

// notify
await this.notificationService.handleNotification(
NotificationType.COLLECTION_HANDLING_FAILED,
undefined,
);
}
await this.finish();
}

private async handleMedia(collection: Collection, media: CollectionMedia) {
Expand Down
4 changes: 4 additions & 0 deletions server/src/modules/collections/collections.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { Exclusion } from '../rules/entities/exclusion.entities';
import { CollectionLog } from '../collections/entities/collection_log.entities';
import { CollectionLogCleanerService } from '../collections/tasks/collection-log-cleaner.service';
import { TautulliApiModule } from '../api/tautulli-api/tautulli-api.module';
import { Notification } from '../notifications/entities/notification.entities';
import { NotificationService } from '../notifications/notifications.service';

@Module({
imports: [
Expand All @@ -25,6 +27,7 @@ import { TautulliApiModule } from '../api/tautulli-api/tautulli-api.module';
CollectionLog,
RuleGroup,
Exclusion,
Notification,
]),
OverseerrApiModule,
TautulliApiModule,
Expand All @@ -36,6 +39,7 @@ import { TautulliApiModule } from '../api/tautulli-api/tautulli-api.module';
CollectionsService,
CollectionWorkerService,
CollectionLogCleanerService,
NotificationService,
],
controllers: [CollectionsController],
exports: [CollectionsService],
Expand Down
2 changes: 2 additions & 0 deletions server/src/modules/collections/collections.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ICollection } from './interfaces/collection.interface';
import { Exclusion } from '../rules/entities/exclusion.entities';
import { CollectionLog } from '../../modules/collections/entities/collection_log.entities';
import { ECollectionLogType } from '../../modules/collections/entities/collection_log.entities';
import { NotificationService } from '../notifications/notifications.service';

interface addCollectionDbResponse {
id: number;
Expand Down Expand Up @@ -54,6 +55,7 @@ export class CollectionsService {
private readonly plexApi: PlexApiService,
private readonly tmdbApi: TmdbApiService,
private readonly tmdbIdHelper: TmdbIdService,
private readonly notificationService: NotificationService,
) {}

async getCollection(id?: number, title?: string) {
Expand Down
24 changes: 24 additions & 0 deletions server/src/modules/notifications/agents/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Notification } from '../entities/notification.entities';
import {
NotificationAgentConfig,
NotificationAgentKey,
NotificationType,
} from '../notifications-interfaces';

export interface NotificationPayload {
Copy link
Collaborator

@benscobie benscobie Dec 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to include some metadata in this payload where relevant, such as the collection name, and/or the media item name. The webhook agent only has access to the subject and message which isn't super useful.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's worth an exploration, i'll look into it.

event?: string;
subject: string;
notifySystem: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with this and the leftover conditions

image?: string;
message?: string;
extra?: { name: string; value: string }[];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a leftover from overseerr as I can't see it being used.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, also something that still needs cleaning up

}

export interface NotificationAgent {
notification: Notification;
shouldSend(): boolean;
send(type: NotificationType, payload: NotificationPayload): Promise<boolean>;
getIdentifier(): NotificationAgentKey;
getSettings(): NotificationAgentConfig;
getNotification(): Notification;
}
Loading
Loading