Add album artist list route

This commit is contained in:
jeffvli 2022-12-30 21:04:06 -08:00
parent 185175aa89
commit 24af17b8fe
19 changed files with 1269 additions and 32 deletions

View file

@ -191,9 +191,19 @@ const getGenreList = async (args: GenreListArgs) => {
return (apiController('getGenreList') as ControllerEndpoint['getGenreList'])?.(args);
};
const getAlbumArtistList = async (args: AlbumArtistListArgs) => {
return (apiController('getAlbumArtistList') as ControllerEndpoint['getAlbumArtistList'])?.(args);
};
const getArtistList = async (args: ArtistListArgs) => {
return (apiController('getArtistList') as ControllerEndpoint['getArtistList'])?.(args);
};
export const controller = {
getAlbumArtistList,
getAlbumDetail,
getAlbumList,
getArtistList,
getGenreList,
getMusicFolderList,
getSongList,

View file

@ -2,6 +2,7 @@ import ky from 'ky';
import { nanoid } from 'nanoid/non-secure';
import type {
JFAlbum,
JFAlbumArtist,
JFAlbumArtistDetail,
JFAlbumArtistDetailResponse,
JFAlbumArtistList,
@ -33,6 +34,7 @@ import type {
import { JFCollectionType } from '/@/renderer/api/jellyfin.types';
import type {
Album,
AlbumArtist,
AlbumArtistDetailArgs,
AlbumArtistListArgs,
AlbumDetailArgs,
@ -138,7 +140,7 @@ const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs): Promise<JFAlbu
};
const data = await api
.get(`/users/${server?.userId}/items/${query.id}`, {
.get(`users/${server?.userId}/items/${query.id}`, {
headers: { 'X-MediaBrowser-Token': server?.credential },
prefixUrl: server?.url,
searchParams: parseSearchParams(searchParams),
@ -170,12 +172,15 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<JFAlbumArt
const { query, server, signal } = args;
const searchParams: JFAlbumArtistListParams = {
fields: 'Genres, DateCreated, ExternalUrls, Overview',
imageTypeLimit: 1,
limit: query.limit,
parentId: query.musicFolderId,
recursive: true,
sortBy: albumArtistListSortMap.jellyfin[query.sortBy],
sortOrder: sortOrderMap.jellyfin[query.sortOrder],
startIndex: query.startIndex,
userId: server?.userId || undefined,
};
const data = await api
@ -187,7 +192,11 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<JFAlbumArt
})
.json<JFAlbumArtistListResponse>();
return data;
return {
items: data.Items,
startIndex: query.startIndex,
totalRecordCount: data.TotalRecordCount,
};
};
const getArtistList = async (args: ArtistListArgs): Promise<JFArtistList> => {
@ -303,9 +312,11 @@ const getSongList = async (args: SongListArgs): Promise<JFSongList> => {
const yearsFilter = yearsGroup.length ? getCommaDelimitedString(yearsGroup) : undefined;
const albumIdsFilter = query.albumIds ? getCommaDelimitedString(query.albumIds) : undefined;
const artistIdsFilter = query.artistIds ? getCommaDelimitedString(query.artistIds) : undefined;
const searchParams: JFSongListParams & { maxYear?: number; minYear?: number } = {
albumIds: albumIdsFilter,
artistIds: artistIdsFilter,
fields: 'Genres, DateCreated, MediaSources, ParentId',
includeItemTypes: 'Audio',
limit: query.limit,
@ -496,6 +507,26 @@ const getStreamUrl = (args: {
);
};
const getAlbumArtistCoverArtUrl = (args: {
baseUrl: string;
item: JFAlbumArtist;
size: number;
}) => {
const size = args.size ? args.size : 300;
if (!args.item.ImageTags?.Primary) {
return null;
}
return (
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
'&quality=96'
);
};
const getAlbumCoverArtUrl = (args: { baseUrl: string; item: JFAlbum; size: number }) => {
const size = args.size ? args.size : 300;
@ -628,6 +659,32 @@ const normalizeAlbum = (item: JFAlbum, server: ServerListItem, imageSize?: numbe
};
};
const normalizeAlbumArtist = (
item: JFAlbumArtist,
server: ServerListItem,
imageSize?: number,
): AlbumArtist => {
return {
albumCount: null,
backgroundImageUrl: null,
biography: item.Overview || null,
duration: item.RunTimeTicks / 10000000,
genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
id: item.Id,
imageUrl: getAlbumArtistCoverArtUrl({
baseUrl: server.url,
item,
size: imageSize || 300,
}),
isFavorite: item.UserData.IsFavorite || false,
lastPlayedAt: null,
name: item.Name,
playCount: item.UserData.PlayCount,
rating: null,
songCount: null,
};
};
// const normalizeArtist = (item: any) => {
// return {
// album: (item.album || []).map((entry: any) => normalizeAlbum(entry)),
@ -717,5 +774,6 @@ export const jellyfinApi = {
export const jfNormalize = {
album: normalizeAlbum,
albumArtist: normalizeAlbumArtist,
song: normalizeSong,
};

View file

@ -23,7 +23,11 @@ export interface JFAlbumArtistListResponse extends JFBasePaginatedResponse {
Items: JFAlbumArtist[];
}
export type JFAlbumArtistList = JFAlbumArtistListResponse;
export type JFAlbumArtistList = {
items: JFAlbumArtist[];
startIndex: number;
totalRecordCount: number;
};
export interface JFArtistListResponse extends JFBasePaginatedResponse {
Items: JFAlbumArtist[];
@ -149,6 +153,13 @@ export type JFAlbumArtist = {
RunTimeTicks: number;
ServerId: string;
Type: string;
UserData: {
IsFavorite: boolean;
Key: string;
PlayCount: number;
PlaybackPositionTicks: number;
Played: boolean;
};
};
export type JFArtist = {
@ -474,6 +485,7 @@ type JFBaseParams = {
imageTypeLimit?: number;
parentId?: string;
recursive?: boolean;
userId?: string;
};
type JFPaginationParams = {

View file

@ -30,6 +30,7 @@ import type {
NDPlaylistDetailResponse,
NDSongList,
NDSongListResponse,
NDAlbumArtist,
} from '/@/renderer/api/navidrome.types';
import { NDPlaylistListSort, NDSongListSort, NDSortOrder } from '/@/renderer/api/navidrome.types';
import type {
@ -49,6 +50,7 @@ import type {
PlaylistDetailArgs,
CreatePlaylistResponse,
PlaylistSongListArgs,
AlbumArtist,
} from '/@/renderer/api/types';
import {
playlistListSortMap,
@ -160,15 +162,21 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<NDAlbumArt
...query.ndParams,
};
const data = await api
.get('api/artist', {
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
searchParams,
signal,
})
.json<NDArtistListResponse>();
const res = await api.get('api/artist', {
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
prefixUrl: server?.url,
searchParams: parseSearchParams(searchParams),
signal,
});
return data;
const data = await res.json<NDArtistListResponse>();
const itemCount = res.headers.get('x-total-count');
return {
items: data,
startIndex: query.startIndex,
totalRecordCount: Number(itemCount),
};
};
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<NDAlbumDetail> => {
@ -238,6 +246,7 @@ const getSongList = async (args: SongListArgs): Promise<NDSongList> => {
_sort: songListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
album_id: query.albumIds,
artist_id: query.artistIds,
title: query.searchTerm,
...query.ndParams,
};
@ -487,6 +496,24 @@ const normalizeAlbum = (item: NDAlbum, server: ServerListItem, imageSize?: numbe
};
};
const normalizeAlbumArtist = (item: NDAlbumArtist): AlbumArtist => {
return {
albumCount: item.albumCount,
backgroundImageUrl: null,
biography: item.biography,
duration: null,
genres: item.genres,
id: item.id,
imageUrl: item.largeImageUrl,
isFavorite: item.starred,
lastPlayedAt: item.playDate ? item.playDate.split('T')[0] : null,
name: item.name,
playCount: item.playCount,
rating: item.rating,
songCount: item.songCount,
};
};
export const navidromeApi = {
authenticate,
createPlaylist,
@ -505,5 +532,6 @@ export const navidromeApi = {
export const ndNormalize = {
album: normalizeAlbum,
albumArtist: normalizeAlbumArtist,
song: normalizeSong,
};

View file

@ -118,7 +118,11 @@ export type NDAlbumArtist = {
export type NDAuthenticationResponse = NDAuthenticate;
export type NDAlbumArtistList = NDAlbumArtist[];
export type NDAlbumArtistList = {
items: NDAlbumArtist[];
startIndex: number;
totalRecordCount: number;
};
export type NDAlbumArtistDetail = NDAlbumArtist;
@ -230,6 +234,7 @@ export enum NDSongListSort {
export type NDSongListParams = {
_sort?: NDSongListSort;
album_id?: string[];
artist_id?: string[];
genre_id?: string;
starred?: boolean;
} & NDPagination &

View file

@ -1,15 +1,17 @@
import { jfNormalize } from '/@/renderer/api/jellyfin.api';
import type {
JFAlbum,
JFAlbumArtist,
JFGenreList,
JFMusicFolderList,
JFSong,
} from '/@/renderer/api/jellyfin.types';
import { ndNormalize } from '/@/renderer/api/navidrome.api';
import type { NDAlbum, NDGenreList, NDSong } from '/@/renderer/api/navidrome.types';
import type { NDAlbum, NDAlbumArtist, NDGenreList, NDSong } from '/@/renderer/api/navidrome.types';
import { SSGenreList, SSMusicFolderList } from '/@/renderer/api/subsonic.types';
import type {
Album,
RawAlbumArtistListResponse,
RawAlbumDetailResponse,
RawAlbumListResponse,
RawGenreListResponse,
@ -136,7 +138,33 @@ const genreList = (data: RawGenreListResponse | undefined, server: ServerListIte
return genres;
};
const albumArtistList = (
data: RawAlbumArtistListResponse | undefined,
server: ServerListItem | null,
) => {
let albumArtists;
switch (server?.type) {
case 'jellyfin':
albumArtists = data?.items.map((item) =>
jfNormalize.albumArtist(item as JFAlbumArtist, server),
);
break;
case 'navidrome':
albumArtists = data?.items.map((item) => ndNormalize.albumArtist(item as NDAlbumArtist));
break;
case 'subsonic':
break;
}
return {
items: albumArtists,
startIndex: data?.startIndex,
totalRecordCount: data?.totalRecordCount,
};
};
export const normalize = {
albumArtistList,
albumDetail,
albumList,
genreList,

View file

@ -1,15 +1,32 @@
import type { AlbumListQuery, SongListQuery, AlbumDetailQuery } from './types';
import type {
AlbumListQuery,
SongListQuery,
AlbumDetailQuery,
AlbumArtistListQuery,
ArtistListQuery,
} from './types';
export const queryKeys = {
albumArtists: {
list: (serverId: string, query?: AlbumArtistListQuery) =>
[serverId, 'albumArtists', 'list', query] as const,
root: (serverId: string) => [serverId, 'albumArtists'] as const,
},
albums: {
detail: (serverId: string, query: AlbumDetailQuery) =>
detail: (serverId: string, query?: AlbumDetailQuery) =>
[serverId, 'albums', 'detail', query] as const,
list: (serverId: string, query: AlbumListQuery) => [serverId, 'albums', 'list', query] as const,
root: ['albums'],
list: (serverId: string, query?: AlbumListQuery) =>
[serverId, 'albums', 'list', query] as const,
root: (serverId: string) => [serverId, 'albums'],
serverRoot: (serverId: string) => [serverId, 'albums'],
songs: (serverId: string, query: SongListQuery) =>
[serverId, 'albums', 'songs', query] as const,
},
artists: {
list: (serverId: string, query?: ArtistListQuery) =>
[serverId, 'artists', 'list', query] as const,
root: (serverId: string) => [serverId, 'artists'] as const,
},
genres: {
list: (serverId: string) => [serverId, 'genres', 'list'] as const,
root: (serverId: string) => [serverId, 'genres'] as const,
@ -21,6 +38,7 @@ export const queryKeys = {
root: (serverId: string) => [serverId] as const,
},
songs: {
list: (serverId: string, query: SongListQuery) => [serverId, 'songs', 'list', query] as const,
list: (serverId: string, query?: SongListQuery) => [serverId, 'songs', 'list', query] as const,
root: (serverId: string) => [serverId, 'songs'] as const,
},
};

View file

@ -177,7 +177,11 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<SSAlbumArt
const artists = (data.artists?.index || []).flatMap((index: SSArtistIndex) => index.artist);
return artists;
return {
items: artists,
startIndex: query.startIndex,
totalRecordCount: null,
};
};
const getGenreList = async (args: GenreListArgs): Promise<SSGenreList> => {

View file

@ -33,7 +33,11 @@ export type SSAlbumArtistDetailResponse = {
};
};
export type SSAlbumArtistList = SSAlbumArtistListEntry[];
export type SSAlbumArtistList = {
items: SSAlbumArtistListEntry[];
startIndex: number;
totalRecordCount: number | null;
};
export type SSAlbumArtistListResponse = {
artists: {

View file

@ -202,13 +202,19 @@ export type Song = {
};
export type AlbumArtist = {
albumCount: number | null;
backgroundImageUrl: string | null;
biography: string | null;
createdAt: string;
duration: number | null;
genres: Genre[];
id: string;
imageUrl: string | null;
isFavorite: boolean;
lastPlayedAt: string | null;
name: string;
remoteCreatedAt: string | null;
serverFolderId: string;
updatedAt: string;
playCount: number | null;
rating: number | null;
songCount: number | null;
};
export type RelatedAlbumArtist = {
@ -418,6 +424,7 @@ export enum SongListSort {
export type SongListQuery = {
albumIds?: string[];
artistIds?: string[];
jfParams?: {
filters?: string;
genreIds?: string;
@ -432,7 +439,8 @@ export type SongListQuery = {
limit?: number;
musicFolderId?: string;
ndParams?: {
artist_id?: string;
album_id?: string[];
artist_id?: string[];
compilation?: boolean;
genre_id?: string;
has_rating?: boolean;
@ -554,6 +562,7 @@ export type AlbumArtistListQuery = {
name?: string;
starred?: boolean;
};
searchTerm?: string;
sortBy: AlbumArtistListSort;
sortOrder: SortOrder;
startIndex: number;