Support tags, and better participants for servers

- Parses `tags` for Navidrome (mapping string: string[])
- Parses `Tags` (and fetches for it) for Jellyfin (map a string to empty, and display as a bool)
- Clean parsing of participants for Navidrome/Subsonic
- Only show `People` for Jellyfin, not clickable
This commit is contained in:
Kendall Garner 2025-05-17 21:35:58 -07:00
parent 89e27ec6ff
commit b0d86ee5c9
No known key found for this signature in database
GPG key ID: 9355F387FE765C94
9 changed files with 131 additions and 13 deletions

View file

@ -246,7 +246,7 @@ export const JellyfinController: ControllerEndpoint = {
userId: apiClientProps.server.userId,
},
query: {
Fields: 'Genres, DateCreated, ChildCount',
Fields: 'Genres, DateCreated, ChildCount, People, Tags',
},
});
@ -255,7 +255,7 @@ export const JellyfinController: ControllerEndpoint = {
userId: apiClientProps.server.userId,
},
query: {
Fields: 'Genres, DateCreated, MediaSources, ParentId',
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags',
IncludeItemTypes: 'Audio',
ParentId: query.id,
SortBy: 'ParentIndexNumber,IndexNumber,SortName',
@ -300,6 +300,7 @@ export const JellyfinController: ControllerEndpoint = {
query.artistIds && {
ContributingArtistIds: query.artistIds[0],
}),
Fields: 'People, Tags',
GenreIds: query.genres ? query.genres.join(',') : undefined,
IncludeItemTypes: 'MusicAlbum',
IsFavorite: query.favorite,
@ -523,7 +524,7 @@ export const JellyfinController: ControllerEndpoint = {
id: query.id,
},
query: {
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId',
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags',
IncludeItemTypes: 'Audio',
Limit: query.limit,
SortBy: query.sortBy ? songListSortMap.jellyfin[query.sortBy] : undefined,
@ -564,7 +565,7 @@ export const JellyfinController: ControllerEndpoint = {
userId: apiClientProps.server?.userId,
},
query: {
Fields: 'Genres, DateCreated, MediaSources, ParentId',
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags',
GenreIds: query.genre ? query.genre : undefined,
IncludeItemTypes: 'Audio',
IsPlayed:
@ -719,7 +720,7 @@ export const JellyfinController: ControllerEndpoint = {
query: {
AlbumIds: albumIdsFilter,
ArtistIds: artistIdsFilter,
Fields: 'Genres, DateCreated, MediaSources, ParentId',
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags',
GenreIds: query.genreIds?.join(','),
IncludeItemTypes: 'Audio',
IsFavorite: query.favorite,
@ -754,7 +755,7 @@ export const JellyfinController: ControllerEndpoint = {
query: {
AlbumIds: albumIdsFilter,
ArtistIds: artistIdsFilter,
Fields: 'Genres, DateCreated, MediaSources, ParentId',
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags',
GenreIds: query.genreIds?.join(','),
IncludeItemTypes: 'Audio',
IsFavorite: query.favorite,
@ -967,6 +968,7 @@ export const JellyfinController: ControllerEndpoint = {
},
query: {
EnableTotalRecordCount: true,
Fields: 'People, Tags',
ImageTypeLimit: 1,
IncludeItemTypes: 'MusicAlbum',
Limit: query.albumLimit,
@ -1014,7 +1016,7 @@ export const JellyfinController: ControllerEndpoint = {
},
query: {
EnableTotalRecordCount: true,
Fields: 'Genres, DateCreated, MediaSources, ParentId',
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags',
IncludeItemTypes: 'Audio',
Limit: query.songLimit,
Recursive: true,

View file

@ -12,6 +12,7 @@ import {
Genre,
ServerListItem,
ServerType,
RelatedArtist,
} from '/@/renderer/api/types';
const getStreamUrl = (args: {
@ -121,6 +122,48 @@ const getPlaylistCoverArtUrl = (args: { baseUrl: string; item: JFPlaylist; size:
);
};
type AlbumOrSong = z.infer<typeof jfType._response.song> | z.infer<typeof jfType._response.album>;
const getPeople = (item: AlbumOrSong): Record<string, RelatedArtist[]> | null => {
if (item.People) {
const participants: Record<string, RelatedArtist[]> = {};
for (const person of item.People) {
const key = person.Type || '';
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: '',
imageUrl: null,
name: person.Name,
};
if (key in participants) {
participants[key].push(item);
} else {
participants[key] = [item];
}
}
return participants;
}
return null;
};
const getTags = (item: AlbumOrSong): Record<string, string[]> | null => {
if (item.Tags) {
const tags: Record<string, string[]> = {};
for (const tag of item.Tags) {
tags[tag] = [];
}
return tags;
}
return null;
};
const normalizeSong = (
item: z.infer<typeof jfType._response.song>,
server: ServerListItem | null,
@ -176,7 +219,7 @@ const normalizeSong = (
lastPlayedAt: null,
lyrics: null,
name: item.Name,
participants: null,
participants: getPeople(item),
path: (item.MediaSources && item.MediaSources[0]?.Path) || null,
peak: null,
playCount: (item.UserData && item.UserData.PlayCount) || 0,
@ -198,6 +241,7 @@ const normalizeSong = (
mediaSourceId: item.MediaSources?.[0]?.Id,
server,
}),
tags: getTags(item),
trackNumber: item.IndexNumber,
uniqueId: nanoid(),
updatedAt: item.DateCreated,
@ -247,7 +291,7 @@ const normalizeAlbum = (
mbzId: item.ProviderIds?.MusicBrainzAlbum || null,
name: item.Name,
originalDate: null,
participants: null,
participants: getPeople(item),
playCount: item.UserData?.PlayCount || 0,
releaseDate: item.PremiereDate?.split('T')[0] || null,
releaseYear: item.ProductionYear || null,
@ -256,6 +300,7 @@ const normalizeAlbum = (
size: null,
songCount: item?.ChildCount || null,
songs: item.Songs?.map((song) => normalizeSong(song, server, '', imageSize)),
tags: getTags(item),
uniqueId: nanoid(),
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
userFavorite: item.UserData?.IsFavorite || false,

View file

@ -387,6 +387,12 @@ const genericItem = z.object({
Name: z.string(),
});
const participant = z.object({
Id: z.string(),
Name: z.string(),
Type: z.string().optional(),
});
const songDetailParameters = baseParameters;
const song = z.object({
@ -415,12 +421,14 @@ const song = z.object({
Name: z.string(),
NormalizationGain: z.number().optional(),
ParentIndexNumber: z.number(),
People: participant.array().optional(),
PlaylistItemId: z.string().optional(),
PremiereDate: z.string().optional(),
ProductionYear: z.number(),
RunTimeTicks: z.number(),
ServerId: z.string(),
SortName: z.string(),
Tags: z.string().array().optional(),
Type: z.string(),
UserData: userData.optional(),
});
@ -475,12 +483,14 @@ const album = z.object({
Name: z.string(),
ParentLogoImageTag: z.string(),
ParentLogoItemId: z.string(),
People: participant.array().optional(),
PremiereDate: z.string().optional(),
ProductionYear: z.number(),
ProviderIds: providerIds.optional(),
RunTimeTicks: z.number(),
ServerId: z.string(),
Songs: z.array(song).optional(), // This is not a native Jellyfin property -- this is used for combined album detail
Tags: z.string().array().optional(),
Type: z.string(),
UserData: userData.optional(),
});