refactor(music): making api calls non monolithic
separted the discography and similar-artist from the monolithic artists call
This commit is contained in:
@@ -203,15 +203,9 @@ musicRoutes.get('/:id', async (req, res, next) => {
|
||||
musicRoutes.get('/:id/artist', async (req, res, next) => {
|
||||
try {
|
||||
const listenbrainzApi = new ListenBrainzAPI();
|
||||
const personMapper = new TmdbPersonMapper();
|
||||
const theAudioDb = new TheAudioDb();
|
||||
const metadataAlbumRepository = getRepository(MetadataAlbum);
|
||||
const metadataArtistRepository = getRepository(MetadataArtist);
|
||||
|
||||
const page = Number(req.query.page) || 1;
|
||||
const pageSize = Number(req.query.pageSize) || 20;
|
||||
const isSlider = req.query.slider === 'true';
|
||||
|
||||
const albumData = await listenbrainzApi.getAlbum(req.params.id);
|
||||
const artistData = albumData?.release_group_metadata?.artist?.artists?.[0];
|
||||
const artistType = artistData?.type;
|
||||
@@ -242,6 +236,72 @@ musicRoutes.get('/:id/artist', async (req, res, next) => {
|
||||
return res.status(404).json({ status: 404, message: 'Artist not found' });
|
||||
}
|
||||
|
||||
const [artistImagesResult] = await Promise.allSettled([
|
||||
!cachedTheAudioDb &&
|
||||
!metadataArtist?.tadbThumb &&
|
||||
!metadataArtist?.tadbCover
|
||||
? theAudioDb.getArtistImages(artistData.artist_mbid)
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
const artistImages =
|
||||
artistImagesResult.status === 'fulfilled'
|
||||
? artistImagesResult.value
|
||||
: null;
|
||||
|
||||
return res.status(200).json({
|
||||
artist: {
|
||||
...artistDetails,
|
||||
artistThumb:
|
||||
cachedTheAudioDb?.artistThumb ??
|
||||
metadataArtist?.tadbThumb ??
|
||||
artistImages?.artistThumb ??
|
||||
null,
|
||||
artistBackdrop:
|
||||
cachedTheAudioDb?.artistBackground ??
|
||||
metadataArtist?.tadbCover ??
|
||||
artistImages?.artistBackground ??
|
||||
null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Something went wrong retrieving artist details', {
|
||||
label: 'Music API',
|
||||
errorMessage: error.message,
|
||||
artistId: req.params.id,
|
||||
});
|
||||
return next({ status: 500, message: 'Unable to retrieve artist details.' });
|
||||
}
|
||||
});
|
||||
|
||||
musicRoutes.get('/:id/artist-discography', async (req, res, next) => {
|
||||
try {
|
||||
const listenbrainzApi = new ListenBrainzAPI();
|
||||
const metadataAlbumRepository = getRepository(MetadataAlbum);
|
||||
|
||||
const page = Number(req.query.page) || 1;
|
||||
const pageSize = Number(req.query.pageSize) || 20;
|
||||
const isSlider = req.query.slider === 'true';
|
||||
|
||||
const albumData = await listenbrainzApi.getAlbum(req.params.id);
|
||||
const artistData = albumData?.release_group_metadata?.artist?.artists?.[0];
|
||||
const artistType = artistData?.type;
|
||||
|
||||
if (!artistData?.artist_mbid || artistType === 'Other') {
|
||||
return res.status(404).json({
|
||||
status: 404,
|
||||
message: 'Artist details not available for this type',
|
||||
});
|
||||
}
|
||||
|
||||
const artistDetails = await listenbrainzApi.getArtist(
|
||||
artistData.artist_mbid
|
||||
);
|
||||
|
||||
if (!artistDetails) {
|
||||
return res.status(404).json({ status: 404, message: 'Artist not found' });
|
||||
}
|
||||
|
||||
const totalReleaseGroups = artistDetails.releaseGroups.length;
|
||||
const paginatedReleaseGroups =
|
||||
isSlider || page === 1
|
||||
@@ -252,32 +312,130 @@ musicRoutes.get('/:id/artist', async (req, res, next) => {
|
||||
);
|
||||
|
||||
const releaseGroupIds = paginatedReleaseGroups.map((rg) => rg.mbid);
|
||||
const similarArtistIds =
|
||||
artistDetails.similarArtists?.artists?.map((a) => a.artist_mbid) ?? [];
|
||||
|
||||
const mediaResponses = await Promise.allSettled([
|
||||
Media.getRelatedMedia(req.user, releaseGroupIds),
|
||||
metadataAlbumRepository.find({
|
||||
where: { mbAlbumId: In(releaseGroupIds) },
|
||||
}),
|
||||
similarArtistIds.length > 0
|
||||
? metadataArtistRepository.find({
|
||||
where: { mbArtistId: In(similarArtistIds) },
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const relatedMedia =
|
||||
mediaResponses[0].status === 'fulfilled' ? mediaResponses[0].value : [];
|
||||
const albumMetadata =
|
||||
mediaResponses[1].status === 'fulfilled' ? mediaResponses[1].value : [];
|
||||
const similarArtistMetadata =
|
||||
mediaResponses[2].status === 'fulfilled' ? mediaResponses[2].value : [];
|
||||
|
||||
const albumMetadataMap = new Map(
|
||||
albumMetadata.map((metadata) => [metadata.mbAlbumId, metadata])
|
||||
);
|
||||
|
||||
const relatedMediaMap = new Map(
|
||||
relatedMedia.map((media) => [media.mbId, media])
|
||||
);
|
||||
|
||||
const transformedReleaseGroups = paginatedReleaseGroups.map(
|
||||
(releaseGroup) => {
|
||||
const metadata = albumMetadataMap.get(releaseGroup.mbid);
|
||||
return {
|
||||
id: releaseGroup.mbid,
|
||||
mediaType: 'album',
|
||||
title: releaseGroup.name,
|
||||
'first-release-date': releaseGroup.date,
|
||||
'artist-credit': [{ name: releaseGroup.artist_credit_name }],
|
||||
'primary-type': releaseGroup.type || 'Other',
|
||||
posterPath: metadata?.caaUrl ?? null,
|
||||
needsCoverArt: !metadata?.caaUrl,
|
||||
mediaInfo: relatedMediaMap.get(releaseGroup.mbid),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
releaseGroups: transformedReleaseGroups,
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
totalItems: totalReleaseGroups,
|
||||
totalPages: Math.ceil(totalReleaseGroups / pageSize),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Something went wrong retrieving artist discography', {
|
||||
label: 'Music API',
|
||||
errorMessage: error.message,
|
||||
artistId: req.params.id,
|
||||
});
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Unable to retrieve artist discography.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
musicRoutes.get('/:id/artist-similar', async (req, res, next) => {
|
||||
try {
|
||||
const listenbrainzApi = new ListenBrainzAPI();
|
||||
const personMapper = new TmdbPersonMapper();
|
||||
const theAudioDb = new TheAudioDb();
|
||||
const metadataArtistRepository = getRepository(MetadataArtist);
|
||||
|
||||
const page = Number(req.query.page) || 1;
|
||||
const pageSize = Number(req.query.pageSize) || 20;
|
||||
|
||||
const albumData = await listenbrainzApi.getAlbum(req.params.id);
|
||||
const artistData = albumData?.release_group_metadata?.artist?.artists?.[0];
|
||||
const artistType = artistData?.type;
|
||||
|
||||
if (!artistData?.artist_mbid || artistType === 'Other') {
|
||||
return res.status(404).json({
|
||||
status: 404,
|
||||
message: 'Artist details not available for this type',
|
||||
});
|
||||
}
|
||||
|
||||
const artistDetails = await listenbrainzApi.getArtist(
|
||||
artistData.artist_mbid
|
||||
);
|
||||
|
||||
if (!artistDetails) {
|
||||
return res.status(404).json({ status: 404, message: 'Artist not found' });
|
||||
}
|
||||
|
||||
const allSimilarArtists =
|
||||
artistDetails.similarArtists?.artists?.sort(
|
||||
(a, b) => b.score - a.score
|
||||
) ?? [];
|
||||
|
||||
const totalResults = allSimilarArtists.length;
|
||||
const totalPages = Math.ceil(totalResults / pageSize);
|
||||
|
||||
const paginatedSimilarArtists = allSimilarArtists.slice(
|
||||
(page - 1) * pageSize,
|
||||
page * pageSize
|
||||
);
|
||||
|
||||
const similarArtistIds = paginatedSimilarArtists.map((a) => a.artist_mbid);
|
||||
|
||||
if (similarArtistIds.length === 0) {
|
||||
return res.status(200).json({
|
||||
page,
|
||||
totalPages,
|
||||
totalResults,
|
||||
results: [],
|
||||
});
|
||||
}
|
||||
|
||||
const [similarArtistMetadataResult] = await Promise.allSettled([
|
||||
metadataArtistRepository.find({
|
||||
where: { mbArtistId: In(similarArtistIds) },
|
||||
}),
|
||||
]);
|
||||
|
||||
const similarArtistMetadata =
|
||||
similarArtistMetadataResult.status === 'fulfilled'
|
||||
? similarArtistMetadataResult.value
|
||||
: [];
|
||||
|
||||
const similarArtistMetadataMap = new Map(
|
||||
similarArtistMetadata.map((metadata) => [metadata.mbArtistId, metadata])
|
||||
);
|
||||
@@ -288,8 +446,8 @@ musicRoutes.get('/:id/artist', async (req, res, next) => {
|
||||
});
|
||||
|
||||
const personArtists =
|
||||
artistDetails.similarArtists?.artists
|
||||
?.filter((artist) => artist.type === 'Person')
|
||||
paginatedSimilarArtists
|
||||
.filter((artist) => artist.type === 'Person')
|
||||
.filter((artist) => {
|
||||
const metadata = similarArtistMetadataMap.get(artist.artist_mbid);
|
||||
return !metadata?.tmdbPersonId;
|
||||
@@ -315,11 +473,6 @@ musicRoutes.get('/:id/artist', async (req, res, next) => {
|
||||
})
|
||||
)
|
||||
: Promise.resolve(similarArtistMetadata),
|
||||
!cachedTheAudioDb &&
|
||||
!metadataArtist?.tadbThumb &&
|
||||
!metadataArtist?.tadbCover
|
||||
? theAudioDb.getArtistImages(artistData.artist_mbid)
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
const artistImageResults =
|
||||
@@ -328,14 +481,6 @@ musicRoutes.get('/:id/artist', async (req, res, next) => {
|
||||
artistResponses[1].status === 'fulfilled'
|
||||
? artistResponses[1].value
|
||||
: similarArtistMetadata;
|
||||
const artistImagesResult =
|
||||
artistResponses[2].status === 'fulfilled'
|
||||
? artistResponses[2].value
|
||||
: null;
|
||||
|
||||
const relatedMediaMap = new Map(
|
||||
relatedMedia.map((media) => [media.mbId, media])
|
||||
);
|
||||
|
||||
const finalArtistMetadataMap = new Map(
|
||||
(updatedArtistMetadata || similarArtistMetadata).map((metadata) => [
|
||||
@@ -344,80 +489,46 @@ musicRoutes.get('/:id/artist', async (req, res, next) => {
|
||||
])
|
||||
);
|
||||
|
||||
const transformedReleaseGroups = paginatedReleaseGroups.map(
|
||||
(releaseGroup) => {
|
||||
const metadata = albumMetadataMap.get(releaseGroup.mbid);
|
||||
return {
|
||||
id: releaseGroup.mbid,
|
||||
mediaType: 'album',
|
||||
title: releaseGroup.name,
|
||||
'first-release-date': releaseGroup.date,
|
||||
'artist-credit': [{ name: releaseGroup.artist_credit_name }],
|
||||
'primary-type': releaseGroup.type || 'Other',
|
||||
posterPath: metadata?.caaUrl ?? null,
|
||||
needsCoverArt: !metadata?.caaUrl,
|
||||
mediaInfo: relatedMediaMap.get(releaseGroup.mbid),
|
||||
};
|
||||
}
|
||||
);
|
||||
const transformedSimilarArtists = paginatedSimilarArtists.map((artist) => {
|
||||
const metadata = finalArtistMetadataMap.get(artist.artist_mbid);
|
||||
const artistImageResult =
|
||||
artistImageResults[
|
||||
artist.artist_mbid as keyof typeof artistImageResults
|
||||
];
|
||||
|
||||
const transformedSimilarArtists =
|
||||
artistDetails.similarArtists?.artists?.map((artist) => {
|
||||
const metadata = finalArtistMetadataMap.get(artist.artist_mbid);
|
||||
const artistImageResult =
|
||||
artistImageResults[
|
||||
artist.artist_mbid as keyof typeof artistImageResults
|
||||
];
|
||||
const artistThumb =
|
||||
metadata?.tadbThumb || (artistImageResult?.artistThumb ?? null);
|
||||
|
||||
const artistThumb =
|
||||
metadata?.tadbThumb || (artistImageResult?.artistThumb ?? null);
|
||||
|
||||
const artistBackground =
|
||||
metadata?.tadbCover || (artistImageResult?.artistBackground ?? null);
|
||||
|
||||
return {
|
||||
...artist,
|
||||
artistThumb: metadata?.tmdbThumb ?? artistThumb,
|
||||
artistBackground: artistBackground,
|
||||
tmdbPersonId: metadata?.tmdbPersonId
|
||||
? Number(metadata.tmdbPersonId)
|
||||
: null,
|
||||
};
|
||||
}) ?? [];
|
||||
return {
|
||||
id: artist.artist_mbid,
|
||||
mediaType: 'artist',
|
||||
name: artist.name,
|
||||
type: artist.type as 'Group' | 'Person',
|
||||
artistThumb: metadata?.tmdbThumb ?? artistThumb,
|
||||
score: artist.score,
|
||||
tmdbPersonId: metadata?.tmdbPersonId
|
||||
? Number(metadata.tmdbPersonId)
|
||||
: null,
|
||||
'sort-name': artist.name,
|
||||
};
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
artist: {
|
||||
...artistDetails,
|
||||
artistThumb:
|
||||
cachedTheAudioDb?.artistThumb ??
|
||||
metadataArtist?.tadbThumb ??
|
||||
artistImagesResult?.artistThumb ??
|
||||
null,
|
||||
artistBackdrop:
|
||||
cachedTheAudioDb?.artistBackground ??
|
||||
metadataArtist?.tadbCover ??
|
||||
artistImagesResult?.artistBackground ??
|
||||
null,
|
||||
similarArtists: {
|
||||
...artistDetails.similarArtists,
|
||||
artists: transformedSimilarArtists,
|
||||
},
|
||||
releaseGroups: transformedReleaseGroups,
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
totalItems: totalReleaseGroups,
|
||||
totalPages: Math.ceil(totalReleaseGroups / pageSize),
|
||||
},
|
||||
},
|
||||
page,
|
||||
totalPages,
|
||||
totalResults,
|
||||
results: transformedSimilarArtists,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Something went wrong retrieving artist details', {
|
||||
logger.error('Something went wrong retrieving similar artists', {
|
||||
label: 'Music API',
|
||||
errorMessage: error.message,
|
||||
artistId: req.params.id,
|
||||
});
|
||||
return next({ status: 500, message: 'Unable to retrieve artist details.' });
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Unable to retrieve similar artists.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
TvResult,
|
||||
} from '@server/models/Search';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import useSWRInfinite from 'swr/infinite';
|
||||
|
||||
interface MixedResult {
|
||||
@@ -34,7 +34,7 @@ interface MixedResult {
|
||||
|
||||
interface MediaSliderProps {
|
||||
title: string;
|
||||
url?: string;
|
||||
url: string;
|
||||
linkUrl?: string;
|
||||
sliderKey: string;
|
||||
hideWhenEmpty?: boolean;
|
||||
@@ -58,20 +58,13 @@ const MediaSlider = ({
|
||||
sliderKey,
|
||||
hideWhenEmpty = false,
|
||||
onNewTitles,
|
||||
items: passedItems,
|
||||
totalItems,
|
||||
}: MediaSliderProps) => {
|
||||
const settings = useSettings();
|
||||
const { hasPermission } = useUser();
|
||||
const [titles, setTitles] = useState<
|
||||
(MovieResult | TvResult | PersonResult | AlbumResult | ArtistResult)[]
|
||||
>([]);
|
||||
const { data, error, setSize, size } = useSWRInfinite<MixedResult>(
|
||||
(pageIndex: number, previousPageData: MixedResult | null) => {
|
||||
if (
|
||||
!url ||
|
||||
(previousPageData && pageIndex + 1 > previousPageData.totalPages)
|
||||
) {
|
||||
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -85,33 +78,19 @@ const MediaSlider = ({
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newTitles =
|
||||
passedItems ??
|
||||
(data ?? []).reduce(
|
||||
(a, v) => [...a, ...v.results],
|
||||
[] as (
|
||||
| MovieResult
|
||||
| TvResult
|
||||
| PersonResult
|
||||
| AlbumResult
|
||||
| ArtistResult
|
||||
)[]
|
||||
);
|
||||
let titles = (data ?? []).reduce(
|
||||
(a, v) => [...a, ...v.results],
|
||||
[] as (MovieResult | TvResult | PersonResult | AlbumResult | ArtistResult)[]
|
||||
);
|
||||
|
||||
if (settings.currentSettings.hideAvailable) {
|
||||
setTitles(
|
||||
newTitles.filter(
|
||||
(i) =>
|
||||
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
|
||||
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
|
||||
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setTitles(newTitles);
|
||||
}
|
||||
}, [data, passedItems, settings.currentSettings.hideAvailable]);
|
||||
if (settings.currentSettings.hideAvailable) {
|
||||
titles = titles.filter(
|
||||
(i) =>
|
||||
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
|
||||
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
|
||||
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
|
||||
);
|
||||
}
|
||||
|
||||
if (settings.currentSettings.hideBlacklisted) {
|
||||
titles = titles.filter(
|
||||
@@ -123,7 +102,6 @@ const MediaSlider = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!passedItems &&
|
||||
titles.length < 24 &&
|
||||
size < 5 &&
|
||||
(data?.[0]?.totalResults ?? 0) > size * 20
|
||||
@@ -136,14 +114,9 @@ const MediaSlider = ({
|
||||
// at all for our purposes.
|
||||
onNewTitles(titles.length);
|
||||
}
|
||||
}, [titles, setSize, size, data, onNewTitles, passedItems]);
|
||||
}, [titles, setSize, size, data, onNewTitles]);
|
||||
|
||||
if (
|
||||
hideWhenEmpty &&
|
||||
(!passedItems
|
||||
? (data?.[0].results ?? []).length === 0
|
||||
: titles.length === 0)
|
||||
) {
|
||||
if (hideWhenEmpty && (data?.[0].results ?? []).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -271,7 +244,7 @@ const MediaSlider = ({
|
||||
</div>
|
||||
<Slider
|
||||
sliderKey={sliderKey}
|
||||
isLoading={!passedItems && !data && !error}
|
||||
isLoading={!data && !error}
|
||||
isEmpty={false}
|
||||
items={finalTitles}
|
||||
/>
|
||||
|
||||
@@ -32,7 +32,7 @@ import { IssueStatus } from '@server/constants/issue';
|
||||
import { MediaStatus, MediaType } from '@server/constants/media';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import type { MusicDetails as MusicDetailsType } from '@server/models/Music';
|
||||
import type { AlbumResult, ArtistResult } from '@server/models/Search';
|
||||
import type { AlbumResult } from '@server/models/Search';
|
||||
import axios from 'axios';
|
||||
import 'country-flag-icons/3x2/flags.css';
|
||||
import Link from 'next/link';
|
||||
@@ -76,16 +76,6 @@ interface MusicDetailsProps {
|
||||
interface ArtistDetails {
|
||||
artist: {
|
||||
releaseGroups: AlbumResult[];
|
||||
similarArtists: {
|
||||
artists: {
|
||||
tmdbPersonId: number;
|
||||
artist_mbid: string;
|
||||
name: string;
|
||||
type: string;
|
||||
artistThumb: string;
|
||||
score: number;
|
||||
}[];
|
||||
};
|
||||
pagination?: {
|
||||
totalItems: number;
|
||||
};
|
||||
@@ -766,7 +756,7 @@ const MusicDetails = ({ music }: MusicDetailsProps) => {
|
||||
title={intl.formatMessage(messages.discography, {
|
||||
artistName: data?.artist.name.split(/[&,]|\sfeat\./)[0].trim() ?? '',
|
||||
})}
|
||||
items={artistData?.artist.releaseGroups}
|
||||
url={`/api/v1/music/${data.id}/artist-discography`}
|
||||
totalItems={artistData?.artist.pagination?.totalItems}
|
||||
linkUrl={`/music/${data.id}/discography`}
|
||||
hideWhenEmpty
|
||||
@@ -775,20 +765,7 @@ const MusicDetails = ({ music }: MusicDetailsProps) => {
|
||||
<MediaSlider
|
||||
sliderKey="artist-similar"
|
||||
title={intl.formatMessage(messages.similarArtists)}
|
||||
items={
|
||||
artistData?.artist.similarArtists.artists.map(
|
||||
(artist): ArtistResult => ({
|
||||
id: artist.artist_mbid,
|
||||
mediaType: 'artist',
|
||||
name: artist.name,
|
||||
type: artist.type as 'Group' | 'Person',
|
||||
artistThumb: artist.artistThumb,
|
||||
score: artist.score,
|
||||
tmdbPersonId: artist.tmdbPersonId,
|
||||
'sort-name': artist.name,
|
||||
})
|
||||
) ?? []
|
||||
}
|
||||
url={`/api/v1/music/${data.id}/artist-similar`}
|
||||
linkUrl={`/music/${data.id}/similar`}
|
||||
hideWhenEmpty
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user