optimize artist page load speed

This commit is contained in:
jeffvli
2025-12-31 15:15:30 -08:00
parent 72475fbcc2
commit 37ed99d0fb
5 changed files with 126 additions and 81 deletions
@@ -218,24 +218,25 @@ export const JellyfinController: InternalControllerEndpoint = {
throw new Error('No userId found');
}
const res = await jfApiClient(apiClientProps).getAlbumArtistDetail({
params: {
id: query.id,
userId: apiClientProps.server?.userId,
},
query: {
Fields: 'Genres, Overview',
},
});
const similarArtistsRes = await jfApiClient(apiClientProps).getSimilarArtistList({
params: {
id: query.id,
},
query: {
Limit: 10,
},
});
const [res, similarArtistsRes] = await Promise.all([
jfApiClient(apiClientProps).getAlbumArtistDetail({
params: {
id: query.id,
userId: apiClientProps.server?.userId,
},
query: {
Fields: 'Genres, Overview',
},
}),
jfApiClient(apiClientProps).getSimilarArtistList({
params: {
id: query.id,
},
query: {
Limit: 10,
},
}),
]);
if (res.status !== 200 || similarArtistsRes.status !== 200) {
throw new Error('Failed to get album artist detail');
@@ -184,18 +184,19 @@ export const NavidromeController: InternalControllerEndpoint = {
getAlbumArtistDetail: async (args) => {
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).getAlbumArtistDetail({
params: {
id: query.id,
},
});
const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
query: {
count: 10,
id: query.id,
},
});
const [res, artistInfoRes] = await Promise.all([
ndApiClient(apiClientProps).getAlbumArtistDetail({
params: {
id: query.id,
},
}),
ssApiClient(apiClientProps).getArtistInfo({
query: {
count: 10,
id: query.id,
},
}),
]);
if (res.status !== 200) {
throw new Error('Failed to get album artist detail');
@@ -256,17 +256,18 @@ export const SubsonicController: InternalControllerEndpoint = {
getAlbumArtistDetail: async (args) => {
const { apiClientProps, query } = args;
const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
query: {
id: query.id,
},
});
const res = await ssApiClient(apiClientProps).getArtist({
query: {
id: query.id,
},
});
const [artistInfoRes, res] = await Promise.all([
ssApiClient(apiClientProps).getArtistInfo({
query: {
id: query.id,
},
}),
ssApiClient(apiClientProps).getArtist({
query: {
id: query.id,
},
}),
]);
if (res.status !== 200) {
throw new Error('Failed to get album artist detail');
@@ -1,8 +1,14 @@
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
import {
useQuery,
useQueryClient,
useSuspenseQuery,
UseSuspenseQueryResult,
} from '@tanstack/react-query';
import { LayoutGroup, motion } from 'motion/react';
import { Suspense } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { createSearchParams, generatePath, Link, useParams } from 'react-router';
import { createSearchParams, generatePath, Link, useLocation, useParams } from 'react-router';
import styles from './album-artist-detail-content.module.css';
@@ -16,7 +22,6 @@ import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { ItemControls } from '/@/renderer/components/item-list/types';
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { AlbumArtistGridCarousel } from '/@/renderer/features/artists/components/album-artist-grid-carousel';
import { useIsPlayerFetching, usePlayer } from '/@/renderer/features/player/context/player-context';
@@ -67,6 +72,7 @@ import {
Album,
AlbumArtist,
AlbumArtistDetailResponse,
AlbumListResponse,
AlbumListSort,
LibraryItem,
RelatedArtist,
@@ -214,7 +220,7 @@ interface AlbumArtistMetadataTopSongsProps {
routeId: string;
}
const AlbumArtistMetadataTopSongs = ({
const AlbumArtistMetadataTopSongsContent = ({
detailQuery,
routeId,
}: AlbumArtistMetadataTopSongsProps) => {
@@ -226,15 +232,22 @@ const AlbumArtistMetadataTopSongs = ({
const currentSong = usePlayerSong();
const player = usePlayer();
const serverId = useCurrentServerId();
const server = useCurrentServer();
const topSongsQuery = useSuspenseQuery(
artistsQueries.topSongs({
query: { artist: detailQuery.data?.name || '', artistId: routeId },
const canStartQuery = server?.type === ServerType.JELLYFIN || !!detailQuery.data?.name;
const topSongsQuery = useQuery({
...artistsQueries.topSongs({
query: {
artist: detailQuery.data?.name || '',
artistId: routeId,
},
serverId: serverId,
}),
);
enabled: canStartQuery,
});
const songs = useMemo(() => topSongsQuery?.data?.items || [], [topSongsQuery?.data?.items]);
const songs = useMemo(() => topSongsQuery.data?.items || [], [topSongsQuery.data?.items]);
const columns = useMemo(() => {
return tableConfig?.columns || [];
@@ -274,6 +287,10 @@ const AlbumArtistMetadataTopSongs = ({
};
}, [player]);
if (topSongsQuery.isLoading || !topSongsQuery.data) {
return null;
}
if (!topSongsQuery?.data?.items?.length) return null;
if (!tableConfig || columns.length === 0) {
@@ -404,6 +421,26 @@ const AlbumArtistMetadataTopSongs = ({
);
};
const AlbumArtistMetadataTopSongs = ({
detailQuery,
routeId,
}: AlbumArtistMetadataTopSongsProps) => {
const server = useCurrentServer();
const location = useLocation();
const artistName = location.state?.item?.name || detailQuery.data?.name;
const canStartQuery = server?.type === ServerType.JELLYFIN || !!artistName;
return (
<Suspense fallback={null}>
{canStartQuery ? (
<AlbumArtistMetadataTopSongsContent detailQuery={detailQuery} routeId={routeId} />
) : null}
</Suspense>
);
};
interface AlbumArtistMetadataExternalLinksProps {
artistName?: string;
externalLinks: boolean;
@@ -543,7 +580,15 @@ const AlbumArtistMetadataSimilarArtists = ({
);
};
export const AlbumArtistDetailContent = () => {
interface AlbumArtistDetailContentProps {
albumsQuery: UseSuspenseQueryResult<AlbumListResponse, Error>;
detailQuery: UseSuspenseQueryResult<AlbumArtistDetailResponse, Error>;
}
export const AlbumArtistDetailContent = ({
albumsQuery,
detailQuery,
}: AlbumArtistDetailContentProps) => {
const { artistItems, artistRadioCount, externalLinks, lastFM, musicBrainz } =
useGeneralSettings();
const { albumArtistId, artistId } = useParams() as {
@@ -567,13 +612,6 @@ export const AlbumArtistDetailContent = () => {
return [enabled, order];
}, [artistItems]);
const detailQuery = useSuspenseQuery(
artistsQueries.albumArtistDetail({
query: { id: routeId },
serverId: server?.id,
}),
);
const artistDiscographyLink = useMemo(
() =>
`${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY, {
@@ -662,7 +700,7 @@ export const AlbumArtistDetailContent = () => {
</Grid.Col>
)}
<Grid.Col order={itemOrder.recentAlbums} span={12}>
<ArtistAlbums />
<ArtistAlbums albumsQuery={albumsQuery} />
</Grid.Col>
{enabledItem.similarArtists && (
<Grid.Col order={itemOrder.similarArtists} span={12}>
@@ -1020,10 +1058,13 @@ const releaseTypeToEnumMap: Record<string, ArtistReleaseTypeItem> = {
spokenword: ArtistReleaseTypeItem.RELEASE_TYPE_SPOKENWORD,
};
const ArtistAlbums = () => {
interface ArtistAlbumsProps {
albumsQuery: UseSuspenseQueryResult<AlbumListResponse, Error>;
}
const ArtistAlbums = ({ albumsQuery }: ArtistAlbumsProps) => {
const { t } = useTranslation();
const { artistReleaseTypeItems } = useGeneralSettings();
const serverId = useCurrentServerId();
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
const albumArtistDetailSort = useAppStore((state) => state.albumArtistDetailSort);
@@ -1038,19 +1079,6 @@ const ArtistAlbums = () => {
};
const routeId = (artistId || albumArtistId) as string;
const albumsQuery = useSuspenseQuery(
albumQueries.list({
query: {
artistIds: [routeId],
limit: -1,
sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC,
startIndex: 0,
},
serverId,
}),
);
const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM);
const controls = useDefaultItemListControls();
@@ -1,9 +1,10 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { useSuspenseQueries } from '@tanstack/react-query';
import { Suspense, useRef } from 'react';
import { useParams } from 'react-router';
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';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { AlbumArtistDetailContent } from '/@/renderer/features/artists/components/album-artist-detail-content';
import { AlbumArtistDetailHeader } from '/@/renderer/features/artists/components/album-artist-detail-header';
@@ -16,14 +17,15 @@ import { LibraryContainer } from '/@/renderer/features/shared/components/library
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { useFastAverageColor, useWaitForColorCalculation } from '/@/renderer/hooks';
import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
import { useCurrentServer, useCurrentServerId, useGeneralSettings } from '/@/renderer/store';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { LibraryItem } from '/@/shared/types/domain-types';
import { AlbumListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-types';
const AlbumArtistDetailRouteContent = () => {
const scrollAreaRef = useRef<HTMLDivElement>(null);
const headerRef = useRef<HTMLDivElement>(null);
const server = useCurrentServer();
const serverId = useCurrentServerId();
const { artistBackground, artistBackgroundBlur } = useGeneralSettings();
const { albumArtistId, artistId } = useParams() as {
@@ -33,9 +35,21 @@ const AlbumArtistDetailRouteContent = () => {
const routeId = (artistId || albumArtistId) as string;
const detailQuery = useSuspenseQuery(
artistsQueries.albumArtistDetail({ query: { id: routeId }, serverId: server?.id }),
);
const [detailQuery, albumsQuery] = useSuspenseQueries({
queries: [
artistsQueries.albumArtistDetail({ query: { id: routeId }, serverId: server?.id }),
albumQueries.list({
query: {
artistIds: [routeId],
limit: -1,
sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC,
startIndex: 0,
},
serverId,
}),
],
});
const imageUrl = useItemImageUrl({
id: detailQuery.data?.imageId || undefined,
@@ -106,7 +120,7 @@ const AlbumArtistDetailRouteContent = () => {
)}
<LibraryContainer>
<AlbumArtistDetailHeader ref={headerRef as React.Ref<HTMLDivElement>} />
<AlbumArtistDetailContent />
<AlbumArtistDetailContent albumsQuery={albumsQuery} detailQuery={detailQuery} />
</LibraryContainer>
</NativeScrollArea>
</AnimatedPage>