add artist list

This commit is contained in:
Kendall Garner 2025-04-23 23:27:06 -07:00
parent 14e9f6ac41
commit e84a4b20bc
No known key found for this signature in database
GPG key ID: 9355F387FE765C94
22 changed files with 1369 additions and 192 deletions

View file

@ -99,6 +99,12 @@ export const controller: GeneralController = {
getAlbumListCount(args) {
return apiController('getAlbumListCount', args.apiClientProps.server?.type)?.(args);
},
getArtistList(args) {
return apiController('getArtistList', args.apiClientProps.server?.type)?.(args);
},
getArtistListCount(args) {
return apiController('getArtistListCount', args.apiClientProps.server?.type)?.(args);
},
getDownloadUrl(args) {
return apiController('getDownloadUrl', args.apiClientProps.server?.type)?.(args);
},
@ -126,6 +132,9 @@ export const controller: GeneralController = {
getRandomSongList(args) {
return apiController('getRandomSongList', args.apiClientProps.server?.type)?.(args);
},
getRoles(args) {
return apiController('getRoles', args.apiClientProps.server?.type)?.(args);
},
getServerInfo(args) {
return apiController('getServerInfo', args.apiClientProps.server?.type)?.(args);
},

View file

@ -152,7 +152,6 @@ export const JellyfinController: ControllerEndpoint = {
const { query, apiClientProps } = args;
const res = await jfApiClient(apiClientProps).deletePlaylist({
body: null,
params: {
id: query.id,
},
@ -331,6 +330,41 @@ export const JellyfinController: ControllerEndpoint = {
apiClientProps,
query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!),
getArtistList: async (args) => {
const { query, apiClientProps } = args;
const res = await jfApiClient(apiClientProps).getArtistList({
query: {
Fields: 'Genres, DateCreated, ExternalUrls, Overview',
ImageTypeLimit: 1,
Limit: query.limit,
ParentId: query.musicFolderId,
Recursive: true,
SearchTerm: query.searchTerm,
SortBy: albumArtistListSortMap.jellyfin[query.sortBy] || 'SortName,Name',
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.startIndex,
UserId: apiClientProps.server?.userId || undefined,
},
});
if (res.status !== 200) {
throw new Error('Failed to get album artist list');
}
return {
items: res.body.Items.map((item) =>
jfNormalize.albumArtist(item, apiClientProps.server),
),
startIndex: query.startIndex,
totalRecordCount: res.body.TotalRecordCount,
};
},
getArtistListCount: async ({ apiClientProps, query }) =>
JellyfinController.getArtistList({
apiClientProps,
query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!),
getDownloadUrl: (args) => {
const { apiClientProps, query } = args;
@ -559,6 +593,7 @@ export const JellyfinController: ControllerEndpoint = {
totalRecordCount: res.body.Items.length || 0,
};
},
getRoles: async () => [],
getServerInfo: async (args) => {
const { apiClientProps } = args;
@ -775,7 +810,6 @@ export const JellyfinController: ControllerEndpoint = {
const { apiClientProps, query } = args;
const res = await jfApiClient(apiClientProps).movePlaylistItem({
body: null,
params: {
itemId: query.trackId,
newIdx: query.endingIndex.toString(),
@ -794,7 +828,6 @@ export const JellyfinController: ControllerEndpoint = {
for (const chunk of chunks) {
const res = await jfApiClient(apiClientProps).removeFromPlaylist({
body: null,
params: {
id: query.id,
},

View file

@ -30,6 +30,22 @@ const VERSION_INFO: VersionInfo = [
['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
];
const NAVIDROME_ROLES: Array<string | { label: string; value: string }> = [
{ label: 'all artists', value: '' },
'arranger',
'artist',
'composer',
'conductor',
'director',
'djmixer',
'engineer',
'lyricist',
'mixer',
'performer',
'producer',
'remixer',
];
const excludeMissing = (server: ServerListItem | null) => {
if (hasFeature(server, ServerFeature.BFR)) {
return { missing: false };
@ -105,7 +121,6 @@ export const NavidromeController: ControllerEndpoint = {
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).deletePlaylist({
body: null,
params: {
id: query.id,
},
@ -261,6 +276,47 @@ export const NavidromeController: ControllerEndpoint = {
apiClientProps,
query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!),
getArtistList: async (args) => {
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
query: {
_end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: albumArtistListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
name: query.searchTerm,
...query._custom?.navidrome,
role: query.role,
},
});
if (res.status !== 200) {
throw new Error('Failed to get artist list');
}
return {
items: res.body.data.map((albumArtist) =>
// Navidrome native API will return only external URL small/medium/large
// image URL. Set large image to undefined to force `albumArtist` to use
// /rest/getCoverArt.view?id=ar-...
ndNormalize.albumArtist(
{
...albumArtist,
largeImageUrl: undefined,
},
apiClientProps.server,
),
),
startIndex: query.startIndex,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
},
getArtistListCount: async ({ apiClientProps, query }) =>
NavidromeController.getArtistList({
apiClientProps,
query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!),
getDownloadUrl: SubsonicController.getDownloadUrl,
getGenreList: async (args) => {
const { query, apiClientProps } = args;
@ -369,6 +425,7 @@ export const NavidromeController: ControllerEndpoint = {
};
},
getRandomSongList: SubsonicController.getRandomSongList,
getRoles: async () => NAVIDROME_ROLES,
getServerInfo: async (args) => {
const { apiClientProps } = args;
@ -564,7 +621,6 @@ export const NavidromeController: ControllerEndpoint = {
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).removeFromPlaylist({
body: null,
params: {
id: query.id,
},

View file

@ -231,6 +231,9 @@ export const queryKeys: Record<
return [serverId, 'playlists', 'songList'] as const;
},
},
roles: {
list: (serverId: string) => [serverId, 'roles'] as const,
},
search: {
list: (serverId: string, query?: SearchQuery) => {
if (query) return [serverId, 'search', 'list', query] as const;

View file

@ -515,6 +515,51 @@ export const SubsonicController: ControllerEndpoint = {
return totalRecordCount;
},
getArtistList: async (args) => {
const { query, apiClientProps } = args;
const res = await ssApiClient(apiClientProps).getArtists({
query: {
musicFolderId: query.musicFolderId,
},
});
if (res.status !== 200) {
throw new Error('Failed to get artist list');
}
let artists = (res.body.artists?.index || []).flatMap((index) => index.artist);
console.log(artists.length);
if (query.role) {
artists = artists.filter(
(artist) => !artist.roles || artist.roles.includes(query.role!),
);
}
let results = artists.map((artist) =>
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
);
if (query.searchTerm) {
const searchResults = filter(results, (artist) => {
return artist.name.toLowerCase().includes(query.searchTerm!.toLowerCase());
});
results = searchResults;
}
if (query.sortBy) {
results = sortAlbumArtistList(results, query.sortBy, query.sortOrder);
}
return {
items: results,
startIndex: query.startIndex,
totalRecordCount: results?.length || 0,
};
},
getArtistListCount: async (args) =>
SubsonicController.getArtistList(args).then((res) => res!.totalRecordCount!),
getDownloadUrl: (args) => {
const { apiClientProps, query } = args;
@ -711,6 +756,31 @@ export const SubsonicController: ControllerEndpoint = {
totalRecordCount: res.body.randomSongs?.song?.length || 0,
};
},
getRoles: async (args) => {
const { apiClientProps } = args;
const res = await ssApiClient(apiClientProps).getArtists({});
if (res.status !== 200) {
throw new Error('Failed to get artist list');
}
const roles = new Set<string>();
for (const index of res.body.artists?.index || []) {
for (const artist of index.artist) {
for (const role of artist.roles || []) {
roles.add(role);
}
}
}
const final: Array<string | { label: string; value: string }> = Array.from(roles).sort();
if (final.length > 0) {
final.splice(0, 0, { label: 'all artists', value: '' });
}
return final;
},
getServerInfo: async (args) => {
const { apiClientProps } = args;
@ -1230,6 +1300,7 @@ export const SubsonicController: ControllerEndpoint = {
return null;
},
search: async (args) => {
const { query, apiClientProps } = args;
@ -1296,109 +1367,3 @@ export const SubsonicController: ControllerEndpoint = {
return null;
},
};
// export const getAlbumArtistDetail = async (
// args: AlbumArtistDetailArgs,
// ): Promise<SSAlbumArtistDetail> => {
// const { server, signal, query } = args;
// const defaultParams = getDefaultParams(server);
// const searchParams: SSAlbumArtistDetailParams = {
// id: query.id,
// ...defaultParams,
// };
// const data = await api
// .get('/getArtist.view', {
// prefixUrl: server?.url,
// searchParams,
// signal,
// })
// .json<SSAlbumArtistDetailResponse>();
// return data.artist;
// };
// const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<SSAlbumArtistList> => {
// const { signal, server, query } = args;
// const defaultParams = getDefaultParams(server);
// const searchParams: SSAlbumArtistListParams = {
// musicFolderId: query.musicFolderId,
// ...defaultParams,
// };
// const data = await api
// .get('rest/getArtists.view', {
// prefixUrl: server?.url,
// searchParams,
// signal,
// })
// .json<SSAlbumArtistListResponse>();
// const artists = (data.artists?.index || []).flatMap((index: SSArtistIndex) => index.artist);
// return {
// items: artists,
// startIndex: query.startIndex,
// totalRecordCount: null,
// };
// };
// const getGenreList = async (args: GenreListArgs): Promise<SSGenreList> => {
// const { server, signal } = args;
// const defaultParams = getDefaultParams(server);
// const data = await api
// .get('rest/getGenres.view', {
// prefixUrl: server?.url,
// searchParams: defaultParams,
// signal,
// })
// .json<SSGenreListResponse>();
// return data.genres.genre;
// };
// const getAlbumDetail = async (args: AlbumDetailArgs): Promise<SSAlbumDetail> => {
// const { server, query, signal } = args;
// const defaultParams = getDefaultParams(server);
// const searchParams = {
// id: query.id,
// ...defaultParams,
// };
// const data = await api
// .get('rest/getAlbum.view', {
// prefixUrl: server?.url,
// searchParams: parseSearchParams(searchParams),
// signal,
// })
// .json<SSAlbumDetailResponse>();
// const { song: songs, ...dataWithoutSong } = data.album;
// return { ...dataWithoutSong, songs };
// };
// const getAlbumList = async (args: AlbumListArgs): Promise<SSAlbumList> => {
// const { server, query, signal } = args;
// const defaultParams = getDefaultParams(server);
// const searchParams = {
// ...defaultParams,
// };
// const data = await api
// .get('rest/getAlbumList2.view', {
// prefixUrl: server?.url,
// searchParams: parseSearchParams(searchParams),
// signal,
// })
// .json<SSAlbumListResponse>();
// return {
// items: data.albumList2.album,
// startIndex: query.startIndex,
// totalRecordCount: null,
// };
// };

View file

@ -162,6 +162,7 @@ const albumArtist = z.object({
coverArt: z.string().optional(),
id,
name: z.string(),
roles: z.array(z.string()).optional(),
starred: z.string().optional(),
});
@ -175,6 +176,7 @@ const artistListEntry = albumArtist.pick({
coverArt: true,
id: true,
name: true,
roles: true,
starred: true,
});

View file

@ -672,7 +672,7 @@ export type AlbumArtistDetailQuery = { id: string };
export type AlbumArtistDetailArgs = { query: AlbumArtistDetailQuery } & BaseEndpointArgs;
// Artist List
export type ArtistListResponse = BasePaginatedResponse<Artist[]> | null | undefined;
export type ArtistListResponse = BasePaginatedResponse<AlbumArtist[]> | null | undefined;
export enum ArtistListSort {
ALBUM = 'album',
@ -695,6 +695,8 @@ export interface ArtistListQuery extends BaseQuery<ArtistListSort> {
};
limit?: number;
musicFolderId?: string;
role?: string;
searchTerm?: string;
startIndex: number;
}
@ -1245,7 +1247,8 @@ export type ControllerEndpoint = {
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
getAlbumListCount: (args: AlbumListArgs) => Promise<number>;
// getArtistInfo?: (args: any) => void;
// getArtistList?: (args: ArtistListArgs) => Promise<ArtistListResponse>;
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
getArtistListCount: (args: ArtistListArgs) => Promise<number>;
getDownloadUrl: (args: DownloadArgs) => string;
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
getLyrics?: (args: LyricsArgs) => Promise<LyricsResponse>;
@ -1255,6 +1258,7 @@ export type ControllerEndpoint = {
getPlaylistListCount: (args: PlaylistListArgs) => Promise<number>;
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
getRoles: (args: BaseEndpointArgs) => Promise<Array<string | { label: string; value: string }>>;
getServerInfo: (args: ServerInfoArgs) => Promise<ServerInfo>;
getSimilarSongs: (args: SimilarSongsArgs) => Promise<Song[]>;
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
@ -1417,7 +1421,7 @@ export const sortSongList = (songs: QueueSong[], sortBy: SongListSort, sortOrder
export const sortAlbumArtistList = (
artists: AlbumArtist[],
sortBy: AlbumArtistListSort,
sortBy: AlbumArtistListSort | ArtistListSort,
sortOrder: SortOrder,
) => {
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';