From 17deac8d6501ace1b73d42fbde4df02a0579ed7f Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 8 Mar 2026 22:06:18 -0700 Subject: [PATCH] decouple AlbumArtistInfo from AlbumArtistDetail (#1809) --- src/renderer/api/controller.ts | 12 +++ .../api/jellyfin/jellyfin-controller.ts | 66 ++++++++++----- .../api/navidrome/navidrome-controller.ts | 68 ++++++++------- src/renderer/api/query-keys.ts | 8 ++ .../api/subsonic/subsonic-controller.ts | 54 +++++++----- .../grid-carousel/grid-carousel-v2.tsx | 11 ++- .../features/artists/api/artists-api.ts | 15 ++++ .../album-artist-detail-content.tsx | 84 ++++++++++++++----- .../components/album-artist-detail-header.tsx | 16 +++- .../components/album-artist-grid-carousel.tsx | 20 ++++- .../routes/album-artist-detail-route.tsx | 42 ++++++++-- .../mutations/favorite-optimistic-updates.ts | 54 ++++++++---- .../mutations/rating-optimistic-updates.ts | 78 ++++++++++------- src/shared/types/domain-types.ts | 14 ++++ 14 files changed, 386 insertions(+), 156 deletions(-) diff --git a/src/renderer/api/controller.ts b/src/renderer/api/controller.ts index 4c474d70e..434919666 100644 --- a/src/renderer/api/controller.ts +++ b/src/renderer/api/controller.ts @@ -200,6 +200,18 @@ export const controller: GeneralController = { server.type, )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, + getAlbumArtistInfo(args) { + const server = getServerById(args.apiClientProps.serverId); + + if (!server) { + return Promise.resolve(null); + } + + const fn = apiController('getAlbumArtistInfo', server.type); + return fn + ? fn(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })) + : Promise.resolve(null); + }, getAlbumArtistList(args) { const server = getServerById(args.apiClientProps.serverId); diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index 90514f4c6..2514d3063 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -258,34 +258,54 @@ export const JellyfinController: InternalControllerEndpoint = { throw new Error('No userId found'); } - const [res, similarArtistsRes] = await Promise.all([ - jfApiClient(apiClientProps).getAlbumArtistDetail({ - params: { - id: query.id, - userId: apiClientProps.server?.userId, - }, - query: { - Fields: JF_FIELDS.ALBUM_ARTIST_DETAIL, - }, - }), - jfApiClient(apiClientProps).getSimilarArtistList({ - params: { - id: query.id, - }, - query: { - Limit: 10, - }, - }), - ]); + const res = await jfApiClient(apiClientProps).getAlbumArtistDetail({ + params: { + id: query.id, + userId: apiClientProps.server?.userId, + }, + query: { + Fields: ['Genres', 'Overview', 'SortName'], + }, + }); - if (res.status !== 200 || similarArtistsRes.status !== 200) { + if (res.status !== 200) { throw new Error('Failed to get album artist detail'); } - return jfNormalize.albumArtist( - { ...res.body, similarArtists: similarArtistsRes.body }, - apiClientProps.server, + return jfNormalize.albumArtist(res.body, apiClientProps.server); + }, + getAlbumArtistInfo: async (args) => { + const { apiClientProps, query } = args; + + const similarArtistsRes = await jfApiClient(apiClientProps).getSimilarArtistList({ + params: { + id: query.id, + }, + query: { + Limit: query.limit ?? 10, + }, + }); + + if (similarArtistsRes.status !== 200) { + return null; + } + + const items = similarArtistsRes.body?.Items?.filter( + (entry) => entry.Name !== 'Various Artists', ); + const similarArtists = + items?.map((entry) => ({ + id: entry.Id, + imageId: entry.ImageTags?.Primary ? entry.Id : null, + imageUrl: null, + name: entry.Name, + userFavorite: entry.UserData?.IsFavorite || false, + userRating: null, + })) ?? null; + + return { + similarArtists, + }; }, getAlbumArtistList: async (args) => { const { apiClientProps, query } = args; diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index 81ccd8c37..d669a3e83 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -190,19 +190,11 @@ export const NavidromeController: InternalControllerEndpoint = { getAlbumArtistDetail: async (args) => { const { apiClientProps, query } = args; - const [res, artistInfoRes] = await Promise.all([ - ndApiClient(apiClientProps).getAlbumArtistDetail({ - params: { - id: query.id, - }, - }), - ssApiClient(apiClientProps).getArtistInfo({ - query: { - count: 10, - id: query.id, - }, - }), - ]); + const res = await ndApiClient(apiClientProps).getAlbumArtistDetail({ + params: { + id: query.id, + }, + }); if (res.status !== 200) { throw new Error('Failed to get album artist detail'); @@ -212,22 +204,42 @@ export const NavidromeController: InternalControllerEndpoint = { throw new Error('Server is required'); } - // Prefer images from getArtistInfo first (which should be proxied) - // Prioritize large > medium > small - return ndNormalize.albumArtist( - { - ...res.body.data, - ...(artistInfoRes.status === 200 && { - largeImageUrl: - artistInfoRes.body.artistInfo.largeImageUrl || - artistInfoRes.body.artistInfo.mediumImageUrl || - artistInfoRes.body.artistInfo.smallImageUrl || - res.body.data.largeImageUrl, - similarArtists: artistInfoRes.body.artistInfo.similarArtist, - }), + return ndNormalize.albumArtist(res.body.data, apiClientProps.server); + }, + getAlbumArtistInfo: async (args) => { + const { apiClientProps, query } = args; + + const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({ + query: { + id: query.id, + ...(query.limit != null && { count: query.limit }), }, - apiClientProps.server, - ); + }); + + if (artistInfoRes.status !== 200) { + return null; + } + + const artistInfo = artistInfoRes.body.artistInfo; + const imageUrl = + artistInfo?.largeImageUrl || + artistInfo?.mediumImageUrl || + artistInfo?.smallImageUrl || + null; + + return { + biography: artistInfo?.biography || null, + imageUrl, + similarArtists: + artistInfo?.similarArtist?.map((artist) => ({ + id: artist.id, + imageId: null, + imageUrl: artist?.artistImageUrl?.replace(/\?size=\d+/, '') ?? null, + name: artist.name, + userFavorite: Boolean(artist.starred) || false, + userRating: artist.userRating ?? null, + })) ?? null, + }; }, getAlbumArtistList: async (args) => { const { apiClientProps, query } = args; diff --git a/src/renderer/api/query-keys.ts b/src/renderer/api/query-keys.ts index 649b97767..d3a4ef029 100644 --- a/src/renderer/api/query-keys.ts +++ b/src/renderer/api/query-keys.ts @@ -1,5 +1,6 @@ import type { AlbumArtistDetailQuery, + AlbumArtistInfoQuery, AlbumArtistListQuery, AlbumDetailQuery, AlbumListQuery, @@ -93,6 +94,13 @@ export const queryKeys: Record< return [serverId, 'albumArtists', 'infiniteList'] as const; }, + info: (serverId: string, query?: AlbumArtistInfoQuery) => { + if (query) { + return [serverId, 'albumArtists', 'info', query] as const; + } + + return [serverId, 'albumArtists', 'info'] as const; + }, list: (serverId: string, query?: AlbumArtistListQuery) => { const { filter, pagination } = splitPaginatedQuery(query); if (query && pagination) { diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index 096e9386f..19fe17346 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -258,18 +258,11 @@ export const SubsonicController: InternalControllerEndpoint = { getAlbumArtistDetail: async (args) => { const { apiClientProps, query } = args; - const [artistInfoRes, res] = await Promise.all([ - ssApiClient(apiClientProps).getArtistInfo({ - query: { - id: query.id, - }, - }), - ssApiClient(apiClientProps).getArtist({ - query: { - id: query.id, - }, - }), - ]); + const res = await ssApiClient(apiClientProps).getArtist({ + query: { + id: query.id, + }, + }); if (res.status !== 200) { throw new Error('Failed to get album artist detail'); @@ -277,11 +270,6 @@ export const SubsonicController: InternalControllerEndpoint = { const artist = res.body.artist; - let artistInfo; - if (artistInfoRes.status === 200) { - artistInfo = artistInfoRes.body.artistInfo; - } - return { ...ssNormalize.albumArtist(artist, apiClientProps.server), albums: artist.album?.map((album) => @@ -292,10 +280,36 @@ export const SubsonicController: InternalControllerEndpoint = { args.context?.pathReplaceWith, ), ), + similarArtists: null, + }; + }, + getAlbumArtistInfo: async (args) => { + const { apiClientProps, query } = args; + + const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({ + query: { + id: query.id, + ...(query.limit != null && { count: query.limit }), + }, + }); + + if (artistInfoRes.status !== 200) { + return null; + } + + const artistInfo = artistInfoRes.body.artistInfo; + + return { + biography: artistInfo?.biography || null, similarArtists: - artistInfo?.similarArtist?.map((artist) => - ssNormalize.albumArtist(artist, apiClientProps.server), - ) || null, + artistInfo?.similarArtist?.map((artist) => ({ + id: artist.id, + imageId: null, + imageUrl: null, + name: artist.name, + userFavorite: Boolean(artist.starred) || false, + userRating: artist.userRating ?? null, + })) ?? null, }; }, getAlbumArtistList: async (args) => { diff --git a/src/renderer/components/grid-carousel/grid-carousel-v2.tsx b/src/renderer/components/grid-carousel/grid-carousel-v2.tsx index c336ed901..3a681d8ea 100644 --- a/src/renderer/components/grid-carousel/grid-carousel-v2.tsx +++ b/src/renderer/components/grid-carousel/grid-carousel-v2.tsx @@ -365,6 +365,7 @@ interface GridCarouselSkeletonProps { containerQuery?: ReturnType; enableRefresh?: boolean; placeholderItemType: LibraryItem; + placeholderRound?: boolean; placeholderRows: DataRow[]; rowCount?: number; title?: ReactNode | string; @@ -375,12 +376,15 @@ const GridCarouselSkeleton = (props: GridCarouselSkeletonProps) => { containerQuery: providedContainerQuery, enableRefresh = false, placeholderItemType, + placeholderRound = false, placeholderRows, rowCount = 1, title, } = props; - const { ...cq } = providedContainerQuery; + const defaultContainerQuery = useGridCarouselContainerQuery(); + const containerQuery = providedContainerQuery ?? defaultContainerQuery; + const { ...cq } = containerQuery; const cardsToShow = cq.isCalculated ? getCardsToShow({ @@ -399,6 +403,7 @@ const GridCarouselSkeleton = (props: GridCarouselSkeletonProps) => { content: ( { ), id: `skeleton-${index}`, })); - }, [cardsToShow, rowCount, placeholderItemType, placeholderRows]); + }, [cardsToShow, placeholderRound, rowCount, placeholderItemType, placeholderRows]); return ( ) => { + return queryOptions({ + queryFn: ({ signal }) => { + return ( + api.controller.getAlbumArtistInfo?.({ + apiClientProps: { serverId: args.serverId, signal }, + query: args.query, + }) ?? Promise.resolve(null) + ); + }, + queryKey: queryKeys.albumArtists.info(args.serverId, args.query), + ...args.options, + }); + }, albumArtistList: (args: QueryHookArgs) => { return queryOptions({ queryFn: ({ signal }) => { 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 a36fdf747..8a2d36d71 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx @@ -5,8 +5,7 @@ import { UseSuspenseQueryResult, } from '@tanstack/react-query'; import { LayoutGroup, motion } from 'motion/react'; -import { Suspense } from 'react'; -import { useCallback, useMemo, useRef, useState } from 'react'; +import { Suspense, useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { createSearchParams, generatePath, Link, useLocation, useParams } from 'react-router'; @@ -70,6 +69,7 @@ import { Grid } from '/@/shared/components/grid/grid'; import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control'; +import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Spinner } from '/@/shared/components/spinner/spinner'; import { Spoiler } from '/@/shared/components/spoiler/spoiler'; import { Stack } from '/@/shared/components/stack/stack'; @@ -201,18 +201,57 @@ const AlbumArtistMetadataGenres = ({ genres }: AlbumArtistMetadataGenresProps) = interface AlbumArtistMetadataBiographyProps { artistName?: string; - biography: null | string | undefined; + routeId: string; } const AlbumArtistMetadataBiography = ({ artistName, - biography, + routeId, }: AlbumArtistMetadataBiographyProps) => { const { t } = useTranslation(); + const server = useCurrentServer(); - if (!biography) return null; + const artistInfoQuery = useQuery({ + ...artistsQueries.albumArtistInfo({ + query: { id: routeId, limit: 10 }, + serverId: server?.id, + }), + enabled: Boolean(server?.id && routeId), + }); - const sanitizedBiography = sanitize(biography); + const detailQuery = useQuery({ + ...artistsQueries.albumArtistDetail({ + query: { id: routeId }, + serverId: server?.id, + }), + enabled: Boolean(server?.id && routeId), + }); + + const biography = artistInfoQuery.data?.biography || detailQuery.data?.biography; + const isLoading = !biography && (artistInfoQuery.isLoading || detailQuery.isLoading); + + const sanitizedBiography = biography ? sanitize(biography) : ''; + + if (isLoading) { + return ( +
+ + {t('page.albumArtistDetail.about', { + artist: artistName, + })} + + + + + + +
+ ); + } + + if (!biography) { + return null; + } return (
@@ -850,20 +889,25 @@ const AlbumArtistMetadataExternalLinks = ({ }; interface AlbumArtistMetadataSimilarArtistsProps { - detailQuery: ReturnType>; routeId: string; } -const AlbumArtistMetadataSimilarArtists = ({ - detailQuery, - routeId, -}: AlbumArtistMetadataSimilarArtistsProps) => { +const AlbumArtistMetadataSimilarArtists = ({ routeId }: AlbumArtistMetadataSimilarArtistsProps) => { const { t } = useTranslation(); const server = useCurrentServer(); const serverId = useCurrentServerId(); + const artistInfoQuery = useQuery({ + ...artistsQueries.albumArtistInfo({ + query: { id: routeId, limit: 10 }, + serverId: server?.id, + }), + enabled: Boolean(server?.id && routeId), + }); + + const relatedArtists = artistInfoQuery.data?.similarArtists ?? null; + const similarArtists = useMemo(() => { - const relatedArtists = detailQuery.data?.similarArtists; if (!relatedArtists || relatedArtists.length === 0) { return []; } @@ -890,7 +934,7 @@ const AlbumArtistMetadataSimilarArtists = ({ userRating: relatedArtist.userRating, }), ); - }, [detailQuery.data?.similarArtists, server?.type, serverId]); + }, [relatedArtists, server?.type, serverId]); const carouselTitle = useMemo( () => ( @@ -908,7 +952,7 @@ const AlbumArtistMetadataSimilarArtists = ({ [t], ); - if (similarArtists.length === 0) { + if (!artistInfoQuery.isLoading && similarArtists.length === 0) { return null; } @@ -916,6 +960,7 @@ const AlbumArtistMetadataSimilarArtists = ({ @@ -977,8 +1022,6 @@ export const AlbumArtistDetailContent = ({ [routeId, detailQuery.data?.name], ); - const biography = - detailQuery.data?.biography && enabledItem.biography ? detailQuery.data.biography : null; const showGenres = detailQuery.data?.genres ? detailQuery.data.genres.length !== 0 : false; const mbzId = detailQuery.data?.mbz; @@ -1034,11 +1077,11 @@ export const AlbumArtistDetailContent = ({ /> )} - {biography && ( + {enabledItem.biography && ( )} @@ -1047,10 +1090,7 @@ export const AlbumArtistDetailContent = ({ {enabledItem.similarArtists && ( - + )} {enabledItem.topSongs && ( diff --git a/src/renderer/features/artists/components/album-artist-detail-header.tsx b/src/renderer/features/artists/components/album-artist-detail-header.tsx index 58599e8d3..a7254df37 100644 --- a/src/renderer/features/artists/components/album-artist-detail-header.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-header.tsx @@ -1,4 +1,4 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; import { forwardRef, Fragment, Ref, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router'; @@ -69,9 +69,9 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref { @@ -137,11 +137,19 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref { - return detailQuery.data?.imageUrl || imageUrl; - }, [detailQuery.data?.imageUrl, imageUrl]); + return detailQuery.data?.imageUrl || artistInfoQuery.data?.imageUrl || imageUrl; + }, [artistInfoQuery.data?.imageUrl, detailQuery.data?.imageUrl, imageUrl]); return ( + ); + } + const handleNextPage = () => {}; const handlePrevPage = () => {}; diff --git a/src/renderer/features/artists/routes/album-artist-detail-route.tsx b/src/renderer/features/artists/routes/album-artist-detail-route.tsx index 4f9f9911e..8de869ea6 100644 --- a/src/renderer/features/artists/routes/album-artist-detail-route.tsx +++ b/src/renderer/features/artists/routes/album-artist-detail-route.tsx @@ -1,7 +1,8 @@ -import { useSuspenseQueries } from '@tanstack/react-query'; -import { Suspense, useRef } from 'react'; +import { useQuery, useQueryClient, useSuspenseQueries } from '@tanstack/react-query'; +import { Suspense, useEffect, useRef } from 'react'; import { useParams } from 'react-router'; +import { queryKeys } from '/@/renderer/api/query-keys'; import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area'; import { albumQueries } from '/@/renderer/features/albums/api/album-api'; @@ -19,7 +20,12 @@ import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-e import { useFastAverageColor } from '/@/renderer/hooks'; import { useArtistBackground, useCurrentServer, useCurrentServerId } from '/@/renderer/store'; import { Spinner } from '/@/shared/components/spinner/spinner'; -import { AlbumListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-types'; +import { + AlbumArtistDetailResponse, + AlbumListSort, + LibraryItem, + SortOrder, +} from '/@/shared/types/domain-types'; const AlbumArtistDetailRouteContent = () => { const scrollAreaRef = useRef(null); @@ -27,6 +33,7 @@ const AlbumArtistDetailRouteContent = () => { const server = useCurrentServer(); const serverId = useCurrentServerId(); const { artistBackground, artistBackgroundBlur } = useArtistBackground(); + const queryClient = useQueryClient(); const { albumArtistId, artistId } = useParams() as { albumArtistId?: string; @@ -35,6 +42,26 @@ const AlbumArtistDetailRouteContent = () => { const routeId = (artistId || albumArtistId) as string; + const artistInfoQuery = useQuery({ + ...artistsQueries.albumArtistInfo({ + query: { id: routeId, limit: 10 }, + serverId: server?.id, + }), + enabled: Boolean(server?.id && routeId), + }); + + useEffect(() => { + const data = artistInfoQuery.data; + if (!data?.imageUrl || !server?.id || !routeId) return; + queryClient.setQueryData( + queryKeys.albumArtists.detail(server.id, { id: routeId }), + (prev: AlbumArtistDetailResponse | undefined) => { + if (!prev) return prev; + return { ...prev, imageUrl: data.imageUrl ?? prev.imageUrl }; + }, + ); + }, [artistInfoQuery.data, queryClient, routeId, server?.id]); + const [detailQuery, albumsQuery] = useSuspenseQueries({ queries: [ artistsQueries.albumArtistDetail({ query: { id: routeId }, serverId: server?.id }), @@ -60,13 +87,14 @@ const AlbumArtistDetailRouteContent = () => { const libraryBackgroundImageUrl = useItemImageUrl({ id: detailQuery.data?.imageId || undefined, + imageUrl: detailQuery.data?.imageUrl, itemType: LibraryItem.ALBUM_ARTIST, type: 'itemCard', }); const selectedImageUrl = imageUrl || detailQuery.data?.imageUrl; - const { background: backgroundColor, isLoading: isColorLoading } = useFastAverageColor({ + const { background: backgroundColor } = useFastAverageColor({ id: artistId, src: selectedImageUrl, srcLoaded: true, @@ -76,9 +104,9 @@ const AlbumArtistDetailRouteContent = () => { const showBlurredImage = artistBackground; - if (isColorLoading) { - return ; - } + // if (isColorLoading) { + // return ; + // } return ( diff --git a/src/renderer/features/shared/mutations/favorite-optimistic-updates.ts b/src/renderer/features/shared/mutations/favorite-optimistic-updates.ts index 51c7e6c65..6cfc8d6fc 100644 --- a/src/renderer/features/shared/mutations/favorite-optimistic-updates.ts +++ b/src/renderer/features/shared/mutations/favorite-optimistic-updates.ts @@ -6,6 +6,7 @@ import { Album, AlbumArtist, AlbumArtistDetailResponse, + AlbumArtistInfoResponse, AlbumArtistListResponse, AlbumDetailResponse, AlbumListResponse, @@ -240,29 +241,44 @@ export const applyFavoriteOptimisticUpdates = ( return { ...prev, userFavorite: isFavorite }; } - 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; }, }); } }); + const infoQueryKey = queryKeys.albumArtists.info(variables.apiClientProps.serverId); + const infoQueries = queryClient.getQueriesData({ + exact: false, + queryKey: infoQueryKey, + }); + + infoQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: AlbumArtistInfoResponse | null | undefined) => { + if (!prev?.similarArtists?.length) return prev; + + const hasMatching = prev.similarArtists.some((artist) => + itemIdSet.has(artist.id), + ); + if (!hasMatching) return prev; + + return { + ...prev, + similarArtists: prev.similarArtists.map((artist) => + itemIdSet.has(artist.id) + ? { ...artist, userFavorite: isFavorite } + : artist, + ), + }; + }, + }); + } + }); + const listQueryKey = queryKeys.albumArtists.list(variables.apiClientProps.serverId); const listQueries = queryClient.getQueriesData({ exact: false, @@ -654,6 +670,10 @@ export const applyFavoriteOptimisticUpdatesDeferred = ( queryKeys.albumArtists.detail(variables.apiClientProps.serverId), 'album-artist-detail', ); + collectQueries( + queryKeys.albumArtists.info(variables.apiClientProps.serverId), + 'album-artist-info', + ); collectQueries( queryKeys.albumArtists.list(variables.apiClientProps.serverId), 'album-artist-list', diff --git a/src/renderer/features/shared/mutations/rating-optimistic-updates.ts b/src/renderer/features/shared/mutations/rating-optimistic-updates.ts index 22c404462..f564e42fc 100644 --- a/src/renderer/features/shared/mutations/rating-optimistic-updates.ts +++ b/src/renderer/features/shared/mutations/rating-optimistic-updates.ts @@ -7,6 +7,7 @@ import { Album, AlbumArtist, AlbumArtistDetailResponse, + AlbumArtistInfoResponse, AlbumArtistListResponse, AlbumDetailResponse, AlbumListResponse, @@ -235,29 +236,44 @@ export const applyRatingOptimisticUpdates = ( return { ...prev, userRating: rating }; } - 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; }, }); } }); + const infoQueryKey = queryKeys.albumArtists.info(variables.apiClientProps.serverId); + const infoQueries = queryClient.getQueriesData({ + exact: false, + queryKey: infoQueryKey, + }); + + infoQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: AlbumArtistInfoResponse | null | undefined) => { + if (!prev?.similarArtists?.length) return prev; + + const hasMatching = prev.similarArtists.some((artist) => + itemIdSet.has(artist.id), + ); + if (!hasMatching) return prev; + + return { + ...prev, + similarArtists: prev.similarArtists.map((artist) => + itemIdSet.has(artist.id) + ? { ...artist, userRating: rating } + : artist, + ), + }; + }, + }); + } + }); + const listQueryKey = queryKeys.albumArtists.list(variables.apiClientProps.serverId); const listQueries = queryClient.getQueriesData({ exact: false, @@ -626,6 +642,10 @@ export const applyRatingOptimisticUpdatesDeferred = ( queryKeys.albumArtists.detail(variables.apiClientProps.serverId), 'album-artist-detail', ); + collectQueries( + queryKeys.albumArtists.info(variables.apiClientProps.serverId), + 'album-artist-info', + ); collectQueries( queryKeys.albumArtists.list(variables.apiClientProps.serverId), 'album-artist-list', @@ -695,19 +715,6 @@ export const applyRatingOptimisticUpdatesDeferred = ( if (itemIdSet.has(prev.id)) { return { ...prev, userRating: rating }; } - if (prev.similarArtists) { - const hasMatch = prev.similarArtists.some((a: any) => - itemIdSet.has(a.id), - ); - if (hasMatch) { - return { - ...prev, - similarArtists: prev.similarArtists.map((a: any) => - itemIdSet.has(a.id) ? { ...a, userRating: rating } : a, - ), - }; - } - } return prev; } case 'album-artist-infinite-list': @@ -732,6 +739,17 @@ export const applyRatingOptimisticUpdatesDeferred = ( } return prev; } + case 'album-artist-info': { + if (!prev?.similarArtists?.length) return prev; + const hasMatch = prev.similarArtists.some((a: any) => itemIdSet.has(a.id)); + if (!hasMatch) return prev; + return { + ...prev, + similarArtists: prev.similarArtists.map((a: any) => + itemIdSet.has(a.id) ? { ...a, userRating: rating } : a, + ), + }; + } case 'album-artist-list': case 'album-list': case 'artist-list': diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index 179cda88e..c661b55ba 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -820,6 +820,16 @@ export type AlbumArtistDetailQuery = { id: string }; export type AlbumArtistDetailResponse = AlbumArtist | null; +export type AlbumArtistInfoArgs = BaseEndpointArgs & { query: AlbumArtistInfoQuery }; + +export type AlbumArtistInfoQuery = { id: string; limit?: number }; + +export type AlbumArtistInfoResponse = { + biography?: null | string; + imageUrl?: null | string; + similarArtists: null | RelatedArtist[]; +}; + export type ArtistListArgs = BaseEndpointArgs & { query: ArtistListQuery }; export type ArtistListCountArgs = BaseEndpointArgs & { query: ListCountQuery }; @@ -1373,6 +1383,7 @@ export type ControllerEndpoint = { ) => Promise; deletePlaylist: (args: DeletePlaylistArgs) => Promise; getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise; + getAlbumArtistInfo?: (args: AlbumArtistInfoArgs) => Promise; getAlbumArtistList: (args: AlbumArtistListArgs) => Promise; getAlbumArtistListCount: (args: AlbumArtistListCountArgs) => Promise; getAlbumDetail: (args: AlbumDetailArgs) => Promise; @@ -1491,6 +1502,9 @@ export type InternalControllerEndpoint = { getAlbumArtistDetail: ( args: ReplaceApiClientProps, ) => Promise; + getAlbumArtistInfo?: ( + args: ReplaceApiClientProps, + ) => Promise; getAlbumArtistList: ( args: ReplaceApiClientProps, ) => Promise;