fix: properly fetch Plex music library with correct release-group mapping

This commit is contained in:
Pierre
2025-01-15 21:08:39 +01:00
committed by HiItsStolas
parent a0a8dfc496
commit f9259cfcdf
3 changed files with 143 additions and 5 deletions

View File

@@ -103,6 +103,73 @@ class MusicBrainz extends ExternalAPI {
}
}
private static requestQueue: Promise<void> = Promise.resolve();
private lastRequestTime = 0;
private readonly RATE_LIMIT_DELAY = 1100;
public async getReleaseGroup({
releaseId,
}: {
releaseId: string;
}): Promise<string | null> {
try {
await MusicBrainz.requestQueue;
MusicBrainz.requestQueue = (async () => {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest < this.RATE_LIMIT_DELAY) {
await new Promise((resolve) =>
setTimeout(resolve, this.RATE_LIMIT_DELAY - timeSinceLastRequest)
);
}
this.lastRequestTime = Date.now();
})();
await MusicBrainz.requestQueue;
const data = await this.getRolling<any>(
`/release/${releaseId}`,
{
inc: 'release-groups',
fmt: 'json',
},
43200,
{
headers: {
'User-Agent':
'Jellyseerr/1.0.0 (https://github.com/Fallenbagel/jellyseerr; hello@jellyseerr.com)',
Accept: 'application/json',
},
},
'https://musicbrainz.org/ws/2'
);
return data['release-group']?.id || null;
} catch (e) {
if (e.message.includes('503')) {
await new Promise((resolve) =>
setTimeout(resolve, this.RATE_LIMIT_DELAY * 2)
);
await MusicBrainz.requestQueue;
MusicBrainz.requestQueue = Promise.resolve();
this.lastRequestTime = Date.now();
try {
return await this.getReleaseGroup({ releaseId });
} catch (retryError) {
throw new Error(
`[MusicBrainz] Failed to fetch release group after retry: ${retryError.message}`
);
}
}
throw new Error(
`[MusicBrainz] Failed to fetch release group: ${e.message}`
);
}
}
public async getWikipediaExtract(
id: string,
language = 'en',

View File

@@ -28,7 +28,7 @@ interface PlexLibraryResponse {
}
export interface PlexLibrary {
type: 'show' | 'movie' | 'music';
type: 'show' | 'movie' | 'artist';
key: string;
title: string;
agent: string;
@@ -155,7 +155,7 @@ class PlexAPI {
(library) =>
library.type === 'movie' ||
library.type === 'show' ||
library.type === 'music'
library.type === 'artist'
)
// Remove libraries that do not have a metadata agent set (usually personal video libraries)
.filter((library) => library.agent !== 'com.plexapp.agents.none')
@@ -168,7 +168,7 @@ class PlexAPI {
id: library.key,
name: library.title,
enabled: existing?.enabled ?? false,
type: library.type,
type: library.type === 'artist' ? 'music' : library.type,
lastScan: existing?.lastScan,
};
});
@@ -230,7 +230,7 @@ class PlexAPI {
options: { addedAt: number } = {
addedAt: Date.now() - 1000 * 60 * 60,
},
mediaType: 'movie' | 'show' | 'music'
mediaType: 'movie' | 'show' | 'artist'
): Promise<PlexLibraryItem[]> {
const response = await this.plexClient.query<PlexLibraryResponse>({
uri: `/library/sections/${id}/all?type=${

View File

@@ -1,5 +1,6 @@
import animeList from '@server/api/animelist';
import { getMetadataProvider } from '@server/api/metadata';
import MusicBrainz from '@server/api/musicbrainz';
import type { PlexLibraryItem, PlexMetadata } from '@server/api/plexapi';
import PlexAPI from '@server/api/plexapi';
import TheMovieDb from '@server/api/themoviedb';
@@ -26,6 +27,7 @@ const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/);
const tmdbRegex = new RegExp(/tmdb:\/\/([0-9]+)/);
const tvdbRegex = new RegExp(/tvdb:\/\/([0-9]+)/);
const tmdbShowRegex = new RegExp(/themoviedb:\/\/([0-9]+)/);
const mbRegex = new RegExp(/mbid:\/\/([0-9a-f-]+)/);
const plexRegex = new RegExp(/plex:\/\//);
// Hama agent uses ASS naming, see details here:
// https://github.com/ZeroQI/Absolute-Series-Scanner/blob/master/README.md#forcing-the-movieseries-id
@@ -95,6 +97,7 @@ class PlexScanner
'info',
{ lastScan: library.lastScan }
);
const mappedType = library.type === 'music' ? 'artist' : library.type;
const libraryItems = await this.plexClient.getRecentlyAdded(
library.id,
library.lastScan
@@ -103,7 +106,7 @@ class PlexScanner
addedAt: library.lastScan - 1000 * 60 * 10,
}
: undefined,
library.type
mappedType
);
// Bundle items up by rating keys
@@ -215,6 +218,12 @@ class PlexScanner
plexitem.type === 'season'
) {
await this.processPlexShow(plexitem);
} else if (
plexitem.type === 'artist' ||
plexitem.type === 'album' ||
plexitem.type === 'track'
) {
await this.processPlexMusic(plexitem);
}
} catch (e) {
this.log('Failed to process Plex media', 'error', {
@@ -381,6 +390,60 @@ class PlexScanner
}
}
private async processPlexMusic(plexitem: PlexLibraryItem) {
const ratingKey =
plexitem.grandparentRatingKey ??
plexitem.parentRatingKey ??
plexitem.ratingKey;
let metadata;
try {
metadata = await this.plexClient.getMetadata(ratingKey, {
includeChildren: true,
});
if (metadata.Children?.Metadata) {
const musicBrainz = new MusicBrainz();
for (const album of metadata.Children.Metadata) {
const albumMetadata = await this.plexClient.getMetadata(
album.ratingKey
);
const mbReleaseId = albumMetadata.Guid?.find((g) => {
const id = g.id.toLowerCase();
return id.startsWith('mbid://');
})?.id.replace('mbid://', '');
if (!mbReleaseId) {
this.log('No MusicBrainz ID found for album', 'debug', {
title: album.title,
artist: metadata.title,
});
continue;
}
const releaseGroupId = await musicBrainz.getReleaseGroup({
releaseId: mbReleaseId,
});
if (releaseGroupId) {
await this.processMusic(releaseGroupId, {
mediaAddedAt: new Date(album.addedAt * 1000),
ratingKey: album.ratingKey,
title: album.title,
});
}
}
}
} catch (e) {
this.log('Failed to process music media', 'error', {
errorMessage: e.message,
title: metadata?.title,
});
}
}
private async getMediaIds(plexitem: PlexLibraryItem): Promise<MediaIds> {
let mediaIds: Partial<MediaIds> = {};
// Check if item is using new plex movie/tv agent
@@ -419,6 +482,8 @@ class PlexScanner
} else if (ref.id.match(tvdbRegex)) {
const tvdbMatch = ref.id.match(tvdbRegex)?.[1];
mediaIds.tvdbId = Number(tvdbMatch);
} else if (ref.id.match(mbRegex)) {
mediaIds.mbId = ref.id.match(mbRegex)?.[1] ?? undefined;
}
});
@@ -534,6 +599,12 @@ class PlexScanner
}
}
}
// Check for MusicBrainz
} else if (plexitem.guid.match(mbRegex)) {
const mbMatch = plexitem.guid.match(mbRegex);
if (mbMatch) {
mediaIds.mbId = mbMatch[1];
}
}
if (!mediaIds.tmdbId) {