mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
Support subsonic album filters
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
import filter from 'lodash/filter';
|
import filter from 'lodash/filter';
|
||||||
import orderBy from 'lodash/orderBy';
|
import orderBy from 'lodash/orderBy';
|
||||||
import md5 from 'md5';
|
import md5 from 'md5';
|
||||||
@@ -13,7 +14,7 @@ import {
|
|||||||
LibraryItem,
|
LibraryItem,
|
||||||
PlaylistListSort,
|
PlaylistListSort,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { sortAlbumArtistList, sortSongList } from '/@/renderer/api/utils';
|
import { sortAlbumArtistList, sortAlbumList, sortSongList } from '/@/renderer/api/utils';
|
||||||
import { randomString } from '/@/renderer/utils';
|
import { randomString } from '/@/renderer/utils';
|
||||||
|
|
||||||
const authenticate = async (
|
const authenticate = async (
|
||||||
@@ -292,6 +293,36 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
getAlbumList: async (args) => {
|
getAlbumList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
|
if (query.searchTerm) {
|
||||||
|
const res = await subsonicApiClient(apiClientProps).search3({
|
||||||
|
query: {
|
||||||
|
albumCount: query.limit,
|
||||||
|
albumOffset: query.startIndex,
|
||||||
|
artistCount: 0,
|
||||||
|
artistOffset: 0,
|
||||||
|
query: query.searchTerm || '""',
|
||||||
|
songCount: 0,
|
||||||
|
songOffset: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
fsLog.error('Failed to get album list');
|
||||||
|
throw new Error('Failed to get album list');
|
||||||
|
}
|
||||||
|
|
||||||
|
const results =
|
||||||
|
res.body['subsonic-response'].searchResult3.album?.map((album) =>
|
||||||
|
subsonicNormalize.album(album, apiClientProps.server),
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: results,
|
||||||
|
startIndex: query.startIndex,
|
||||||
|
totalRecordCount: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const sortType: Record<AlbumListSort, AlbumListSortType | undefined> = {
|
const sortType: Record<AlbumListSort, AlbumListSortType | undefined> = {
|
||||||
[AlbumListSort.RANDOM]: SubsonicApi.getAlbumList2.enum.AlbumListSortType.RANDOM,
|
[AlbumListSort.RANDOM]: SubsonicApi.getAlbumList2.enum.AlbumListSortType.RANDOM,
|
||||||
[AlbumListSort.ALBUM_ARTIST]:
|
[AlbumListSort.ALBUM_ARTIST]:
|
||||||
@@ -312,13 +343,9 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
[AlbumListSort.SONG_COUNT]: undefined,
|
[AlbumListSort.SONG_COUNT]: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (query.isCompilation) {
|
let type =
|
||||||
return {
|
sortType[query.sortBy] ??
|
||||||
items: [],
|
SubsonicApi.getAlbumList2.enum.AlbumListSortType.ALPHABETICAL_BY_NAME;
|
||||||
startIndex: 0,
|
|
||||||
totalRecordCount: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.artistIds) {
|
if (query.artistIds) {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
@@ -351,17 +378,63 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.isFavorite) {
|
||||||
|
const res = await subsonicApiClient(apiClientProps).getStarred({
|
||||||
|
query: {
|
||||||
|
musicFolderId: query.musicFolderId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
fsLog.error('Failed to get album list');
|
||||||
|
throw new Error('Failed to get album list');
|
||||||
|
}
|
||||||
|
|
||||||
|
const results =
|
||||||
|
res.body['subsonic-response'].starred.album?.map((album) =>
|
||||||
|
subsonicNormalize.album(album, apiClientProps.server),
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: sortAlbumList(results, query.sortBy, query.sortOrder),
|
||||||
|
startIndex: 0,
|
||||||
|
totalRecordCount: res.body['subsonic-response'].starred.album?.length || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.genre) {
|
||||||
|
type = SubsonicApi.getAlbumList2.enum.AlbumListSortType.BY_GENRE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.minYear || query.maxYear) {
|
||||||
|
type = SubsonicApi.getAlbumList2.enum.AlbumListSortType.BY_YEAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fromYear;
|
||||||
|
let toYear;
|
||||||
|
|
||||||
|
if (query.minYear) {
|
||||||
|
fromYear = query.minYear;
|
||||||
|
toYear = dayjs().year();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.maxYear) {
|
||||||
|
toYear = query.maxYear;
|
||||||
|
|
||||||
|
if (!query.minYear) {
|
||||||
|
fromYear = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const res = await subsonicApiClient(apiClientProps).getAlbumList2({
|
const res = await subsonicApiClient(apiClientProps).getAlbumList2({
|
||||||
query: {
|
query: {
|
||||||
fromYear: query.minYear,
|
fromYear,
|
||||||
genre: query.genre,
|
genre: query.genre,
|
||||||
musicFolderId: query.musicFolderId,
|
musicFolderId: query.musicFolderId,
|
||||||
offset: query.startIndex,
|
offset: query.startIndex,
|
||||||
size: query.limit,
|
size: query.limit,
|
||||||
toYear: query.maxYear,
|
toYear,
|
||||||
type:
|
type,
|
||||||
sortType[query.sortBy] ??
|
|
||||||
SubsonicApi.getAlbumList2.enum.AlbumListSortType.ALPHABETICAL_BY_NAME,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -371,9 +444,10 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body['subsonic-response'].albumList2.album.map((album) =>
|
items:
|
||||||
subsonicNormalize.album(album, apiClientProps.server),
|
res.body['subsonic-response'].albumList2.album?.map((album) =>
|
||||||
),
|
subsonicNormalize.album(album, apiClientProps.server, 300),
|
||||||
|
) || [],
|
||||||
startIndex: query.startIndex,
|
startIndex: query.startIndex,
|
||||||
totalRecordCount: null,
|
totalRecordCount: null,
|
||||||
};
|
};
|
||||||
@@ -381,6 +455,41 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
getAlbumListCount: async (args) => {
|
getAlbumListCount: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
|
if (query.searchTerm) {
|
||||||
|
let fetchNextPage = true;
|
||||||
|
let startIndex = 0;
|
||||||
|
let totalRecordCount = 0;
|
||||||
|
|
||||||
|
while (fetchNextPage) {
|
||||||
|
const res = await subsonicApiClient(apiClientProps).search3({
|
||||||
|
query: {
|
||||||
|
albumCount: 500,
|
||||||
|
albumOffset: startIndex,
|
||||||
|
artistCount: 0,
|
||||||
|
artistOffset: 0,
|
||||||
|
query: query.searchTerm || '""',
|
||||||
|
songCount: 0,
|
||||||
|
songOffset: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
fsLog.error('Failed to get album list count');
|
||||||
|
throw new Error('Failed to get album list count');
|
||||||
|
}
|
||||||
|
|
||||||
|
const albumCount = res.body['subsonic-response'].searchResult3.album?.length;
|
||||||
|
|
||||||
|
totalRecordCount += albumCount;
|
||||||
|
startIndex += albumCount;
|
||||||
|
|
||||||
|
// The max limit size for Subsonic is 500
|
||||||
|
fetchNextPage = albumCount === 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalRecordCount;
|
||||||
|
}
|
||||||
|
|
||||||
const sortType: Record<AlbumListSort, AlbumListSortType | undefined> = {
|
const sortType: Record<AlbumListSort, AlbumListSortType | undefined> = {
|
||||||
[AlbumListSort.RANDOM]: SubsonicApi.getAlbumList2.enum.AlbumListSortType.RANDOM,
|
[AlbumListSort.RANDOM]: SubsonicApi.getAlbumList2.enum.AlbumListSortType.RANDOM,
|
||||||
[AlbumListSort.ALBUM_ARTIST]:
|
[AlbumListSort.ALBUM_ARTIST]:
|
||||||
@@ -401,22 +510,63 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
[AlbumListSort.SONG_COUNT]: undefined,
|
[AlbumListSort.SONG_COUNT]: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (query.isFavorite) {
|
||||||
|
const res = await subsonicApiClient(apiClientProps).getStarred({
|
||||||
|
query: {
|
||||||
|
musicFolderId: query.musicFolderId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
fsLog.error('Failed to get album list');
|
||||||
|
throw new Error('Failed to get album list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.body['subsonic-response'].starred.album?.length || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let type =
|
||||||
|
sortType[query.sortBy] ??
|
||||||
|
SubsonicApi.getAlbumList2.enum.AlbumListSortType.ALPHABETICAL_BY_NAME;
|
||||||
|
|
||||||
let fetchNextPage = true;
|
let fetchNextPage = true;
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
let totalRecordCount = 0;
|
let totalRecordCount = 0;
|
||||||
|
|
||||||
|
if (query.genre) {
|
||||||
|
type = SubsonicApi.getAlbumList2.enum.AlbumListSortType.BY_GENRE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.minYear || query.maxYear) {
|
||||||
|
type = SubsonicApi.getAlbumList2.enum.AlbumListSortType.BY_YEAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fromYear;
|
||||||
|
let toYear;
|
||||||
|
|
||||||
|
if (query.minYear) {
|
||||||
|
fromYear = query.minYear;
|
||||||
|
toYear = dayjs().year();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.maxYear) {
|
||||||
|
toYear = query.maxYear;
|
||||||
|
|
||||||
|
if (!query.minYear) {
|
||||||
|
fromYear = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (fetchNextPage) {
|
while (fetchNextPage) {
|
||||||
const res = await subsonicApiClient(apiClientProps).getAlbumList2({
|
const res = await subsonicApiClient(apiClientProps).getAlbumList2({
|
||||||
query: {
|
query: {
|
||||||
fromYear: query.minYear,
|
fromYear,
|
||||||
genre: query.genre,
|
genre: query.genre,
|
||||||
musicFolderId: query.musicFolderId,
|
musicFolderId: query.musicFolderId,
|
||||||
offset: startIndex,
|
offset: startIndex,
|
||||||
size: 500,
|
size: 500,
|
||||||
toYear: query.maxYear,
|
toYear,
|
||||||
type:
|
type,
|
||||||
sortType[query.sortBy] ??
|
|
||||||
SubsonicApi.getAlbumList2.enum.AlbumListSortType.ALPHABETICAL_BY_NAME,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -153,13 +153,14 @@ const normalizeAlbum = (
|
|||||||
| z.infer<typeof SubsonicApi._baseTypes.album>
|
| z.infer<typeof SubsonicApi._baseTypes.album>
|
||||||
| z.infer<typeof SubsonicApi._baseTypes.albumListEntry>,
|
| z.infer<typeof SubsonicApi._baseTypes.albumListEntry>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
|
size?: number,
|
||||||
): Album => {
|
): Album => {
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
getCoverArtUrl({
|
getCoverArtUrl({
|
||||||
baseUrl: server?.url,
|
baseUrl: server?.url,
|
||||||
coverArtId: item.coverArt,
|
coverArtId: item.coverArt,
|
||||||
credential: server?.credential,
|
credential: server?.credential,
|
||||||
size: 300,
|
size: size || 300,
|
||||||
}) || null;
|
}) || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -378,6 +378,7 @@ export type AlbumListQuery = {
|
|||||||
artistIds?: string[];
|
artistIds?: string[];
|
||||||
genre?: string;
|
genre?: string;
|
||||||
isCompilation?: boolean;
|
isCompilation?: boolean;
|
||||||
|
isFavorite?: boolean;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
maxYear?: number;
|
maxYear?: number;
|
||||||
minYear?: number;
|
minYear?: number;
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { toast } from '/@/renderer/components';
|
|||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
import { ServerListItem } from '/@/renderer/types';
|
import { ServerListItem } from '/@/renderer/types';
|
||||||
import {
|
import {
|
||||||
|
Album,
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
AlbumArtistListSort,
|
AlbumArtistListSort,
|
||||||
|
AlbumListSort,
|
||||||
QueueSong,
|
QueueSong,
|
||||||
SongListSort,
|
SongListSort,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
@@ -49,6 +51,56 @@ export const authenticationFailure = (currentServer: ServerListItem | null) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder: SortOrder) => {
|
||||||
|
let results = albums;
|
||||||
|
|
||||||
|
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
|
||||||
|
|
||||||
|
switch (sortBy) {
|
||||||
|
case AlbumListSort.ALBUM_ARTIST:
|
||||||
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['albumArtist', (v) => v.name.toLowerCase()],
|
||||||
|
[order, 'asc'],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case AlbumListSort.DURATION:
|
||||||
|
results = orderBy(results, ['duration'], [order]);
|
||||||
|
break;
|
||||||
|
case AlbumListSort.FAVORITED:
|
||||||
|
results = orderBy(results, ['starred'], [order]);
|
||||||
|
break;
|
||||||
|
case AlbumListSort.NAME:
|
||||||
|
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
|
||||||
|
break;
|
||||||
|
case AlbumListSort.PLAY_COUNT:
|
||||||
|
results = orderBy(results, ['playCount'], [order]);
|
||||||
|
break;
|
||||||
|
case AlbumListSort.RANDOM:
|
||||||
|
results = shuffle(results);
|
||||||
|
break;
|
||||||
|
case AlbumListSort.RECENTLY_ADDED:
|
||||||
|
results = orderBy(results, ['createdAt'], [order]);
|
||||||
|
break;
|
||||||
|
case AlbumListSort.RECENTLY_PLAYED:
|
||||||
|
results = orderBy(results, ['lastPlayedAt'], [order]);
|
||||||
|
break;
|
||||||
|
case AlbumListSort.RATING:
|
||||||
|
results = orderBy(results, ['userRating'], [order]);
|
||||||
|
break;
|
||||||
|
case AlbumListSort.YEAR:
|
||||||
|
results = orderBy(results, ['releaseYear'], [order]);
|
||||||
|
break;
|
||||||
|
case AlbumListSort.SONG_COUNT:
|
||||||
|
results = orderBy(results, ['songCount'], [order]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
export const sortSongList = (songs: QueueSong[], sortBy: SongListSort, sortOrder: SortOrder) => {
|
export const sortSongList = (songs: QueueSong[], sortBy: SongListSort, sortOrder: SortOrder) => {
|
||||||
let results = songs;
|
let results = songs;
|
||||||
|
|
||||||
|
|||||||
@@ -183,15 +183,14 @@ export const useVirtualTable = <TFilter>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (results.totalRecordCount === null) {
|
if (results.totalRecordCount === null) {
|
||||||
const totalRecordCount: number | undefined = itemCount;
|
|
||||||
const hasMoreRows = results?.items?.length === properties.filter.limit;
|
const hasMoreRows = results?.items?.length === properties.filter.limit;
|
||||||
const lastRowIndex = hasMoreRows
|
const lastRowIndex = hasMoreRows
|
||||||
? undefined
|
? undefined
|
||||||
: properties.filter.offset + results.items.length;
|
: (properties.filter.offset || 0) + results.items.length;
|
||||||
|
|
||||||
params.successCallback(
|
params.successCallback(
|
||||||
results?.items || [],
|
results?.items || [],
|
||||||
totalRecordCount || lastRowIndex,
|
hasMoreRows ? undefined : lastRowIndex,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -212,7 +211,6 @@ export const useVirtualTable = <TFilter>({
|
|||||||
queryClient,
|
queryClient,
|
||||||
isClientSideSort,
|
isClientSideSort,
|
||||||
queryFn,
|
queryFn,
|
||||||
itemCount,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
|||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters';
|
import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters';
|
||||||
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
|
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
|
||||||
|
import { SubsonicAlbumFilters } from '/@/renderer/features/albums/components/subsonic-album-filters';
|
||||||
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared';
|
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh';
|
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh';
|
||||||
@@ -233,27 +234,35 @@ export const AlbumListHeaderFilters = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenFiltersModal = () => {
|
const handleOpenFiltersModal = () => {
|
||||||
|
let FilterComponent;
|
||||||
|
|
||||||
|
switch (server?.type) {
|
||||||
|
case ServerType.NAVIDROME:
|
||||||
|
FilterComponent = NavidromeAlbumFilters;
|
||||||
|
break;
|
||||||
|
case ServerType.JELLYFIN:
|
||||||
|
FilterComponent = JellyfinAlbumFilters;
|
||||||
|
break;
|
||||||
|
case ServerType.SUBSONIC:
|
||||||
|
FilterComponent = SubsonicAlbumFilters;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FilterComponent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
children: (
|
children: (
|
||||||
<>
|
<FilterComponent
|
||||||
{server?.type === ServerType.NAVIDROME ? (
|
customFilters={customFilters}
|
||||||
<NavidromeAlbumFilters
|
disableArtistFilter={!!customFilters}
|
||||||
customFilters={customFilters}
|
pageKey={pageKey}
|
||||||
disableArtistFilter={!!customFilters}
|
serverId={server?.id}
|
||||||
pageKey={pageKey}
|
onFilterChange={onFilterChange}
|
||||||
serverId={server?.id}
|
/>
|
||||||
onFilterChange={onFilterChange}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<JellyfinAlbumFilters
|
|
||||||
customFilters={customFilters}
|
|
||||||
disableArtistFilter={!!customFilters}
|
|
||||||
pageKey={pageKey}
|
|
||||||
serverId={server?.id}
|
|
||||||
onFilterChange={onFilterChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
),
|
),
|
||||||
title: 'Album Filters',
|
title: 'Album Filters',
|
||||||
});
|
});
|
||||||
@@ -389,8 +398,20 @@ export const AlbumListHeaderFilters = ({
|
|||||||
filter?._custom?.jellyfin &&
|
filter?._custom?.jellyfin &&
|
||||||
Object.values(filter?._custom?.jellyfin).some((value) => value !== undefined);
|
Object.values(filter?._custom?.jellyfin).some((value) => value !== undefined);
|
||||||
|
|
||||||
return isNavidromeFilterApplied || isJellyfinFilterApplied;
|
const isSubsonicFilterApplied =
|
||||||
}, [filter?._custom?.jellyfin, filter?._custom?.navidrome, server?.type]);
|
server?.type === ServerType.SUBSONIC &&
|
||||||
|
(filter.maxYear || filter.minYear || filter.genre || filter.isFavorite);
|
||||||
|
|
||||||
|
return isNavidromeFilterApplied || isJellyfinFilterApplied || isSubsonicFilterApplied;
|
||||||
|
}, [
|
||||||
|
filter?._custom?.jellyfin,
|
||||||
|
filter?._custom?.navidrome,
|
||||||
|
filter.genre,
|
||||||
|
filter.isFavorite,
|
||||||
|
filter.maxYear,
|
||||||
|
filter.minYear,
|
||||||
|
server?.type,
|
||||||
|
]);
|
||||||
|
|
||||||
const isFolderFilterApplied = useMemo(() => {
|
const isFolderFilterApplied = useMemo(() => {
|
||||||
return filter.musicFolderId !== undefined;
|
return filter.musicFolderId !== undefined;
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { Divider, Group, Stack } from '@mantine/core';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { ChangeEvent, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
||||||
|
import { NumberInput, Select, Switch, Text } from '/@/renderer/components';
|
||||||
|
import { useGenreList } from '/@/renderer/features/genres';
|
||||||
|
import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
|
||||||
|
|
||||||
|
interface SubsonicAlbumFiltersProps {
|
||||||
|
onFilterChange: (filters: AlbumListFilter) => void;
|
||||||
|
pageKey: string;
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubsonicAlbumFilters = ({
|
||||||
|
onFilterChange,
|
||||||
|
pageKey,
|
||||||
|
serverId,
|
||||||
|
}: SubsonicAlbumFiltersProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { filter } = useListStoreByKey({ key: pageKey });
|
||||||
|
const { setFilter } = useListStoreActions();
|
||||||
|
|
||||||
|
const genreListQuery = useGenreList({
|
||||||
|
query: {
|
||||||
|
sortBy: GenreListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const genreList = useMemo(() => {
|
||||||
|
if (!genreListQuery?.data) return [];
|
||||||
|
return genreListQuery.data.items.map((genre) => ({
|
||||||
|
label: genre.name,
|
||||||
|
value: genre.id,
|
||||||
|
}));
|
||||||
|
}, [genreListQuery.data]);
|
||||||
|
|
||||||
|
const handleGenresFilter = debounce((e: string | null) => {
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
data: {
|
||||||
|
genre: e || undefined,
|
||||||
|
},
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
key: pageKey,
|
||||||
|
}) as AlbumListFilter;
|
||||||
|
|
||||||
|
onFilterChange(updatedFilters);
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
const toggleFilters = [
|
||||||
|
{
|
||||||
|
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||||
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
data: {
|
||||||
|
isFavorite: e.target.checked ? true : undefined,
|
||||||
|
},
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
key: pageKey,
|
||||||
|
}) as AlbumListFilter;
|
||||||
|
onFilterChange(updatedFilters);
|
||||||
|
},
|
||||||
|
value: filter.isFavorite,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleYearFilter = debounce((e: number | string, type: 'min' | 'max') => {
|
||||||
|
let data = {};
|
||||||
|
|
||||||
|
if (type === 'min') {
|
||||||
|
data = {
|
||||||
|
minYear: e || undefined,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
data = {
|
||||||
|
maxYear: e || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('data', data);
|
||||||
|
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
data,
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
key: pageKey,
|
||||||
|
}) as AlbumListFilter;
|
||||||
|
|
||||||
|
onFilterChange(updatedFilters);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack p="0.8rem">
|
||||||
|
{toggleFilters.map((filter) => (
|
||||||
|
<Group
|
||||||
|
key={`nd-filter-${filter.label}`}
|
||||||
|
position="apart"
|
||||||
|
>
|
||||||
|
<Text>{filter.label}</Text>
|
||||||
|
<Switch
|
||||||
|
checked={filter?.value || false}
|
||||||
|
onChange={filter.onChange}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
<Divider my="0.5rem" />
|
||||||
|
<Group grow>
|
||||||
|
<NumberInput
|
||||||
|
defaultValue={filter.minYear}
|
||||||
|
disabled={filter.genre}
|
||||||
|
hideControls={false}
|
||||||
|
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
||||||
|
max={5000}
|
||||||
|
min={0}
|
||||||
|
onChange={(e) => handleYearFilter(e, 'min')}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
defaultValue={filter.maxYear}
|
||||||
|
disabled={filter.genre}
|
||||||
|
hideControls={false}
|
||||||
|
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
||||||
|
max={5000}
|
||||||
|
min={0}
|
||||||
|
onChange={(e) => handleYearFilter(e, 'max')}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group grow>
|
||||||
|
<Select
|
||||||
|
clearable
|
||||||
|
searchable
|
||||||
|
data={genreList}
|
||||||
|
defaultValue={filter.genre}
|
||||||
|
disabled={filter.minYear || filter.maxYear}
|
||||||
|
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
||||||
|
onChange={handleGenresFilter}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -12,7 +12,10 @@ export const getAlbumListCountQuery = (query: AlbumListQuery) => {
|
|||||||
if (query.maxYear) filter.maxYear = query.maxYear;
|
if (query.maxYear) filter.maxYear = query.maxYear;
|
||||||
if (query.minYear) filter.minYear = query.minYear;
|
if (query.minYear) filter.minYear = query.minYear;
|
||||||
if (query.searchTerm) filter.searchTerm = query.searchTerm;
|
if (query.searchTerm) filter.searchTerm = query.searchTerm;
|
||||||
|
if (query.genre) filter.genre = query.genre;
|
||||||
if (query.musicFolderId) filter.musicFolderId = query.musicFolderId;
|
if (query.musicFolderId) filter.musicFolderId = query.musicFolderId;
|
||||||
|
if (query.isCompilation) filter.isCompilation = query.isCompilation;
|
||||||
|
if (query.isFavorite) filter.isCompilation = query.isFavorite;
|
||||||
|
|
||||||
if (Object.keys(filter).length === 0) return undefined;
|
if (Object.keys(filter).length === 0) return undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const useListFilterRefresh = ({
|
|||||||
|
|
||||||
const queryKey = queryKeyFn(server?.id || '', query);
|
const queryKey = queryKeyFn(server?.id || '', query);
|
||||||
|
|
||||||
const res = await queryClient.fetchQuery({
|
const results = (await queryClient.fetchQuery({
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
return queryFn({
|
return queryFn({
|
||||||
apiClientProps: {
|
apiClientProps: {
|
||||||
@@ -91,23 +91,39 @@ export const useListFilterRefresh = ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
queryKey,
|
queryKey,
|
||||||
});
|
})) as BasePaginatedResponse<any>;
|
||||||
|
|
||||||
if (isClientSideSort && res?.items) {
|
if (isClientSideSort && results?.items) {
|
||||||
const sortedResults = orderBy(
|
const sortedResults = orderBy(
|
||||||
res.items,
|
results.items,
|
||||||
[(item) => String(item[filter.sortBy]).toLowerCase()],
|
[(item) => String(item[filter.sortBy]).toLowerCase()],
|
||||||
filter.sortOrder === 'DESC' ? ['desc'] : ['asc'],
|
filter.sortOrder === 'DESC' ? ['desc'] : ['asc'],
|
||||||
);
|
);
|
||||||
|
|
||||||
params.successCallback(
|
params.successCallback(
|
||||||
sortedResults || [],
|
sortedResults || [],
|
||||||
res?.totalRecordCount || itemCount,
|
results?.totalRecordCount || itemCount,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
params.successCallback(res?.items || [], res?.totalRecordCount || itemCount);
|
if (results.totalRecordCount === null) {
|
||||||
|
const hasMoreRows = results?.items?.length === filter.limit;
|
||||||
|
const lastRowIndex = hasMoreRows
|
||||||
|
? undefined
|
||||||
|
: (filter.offset || 0) + results.items.length;
|
||||||
|
|
||||||
|
params.successCallback(
|
||||||
|
results?.items || [],
|
||||||
|
hasMoreRows ? undefined : lastRowIndex,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
params.successCallback(
|
||||||
|
results?.items || [],
|
||||||
|
results?.totalRecordCount || itemCount,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
rowCount: undefined,
|
rowCount: undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user