From 1a930021b6077f44ae5a35f197ffa8182e504607 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 28 Dec 2025 03:43:59 -0800 Subject: [PATCH] handle favorite/update in similarArtists --- .../api/subsonic/subsonic-controller.ts | 10 +++---- .../album-artist-detail-content.tsx | 4 +-- .../mutations/favorite-optimistic-updates.ts | 25 +++++++++++++++- .../mutations/rating-optimistic-updates.ts | 26 +++++++++++++++- src/shared/api/jellyfin/jellyfin-normalize.ts | 12 ++++++++ .../api/navidrome/navidrome-normalize.ts | 30 +++++++++++++++++-- src/shared/api/subsonic/subsonic-normalize.ts | 28 +++++++++++++++-- src/shared/api/subsonic/subsonic-types.ts | 2 ++ src/shared/types/domain-types.ts | 2 ++ 9 files changed, 124 insertions(+), 15 deletions(-) diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index 9c00e19f0..41795203c 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -279,11 +279,11 @@ export const SubsonicController: InternalControllerEndpoint = { } return { - ...ssNormalize.albumArtist(artist, apiClientProps.server, 300), + ...ssNormalize.albumArtist(artist, apiClientProps.server), albums: artist.album?.map((album) => ssNormalize.album(album, apiClientProps.server)), similarArtists: artistInfo?.similarArtist?.map((artist) => - ssNormalize.albumArtist(artist, apiClientProps.server, 300), + ssNormalize.albumArtist(artist, apiClientProps.server), ) || null, }; }, @@ -303,7 +303,7 @@ export const SubsonicController: InternalControllerEndpoint = { const artists = (res.body.artists?.index || []).flatMap((index) => index.artist); let results = artists.map((artist) => - ssNormalize.albumArtist(artist, apiClientProps.server, 300), + ssNormalize.albumArtist(artist, apiClientProps.server), ); if (query.searchTerm) { @@ -488,7 +488,7 @@ export const SubsonicController: InternalControllerEndpoint = { return { items: res.body.albumList2.album?.map((album) => - ssNormalize.album(album, apiClientProps.server, 300), + ssNormalize.album(album, apiClientProps.server), ) || [], startIndex: query.startIndex, totalRecordCount: null, @@ -658,7 +658,7 @@ export const SubsonicController: InternalControllerEndpoint = { } let results = artists.map((artist) => - ssNormalize.albumArtist(artist, apiClientProps.server, 300), + ssNormalize.albumArtist(artist, apiClientProps.server), ); if (query.searchTerm) { diff --git a/src/renderer/features/artists/components/album-artist-detail-content.tsx b/src/renderer/features/artists/components/album-artist-detail-content.tsx index c4be8ff74..0ec065144 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx @@ -506,8 +506,8 @@ const AlbumArtistMetadataSimilarArtists = ({ playCount: null, similarArtists: null, songCount: null, - userFavorite: false, - userRating: null, + userFavorite: relatedArtist.userFavorite, + userRating: relatedArtist.userRating, }), ); }, [detailQuery.data?.similarArtists, server?.type, serverId]); diff --git a/src/renderer/features/shared/mutations/favorite-optimistic-updates.ts b/src/renderer/features/shared/mutations/favorite-optimistic-updates.ts index edf96562a..cbd1c26b2 100644 --- a/src/renderer/features/shared/mutations/favorite-optimistic-updates.ts +++ b/src/renderer/features/shared/mutations/favorite-optimistic-updates.ts @@ -215,13 +215,36 @@ export const applyFavoriteOptimisticUpdates = ( queryClient.setQueryData( queryKey, (prev: AlbumArtistDetailResponse | undefined) => { - if (prev && itemIdSet.has(prev.id)) { + if (!prev) { + return prev; + } + + // Update the main artist if it matches + if (itemIdSet.has(prev.id)) { return { ...prev, userFavorite: isFavorite, }; } + // Update similar artists if any match + if (prev.similarArtists && prev.similarArtists.length > 0) { + const hasMatchingSimilarArtist = prev.similarArtists.some( + (artist) => itemIdSet.has(artist.id), + ); + + if (hasMatchingSimilarArtist) { + return { + ...prev, + similarArtists: prev.similarArtists.map((artist) => + itemIdSet.has(artist.id) + ? { ...artist, userFavorite: isFavorite } + : artist, + ), + }; + } + } + return prev; }, ); diff --git a/src/renderer/features/shared/mutations/rating-optimistic-updates.ts b/src/renderer/features/shared/mutations/rating-optimistic-updates.ts index 8e876d2a5..78fdf8d4c 100644 --- a/src/renderer/features/shared/mutations/rating-optimistic-updates.ts +++ b/src/renderer/features/shared/mutations/rating-optimistic-updates.ts @@ -184,9 +184,33 @@ export const applyRatingOptimisticUpdates = ( queryClient.setQueryData( queryKey, (prev: AlbumArtistDetailResponse | undefined) => { - if (prev && itemIdSet.has(prev.id)) { + if (!prev) { + return prev; + } + + // Update the main artist if it matches + if (itemIdSet.has(prev.id)) { return { ...prev, userRating: rating }; } + + // Update similar artists if any match + if (prev.similarArtists && prev.similarArtists.length > 0) { + const hasMatchingSimilarArtist = prev.similarArtists.some( + (artist) => itemIdSet.has(artist.id), + ); + + if (hasMatchingSimilarArtist) { + return { + ...prev, + similarArtists: prev.similarArtists.map((artist) => + itemIdSet.has(artist.id) + ? { ...artist, userRating: rating } + : artist, + ), + }; + } + } + return prev; }, ); diff --git a/src/shared/api/jellyfin/jellyfin-normalize.ts b/src/shared/api/jellyfin/jellyfin-normalize.ts index 6a76aa5ba..eb3b8ff85 100644 --- a/src/shared/api/jellyfin/jellyfin-normalize.ts +++ b/src/shared/api/jellyfin/jellyfin-normalize.ts @@ -37,6 +37,8 @@ const getPeople = (item: AlbumOrSong): null | Record => imageId: null, imageUrl: null, name: person.Name, + userFavorite: false, + userRating: null, }; if (key in participants) { @@ -148,6 +150,8 @@ const normalizeSong = ( imageId: entry.Id, imageUrl: null, name: entry.Name, + userFavorite: false, + userRating: null, })), albumId: item.AlbumId || `dummy/${item.Id}`, artistName: item?.ArtistItems?.[0]?.Name || item?.AlbumArtists?.[0]?.Name, @@ -157,6 +161,8 @@ const normalizeSong = ( imageId: null, imageUrl: null, name: entry.Name, + userFavorite: false, + userRating: null, }), ), bitDepth: null, @@ -232,6 +238,8 @@ const normalizeAlbum = ( imageId: entry.Id, imageUrl: null, name: entry.Name, + userFavorite: false, + userRating: null, })) || [], artists: (item.ArtistItems?.length ? item.ArtistItems : item.AlbumArtists)?.map( (entry) => ({ @@ -239,6 +247,8 @@ const normalizeAlbum = ( imageId: entry.Id, imageUrl: null, name: entry.Name, + userFavorite: false, + userRating: null, }), ), comment: null, @@ -295,6 +305,8 @@ const normalizeAlbumArtist = ( imageId: getAlbumArtistImageId(entry), imageUrl: null, name: entry.Name, + userFavorite: entry.UserData?.IsFavorite || false, + userRating: null, }), ) || []; diff --git a/src/shared/api/navidrome/navidrome-normalize.ts b/src/shared/api/navidrome/navidrome-normalize.ts index af84de579..ebf789783 100644 --- a/src/shared/api/navidrome/navidrome-normalize.ts +++ b/src/shared/api/navidrome/navidrome-normalize.ts @@ -51,6 +51,8 @@ const getArtists = ( imageId: null, imageUrl: null, name: item.name, + userFavorite: false, + userRating: null, })); if (role === 'albumartist') { @@ -67,6 +69,8 @@ const getArtists = ( imageId: null, imageUrl: null, name: artist.name, + userFavorite: false, + userRating: null, }; if (subRoles.has(artist.subRole)) { @@ -89,12 +93,28 @@ const getArtists = ( if (albumArtists === undefined) { albumArtists = [ - { id: item.albumArtistId, imageId: null, imageUrl: null, name: item.albumArtist }, + { + id: item.albumArtistId, + imageId: null, + imageUrl: null, + name: item.albumArtist, + userFavorite: false, + userRating: null, + }, ]; } if (artists === undefined) { - artists = [{ id: item.artistId, imageId: null, imageUrl: null, name: item.artist }]; + artists = [ + { + id: item.artistId, + imageId: null, + imageUrl: null, + name: item.artist, + userFavorite: false, + userRating: null, + }, + ]; } return { albumArtists, artists, participants }; @@ -304,6 +324,8 @@ const normalizeAlbumArtist = ( songCount = item.songCount; } + console.log('similarArtists', item.similarArtists); + return { _itemType: LibraryItem.ALBUM_ARTIST, _serverId: server?.id || 'unknown', @@ -335,7 +357,9 @@ const normalizeAlbumArtist = ( imageId: null, imageUrl: artist?.artistImageUrl?.replace(/\?size=\d+/, '') || null, name: artist.name, - })) || null, + userFavorite: Boolean(artist.starred) || false, + userRating: artist.userRating || null, + })) || [], songCount, userFavorite: item.starred || false, userRating: item.rating || null, diff --git a/src/shared/api/subsonic/subsonic-normalize.ts b/src/shared/api/subsonic/subsonic-normalize.ts index 8ef65e3c5..c2fdf0104 100644 --- a/src/shared/api/subsonic/subsonic-normalize.ts +++ b/src/shared/api/subsonic/subsonic-normalize.ts @@ -27,6 +27,8 @@ const getArtistList = ( imageId: null, imageUrl: null, name: item.name, + userFavorite: false, + userRating: null, })) : [ { @@ -34,6 +36,8 @@ const getArtistList = ( imageId: null, imageUrl: null, name: artistName || '', + userFavorite: false, + userRating: null, }, ]; }; @@ -55,6 +59,8 @@ const getParticipants = ( imageId: null, imageUrl: null, name: contributor.artist.name || '', + userFavorite: false, + userRating: null, }; const role = contributor.subRole @@ -178,8 +184,16 @@ const normalizeSong = ( const normalizeAlbumArtist = ( item: - | z.infer - | z.infer, + | (z.infer & { + similarArtists?: z.infer< + typeof ssType._response.artistInfo + >['artistInfo']['similarArtist']; + }) + | (z.infer & { + similarArtists?: z.infer< + typeof ssType._response.artistInfo + >['artistInfo']['similarArtist']; + }), server?: null | ServerListItemWithCredential, ): AlbumArtist => { return { @@ -197,7 +211,15 @@ const normalizeAlbumArtist = ( mbz: null, name: item.name, playCount: null, - similarArtists: [], + similarArtists: + item.similarArtists?.map((artist) => ({ + id: artist.id, + imageId: null, + imageUrl: null, + name: artist.name, + userFavorite: Boolean(artist.starred) || false, + userRating: artist.userRating || null, + })) || [], songCount: null, userFavorite: Boolean(item.starred) || false, userRating: null, diff --git a/src/shared/api/subsonic/subsonic-types.ts b/src/shared/api/subsonic/subsonic-types.ts index 8aa0a0ac6..399396d61 100644 --- a/src/shared/api/subsonic/subsonic-types.ts +++ b/src/shared/api/subsonic/subsonic-types.ts @@ -236,6 +236,8 @@ const artistInfo = z.object({ coverArt: z.string().optional(), id: z.string(), name: z.string(), + starred: z.string().optional(), + userRating: z.number().optional(), }), ), smallImageUrl: z.string().optional(), diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index 62e843c8f..f28c7f8c4 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -356,6 +356,8 @@ export type RelatedArtist = { imageId: null | string; imageUrl: null | string; name: string; + userFavorite: boolean; + userRating: null | number; }; export type Song = {