Compare commits

...

2 Commits

Author SHA1 Message Date
Hosted Weblate 14bad5dbd7 Translated using Weblate
Currently translated at 100.0% (1287 of 1287 strings) (Chinese (Traditional Han script))
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hant/

Translated using Weblate

Currently translated at 69.1% (889 of 1285 strings) (Hungarian)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/hu/

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Soderes Sanyi <kennex@protonmail.com>
Co-authored-by: York <goog10216922@gmail.com>
2026-06-29 07:21:14 +02:00
BlackHoleFox 94aa34f6b2 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>
2026-06-29 05:21:04 +00:00
11 changed files with 145 additions and 118 deletions
+60 -21
View File
@@ -7,7 +7,10 @@
"viewPlaylists": "$t(entity.playlist, {\"count\": 2}) megtekintése", "viewPlaylists": "$t(entity.playlist, {\"count\": 2}) megtekintése",
"openIn": { "openIn": {
"lastfm": "Megnyitás Last.fm-ben", "lastfm": "Megnyitás Last.fm-ben",
"musicbrainz": "Megnyitás MusicBrainz-ben" "musicbrainz": "Megnyitás MusicBrainz-ben",
"listenbrainz": "Megnyitás ListenBrainz-ben",
"qobuz": "Megnyitás Qobuz_ban",
"spotify": "Megnyitás Spotify-ban"
}, },
"clearQueue": "Műsorlista kiürítése", "clearQueue": "Műsorlista kiürítése",
"createPlaylist": "$t(entity.playlist, {\"count\": 1}) létrehozása", "createPlaylist": "$t(entity.playlist, {\"count\": 1}) létrehozása",
@@ -117,7 +120,7 @@
"none": "Egyik sem", "none": "Egyik sem",
"restartRequired": "Újraindítás szükséges", "restartRequired": "Újraindítás szükséges",
"setting_one": "Beállítás", "setting_one": "Beállítás",
"setting_other": "", "setting_other": "Beállítások",
"translation": "Fordítás", "translation": "Fordítás",
"rating": "Értékelés", "rating": "Értékelés",
"reload": "Újratöltés", "reload": "Újratöltés",
@@ -154,7 +157,14 @@
"view": "Nézet", "view": "Nézet",
"noFilters": "Nincs konfigurált szűrő", "noFilters": "Nincs konfigurált szűrő",
"countSelected": "{{count}} kiválasztott", "countSelected": "{{count}} kiválasztott",
"retry": "Újra" "retry": "Újra",
"openFolder": "Mappa megnyitás",
"example": "Példa",
"filter_single": "Egy",
"filter_multiple": "Több",
"mood": "Hangulat",
"numberOfResults": "{{numberOfResults}} eredmény",
"grouping": "Csoportosítás"
}, },
"entity": { "entity": {
"albumArtist_one": "Zenész", "albumArtist_one": "Zenész",
@@ -207,8 +217,8 @@
"openError": "A fájl megnyitása sikertelen volt", "openError": "A fájl megnyitása sikertelen volt",
"playbackError": "Hiba történt a média lejátszásakor", "playbackError": "Hiba történt a média lejátszásakor",
"remoteEnableError": "Hiba történt a távoli szerver műveletkor: $t(common.enable)", "remoteEnableError": "Hiba történt a távoli szerver műveletkor: $t(common.enable)",
"remotePortError": "Hiba történt a távoli szerver PORT-jának beállításakor", "remotePortError": "Hiba történt a távoli szerver port-jának beállításakor",
"remotePortWarning": "Indítsd újra a szervert az új PORT használatához", "remotePortWarning": "Indítsd újra a szervert az új port használatához",
"genericError": "Hiba történt", "genericError": "Hiba történt",
"endpointNotImplementedError": "A(z) {{endpoint}} végpont nincs implementálva a következőhöz: {{serverType}}", "endpointNotImplementedError": "A(z) {{endpoint}} végpont nincs implementálva a következőhöz: {{serverType}}",
"badAlbum": "Azért látod ezt az oldalt mert ez a zeneszám nem része egy albumnak. ez általában akkor történik amikor egy szám a zenekönyvtárad gyökerébe kerül. a Jellyfin csak mappákba rendezett számokat csoportosít", "badAlbum": "Azért látod ezt az oldalt mert ez a zeneszám nem része egy albumnak. ez általában akkor történik amikor egy szám a zenekönyvtárad gyökerébe kerül. a Jellyfin csak mappákba rendezett számokat csoportosít",
@@ -226,7 +236,10 @@
"noNetworkDescription": "Nem tudok csatlakozni a szerverhez", "noNetworkDescription": "Nem tudok csatlakozni a szerverhez",
"saveQueueFailed": "Műsorlista mentése sikertelen", "saveQueueFailed": "Műsorlista mentése sikertelen",
"settingsSyncError": "Eltéréseket találtam a leképző és a fő folyamat beállításai között. Indítsd újra az alkalmazást", "settingsSyncError": "Eltéréseket találtam a leképző és a fő folyamat beállításai között. Indítsd újra az alkalmazást",
"multipleServerSaveQueueError": "A műsorlistában egy vagy több olyan dal található, amely nem az aktuális szerverről származik. Ez nem támogatott" "multipleServerSaveQueueError": "A műsorlistában egy vagy több olyan dal található, amely nem az aktuális szerverről származik. Ez nem támogatott",
"invalidJson": "Érvénytelen JSON",
"playbackPausedDueToError": "Lejátszás szüneteltetve hiba miatt",
"serverLockSingleServer": "A szerver zárolása esetén csak egy szerver engedélyezett"
}, },
"filter": { "filter": {
"albumCount": "$t(entity.album, {\"count\": 2}) darab", "albumCount": "$t(entity.album, {\"count\": 2}) darab",
@@ -269,9 +282,11 @@
"trackNumber": "Sáv", "trackNumber": "Sáv",
"artist": "$t(entity.artist, {\"count\": 1})", "artist": "$t(entity.artist, {\"count\": 1})",
"bpm": "Bpm", "bpm": "Bpm",
"channels": "$t(common.channel_other)", "channels": "$t(common.channel, {\"count\": 2})",
"genre": "$t(entity.genre, {\"count\": 1})", "genre": "$t(entity.genre, {\"count\": 1})",
"explicitStatus": "$t(common.explicitStatus)" "explicitStatus": "$t(common.explicitStatus)",
"matchAnd": "és",
"matchOr": "vagy"
}, },
"form": { "form": {
"addServer": { "addServer": {
@@ -671,7 +686,7 @@
"customFontPath_description": "Beállítja az alkalmazáshoz használandó egyéni betűtípus elérési útját", "customFontPath_description": "Beállítja az alkalmazáshoz használandó egyéni betűtípus elérési útját",
"contextMenu": "Kontextusmenü (jobb klikk) beállítás", "contextMenu": "Kontextusmenü (jobb klikk) beállítás",
"crossfadeDuration_description": "Beállítja áthúzás effekt időtartamát", "crossfadeDuration_description": "Beállítja áthúzás effekt időtartamát",
"crossfadeDuration": "Áthúzás Itartam", "crossfadeDuration": "Áthúzás Itartam",
"crossfadeStyle": "Áthúzás stílus", "crossfadeStyle": "Áthúzás stílus",
"crossfadeStyle_description": "Válaszd ki az audiolejátszóhoz használni kívánt áthúzás stílust", "crossfadeStyle_description": "Válaszd ki az audiolejátszóhoz használni kívánt áthúzás stílust",
"releaseChannel_description": "Válassz a stabil kiadás vagy a béta kiadás közül az automatikus frissítésekhez", "releaseChannel_description": "Válassz a stabil kiadás vagy a béta kiadás közül az automatikus frissítésekhez",
@@ -852,7 +867,7 @@
"sidebarPlaylistList_description": "Lejátszási lista megjelenítése vagy elrejtése az oldalsávban", "sidebarPlaylistList_description": "Lejátszási lista megjelenítése vagy elrejtése az oldalsávban",
"sidebarPlaylistList": "Oldalsáv lejátszási lista", "sidebarPlaylistList": "Oldalsáv lejátszási lista",
"sidePlayQueueStyle_description": "Beállítja az oldalsó műsorlista stílusát", "sidePlayQueueStyle_description": "Beállítja az oldalsó műsorlista stílusát",
"mediaSession_description": "Lehetővé teszi a Windows Media Session integrációját, a médiavezérlők és metaadatok megjelenítését a rendszer hangerő-átfedésben és a zárolási képernyőn (csak Windows)", "mediaSession_description": "Bekapcsolja a media session integrációját, a médiavezérlők és metaadatok megjelenítését a rendszer hangerő-átfedésben és a zárolási képernyőn. (Web audiolejátszó szükséges.)",
"mediaSession": "Média munkamenet engedélyezése", "mediaSession": "Média munkamenet engedélyezése",
"sidePlayQueueStyle": "Oldalsó műsorlista stílus", "sidePlayQueueStyle": "Oldalsó műsorlista stílus",
"skipDuration": "Átugrás hossza", "skipDuration": "Átugrás hossza",
@@ -918,7 +933,7 @@
"autoDJ": "Auto DJ", "autoDJ": "Auto DJ",
"autoDJ_timing": "Időzítés", "autoDJ_timing": "Időzítés",
"autoDJ_itemCount": "Elem szám", "autoDJ_itemCount": "Elem szám",
"autoDJ_itemCount_description": "Az auto DJ engedélyezésekor a műsorsorba felvenni kívánt elemek száma", "autoDJ_itemCount_description": "A műsorsorba felvenni kívánt elemek száma",
"autoDJ_timing_description": "Az auto DJ elindulása előtt a műsorlistában maradt dalok száma", "autoDJ_timing_description": "Az auto DJ elindulása előtt a műsorlistában maradt dalok száma",
"followCurrentSong_description": "Automatikusan görgesse a műsorlistát az aktuálisan lejátszott dalra", "followCurrentSong_description": "Automatikusan görgesse a műsorlistát az aktuálisan lejátszott dalra",
"followCurrentSong": "Kövesd az aktuális dalt", "followCurrentSong": "Kövesd az aktuális dalt",
@@ -959,13 +974,13 @@
"biography": "$t(common.biography)", "biography": "$t(common.biography)",
"bitrate": "$t(common.bitrate)", "bitrate": "$t(common.bitrate)",
"bpm": "$t(common.bpm)", "bpm": "$t(common.bpm)",
"channels": "$t(common.channel_other)", "channels": "$t(common.channel, {\"count\": 2})",
"codec": "$t(common.codec)", "codec": "$t(common.codec)",
"dateAdded": "Hozzáadva", "dateAdded": "Hozzáadva",
"discNumber": "Lemezszám", "discNumber": "Lemezszám",
"duration": "$t(common.duration)", "duration": "$t(common.duration)",
"favorite": "$t(common.favorite)", "favorite": "$t(common.favorite)",
"actions": "$t(common.action_other)", "actions": "$t(common.action, {\"count\": 2})",
"album": "$t(entity.album, {\"count\": 1})", "album": "$t(entity.album, {\"count\": 1})",
"albumCount": "$t(entity.album, {\"count\": 2})", "albumCount": "$t(entity.album, {\"count\": 2})",
"genreBadge": "$t(entity.genre, {\"count\": 1}) (jelvények)", "genreBadge": "$t(entity.genre, {\"count\": 1}) (jelvények)",
@@ -1011,33 +1026,33 @@
} }
}, },
"column": { "column": {
"albumCount": "$t(entity.album, {\"count\": 2})", "albumCount": "Albumok",
"artist": "$t(entity.artist, {\"count\": 1})", "artist": "Előadó",
"biography": "Életrajz", "biography": "Életrajz",
"bitrate": "Bitráta", "bitrate": "Bitráta",
"bpm": "BPM", "bpm": "BPM",
"channels": "$t(common.channel_other)", "channels": "Csatornák",
"codec": "$t(common.codec)", "codec": "$t(common.codec)",
"comment": "Komment", "comment": "Komment",
"dateAdded": "Hozzáadva", "dateAdded": "Hozzáadva",
"discNumber": "Lemez", "discNumber": "Lemez",
"favorite": "Kedvenc", "favorite": "Kedvenc",
"genre": "$t(entity.genre, {\"count\": 1})", "genre": "Műfaj",
"lastPlayed": "Utoljára játszott", "lastPlayed": "Utoljára játszott",
"path": "Elérési út", "path": "Elérési út",
"playCount": "Lejátszások", "playCount": "Lejátszások",
"rating": "Értékelés", "rating": "Értékelés",
"releaseDate": "Megjelenés", "releaseDate": "Megjelenés",
"releaseYear": "Év", "releaseYear": "Év",
"size": "$t(common.size)", "size": "Méret",
"songCount": "$t(entity.track, {\"count\": 2})", "songCount": "Sávok",
"title": "Cím", "title": "Cím",
"trackNumber": "Sáv", "trackNumber": "Sáv",
"album": "Album", "album": "Album",
"albumArtist": "Album előadó", "albumArtist": "Album előadó",
"owner": "Tulajdonos", "owner": "Tulajdonos",
"bitDepth": "$t(common.bitDepth)", "bitDepth": "Bitmélység",
"sampleRate": "$t(common.sampleRate)" "sampleRate": "Mintavételi frekvencia"
} }
}, },
"queryBuilder": { "queryBuilder": {
@@ -1070,5 +1085,29 @@
"secondShort": "Mp", "secondShort": "Mp",
"hourShort": "Óra", "hourShort": "Óra",
"dayShort": "Nap" "dayShort": "Nap"
},
"visualizer": {
"options": {
"weightingFilter": {
"a": "A",
"b": "B",
"c": "C",
"d": "D",
"z": "Z",
"none": "Nincs"
},
"mode": {
"1": "[1] 1/24 oktáv / 240 sáv",
"2": "[2] 1/12 oktáv / 120 sáv",
"3": "[3] 1/8 oktáv / 80 sáv",
"4": "[4] 1/6 oktáv / 60 sáv",
"5": "[5] 1/4 oktáv / 40 sáv",
"6": "[6] 1/3 oktáv / 30 sáv",
"7": "[7] Fél oktáv / 20 sáv",
"8": "[8] Teljes oktáv / 10 sáv",
"10": "[10] Vonal / Területdiagram"
}
},
"showFPS": "Mutat FPS"
} }
} }
+3 -1
View File
@@ -866,7 +866,9 @@
"compressorThreshold_description": "開始進行壓縮的訊號電平。", "compressorThreshold_description": "開始進行壓縮的訊號電平。",
"compressorThreshold": "閥值", "compressorThreshold": "閥值",
"enableRomaji_description": "在日文歌詞下方顯示羅馬拼音。", "enableRomaji_description": "在日文歌詞下方顯示羅馬拼音。",
"enableRomaji": "啟用羅馬拼音顯示" "enableRomaji": "啟用羅馬拼音顯示",
"windowBarTrackinfo": "在視窗標題列顯示曲目資訊",
"windowBarTrackinfo_description": "在視窗標題列中顯示目前播放曲目的標題與藝人、播放佇列中的位置,以及播放/暫停狀態。"
}, },
"table": { "table": {
"config": { "config": {
+12
View File
@@ -546,6 +546,18 @@ export const controller: GeneralController = {
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
}, },
getPlaylistSongIds(args) {
const server = getServerById(args.apiClientProps.serverId);
if (!server) {
throw new Error(`${i18n.t('error.apiRouteError')}: getPlaylistSongIds`);
}
return apiController(
'getPlaylistSongIds',
server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
},
getPlaylistSongList(args) { getPlaylistSongList(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -197,8 +197,8 @@ const JF_FIELDS = {
'SortName', 'SortName',
'ProviderIds', 'ProviderIds',
], ],
ALBUM_DETAIL: ['Genres', 'DateCreated', 'ChildCount', 'People', 'Tags', 'ProviderIds'], ALBUM_DETAIL: ['Genres', 'DateCreated', 'ChildCount', 'Tags', 'ProviderIds'],
ALBUM_LIST: ['People', 'Tags', 'Studios', 'SortName', 'ProviderIds', 'ChildCount'], ALBUM_LIST: ['Tags', 'Studios', 'SortName', 'ProviderIds', 'ChildCount'],
FOLDER: ['Genres', 'DateCreated', 'MediaSources', 'ParentId'], FOLDER: ['Genres', 'DateCreated', 'MediaSources', 'ParentId'],
GENRE: ['ItemCounts'], GENRE: ['ItemCounts'],
PLAYLIST_DETAIL: [ PLAYLIST_DETAIL: [
@@ -210,16 +210,7 @@ const JF_FIELDS = {
'SortName', 'SortName',
], ],
PLAYLIST_LIST: ['ChildCount', 'Genres', 'DateCreated', 'ParentId', 'Overview'], PLAYLIST_LIST: ['ChildCount', 'Genres', 'DateCreated', 'ParentId', 'Overview'],
SONG: [ SONG: ['Genres', 'DateCreated', 'MediaSources', 'ParentId', 'Tags', 'SortName', 'ProviderIds'],
'Genres',
'DateCreated',
'MediaSources',
'ParentId',
'People',
'Tags',
'SortName',
'ProviderIds',
],
} as const; } as const;
export const JellyfinController: InternalControllerEndpoint = { export const JellyfinController: InternalControllerEndpoint = {
@@ -1056,6 +1047,35 @@ export const JellyfinController: InternalControllerEndpoint = {
apiClientProps, apiClientProps,
query: { ...query, limit: 1, startIndex: 0 }, query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!), }).then((result) => result!.totalRecordCount!),
getPlaylistSongIds: async (args) => {
const { apiClientProps, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
}
const res = await jfApiClient(apiClientProps).getPlaylistSongList({
params: {
id: query.id,
},
query: {
// XXX: No fields are required for only IDs, which saves processing time between
// the Jellyfin server query, network (MBs vs KBs), and in-app parsing.
IncludeItemTypes: 'Audio',
UserId: apiClientProps.server?.userId,
},
});
if (res.status !== 200) {
throw new Error('Failed to get playlist song list IDs');
}
return {
items: res.body.Items.map((item) => item.Id),
startIndex: 0,
totalRecordCount: res.body.TotalRecordCount,
};
},
getPlaylistSongList: async (args) => { getPlaylistSongList: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -1068,7 +1088,7 @@ export const JellyfinController: InternalControllerEndpoint = {
id: query.id, id: query.id,
}, },
query: { query: {
Fields: JF_FIELDS.SONG, Fields: JF_FIELDS.PLAYLIST_DETAIL,
IncludeItemTypes: 'Audio', IncludeItemTypes: 'Audio',
UserId: apiClientProps.server?.userId, UserId: apiClientProps.server?.userId,
}, },
@@ -663,6 +663,11 @@ export const NavidromeController: InternalControllerEndpoint = {
apiClientProps, apiClientProps,
query: { ...query, limit: 1, startIndex: 0 }, query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!), }).then((result) => result!.totalRecordCount!),
getPlaylistSongIds: async (args) =>
NavidromeController.getPlaylistSongList(args).then((result) => ({
...result,
items: result.items.map((song) => song.id),
})),
getPlaylistSongList: async (args: PlaylistSongListArgs): Promise<PlaylistSongListResponse> => { getPlaylistSongList: async (args: PlaylistSongListArgs): Promise<PlaylistSongListResponse> => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
+3
View File
@@ -338,6 +338,9 @@ export const queryKeys: Record<
return [serverId, 'playlists', 'songList'] as const; return [serverId, 'playlists', 'songList'] as const;
}, },
songListIds: (serverId: string, id: string) => {
return [serverId, 'playlists', 'songListIds', id] as const;
},
}, },
radio: { radio: {
list: (serverId: string) => [serverId, 'radio', 'list'] as const, list: (serverId: string) => [serverId, 'radio', 'list'] as const,
@@ -1229,6 +1229,11 @@ export const SubsonicController: InternalControllerEndpoint = {
return results.length; return results.length;
}, },
getPlaylistSongIds: async (args) =>
SubsonicController.getPlaylistSongList(args).then((result) => ({
...result,
items: result.items.map((song) => song.id),
})),
getPlaylistSongList: async ({ apiClientProps, query }) => { getPlaylistSongList: async ({ apiClientProps, query }) => {
const res = await ssApiClient(apiClientProps).getPlaylist({ const res = await ssApiClient(apiClientProps).getPlaylist({
query: { query: {
@@ -211,11 +211,11 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
let songsToAdd: string[] = allSongIds; let songsToAdd: string[] = allSongIds;
if (skipDuplicates) { if (skipDuplicates) {
const queryKey = queryKeys.playlists.songList(serverId, playlistId); const queryKey = queryKeys.playlists.songListIds(serverId, playlistId);
const playlistSongsRes = await queryClient.fetchQuery({ const playlistSongsRes = await queryClient.fetchQuery({
queryFn: ({ signal }) => { queryFn: ({ signal }) => {
return api.controller.getPlaylistSongList({ return api.controller.getPlaylistSongIds({
apiClientProps: { apiClientProps: {
serverId, serverId,
signal, signal,
@@ -228,7 +228,7 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
queryKey, queryKey,
}); });
const playlistSongIds = playlistSongsRes?.items?.map((song) => song.id); const playlistSongIds = playlistSongsRes?.items;
const uniqueSongIds: string[] = []; const uniqueSongIds: string[] = [];
for (const songId of allSongIds) { for (const songId of allSongIds) {
@@ -231,11 +231,11 @@ export const AddToPlaylistContextModal = ({
const uniqueSongIds: string[] = []; const uniqueSongIds: string[] = [];
if (values.skipDuplicates) { if (values.skipDuplicates) {
const queryKey = queryKeys.playlists.songList(serverId, playlistId); const queryKey = queryKeys.playlists.songListIds(serverId, playlistId);
const playlistSongsRes = await queryClient.fetchQuery({ const playlistSongsRes = await queryClient.fetchQuery({
queryFn: ({ signal }) => { queryFn: ({ signal }) => {
return api.controller.getPlaylistSongList({ return api.controller.getPlaylistSongIds({
apiClientProps: { apiClientProps: {
serverId, serverId,
signal, signal,
@@ -248,7 +248,7 @@ export const AddToPlaylistContextModal = ({
queryKey, queryKey,
}); });
const playlistSongIds = playlistSongsRes?.items?.map((song) => song.id); const playlistSongIds = playlistSongsRes?.items;
for (const songId of allSongIds) { for (const songId of allSongIds) {
if (!playlistSongIds?.includes(songId)) { if (!playlistSongIds?.includes(songId)) {
+12 -77
View File
@@ -10,7 +10,6 @@ import {
LibraryItem, LibraryItem,
MusicFolder, MusicFolder,
Playlist, Playlist,
RelatedArtist,
Song, Song,
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
import { ServerListItem, ServerType } from '/@/shared/types/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>; 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[]> => { const getTags = (item: AlbumOrSong): null | Record<string, string[]> => {
if (item.Tags) { if (item.Tags) {
const tags: Record<string, string[]> = {}; const tags: Record<string, string[]> = {};
@@ -106,39 +69,6 @@ const getPlaylistImageId = (item: z.infer<typeof jfType._response.playlist>): nu
return null; 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: { const jellyfinPremiereFields = (item: {
PremiereDate?: string; PremiereDate?: string;
ProductionYear?: number; ProductionYear?: number;
@@ -189,10 +119,6 @@ const normalizeSong = (
console.warn('Jellyfin song retrieved with no media sources', item); console.warn('Jellyfin song retrieved with no media sources', item);
} }
const participants = getPeople(item);
const artists = getArtists(item, participants);
const { releaseDate, releaseYear } = jellyfinPremiereFields(item); const { releaseDate, releaseYear } = jellyfinPremiereFields(item);
return { return {
@@ -211,7 +137,16 @@ const normalizeSong = (
})), })),
albumId: item.AlbumId || `dummy/${item.Id}`, albumId: item.AlbumId || `dummy/${item.Id}`,
artistName: item?.ArtistItems?.map((entry) => entry.Name).join(', ') || '', 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, bitDepth,
bitRate, bitRate,
bpm: null, bpm: null,
@@ -253,7 +188,7 @@ const normalizeSong = (
mbzRecordingId: null, mbzRecordingId: null,
mbzTrackId: item.ProviderIds?.MusicBrainzTrack || null, mbzTrackId: item.ProviderIds?.MusicBrainzTrack || null,
name: item.Name, name: item.Name,
participants, participants: null,
path: path || '', path: path || '',
peak: null, peak: null,
playCount: (item.UserData && item.UserData.PlayCount) || 0, playCount: (item.UserData && item.UserData.PlayCount) || 0,
@@ -328,7 +263,7 @@ const normalizeAlbum = (
name: item.Name, name: item.Name,
originalDate: releaseDate, originalDate: releaseDate,
originalYear, originalYear,
participants: getPeople(item), participants: null,
playCount: item.UserData?.PlayCount || 0, playCount: item.UserData?.PlayCount || 0,
recordLabels: item.Studios?.map((entry) => entry.Name) || [], recordLabels: item.Studios?.map((entry) => entry.Name) || [],
releaseDate, releaseDate,
+6
View File
@@ -636,6 +636,8 @@ export type AlbumInfo = {
notes: null | string; notes: null | string;
}; };
export type SongIdListResponse = BasePaginatedResponse<string[]>;
export type SongListArgs = BaseEndpointArgs & { query: SongListQuery }; export type SongListArgs = BaseEndpointArgs & { query: SongListQuery };
export type SongListCountArgs = BaseEndpointArgs & { query: ListCountQuery<SongListQuery> }; export type SongListCountArgs = BaseEndpointArgs & { query: ListCountQuery<SongListQuery> };
@@ -1522,6 +1524,7 @@ export type ControllerEndpoint = {
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>; getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>; getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
getPlaylistListCount: (args: PlaylistListCountArgs) => Promise<number>; getPlaylistListCount: (args: PlaylistListCountArgs) => Promise<number>;
getPlaylistSongIds: (args: PlaylistSongListArgs) => Promise<SongIdListResponse>;
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>; getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
getPlayQueue: (args: GetQueueArgs) => Promise<GetQueueResponse>; getPlayQueue: (args: GetQueueArgs) => Promise<GetQueueResponse>;
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>; getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
@@ -1676,6 +1679,9 @@ export type InternalControllerEndpoint = {
args: ReplaceApiClientProps<PlaylistListArgs>, args: ReplaceApiClientProps<PlaylistListArgs>,
) => Promise<PlaylistListResponse>; ) => Promise<PlaylistListResponse>;
getPlaylistListCount: (args: ReplaceApiClientProps<PlaylistListCountArgs>) => Promise<number>; getPlaylistListCount: (args: ReplaceApiClientProps<PlaylistListCountArgs>) => Promise<number>;
getPlaylistSongIds: (
args: ReplaceApiClientProps<PlaylistSongListArgs>,
) => Promise<SongIdListResponse>;
getPlaylistSongList: ( getPlaylistSongList: (
args: ReplaceApiClientProps<PlaylistSongListArgs>, args: ReplaceApiClientProps<PlaylistSongListArgs>,
) => Promise<SongListResponse>; ) => Promise<SongListResponse>;