fix subsonic / jellyfin filters

This commit is contained in:
jeffvli
2025-11-30 17:25:44 -08:00
parent c5c2b24a9d
commit 96acf759ff
8 changed files with 395 additions and 253 deletions
@@ -1,5 +1,4 @@
import { useQuery } from '@tanstack/react-query';
import debounce from 'lodash/debounce';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -7,7 +6,7 @@ import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-i
import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
import { SongListFilter, useCurrentServer } from '/@/renderer/store';
import { SongListFilter, useCurrentServerId } from '/@/renderer/store';
import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group';
import { NumberInput } from '/@/shared/components/number-input/number-input';
@@ -21,7 +20,7 @@ interface JellyfinSongFiltersProps {
}
export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps) => {
const server = useCurrentServer();
const serverId = useCurrentServerId();
const { t } = useTranslation();
const { query, setCustom, setFavorite, setMaxYear, setMinYear } = useSongListFilters();
@@ -44,7 +43,7 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps)
query: {
type: LibraryItem.SONG,
},
serverId: server.id,
serverId,
}),
);
@@ -66,33 +65,91 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps)
},
];
const handleMinYearFilter = debounce((e: number | string) => {
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
setMinYear(e === '' ? null : (e as number));
}, 500);
const handleMinYearFilter = useMemo(
() => (e: number | string) => {
// Handle empty string, null, undefined, or invalid numbers as clearing
if (e === '' || e === null || e === undefined || isNaN(Number(e))) {
setMinYear(null);
return;
}
const handleMaxYearFilter = debounce((e: number | string) => {
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
setMaxYear(e === '' ? null : (e as number));
}, 500);
const year = typeof e === 'number' ? e : Number(e);
// If it's a valid number within range, set it; otherwise clear
if (!isNaN(year) && isFinite(year) && year >= 1700 && year <= 2300) {
setMinYear(year);
} else {
setMinYear(null);
}
},
[setMinYear],
);
const handleGenresFilter = debounce((e: string[] | undefined) => {
setCustom((prev) => ({
...prev,
GenreIds: e?.join(',') || undefined,
IncludeItemTypes: 'Audio',
...prev?.jellyfin,
}));
}, 250);
const handleMaxYearFilter = useMemo(
() => (e: number | string) => {
// Handle empty string, null, undefined, or invalid numbers as clearing
if (e === '' || e === null || e === undefined || isNaN(Number(e))) {
setMaxYear(null);
return;
}
const handleTagFilter = debounce((e: string[] | undefined) => {
setCustom((prev) => ({
...prev,
IncludeItemTypes: 'Audio',
Tags: e?.join('|') || undefined,
...prev?.jellyfin,
}));
}, 250);
const year = typeof e === 'number' ? e : Number(e);
// If it's a valid number within range, set it; otherwise clear
if (!isNaN(year) && isFinite(year) && year >= 1700 && year <= 2300) {
setMaxYear(year);
} else {
setMaxYear(null);
}
},
[setMaxYear],
);
const handleGenresFilter = useMemo(
() => (e: string[] | undefined) => {
setCustom((prev) => {
if (!e || e.length === 0) {
// Remove GenreIds and IncludeItemTypes if genres are cleared
const rest = { ...prev };
delete rest.GenreIds;
delete rest.IncludeItemTypes;
// Keep jellyfin-specific properties
return Object.keys(rest).length === 0 ? null : rest;
}
return {
...prev,
GenreIds: e.join(','),
IncludeItemTypes: 'Audio',
...prev?.jellyfin,
};
});
},
[setCustom],
);
const handleTagFilter = useMemo(
() => (e: string[] | undefined) => {
setCustom((prev) => {
if (!e || e.length === 0) {
// Remove Tags if cleared
const rest = { ...prev };
delete rest.Tags;
// Keep IncludeItemTypes and jellyfin-specific properties
if (rest.IncludeItemTypes) {
return rest;
}
return Object.keys(rest).length === 0 ? null : rest;
}
return {
...prev,
IncludeItemTypes: 'Audio',
Tags: e.join('|'),
...prev?.jellyfin,
};
});
},
[setCustom],
);
return (
<Stack p="0.8rem">
@@ -105,19 +162,21 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps)
<Divider my="0.5rem" />
<Group grow>
<NumberInput
defaultValue={query.minYear}
defaultValue={query.minYear ?? undefined}
hideControls={false}
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
max={2300}
min={1700}
onChange={handleMinYearFilter}
onBlur={(e) => handleMinYearFilter(e.currentTarget.value)}
required={!!query.minYear}
/>
<NumberInput
defaultValue={query.maxYear}
defaultValue={query.maxYear ?? undefined}
hideControls={false}
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
max={2300}
min={1700}
onChange={handleMaxYearFilter}
onBlur={(e) => handleMaxYearFilter(e.currentTarget.value)}
required={!!query.minYear}
/>
</Group>
@@ -128,7 +187,7 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps)
data={genreList}
defaultValue={selectedGenres}
label={t('entity.genre', { count: 1, postProcess: 'sentenceCase' })}
onChange={handleGenresFilter}
onChange={(e) => handleGenresFilter(e)}
searchable
width={250}
/>
@@ -141,7 +200,7 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps)
data={tagsQuery.data.boolTags}
defaultValue={selectedTags}
label={t('common.tags', { postProcess: 'sentenceCase' })}
onChange={handleTagFilter}
onChange={(e) => handleTagFilter(e)}
searchable
width={250}
/>
@@ -1,5 +1,4 @@
import debounce from 'lodash/debounce';
import { useMemo } from 'react';
import { ChangeEvent, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
@@ -9,8 +8,8 @@ import { SongListFilter } from '/@/renderer/store';
import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group';
import { Stack } from '/@/shared/components/stack/stack';
import { Switch } from '/@/shared/components/switch/switch';
import { Text } from '/@/shared/components/text/text';
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
interface SubsonicSongFiltersProps {
customFilters?: Partial<SongListFilter>;
@@ -32,26 +31,33 @@ export const SubsonicSongFilters = ({ customFilters }: SubsonicSongFiltersProps)
}));
}, [genreListQuery.data]);
const handleGenresFilter = debounce((e: null | string) => {
setGenreId(e ? [e] : null);
}, 250);
const toggleFilters = [
{
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
onChange: (favorite: boolean | undefined) => {
setFavorite(favorite ?? null);
},
value: query.favorite,
const handleGenresFilter = useMemo(
() => (e: null | string) => {
setGenreId(e ? [e] : null);
},
];
[setGenreId],
);
const toggleFilters = useMemo(
() => [
{
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
onChange: (e: ChangeEvent<HTMLInputElement>) => {
const favoriteValue = e.target.checked ? true : undefined;
setFavorite(favoriteValue ?? null);
},
value: query.favorite,
},
],
[t, query.favorite, setFavorite],
);
return (
<Stack p="0.8rem">
{toggleFilters.map((filter) => (
<Group justify="space-between" key={`ss-filter-${filter.label}`}>
<Text>{filter.label}</Text>
<YesNoSelect onChange={filter.onChange} size="xs" value={filter.value} />
<Switch checked={filter.value ?? false} onChange={filter.onChange} />
</Group>
))}
<Divider my="0.5rem" />