Skip to content

Commit

Permalink
Support subsonic song filters
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffvli committed Dec 19, 2023
1 parent f7fcf6c commit d6cc6a4
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 37 deletions.
121 changes: 112 additions & 9 deletions src/renderer/api/subsonic/subsonic-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export const SubsonicController: ControllerEndpoint = {
);

let results = artists.map((artist) =>
subsonicNormalize.albumArtist(artist, apiClientProps.server),
subsonicNormalize.albumArtist(artist, apiClientProps.server, 300),
);

if (query.searchTerm) {
Expand Down Expand Up @@ -880,11 +880,39 @@ export const SubsonicController: ControllerEndpoint = {
const artistDetailPromises = [];
let results: any[] = [];

if (query.genreId) {
if (query.searchTerm) {
const res = await subsonicApiClient(apiClientProps).search3({
query: {
albumCount: 0,
albumOffset: 0,
artistCount: 0,
artistOffset: 0,
query: query.searchTerm || '""',
songCount: query.limit,
songOffset: query.startIndex,
},
});

if (res.status !== 200) {
fsLog.error('Failed to get song list');
throw new Error('Failed to get song list');
}

return {
items:
res.body['subsonic-response'].searchResult3?.song?.map((song) =>
subsonicNormalize.song(song, apiClientProps.server, ''),
) || [],
startIndex: query.startIndex,
totalRecordCount: null,
};
}

if (query.genre) {
const res = await subsonicApiClient(apiClientProps).getSongsByGenre({
query: {
count: query.limit,
genre: query.genreId,
genre: query.genre,
musicFolderId: query.musicFolderId,
offset: query.startIndex,
},
Expand All @@ -896,14 +924,39 @@ export const SubsonicController: ControllerEndpoint = {
}

return {
items: res.body['subsonic-response'].songsByGenre.song.map((song) =>
subsonicNormalize.song(song, apiClientProps.server, ''),
),
items:
res.body['subsonic-response'].songsByGenre.song?.map((song) =>
subsonicNormalize.song(song, apiClientProps.server, ''),
) || [],
startIndex: 0,
totalRecordCount: null,
};
}

if (query.isFavorite) {
const res = await subsonicApiClient(apiClientProps).getStarred({
query: {
musicFolderId: query.musicFolderId,
},
});

if (res.status !== 200) {
fsLog.error('Failed to get song list');
throw new Error('Failed to get song list');
}

const results =
res.body['subsonic-response'].starred.song?.map((song) =>
subsonicNormalize.song(song, apiClientProps.server, ''),
) || [];

return {
items: sortSongList(results, query.sortBy, query.sortOrder),
startIndex: 0,
totalRecordCount: res.body['subsonic-response'].starred.song?.length || 0,
};
}

if (query.albumIds || query.artistIds) {
if (query.albumIds) {
for (const albumId of query.albumIds) {
Expand Down Expand Up @@ -1009,13 +1062,48 @@ export const SubsonicController: ControllerEndpoint = {
let fetchNextSection = true;
let sectionIndex = 0;

if (query.genreId) {
if (query.searchTerm) {
let fetchNextPage = true;
let startIndex = 0;
let totalRecordCount = 0;

while (fetchNextPage) {
const res = await subsonicApiClient(apiClientProps).search3({
query: {
albumCount: 0,
albumOffset: 0,
artistCount: 0,
artistOffset: 0,
query: query.searchTerm || '""',
songCount: 500,
songOffset: startIndex,
},
});

if (res.status !== 200) {
fsLog.error('Failed to get song list count');
throw new Error('Failed to get song list count');
}

const songCount = res.body['subsonic-response'].searchResult3.song?.length;

totalRecordCount += songCount;
startIndex += songCount;

// The max limit size for Subsonic is 500
fetchNextPage = songCount === 500;
}

return totalRecordCount;
}

if (query.genre) {
let totalRecordCount = 0;
while (fetchNextSection) {
const res = await subsonicApiClient(apiClientProps).getSongsByGenre({
query: {
count: 1,
genre: query.genreId,
genre: query.genre,
musicFolderId: query.musicFolderId,
offset: sectionIndex,
},
Expand All @@ -1042,7 +1130,7 @@ export const SubsonicController: ControllerEndpoint = {
const res = await subsonicApiClient(apiClientProps).getSongsByGenre({
query: {
count: 500,
genre: query.genreId,
genre: query.genre,
musicFolderId: query.musicFolderId,
offset: startIndex,
},
Expand All @@ -1065,6 +1153,21 @@ export const SubsonicController: ControllerEndpoint = {
return totalRecordCount;
}

if (query.isFavorite) {
const res = await subsonicApiClient(apiClientProps).getStarred({
query: {
musicFolderId: query.musicFolderId,
},
});

if (res.status !== 200) {
fsLog.error('Failed to get song list');
throw new Error('Failed to get song list');
}

return res.body['subsonic-response'].starred.song?.length || 0;
}

let totalRecordCount = 0;

while (fetchNextSection) {
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/api/subsonic/subsonic-normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ const normalizeSong = (
item: z.infer<typeof SubsonicApi._baseTypes.song>,
server: ServerListItem | null,
deviceId: string,
size?: number,
): QueueSong => {
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
credential: server?.credential,
size: 100,
size: size || 300,
}) || null;

const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=feishin_${deviceId}&${server?.credential}`;
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,8 +492,10 @@ export type SongListQuery = {
};
albumIds?: string[];
artistIds?: string[];
genre?: string;
genreId?: string;
imageSize?: number;
isFavorite?: boolean;
limit?: number;
maxYear?: number;
minYear?: number;
Expand Down
10 changes: 8 additions & 2 deletions src/renderer/components/virtual-table/hooks/use-virtual-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface UseAgGridProps<TFilter> {
columnType?: 'albumDetail' | 'generic';
contextMenu: SetContextMenuItems;
customFilters?: Partial<TFilter>;
isClientSide?: boolean;
isClientSideSort?: boolean;
isSearchParams?: boolean;
itemCount?: number;
Expand All @@ -43,6 +44,8 @@ interface UseAgGridProps<TFilter> {
tableRef: MutableRefObject<AgGridReactType | null>;
}

const BLOCK_SIZE = 500;

export const useVirtualTable = <TFilter>({
server,
tableRef,
Expand All @@ -52,6 +55,7 @@ export const useVirtualTable = <TFilter>({
itemCount,
customFilters,
isSearchParams,
isClientSide,
isClientSideSort,
columnType,
}: UseAgGridProps<TFilter>) => {
Expand Down Expand Up @@ -183,7 +187,7 @@ export const useVirtualTable = <TFilter>({
}

if (results.totalRecordCount === null) {
const hasMoreRows = results?.items?.length === properties.filter.limit;
const hasMoreRows = results?.items?.length === BLOCK_SIZE;
const lastRowIndex = hasMoreRows
? undefined
: (properties.filter.offset || 0) + results.items.length;
Expand Down Expand Up @@ -334,6 +338,7 @@ export const useVirtualTable = <TFilter>({
alwaysShowHorizontalScroll: true,
autoFitColumns: properties.table.autoFit,
blockLoadDebounceMillis: 200,
cacheBlockSize: 500,
getRowId: (data: GetRowIdParams<any>) => data.data.id,
infiniteInitialRowCount: itemCount || 100,
pagination: isPaginationEnabled,
Expand All @@ -348,10 +353,11 @@ export const useVirtualTable = <TFilter>({
: undefined,
rowBuffer: 20,
rowHeight: properties.table.rowHeight || 40,
rowModelType: 'infinite' as RowModelType,
rowModelType: isClientSide ? 'clientSide' : ('infinite' as RowModelType),
suppressRowDrag: true,
};
}, [
isClientSide,
isPaginationEnabled,
isSearchParams,
itemCount,
Expand Down
71 changes: 47 additions & 24 deletions src/renderer/features/songs/components/song-list-header-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { queryClient } from '/@/renderer/lib/react-query';
import { SongListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store';
import { ListDisplayType, Play, ServerType, TableColumn } from '/@/renderer/types';
import i18n from '/@/i18n/i18n';
import { SubsonicSongFilters } from '/@/renderer/features/songs/components/subsonic-song-filters';

const FILTERS = {
jellyfin: [
Expand Down Expand Up @@ -400,25 +401,34 @@ export const SongListHeaderFilters = ({
};

const handleOpenFiltersModal = () => {
let FilterComponent;

switch (server?.type) {
case ServerType.NAVIDROME:
FilterComponent = NavidromeSongFilters;
break;
case ServerType.JELLYFIN:
FilterComponent = JellyfinSongFilters;
break;
case ServerType.SUBSONIC:
FilterComponent = SubsonicSongFilters;
break;
default:
break;
}

if (!FilterComponent) {
return;
}

openModal({
children: (
<>
{server?.type === ServerType.NAVIDROME ? (
<NavidromeSongFilters
customFilters={customFilters}
pageKey={pageKey}
serverId={server?.id}
onFilterChange={onFilterChange}
/>
) : (
<JellyfinSongFilters
customFilters={customFilters}
pageKey={pageKey}
serverId={server?.id}
onFilterChange={onFilterChange}
/>
)}
</>
<FilterComponent
customFilters={customFilters}
pageKey={pageKey}
serverId={server?.id}
onFilterChange={onFilterChange}
/>
),
title: 'Song Filters',
});
Expand All @@ -437,8 +447,17 @@ export const SongListHeaderFilters = ({
.filter((value) => value !== 'Audio') // Don't account for includeItemTypes: Audio
.some((value) => value !== undefined);

return isNavidromeFilterApplied || isJellyfinFilterApplied;
}, [filter?._custom?.jellyfin, filter?._custom?.navidrome, server?.type]);
const isSubsonicFilterApplied =
server?.type === ServerType.SUBSONIC && (filter?.isFavorite || filter?.genre);

return isNavidromeFilterApplied || isJellyfinFilterApplied || isSubsonicFilterApplied;
}, [
filter._custom?.jellyfin,
filter._custom?.navidrome,
filter?.genre,
filter?.isFavorite,
server?.type,
]);

const isFolderFilterApplied = useMemo(() => {
return filter.musicFolderId !== undefined;
Expand Down Expand Up @@ -475,11 +494,15 @@ export const SongListHeaderFilters = ({
))}
</DropdownMenu.Dropdown>
</DropdownMenu>
<Divider orientation="vertical" />
<OrderToggleButton
sortOrder={filter.sortOrder}
onToggle={handleToggleSortOrder}
/>
{server?.type !== ServerType.SUBSONIC && (
<>
<Divider orientation="vertical" />
<OrderToggleButton
sortOrder={filter.sortOrder}
onToggle={handleToggleSortOrder}
/>
</>
)}
{server?.type === ServerType.JELLYFIN && (
<>
<Divider orientation="vertical" />
Expand Down
Loading

0 comments on commit d6cc6a4

Please sign in to comment.