Tag filter support

- Jellyfin: Uses `/items/filters` to get list of boolean tags. Notably, does not use this same filter for genres. Separate filter for song/album
- Navidrome: Uses `/api/tags`, which appears to be album-level as multiple independent selects. Same filter for song/album
This commit is contained in:
Kendall Garner 2025-05-18 09:23:52 -07:00
parent b0d86ee5c9
commit e1aa8d74f3
No known key found for this signature in database
GPG key ID: 9355F387FE765C94
17 changed files with 360 additions and 16 deletions

View file

@ -104,6 +104,15 @@ export const contract = c.router({
400: jfType._response.error,
},
},
getFilterList: {
method: 'GET',
path: 'items/filters',
query: jfType._parameters.filterList,
responses: {
200: jfType._response.filters,
400: jfType._response.error,
},
},
getGenreList: {
method: 'GET',
path: 'genres',

View file

@ -8,6 +8,7 @@ import {
Song,
Played,
ControllerEndpoint,
LibraryItem,
} from '/@/renderer/api/types';
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
import { jfNormalize } from './jellyfin-normalize';
@ -15,7 +16,7 @@ import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
import { z } from 'zod';
import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types';
import { ServerFeature } from '/@/renderer/api/features-types';
import { VersionInfo, getFeatures } from '/@/renderer/api/utils';
import { VersionInfo, getFeatures, hasFeature } from '/@/renderer/api/utils';
import chunk from 'lodash/chunk';
const formatCommaDelimitedString = (value: string[]) => {
@ -35,6 +36,7 @@ const VERSION_INFO: VersionInfo = [
[ServerFeature.PUBLIC_PLAYLIST]: [1],
},
],
['10.0.0', { [ServerFeature.TAGS]: [1] }],
];
export const JellyfinController: ControllerEndpoint = {
@ -803,6 +805,31 @@ export const JellyfinController: ControllerEndpoint = {
apiClientProps,
query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!),
getTags: async (args) => {
const { apiClientProps, query } = args;
if (!hasFeature(apiClientProps.server, ServerFeature.TAGS)) {
return { boolTags: undefined, enumTags: undefined };
}
const res = await jfApiClient(apiClientProps).getFilterList({
query: {
IncludeItemTypes: query.type === LibraryItem.SONG ? 'Audio' : 'MusicAlbum',
ParentId: query.folder,
UserId: apiClientProps.server?.userId ?? '',
},
});
if (res.status !== 200) {
throw new Error('failed to get tags');
}
return {
boolTags: res.body.Tags?.sort((a, b) =>
a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()),
),
};
},
getTopSongs: async (args) => {
const { apiClientProps, query } = args;

View file

@ -697,6 +697,18 @@ export enum JellyfinExtensions {
const moveItem = z.null();
const filterListParameters = z.object({
IncludeItemTypes: z.string().optional(),
ParentId: z.string().optional(),
UserId: z.string().optional(),
});
const filters = z.object({
Genres: z.string().array().optional(),
Tags: z.string().array().optional(),
Years: z.number().array().optional(),
});
export const jfType = {
_enum: {
albumArtistList: albumArtistListSort,
@ -718,6 +730,7 @@ export const jfType = {
createPlaylist: createPlaylistParameters,
deletePlaylist: deletePlaylistParameters,
favorite: favoriteParameters,
filterList: filterListParameters,
genreList: genreListParameters,
musicFolderList: musicFolderListParameters,
playlistDetail: playlistDetailParameters,
@ -742,6 +755,7 @@ export const jfType = {
deletePlaylist,
error,
favorite,
filters,
genre,
genreList,
lyrics,