prepare bfr changes (#882)

* prepare bfr changes

* contributors to subsonic/navidrome

* show performer roles

* Add BFR smart playlist fields

* Fix upload-artifact action to handle v4

---------

Co-authored-by: jeffvli <jeffvictorli@gmail.com>
This commit is contained in:
Kendall Garner 2025-03-09 23:55:27 +00:00 committed by GitHub
parent 571aacbaa0
commit c6d7dc0b32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 378 additions and 88 deletions

View file

@ -1,6 +1,7 @@
// Should follow a strict naming convention: "<FEATURE GROUP>_<FEATURE NAME>"
// For example: <FEATURE GROUP>: "Playlists", <FEATURE NAME>: "Smart" = "PLAYLISTS_SMART"
export enum ServerFeature {
BFR = 'bfr',
LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured',
LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured',
PLAYLISTS_SMART = 'playlistsSmart',

View file

@ -176,6 +176,7 @@ const normalizeSong = (
lastPlayedAt: null,
lyrics: null,
name: item.Name,
participants: null,
path: (item.MediaSources && item.MediaSources[0]?.Path) || null,
peak: null,
playCount: (item.UserData && item.UserData.PlayCount) || 0,
@ -235,6 +236,7 @@ const normalizeAlbum = (
})),
id: item.Id,
imagePlaceholderUrl: null,
participants: null,
imageUrl: getAlbumCoverArtUrl({
baseUrl: server?.url || '',
item,

View file

@ -381,39 +381,99 @@ export type NDPlaylistSongList = {
export const NDSongQueryFields = [
{ label: 'Album', type: 'string', value: 'album' },
{ label: 'Album Artist', type: 'string', value: 'albumartist' },
{ label: 'Album Artists', type: 'string', value: 'albumartists' },
{ label: 'Album Comment', type: 'string', value: 'albumcomment' },
{ label: 'Album Type', type: 'string', value: 'albumtype' },
{ label: 'Album Version', type: 'string', value: 'albumversion' },
{ label: 'Arranger', type: 'string', value: 'arranger' },
{ label: 'Artist', type: 'string', value: 'artist' },
{ label: 'Artists', type: 'string', value: 'artists' },
{ label: 'Barcode', type: 'string', value: 'barcode' },
{ label: 'Bitrate', type: 'number', value: 'bitrate' },
{ label: 'BPM', type: 'number', value: 'bpm' },
{ label: 'Catalog Number', type: 'string', value: 'catalognumber' },
{ label: 'Channels', type: 'number', value: 'channels' },
{ label: 'Comment', type: 'string', value: 'comment' },
{ label: 'Composer', type: 'string', value: 'composer' },
{ label: 'Conductor', type: 'string', value: 'conductor' },
{ label: 'Copyright', type: 'string', value: 'copyright' },
{ label: 'Date Added', type: 'date', value: 'dateadded' },
{ label: 'Date Favorited', type: 'date', value: 'dateloved' },
{ label: 'Date Last Played', type: 'date', value: 'lastplayed' },
{ label: 'Date Modified', type: 'date', value: 'datemodified' },
{ label: 'Disc Subtitle', type: 'string', value: 'discsubtitle' },
{ label: 'DJ Mixer', type: 'string', value: 'djmixer' },
{ label: 'Director', type: 'string', value: 'director' },
{ label: 'Disc Number', type: 'number', value: 'discnumber' },
{ label: 'Disc Subtitle', type: 'string', value: 'discsubtitle' },
{ label: 'Disc Total', type: 'number', value: 'disctotal' },
{ label: 'Duration', type: 'number', value: 'duration' },
{ label: 'Encoded By', type: 'string', value: 'encodedby' },
{ label: 'Encoder Settings', type: 'string', value: 'encodersettings' },
{ label: 'Engineer', type: 'string', value: 'engineer' },
{ label: 'Explicit Status', type: 'string', value: 'explicitstatus' },
{ label: 'File Path', type: 'string', value: 'filepath' },
{ label: 'File Type', type: 'string', value: 'filetype' },
{ label: 'Genre', type: 'string', value: 'genre' },
{ label: 'Grouping', type: 'string', value: 'grouping' },
{ label: 'Has CoverArt', type: 'boolean', value: 'hascoverart' },
{ label: 'Playlist', type: 'playlist', value: 'id' },
{ label: 'Is Compilation', type: 'boolean', value: 'compilation' },
{ label: 'Is Favorite', type: 'boolean', value: 'loved' },
{ label: 'ISRC', type: 'string', value: 'isrc' },
{ label: 'Key', type: 'string', value: 'key' },
{ label: 'Language', type: 'string', value: 'language' },
{ label: 'License', type: 'string', value: 'license' },
{ label: 'Lyricist', type: 'string', value: 'lyricist' },
{ label: 'Lyrics', type: 'string', value: 'lyrics' },
{ label: 'Media', type: 'string', value: 'media' },
{ label: 'Mixer', type: 'string', value: 'mixer' },
{ label: 'Mood', type: 'string', value: 'mood' },
{ label: 'Movement', type: 'string', value: 'movement' },
{ label: 'Movement Name', type: 'string', value: 'movementname' },
{ label: 'Movement Total', type: 'number', value: 'movementtotal' },
{ label: 'MusicBrainz Artist Id', type: 'string', value: 'musicbrainz_albumartistid' },
{ label: 'MusicBrainz Album Artist Id', type: 'string', value: 'musicbrainz_albumartistid' },
{ label: 'MusicBrainz Album Id', type: 'string', value: 'musicbrainz_albumid' },
{ label: 'MusicBrainz Disc Id', type: 'string', value: 'musicbrainz_discid' },
{ label: 'MusicBrainz Recording Id', type: 'string', value: 'musicbrainz_recordingid' },
{ label: 'MusicBrainz Release Group Id', type: 'string', value: 'musicbrainz_releasegroupid' },
{ label: 'MusicBrainz Track Id', type: 'string', value: 'musicbrainz_trackid' },
{ label: 'MusicBrainz Work Id', type: 'string', value: 'musicbrainz_workid' },
{ label: 'Name', type: 'string', value: 'title' },
{ label: 'Original Date', type: 'date', value: 'originaldate' },
{ label: 'Performer', type: 'string', value: 'performer' },
{ label: 'Play Count', type: 'number', value: 'playcount' },
{ label: 'Playlist', type: 'playlist', value: 'id' },
{ label: 'Producer', type: 'string', value: 'producer' },
{ label: 'R128 Album Gain', type: 'number', value: 'r128_album_gain' },
{ label: 'R128 Track Gain', type: 'number', value: 'r128_track_gain' },
{ label: 'Rating', type: 'number', value: 'rating' },
{ label: 'Record Label', type: 'string', value: 'recordlabel' },
{ label: 'Recording Date', type: 'date', value: 'recordingdate' },
{ label: 'Release Country', type: 'string', value: 'releasecountry' },
{ label: 'Release Date', type: 'date', value: 'releasedate' },
{ label: 'Release Status', type: 'string', value: 'releasestatus' },
{ label: 'Release Type', type: 'string', value: 'releasetype' },
{ label: 'ReplayGain Album Gain', type: 'number', value: 'replaygain_album_gain' },
{ label: 'ReplayGain Album Peak', type: 'number', value: 'replaygain_album_peak' },
{ label: 'ReplayGain Track Gain', type: 'number', value: 'replaygain_track_gain' },
{ label: 'ReplayGain Track Peak', type: 'number', value: 'replaygain_track_peak' },
{ label: 'Remixer', type: 'string', value: 'remixer' },
{ label: 'Script', type: 'string', value: 'script' },
{ label: 'Size', type: 'number', value: 'size' },
{ label: 'Sort Album', type: 'string', value: 'sortalbum' },
{ label: 'Sort Album Artist', type: 'string', value: 'sortalbumartist' },
{ label: 'Sort Artist', type: 'string', value: 'sortartist' },
{ label: 'Sort Name', type: 'string', value: 'sorttitle' },
{ label: 'Track Number', type: 'number', value: 'tracknumber' },
{ label: 'Sort Album', type: 'string', value: 'albumsort' },
{ label: 'Sort Album Artist', type: 'string', value: 'albumartistsort' },
{ label: 'Sort Album Artists', type: 'string', value: 'albumartistssort' },
{ label: 'Sort Artist', type: 'string', value: 'artistsort' },
{ label: 'Sort Artists', type: 'string', value: 'artistssort' },
{ label: 'Sort Composer', type: 'string', value: 'composersort' },
{ label: 'Sort Lyricist', type: 'string', value: 'lyricistsort' },
{ label: 'Sort Name', type: 'string', value: 'titlesort' },
{ label: 'Subtitle', type: 'string', value: 'subtitle' },
{ label: 'Track Number', type: 'number', value: 'track' },
{ label: 'Track Total', type: 'number', value: 'tracktotal' },
{ label: 'Year', type: 'number', value: 'year' },
{ label: 'Website', type: 'string', value: 'website' },
{ label: 'Work', type: 'string', value: 'work' },
];
export const NDSongQueryPlaylistOperators = [

View file

@ -15,6 +15,7 @@ import {
genreListSortMap,
Song,
ControllerEndpoint,
ServerListItem,
} from '../types';
import { VersionInfo, getFeatures, hasFeature } from '/@/renderer/api/utils';
import { ServerFeature, ServerFeatures } from '/@/renderer/api/features-types';
@ -24,10 +25,19 @@ import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize';
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
const VERSION_INFO: VersionInfo = [
['0.55.0', { [ServerFeature.BFR]: [1] }],
['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
];
const excludeMissing = (server: ServerListItem | null) => {
if (hasFeature(server, ServerFeature.BFR)) {
return { missing: false };
}
return undefined;
};
export const NavidromeController: ControllerEndpoint = {
addToPlaylist: async (args) => {
const { body, query, apiClientProps } = args;
@ -159,6 +169,7 @@ export const NavidromeController: ControllerEndpoint = {
_start: query.startIndex,
name: query.searchTerm,
...query._custom?.navidrome,
role: hasFeature(apiClientProps.server, ServerFeature.BFR) ? 'albumartist' : '',
},
});
@ -231,6 +242,7 @@ export const NavidromeController: ControllerEndpoint = {
name: query.searchTerm,
...query._custom?.navidrome,
starred: query.favorite,
...excludeMissing(apiClientProps.server),
},
});
@ -367,6 +379,10 @@ export const NavidromeController: ControllerEndpoint = {
throw new Error('Failed to ping server');
}
if (ping.body.serverVersion?.includes('pr-2709')) {
ping.body.serverVersion = '0.55.0';
}
const navidromeFeatures: Record<string, number[]> = getFeatures(
VERSION_INFO,
ping.body.serverVersion!,
@ -390,6 +406,7 @@ export const NavidromeController: ControllerEndpoint = {
}
const features: ServerFeatures = {
bfr: !!navidromeFeatures[ServerFeature.BFR],
lyricsMultipleStructured: !!navidromeFeatures[SubsonicExtensions.SONG_LYRICS],
playlistsSmart: !!navidromeFeatures[ServerFeature.PLAYLISTS_SMART],
publicPlaylist: true,
@ -479,6 +496,7 @@ export const NavidromeController: ControllerEndpoint = {
starred: query.favorite,
title: query.searchTerm,
...query._custom?.navidrome,
...excludeMissing(apiClientProps.server),
},
});

View file

@ -9,6 +9,7 @@ import {
Genre,
ServerListItem,
ServerType,
RelatedArtist,
} from '/@/renderer/api/types';
import z from 'zod';
import { ndType } from './navidrome-types';
@ -54,6 +55,70 @@ const normalizePlayDate = (item: WithDate): string | null => {
return !item.playDate || item.playDate.includes('0001-') ? null : item.playDate;
};
const getArtists = (
item:
| z.infer<typeof ndType._response.song>
| z.infer<typeof ndType._response.playlistSong>
| z.infer<typeof ndType._response.album>,
) => {
let albumArtists: RelatedArtist[] | undefined;
let artists: RelatedArtist[] | undefined;
let participants: Record<string, RelatedArtist[]> | null = null;
if (item.participants) {
participants = {};
for (const [role, list] of Object.entries(item.participants)) {
if (role === 'albumartist' || role === 'artist') {
const roleList = list.map((item) => ({
id: item.id,
imageUrl: null,
name: item.name,
}));
if (role === 'albumartist') {
albumArtists = roleList;
} else {
artists = roleList;
}
} else {
const subRoles = new Map<string | undefined, RelatedArtist[]>();
for (const artist of list) {
const item: RelatedArtist = {
id: artist.id,
imageUrl: null,
name: artist.name,
};
if (subRoles.has(artist.subRole)) {
subRoles.get(artist.subRole)!.push(item);
} else {
subRoles.set(artist.subRole, [item]);
}
}
for (const [subRole, items] of subRoles.entries()) {
if (subRole) {
participants[`${role} (${subRole})`] = items;
} else {
participants[role] = items;
}
}
}
}
}
if (albumArtists === undefined) {
albumArtists = [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }];
}
if (artists === undefined) {
artists = [{ id: item.artistId, imageUrl: null, name: item.artist }];
}
return { albumArtists, artists, participants };
};
const normalizeSong = (
item: z.infer<typeof ndType._response.song> | z.infer<typeof ndType._response.playlistSong>,
server: ServerListItem | null,
@ -80,10 +145,9 @@ const normalizeSong = (
const imagePlaceholderUrl = null;
return {
album: item.album,
albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }],
albumId: item.albumId,
...getArtists(item),
artistName: item.artist,
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
bitRate: item.bitRate,
bpm: item.bpm ? item.bpm : null,
channels: item.channels ? item.channels : null,
@ -116,7 +180,7 @@ const normalizeSong = (
item.rgAlbumPeak || item.rgTrackPeak
? { album: item.rgAlbumPeak, track: item.rgTrackPeak }
: null,
playCount: item.playCount,
playCount: item.playCount || 0,
playlistItemId,
releaseDate: (item.releaseDate
? new Date(item.releaseDate)
@ -155,12 +219,11 @@ const normalizeAlbum = (
return {
albumArtist: item.albumArtist,
albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }],
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
...getArtists(item),
backdropImageUrl: imageBackdropUrl,
comment: item.comment || null,
createdAt: item.createdAt.split('T')[0],
duration: item.duration * 1000 || null,
duration: item.duration !== undefined ? item.duration * 1000 : null,
genres: (item.genres || []).map((genre) => ({
id: genre.id,
imageUrl: null,
@ -180,7 +243,7 @@ const normalizeAlbum = (
: item.originalYear
? new Date(item.originalYear, 0, 1).toISOString()
: null,
playCount: item.playCount,
playCount: item.playCount || 0,
releaseDate: (item.releaseDate
? new Date(item.releaseDate)
: new Date(item.minYear, 0, 1)
@ -232,7 +295,7 @@ const normalizeAlbumArtist = (
lastPlayedAt: normalizePlayDate(item),
mbz: item.mbzArtistId || null,
name: item.name,
playCount: item.playCount,
playCount: item.playCount || 0,
serverId: server?.id || 'unknown',
serverType: ServerType.NAVIDROME,
similarArtists:

View file

@ -83,7 +83,7 @@ const albumArtist = z.object({
mediumImageUrl: z.string().optional(),
name: z.string(),
orderArtistName: z.string(),
playCount: z.number(),
playCount: z.number().optional(),
playDate: z.string().optional(),
rating: z.number(),
size: z.number(),
@ -98,10 +98,20 @@ const albumArtistList = z.array(albumArtist);
const albumArtistListParameters = paginationParameters.extend({
_sort: z.nativeEnum(NDAlbumArtistListSort).optional(),
genre_id: z.string().optional(),
missing: z.boolean().optional(),
name: z.string().optional(),
role: z.string().optional(),
starred: z.boolean().optional(),
});
const participant = z.object({
id: z.string(),
name: z.string(),
subRole: z.string().optional(),
});
const participants = z.record(z.string(), z.array(participant));
const album = z.object({
albumArtist: z.string(),
albumArtistId: z.string(),
@ -113,7 +123,7 @@ const album = z.object({
coverArtId: z.string().optional(), // Removed after v0.48.0
coverArtPath: z.string().optional(), // Removed after v0.48.0
createdAt: z.string(),
duration: z.number(),
duration: z.number().optional(),
fullText: z.string(),
genre: z.string(),
genres: z.array(genre).nullable(),
@ -127,7 +137,8 @@ const album = z.object({
orderAlbumName: z.string(),
originalDate: z.string().optional(),
originalYear: z.number().optional(),
playCount: z.number(),
participants: z.optional(participants),
playCount: z.number().optional(),
playDate: z.string().optional(),
rating: z.number().optional(),
releaseDate: z.string().optional(),
@ -195,8 +206,9 @@ const song = z.object({
orderAlbumName: z.string(),
orderArtistName: z.string(),
orderTitle: z.string(),
participants: z.optional(participants),
path: z.string(),
playCount: z.number(),
playCount: z.number().optional(),
playDate: z.string().optional(),
rating: z.number().optional(),
releaseDate: z.string().optional(),
@ -211,6 +223,7 @@ const song = z.object({
starred: z.boolean(),
starredAt: z.string().optional(),
suffix: z.string(),
tags: z.record(z.string(), z.array(z.string())).optional(),
title: z.string(),
trackNumber: z.number(),
updatedAt: z.string(),

View file

@ -10,6 +10,7 @@ import {
ServerType,
Playlist,
Genre,
RelatedArtist,
} from '/@/renderer/api/types';
const getCoverArtUrl = (args: {
@ -34,6 +35,92 @@ const getCoverArtUrl = (args: {
);
};
const getArtists = (
item:
| z.infer<typeof ssType._response.song>
| z.infer<typeof ssType._response.album>
| z.infer<typeof ssType._response.albumListEntry>,
) => {
const albumArtists: RelatedArtist[] = item.albumArtists
? item.albumArtists.map((item) => ({
id: item.id.toString(),
imageUrl: null,
name: item.name,
}))
: [
{
id: item.artistId?.toString() || '',
imageUrl: null,
name: item.artist || '',
},
];
const artists: RelatedArtist[] = item.artists
? item.artists.map((item) => ({
id: item.id.toString(),
imageUrl: null,
name: item.name,
}))
: [
{
id: item.artistId?.toString() || '',
imageUrl: null,
name: item.artist || '',
},
];
let participants: Record<string, RelatedArtist[]> | null = null;
if (item.contributors) {
participants = {};
for (const contributor of item.contributors) {
const artist = {
id: contributor.artist.id?.toString() || '',
imageUrl: null,
name: contributor.artist.name || '',
};
const role = contributor.subRole
? `${contributor.role} (${contributor.subRole})`
: contributor.role;
if (role in participants) {
participants[role].push(artist);
} else {
participants[role] = [artist];
}
}
}
return { albumArtists, artists, participants };
};
const getGenres = (
item:
| z.infer<typeof ssType._response.song>
| z.infer<typeof ssType._response.album>
| z.infer<typeof ssType._response.albumListEntry>,
): Genre[] => {
return item.genres
? item.genres.map((genre) => ({
id: genre.name,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: genre.name,
}))
: item.genre
? [
{
id: item.genre,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: item.genre,
},
]
: [];
};
const normalizeSong = (
item: z.infer<typeof ssType._response.song>,
server: ServerListItem | null,
@ -51,22 +138,9 @@ const normalizeSong = (
return {
album: item.album || '',
albumArtists: [
{
id: item.artistId?.toString() || '',
imageUrl: null,
name: item.artist || '',
},
],
albumId: item.albumId?.toString() || '',
artistName: item.artist || '',
artists: [
{
id: item.artistId?.toString() || '',
imageUrl: null,
name: item.artist || '',
},
],
...getArtists(item),
bitRate: item.bitRate || 0,
bpm: item.bpm || null,
channels: null,
@ -84,16 +158,7 @@ const normalizeSong = (
track: item.replayGain.trackGain,
}
: null,
genres: item.genre
? [
{
id: item.genre,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: item.genre,
},
]
: [],
genres: getGenres(item),
id: item.id.toString(),
imagePlaceholderUrl: null,
imageUrl,
@ -176,26 +241,12 @@ const normalizeAlbum = (
return {
albumArtist: item.artist,
albumArtists: item.artistId
? [{ id: item.artistId.toString(), imageUrl: null, name: item.artist }]
: [],
artists: item.artistId
? [{ id: item.artistId.toString(), imageUrl: null, name: item.artist }]
: [],
...getArtists(item),
backdropImageUrl: null,
comment: null,
createdAt: item.created,
duration: item.duration * 1000,
genres: item.genre
? [
{
id: item.genre,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: item.genre,
},
]
: [],
genres: getGenres(item),
id: item.id.toString(),
imagePlaceholderUrl: null,
imageUrl,

View file

@ -66,15 +66,29 @@ const genreItem = z.object({
name: z.string(),
});
const simpleArtist = z.object({
id: z.string(),
name: z.string(),
});
const contributor = z.object({
artist: simpleArtist,
role: z.string(),
subRole: z.string().optional(),
});
const song = z.object({
album: z.string().optional(),
albumArtists: z.array(simpleArtist),
albumId: id.optional(),
artist: z.string().optional(),
artistId: id.optional(),
artists: z.array(simpleArtist),
averageRating: z.number().optional(),
bitRate: z.number().optional(),
bpm: z.number().optional(),
contentType: z.string(),
contributors: z.array(contributor).optional(),
coverArt: z.string().optional(),
created: z.string(),
discNumber: z.number(),
@ -101,12 +115,16 @@ const song = z.object({
const album = z.object({
album: z.string(),
albumArtists: z.array(simpleArtist),
artist: z.string(),
artistId: id,
artists: z.array(simpleArtist),
contributors: z.array(contributor).optional(),
coverArt: z.string(),
created: z.string(),
duration: z.number(),
genre: z.string().optional(),
genres: z.array(genreItem).optional(),
id,
isCompilation: z.boolean().optional(),
isDir: z.boolean(),

View file

@ -168,6 +168,7 @@ export type Album = {
mbzId: string | null;
name: string;
originalDate: string | null;
participants: Record<string, RelatedArtist[]> | null;
playCount: number | null;
releaseDate: string | null;
releaseYear: number | null;
@ -212,6 +213,7 @@ export type Song = {
lastPlayedAt: string | null;
lyrics: string | null;
name: string;
participants: Record<string, RelatedArtist[]> | null;
path: string | null;
peak: GainInfo | null;
playCount: number;

View file

@ -88,10 +88,7 @@ export const AlbumArtistListGridView = ({ itemCount, gridRef }: AlbumArtistListG
server,
signal,
},
query: {
limit,
...filter,
},
query,
}),
{ cacheTime: 1000 * 60 * 1 },
);

View file

@ -2,7 +2,14 @@ import { Group, Table } from '@mantine/core';
import { RiCheckFill, RiCloseFill } from 'react-icons/ri';
import { TFunction, useTranslation } from 'react-i18next';
import { ReactNode } from 'react';
import { Album, AlbumArtist, AnyLibraryItem, LibraryItem, Song } from '/@/renderer/api/types';
import {
Album,
AlbumArtist,
AnyLibraryItem,
LibraryItem,
RelatedArtist,
Song,
} from '/@/renderer/api/types';
import { formatDurationString, formatSizeString } from '/@/renderer/utils';
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
import { Spoiler, Text } from '/@/renderer/components';
@ -46,8 +53,8 @@ const handleRow = <T extends AnyLibraryItem>(t: TFunction, item: T, rule: ItemDe
);
};
const formatArtists = (isAlbumArtist: boolean) => (item: Album | Song) =>
(isAlbumArtist ? item.albumArtists : item.artists)?.map((artist, index) => (
const formatArtists = (artists: RelatedArtist[] | undefined | null) =>
artists?.map((artist, index) => (
<span key={artist.id || artist.name}>
{index > 0 && <Separator />}
{artist.id ? (
@ -106,7 +113,7 @@ const BoolField = (key: boolean) =>
const AlbumPropertyMapping: ItemDetailRow<Album>[] = [
{ key: 'name', label: 'common.title' },
{ label: 'entity.albumArtist_one', render: formatArtists(true) },
{ label: 'entity.albumArtist_one', render: (item) => formatArtists(item.albumArtists) },
{ label: 'entity.genre_other', render: FormatGenre },
{
label: 'common.duration',
@ -198,8 +205,8 @@ const AlbumArtistPropertyMapping: ItemDetailRow<AlbumArtist>[] = [
const SongPropertyMapping: ItemDetailRow<Song>[] = [
{ key: 'name', label: 'common.title' },
{ key: 'path', label: 'common.path', render: SongPath },
{ label: 'entity.albumArtist_one', render: formatArtists(true) },
{ key: 'artists', label: 'entity.artist_other', render: formatArtists(false) },
{ label: 'entity.albumArtist_one', render: (item) => formatArtists(item.albumArtists) },
{ key: 'artists', label: 'entity.artist_other', render: (item) => formatArtists(item.artists) },
{
key: 'album',
label: 'entity.album_one',
@ -270,22 +277,42 @@ const SongPropertyMapping: ItemDetailRow<Song>[] = [
{ label: 'filter.comment', render: formatComment },
];
const handleParticipants = (item: Album | Song) => {
if (item.participants) {
return Object.entries(item.participants).map(([role, participants]) => {
return (
<tr key={role}>
<td>
{role.slice(0, 1).toLocaleUpperCase()}
{role.slice(1)}
</td>
<td>{formatArtists(participants)}</td>
</tr>
);
});
}
return [];
};
export const ItemDetailsModal = ({ item }: ItemDetailsModalProps) => {
const { t } = useTranslation();
let body: ReactNode;
let body: ReactNode[] = [];
switch (item.itemType) {
case LibraryItem.ALBUM:
body = AlbumPropertyMapping.map((rule) => handleRow(t, item, rule));
body.push(...handleParticipants(item));
break;
case LibraryItem.ALBUM_ARTIST:
body = AlbumArtistPropertyMapping.map((rule) => handleRow(t, item, rule));
break;
case LibraryItem.SONG:
body = SongPropertyMapping.map((rule) => handleRow(t, item, rule));
body.push(...handleParticipants(item));
break;
default:
body = null;
body = [];
}
return (