mirror of
https://github.com/antebudimir/feishin.git
synced 2025-12-31 10:03:33 +00:00
Compilation support for Jellyfin artist albums, misc other album filter fixes
- Jellyfin will use `ContributingArtistsId` (compilation), `AlbumArtistIds` (compilation is false), or `ArtistIds` (unspecified; all) - Jellyfin can filter by compilation _only_ on the artist discography page - Navidrome album filter fix for `defaultValue` display and prevent showing `tagQuery` 0 when querying - Subsonic can filter by one or more artists in the album page. Sort is also applied on these items - Bump genre/tag cache/stale time to 2/1 minutes - Fix various cases where the album filter would display as active when it wasn't
This commit is contained in:
parent
6f5dd4881a
commit
176a95a946
7 changed files with 167 additions and 47 deletions
|
|
@ -290,19 +290,32 @@ export const JellyfinController: ControllerEndpoint = {
|
|||
|
||||
const yearsFilter = yearsGroup.length ? yearsGroup.join(',') : undefined;
|
||||
|
||||
let artistQuery:
|
||||
| Omit<z.infer<typeof jfType._parameters.albumList>, 'IncludeItemTypes'>
|
||||
| undefined;
|
||||
|
||||
if (query.artistIds) {
|
||||
// Based mostly off of observation, this is the behavior I've seen:
|
||||
// ContributingArtistIds is the _closest_ to where the album is a compilation and the artist is involved
|
||||
// AlbumArtistIds is where the artist is an album artist
|
||||
// ArtistIds is all credits
|
||||
if (query.compilation) {
|
||||
artistQuery = {
|
||||
ContributingArtistIds: formatCommaDelimitedString(query.artistIds),
|
||||
};
|
||||
} else if (query.compilation === false) {
|
||||
artistQuery = { AlbumArtistIds: formatCommaDelimitedString(query.artistIds) };
|
||||
} else {
|
||||
artistQuery = { ArtistIds: formatCommaDelimitedString(query.artistIds) };
|
||||
}
|
||||
}
|
||||
|
||||
const res = await jfApiClient(apiClientProps).getAlbumList({
|
||||
params: {
|
||||
userId: apiClientProps.server?.userId,
|
||||
},
|
||||
query: {
|
||||
...(!query.compilation &&
|
||||
query.artistIds && {
|
||||
AlbumArtistIds: formatCommaDelimitedString(query.artistIds),
|
||||
}),
|
||||
...(query.compilation &&
|
||||
query.artistIds && {
|
||||
ContributingArtistIds: query.artistIds[0],
|
||||
}),
|
||||
...artistQuery,
|
||||
Fields: 'People, Tags',
|
||||
GenreIds: query.genres ? query.genres.join(',') : undefined,
|
||||
IncludeItemTypes: 'MusicAlbum',
|
||||
|
|
|
|||
|
|
@ -316,8 +316,10 @@ export const SubsonicController: ControllerEndpoint = {
|
|||
return artist.body.artist.album ?? [];
|
||||
});
|
||||
|
||||
const items = albums.map((album) => ssNormalize.album(album, apiClientProps.server));
|
||||
|
||||
return {
|
||||
items: albums.map((album) => ssNormalize.album(album, apiClientProps.server)),
|
||||
items: sortAlbumList(items, query.sortBy, query.sortOrder),
|
||||
startIndex: 0,
|
||||
totalRecordCount: albums.length,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -405,31 +405,35 @@ export const AlbumListHeaderFilters = ({
|
|||
const isFilterApplied = useMemo(() => {
|
||||
const isNavidromeFilterApplied =
|
||||
server?.type === ServerType.NAVIDROME &&
|
||||
filter?._custom?.navidrome &&
|
||||
Object.values(filter?._custom?.navidrome).some((value) => value !== undefined);
|
||||
((filter?._custom?.navidrome &&
|
||||
Object.values(filter?._custom?.navidrome).some((value) => value !== undefined)) ||
|
||||
// Compilation is always valid
|
||||
filter.compilation !== undefined);
|
||||
|
||||
const isJellyfinFilterApplied =
|
||||
server?.type === ServerType.JELLYFIN &&
|
||||
filter?._custom?.jellyfin &&
|
||||
Object.values(filter?._custom?.jellyfin).some((value) => value !== undefined);
|
||||
((filter?._custom?.jellyfin &&
|
||||
Object.values(filter?._custom?.jellyfin).some((value) => value !== undefined)) ||
|
||||
// Compilation filter is only valid when on the artist page
|
||||
(filter.compilation !== undefined && customFilters?.artistIds));
|
||||
|
||||
const isSubsonicFilterApplied =
|
||||
server?.type === ServerType.SUBSONIC && (filter.maxYear || filter.minYear);
|
||||
|
||||
const isCompilationFilterApplied =
|
||||
server?.type === ServerType.NAVIDROME && filter.compilation !== undefined;
|
||||
|
||||
return (
|
||||
isNavidromeFilterApplied ||
|
||||
isJellyfinFilterApplied ||
|
||||
isSubsonicFilterApplied ||
|
||||
filter.genres?.length ||
|
||||
filter.favorite !== undefined ||
|
||||
isCompilationFilterApplied
|
||||
// If we are on the artist page, the artist id filter should not be active
|
||||
(filter.artistIds?.length && !(customFilters?.artistIds as any | undefined)?.length)
|
||||
);
|
||||
}, [
|
||||
customFilters?.artistIds,
|
||||
filter?._custom?.jellyfin,
|
||||
filter?._custom?.navidrome,
|
||||
filter.artistIds?.length,
|
||||
filter.compilation,
|
||||
filter.favorite,
|
||||
filter.genres?.length,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||
|
|
@ -43,6 +43,10 @@ export const JellyfinAlbumFilters = ({
|
|||
|
||||
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
|
||||
const genreListQuery = useGenreList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
},
|
||||
query: {
|
||||
musicFolderId: filter?.musicFolderId,
|
||||
sortBy: GenreListSort.NAME,
|
||||
|
|
@ -61,6 +65,10 @@ export const JellyfinAlbumFilters = ({
|
|||
}, [genreListQuery.data]);
|
||||
|
||||
const tagsQuery = useTagList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
},
|
||||
query: {
|
||||
folder: filter?.musicFolderId,
|
||||
type: LibraryItem.ALBUM,
|
||||
|
|
@ -72,24 +80,55 @@ export const JellyfinAlbumFilters = ({
|
|||
return filter?._custom?.jellyfin?.Tags?.split('|');
|
||||
}, [filter?._custom?.jellyfin?.Tags]);
|
||||
|
||||
const yesNoFilter = [
|
||||
{
|
||||
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||
onChange: (favorite?: boolean) => {
|
||||
const updatedFilters = setFilter({
|
||||
customFilters,
|
||||
data: {
|
||||
_custom: filter?._custom,
|
||||
favorite,
|
||||
},
|
||||
itemType: LibraryItem.ALBUM,
|
||||
key: pageKey,
|
||||
}) as AlbumListFilter;
|
||||
onFilterChange(updatedFilters);
|
||||
const yesNoFilter = useMemo(() => {
|
||||
const filters = [
|
||||
{
|
||||
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||
onChange: (favorite?: boolean) => {
|
||||
const updatedFilters = setFilter({
|
||||
customFilters,
|
||||
data: {
|
||||
_custom: filter?._custom,
|
||||
favorite,
|
||||
},
|
||||
itemType: LibraryItem.ALBUM,
|
||||
key: pageKey,
|
||||
}) as AlbumListFilter;
|
||||
onFilterChange(updatedFilters);
|
||||
},
|
||||
value: filter?.favorite,
|
||||
},
|
||||
value: filter?.favorite,
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
if (customFilters?.artistIds) {
|
||||
filters.push({
|
||||
label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
|
||||
onChange: (compilation?: boolean) => {
|
||||
const updatedFilters = setFilter({
|
||||
customFilters,
|
||||
data: {
|
||||
_custom: filter._custom,
|
||||
compilation,
|
||||
},
|
||||
itemType: LibraryItem.ALBUM,
|
||||
key: pageKey,
|
||||
}) as AlbumListFilter;
|
||||
onFilterChange(updatedFilters);
|
||||
},
|
||||
value: filter.compilation,
|
||||
});
|
||||
}
|
||||
return filters;
|
||||
}, [
|
||||
customFilters,
|
||||
filter._custom,
|
||||
filter.compilation,
|
||||
filter?.favorite,
|
||||
onFilterChange,
|
||||
pageKey,
|
||||
setFilter,
|
||||
t,
|
||||
]);
|
||||
|
||||
const handleMinYearFilter = debounce((e: number | string) => {
|
||||
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
|
||||
|
|
@ -132,8 +171,6 @@ export const JellyfinAlbumFilters = ({
|
|||
onFilterChange(updatedFilters);
|
||||
}, 250);
|
||||
|
||||
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
|
||||
|
||||
const albumArtistListQuery = useAlbumArtistList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60 * 2,
|
||||
|
|
@ -161,7 +198,7 @@ export const JellyfinAlbumFilters = ({
|
|||
customFilters,
|
||||
data: {
|
||||
_custom: filter?._custom,
|
||||
artistIds: e || undefined,
|
||||
artistIds: e?.length ? e : undefined,
|
||||
},
|
||||
itemType: LibraryItem.ALBUM,
|
||||
key: pageKey,
|
||||
|
|
@ -238,16 +275,14 @@ export const JellyfinAlbumFilters = ({
|
|||
<MultiSelectWithInvalidData
|
||||
clearable
|
||||
data={selectableAlbumArtists}
|
||||
defaultValue={filter?._custom?.jellyfin?.AlbumArtistIds?.split(',')}
|
||||
defaultValue={filter?.artistIds}
|
||||
disabled={disableArtistFilter}
|
||||
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
|
||||
limit={300}
|
||||
onChange={handleAlbumArtistFilter}
|
||||
onSearchChange={setAlbumArtistSearchTerm}
|
||||
placeholder="Type to search for an artist"
|
||||
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
|
||||
searchable
|
||||
searchValue={albumArtistSearchTerm}
|
||||
/>
|
||||
</Group>
|
||||
{tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import { ChangeEvent, useMemo, useState } from 'react';
|
||||
import { ChangeEvent, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||
|
|
@ -43,6 +43,10 @@ export const NavidromeAlbumFilters = ({
|
|||
const { setFilter } = useListStoreActions();
|
||||
|
||||
const genreListQuery = useGenreList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
},
|
||||
query: {
|
||||
sortBy: GenreListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
|
|
@ -73,6 +77,10 @@ export const NavidromeAlbumFilters = ({
|
|||
}, 250);
|
||||
|
||||
const tagsQuery = useTagList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
},
|
||||
query: {
|
||||
type: LibraryItem.ALBUM,
|
||||
},
|
||||
|
|
@ -177,8 +185,6 @@ export const NavidromeAlbumFilters = ({
|
|||
onFilterChange(updatedFilters);
|
||||
}, 500);
|
||||
|
||||
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
|
||||
|
||||
const albumArtistListQuery = useAlbumArtistList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60 * 2,
|
||||
|
|
@ -293,13 +299,12 @@ export const NavidromeAlbumFilters = ({
|
|||
label={t('entity.artist', { count: 1, postProcess: 'titleCase' })}
|
||||
limit={300}
|
||||
onChange={handleAlbumArtistFilter}
|
||||
onSearchChange={setAlbumArtistSearchTerm}
|
||||
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
|
||||
searchable
|
||||
searchValue={albumArtistSearchTerm}
|
||||
/>
|
||||
</Group>
|
||||
{tagsQuery.data?.enumTags?.length &&
|
||||
tagsQuery.data.enumTags.length > 0 &&
|
||||
tagsQuery.data.enumTags.map((tag) => (
|
||||
<Group
|
||||
grow
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import { ChangeEvent, useMemo } from 'react';
|
||||
import { ChangeEvent, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { MultiSelectWithInvalidData } 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 { AlbumListFilter, useListStoreActions, useListStoreByKey } 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';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import {
|
||||
AlbumArtistListSort,
|
||||
AlbumListQuery,
|
||||
GenreListSort,
|
||||
LibraryItem,
|
||||
|
|
@ -19,12 +23,14 @@ import {
|
|||
} from '/@/shared/types/domain-types';
|
||||
|
||||
interface SubsonicAlbumFiltersProps {
|
||||
disableArtistFilter?: boolean;
|
||||
onFilterChange: (filters: AlbumListFilter) => void;
|
||||
pageKey: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const SubsonicAlbumFilters = ({
|
||||
disableArtistFilter,
|
||||
onFilterChange,
|
||||
pageKey,
|
||||
serverId,
|
||||
|
|
@ -32,8 +38,46 @@ export const SubsonicAlbumFilters = ({
|
|||
const { t } = useTranslation();
|
||||
const { filter } = useListStoreByKey<AlbumListQuery>({ key: pageKey });
|
||||
const { setFilter } = useListStoreActions();
|
||||
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
|
||||
|
||||
const albumArtistListQuery = useAlbumArtistList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
},
|
||||
query: {
|
||||
sortBy: AlbumArtistListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
},
|
||||
serverId,
|
||||
});
|
||||
|
||||
const selectableAlbumArtists = useMemo(() => {
|
||||
if (!albumArtistListQuery?.data?.items) return [];
|
||||
|
||||
return albumArtistListQuery?.data?.items?.map((artist) => ({
|
||||
label: artist.name,
|
||||
value: artist.id,
|
||||
}));
|
||||
}, [albumArtistListQuery?.data?.items]);
|
||||
|
||||
const handleAlbumArtistFilter = (e: null | string[]) => {
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
artistIds: e?.length ? e : undefined,
|
||||
},
|
||||
itemType: LibraryItem.ALBUM,
|
||||
key: pageKey,
|
||||
}) as AlbumListFilter;
|
||||
onFilterChange(updatedFilters);
|
||||
};
|
||||
|
||||
const genreListQuery = useGenreList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
},
|
||||
query: {
|
||||
sortBy: GenreListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
|
|
@ -147,6 +191,22 @@ export const SubsonicAlbumFilters = ({
|
|||
searchable
|
||||
/>
|
||||
</Group>
|
||||
<Group grow>
|
||||
<MultiSelectWithInvalidData
|
||||
clearable
|
||||
data={selectableAlbumArtists}
|
||||
defaultValue={filter?.artistIds}
|
||||
disabled={disableArtistFilter}
|
||||
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
|
||||
limit={300}
|
||||
onChange={handleAlbumArtistFilter}
|
||||
onSearchChange={setAlbumArtistSearchTerm}
|
||||
placeholder="Type to search for an artist"
|
||||
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
|
||||
searchable
|
||||
searchValue={albumArtistSearchTerm}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ export const NavidromeSongFilters = ({
|
|||
)}
|
||||
</Group>
|
||||
{tagsQuery.data?.enumTags?.length &&
|
||||
tagsQuery.data.enumTags.length > 0 &&
|
||||
tagsQuery.data.enumTags.map((tag) => (
|
||||
<Group
|
||||
grow
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue