Add query parameters for maxElements and offset in API and update usage in music route

pull/3800/merge^2
Anatole Sot 4 months ago
parent 227eebaaca
commit f2a77ea7e4

@ -6117,6 +6117,18 @@ paths:
type: boolean
example: false
default: false
- in: query
name: maxElements
schema:
type: number
example: 50
default: 25
- in: query
name: offset
schema:
type: number
example: 25
default: 0
responses:
'200':
description: Artist details

@ -485,7 +485,11 @@ class MusicBrainz extends BaseNodeBrainz {
}
};
public getFullArtist = (artistId: string): Promise<mbArtist> => {
public getFullArtist = (
artistId: string,
maxElements = 25,
startOffset = 0
): Promise<mbArtist> => {
try {
return new Promise<mbArtist>((resolve, reject) => {
this.artist(
@ -498,12 +502,26 @@ class MusicBrainz extends BaseNodeBrainz {
reject(error);
} else {
const results = convertArtist(data as Artist);
results.releaseGroups = await this.getReleaseGroups(artistId);
/*
results.releases = await this.getReleases(artistId);
results.recordings = await this.getRecordings(artistId);
results.works = await this.getWorks(artistId);
*/
results.releaseGroups = await this.getReleaseGroups(
artistId,
maxElements,
startOffset
);
results.releases = await this.getReleases(
artistId,
maxElements,
startOffset
);
results.recordings = await this.getRecordings(
artistId,
maxElements,
startOffset
);
results.works = await this.getWorks(
artistId,
maxElements,
startOffset
);
resolve(results);
}
}
@ -518,6 +536,88 @@ class MusicBrainz extends BaseNodeBrainz {
}
};
public getRecordings = (
artistId: string,
maxElements = 50,
startOffset = 0
): Promise<mbRecording[]> => {
try {
return new Promise<mbRecording[]>((resolve, reject) => {
this.browse(
'recording',
{ artist: artistId, offset: startOffset },
async (error, data) => {
if (error) {
reject(error);
} else {
data = data as {
'recording-count': number;
'recording-offset': number;
recordings: Recording[];
};
// Get the first 25 results
const total = data['recording-count'];
let results: mbRecording[] = await data.recordings.map(
convertRecording
);
// Slice the results into smaller chunks to avoid hitting the limit of 100
for (
let i = data.recordings.length + startOffset;
i < total && i < maxElements;
i += 100
) {
results = results.concat(
await new Promise<mbRecording[]>((resolve2, reject2) => {
this.browse(
'recording',
{
artist: artistId,
offset: i,
limit: 100,
},
(error, data) => {
if (error) {
reject2(error);
} else {
const results = (
(
data as {
'recording-count': number;
'recording-offset': number;
recordings: Recording[];
}
).recordings ?? []
).map(convertRecording);
resolve2(results);
}
}
);
})
);
}
results = results.reduce((arr: mbRecording[], item) => {
const exists = !!arr.find((x) => x.title === item.title);
if (!exists) {
arr.push(item);
}
return arr;
}, []);
resolve(results);
}
}
);
});
} catch (e) {
logger.error('Failed to get recordings by artist', {
label: 'MusicBrainz',
message: e.message,
});
return new Promise<mbRecording[]>((resolve) => resolve([]));
}
};
public getRecording = (recordingId: string): Promise<mbRecording> => {
try {
return new Promise<mbRecording>((resolve, reject) => {
@ -545,50 +645,74 @@ class MusicBrainz extends BaseNodeBrainz {
}
};
public getReleaseGroups = (artistId: string): Promise<mbReleaseGroup[]> => {
public getReleaseGroups = (
artistId: string,
maxElements = 50,
startOffset = 0
): Promise<mbReleaseGroup[]> => {
try {
return new Promise<mbReleaseGroup[]>((resolve, reject) => {
this.browse(
'release-group',
{ artist: artistId },
{ artist: artistId, offset: startOffset },
async (error, data) => {
if (error) {
reject(error);
} else {
const results = await new Promise<mbReleaseGroup[]>(
(resolve2, reject2) => {
this.browse(
'release-group',
{
artist: artistId,
limit:
(
data as {
'release-group-count': number;
'release-group-offset': number;
'release-groups': Group[];
}
)['release-group-count'] ?? 25,
},
(error, data) => {
if (error) {
reject2(error);
} else {
const results = (
(
data as {
'release-group-count': number;
'release-group-offset': number;
'release-groups': Group[];
}
)['release-groups'] ?? []
).map(convertReleaseGroup);
resolve2(results);
data = data as {
'release-group-count': number;
'release-group-offset': number;
'release-groups': Group[];
};
// Get the first 25 results
const total = data['release-group-count'];
let results: mbReleaseGroup[] = await data['release-groups'].map(
convertReleaseGroup
);
// Slice the results into smaller chunks to avoid hitting the limit of 100
for (
let i = data['release-groups'].length + startOffset;
i < total && i < maxElements;
i += 100
) {
results = results.concat(
await new Promise<mbReleaseGroup[]>((resolve2, reject2) => {
this.browse(
'release-group',
{
artist: artistId,
offset: i,
limit: 100,
},
(error, data) => {
if (error) {
reject2(error);
} else {
const results = (
(
data as {
'release-group-count': number;
'release-group-offset': number;
'release-groups': Group[];
}
)['release-groups'] ?? []
).map(convertReleaseGroup);
resolve2(results);
}
}
}
);
);
})
);
}
results = results.reduce((arr: mbReleaseGroup[], item) => {
const exists = !!arr.find((x) => x.title === item.title);
if (!exists) {
arr.push(item);
}
);
return arr;
}, []);
resolve(results);
}
}
@ -634,6 +758,88 @@ class MusicBrainz extends BaseNodeBrainz {
}
};
public getReleases = (
artistId: string,
maxElements = 50,
startOffset = 0
): Promise<mbRelease[]> => {
try {
return new Promise<mbRelease[]>((resolve, reject) => {
this.browse(
'release',
{ artist: artistId, offset: startOffset },
async (error, data) => {
if (error) {
reject(error);
} else {
data = data as {
'release-count': number;
'release-offset': number;
releases: Release[];
};
// Get the first 25 results
const total = data['release-count'];
let results: mbRelease[] = await data.releases.map(
convertRelease
);
// Slice the results into smaller chunks to avoid hitting the limit of 100
for (
let i = data.releases.length + startOffset;
i < total && i < maxElements;
i += 100
) {
results = results.concat(
await new Promise<mbRelease[]>((resolve2, reject2) => {
this.browse(
'release',
{
artist: artistId,
offset: i,
limit: 100,
},
(error, data) => {
if (error) {
reject2(error);
} else {
const results = (
(
data as {
'release-count': number;
'release-offset': number;
releases: Release[];
}
).releases ?? []
).map(convertRelease);
resolve2(results);
}
}
);
})
);
}
results = results.reduce((arr: mbRelease[], item) => {
const exists = !!arr.find((x) => x.title === item.title);
if (!exists) {
arr.push(item);
}
return arr;
}, []);
resolve(results);
}
}
);
});
} catch (e) {
logger.error('Failed to get releases by artist', {
label: 'MusicBrainz',
message: e.message,
});
return new Promise<mbRelease[]>((resolve) => resolve([]));
}
};
public getRelease = (releaseId: string): Promise<mbRelease> => {
try {
return new Promise<mbRelease>((resolve, reject) => {
@ -661,6 +867,86 @@ class MusicBrainz extends BaseNodeBrainz {
}
};
public getWorks = (
artistId: string,
maxElements = 50,
startOffset = 0
): Promise<mbWork[]> => {
try {
return new Promise<mbWork[]>((resolve, reject) => {
this.browse(
'work',
{ artist: artistId, offset: startOffset },
async (error, data) => {
if (error) {
reject(error);
} else {
data = data as {
'work-count': number;
'work-offset': number;
works: Work[];
};
// Get the first 25 results
const total = data['work-count'];
let results: mbWork[] = await data.works.map(convertWork);
// Slice the results into smaller chunks to avoid hitting the limit of 100
for (
let i = data.works.length + startOffset;
i < total && i < maxElements;
i += 100
) {
results = results.concat(
await new Promise<mbWork[]>((resolve2, reject2) => {
this.browse(
'work',
{
artist: artistId,
offset: i,
limit: 100,
},
(error, data) => {
if (error) {
reject2(error);
} else {
const results = (
(
data as {
'work-count': number;
'work-offset': number;
works: Work[];
}
).works ?? []
).map(convertWork);
resolve2(results);
}
}
);
})
);
}
results = results.reduce((arr: mbWork[], item) => {
const exists = !!arr.find((x) => x.title === item.title);
if (!exists) {
arr.push(item);
}
return arr;
}, []);
resolve(results);
}
}
);
});
} catch (e) {
logger.error('Failed to get works by artist', {
label: 'MusicBrainz',
message: e.message,
});
return new Promise<mbWork[]>((resolve) => resolve([]));
}
};
public getWork = (workId: string): Promise<mbWork> => {
try {
return new Promise<mbWork>((resolve, reject) => {

@ -204,6 +204,26 @@ class PlexAPI {
};
}
public async getMusicLibraryContents(
id: string,
{ offset = 0, size = 50 }: { offset?: number; size?: number } = {}
): Promise<{ totalSize: number; items: PlexLibraryItem[] }> {
const response = await this.getLibraryContents(id, { offset, size });
const response2 = await this.plexClient.query<PlexLibraryResponse>({
uri: `/library/sections/${id}/albums?includeGuids=1`,
extraHeaders: {
'X-Plex-Container-Start': `${offset}`,
'X-Plex-Container-Size': `${size}`,
},
});
return {
totalSize: response.totalSize + response2.MediaContainer.totalSize,
items: response.items.concat(response2.MediaContainer.Metadata) ?? [],
};
}
public async getMetadata(
key: string,
options: { includeChildren?: boolean } = {}
@ -241,7 +261,21 @@ class PlexAPI {
'X-Plex-Container-Size': `500`,
},
});
if (mediaType === 'artist') {
const albumResponse = await this.plexClient.query<PlexLibraryResponse>({
uri: `/library/sections/${id}/albums&sort=addedAt%3Adesc&addedAt>>=${Math.floor(
options.addedAt / 1000
)}`,
extraHeaders: {
'X-Plex-Container-Start': `0`,
'X-Plex-Container-Size': `500`,
},
});
// concat the album and artist response mediacontainer.metadata
return response.MediaContainer.Metadata.concat(
albumResponse.MediaContainer.Metadata
);
}
return response.MediaContainer.Metadata;
}
}

@ -590,6 +590,93 @@ class BaseScanner<T> {
});
}
protected async processAlbum(
mbId: string,
{
mediaAddedAt,
ratingKey,
serviceId,
externalServiceId,
processing = false,
title = 'Unknown Title',
}: ProcessOptions = {}
): Promise<void> {
const mediaRepository = getRepository(Media);
await this.asyncLock.dispatch(mbId, async () => {
const existing = await this.getExisting(mbId, MediaType.MUSIC);
if (existing) {
let changedExisting = false;
if (existing['status'] !== MediaStatus.AVAILABLE) {
existing['status'] = processing
? MediaStatus.PROCESSING
: MediaStatus.AVAILABLE;
if (mediaAddedAt) {
existing.mediaAddedAt = mediaAddedAt;
}
changedExisting = true;
}
if (!changedExisting && !existing.mediaAddedAt && mediaAddedAt) {
existing.mediaAddedAt = mediaAddedAt;
changedExisting = true;
}
if (ratingKey && existing['ratingKey'] !== ratingKey) {
existing['ratingKey'] = ratingKey;
changedExisting = true;
}
if (serviceId !== undefined && existing['serviceId'] !== serviceId) {
existing['serviceId'] = serviceId;
changedExisting = true;
}
if (
externalServiceId !== undefined &&
existing['externalServiceId'] !== externalServiceId
) {
existing['externalServiceId'] = externalServiceId;
changedExisting = true;
}
if (changedExisting) {
await mediaRepository.save(existing);
this.log(
`Media for ${title} exists. Changes were detected and the title will be updated.`,
'info'
);
} else {
this.log(`Title already exists and no changes detected for ${title}`);
}
} else {
const newMedia = new Media();
newMedia.mbId = mbId;
newMedia.secondaryType = SecondaryType.RELEASE;
newMedia.status = !processing
? MediaStatus.AVAILABLE
: processing
? MediaStatus.PROCESSING
: MediaStatus.UNKNOWN;
newMedia.mediaType = MediaType.MUSIC;
newMedia.serviceId = serviceId;
newMedia.externalServiceId = externalServiceId;
if (mediaAddedAt) {
newMedia.mediaAddedAt = mediaAddedAt;
}
if (ratingKey) {
newMedia.ratingKey = ratingKey;
}
await mediaRepository.save(newMedia);
this.log(`Saved new media: ${title}`);
}
});
}
/**
* Call startRun from child class whenever a run is starting to
* ensure required values are set

@ -136,7 +136,13 @@ class PlexScanner
for (const library of this.libraries) {
this.currentLibrary = library;
this.log(`Beginning to process library: ${library.name}`, 'info');
await this.paginateLibrary(library, { sessionId });
try {
await this.paginateLibrary(library, { sessionId });
} catch (e) {
this.log('Failed to paginate library', 'error', {
errorMessage: e.message,
});
}
}
}
this.log(
@ -165,12 +171,16 @@ class PlexScanner
if (this.sessionId !== sessionId) {
throw new Error('New session was started. Old session aborted.');
}
const response = await this.plexClient.getLibraryContents(library.id, {
size: this.protectedBundleSize,
offset: start,
});
const response =
library.type === 'artist'
? await this.plexClient.getMusicLibraryContents(library.id, {
size: this.protectedBundleSize,
offset: start,
})
: await this.plexClient.getLibraryContents(library.id, {
size: this.protectedBundleSize,
offset: start,
});
this.progress = start;
this.totalSize = response.totalSize;
@ -212,6 +222,8 @@ class PlexScanner
await this.processPlexShow(plexitem);
} else if (plexitem.type === 'artist') {
await this.processPlexArtist(plexitem);
} else if (plexitem.type === 'album') {
await this.processPlexAlbum(plexitem);
}
} catch (e) {
this.log('Failed to process Plex media', 'error', {
@ -359,6 +371,21 @@ class PlexScanner
}
}
private async processPlexAlbum(plexitem: PlexLibraryItem) {
const mediaIds = await this.getMediaIds(plexitem);
if (mediaIds.mbId) {
await this.processAlbum(mediaIds.mbId, {
mediaAddedAt: new Date(plexitem.addedAt * 1000),
ratingKey: plexitem.ratingKey,
title: plexitem.title,
});
} else {
this.log('No MusicBrainz ID found for album', 'warn', {
title: plexitem.title,
});
}
}
private async getMediaIds(plexitem: PlexLibraryItem): Promise<MediaIds> {
let mediaIds: Partial<MediaIds> = {};
// Check if item is using new plex movie/tv agent

@ -15,8 +15,12 @@ musicRoutes.get('/artist/:id', async (req, res, next) => {
const mb = new MusicBrainz();
try {
const offset = req.query.offset ? parseInt(req.query.offset as string) : 0;
const maxElements = req.query.maxElements
? parseInt(req.query.maxElements as string)
: 25;
const artist = req.query.full
? await mb.getFullArtist(req.params.id)
? await mb.getFullArtist(req.params.id, maxElements, offset)
: await mb.getArtist(req.params.id);
const media = await Media.getMedia(artist.id, MediaType.MUSIC);

Loading…
Cancel
Save