diff --git a/src/renderer/features/albums/components/joined-artists.tsx b/src/renderer/features/albums/components/joined-artists.tsx index e9c4f2e5e..6fada565d 100644 --- a/src/renderer/features/albums/components/joined-artists.tsx +++ b/src/renderer/features/albums/components/joined-artists.tsx @@ -96,24 +96,38 @@ export const JoinedArtists = ({ const hasArtistMatches = parts.some((part) => typeof part !== 'string'); + // Find artists that were matched + const matchedArtistIds = new Set(nonOverlappingMatches.map((match) => match.artist.id)); + + // Find artists that are not present in the artist name + const unmatchedArtists = artists.filter( + (artist) => artist.name && !matchedArtistIds.has(artist.id), + ); + // If no matches found and there are album artists, return the album artists if (!hasArtistMatches && artists.length > 0) { return ( {artists.map((artist, index) => ( - + {index > 0 && ', '} - - {artist.name} - + {artist.id ? ( + + {artist.name} + + ) : ( + + {artist.name} + + )} ))} @@ -137,21 +151,56 @@ export const JoinedArtists = ({ } const { artist, text } = part; + + if (artist.id) { + return ( + + {text} + + ); + } return ( - + {text} ); })} + {unmatchedArtists.length > 0 && ( + <> + {', '} + {unmatchedArtists.map((artist, index) => ( + + {index > 0 && ', '} + {artist.id ? ( + + {artist.name} + + ) : ( + + {artist.name} + + )} + + ))} + + )} ); }; diff --git a/src/shared/api/jellyfin/jellyfin-normalize.ts b/src/shared/api/jellyfin/jellyfin-normalize.ts index ca969c39a..d6d404924 100644 --- a/src/shared/api/jellyfin/jellyfin-normalize.ts +++ b/src/shared/api/jellyfin/jellyfin-normalize.ts @@ -106,6 +106,33 @@ const getPlaylistImageId = (item: z.infer): nu return null; }; +const getArtists = ( + item: z.infer, + participants?: null | Record, +): RelatedArtist[] => { + if (!item?.ArtistItems?.length && !item.AlbumArtists && !participants) { + return []; + } + + const result: RelatedArtist[] = []; + + (item?.ArtistItems?.length ? item.ArtistItems : item.AlbumArtists)?.forEach((entry) => { + result.push({ + id: entry.Id, + imageId: null, + imageUrl: null, + name: entry.Name, + userFavorite: false, + userRating: null, + }); + }); + + if (participants?.['Remixer']) { + result.push(...participants['Remixer']); + } + + return result; +}; const normalizeSong = ( item: z.infer, server: null | ServerListItem, @@ -143,6 +170,11 @@ const normalizeSong = ( console.warn('Jellyfin song retrieved with no media sources', item); } + const participants = getPeople(item); + + const artists = getArtists(item, participants); + + console.log('artists', artists); return { _itemType: LibraryItem.SONG, _serverId: server?.id || '', @@ -159,16 +191,7 @@ const normalizeSong = ( })), albumId: item.AlbumId || `dummy/${item.Id}`, artistName: item?.ArtistItems?.map((entry) => entry.Name).join(', ') || '', - artists: (item?.ArtistItems?.length ? item.ArtistItems : item.AlbumArtists)?.map( - (entry) => ({ - id: entry.Id, - imageId: null, - imageUrl: null, - name: entry.Name, - userFavorite: false, - userRating: null, - }), - ), + artists, bitDepth: null, bitRate, bpm: null, @@ -210,7 +233,7 @@ const normalizeSong = ( mbzRecordingId: null, mbzTrackId: item.ProviderIds?.MusicBrainzTrack || null, name: item.Name, - participants: getPeople(item), + participants, path: replacePathPrefix(path || '', pathReplace, pathReplaceWith), peak: null, playCount: (item.UserData && item.UserData.PlayCount) || 0, diff --git a/src/shared/api/navidrome/navidrome-normalize.ts b/src/shared/api/navidrome/navidrome-normalize.ts index 5bdb4a6e8..43fc5cd38 100644 --- a/src/shared/api/navidrome/navidrome-normalize.ts +++ b/src/shared/api/navidrome/navidrome-normalize.ts @@ -57,12 +57,13 @@ const getArtists = ( ) => { let albumArtists: RelatedArtist[] | undefined; let artists: RelatedArtist[] | undefined; + let remixers: RelatedArtist[] | undefined; let participants: null | Record = null; if (item.participants) { participants = {}; for (const [role, list] of Object.entries(item.participants)) { - if (role === 'albumartist' || role === 'artist') { + if (role === 'albumartist' || role === 'artist' || role === 'remixer') { const roleList = list.map((item) => ({ id: item.id, imageId: null, @@ -74,6 +75,8 @@ const getArtists = ( if (role === 'albumartist') { albumArtists = roleList; + } else if (role === 'remixer') { + remixers = roleList; } else { artists = roleList; } @@ -121,7 +124,7 @@ const getArtists = ( ]; } - if (artists === undefined) { + if (artists === undefined && remixers === undefined) { artists = [ { id: item.artistId, @@ -134,7 +137,7 @@ const getArtists = ( ]; } - return { albumArtists, artists, participants }; + return { albumArtists, artists: [...(artists || []), ...(remixers || [])], participants }; }; const normalizeSong = ( diff --git a/src/shared/api/subsonic/subsonic-normalize.ts b/src/shared/api/subsonic/subsonic-normalize.ts index 7f28635d0..58c39628e 100644 --- a/src/shared/api/subsonic/subsonic-normalize.ts +++ b/src/shared/api/subsonic/subsonic-normalize.ts @@ -21,26 +21,39 @@ const getArtistList = ( artists?: typeof ssType._response.song._type.artists, artistId?: number | string, artistName?: string, + participants?: null | Record, ) => { - return artists - ? artists.map((item) => ({ - id: item.id.toString(), - imageId: null, - imageUrl: null, - name: item.name, - userFavorite: false, - userRating: null, - })) - : [ - { - id: artistId?.toString() || '', - imageId: null, - imageUrl: null, - name: artistName || '', - userFavorite: false, - userRating: null, - }, - ]; + if (!artists && !participants) { + return [ + { + id: artistId?.toString() || '', + imageId: null, + imageUrl: null, + name: artistName || '', + userFavorite: false, + userRating: null, + }, + ]; + } + + const result: RelatedArtist[] = []; + + artists?.forEach((item) => { + result.push({ + id: item.id.toString(), + imageId: null, + imageUrl: null, + name: item.name, + userFavorite: false, + userRating: null, + }); + }); + + if (participants?.['remixer']) { + result.push(...participants['remixer']); + } + + return result; }; const getParticipants = ( @@ -121,6 +134,8 @@ const normalizeSong = ( pathReplace?: string, pathReplaceWith?: string, ): Song => { + const participants = getParticipants(item); + return { _itemType: LibraryItem.SONG, _serverId: server?.id || 'unknown', @@ -130,7 +145,7 @@ const normalizeSong = ( albumArtists: getArtistList(item.albumArtists, item.artistId, item.artist), albumId: item.albumId?.toString() || '', artistName: item.artist || '', - artists: getArtistList(item.artists, item.artistId, item.artist), + artists: getArtistList(item.artists, item.artistId, item.artist, participants), bitDepth: item.bitDepth || null, bitRate: item.bitRate || 0, bpm: item.bpm || null, @@ -164,7 +179,7 @@ const normalizeSong = ( mbzRecordingId: item.musicBrainzId || null, mbzTrackId: null, name: item.title, - participants: getParticipants(item), + participants, path: replacePathPrefix(item.path || '', pathReplace, pathReplaceWith), peak: item.replayGain && (item.replayGain.albumPeak || item.replayGain.trackPeak)