refactor(music): making api calls non monolithic

separted the discography and similar-artist from the monolithic artists call
This commit is contained in:
HiItsStolas
2025-11-07 17:05:00 +10:00
parent c3386d42f5
commit bcdc18e3b9
3 changed files with 226 additions and 165 deletions

View File

@@ -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.',
});
}
});

View File

@@ -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}
/>

View File

@@ -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
/>