diff --git a/server/api/musicbrainz/index.ts b/server/api/musicbrainz/index.ts index ab251cd0..75f1e5fd 100644 --- a/server/api/musicbrainz/index.ts +++ b/server/api/musicbrainz/index.ts @@ -66,7 +66,7 @@ class MusicBrainz extends ExternalAPI { public async getAlbum({ albumId, }: { - albumId: string; + albumId?: string; }): Promise { try { const data = await this.get( diff --git a/server/lib/notifications/agents/webpush.ts b/server/lib/notifications/agents/webpush.ts index fcb7416e..d44abb39 100644 --- a/server/lib/notifications/agents/webpush.ts +++ b/server/lib/notifications/agents/webpush.ts @@ -47,7 +47,9 @@ class WebPushAgent const mediaType = payload.media ? payload.media.mediaType === MediaType.MOVIE ? 'movie' - : 'series' + : payload.media.mediaType === MediaType.TV + ? 'series' + : 'album' : undefined; const is4k = payload.request?.is4k; @@ -119,7 +121,9 @@ class WebPushAgent const actionUrl = payload.issue ? `/issues/${payload.issue.id}` : payload.media - ? `/${payload.media.mediaType}/${payload.media.tmdbId}` + ? payload.media.mediaType === MediaType.MUSIC + ? `/music/${payload.media.mbId}` + : `/${payload.media.mediaType}/${payload.media.tmdbId}` : undefined; const actionUrlTitle = actionUrl diff --git a/server/subscriber/MediaRequestSubscriber.ts b/server/subscriber/MediaRequestSubscriber.ts index a8742990..2c6985c8 100644 --- a/server/subscriber/MediaRequestSubscriber.ts +++ b/server/subscriber/MediaRequestSubscriber.ts @@ -4,6 +4,8 @@ import type { AddSeriesOptions, SonarrSeries, } from '@server/api/servarr/sonarr'; +import LidarrAPI from '@server/api/servarr/lidarr'; +import MusicBrainz from '@server/api/musicbrainz' import SonarrAPI from '@server/api/servarr/sonarr'; import TheMovieDb from '@server/api/themoviedb'; import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants'; @@ -26,6 +28,7 @@ import type { InsertEvent, RemoveEvent, UpdateEvent, + Not } from 'typeorm'; import { EventSubscriber } from 'typeorm'; @@ -171,6 +174,65 @@ export class MediaRequestSubscriber } } + private async notifyAvailableMusic( + entity: MediaRequest, + event?: UpdateEvent + ) { + + // Get fresh media state using event manager + let latestMedia: Media | null = null; + if (event?.manager) { + latestMedia = await event.manager.findOne(Media, { + where: { id: entity.media.id }, + }); + } + if (!latestMedia) { + const mediaRepository = getRepository(Media); + latestMedia = await mediaRepository.findOne({ + where: { id: entity.media.id }, + }); + + if(!latestMedia + || latestMedia.mediaType !== MediaType.MUSIC + || latestMedia['status'] != MediaStatus.AVAILABLE) + { + return + } + + try { + const musicbrainz = new MusicBrainz(); + const albumDetails = await musicbrainz.getAlbum({ + albumId: latestMedia.mbId, + }); + + const coverImage = albumDetails.images?.find( + (img) => img.CoverType.toLowerCase() === 'cover' + )?.Url; + + notificationManager.sendNotification( + Notification.MEDIA_AVAILABLE, + { + event: `Album Request Now Available`, + notifyAdmin: false, + notifySystem: true, + notifyUser: entity.requestedBy, + subject: albumDetails.title ?? latestMedia.mbId ?? 'Unknown Album', + message: albumDetails.overview || 'Album is now available.', + media: latestMedia, + request: entity, + image: coverImage, + } + ); + } catch (e) { + logger.error('Something went wrong sending media notification(s)', { + label: 'Notifications', + errorMessage: e.message, + mediaId: entity.id, + }); + } + } + } + public async sendToRadarr(entity: MediaRequest): Promise { if ( entity.status === MediaRequestStatus.APPROVED && @@ -738,10 +800,10 @@ export class MediaRequestSubscriber } } - public async sendToLidarr(): Promise { + public async sendToLidarr(entity: MediaRequest): Promise { if ( - this.status !== MediaRequestStatus.APPROVED || - this.type !== MediaType.MUSIC + entity.status !== MediaRequestStatus.APPROVED || + entity.type !== MediaType.MUSIC ) { return; } @@ -750,7 +812,7 @@ export class MediaRequestSubscriber const mediaRepository = getRepository(Media); const settings = getSettings(); const media = await mediaRepository.findOne({ - where: { id: this.media.id }, + where: { id: entity.media.id }, relations: { requests: true }, }); @@ -759,21 +821,21 @@ export class MediaRequestSubscriber } const lidarrSettings = - this.serverId !== null && this.serverId >= 0 - ? settings.lidarr.find((l) => l.id === this.serverId) + entity.serverId !== null && entity.serverId >= 0 + ? settings.lidarr.find((l) => l.id === entity.serverId) : settings.lidarr.find((l) => l.isDefault); if (!lidarrSettings) { logger.warn('No valid Lidarr server configured', { label: 'Media Request', - requestId: this.id, - mediaId: this.media.id, + requestId: entity.id, + mediaId: entity.media.id, }); return; } - const rootFolder = this.rootFolder || lidarrSettings.activeDirectory; - const qualityProfile = this.profileId || lidarrSettings.activeProfileId; + const rootFolder = entity.rootFolder || lidarrSettings.activeDirectory; + const qualityProfile = entity.profileId || lidarrSettings.activeProfileId; const tags = lidarrSettings.tags?.map((t) => t.toString()) || []; const lidarr = new LidarrAPI({ @@ -881,26 +943,26 @@ export class MediaRequestSubscriber logger.error('Failed final album monitoring check', { label: 'Media Request', error: err.message, - requestId: this.id, - mediaId: this.media.id, - albumId: album.id, + requestId: entity.id, + mediaId: entity.media.id, + albumId: entity.id, }); } }, 20000); logger.info('Completed album monitoring setup', { label: 'Media Request', - requestId: this.id, - mediaId: this.media.id, - albumId: album.id, + requestId: entity.id, + mediaId: entity.media.id, + albumId: entity.id, }); } catch (err) { logger.error('Failed to process album monitoring', { label: 'Media Request', error: err.message, - requestId: this.id, - mediaId: this.media.id, - albumId: album.id, + requestId: entity.id, + mediaId: entity.media.id, + albumId: entity.id, }); } }, 60000); @@ -934,8 +996,8 @@ export class MediaRequestSubscriber logger.error('Failed to process existing album', { label: 'Media Request', error: err.message, - requestId: this.id, - mediaId: this.media.id, + requestId: entity.id, + mediaId: entity.media.id, albumId: existingAlbum.id, }); } @@ -943,9 +1005,9 @@ export class MediaRequestSubscriber } } else { const requestRepository = getRepository(MediaRequest); - this.status = MediaRequestStatus.FAILED; + entity.status = MediaRequestStatus.FAILED; await requestRepository.save(this); - this.sendNotification(media, Notification.MEDIA_FAILED); + MediaRequest.sendNotification(entity, media, Notification.MEDIA_FAILED); throw error; } } @@ -953,8 +1015,8 @@ export class MediaRequestSubscriber logger.error('Failed to process Lidarr request', { label: 'Media Request', error: e.message, - requestId: this.id, - mediaId: this.media.id, + requestId: entity.id, + mediaId: entity.media.id, }); } } @@ -1060,6 +1122,7 @@ export class MediaRequestSubscriber this.sendToRadarr(event.entity as MediaRequest); this.sendToSonarr(event.entity as MediaRequest); + this.sendToLidarr(event.entity as MediaRequest); this.updateParentStatus(event.entity as MediaRequest); @@ -1070,6 +1133,9 @@ export class MediaRequestSubscriber if (event.entity.media.mediaType === MediaType.TV) { this.notifyAvailableSeries(event.entity as MediaRequest, event); } + if (event.entity.media.mediaType === MediaType.MUSIC) { + this.notifyAvailableMusic(event.entity as MediaRequest, event); + } } } @@ -1080,6 +1146,7 @@ export class MediaRequestSubscriber this.sendToRadarr(event.entity as MediaRequest); this.sendToSonarr(event.entity as MediaRequest); + this.sendToLidarr(event.entity as MediaRequest); this.updateParentStatus(event.entity as MediaRequest); } diff --git a/server/subscriber/MediaSubscriber.ts b/server/subscriber/MediaSubscriber.ts index 3cf8229f..1f860940 100644 --- a/server/subscriber/MediaSubscriber.ts +++ b/server/subscriber/MediaSubscriber.ts @@ -109,6 +109,11 @@ export class MediaSubscriber implements EntitySubscriberInterface { const allSeasonsReady = allSeasonResults.every((result) => result); shouldComplete = allSeasonsReady; + } else if (event.mediaType === MediaType.MUSIC) + { + if(event['status'] == MediaStatus.AVAILABLE || event['status'] === MediaStatus.DELETED) { + shouldComplete = true; + } } if (shouldComplete) {