From fe37a1de98c6c9beac0f428ae2f9b0dedf6a4476 Mon Sep 17 00:00:00 2001 From: Pierre <63404022+0-Pierre@users.noreply.github.com> Date: Sat, 18 Jan 2025 14:31:31 +0100 Subject: [PATCH] fix: resolved issues with the music slider displaying all menus, and ensured media are properly removed from Lidarr. --- server/entity/Media.ts | 2 +- server/lib/scanners/baseScanner.ts | 62 ++++-- server/lib/scanners/lidarr/index.ts | 7 +- server/routes/music.ts | 13 +- server/subscriber/MediaRequestSubscriber.ts | 220 ++++++++++++++++++++ src/components/ManageSlideOver/index.tsx | 16 +- src/components/RequestBlock/index.tsx | 31 +-- 7 files changed, 303 insertions(+), 48 deletions(-) diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 34bb82d1..e9bed8b5 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -171,7 +171,7 @@ class Media { }) public mediaAddedAt: Date; - @Column({ nullable: true, type: 'int' }) + @Column({ nullable: false, type: 'int', default: 0 }) public serviceId?: number | null; @Column({ nullable: true, type: 'int' }) diff --git a/server/lib/scanners/baseScanner.ts b/server/lib/scanners/baseScanner.ts index 2b93c441..a498cd62 100644 --- a/server/lib/scanners/baseScanner.ts +++ b/server/lib/scanners/baseScanner.ts @@ -566,26 +566,13 @@ class BaseScanner { ? MediaStatus.PROCESSING : MediaStatus.AVAILABLE; newMedia.mediaType = MediaType.MUSIC; - - if (mediaAddedAt) { - newMedia.mediaAddedAt = mediaAddedAt; - } - - if (ratingKey) { - newMedia.ratingKey = ratingKey; - } - - if (serviceId) { - newMedia.serviceId = serviceId; - } - - if (externalServiceId) { - newMedia.externalServiceId = externalServiceId; - } - - if (externalServiceSlug) { - newMedia.externalServiceSlug = externalServiceSlug; - } + newMedia.mediaAddedAt = mediaAddedAt ?? newMedia.mediaAddedAt; + newMedia.ratingKey = ratingKey ?? newMedia.ratingKey; + newMedia.serviceId = serviceId ?? newMedia.serviceId; + newMedia.externalServiceId = + externalServiceId ?? newMedia.externalServiceId; + newMedia.externalServiceSlug = + externalServiceSlug ?? newMedia.externalServiceSlug; try { await mediaRepository.save(newMedia); @@ -596,6 +583,41 @@ class BaseScanner { error: err.message, }); } + } else { + let hasChanges = false; + + if (serviceId && !existing.serviceId) { + existing.serviceId = serviceId; + hasChanges = true; + } + if (externalServiceId && !existing.externalServiceId) { + existing.externalServiceId = externalServiceId; + hasChanges = true; + } + if (externalServiceSlug && !existing.externalServiceSlug) { + existing.externalServiceSlug = externalServiceSlug; + hasChanges = true; + } + if (mediaAddedAt && !existing.mediaAddedAt) { + existing.mediaAddedAt = mediaAddedAt; + hasChanges = true; + } + if (ratingKey && !existing.ratingKey) { + existing.ratingKey = ratingKey; + hasChanges = true; + } + + if (hasChanges) { + try { + await mediaRepository.save(existing); + this.log(`Updated existing media: ${title}`); + } catch (err) { + this.log('Failed to update existing media', 'error', { + title, + error: err.message, + }); + } + } } }); } diff --git a/server/lib/scanners/lidarr/index.ts b/server/lib/scanners/lidarr/index.ts index 8367c5cd..d156aeae 100644 --- a/server/lib/scanners/lidarr/index.ts +++ b/server/lib/scanners/lidarr/index.ts @@ -41,7 +41,6 @@ class LidarrScanner const sessionId = this.startRun(); try { - // Filter out duplicate servers this.servers = uniqWith(settings.lidarr, (lidarrA, lidarrB) => { return ( lidarrA.hostname === lidarrB.hostname && @@ -81,14 +80,10 @@ class LidarrScanner private async processLidarrAlbum(lidarrAlbum: LidarrAlbum): Promise { try { if (!lidarrAlbum.monitored) { - this.log('Title is unmonitored. Skipping item.', 'debug', { - title: lidarrAlbum.title, - }); return; } const mbId = lidarrAlbum.foreignAlbumId; - if (!mbId) { this.log( 'No MusicBrainz ID found for this title. Skipping item.', @@ -103,7 +98,7 @@ class LidarrScanner await this.processMusic(mbId, { serviceId: this.currentServer.id, externalServiceId: lidarrAlbum.id, - externalServiceSlug: lidarrAlbum.titleSlug, + externalServiceSlug: mbId, title: lidarrAlbum.title, processing: lidarrAlbum.monitored && diff --git a/server/routes/music.ts b/server/routes/music.ts index 4c7db786..12778f2c 100644 --- a/server/routes/music.ts +++ b/server/routes/music.ts @@ -26,12 +26,15 @@ musicRoutes.get('/:id', async (req, res, next) => { const [media, onUserWatchlist] = await Promise.all([ getRepository(Media) - .findOne({ - where: { - mbId: req.params.id, - mediaType: MediaType.MUSIC, - }, + .createQueryBuilder('media') + .leftJoinAndSelect('media.requests', 'requests') + .leftJoinAndSelect('requests.requestedBy', 'requestedBy') + .leftJoinAndSelect('requests.modifiedBy', 'modifiedBy') + .where({ + mbId: req.params.id, + mediaType: MediaType.MUSIC, }) + .getOne() .then((media) => media ?? undefined), getRepository(Watchlist).exist({ diff --git a/server/subscriber/MediaRequestSubscriber.ts b/server/subscriber/MediaRequestSubscriber.ts index 5c1b9bb0..a8742990 100644 --- a/server/subscriber/MediaRequestSubscriber.ts +++ b/server/subscriber/MediaRequestSubscriber.ts @@ -738,6 +738,226 @@ export class MediaRequestSubscriber } } + public async sendToLidarr(): Promise { + if ( + this.status !== MediaRequestStatus.APPROVED || + this.type !== MediaType.MUSIC + ) { + return; + } + + try { + const mediaRepository = getRepository(Media); + const settings = getSettings(); + const media = await mediaRepository.findOne({ + where: { id: this.media.id }, + relations: { requests: true }, + }); + + if (!media?.mbId) { + throw new Error('Media data or MusicBrainz ID not found'); + } + + const lidarrSettings = + this.serverId !== null && this.serverId >= 0 + ? settings.lidarr.find((l) => l.id === this.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, + }); + return; + } + + const rootFolder = this.rootFolder || lidarrSettings.activeDirectory; + const qualityProfile = this.profileId || lidarrSettings.activeProfileId; + const tags = lidarrSettings.tags?.map((t) => t.toString()) || []; + + const lidarr = new LidarrAPI({ + apiKey: lidarrSettings.apiKey, + url: LidarrAPI.buildUrl(lidarrSettings, '/api/v1'), + }); + + const lidarrAlbum = await lidarr.getAlbumByMusicBrainzId(media.mbId); + + let artistId: number; + try { + const existingArtist = await lidarr.getArtistByMusicBrainzId( + lidarrAlbum.artist.foreignArtistId + ); + artistId = existingArtist.id; + } catch { + const addedArtist = await lidarr.addArtist({ + artistName: lidarrAlbum.artist.artistName, + foreignArtistId: lidarrAlbum.artist.foreignArtistId, + qualityProfileId: qualityProfile, + profileId: qualityProfile, + metadataProfileId: qualityProfile, + rootFolderPath: rootFolder, + monitored: false, + tags: tags.map((t) => Number(t)), + searchNow: !lidarrSettings.preventSearch, + monitorNewItems: 'none', + monitor: 'none', + searchForMissingAlbums: false, + addOptions: { + monitor: 'none', + monitored: false, + searchForMissingAlbums: false, + }, + }); + + await new Promise((resolve) => setTimeout(resolve, 60000)); + artistId = addedArtist.id; + } + + try { + const album = await lidarr.addAlbum({ + mbId: media.mbId, + foreignAlbumId: media.mbId, + title: lidarrAlbum.title, + qualityProfileId: qualityProfile, + profileId: qualityProfile, + metadataProfileId: qualityProfile, + rootFolderPath: rootFolder, + monitored: false, + tags, + searchNow: false, + artistId, + images: lidarrAlbum.images?.length + ? lidarrAlbum.images + : [ + { + url: '', + coverType: 'cover', + }, + ], + addOptions: { + monitor: 'none', + monitored: false, + searchForMissingAlbums: false, + }, + artist: { + id: artistId, + foreignArtistId: lidarrAlbum.artist.foreignArtistId, + artistName: lidarrAlbum.artist.artistName, + qualityProfileId: qualityProfile, + metadataProfileId: qualityProfile, + rootFolderPath: rootFolder, + monitored: false, + monitorNewItems: 'none', + }, + }); + + media.externalServiceId = album.id; + (media.externalServiceSlug = media.mbId), + (media.serviceId = lidarrSettings.id); + media.status = MediaStatus.PROCESSING; + await mediaRepository.save(media); + setTimeout(async () => { + try { + const albumDetails = await lidarr.getAlbum({ id: album.id }); + albumDetails.monitored = true; + await lidarr.updateAlbum(albumDetails); + + if (!lidarrSettings.preventSearch) { + await lidarr.searchAlbum(album.id); + } + + setTimeout(async () => { + try { + const finalAlbumDetails = await lidarr.getAlbum({ + id: album.id, + }); + if (!finalAlbumDetails.monitored) { + finalAlbumDetails.monitored = true; + await lidarr.updateAlbum(finalAlbumDetails); + await lidarr.searchAlbum(album.id); + } + } catch (err) { + logger.error('Failed final album monitoring check', { + label: 'Media Request', + error: err.message, + requestId: this.id, + mediaId: this.media.id, + albumId: album.id, + }); + } + }, 20000); + + logger.info('Completed album monitoring setup', { + label: 'Media Request', + requestId: this.id, + mediaId: this.media.id, + albumId: album.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, + }); + } + }, 60000); + } catch (error) { + if (error.message.includes('This album has already been added')) { + const existingAlbums = await lidarr.getAlbums(); + const existingAlbum = existingAlbums.find( + (a) => a.foreignAlbumId === media.mbId + ); + + if (existingAlbum) { + media.externalServiceId = existingAlbum.id; + media.externalServiceSlug = media.mbId; + media.serviceId = lidarrSettings.id; + media.status = MediaStatus.PROCESSING; + await mediaRepository.save(media); + + setTimeout(async () => { + try { + await new Promise((resolve) => setTimeout(resolve, 20000)); + const albumDetails = await lidarr.getAlbum({ + id: existingAlbum.id, + }); + albumDetails.monitored = true; + await lidarr.updateAlbum(albumDetails); + + if (!lidarrSettings.preventSearch) { + await lidarr.searchAlbum(existingAlbum.id); + } + } catch (err) { + logger.error('Failed to process existing album', { + label: 'Media Request', + error: err.message, + requestId: this.id, + mediaId: this.media.id, + albumId: existingAlbum.id, + }); + } + }, 0); + } + } else { + const requestRepository = getRepository(MediaRequest); + this.status = MediaRequestStatus.FAILED; + await requestRepository.save(this); + this.sendNotification(media, Notification.MEDIA_FAILED); + throw error; + } + } + } catch (e) { + logger.error('Failed to process Lidarr request', { + label: 'Media Request', + error: e.message, + requestId: this.id, + mediaId: this.media.id, + }); + } + } public async updateParentStatus(entity: MediaRequest): Promise { const mediaRepository = getRepository(Media); const media = await mediaRepository.findOne({ diff --git a/src/components/ManageSlideOver/index.tsx b/src/components/ManageSlideOver/index.tsx index b9a9b953..9459a759 100644 --- a/src/components/ManageSlideOver/index.tsx +++ b/src/components/ManageSlideOver/index.tsx @@ -68,6 +68,7 @@ const messages = defineMessages('components.ManageSlideOver', { playedby: 'Played By', movie: 'movie', tvshow: 'series', + album: 'album', }); const isMovie = ( @@ -515,9 +516,16 @@ const ManageSlideOver = ({ mediaType: intl.formatMessage( mediaType === 'movie' ? messages.movie + : mediaType === 'music' + ? messages.album : messages.tvshow ), - arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr', + arr: + mediaType === 'movie' + ? 'Radarr' + : mediaType === 'music' + ? 'Lidarr' + : 'Sonarr', } )} @@ -742,7 +750,11 @@ const ManageSlideOver = ({
{intl.formatMessage(messages.manageModalClearMediaWarning, { mediaType: intl.formatMessage( - mediaType === 'movie' ? messages.movie : messages.tvshow + mediaType === 'movie' + ? messages.movie + : mediaType === 'music' + ? messages.album + : messages.tvshow ), mediaServerName: settings.currentSettings.mediaServerType === diff --git a/src/components/RequestBlock/index.tsx b/src/components/RequestBlock/index.tsx index 2ebc9f23..08e14351 100644 --- a/src/components/RequestBlock/index.tsx +++ b/src/components/RequestBlock/index.tsx @@ -78,20 +78,23 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => { return (
- setShowEditModal(false)} - onComplete={() => { - if (onUpdate) { - onUpdate(); - } - setShowEditModal(false); - }} - /> + {request.media && ( + setShowEditModal(false)} + onComplete={() => { + if (onUpdate) { + onUpdate(); + } + setShowEditModal(false); + }} + /> + )}