Improve Jellyfin playlist loading and modification performance times (#2184)

* Remove unneeded Fields from getPlaylistSongList

* Add optimized controller function for playlist addition duplication checks

* Remove Jellyfin People data handling

* move artist map inline

---------

Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
This commit is contained in:
BlackHoleFox
2026-06-29 00:21:04 -05:00
committed by GitHub
parent da445b815d
commit 94aa34f6b2
9 changed files with 82 additions and 96 deletions
+12 -77
View File
@@ -10,7 +10,6 @@ import {
LibraryItem,
MusicFolder,
Playlist,
RelatedArtist,
Song,
} from '/@/shared/types/domain-types';
import { ServerListItem, ServerType } from '/@/shared/types/types';
@@ -19,42 +18,6 @@ const TICKS_PER_MS = 10000;
type AlbumOrSong = z.infer<typeof jfType._response.album> | z.infer<typeof jfType._response.song>;
const KEYS_TO_OMIT = new Set(['AlbumArtist', 'Artist']);
const getPeople = (item: AlbumOrSong): null | Record<string, RelatedArtist[]> => {
if (item.People) {
const participants: Record<string, RelatedArtist[]> = {};
for (const person of item.People) {
const key = person.Type || '';
if (KEYS_TO_OMIT.has(key)) {
continue;
}
const item: RelatedArtist = {
// for other roles, we just want to display this and not filter.
// filtering (and links) would require a separate field, PersonIds
id: '',
imageId: null,
imageUrl: null,
name: person.Name,
userFavorite: false,
userRating: null,
};
if (key in participants) {
participants[key].push(item);
} else {
participants[key] = [item];
}
}
return participants;
}
return null;
};
const getTags = (item: AlbumOrSong): null | Record<string, string[]> => {
if (item.Tags) {
const tags: Record<string, string[]> = {};
@@ -106,39 +69,6 @@ const getPlaylistImageId = (item: z.infer<typeof jfType._response.playlist>): nu
return null;
};
const getArtists = (
item: z.infer<typeof jfType._response.song>,
participants?: null | Record<string, RelatedArtist[]>,
): 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']) {
const existingIds = new Set(result.map((artist) => artist.id));
for (const participant of participants['Remixer']) {
if (!existingIds.has(participant.id)) {
result.push(participant);
}
}
}
return result;
};
const jellyfinPremiereFields = (item: {
PremiereDate?: string;
ProductionYear?: number;
@@ -189,10 +119,6 @@ const normalizeSong = (
console.warn('Jellyfin song retrieved with no media sources', item);
}
const participants = getPeople(item);
const artists = getArtists(item, participants);
const { releaseDate, releaseYear } = jellyfinPremiereFields(item);
return {
@@ -211,7 +137,16 @@ const normalizeSong = (
})),
albumId: item.AlbumId || `dummy/${item.Id}`,
artistName: item?.ArtistItems?.map((entry) => entry.Name).join(', ') || '',
artists,
artists: (item?.ArtistItems?.length ? item.ArtistItems : item.AlbumArtists)?.map(
(entry) => ({
id: entry.Id,
imageId: null,
imageUrl: null,
name: entry.Name,
userFavorite: false,
userRating: null,
}),
),
bitDepth,
bitRate,
bpm: null,
@@ -253,7 +188,7 @@ const normalizeSong = (
mbzRecordingId: null,
mbzTrackId: item.ProviderIds?.MusicBrainzTrack || null,
name: item.Name,
participants,
participants: null,
path: path || '',
peak: null,
playCount: (item.UserData && item.UserData.PlayCount) || 0,
@@ -328,7 +263,7 @@ const normalizeAlbum = (
name: item.Name,
originalDate: releaseDate,
originalYear,
participants: getPeople(item),
participants: null,
playCount: item.UserData?.PlayCount || 0,
recordLabels: item.Studios?.map((entry) => entry.Name) || [],
releaseDate,
+6
View File
@@ -636,6 +636,8 @@ export type AlbumInfo = {
notes: null | string;
};
export type SongIdListResponse = BasePaginatedResponse<string[]>;
export type SongListArgs = BaseEndpointArgs & { query: SongListQuery };
export type SongListCountArgs = BaseEndpointArgs & { query: ListCountQuery<SongListQuery> };
@@ -1522,6 +1524,7 @@ export type ControllerEndpoint = {
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
getPlaylistListCount: (args: PlaylistListCountArgs) => Promise<number>;
getPlaylistSongIds: (args: PlaylistSongListArgs) => Promise<SongIdListResponse>;
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
getPlayQueue: (args: GetQueueArgs) => Promise<GetQueueResponse>;
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
@@ -1676,6 +1679,9 @@ export type InternalControllerEndpoint = {
args: ReplaceApiClientProps<PlaylistListArgs>,
) => Promise<PlaylistListResponse>;
getPlaylistListCount: (args: ReplaceApiClientProps<PlaylistListCountArgs>) => Promise<number>;
getPlaylistSongIds: (
args: ReplaceApiClientProps<PlaylistSongListArgs>,
) => Promise<SongIdListResponse>;
getPlaylistSongList: (
args: ReplaceApiClientProps<PlaylistSongListArgs>,
) => Promise<SongListResponse>;