mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-29 04:59:46 -05:00
fix: properly fetch Plex music library with correct release-group mapping
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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=${
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user