Add initial genre list support

This commit is contained in:
jeffvli 2023-07-31 17:16:48 -07:00
parent 4d5085f230
commit 8029712b55
25 changed files with 968 additions and 41 deletions

View file

@ -15,6 +15,10 @@ export interface JFGenreListResponse extends JFBasePaginatedResponse {
export type JFGenreList = JFGenreListResponse;
export enum JFGenreListSort {
NAME = 'Name,SortName',
}
export type JFAlbumArtistDetailResponse = JFAlbumArtist;
export type JFAlbumArtistDetail = JFAlbumArtistDetailResponse;

View file

@ -108,6 +108,7 @@ export const contract = c.router({
getGenreList: {
method: 'GET',
path: 'genres',
query: jfType._parameters.genreList,
responses: {
200: jfType._response.genreList,
400: jfType._response.error,

View file

@ -46,6 +46,7 @@ import {
RandomSongListArgs,
LyricsArgs,
LyricsResponse,
genreListSortMap,
} from '/@/renderer/api/types';
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
import { jfNormalize } from './jellyfin-normalize';
@ -116,9 +117,16 @@ const getMusicFolderList = async (args: MusicFolderListArgs): Promise<MusicFolde
};
const getGenreList = async (args: GenreListArgs): Promise<GenreListResponse> => {
const { apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await jfApiClient(apiClientProps).getGenreList();
const res = await jfApiClient(apiClientProps).getGenreList({
query: {
SearchTerm: query?.searchTerm,
SortBy: genreListSortMap.jellyfin[query.sortBy] || 'Name,SortName',
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.startIndex,
},
});
if (res.status !== 200) {
throw new Error('Failed to get genre list');
@ -126,8 +134,8 @@ const getGenreList = async (args: GenreListArgs): Promise<GenreListResponse> =>
return {
items: res.body.Items.map(jfNormalize.genre),
startIndex: 0,
totalRecordCount: res.body?.Items?.length || 0,
startIndex: query.startIndex || 0,
totalRecordCount: res.body?.TotalRecordCount || 0,
};
};

View file

@ -304,10 +304,21 @@ const genre = z.object({
Type: z.string(),
});
const genreList = z.object({
const genreList = pagination.extend({
Items: z.array(genre),
});
const genreListSort = {
NAME: 'Name,SortName',
} as const;
const genreListParameters = paginationParameters.merge(
baseParameters.extend({
SearchTerm: z.string().optional(),
SortBy: z.nativeEnum(genreListSort).optional(),
}),
);
const musicFolder = z.object({
BackdropImageTags: z.array(z.string()),
ChannelId: z.null(),
@ -352,7 +363,7 @@ const playlist = z.object({
UserData: userData,
});
const jfPlaylistListSort = {
const playlistListSort = {
ALBUM_ARTIST: 'AlbumArtist,SortName',
DURATION: 'Runtime',
NAME: 'SortName',
@ -363,7 +374,7 @@ const jfPlaylistListSort = {
const playlistListParameters = paginationParameters.merge(
baseParameters.extend({
IncludeItemTypes: z.literal('Playlist'),
SortBy: z.nativeEnum(jfPlaylistListSort).optional(),
SortBy: z.nativeEnum(playlistListSort).optional(),
}),
);
@ -461,7 +472,7 @@ const album = z.object({
UserData: userData.optional(),
});
const jfAlbumListSort = {
const albumListSort = {
ALBUM_ARTIST: 'AlbumArtist,SortName',
COMMUNITY_RATING: 'CommunityRating,SortName',
CRITIC_RATING: 'CriticRating,SortName',
@ -479,7 +490,7 @@ const albumListParameters = paginationParameters.merge(
IncludeItemTypes: z.literal('MusicAlbum'),
IsFavorite: z.boolean().optional(),
SearchTerm: z.string().optional(),
SortBy: z.nativeEnum(jfAlbumListSort).optional(),
SortBy: z.nativeEnum(albumListSort).optional(),
Tags: z.string().optional(),
Years: z.string().optional(),
}),
@ -489,7 +500,7 @@ const albumList = pagination.extend({
Items: z.array(album),
});
const jfAlbumArtistListSort = {
const albumArtistListSort = {
ALBUM: 'Album,SortName',
DURATION: 'Runtime,AlbumArtist,Album,SortName',
NAME: 'Name,SortName',
@ -502,7 +513,7 @@ const albumArtistListParameters = paginationParameters.merge(
baseParameters.extend({
Filters: z.string().optional(),
Genres: z.string().optional(),
SortBy: z.nativeEnum(jfAlbumArtistListSort).optional(),
SortBy: z.nativeEnum(albumArtistListSort).optional(),
Years: z.string().optional(),
}),
);
@ -515,7 +526,7 @@ const similarArtistListParameters = baseParameters.extend({
Limit: z.number().optional(),
});
const jfSongListSort = {
const songListSort = {
ALBUM: 'Album,SortName',
ALBUM_ARTIST: 'AlbumArtist,Album,SortName',
ARTIST: 'Artist,Album,SortName',
@ -539,7 +550,7 @@ const songListParameters = paginationParameters.merge(
Genres: z.string().optional(),
IsFavorite: z.boolean().optional(),
SearchTerm: z.string().optional(),
SortBy: z.nativeEnum(jfSongListSort).optional(),
SortBy: z.nativeEnum(songListSort).optional(),
Tags: z.string().optional(),
Years: z.string().optional(),
}),
@ -642,9 +653,14 @@ const lyrics = z.object({
export const jfType = {
_enum: {
albumArtistList: albumArtistListSort,
albumList: albumListSort,
collection: jfCollection,
external: jfExternal,
genreList: genreListSort,
image: jfImage,
playlistList: playlistListSort,
songList: songListSort,
},
_parameters: {
addToPlaylist: addToPlaylistParameters,
@ -656,6 +672,7 @@ export const jfType = {
createPlaylist: createPlaylistParameters,
deletePlaylist: deletePlaylistParameters,
favorite: favoriteParameters,
genreList: genreListParameters,
musicFolderList: musicFolderListParameters,
playlistDetail: playlistDetailParameters,
playlistList: playlistListParameters,

View file

@ -88,6 +88,7 @@ export const contract = c.router({
getGenreList: {
method: 'GET',
path: 'genre',
query: ndType._parameters.genreList,
responses: {
200: resultWithHeaders(ndType._response.genreList),
500: resultWithHeaders(ndType._response.error),

View file

@ -38,6 +38,7 @@ import {
PlaylistSongListResponse,
RemoveFromPlaylistResponse,
RemoveFromPlaylistArgs,
genreListSortMap,
} from '../types';
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
@ -94,9 +95,17 @@ const getUserList = async (args: UserListArgs): Promise<UserListResponse> => {
};
const getGenreList = async (args: GenreListArgs): Promise<GenreListResponse> => {
const { apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getGenreList({});
const res = await ndApiClient(apiClientProps).getGenreList({
query: {
_end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: genreListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
name: query.searchTerm,
},
});
if (res.status !== 200) {
throw new Error('Failed to get genre list');
@ -104,7 +113,7 @@ const getGenreList = async (args: GenreListArgs): Promise<GenreListResponse> =>
return {
items: res.body.data,
startIndex: 0,
startIndex: query.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
};

View file

@ -52,6 +52,16 @@ const genre = z.object({
name: z.string(),
});
const genreListSort = {
NAME: 'name',
SONG_COUNT: 'songCount',
} as const;
const genreListParameters = paginationParameters.extend({
_sort: z.nativeEnum(genreListSort).optional(),
name: z.string().optional(),
});
const genreList = z.array(genre);
const albumArtist = z.object({
@ -322,6 +332,7 @@ export const ndType = {
_enum: {
albumArtistList: ndAlbumArtistListSort,
albumList: ndAlbumListSort,
genreList: genreListSort,
playlistList: ndPlaylistListSort,
songList: ndSongListSort,
userList: ndUserListSort,
@ -332,6 +343,7 @@ export const ndType = {
albumList: albumListParameters,
authenticate: authenticateParameters,
createPlaylist: createPlaylistParameters,
genreList: genreListParameters,
playlistList: playlistListParameters,
removeFromPlaylist: removeFromPlaylistParameters,
songList: songListParameters,

View file

@ -17,6 +17,7 @@ import type {
RandomSongListQuery,
LyricsQuery,
LyricSearchQuery,
GenreListQuery,
} from './types';
export const splitPaginatedQuery = (key: any) => {
@ -106,7 +107,18 @@ export const queryKeys: Record<
root: (serverId: string) => [serverId, 'artists'] as const,
},
genres: {
list: (serverId: string) => [serverId, 'genres', 'list'] as const,
list: (serverId: string, query?: GenreListQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'genres', 'list', filter, pagination] as const;
}
if (query) {
return [serverId, 'genres', 'list', filter] as const;
}
return [serverId, 'genres', 'list'] as const;
},
root: (serverId: string) => [serverId, 'genres'] as const,
},
musicFolders: {

View file

@ -1,4 +1,5 @@
import { z } from 'zod';
import { jfType } from './jellyfin/jellyfin-types';
import {
JFSortOrder,
JFAlbumListSort,
@ -6,8 +7,9 @@ import {
JFAlbumArtistListSort,
JFArtistListSort,
JFPlaylistListSort,
JFGenreListSort,
} from './jellyfin.types';
import { jfType } from './jellyfin/jellyfin-types';
import { ndType } from './navidrome/navidrome-types';
import {
NDSortOrder,
NDOrder,
@ -16,13 +18,14 @@ import {
NDPlaylistListSort,
NDSongListSort,
NDUserListSort,
NDGenreListSort,
} from './navidrome.types';
import { ndType } from './navidrome/navidrome-types';
export enum LibraryItem {
ALBUM = 'album',
ALBUM_ARTIST = 'albumArtist',
ARTIST = 'artist',
GENRE = 'genre',
PLAYLIST = 'playlist',
SONG = 'song',
}
@ -292,7 +295,40 @@ export type GenreListResponse = BasePaginatedResponse<Genre[]> | null | undefine
export type GenreListArgs = { query: GenreListQuery } & BaseEndpointArgs;
export type GenreListQuery = null;
export enum GenreListSort {
NAME = 'name',
}
export type GenreListQuery = {
_custom?: {
jellyfin?: null;
navidrome?: null;
};
limit?: number;
musicFolderId?: string;
searchTerm?: string;
sortBy: GenreListSort;
sortOrder: SortOrder;
startIndex: number;
};
type GenreListSortMap = {
jellyfin: Record<GenreListSort, JFGenreListSort | undefined>;
navidrome: Record<GenreListSort, NDGenreListSort | undefined>;
subsonic: Record<UserListSort, undefined>;
};
export const genreListSortMap: GenreListSortMap = {
jellyfin: {
name: JFGenreListSort.NAME,
},
navidrome: {
name: NDGenreListSort.NAME,
},
subsonic: {
name: undefined,
},
};
// Album List
export type AlbumListResponse = BasePaginatedResponse<Album[]> | null | undefined;