mirror of
https://github.com/jeffvli/feishin.git
synced 2024-11-20 14:37:06 +01:00
Support entity list pages for subsonic
This commit is contained in:
parent
2ecafea759
commit
b2f14d7369
@ -131,6 +131,15 @@ const getAlbumList = async (args: AlbumListArgs) => {
|
||||
)?.(args);
|
||||
};
|
||||
|
||||
const getAlbumListCount = async (args: AlbumListArgs) => {
|
||||
return (
|
||||
apiController(
|
||||
'getAlbumListCount',
|
||||
args.apiClientProps.server?.type,
|
||||
) as ControllerEndpoint['getAlbumListCount']
|
||||
)?.(args);
|
||||
};
|
||||
|
||||
const getAlbumDetail = async (args: AlbumDetailArgs) => {
|
||||
return (
|
||||
apiController(
|
||||
@ -149,6 +158,15 @@ const getSongList = async (args: SongListArgs) => {
|
||||
)?.(args);
|
||||
};
|
||||
|
||||
const getSongListCount = async (args: SongListArgs) => {
|
||||
return (
|
||||
apiController(
|
||||
'getSongListCount',
|
||||
args.apiClientProps.server?.type,
|
||||
) as ControllerEndpoint['getSongListCount']
|
||||
)?.(args);
|
||||
};
|
||||
|
||||
const getSongDetail = async (args: SongDetailArgs) => {
|
||||
return (
|
||||
apiController(
|
||||
@ -194,6 +212,15 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs) => {
|
||||
)?.(args);
|
||||
};
|
||||
|
||||
const getAlbumArtistListCount = async (args: AlbumArtistListArgs) => {
|
||||
return (
|
||||
apiController(
|
||||
'getAlbumArtistListCount',
|
||||
args.apiClientProps.server?.type,
|
||||
) as ControllerEndpoint['getAlbumArtistListCount']
|
||||
)?.(args);
|
||||
};
|
||||
|
||||
const getArtistList = async (args: ArtistListArgs) => {
|
||||
return (
|
||||
apiController(
|
||||
@ -212,6 +239,15 @@ const getPlaylistList = async (args: PlaylistListArgs) => {
|
||||
)?.(args);
|
||||
};
|
||||
|
||||
const getPlaylistListCount = async (args: PlaylistListArgs) => {
|
||||
return (
|
||||
apiController(
|
||||
'getPlaylistListCount',
|
||||
args.apiClientProps.server?.type,
|
||||
) as ControllerEndpoint['getPlaylistListCount']
|
||||
)?.(args);
|
||||
};
|
||||
|
||||
const createPlaylist = async (args: CreatePlaylistArgs) => {
|
||||
return (
|
||||
apiController(
|
||||
@ -362,18 +398,22 @@ export const controller = {
|
||||
deletePlaylist,
|
||||
getAlbumArtistDetail,
|
||||
getAlbumArtistList,
|
||||
getAlbumArtistListCount,
|
||||
getAlbumDetail,
|
||||
getAlbumList,
|
||||
getAlbumListCount,
|
||||
getArtistList,
|
||||
getGenreList,
|
||||
getLyrics,
|
||||
getMusicFolderList,
|
||||
getPlaylistDetail,
|
||||
getPlaylistList,
|
||||
getPlaylistListCount,
|
||||
getPlaylistSongList,
|
||||
getRandomSongList,
|
||||
getSongDetail,
|
||||
getSongList,
|
||||
getSongListCount,
|
||||
getTopSongList,
|
||||
getUserList,
|
||||
removeFromPlaylist,
|
||||
|
@ -480,7 +480,6 @@ const removeFromPlaylist = async (
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await jfApiClient(apiClientProps).removeFromPlaylist({
|
||||
body: null,
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
@ -648,7 +647,6 @@ const deletePlaylist = async (args: DeletePlaylistArgs): Promise<null> => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await jfApiClient(apiClientProps).deletePlaylist({
|
||||
body: null,
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
|
@ -11,8 +11,8 @@ import {
|
||||
import { ServerListItem, ServerType } from '/@/renderer/types';
|
||||
import z from 'zod';
|
||||
import { ndType } from './navidrome-types';
|
||||
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
|
||||
import { NDGenre } from '/@/renderer/api/navidrome.types';
|
||||
import { SubsonicApi } from '/@/renderer/api/subsonic/subsonic-types';
|
||||
|
||||
const getImageUrl = (args: { url: string | null }) => {
|
||||
const { url } = args;
|
||||
@ -186,7 +186,9 @@ const normalizeAlbum = (
|
||||
|
||||
const normalizeAlbumArtist = (
|
||||
item: z.infer<typeof ndType._response.albumArtist> & {
|
||||
similarArtists?: z.infer<typeof ssType._response.artistInfo>['artistInfo']['similarArtist'];
|
||||
similarArtists?: z.infer<
|
||||
typeof SubsonicApi.getArtistInfo2.response
|
||||
>['subsonic-response']['artistInfo2']['similarArtist'];
|
||||
},
|
||||
server: ServerListItem | null,
|
||||
): AlbumArtist => {
|
||||
|
@ -49,6 +49,19 @@ export const queryKeys: Record<
|
||||
Record<string, (...props: any) => QueryFunctionContext['queryKey']>
|
||||
> = {
|
||||
albumArtists: {
|
||||
count: (serverId: string, query?: AlbumArtistListQuery) => {
|
||||
const { pagination, filter } = splitPaginatedQuery(query);
|
||||
|
||||
if (query && pagination) {
|
||||
return [serverId, 'albumArtists', 'count', filter, pagination] as const;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
return [serverId, 'albumArtists', 'count', filter] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'albumArtists', 'count'] as const;
|
||||
},
|
||||
detail: (serverId: string, query?: AlbumArtistDetailQuery) => {
|
||||
if (query) return [serverId, 'albumArtists', 'detail', query] as const;
|
||||
return [serverId, 'albumArtists', 'detail'] as const;
|
||||
@ -72,23 +85,40 @@ export const queryKeys: Record<
|
||||
},
|
||||
},
|
||||
albums: {
|
||||
detail: (serverId: string, query?: AlbumDetailQuery) =>
|
||||
[serverId, 'albums', 'detail', query] as const,
|
||||
list: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
|
||||
count: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
|
||||
const { pagination, filter } = splitPaginatedQuery(query);
|
||||
|
||||
if (query && pagination && artistId) {
|
||||
return [serverId, 'albums', 'list', artistId, filter, pagination] as const;
|
||||
return [serverId, 'albums', 'count', artistId, filter, pagination] as const;
|
||||
}
|
||||
|
||||
if (query && pagination) {
|
||||
return [serverId, 'albums', 'list', filter, pagination] as const;
|
||||
return [serverId, 'albums', 'count', filter, pagination] as const;
|
||||
}
|
||||
|
||||
if (query && artistId) {
|
||||
return [serverId, 'albums', 'list', artistId, filter] as const;
|
||||
return [serverId, 'albums', 'count', artistId, filter] as const;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
return [serverId, 'albums', 'count', filter] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'albums', 'count'] as const;
|
||||
},
|
||||
detail: (serverId: string, query?: AlbumDetailQuery) =>
|
||||
[serverId, 'albums', 'detail', query] as const,
|
||||
list: (
|
||||
serverId: string,
|
||||
query?: {
|
||||
artistIds?: string[];
|
||||
maxYear?: number;
|
||||
minYear?: number;
|
||||
searchTerm?: string;
|
||||
},
|
||||
) => {
|
||||
const { filter } = splitPaginatedQuery(query);
|
||||
|
||||
if (query) {
|
||||
return [serverId, 'albums', 'list', filter] as const;
|
||||
}
|
||||
@ -207,6 +237,19 @@ export const queryKeys: Record<
|
||||
root: (serverId: string) => [serverId] as const,
|
||||
},
|
||||
songs: {
|
||||
count: (serverId: string, query?: AlbumArtistListQuery) => {
|
||||
const { pagination, filter } = splitPaginatedQuery(query);
|
||||
|
||||
if (query && pagination) {
|
||||
return [serverId, 'songs', 'count', filter, pagination] as const;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
return [serverId, 'songs', 'count', filter] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'songs', 'count'] as const;
|
||||
},
|
||||
detail: (serverId: string, query?: SongDetailQuery) => {
|
||||
if (query) return [serverId, 'songs', 'detail', query] as const;
|
||||
return [serverId, 'songs', 'detail'] as const;
|
||||
|
@ -435,16 +435,21 @@ axiosClient.interceptors.response.use(
|
||||
(response) => {
|
||||
const data = response.data;
|
||||
|
||||
if (data['subsonic-response'].status !== 'ok') {
|
||||
// Ping endpoint returns a string
|
||||
if (typeof data === 'string') {
|
||||
return response;
|
||||
}
|
||||
|
||||
if (data['subsonic-response']?.status !== 'ok') {
|
||||
// Suppress code related to non-linked lastfm or spotify from Navidrome
|
||||
if (data['subsonic-response'].error.code !== 0) {
|
||||
if (data['subsonic-response']?.error.code !== 0) {
|
||||
toast.error({
|
||||
message: data['subsonic-response'].error.message,
|
||||
message: data['subsonic-response']?.error.message,
|
||||
title: i18n.t('error.genericError', { postProcess: 'sentenceCase' }) as string,
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(data['subsonic-response'].error);
|
||||
return Promise.reject(data['subsonic-response']?.error);
|
||||
}
|
||||
|
||||
return response;
|
||||
@ -513,9 +518,9 @@ export const subsonicApiClient = (args: {
|
||||
});
|
||||
|
||||
return {
|
||||
body: result.data,
|
||||
headers: result.headers as any,
|
||||
status: result.status,
|
||||
body: result?.data,
|
||||
headers: result?.headers as any,
|
||||
status: result?.status,
|
||||
};
|
||||
} catch (e: Error | AxiosError | any) {
|
||||
if (isAxiosError(e)) {
|
||||
|
@ -1,15 +1,20 @@
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import filter from 'lodash/filter';
|
||||
import md5 from 'md5';
|
||||
import { fsLog } from '/@/logger';
|
||||
import { subsonicApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||
import { subsonicNormalize } from '/@/renderer/api/subsonic/subsonic-normalize';
|
||||
import { AlbumListSortType, SubsonicApi } from '/@/renderer/api/subsonic/subsonic-types';
|
||||
import {
|
||||
AlbumArtistListSort,
|
||||
AlbumListSort,
|
||||
AuthenticationResponse,
|
||||
ControllerEndpoint,
|
||||
GenreListSort,
|
||||
LibraryItem,
|
||||
PlaylistListSort,
|
||||
} from '/@/renderer/api/types';
|
||||
import { randomString } from '/@/renderer/utils';
|
||||
import { fsLog } from '/@/logger';
|
||||
|
||||
const authenticate = async (
|
||||
url: string,
|
||||
@ -184,7 +189,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
...subsonicNormalize.albumArtist(artist, apiClientProps.server),
|
||||
...subsonicNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||
albums: artist.album.map((album) =>
|
||||
subsonicNormalize.album(album, apiClientProps.server),
|
||||
),
|
||||
@ -193,6 +198,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
},
|
||||
getAlbumArtistList: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getArtists({
|
||||
query: {
|
||||
@ -209,14 +215,79 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
(index) => index.artist,
|
||||
);
|
||||
|
||||
let results = artists;
|
||||
let totalRecordCount = artists.length;
|
||||
|
||||
if (query.searchTerm) {
|
||||
const searchResults = filter(results, (artist) => {
|
||||
return artist.name.toLowerCase().includes(query.searchTerm!.toLowerCase());
|
||||
});
|
||||
|
||||
results = searchResults;
|
||||
totalRecordCount = searchResults.length;
|
||||
}
|
||||
|
||||
switch (query.sortBy) {
|
||||
case AlbumArtistListSort.ALBUM_COUNT:
|
||||
results = orderBy(
|
||||
artists,
|
||||
['albumCount', (v) => v.name.toLowerCase()],
|
||||
[sortOrder, 'asc'],
|
||||
);
|
||||
break;
|
||||
case AlbumArtistListSort.NAME:
|
||||
results = orderBy(artists, [(v) => v.name.toLowerCase()], [sortOrder]);
|
||||
break;
|
||||
case AlbumArtistListSort.FAVORITED:
|
||||
results = orderBy(artists, ['starred'], [sortOrder]);
|
||||
break;
|
||||
case AlbumArtistListSort.RATING:
|
||||
results = orderBy(artists, ['userRating'], [sortOrder]);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
items: artists.map((artist) =>
|
||||
items: results.map((artist) =>
|
||||
subsonicNormalize.albumArtist(artist, apiClientProps.server),
|
||||
),
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: null,
|
||||
totalRecordCount,
|
||||
};
|
||||
},
|
||||
getAlbumArtistListCount: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getArtists({
|
||||
query: {
|
||||
musicFolderId: query.musicFolderId,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get album artist list count');
|
||||
throw new Error('Failed to get album artist list count');
|
||||
}
|
||||
|
||||
const artists = (res.body['subsonic-response'].artists?.index || []).flatMap(
|
||||
(index) => index.artist,
|
||||
);
|
||||
|
||||
let results = artists;
|
||||
let totalRecordCount = artists.length;
|
||||
|
||||
if (query.searchTerm) {
|
||||
const searchResults = filter(results, (artist) => {
|
||||
return artist.name.toLowerCase().includes(query.searchTerm!.toLowerCase());
|
||||
});
|
||||
|
||||
results = searchResults;
|
||||
totalRecordCount = searchResults.length;
|
||||
}
|
||||
|
||||
return totalRecordCount;
|
||||
},
|
||||
getAlbumDetail: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
@ -285,6 +356,73 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
totalRecordCount: null,
|
||||
};
|
||||
},
|
||||
getAlbumListCount: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const sortType: Record<AlbumListSort, AlbumListSortType | undefined> = {
|
||||
[AlbumListSort.RANDOM]: SubsonicApi.getAlbumList2.enum.AlbumListSortType.RANDOM,
|
||||
[AlbumListSort.ALBUM_ARTIST]:
|
||||
SubsonicApi.getAlbumList2.enum.AlbumListSortType.ALPHABETICAL_BY_ARTIST,
|
||||
[AlbumListSort.PLAY_COUNT]: SubsonicApi.getAlbumList2.enum.AlbumListSortType.FREQUENT,
|
||||
[AlbumListSort.RECENTLY_ADDED]: SubsonicApi.getAlbumList2.enum.AlbumListSortType.NEWEST,
|
||||
[AlbumListSort.FAVORITED]: SubsonicApi.getAlbumList2.enum.AlbumListSortType.STARRED,
|
||||
[AlbumListSort.YEAR]: SubsonicApi.getAlbumList2.enum.AlbumListSortType.RECENT,
|
||||
[AlbumListSort.NAME]:
|
||||
SubsonicApi.getAlbumList2.enum.AlbumListSortType.ALPHABETICAL_BY_NAME,
|
||||
[AlbumListSort.COMMUNITY_RATING]: undefined,
|
||||
[AlbumListSort.DURATION]: undefined,
|
||||
[AlbumListSort.CRITIC_RATING]: undefined,
|
||||
[AlbumListSort.RATING]: undefined,
|
||||
[AlbumListSort.ARTIST]: undefined,
|
||||
[AlbumListSort.RECENTLY_PLAYED]: undefined,
|
||||
[AlbumListSort.RELEASE_DATE]: undefined,
|
||||
[AlbumListSort.SONG_COUNT]: undefined,
|
||||
};
|
||||
|
||||
let fetchNextPage = true;
|
||||
let startIndex = 0;
|
||||
let totalRecordCount = 0;
|
||||
|
||||
while (fetchNextPage) {
|
||||
const res = await subsonicApiClient(apiClientProps).getAlbumList2({
|
||||
query: {
|
||||
fromYear: query.minYear,
|
||||
genre: query.genre,
|
||||
musicFolderId: query.musicFolderId,
|
||||
offset: startIndex,
|
||||
size: 500,
|
||||
toYear: query.maxYear,
|
||||
type:
|
||||
sortType[query.sortBy] ??
|
||||
SubsonicApi.getAlbumList2.enum.AlbumListSortType.ALPHABETICAL_BY_NAME,
|
||||
},
|
||||
});
|
||||
|
||||
const headers = res.headers;
|
||||
|
||||
// Navidrome returns the total count in the header
|
||||
if (headers.get('x-total-count')) {
|
||||
fetchNextPage = false;
|
||||
totalRecordCount = Number(headers.get('x-total-count'));
|
||||
break;
|
||||
}
|
||||
|
||||
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'].albumList2.album.length;
|
||||
|
||||
totalRecordCount += albumCount;
|
||||
startIndex += albumCount;
|
||||
|
||||
// The max limit size for Subsonic is 500
|
||||
fetchNextPage = albumCount === 500;
|
||||
}
|
||||
|
||||
return totalRecordCount;
|
||||
},
|
||||
getAlbumSongList: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
@ -326,7 +464,8 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
return res.body['subsonic-response'].artistInfo;
|
||||
},
|
||||
getGenreList: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
const { query, apiClientProps } = args;
|
||||
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getGenres({});
|
||||
|
||||
@ -335,7 +474,31 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
throw new Error('Failed to get genre list');
|
||||
}
|
||||
|
||||
const genres = res.body['subsonic-response'].genres.genre.map(subsonicNormalize.genre);
|
||||
let results = res.body['subsonic-response'].genres.genre;
|
||||
|
||||
if (query.searchTerm) {
|
||||
const searchResults = filter(results, (genre) =>
|
||||
genre.value.toLowerCase().includes(query.searchTerm!.toLowerCase()),
|
||||
);
|
||||
|
||||
results = searchResults;
|
||||
}
|
||||
|
||||
switch (query.sortBy) {
|
||||
case GenreListSort.NAME:
|
||||
results = orderBy(results, [(v) => v.value.toLowerCase()], [sortOrder]);
|
||||
break;
|
||||
case GenreListSort.ALBUM_COUNT:
|
||||
results = orderBy(results, ['albumCount'], [sortOrder]);
|
||||
break;
|
||||
case GenreListSort.SONG_COUNT:
|
||||
results = orderBy(results, ['songCount'], [sortOrder]);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const genres = results.map(subsonicNormalize.genre);
|
||||
|
||||
return {
|
||||
items: genres,
|
||||
@ -361,6 +524,70 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
totalRecordCount: res.body['subsonic-response'].musicFolders.musicFolder.length,
|
||||
};
|
||||
},
|
||||
getPlaylistList: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getPlaylists({});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get playlist list');
|
||||
throw new Error('Failed to get playlist list');
|
||||
}
|
||||
|
||||
let results = res.body['subsonic-response'].playlists.playlist;
|
||||
|
||||
if (query.searchTerm) {
|
||||
const searchResults = filter(results, (playlist) => {
|
||||
return playlist.name.toLowerCase().includes(query.searchTerm!.toLowerCase());
|
||||
});
|
||||
|
||||
results = searchResults;
|
||||
}
|
||||
|
||||
switch (query.sortBy) {
|
||||
case PlaylistListSort.DURATION:
|
||||
results = orderBy(results, ['duration'], [sortOrder]);
|
||||
break;
|
||||
case PlaylistListSort.NAME:
|
||||
results = orderBy(results, [(v) => v.name?.toLowerCase()], [sortOrder]);
|
||||
break;
|
||||
case PlaylistListSort.OWNER:
|
||||
results = orderBy(results, [(v) => v.owner?.toLowerCase()], [sortOrder]);
|
||||
break;
|
||||
case PlaylistListSort.PUBLIC:
|
||||
results = orderBy(results, ['public'], [sortOrder]);
|
||||
break;
|
||||
case PlaylistListSort.SONG_COUNT:
|
||||
results = orderBy(results, ['songCount'], [sortOrder]);
|
||||
break;
|
||||
case PlaylistListSort.UPDATED_AT:
|
||||
results = orderBy(results, ['changed'], [sortOrder]);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
items: results.map((playlist) =>
|
||||
subsonicNormalize.playlist(playlist, apiClientProps.server),
|
||||
),
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.body['subsonic-response'].playlists.playlist.length,
|
||||
};
|
||||
},
|
||||
getPlaylistListCount: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getPlaylists({});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get playlist list count');
|
||||
throw new Error('Failed to get playlist list count');
|
||||
}
|
||||
|
||||
return res.body['subsonic-response'].playlists.playlist.length;
|
||||
},
|
||||
getRandomSongList: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
@ -407,6 +634,259 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
'',
|
||||
);
|
||||
},
|
||||
getSongList: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const fromAlbumPromises = [];
|
||||
const artistDetailPromises = [];
|
||||
let results: any[] = [];
|
||||
|
||||
if (query.genreId) {
|
||||
const res = await subsonicApiClient(apiClientProps).getSongsByGenre({
|
||||
query: {
|
||||
count: query.limit,
|
||||
genre: query.genreId,
|
||||
musicFolderId: query.musicFolderId,
|
||||
offset: 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'].songsByGenre.song.map((song) =>
|
||||
subsonicNormalize.song(song, apiClientProps.server, ''),
|
||||
),
|
||||
startIndex: 0,
|
||||
totalRecordCount: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (query.albumIds || query.artistIds) {
|
||||
if (query.albumIds) {
|
||||
for (const albumId of query.albumIds) {
|
||||
fromAlbumPromises.push(
|
||||
subsonicApiClient(apiClientProps).getAlbum({
|
||||
query: {
|
||||
id: albumId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (query.artistIds) {
|
||||
for (const artistId of query.artistIds) {
|
||||
artistDetailPromises.push(
|
||||
subsonicApiClient(apiClientProps).getArtist({
|
||||
query: {
|
||||
id: artistId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const artistResult = await Promise.all(artistDetailPromises);
|
||||
|
||||
const albums = artistResult.flatMap((artist) => {
|
||||
if (artist.status !== 200) {
|
||||
fsLog.warn('Failed to get artist detail', { context: { artist } });
|
||||
return [];
|
||||
}
|
||||
|
||||
return artist.body['subsonic-response'].artist.album;
|
||||
});
|
||||
|
||||
const albumIds = albums.map((album) => album.id);
|
||||
|
||||
for (const albumId of albumIds) {
|
||||
fromAlbumPromises.push(
|
||||
subsonicApiClient(apiClientProps).getAlbum({
|
||||
query: {
|
||||
id: albumId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (fromAlbumPromises) {
|
||||
const albumsResult = await Promise.all(fromAlbumPromises);
|
||||
|
||||
results = albumsResult.flatMap((album) => {
|
||||
if (album.status !== 200) {
|
||||
fsLog.warn('Failed to get album detail', { context: { album } });
|
||||
return [];
|
||||
}
|
||||
|
||||
return album.body['subsonic-response'].album.song;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
items: results.map((song) =>
|
||||
subsonicNormalize.song(song, apiClientProps.server, ''),
|
||||
),
|
||||
startIndex: 0,
|
||||
totalRecordCount: results.length,
|
||||
};
|
||||
}
|
||||
|
||||
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: 0,
|
||||
totalRecordCount: null,
|
||||
};
|
||||
},
|
||||
getSongListCount: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
let fetchNextPage = true;
|
||||
let startIndex = 0;
|
||||
|
||||
let fetchNextSection = true;
|
||||
let sectionIndex = 0;
|
||||
|
||||
if (query.genreId) {
|
||||
let totalRecordCount = 0;
|
||||
while (fetchNextSection) {
|
||||
const res = await subsonicApiClient(apiClientProps).getSongsByGenre({
|
||||
query: {
|
||||
count: 1,
|
||||
genre: query.genreId,
|
||||
musicFolderId: query.musicFolderId,
|
||||
offset: sectionIndex,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get song list count');
|
||||
throw new Error('Failed to get song list count');
|
||||
}
|
||||
|
||||
const numberOfResults =
|
||||
res.body['subsonic-response'].songsByGenre.song?.length || 0;
|
||||
|
||||
if (numberOfResults !== 1) {
|
||||
fetchNextSection = false;
|
||||
startIndex = sectionIndex === 0 ? 0 : sectionIndex - 5000;
|
||||
break;
|
||||
} else {
|
||||
sectionIndex += 5000;
|
||||
}
|
||||
}
|
||||
|
||||
while (fetchNextPage) {
|
||||
const res = await subsonicApiClient(apiClientProps).getSongsByGenre({
|
||||
query: {
|
||||
count: 500,
|
||||
genre: query.genreId,
|
||||
musicFolderId: query.musicFolderId,
|
||||
offset: startIndex,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get song list count');
|
||||
throw new Error('Failed to get song list count');
|
||||
}
|
||||
|
||||
const numberOfResults =
|
||||
res.body['subsonic-response'].songsByGenre.song?.length || 0;
|
||||
|
||||
totalRecordCount = startIndex + numberOfResults;
|
||||
startIndex += numberOfResults;
|
||||
|
||||
fetchNextPage = numberOfResults === 500;
|
||||
}
|
||||
|
||||
return totalRecordCount;
|
||||
}
|
||||
|
||||
let totalRecordCount = 0;
|
||||
|
||||
while (fetchNextSection) {
|
||||
const res = await subsonicApiClient(apiClientProps).search3({
|
||||
query: {
|
||||
albumCount: 0,
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
query: query.searchTerm || '""',
|
||||
songCount: 1,
|
||||
songOffset: sectionIndex,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get song list count');
|
||||
throw new Error('Failed to get song list count');
|
||||
}
|
||||
|
||||
const numberOfResults = res.body['subsonic-response'].searchResult3.song?.length || 0;
|
||||
|
||||
// Check each batch of 5000 songs to check for data
|
||||
sectionIndex += 5000;
|
||||
fetchNextSection = numberOfResults === 1;
|
||||
|
||||
if (!fetchNextSection) {
|
||||
// fetchNextBlock will be false on the next loop so we need to subtract 5000 * 2
|
||||
startIndex = sectionIndex - 10000;
|
||||
}
|
||||
}
|
||||
|
||||
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 numberOfResults = res.body['subsonic-response'].searchResult3.song?.length || 0;
|
||||
|
||||
totalRecordCount = startIndex + numberOfResults;
|
||||
startIndex += numberOfResults;
|
||||
|
||||
// The max limit size for Subsonic is 500
|
||||
fetchNextPage = numberOfResults === 500;
|
||||
}
|
||||
|
||||
return totalRecordCount;
|
||||
},
|
||||
getTopSongs: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
Album,
|
||||
Genre,
|
||||
MusicFolder,
|
||||
Playlist,
|
||||
} from '/@/renderer/api/types';
|
||||
import { ServerListItem, ServerType } from '/@/renderer/types';
|
||||
|
||||
@ -116,13 +117,14 @@ const normalizeAlbumArtist = (
|
||||
| z.infer<typeof SubsonicApi._baseTypes.artist>
|
||||
| z.infer<typeof SubsonicApi._baseTypes.artistListEntry>,
|
||||
server: ServerListItem | null,
|
||||
imageSize?: number,
|
||||
): AlbumArtist => {
|
||||
const imageUrl =
|
||||
getCoverArtUrl({
|
||||
baseUrl: server?.url,
|
||||
coverArtId: item.coverArt,
|
||||
credential: server?.credential,
|
||||
size: 100,
|
||||
size: imageSize || 100,
|
||||
}) || null;
|
||||
|
||||
return {
|
||||
@ -167,7 +169,7 @@ const normalizeAlbum = (
|
||||
artists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [],
|
||||
backdropImageUrl: null,
|
||||
createdAt: item.created,
|
||||
duration: item.duration,
|
||||
duration: item.duration * 1000,
|
||||
genres: item.genre
|
||||
? [
|
||||
{
|
||||
@ -192,7 +194,10 @@ const normalizeAlbum = (
|
||||
serverType: ServerType.SUBSONIC,
|
||||
size: null,
|
||||
songCount: item.songCount,
|
||||
songs: [],
|
||||
songs:
|
||||
(item as z.infer<typeof SubsonicApi._baseTypes.album>).song?.map((song) =>
|
||||
normalizeSong(song, server, ''),
|
||||
) || [],
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item.created,
|
||||
userFavorite: item.starred || false,
|
||||
@ -220,10 +225,41 @@ const normalizeMusicFolder = (
|
||||
};
|
||||
};
|
||||
|
||||
const normalizePlaylist = (
|
||||
item:
|
||||
| z.infer<typeof SubsonicApi._baseTypes.playlist>
|
||||
| z.infer<typeof SubsonicApi._baseTypes.playlistListEntry>,
|
||||
server: ServerListItem | null,
|
||||
): Playlist => {
|
||||
return {
|
||||
description: item.comment || null,
|
||||
duration: item.duration,
|
||||
genres: [],
|
||||
id: item.id,
|
||||
imagePlaceholderUrl: null,
|
||||
imageUrl: getCoverArtUrl({
|
||||
baseUrl: server?.url,
|
||||
coverArtId: item.coverArt,
|
||||
credential: server?.credential,
|
||||
size: 300,
|
||||
}),
|
||||
itemType: LibraryItem.PLAYLIST,
|
||||
name: item.name,
|
||||
owner: item.owner,
|
||||
ownerId: item.owner,
|
||||
public: item.public,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.SUBSONIC,
|
||||
size: null,
|
||||
songCount: item.songCount,
|
||||
};
|
||||
};
|
||||
|
||||
export const subsonicNormalize = {
|
||||
album: normalizeAlbum,
|
||||
albumArtist: normalizeAlbumArtist,
|
||||
genre: normalizeGenre,
|
||||
musicFolder: normalizeMusicFolder,
|
||||
playlist: normalizePlaylist,
|
||||
song: normalizeSong,
|
||||
};
|
||||
|
@ -582,7 +582,7 @@ const search3 = {
|
||||
artistCount: z.number().optional(),
|
||||
artistOffset: z.number().optional(),
|
||||
musicFolderId: z.string().optional(),
|
||||
query: z.string(),
|
||||
query: z.string().or(z.literal('""')),
|
||||
songCount: z.number().optional(),
|
||||
songOffset: z.number().optional(),
|
||||
}),
|
||||
|
@ -306,7 +306,9 @@ export type GenreListResponse = BasePaginatedResponse<Genre[]> | null | undefine
|
||||
export type GenreListArgs = { query: GenreListQuery } & BaseEndpointArgs;
|
||||
|
||||
export enum GenreListSort {
|
||||
ALBUM_COUNT = 'albumCount',
|
||||
NAME = 'name',
|
||||
SONG_COUNT = 'songCount',
|
||||
}
|
||||
|
||||
export type GenreListQuery = {
|
||||
@ -330,10 +332,14 @@ type GenreListSortMap = {
|
||||
|
||||
export const genreListSortMap: GenreListSortMap = {
|
||||
jellyfin: {
|
||||
albumCount: undefined,
|
||||
name: JFGenreListSort.NAME,
|
||||
songCount: undefined,
|
||||
},
|
||||
navidrome: {
|
||||
albumCount: undefined,
|
||||
name: NDGenreListSort.NAME,
|
||||
songCount: undefined,
|
||||
},
|
||||
subsonic: {
|
||||
name: undefined,
|
||||
@ -484,8 +490,11 @@ export type SongListQuery = {
|
||||
};
|
||||
albumIds?: string[];
|
||||
artistIds?: string[];
|
||||
genreId?: string;
|
||||
imageSize?: number;
|
||||
limit?: number;
|
||||
maxYear?: number;
|
||||
minYear?: number;
|
||||
musicFolderId?: string;
|
||||
searchTerm?: string;
|
||||
sortBy: SongListSort;
|
||||
@ -1161,8 +1170,10 @@ export type ControllerEndpoint = Partial<{
|
||||
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
|
||||
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
|
||||
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
|
||||
getAlbumArtistListCount: (args: AlbumArtistListArgs) => Promise<number>;
|
||||
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
|
||||
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
|
||||
getAlbumListCount: (args: AlbumListArgs) => Promise<number>;
|
||||
getAlbumSongList: (args: AlbumDetailArgs) => Promise<SongListResponse>; // TODO
|
||||
getArtistDetail: () => void;
|
||||
getArtistInfo: (args: any) => void;
|
||||
@ -1176,10 +1187,12 @@ export type ControllerEndpoint = Partial<{
|
||||
getMusicFolderList: (args: MusicFolderListArgs) => Promise<MusicFolderListResponse>;
|
||||
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
|
||||
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
|
||||
getPlaylistListCount: (args: PlaylistListArgs) => Promise<number>;
|
||||
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
|
||||
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
|
||||
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
|
||||
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
|
||||
getSongListCount: (args: SongListArgs) => Promise<number>;
|
||||
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
|
||||
getUserList: (args: UserListArgs) => Promise<UserListResponse>;
|
||||
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
|
||||
|
@ -182,6 +182,20 @@ export const useVirtualTable = <TFilter>({
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.totalRecordCount === null) {
|
||||
const totalRecordCount: number | undefined = itemCount;
|
||||
const hasMoreRows = results?.items?.length === properties.filter.limit;
|
||||
const lastRowIndex = hasMoreRows
|
||||
? undefined
|
||||
: properties.filter.offset + results.items.length;
|
||||
|
||||
params.successCallback(
|
||||
results?.items || [],
|
||||
totalRecordCount || lastRowIndex,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
params.successCallback(results?.items || [], results?.totalRecordCount || 0);
|
||||
},
|
||||
rowCount: undefined,
|
||||
@ -198,6 +212,7 @@ export const useVirtualTable = <TFilter>({
|
||||
queryClient,
|
||||
isClientSideSort,
|
||||
queryFn,
|
||||
itemCount,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -139,14 +139,61 @@ const FILTERS = {
|
||||
value: AlbumListSort.YEAR,
|
||||
},
|
||||
],
|
||||
subsonic: [
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.ALBUM_ARTIST,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.PLAY_COUNT,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.NAME,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.RANDOM,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.RECENTLY_ADDED,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.RECENTLY_PLAYED,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.favorited', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.FAVORITED,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.YEAR,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
interface AlbumListHeaderFiltersProps {
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
itemCount: number | undefined;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFiltersProps) => {
|
||||
export const AlbumListHeaderFilters = ({
|
||||
gridRef,
|
||||
tableRef,
|
||||
itemCount,
|
||||
}: AlbumListHeaderFiltersProps) => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const { pageKey, customFilters, handlePlay } = useListContext();
|
||||
@ -159,6 +206,7 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
|
||||
itemCount,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
server,
|
||||
});
|
||||
|
@ -38,6 +38,7 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef, title }: AlbumLi
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
|
||||
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
|
||||
itemCount,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
server,
|
||||
});
|
||||
@ -94,6 +95,7 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef, title }: AlbumLi
|
||||
<FilterBar>
|
||||
<AlbumListHeaderFilters
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</FilterBar>
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import type { AlbumListQuery } from '/@/renderer/api/types';
|
||||
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { getServerById } from '/@/renderer/store';
|
||||
|
||||
export const getAlbumListCountQuery = (query: AlbumListQuery) => {
|
||||
const filter: Record<string, unknown> = {};
|
||||
|
||||
if (query.artistIds) filter.artistIds = query.artistIds;
|
||||
if (query.maxYear) filter.maxYear = query.maxYear;
|
||||
if (query.minYear) filter.minYear = query.minYear;
|
||||
if (query.searchTerm) filter.searchTerm = query.searchTerm;
|
||||
if (query.musicFolderId) filter.musicFolderId = query.musicFolderId;
|
||||
|
||||
if (Object.keys(filter).length === 0) return undefined;
|
||||
|
||||
return filter;
|
||||
};
|
||||
|
||||
export const useAlbumListCount = (args: QueryHookArgs<AlbumListQuery>) => {
|
||||
const { options, query, serverId } = args;
|
||||
const server = getServerById(serverId);
|
||||
|
||||
return useQuery({
|
||||
enabled: !!serverId,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!server) throw new Error('Server not found');
|
||||
return api.controller.getAlbumListCount({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query,
|
||||
});
|
||||
},
|
||||
queryKey: queryKeys.albums.count(serverId || '', getAlbumListCountQuery(query)),
|
||||
...options,
|
||||
});
|
||||
};
|
@ -9,12 +9,12 @@ import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { ListContext } from '/@/renderer/context/list-context';
|
||||
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
|
||||
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
|
||||
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||
import { queryClient } from '/@/renderer/lib/react-query';
|
||||
import { useCurrentServer, useListFilterByKey } from '/@/renderer/store';
|
||||
import { Play } from '/@/renderer/types';
|
||||
import { useAlbumListCount } from '/@/renderer/features/albums/queries/album-list-count-query';
|
||||
|
||||
const AlbumListRoute = () => {
|
||||
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||
@ -42,23 +42,18 @@ const AlbumListRoute = () => {
|
||||
key: pageKey,
|
||||
});
|
||||
|
||||
const itemCountCheck = useAlbumList({
|
||||
const itemCountCheck = useAlbumListCount({
|
||||
options: {
|
||||
cacheTime: 1000 * 60,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
query: {
|
||||
limit: 1,
|
||||
startIndex: 0,
|
||||
...albumListFilter,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const itemCount =
|
||||
itemCountCheck.data?.totalRecordCount === null
|
||||
? undefined
|
||||
: itemCountCheck.data?.totalRecordCount;
|
||||
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
|
||||
|
||||
const handlePlay = useCallback(
|
||||
async (args: { initialSongId?: string; playType: Play }) => {
|
||||
|
@ -85,6 +85,28 @@ const FILTERS = {
|
||||
value: AlbumArtistListSort.SONG_COUNT,
|
||||
},
|
||||
],
|
||||
subsonic: [
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
|
||||
value: AlbumArtistListSort.ALBUM_COUNT,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
|
||||
value: AlbumArtistListSort.FAVORITED,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||
value: AlbumArtistListSort.NAME,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
|
||||
value: AlbumArtistListSort.RATING,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
interface AlbumArtistListHeaderFiltersProps {
|
||||
|
@ -35,6 +35,7 @@ export const AlbumArtistListHeader = ({
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
|
||||
itemCount,
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
server,
|
||||
});
|
||||
|
@ -0,0 +1,38 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { AlbumArtistListQuery } from '/@/renderer/api/types';
|
||||
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { getServerById } from '/@/renderer/store';
|
||||
|
||||
export const getAlbumArtistListCountQuery = (query: AlbumArtistListQuery) => {
|
||||
const filter: Record<string, unknown> = {};
|
||||
|
||||
if (query.searchTerm) filter.searchTerm = query.searchTerm;
|
||||
if (query.musicFolderId) filter.musicFolderId = query.musicFolderId;
|
||||
|
||||
if (Object.keys(filter).length === 0) return undefined;
|
||||
|
||||
return filter;
|
||||
};
|
||||
|
||||
export const useAlbumArtistListCount = (args: QueryHookArgs<AlbumArtistListQuery>) => {
|
||||
const { options, query, serverId } = args;
|
||||
const server = getServerById(serverId);
|
||||
|
||||
return useQuery({
|
||||
enabled: !!serverId,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!server) throw new Error('Server not found');
|
||||
return api.controller.getAlbumArtistListCount({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query,
|
||||
});
|
||||
},
|
||||
queryKey: queryKeys.albumArtists.count(serverId || '', getAlbumArtistListCountQuery(query)),
|
||||
...options,
|
||||
});
|
||||
};
|
@ -7,7 +7,7 @@ import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { ListContext } from '/@/renderer/context/list-context';
|
||||
import { AlbumArtistListContent } from '/@/renderer/features/artists/components/album-artist-list-content';
|
||||
import { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header';
|
||||
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
|
||||
import { useAlbumArtistListCount } from '/@/renderer/features/artists/queries/album-artist-list-count-query';
|
||||
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||
|
||||
const AlbumArtistListRoute = () => {
|
||||
@ -18,23 +18,18 @@ const AlbumArtistListRoute = () => {
|
||||
|
||||
const albumArtistListFilter = useListFilterByKey({ key: pageKey });
|
||||
|
||||
const itemCountCheck = useAlbumArtistList({
|
||||
const itemCountCheck = useAlbumArtistListCount({
|
||||
options: {
|
||||
cacheTime: 1000 * 60,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
query: {
|
||||
limit: 1,
|
||||
startIndex: 0,
|
||||
...albumArtistListFilter,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const itemCount =
|
||||
itemCountCheck.data?.totalRecordCount === null
|
||||
? undefined
|
||||
: itemCountCheck.data?.totalRecordCount;
|
||||
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
|
||||
|
||||
const providerValue = useMemo(() => {
|
||||
return {
|
||||
|
@ -37,14 +37,36 @@ const FILTERS = {
|
||||
value: GenreListSort.NAME,
|
||||
},
|
||||
],
|
||||
subsonic: [
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||
value: GenreListSort.NAME,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
|
||||
value: GenreListSort.ALBUM_COUNT,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
|
||||
value: GenreListSort.SONG_COUNT,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
interface GenreListHeaderFiltersProps {
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
itemCount: number | undefined;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFiltersProps) => {
|
||||
export const GenreListHeaderFilters = ({
|
||||
gridRef,
|
||||
tableRef,
|
||||
itemCount,
|
||||
}: GenreListHeaderFiltersProps) => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const { pageKey, customFilters } = useListContext();
|
||||
@ -54,6 +76,7 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
|
||||
itemCount,
|
||||
itemType: LibraryItem.GENRE,
|
||||
server,
|
||||
});
|
||||
|
@ -34,6 +34,7 @@ export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeade
|
||||
const { setFilter, setTablePagination } = useListStoreActions();
|
||||
|
||||
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
|
||||
itemCount,
|
||||
itemType: LibraryItem.GENRE,
|
||||
server,
|
||||
});
|
||||
@ -89,6 +90,7 @@ export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeade
|
||||
<FilterBar>
|
||||
<GenreListHeaderFilters
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</FilterBar>
|
||||
|
@ -69,6 +69,38 @@ const FILTERS = {
|
||||
value: PlaylistListSort.UPDATED_AT,
|
||||
},
|
||||
],
|
||||
subsonic: [
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
|
||||
value: PlaylistListSort.DURATION,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||
value: PlaylistListSort.NAME,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.owner', { postProcess: 'titleCase' }),
|
||||
value: PlaylistListSort.OWNER,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.isPublic', { postProcess: 'titleCase' }),
|
||||
value: PlaylistListSort.PUBLIC,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
|
||||
value: PlaylistListSort.SONG_COUNT,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.recentlyUpdated', { postProcess: 'titleCase' }),
|
||||
value: PlaylistListSort.UPDATED_AT,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
interface PlaylistListHeaderFiltersProps {
|
||||
|
@ -44,6 +44,7 @@ export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistLis
|
||||
};
|
||||
|
||||
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
|
||||
itemCount,
|
||||
itemType: LibraryItem.PLAYLIST,
|
||||
server,
|
||||
});
|
||||
|
@ -160,14 +160,26 @@ const FILTERS = {
|
||||
value: SongListSort.YEAR,
|
||||
},
|
||||
],
|
||||
subsonic: [
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.NAME,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
interface SongListHeaderFiltersProps {
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
itemCount: number | undefined;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const SongListHeaderFilters = ({ gridRef, tableRef }: SongListHeaderFiltersProps) => {
|
||||
export const SongListHeaderFilters = ({
|
||||
gridRef,
|
||||
tableRef,
|
||||
itemCount,
|
||||
}: SongListHeaderFiltersProps) => {
|
||||
const { t } = useTranslation();
|
||||
const server = useCurrentServer();
|
||||
const { pageKey, handlePlay, customFilters } = useListContext();
|
||||
@ -179,6 +191,7 @@ export const SongListHeaderFilters = ({ gridRef, tableRef }: SongListHeaderFilte
|
||||
useListStoreActions();
|
||||
|
||||
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
|
||||
itemCount,
|
||||
itemType: LibraryItem.SONG,
|
||||
server,
|
||||
});
|
||||
|
@ -32,6 +32,7 @@ export const SongListHeader = ({ gridRef, title, itemCount, tableRef }: SongList
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
|
||||
itemCount,
|
||||
itemType: LibraryItem.SONG,
|
||||
server,
|
||||
});
|
||||
@ -94,6 +95,7 @@ export const SongListHeader = ({ gridRef, title, itemCount, tableRef }: SongList
|
||||
<FilterBar>
|
||||
<SongListHeaderFilters
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</FilterBar>
|
||||
|
39
src/renderer/features/songs/queries/song-list-count-query.ts
Normal file
39
src/renderer/features/songs/queries/song-list-count-query.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import type { SongListQuery } from '/@/renderer/api/types';
|
||||
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { getServerById } from '/@/renderer/store';
|
||||
|
||||
export const getSongListCountQuery = (query: SongListQuery) => {
|
||||
const filter: Record<string, unknown> = {};
|
||||
|
||||
if (query.searchTerm) filter.searchTerm = query.searchTerm;
|
||||
if (query.genreId) filter.genreId = query.genreId;
|
||||
if (query.musicFolderId) filter.musicFolderId = query.musicFolderId;
|
||||
|
||||
if (Object.keys(filter).length === 0) return undefined;
|
||||
|
||||
return filter;
|
||||
};
|
||||
|
||||
export const useSongListCount = (args: QueryHookArgs<SongListQuery>) => {
|
||||
const { options, query, serverId } = args;
|
||||
const server = getServerById(serverId);
|
||||
|
||||
return useQuery({
|
||||
enabled: !!serverId,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!server) throw new Error('Server not found');
|
||||
return api.controller.getSongListCount({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query,
|
||||
});
|
||||
},
|
||||
queryKey: queryKeys.songs.count(serverId || '', getSongListCountQuery(query)),
|
||||
...options,
|
||||
});
|
||||
};
|
@ -9,11 +9,11 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||
import { SongListContent } from '/@/renderer/features/songs/components/song-list-content';
|
||||
import { SongListHeader } from '/@/renderer/features/songs/components/song-list-header';
|
||||
import { useSongList } from '/@/renderer/features/songs/queries/song-list-query';
|
||||
import { useCurrentServer, useListFilterByKey } from '/@/renderer/store';
|
||||
import { Play } from '/@/renderer/types';
|
||||
import { titleCase } from '/@/renderer/utils';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { useSongListCount } from '/@/renderer/features/songs/queries/song-list-count-query';
|
||||
|
||||
const TrackListRoute = () => {
|
||||
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||
@ -36,6 +36,7 @@ const TrackListRoute = () => {
|
||||
genre_id: genreId,
|
||||
},
|
||||
},
|
||||
genreId,
|
||||
}),
|
||||
};
|
||||
|
||||
@ -74,7 +75,7 @@ const TrackListRoute = () => {
|
||||
return genre?.name;
|
||||
}, [genreId, genreList.data]);
|
||||
|
||||
const itemCountCheck = useSongList({
|
||||
const itemCountCheck = useSongListCount({
|
||||
options: {
|
||||
cacheTime: 1000 * 60,
|
||||
staleTime: 1000 * 60,
|
||||
@ -87,10 +88,7 @@ const TrackListRoute = () => {
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const itemCount =
|
||||
itemCountCheck.data?.totalRecordCount === null
|
||||
? undefined
|
||||
: itemCountCheck.data?.totalRecordCount;
|
||||
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
|
||||
|
||||
const handlePlay = useCallback(
|
||||
async (args: { initialSongId?: string; playType: Play }) => {
|
||||
|
@ -10,6 +10,7 @@ import orderBy from 'lodash/orderBy';
|
||||
|
||||
interface UseHandleListFilterChangeProps {
|
||||
isClientSideSort?: boolean;
|
||||
itemCount?: number;
|
||||
itemType: LibraryItem;
|
||||
server: ServerListItem | null;
|
||||
}
|
||||
@ -18,6 +19,7 @@ export const useListFilterRefresh = ({
|
||||
server,
|
||||
itemType,
|
||||
isClientSideSort,
|
||||
itemCount,
|
||||
}: UseHandleListFilterChangeProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@ -98,11 +100,14 @@ export const useListFilterRefresh = ({
|
||||
filter.sortOrder === 'DESC' ? ['desc'] : ['asc'],
|
||||
);
|
||||
|
||||
params.successCallback(sortedResults || [], res?.totalRecordCount || 0);
|
||||
params.successCallback(
|
||||
sortedResults || [],
|
||||
res?.totalRecordCount || itemCount,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
params.successCallback(res?.items || [], res?.totalRecordCount || 0);
|
||||
params.successCallback(res?.items || [], res?.totalRecordCount || itemCount);
|
||||
},
|
||||
|
||||
rowCount: undefined,
|
||||
@ -112,7 +117,7 @@ export const useListFilterRefresh = ({
|
||||
tableRef.current?.api.purgeInfiniteCache();
|
||||
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||
},
|
||||
[isClientSideSort, queryClient, queryFn, queryKeyFn, server],
|
||||
[isClientSideSort, itemCount, queryClient, queryFn, queryKeyFn, server],
|
||||
);
|
||||
|
||||
const handleRefreshGrid = useCallback(
|
||||
|
Loading…
Reference in New Issue
Block a user