mirror of
https://github.com/antebudimir/feishin.git
synced 2025-12-31 10:03:33 +00:00
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:
parent
89e27ec6ff
commit
b0d86ee5c9
9 changed files with 131 additions and 13 deletions
|
|
@ -27,6 +27,7 @@
|
|||
"action_one": "action",
|
||||
"action_other": "actions",
|
||||
"add": "add",
|
||||
"additionalParticipants": "additional participants",
|
||||
"albumGain": "album gain",
|
||||
"albumPeak": "album peak",
|
||||
"areYouSure": "are you sure?",
|
||||
|
|
@ -106,6 +107,7 @@
|
|||
"share": "share",
|
||||
"size": "size",
|
||||
"sortOrder": "order",
|
||||
"tags": "tags",
|
||||
"title": "title",
|
||||
"trackNumber": "track",
|
||||
"trackGain": "track gain",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ const normalizeSong = (
|
|||
serverType: ServerType.NAVIDROME,
|
||||
size: item.size,
|
||||
streamUrl: `${server?.url}/rest/stream.view?id=${id}&v=1.13.0&c=Feishin&${server?.credential}`,
|
||||
tags: item.tags || null,
|
||||
trackNumber: item.trackNumber,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item.updatedAt,
|
||||
|
|
@ -236,6 +237,7 @@ const normalizeAlbum = (
|
|||
isCompilation: item.compilation,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
lastPlayedAt: normalizePlayDate(item),
|
||||
|
||||
mbzId: item.mbzAlbumId || null,
|
||||
name: item.name,
|
||||
originalDate: item.originalDate
|
||||
|
|
@ -254,6 +256,7 @@ const normalizeAlbum = (
|
|||
size: item.size,
|
||||
songCount: item.songCount,
|
||||
songs: item.songs ? item.songs.map((song) => normalizeSong(song, server)) : undefined,
|
||||
tags: item.tags || null,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item.updatedAt,
|
||||
userFavorite: item.starred,
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ const album = z.object({
|
|||
sortArtistName: z.string(),
|
||||
starred: z.boolean(),
|
||||
starredAt: z.string().optional(),
|
||||
tags: z.record(z.string(), z.array(z.string())).optional(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ const normalizeSong = (
|
|||
serverType: ServerType.SUBSONIC,
|
||||
size: item.size,
|
||||
streamUrl,
|
||||
tags: null,
|
||||
trackNumber: item.track || 1,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: '',
|
||||
|
|
@ -267,6 +268,7 @@ const normalizeAlbum = (
|
|||
(item as z.infer<typeof ssType._response.album>).song?.map((song) =>
|
||||
normalizeSong(song, server),
|
||||
) || [],
|
||||
tags: item.tags || null,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item.created,
|
||||
userFavorite: item.starred || false,
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ export type Album = {
|
|||
size: number | null;
|
||||
songCount: number | null;
|
||||
songs?: Song[];
|
||||
tags: Record<string, string[]> | null;
|
||||
uniqueId: string;
|
||||
updatedAt: string;
|
||||
userFavorite: boolean;
|
||||
|
|
@ -224,6 +225,7 @@ export type Song = {
|
|||
serverType: ServerType;
|
||||
size: number;
|
||||
streamUrl: string;
|
||||
tags: Record<string, string[]> | null;
|
||||
trackNumber: number;
|
||||
uniqueId: string;
|
||||
updatedAt: string;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { AppRoute } from '/@/renderer/router/routes';
|
|||
import { Separator } from '/@/renderer/components/separator';
|
||||
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
|
||||
import { formatDateRelative, formatRating } from '/@/renderer/utils/format';
|
||||
import { SEPARATOR_STRING } from '/@/renderer/api/utils';
|
||||
|
||||
export type ItemDetailsModalProps = {
|
||||
item: Album | AlbumArtist | Song;
|
||||
|
|
@ -277,9 +278,40 @@ const SongPropertyMapping: ItemDetailRow<Song>[] = [
|
|||
{ label: 'filter.comment', render: formatComment },
|
||||
];
|
||||
|
||||
const handleParticipants = (item: Album | Song) => {
|
||||
const handleTags = (item: Album | Song, t: TFunction) => {
|
||||
if (item.tags) {
|
||||
const tags = Object.entries(item.tags).map(([tag, fields]) => {
|
||||
return (
|
||||
<tr key={tag}>
|
||||
<td>
|
||||
{tag.slice(0, 1).toLocaleUpperCase()}
|
||||
{tag.slice(1)}
|
||||
</td>
|
||||
<td>{fields.length === 0 ? BoolField(true) : fields.join(SEPARATOR_STRING)}</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
if (tags.length) {
|
||||
return [
|
||||
<tr key="tags">
|
||||
<td>
|
||||
<h3>{t('common.tags', { postProcess: 'sentenceCase' })}</h3>
|
||||
</td>
|
||||
<td>
|
||||
<h3>{tags.length}</h3>
|
||||
</td>
|
||||
</tr>,
|
||||
].concat(tags);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const handleParticipants = (item: Album | Song, t: TFunction) => {
|
||||
if (item.participants) {
|
||||
return Object.entries(item.participants).map(([role, participants]) => {
|
||||
const participants = Object.entries(item.participants).map(([role, participants]) => {
|
||||
return (
|
||||
<tr key={role}>
|
||||
<td>
|
||||
|
|
@ -290,6 +322,23 @@ const handleParticipants = (item: Album | Song) => {
|
|||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
if (participants.length) {
|
||||
return [
|
||||
<tr key="participants">
|
||||
<td>
|
||||
<h3>
|
||||
{t('common.additionalParticipants', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</h3>
|
||||
</td>
|
||||
<td>
|
||||
<h3>{participants.length}</h3>
|
||||
</td>
|
||||
</tr>,
|
||||
].concat(participants);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
|
|
@ -302,14 +351,16 @@ export const ItemDetailsModal = ({ item }: ItemDetailsModalProps) => {
|
|||
switch (item.itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
body = AlbumPropertyMapping.map((rule) => handleRow(t, item, rule));
|
||||
body.push(...handleParticipants(item));
|
||||
body.push(...handleParticipants(item, t));
|
||||
body.push(...handleTags(item, t));
|
||||
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));
|
||||
body.push(...handleParticipants(item, t));
|
||||
body.push(...handleTags(item, t));
|
||||
break;
|
||||
default:
|
||||
body = [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue