mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
use tags list for Navidrome genres to support counts
This commit is contained in:
@@ -853,7 +853,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
getTags: async (args) => {
|
getTagList: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (!hasFeature(apiClientProps.server, ServerFeature.TAGS)) {
|
if (!hasFeature(apiClientProps.server, ServerFeature.TAGS)) {
|
||||||
|
|||||||
@@ -140,11 +140,12 @@ export const contract = c.router({
|
|||||||
500: resultWithHeaders(ndType._response.error),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getTags: {
|
getTagList: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'tag',
|
path: 'tag',
|
||||||
|
query: ndType._parameters.tagList,
|
||||||
responses: {
|
responses: {
|
||||||
200: resultWithHeaders(ndType._response.tags),
|
200: resultWithHeaders(ndType._response.tagList),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
Song,
|
Song,
|
||||||
songListSortMap,
|
songListSortMap,
|
||||||
sortOrderMap,
|
sortOrderMap,
|
||||||
|
tagListSortMap,
|
||||||
userListSortMap,
|
userListSortMap,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ServerFeature } from '/@/shared/types/features-types';
|
import { ServerFeature } from '/@/shared/types/features-types';
|
||||||
@@ -395,6 +396,40 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
getGenreList: async (args) => {
|
getGenreList: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
if (hasFeature(apiClientProps.server, ServerFeature.BFR)) {
|
||||||
|
const res = await ndApiClient(apiClientProps).getTagList({
|
||||||
|
query: {
|
||||||
|
_end: query.startIndex + (query.limit || 0),
|
||||||
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
|
_sort: tagListSortMap.navidrome[query.sortBy],
|
||||||
|
_start: query.startIndex,
|
||||||
|
library_id: getLibraryId(query.musicFolderId),
|
||||||
|
tag_name: 'genre',
|
||||||
|
tag_value: query.searchTerm,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get genre list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: res.body.data.map((genre) =>
|
||||||
|
ndNormalize.genre(
|
||||||
|
{
|
||||||
|
albumCount: genre.albumCount,
|
||||||
|
id: genre.id,
|
||||||
|
name: genre.tagValue,
|
||||||
|
songCount: genre.songCount,
|
||||||
|
},
|
||||||
|
apiClientProps.server,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
startIndex: query.startIndex || 0,
|
||||||
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getGenreList({
|
const res = await ndApiClient(apiClientProps).getGenreList({
|
||||||
query: {
|
query: {
|
||||||
_end: query.startIndex + (query.limit || 0),
|
_end: query.startIndex + (query.limit || 0),
|
||||||
@@ -637,14 +672,16 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
}).then((result) => result!.totalRecordCount!),
|
}).then((result) => result!.totalRecordCount!),
|
||||||
getStreamUrl: SubsonicController.getStreamUrl,
|
getStreamUrl: SubsonicController.getStreamUrl,
|
||||||
getStructuredLyrics: SubsonicController.getStructuredLyrics,
|
getStructuredLyrics: SubsonicController.getStructuredLyrics,
|
||||||
getTags: async (args) => {
|
getTagList: async (args) => {
|
||||||
const { apiClientProps } = args;
|
const { apiClientProps } = args;
|
||||||
|
|
||||||
if (!hasFeature(apiClientProps.server, ServerFeature.TAGS)) {
|
if (!hasFeature(apiClientProps.server, ServerFeature.TAGS)) {
|
||||||
return { boolTags: undefined, enumTags: undefined, excluded: { album: [], song: [] } };
|
return { boolTags: undefined, enumTags: undefined, excluded: { album: [], song: [] } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getTags();
|
const res = await ndApiClient(apiClientProps).getTagList({
|
||||||
|
query: {},
|
||||||
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('failed to get tags');
|
throw new Error('failed to get tags');
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
|
|||||||
}, [genreListQuery.data]);
|
}, [genreListQuery.data]);
|
||||||
|
|
||||||
const tagsQuery = useQuery(
|
const tagsQuery = useQuery(
|
||||||
sharedQueries.tags({
|
sharedQueries.tagList({
|
||||||
options: {
|
options: {
|
||||||
gcTime: 1000 * 60 * 2,
|
gcTime: 1000 * 60 * 2,
|
||||||
staleTime: 1000 * 60 * 1,
|
staleTime: 1000 * 60 * 1,
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ const TagFilters = () => {
|
|||||||
const serverId = useCurrentServerId();
|
const serverId = useCurrentServerId();
|
||||||
|
|
||||||
const tagsQuery = useSuspenseQuery(
|
const tagsQuery = useSuspenseQuery(
|
||||||
sharedQueries.tags({
|
sharedQueries.tagList({
|
||||||
options: {
|
options: {
|
||||||
gcTime: 1000 * 60 * 60,
|
gcTime: 1000 * 60 * 60,
|
||||||
staleTime: 1000 * 60 * 60,
|
staleTime: 1000 * 60 * 60,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { queryOptions } from '@tanstack/react-query';
|
|||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||||
import { MusicFolderListQuery, TagQuery, UserListQuery } from '/@/shared/types/domain-types';
|
import { MusicFolderListQuery, TagListQuery, UserListQuery } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export const sharedQueries = {
|
export const sharedQueries = {
|
||||||
musicFolders: (args: QueryHookArgs<MusicFolderListQuery>) => {
|
musicFolders: (args: QueryHookArgs<MusicFolderListQuery>) => {
|
||||||
@@ -28,11 +28,11 @@ export const sharedQueries = {
|
|||||||
...args.options,
|
...args.options,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
tags: (args: QueryHookArgs<TagQuery>) => {
|
tagList: (args: QueryHookArgs<TagListQuery>) => {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
gcTime: 1000 * 60,
|
gcTime: 1000 * 60,
|
||||||
queryFn: ({ signal }) => {
|
queryFn: ({ signal }) => {
|
||||||
return api.controller.getTags({
|
return api.controller.getTagList({
|
||||||
apiClientProps: { serverId: args.serverId, signal },
|
apiClientProps: { serverId: args.serverId, signal },
|
||||||
query: args.query,
|
query: args.query,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const JellyfinSongFilters = () => {
|
|||||||
}, [genreListQuery.data]);
|
}, [genreListQuery.data]);
|
||||||
|
|
||||||
const tagsQuery = useQuery(
|
const tagsQuery = useQuery(
|
||||||
sharedQueries.tags({
|
sharedQueries.tagList({
|
||||||
query: {
|
query: {
|
||||||
type: LibraryItem.SONG,
|
type: LibraryItem.SONG,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ const TagFilters = () => {
|
|||||||
const serverId = useCurrentServerId();
|
const serverId = useCurrentServerId();
|
||||||
|
|
||||||
const tagsQuery = useSuspenseQuery(
|
const tagsQuery = useSuspenseQuery(
|
||||||
sharedQueries.tags({
|
sharedQueries.tagList({
|
||||||
query: { type: LibraryItem.SONG },
|
query: { type: LibraryItem.SONG },
|
||||||
serverId,
|
serverId,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -442,18 +442,18 @@ const normalizePlaylist = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const normalizeGenre = (
|
const normalizeGenre = (
|
||||||
item: z.infer<typeof ndType._response.genre>,
|
item: z.infer<typeof ndType._response.genre> & { albumCount?: number; songCount?: number },
|
||||||
server: null | ServerListItem,
|
server: null | ServerListItem,
|
||||||
): Genre => {
|
): Genre => {
|
||||||
return {
|
return {
|
||||||
_itemType: LibraryItem.GENRE,
|
_itemType: LibraryItem.GENRE,
|
||||||
_serverId: server?.id || 'unknown',
|
_serverId: server?.id || 'unknown',
|
||||||
_serverType: ServerType.NAVIDROME,
|
_serverType: ServerType.NAVIDROME,
|
||||||
albumCount: null,
|
albumCount: item.albumCount ?? null,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
songCount: null,
|
songCount: item.songCount ?? null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -235,6 +235,8 @@ const paginationParameters = z.object({
|
|||||||
_start: z.number().optional(),
|
_start: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const optionalPaginationParameters = paginationParameters.partial();
|
||||||
|
|
||||||
const authenticate = z.object({
|
const authenticate = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
isAdmin: z.boolean(),
|
isAdmin: z.boolean(),
|
||||||
@@ -578,7 +580,18 @@ const tag = z.object({
|
|||||||
tagValue: z.string(),
|
tagValue: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const tags = z.array(tag);
|
const tagList = z.array(tag);
|
||||||
|
|
||||||
|
export enum NDTagListSort {
|
||||||
|
TAG_VALUE = 'tagValue',
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagListParameters = optionalPaginationParameters.extend({
|
||||||
|
_sort: z.nativeEnum(NDTagListSort).optional(),
|
||||||
|
library_id: z.array(z.string()).optional(),
|
||||||
|
tag_name: z.string().optional(),
|
||||||
|
tag_value: z.string().optional(), // Search
|
||||||
|
});
|
||||||
|
|
||||||
export const ndType = {
|
export const ndType = {
|
||||||
_enum: {
|
_enum: {
|
||||||
@@ -587,6 +600,7 @@ export const ndType = {
|
|||||||
genreList: genreListSort,
|
genreList: genreListSort,
|
||||||
playlistList: NDPlaylistListSort,
|
playlistList: NDPlaylistListSort,
|
||||||
songList: NDSongListSort,
|
songList: NDSongListSort,
|
||||||
|
tagList: NDTagListSort,
|
||||||
userList: ndUserListSort,
|
userList: ndUserListSort,
|
||||||
},
|
},
|
||||||
_parameters: {
|
_parameters: {
|
||||||
@@ -601,6 +615,7 @@ export const ndType = {
|
|||||||
removeFromPlaylist: removeFromPlaylistParameters,
|
removeFromPlaylist: removeFromPlaylistParameters,
|
||||||
shareItem: shareItemParameters,
|
shareItem: shareItemParameters,
|
||||||
songList: songListParameters,
|
songList: songListParameters,
|
||||||
|
tagList: tagListParameters,
|
||||||
updatePlaylist: updatePlaylistParameters,
|
updatePlaylist: updatePlaylistParameters,
|
||||||
userList: userListParameters,
|
userList: userListParameters,
|
||||||
},
|
},
|
||||||
@@ -625,7 +640,7 @@ export const ndType = {
|
|||||||
shareItem,
|
shareItem,
|
||||||
song,
|
song,
|
||||||
songList,
|
songList,
|
||||||
tags,
|
tagList,
|
||||||
updatePlaylist,
|
updatePlaylist,
|
||||||
user,
|
user,
|
||||||
userList,
|
userList,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
NDPlaylistListSort,
|
NDPlaylistListSort,
|
||||||
NDSongListSort,
|
NDSongListSort,
|
||||||
NDSortOrder,
|
NDSortOrder,
|
||||||
|
NDTagListSort,
|
||||||
NDUserListSort,
|
NDUserListSort,
|
||||||
} from '/@/shared/api/navidrome/navidrome-types';
|
} from '/@/shared/api/navidrome/navidrome-types';
|
||||||
import { ServerFeatures } from '/@/shared/types/features-types';
|
import { ServerFeatures } from '/@/shared/types/features-types';
|
||||||
@@ -159,6 +160,10 @@ export enum ImageType {
|
|||||||
SCREENSHOT = 'SCREENSHOT',
|
SCREENSHOT = 'SCREENSHOT',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum TagListSort {
|
||||||
|
TAG_VALUE = 'tagValue',
|
||||||
|
}
|
||||||
|
|
||||||
export type Album = {
|
export type Album = {
|
||||||
_itemType: LibraryItem.ALBUM;
|
_itemType: LibraryItem.ALBUM;
|
||||||
_serverId: string;
|
_serverId: string;
|
||||||
@@ -399,6 +404,24 @@ export const genreListSortMap: GenreListSortMap = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TagListSortMap = {
|
||||||
|
jellyfin: Record<TagListSort, undefined>;
|
||||||
|
navidrome: Record<TagListSort, NDTagListSort | undefined>;
|
||||||
|
subsonic: Record<TagListSort, undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tagListSortMap: TagListSortMap = {
|
||||||
|
jellyfin: {
|
||||||
|
tagValue: undefined,
|
||||||
|
},
|
||||||
|
navidrome: {
|
||||||
|
tagValue: NDTagListSort.TAG_VALUE,
|
||||||
|
},
|
||||||
|
subsonic: {
|
||||||
|
tagValue: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export enum AlbumListSort {
|
export enum AlbumListSort {
|
||||||
ALBUM_ARTIST = 'albumArtist',
|
ALBUM_ARTIST = 'albumArtist',
|
||||||
ARTIST = 'artist',
|
ARTIST = 'artist',
|
||||||
@@ -1224,7 +1247,7 @@ export type ControllerEndpoint = {
|
|||||||
getSongListCount: (args: SongListCountArgs) => Promise<number>;
|
getSongListCount: (args: SongListCountArgs) => Promise<number>;
|
||||||
getStreamUrl: (args: StreamArgs) => string;
|
getStreamUrl: (args: StreamArgs) => string;
|
||||||
getStructuredLyrics?: (args: StructuredLyricsArgs) => Promise<StructuredLyric[]>;
|
getStructuredLyrics?: (args: StructuredLyricsArgs) => Promise<StructuredLyric[]>;
|
||||||
getTags?: (args: TagArgs) => Promise<TagsResponse>;
|
getTagList?: (args: TagListArgs) => Promise<TagListResponse>;
|
||||||
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
|
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
|
||||||
getUserList?: (args: UserListArgs) => Promise<UserListResponse>;
|
getUserList?: (args: UserListArgs) => Promise<UserListResponse>;
|
||||||
movePlaylistItem?: (args: MoveItemArgs) => Promise<void>;
|
movePlaylistItem?: (args: MoveItemArgs) => Promise<void>;
|
||||||
@@ -1316,7 +1339,7 @@ export type InternalControllerEndpoint = {
|
|||||||
getStructuredLyrics?: (
|
getStructuredLyrics?: (
|
||||||
args: ReplaceApiClientProps<StructuredLyricsArgs>,
|
args: ReplaceApiClientProps<StructuredLyricsArgs>,
|
||||||
) => Promise<StructuredLyric[]>;
|
) => Promise<StructuredLyric[]>;
|
||||||
getTags?: (args: ReplaceApiClientProps<TagArgs>) => Promise<TagsResponse>;
|
getTagList?: (args: ReplaceApiClientProps<TagListArgs>) => Promise<TagListResponse>;
|
||||||
getTopSongs: (args: ReplaceApiClientProps<TopSongListArgs>) => Promise<TopSongListResponse>;
|
getTopSongs: (args: ReplaceApiClientProps<TopSongListArgs>) => Promise<TopSongListResponse>;
|
||||||
getUserList?: (args: ReplaceApiClientProps<UserListArgs>) => Promise<UserListResponse>;
|
getUserList?: (args: ReplaceApiClientProps<UserListArgs>) => Promise<UserListResponse>;
|
||||||
movePlaylistItem?: (args: ReplaceApiClientProps<MoveItemArgs>) => Promise<void>;
|
movePlaylistItem?: (args: ReplaceApiClientProps<MoveItemArgs>) => Promise<void>;
|
||||||
@@ -1413,16 +1436,17 @@ export type Tag = {
|
|||||||
options: { id: string; name: string }[];
|
options: { id: string; name: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TagArgs = BaseEndpointArgs & {
|
export type TagListArgs = BaseEndpointArgs & {
|
||||||
query: TagQuery;
|
query: TagListQuery;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TagQuery = {
|
export type TagListQuery = {
|
||||||
folder?: string;
|
folder?: string;
|
||||||
|
tagName?: string;
|
||||||
type: LibraryItem.ALBUM | LibraryItem.SONG;
|
type: LibraryItem.ALBUM | LibraryItem.SONG;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TagsResponse = {
|
export type TagListResponse = {
|
||||||
boolTags?: string[];
|
boolTags?: string[];
|
||||||
enumTags?: { name: string; options: { id: string; name: string }[] }[];
|
enumTags?: { name: string; options: { id: string; name: string }[] }[];
|
||||||
excluded: {
|
excluded: {
|
||||||
|
|||||||
Reference in New Issue
Block a user