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

@ -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(),