add multiple genre support for nd albums/tracks

This commit is contained in:
Kendall Garner 2025-09-28 19:59:20 -07:00
parent 6df270ba34
commit 4a48598260
No known key found for this signature in database
GPG key ID: 9355F387FE765C94
4 changed files with 77 additions and 20 deletions

View file

@ -271,6 +271,10 @@ export const NavidromeController: ControllerEndpoint = {
getAlbumList: async (args) => {
const { apiClientProps, query } = args;
const genres = hasFeature(apiClientProps.server, ServerFeature.BFR)
? query.genres
: query.genres?.[0];
const res = await ndApiClient(apiClientProps).getAlbumList({
query: {
_end: query.startIndex + (query.limit || 0),
@ -279,7 +283,7 @@ export const NavidromeController: ControllerEndpoint = {
_start: query.startIndex,
artist_id: query.artistIds?.[0],
compilation: query.compilation,
genre_id: query.genres?.[0],
genre_id: genres,
name: query.searchTerm,
...query._custom?.navidrome,
starred: query.favorite,

View file

@ -2,12 +2,21 @@ import debounce from 'lodash/debounce';
import { ChangeEvent, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
import {
MultiSelectWithInvalidData,
SelectWithInvalidData,
} from '/@/renderer/components/select-with-invalid-data';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
import { useGenreList } from '/@/renderer/features/genres';
import { useTagList } from '/@/renderer/features/tag/queries/use-tag-list';
import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
import {
AlbumListFilter,
getServerById,
useListStoreActions,
useListStoreByKey,
} from '/@/renderer/store';
import { NDSongQueryFields } from '/@/shared/api/navidrome.types';
import { hasFeature } from '/@/shared/api/utils';
import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group';
import { NumberInput } from '/@/shared/components/number-input/number-input';
@ -23,6 +32,7 @@ import {
LibraryItem,
SortOrder,
} from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
interface NavidromeAlbumFiltersProps {
customFilters?: Partial<AlbumListFilter>;
@ -42,6 +52,7 @@ export const NavidromeAlbumFilters = ({
const { t } = useTranslation();
const { filter } = useListStoreByKey<AlbumListQuery>({ key: pageKey });
const { setFilter } = useListStoreActions();
const server = getServerById(serverId);
const genreListQuery = useGenreList({
options: {
@ -64,12 +75,14 @@ export const NavidromeAlbumFilters = ({
}));
}, [genreListQuery.data]);
const handleGenresFilter = debounce((e: null | string) => {
const hasBrf = hasFeature(server, ServerFeature.BFR);
const handleGenresFilter = debounce((e: null | string[]) => {
const updatedFilters = setFilter({
customFilters,
data: {
_custom: filter._custom,
genres: e ? [e] : undefined,
genres: e ? e : undefined,
},
itemType: LibraryItem.ALBUM,
key: pageKey,
@ -269,15 +282,29 @@ export const NavidromeAlbumFilters = ({
min={0}
onChange={(e) => handleYearFilter(e)}
/>
<SelectWithInvalidData
clearable
data={genreList}
defaultValue={filter.genres && filter.genres[0]}
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
onChange={handleGenresFilter}
searchable
/>
{!hasBrf && (
<SelectWithInvalidData
clearable
data={genreList}
defaultValue={filter.genres && filter.genres[0]}
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
onChange={(value) => handleGenresFilter(value !== null ? [value] : null)}
searchable
/>
)}
</Group>
{hasBrf && (
<Group grow>
<MultiSelectWithInvalidData
clearable
data={genreList}
defaultValue={filter.genres}
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
onChange={handleGenresFilter}
searchable
/>
</Group>
)}
<Group grow>
<SelectWithInvalidData
clearable

View file

@ -2,11 +2,20 @@ import debounce from 'lodash/debounce';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
import {
MultiSelectWithInvalidData,
SelectWithInvalidData,
} from '/@/renderer/components/select-with-invalid-data';
import { useGenreList } from '/@/renderer/features/genres';
import { useTagList } from '/@/renderer/features/tag/queries/use-tag-list';
import { SongListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store';
import {
getServerById,
SongListFilter,
useListFilterByKey,
useListStoreActions,
} from '/@/renderer/store';
import { NDSongQueryFields } from '/@/shared/api/navidrome.types';
import { hasFeature } from '/@/shared/api/utils';
import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group';
import { NumberInput } from '/@/shared/components/number-input/number-input';
@ -14,6 +23,7 @@ import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
interface NavidromeSongFiltersProps {
customFilters?: Partial<SongListFilter>;
@ -31,6 +41,7 @@ export const NavidromeSongFilters = ({
const { t } = useTranslation();
const { setFilter } = useListStoreActions();
const filter = useListFilterByKey<SongListQuery>({ key: pageKey });
const server = getServerById(serverId);
const isGenrePage = customFilters?.genreIds !== undefined;
@ -58,12 +69,14 @@ export const NavidromeSongFilters = ({
}));
}, [genreListQuery.data]);
const handleGenresFilter = debounce((e: null | string) => {
const hasBrf = hasFeature(server, ServerFeature.BFR);
const handleGenresFilter = debounce((e: null | string[]) => {
const updatedFilters = setFilter({
customFilters,
data: {
_custom: filter._custom,
genreIds: e ? [e] : undefined,
genreIds: e ? e : undefined,
},
itemType: LibraryItem.SONG,
key: pageKey,
@ -148,18 +161,30 @@ export const NavidromeSongFilters = ({
value={filter._custom?.navidrome?.year}
width={50}
/>
{!isGenrePage && (
{!isGenrePage && !hasBrf && (
<SelectWithInvalidData
clearable
data={genreList}
defaultValue={filter.genreIds ? filter.genreIds[0] : undefined}
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
onChange={handleGenresFilter}
onChange={(value) => handleGenresFilter(value !== null ? [value] : null)}
searchable
width={150}
/>
)}
</Group>
{!isGenrePage && hasBrf && (
<Group grow>
<MultiSelectWithInvalidData
clearable
data={genreList}
defaultValue={filter.genreIds}
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
onChange={handleGenresFilter}
searchable
/>
</Group>
)}
{tagsQuery.data?.enumTags?.length &&
tagsQuery.data.enumTags.length > 0 &&
tagsQuery.data.enumTags.map((tag) => (

View file

@ -169,7 +169,8 @@ const albumListParameters = paginationParameters.extend({
album_id: z.string().optional(),
artist_id: z.string().optional(),
compilation: z.boolean().optional(),
genre_id: z.string().optional(),
// in older versions, this was a single string. post BFR, you can repeat it multiple times
genre_id: z.union([z.string(), z.string().array()]).optional(),
has_rating: z.boolean().optional(),
id: z.string().optional(),
name: z.string().optional(),