progress on subsonic api

This commit is contained in:
jeffvli
2025-07-21 00:08:58 -07:00
parent 96221c8fa7
commit 98e8bda45d
94 changed files with 3083 additions and 798 deletions
+2 -4
View File
@@ -73,7 +73,7 @@ function createLoggedFunction<TRequest, TResponse>(
return undefined;
}
return async (request: TRequest, server: ServerListItem, options?: any) => {
return async (request: TRequest, requestOptions?: any) => {
const startTime = Date.now();
const requestId = Math.random().toString(36).substring(2, 15);
@@ -96,13 +96,11 @@ function createLoggedFunction<TRequest, TResponse>(
logger.info(`[API] ${functionName} called`, {
request: truncatedRequest,
requestId,
serverId: server.id,
serverType: server.type,
});
}
try {
const result = await originalFn(request, server, options);
const result = await originalFn(request, requestOptions);
const duration = Date.now() - startTime;
if (result[0]) {
+41 -13
View File
@@ -1,47 +1,75 @@
import { createLoggedApiController } from '/@/renderer/api/api-controller-logger';
import { getServerById } from '/@/renderer/store';
import { useAuthStore } from '/@/renderer/store';
import {
apiClient as subsonicApiClient,
controller as subsonicBaseAdapter,
createApiClient as subsonicApiClient,
authenticate as subsonicAuthenticate,
controller as subsonicController,
middleware as subsonicMiddleware,
} from '/@/shared/api/subsonic/subsonic-controller';
import { ApiController } from '/@/shared/types/adapter/api-controller-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
export const serverApi = {
export const serverApiMap = {
[ServerType.JELLYFIN]: {
apiClient: null,
authenticate: null,
controller: {},
middleware: null,
},
[ServerType.NAVIDROME]: {
apiClient: null,
authenticate: null,
controller: {},
middleware: null,
},
[ServerType.SUBSONIC]: {
apiClient: subsonicApiClient,
controller: createLoggedApiController(subsonicBaseAdapter),
authenticate: subsonicAuthenticate,
controller: subsonicController,
middleware: subsonicMiddleware,
},
};
export const api = (serverId: string): ApiController => {
const server = getServerById(serverId);
const getApiByServer = (serverId: string): ApiController => {
const servers = useAuthStore.getState().serverList;
const server = servers[serverId];
if (!server) {
throw new Error('No server or api client selected');
}
const { apiClient, controller, middleware } = serverApi[server.type];
if (middleware) {
apiClient.use(middleware(server));
}
const { apiClient, controller, middleware } = serverApiMap[server.type];
if (!apiClient) {
throw new Error('No api client found');
}
return controller as ApiController;
const client = apiClient(server, middleware);
return createLoggedApiController(controller(client, server));
};
const getAppApi = () => {
const servers = useAuthStore.getState().serverList;
return Object.entries(servers).reduce(
(acc, [id]) => {
acc[id] = getApiByServer(id);
return acc;
},
{} as Record<string, ApiController>,
);
};
export const api = {
authenticate: (serverType: ServerType) => {
const { authenticate } = serverApiMap[serverType];
if (!serverType || !authenticate) {
throw new Error();
}
return authenticate;
},
controller: getAppApi(),
};
@@ -322,7 +322,7 @@ export const JellyfinController: ControllerEndpoint = {
SearchTerm: query.searchTerm,
SortBy: albumListSortMap.jellyfin[query.sortBy] || 'SortName',
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.startIndex,
StartIndex: query.offset,
...query._custom?.jellyfin,
Years: yearsFilter,
},
@@ -334,14 +334,14 @@ export const JellyfinController: ControllerEndpoint = {
return {
items: res.body.Items.map((item) => jfNormalize.album(item, apiClientProps.server)),
startIndex: query.startIndex,
offset: query.offset,
totalRecordCount: res.body.TotalRecordCount,
};
},
getAlbumListCount: async ({ apiClientProps, query }) =>
JellyfinController.getAlbumList({
apiClientProps,
query: { ...query, limit: 1, startIndex: 0 },
query: { ...query, limit: 1, offset: 0 },
}).then((result) => result!.totalRecordCount!),
getArtistList: async (args) => {
const { apiClientProps, query } = args;
@@ -356,7 +356,7 @@ export const JellyfinController: ControllerEndpoint = {
SearchTerm: query.searchTerm,
SortBy: albumArtistListSortMap.jellyfin[query.sortBy] || 'SortName,Name',
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.startIndex,
StartIndex: query.offset,
UserId: apiClientProps.server?.userId || undefined,
},
});
@@ -369,14 +369,14 @@ export const JellyfinController: ControllerEndpoint = {
items: res.body.Items.map((item) =>
jfNormalize.albumArtist(item, apiClientProps.server),
),
startIndex: query.startIndex,
offset: query.offset,
totalRecordCount: res.body.TotalRecordCount,
};
},
getArtistListCount: async ({ apiClientProps, query }) =>
JellyfinController.getArtistList({
apiClientProps,
query: { ...query, limit: 1, startIndex: 0 },
query: { ...query, limit: 1, offset: 0 },
}).then((result) => result!.totalRecordCount!),
getDownloadUrl: (args) => {
const { apiClientProps, query } = args;
@@ -398,7 +398,7 @@ export const JellyfinController: ControllerEndpoint = {
SearchTerm: query?.searchTerm,
SortBy: genreListSortMap.jellyfin[query.sortBy] || 'SortName',
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.startIndex,
StartIndex: query.offset,
UserId: apiClientProps.server?.userId,
},
});
@@ -409,7 +409,7 @@ export const JellyfinController: ControllerEndpoint = {
return {
items: res.body.Items.map((item) => jfNormalize.genre(item, apiClientProps.server)),
startIndex: query.startIndex || 0,
offset: query.offset || 0,
totalRecordCount: res.body?.TotalRecordCount || 0,
};
},
@@ -458,7 +458,7 @@ export const JellyfinController: ControllerEndpoint = {
return {
items: musicFolders.map(jfNormalize.musicFolder),
startIndex: 0,
offset: 0,
totalRecordCount: musicFolders?.length || 0,
};
},
@@ -505,7 +505,7 @@ export const JellyfinController: ControllerEndpoint = {
SearchTerm: query.searchTerm,
SortBy: playlistListSortMap.jellyfin[query.sortBy],
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.startIndex,
StartIndex: query.offset,
},
});
@@ -515,14 +515,14 @@ export const JellyfinController: ControllerEndpoint = {
return {
items: res.body.Items.map((item) => jfNormalize.playlist(item, apiClientProps.server)),
startIndex: 0,
offset: 0,
totalRecordCount: res.body.TotalRecordCount,
};
},
getPlaylistListCount: async ({ apiClientProps, query }) =>
JellyfinController.getPlaylistList({
apiClientProps,
query: { ...query, limit: 1, startIndex: 0 },
query: { ...query, limit: 1, offset: 0 },
}).then((result) => result!.totalRecordCount!),
getPlaylistSongList: async (args) => {
const { apiClientProps, query } = args;
@@ -552,7 +552,7 @@ export const JellyfinController: ControllerEndpoint = {
return {
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
startIndex: query.startIndex,
offset: query.startIndex,
totalRecordCount: res.body.TotalRecordCount,
};
},
@@ -602,7 +602,7 @@ export const JellyfinController: ControllerEndpoint = {
return {
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
startIndex: 0,
offset: 0,
totalRecordCount: res.body.Items.length || 0,
};
},
@@ -742,7 +742,7 @@ export const JellyfinController: ControllerEndpoint = {
SearchTerm: query.searchTerm,
SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName',
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.startIndex,
StartIndex: query.offset,
...query._custom?.jellyfin,
Years: yearsFilter,
},
@@ -777,7 +777,7 @@ export const JellyfinController: ControllerEndpoint = {
SearchTerm: query.searchTerm,
SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName',
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.startIndex,
StartIndex: query.offset,
...query._custom?.jellyfin,
Years: yearsFilter,
},
@@ -806,14 +806,14 @@ export const JellyfinController: ControllerEndpoint = {
items: items.map((item) =>
jfNormalize.song(item, apiClientProps.server, '', query.imageSize),
),
startIndex: query.startIndex,
offset: query.offset,
totalRecordCount,
};
},
getSongListCount: async ({ apiClientProps, query }) =>
JellyfinController.getSongList({
apiClientProps,
query: { ...query, limit: 1, startIndex: 0 },
query: { ...query, limit: 1, offset: 0 },
}).then((result) => result!.totalRecordCount!),
getTags: async (args) => {
const { apiClientProps, query } = args;
@@ -869,7 +869,7 @@ export const JellyfinController: ControllerEndpoint = {
return {
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
startIndex: 0,
offset: 0,
totalRecordCount: res.body.TotalRecordCount,
};
},
@@ -273,10 +273,10 @@ export const NavidromeController: ControllerEndpoint = {
const res = await ndApiClient(apiClientProps).getAlbumList({
query: {
_end: query.startIndex + (query.limit || 0),
_end: query.offset + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: albumListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
_start: query.offset,
artist_id: query.artistIds?.[0],
compilation: query.compilation,
genre_id: query.genres?.[0],
@@ -293,24 +293,24 @@ export const NavidromeController: ControllerEndpoint = {
return {
items: res.body.data.map((album) => ndNormalize.album(album, apiClientProps.server)),
startIndex: query?.startIndex || 0,
offset: query?.offset || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
},
getAlbumListCount: async ({ apiClientProps, query }) =>
NavidromeController.getAlbumList({
apiClientProps,
query: { ...query, limit: 1, startIndex: 0 },
query: { ...query, limit: 1, offset: 0 },
}).then((result) => result!.totalRecordCount!),
getArtistList: async (args) => {
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
query: {
_end: query.startIndex + (query.limit || 0),
_end: query.offset + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: albumArtistListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
_start: query.offset,
name: query.searchTerm,
...query._custom?.navidrome,
role: query.role || undefined,
@@ -335,14 +335,14 @@ export const NavidromeController: ControllerEndpoint = {
apiClientProps.server,
),
),
startIndex: query.startIndex,
offset: query.offset,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
},
getArtistListCount: async ({ apiClientProps, query }) =>
NavidromeController.getArtistList({
apiClientProps,
query: { ...query, limit: 1, startIndex: 0 },
query: { ...query, limit: 1, offset: 0 },
}).then((result) => result!.totalRecordCount!),
getDownloadUrl: SubsonicController.getDownloadUrl,
getGenreList: async (args) => {
@@ -350,10 +350,10 @@ export const NavidromeController: ControllerEndpoint = {
const res = await ndApiClient(apiClientProps).getGenreList({
query: {
_end: query.startIndex + (query.limit || 0),
_end: query.offset + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: genreListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
_start: query.offset,
name: query.searchTerm,
},
});
@@ -364,7 +364,7 @@ export const NavidromeController: ControllerEndpoint = {
return {
items: res.body.data.map((genre) => ndNormalize.genre(genre)),
startIndex: query.startIndex || 0,
offset: query.offset || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
},
@@ -400,10 +400,10 @@ export const NavidromeController: ControllerEndpoint = {
const res = await ndApiClient(apiClientProps).getPlaylistList({
query: {
_end: query.startIndex + (query.limit || 0),
_end: query.offset + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined,
_start: query.startIndex,
_start: query.offset,
q: query.searchTerm,
...customQuery,
},
@@ -415,14 +415,14 @@ export const NavidromeController: ControllerEndpoint = {
return {
items: res.body.data.map((item) => ndNormalize.playlist(item, apiClientProps.server)),
startIndex: query?.startIndex || 0,
offset: query?.offset || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
},
getPlaylistListCount: async ({ apiClientProps, query }) =>
NavidromeController.getPlaylistList({
apiClientProps,
query: { ...query, limit: 1, startIndex: 0 },
query: { ...query, limit: 1, offset: 0 },
}).then((result) => result!.totalRecordCount!),
getPlaylistSongList: async (
args: PlaylistSongListRequest,
@@ -450,7 +450,7 @@ export const NavidromeController: ControllerEndpoint = {
return {
items: res.body.data.map((item) => ndNormalize.song(item, apiClientProps.server)),
startIndex: query?.startIndex || 0,
offset: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
},
@@ -576,10 +576,10 @@ export const NavidromeController: ControllerEndpoint = {
const res = await ndApiClient(apiClientProps).getSongList({
query: {
_end: query.startIndex + (query.limit || -1),
_end: query.offset + (query.limit || -1),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: songListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
_start: query.offset,
album_artist_id: query.albumArtistIds,
album_id: query.albumIds,
artist_id: query.artistIds,
@@ -599,14 +599,14 @@ export const NavidromeController: ControllerEndpoint = {
items: res.body.data.map((song) =>
ndNormalize.song(song, apiClientProps.server, query.imageSize),
),
startIndex: query?.startIndex || 0,
offset: query?.offset || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
},
getSongListCount: async ({ apiClientProps, query }) =>
NavidromeController.getSongList({
apiClientProps,
query: { ...query, limit: 1, startIndex: 0 },
query: { ...query, limit: 1, offset: 0 },
}).then((result) => result!.totalRecordCount!),
getStructuredLyrics: SubsonicController.getStructuredLyrics,
getTags: async (args) => {
@@ -655,10 +655,10 @@ export const NavidromeController: ControllerEndpoint = {
const res = await ndApiClient(apiClientProps).getUserList({
query: {
_end: query.startIndex + (query.limit || 0),
_end: query.offset + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: userListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
_start: query.offset,
...query._custom?.navidrome,
},
});
@@ -669,7 +669,7 @@ export const NavidromeController: ControllerEndpoint = {
return {
items: res.body.data.map((user) => ndNormalize.user(user)),
startIndex: query?.startIndex || 0,
offset: query?.offset || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
},
@@ -269,7 +269,7 @@ export const SubsonicController: ControllerEndpoint = {
const res = await ssApiClient(apiClientProps).search3({
query: {
albumCount: query.limit,
albumOffset: query.startIndex,
albumOffset: query.offset,
artistCount: 0,
artistOffset: 0,
query: query.searchTerm || '',
@@ -289,7 +289,7 @@ export const SubsonicController: ControllerEndpoint = {
return {
items: results,
startIndex: query.startIndex,
offset: query.offset,
totalRecordCount: null,
};
}
@@ -323,7 +323,7 @@ export const SubsonicController: ControllerEndpoint = {
return {
items: sortAlbumList(items, query.sortBy, query.sortOrder),
startIndex: 0,
offset: 0,
totalRecordCount: albums.length,
};
}
@@ -346,7 +346,7 @@ export const SubsonicController: ControllerEndpoint = {
return {
items: sortAlbumList(results, query.sortBy, query.sortOrder),
startIndex: 0,
offset: 0,
totalRecordCount: res.body.starred?.album?.length || 0,
};
}
@@ -390,7 +390,7 @@ export const SubsonicController: ControllerEndpoint = {
fromYear,
genre: query.genres?.length ? query.genres[0] : undefined,
musicFolderId: query.musicFolderId,
offset: query.startIndex,
offset: query.offset,
size: query.limit,
toYear,
type,
@@ -406,7 +406,7 @@ export const SubsonicController: ControllerEndpoint = {
res.body.albumList2.album?.map((album) =>
normalize.album(album, apiClientProps.server, 300),
) || [],
startIndex: query.startIndex,
offset: query.offset,
totalRecordCount: null,
};
},
@@ -591,7 +591,7 @@ export const SubsonicController: ControllerEndpoint = {
return {
items: results,
startIndex: query.startIndex,
offset: query.offset,
totalRecordCount: results?.length || 0,
};
},
@@ -639,7 +639,7 @@ export const SubsonicController: ControllerEndpoint = {
return {
items: genres,
startIndex: 0,
offset: 0,
totalRecordCount: genres.length,
};
},
@@ -657,7 +657,7 @@ export const SubsonicController: ControllerEndpoint = {
id: folder.id.toString(),
name: folder.name,
})),
startIndex: 0,
offset: 0,
totalRecordCount: res.body.musicFolders.musicFolder.length,
};
},
@@ -720,7 +720,7 @@ export const SubsonicController: ControllerEndpoint = {
return {
items: results.map((playlist) => normalize.playlist(playlist, apiClientProps.server)),
startIndex: 0,
offset: 0,
totalRecordCount: results.length,
};
},
@@ -764,7 +764,7 @@ export const SubsonicController: ControllerEndpoint = {
return {
items: results,
startIndex: 0,
offset: 0,
totalRecordCount: results?.length || 0,
};
},
@@ -789,7 +789,7 @@ export const SubsonicController: ControllerEndpoint = {
return {
items: results.map((song) => normalize.song(song, apiClientProps.server)),
startIndex: 0,
offset: 0,
totalRecordCount: res.body.randomSongs?.song?.length || 0,
};
},
@@ -907,7 +907,7 @@ export const SubsonicController: ControllerEndpoint = {
artistOffset: 0,
query: query.searchTerm || '',
songCount: query.limit,
songOffset: query.startIndex,
songOffset: query.offset,
},
});
@@ -920,7 +920,7 @@ export const SubsonicController: ControllerEndpoint = {
res.body.searchResult3?.song?.map((song) =>
normalize.song(song, apiClientProps.server),
) || [],
startIndex: query.startIndex,
offset: query.offset,
totalRecordCount: null,
};
}
@@ -931,7 +931,7 @@ export const SubsonicController: ControllerEndpoint = {
count: query.limit,
genre: query.genreIds[0],
musicFolderId: query.musicFolderId,
offset: query.startIndex,
offset: query.offset,
},
});
@@ -943,7 +943,7 @@ export const SubsonicController: ControllerEndpoint = {
return {
items: results.map((song) => normalize.song(song, apiClientProps.server)) || [],
startIndex: 0,
offset: 0,
totalRecordCount: null,
};
}
@@ -966,7 +966,7 @@ export const SubsonicController: ControllerEndpoint = {
return {
items: sortSongList(results, query.sortBy, query.sortOrder),
startIndex: 0,
offset: 0,
totalRecordCount: (res.body.starred?.song || []).length || 0,
};
}
@@ -1036,7 +1036,7 @@ export const SubsonicController: ControllerEndpoint = {
return {
items: results.map((song) => normalize.song(song, apiClientProps.server)),
startIndex: 0,
offset: 0,
totalRecordCount: results.length,
};
}
@@ -1049,7 +1049,7 @@ export const SubsonicController: ControllerEndpoint = {
artistOffset: 0,
query: query.searchTerm || '',
songCount: query.limit,
songOffset: query.startIndex,
songOffset: query.offset,
},
});
@@ -1062,7 +1062,7 @@ export const SubsonicController: ControllerEndpoint = {
res.body.searchResult3?.song?.map((song) =>
normalize.song(song, apiClientProps.server),
) || [],
startIndex: 0,
offset: 0,
totalRecordCount: null,
};
},
@@ -1299,7 +1299,7 @@ export const SubsonicController: ControllerEndpoint = {
res.body.topSongs?.song?.map((song) =>
normalize.song(song, apiClientProps.server),
) || [],
startIndex: 0,
offset: 0,
totalRecordCount: res.body.topSongs?.song?.length || 0,
};
},
@@ -23,7 +23,10 @@ import { SetContextMenuItems, useHandleTableContextMenu } from '/@/renderer/feat
import { AppRoute } from '/@/renderer/router/routes';
import { PersistedTableColumn, useListStoreActions } from '/@/renderer/store';
import { ListKey, useListStoreByKey } from '/@/renderer/store/list.store';
import { BasePaginatedResponse, BaseQuery } from '/@/shared/types/adapter/api-controller-types';
import {
BasePaginatedResponse,
BasePaginatedQuery,
} from '/@/shared/types/adapter/api-controller-types';
import { ServerListItem } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import { ListDisplayType, TablePagination } from '/@/shared/types/types';
@@ -49,7 +52,7 @@ interface UseAgGridProps<TFilter> {
const BLOCK_SIZE = 500;
export const useVirtualTable = <TFilter extends BaseQuery<any>>({
export const useVirtualTable = <TFilter extends BasePaginatedQuery<any>>({
columnType,
contextMenu,
customFilters,
@@ -150,8 +150,8 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
const artistQuery = useAlbumList({
options: {
cacheTime: 1000 * 60,
enabled: detailQuery?.data?.albumArtists[0]?.id !== undefined,
gcTime: 1000 * 60,
keepPreviousData: true,
staleTime: 1000 * 60,
},
@@ -165,9 +165,9 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
? [detailQuery?.data?.albumArtists[0].id]
: undefined,
limit: 15,
offset: 0,
sortBy: AlbumListSort.YEAR,
sortOrder: ListSortOrder.DESC,
startIndex: 0,
},
serverId: server?.id,
});
@@ -175,15 +175,15 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
const relatedAlbumGenresRequest: AlbumListQuery = {
genres: detailQuery.data?.genres.length ? [detailQuery.data.genres[0].id] : undefined,
limit: 15,
offset: 0,
sortBy: AlbumListSort.RANDOM,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
};
const relatedAlbumGenresQuery = useAlbumList({
options: {
cacheTime: 1000 * 60,
enabled: !!detailQuery?.data?.genres?.[0],
gcTime: 1000 * 60,
queryKey: queryKeys.albums.related(
server?.id || '',
albumId,
@@ -137,15 +137,11 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
const itemData: Album[] = [];
for (const [, data] of queriesFromCache) {
const { items, startIndex } = data || {};
const { items, offset } = data || {};
if (items && items.length !== 1 && startIndex !== undefined) {
if (items && items.length !== 1 && offset !== undefined) {
let itemIndex = 0;
for (
let rowIndex = startIndex;
rowIndex < startIndex + items.length;
rowIndex += 1
) {
for (let rowIndex = offset; rowIndex < offset + items.length; rowIndex += 1) {
itemData[rowIndex] = items[itemIndex];
itemIndex += 1;
}
@@ -165,7 +161,7 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
limit: take,
...filter,
...customFilters,
startIndex: skip,
offset: skip,
};
const queryKey = queryKeys.albums.list(server?.id || '', query, id);
@@ -41,14 +41,14 @@ export const JellyfinAlbumFilters = ({
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
const genreListQuery = useGenreList({
options: {
cacheTime: 1000 * 60 * 2,
gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: {
musicFolderId: filter?.musicFolderId,
sortBy: GenreListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
serverId,
});
@@ -63,7 +63,7 @@ export const JellyfinAlbumFilters = ({
const tagsQuery = useTagList({
options: {
cacheTime: 1000 * 60 * 2,
gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: {
@@ -170,7 +170,7 @@ export const JellyfinAlbumFilters = ({
const albumArtistListQuery = useAlbumArtistList({
options: {
cacheTime: 1000 * 60 * 2,
gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: {
@@ -42,13 +42,13 @@ export const NavidromeAlbumFilters = ({
const genreListQuery = useGenreList({
options: {
cacheTime: 1000 * 60 * 2,
gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: {
sortBy: GenreListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
serverId,
});
@@ -76,7 +76,7 @@ export const NavidromeAlbumFilters = ({
const tagsQuery = useTagList({
options: {
cacheTime: 1000 * 60 * 2,
gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: {
@@ -185,7 +185,7 @@ export const NavidromeAlbumFilters = ({
const albumArtistListQuery = useAlbumArtistList({
options: {
cacheTime: 1000 * 60 * 2,
gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: {
@@ -39,7 +39,7 @@ export const SubsonicAlbumFilters = ({
const albumArtistListQuery = useAlbumArtistList({
options: {
cacheTime: 1000 * 60 * 2,
gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: {
@@ -72,13 +72,13 @@ export const SubsonicAlbumFilters = ({
const genreListQuery = useGenreList({
options: {
cacheTime: 1000 * 60 * 2,
gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: {
sortBy: GenreListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
serverId,
});
@@ -4,12 +4,12 @@ import { useQuery } from '@tanstack/react-query';
import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { AlbumDetailQuery } from '/@/shared/types/domain/album-domain-types';
export const useAlbumDetail = (args: QueryHookArgs<AlbumDetailQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
queryFn: ({ signal }) => {
@@ -4,12 +4,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { AlbumListQuery } from '/@/shared/types/domain/album-domain-types';
export const useAlbumListCount = (args: QueryHookArgs<AlbumListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!serverId,
@@ -5,12 +5,12 @@ import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { AlbumListQuery, AlbumListResponse } from '/@/shared/types/domain/album-domain-types';
export const useAlbumList = (args: QueryHookArgs<AlbumListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!serverId,
@@ -35,7 +35,7 @@ export const useAlbumList = (args: QueryHookArgs<AlbumListQuery>) => {
export const useAlbumListInfinite = (args: QueryHookArgs<AlbumListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
const server = useServerById(serverId);
return useInfiniteQuery({
enabled: !!serverId,
@@ -57,7 +57,7 @@ export const useAlbumListInfinite = (args: QueryHookArgs<AlbumListQuery>) => {
query: {
...query,
limit: query.limit || 50,
startIndex: pageParam * (query.limit || 50),
offset: pageParam * (query.limit || 50),
},
});
},
@@ -52,13 +52,13 @@ const AlbumListRoute = () => {
const genreList = useGenreList({
options: {
cacheTime: 1000 * 60 * 60,
gcTime: 1000 * 60 * 60,
enabled: !!genreId,
},
query: {
sortBy: GenreListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
});
@@ -74,7 +74,7 @@ const AlbumListRoute = () => {
const itemCountCheck = useAlbumListCount({
options: {
cacheTime: 1000 * 60,
gcTime: 1000 * 60,
staleTime: 1000 * 60,
},
query: {
@@ -102,7 +102,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
limit: 15,
sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: ListSortOrder.DESC,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
});
@@ -117,7 +117,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
limit: 15,
sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: ListSortOrder.DESC,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
});
@@ -55,15 +55,11 @@ export const ArtistListGridView = ({ gridRef, itemCount }: ArtistListGridViewPro
const itemData: AlbumArtist[] = [];
for (const [, data] of queriesFromCache) {
const { items, startIndex } = data || {};
const { items, offset } = data || {};
if (items && items.length !== 1 && startIndex !== undefined) {
if (items && items.length !== 1 && offset !== undefined) {
let itemIndex = 0;
for (
let rowIndex = startIndex;
rowIndex < startIndex + items.length;
rowIndex += 1
) {
for (let rowIndex = offset; rowIndex < offset + items.length; rowIndex += 1) {
itemData[rowIndex] = items[itemIndex];
itemIndex += 1;
}
@@ -78,7 +74,7 @@ export const ArtistListGridView = ({ gridRef, itemCount }: ArtistListGridViewPro
const query: ArtistListQuery = {
...filter,
limit,
startIndex,
offset,
};
const queryKey = queryKeys.artists.list(server?.id || '', query);
@@ -140,7 +140,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
const cq = useContainerQuery();
const roles = useRoles({
options: {
cacheTime: 1000 * 60 * 60 * 2,
gcTime: 1000 * 60 * 60 * 2,
staleTime: 1000 * 60 * 60 * 2,
},
query: {},
@@ -188,7 +188,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
},
query: {
limit,
startIndex,
offset,
...filters,
},
}),
@@ -224,7 +224,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
},
query: {
limit,
startIndex,
offset,
...filters,
},
}),
@@ -3,12 +3,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { AlbumArtistDetailQuery } from '/@/shared/types/domain/artist-domain-types';
export const useAlbumArtistDetail = (args: QueryHookArgs<AlbumArtistDetailQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!server?.id && !!query.id,
@@ -3,12 +3,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { AlbumArtistListQuery } from '/@/shared/types/domain/artist-domain-types';
export const useAlbumArtistListCount = (args: QueryHookArgs<AlbumArtistListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!serverId,
@@ -3,12 +3,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { AlbumArtistListQuery } from '/@/shared/types/domain/artist-domain-types';
export const useAlbumArtistList = (args: QueryHookArgs<AlbumArtistListQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!server?.id,
@@ -3,12 +3,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { AlbumArtistDetailQuery } from '/@/shared/types/domain/artist-domain-types';
export const useAlbumArtistInfo = (args: QueryHookArgs<AlbumArtistDetailQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!server?.id && !!query.id,
@@ -3,12 +3,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { ArtistListQuery } from '/@/shared/types/domain/artist-domain-types';
export const useArtistListCount = (args: QueryHookArgs<ArtistListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!serverId,
@@ -3,11 +3,11 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
export const useRoles = (args: QueryHookArgs<object>) => {
const { options, serverId } = args;
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!serverId,
@@ -4,12 +4,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { TopSongListQuery } from '/@/shared/types/domain/song-domain-types';
export const useTopSongsList = (args: QueryHookArgs<TopSongListQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!server?.id,
@@ -23,7 +23,7 @@ const AlbumArtistListRoute = () => {
const itemCountCheck = useAlbumArtistListCount({
options: {
cacheTime: 1000 * 60,
gcTime: 1000 * 60,
staleTime: 1000 * 60,
},
query: albumArtistListFilter,
@@ -23,7 +23,7 @@ const ArtistListRoute = () => {
const itemCountCheck = useArtistListCount({
options: {
cacheTime: 1000 * 60,
gcTime: 1000 * 60,
staleTime: 1000 * 60,
},
query: artistListFilter,
@@ -37,7 +37,7 @@ import { useRemoveFromPlaylist } from '/@/renderer/features/playlists/mutations/
import { useCreateFavorite, useDeleteFavorite, useSetRating } from '/@/renderer/features/shared';
import { AppRoute } from '/@/renderer/router/routes';
import {
getServerById,
useServerById,
useAuthStore,
useCurrentServer,
usePlayerStore,
@@ -712,7 +712,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
const item = ctx.data[0];
const songs = await controller.getSimilarSongs({
apiClientProps: {
server: getServerById(item.serverId),
server: useServerById(item.serverId),
signal: undefined,
},
query: { albumArtistIds: item.albumArtistIds, songId: item.id },
@@ -5,15 +5,15 @@ import { useCallback, useEffect, useState } from 'react';
import { controller } from '/@/renderer/api/controller';
import {
DiscordDisplayType,
getServerById,
useAppStore,
useDiscordSettings,
useServerById,
useGeneralSettings,
usePlayerStore,
} from '/@/renderer/store';
import { QueueSong } from '/@/shared/types/domain/player-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { PlayerStatus, PlayerStatus } from '/@/shared/types/types';
import { PlayerStatus } from '/@/shared/types/types';
const discordRpc = isElectron() ? window.api.discordRpc : null;
@@ -93,7 +93,7 @@ export const useDiscordRpc = () => {
if (song.serverType === ServerType.JELLYFIN && song.imageUrl) {
activity.largeImageKey = song.imageUrl;
} else if (song.serverType === ServerType.NAVIDROME) {
const server = getServerById(song.serverId);
const server = useServerById(song.serverId);
try {
const info = await controller.getAlbumInfo({
@@ -74,15 +74,11 @@ export const GenreListGridView = ({ gridRef, itemCount }: any) => {
const itemData: Genre[] = [];
for (const [, data] of queriesFromCache) {
const { items, startIndex } = data || {};
const { items, offset } = data || {};
if (items && items.length !== 1 && startIndex !== undefined) {
if (items && items.length !== 1 && offset !== undefined) {
let itemIndex = 0;
for (
let rowIndex = startIndex;
rowIndex < startIndex + items.length;
rowIndex += 1
) {
for (let rowIndex = offset; rowIndex < offset + items.length; rowIndex += 1) {
itemData[rowIndex] = items[itemIndex];
itemIndex += 1;
}
@@ -101,7 +97,7 @@ export const GenreListGridView = ({ gridRef, itemCount }: any) => {
const query: GenreListQuery = {
...filter,
limit: take,
startIndex: skip,
offset: skip,
};
const queryKey = queryKeys.albums.list(server?.id || '', query);
@@ -4,12 +4,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { GenreListQuery } from '/@/shared/types/domain/genre-domain-types';
export const useGenreList = (args: QueryHookArgs<GenreListQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!server,
@@ -23,7 +23,7 @@ const GenreListRoute = () => {
query: {
...filter,
limit: 1,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
});
@@ -3,19 +3,19 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { AlbumListQuery, AlbumListSort } from '/@/shared/types/domain/album-domain-types';
import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
export const useRecentlyPlayed = (args: QueryHookArgs<Partial<AlbumListQuery>>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
const server = useServerById(serverId);
const requestQuery: AlbumListQuery = {
limit: 5,
sortBy: AlbumListSort.RECENTLY_PLAYED,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
...query,
};
@@ -40,7 +40,7 @@ const HomeRoute = () => {
const feature = useAlbumList({
options: {
cacheTime: 1000 * 60,
gcTime: 1000 * 60,
enabled: homeFeature,
staleTime: 1000 * 60,
},
@@ -48,7 +48,7 @@ const HomeRoute = () => {
limit: 20,
sortBy: AlbumListSort.RANDOM,
sortOrder: ListSortOrder.DESC,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
});
@@ -65,7 +65,7 @@ const HomeRoute = () => {
limit: itemsPerPage,
sortBy: AlbumListSort.RANDOM,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
});
@@ -78,7 +78,7 @@ const HomeRoute = () => {
limit: itemsPerPage,
sortBy: AlbumListSort.RECENTLY_PLAYED,
sortOrder: ListSortOrder.DESC,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
});
@@ -91,7 +91,7 @@ const HomeRoute = () => {
limit: itemsPerPage,
sortBy: AlbumListSort.RECENTLY_ADDED,
sortOrder: ListSortOrder.DESC,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
});
@@ -105,7 +105,7 @@ const HomeRoute = () => {
limit: itemsPerPage,
sortBy: AlbumListSort.PLAY_COUNT,
sortOrder: ListSortOrder.DESC,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
});
@@ -120,7 +120,7 @@ const HomeRoute = () => {
limit: itemsPerPage,
sortBy: SongListSort.PLAY_COUNT,
sortOrder: ListSortOrder.DESC,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
},
@@ -4,7 +4,7 @@ import isElectron from 'is-electron';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById, useLyricsSettings } from '/@/renderer/store';
import { useServerById, useLyricsSettings } from '/@/renderer/store';
import { hasFeature } from '/@/shared/api/utils';
import {
FullLyricsMetadata,
@@ -64,7 +64,7 @@ export const useServerLyrics = (
args: QueryHookArgs<LyricsQuery>,
): UseQueryResult<null | string> => {
const { query, serverId } = args;
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
// Note: This currently fetches for every song, even if it shouldn't have
@@ -86,7 +86,7 @@ export const useSongLyricsBySong = (
): UseQueryResult<FullLyricsMetadata | StructuredLyric[]> => {
const { query } = args;
const { fetch, preferLocalLyrics } = useLyricsSettings();
const server = getServerById(song?.serverId);
const server = useServerById(song?.serverId);
return useQuery({
cacheTime: Infinity,
@@ -258,7 +258,7 @@ export const openShuffleAllModal = async (
query: {
sortBy: GenreListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
}),
queryKey: queryKeys.genres.list(server?.id),
@@ -3,7 +3,7 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { MutationOptions } from '/@/renderer/lib/react-query';
import { getServerById, useIncrementQueuePlayCount } from '/@/renderer/store';
import { useServerById, useIncrementQueuePlayCount } from '/@/renderer/store';
import { usePlayEvent } from '/@/renderer/store/event.store';
import { ScrobbleRequest, ScrobbleResponse } from '/@/shared/types/domain/user-domain-types';
@@ -18,7 +18,7 @@ export const useSendScrobble = (options?: MutationOptions) => {
null
>({
mutationFn: (args) => {
const server = getServerById(args.serverId);
const server = useServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.scrobble({ ...args, apiClientProps: { server } });
},
+7 -7
View File
@@ -62,7 +62,7 @@ export const getAlbumSongsById = async (args: {
albumIds: id,
sortBy: SongListSort.ALBUM,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
...query,
};
@@ -98,7 +98,7 @@ export const getGenreSongsById = async (args: {
const data: SongListResponse = {
items: [],
startIndex: 0,
offset: 0,
totalRecordCount: 0,
};
for (const genreId of id) {
@@ -106,7 +106,7 @@ export const getGenreSongsById = async (args: {
genreIds: [genreId],
sortBy: SongListSort.GENRE,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
...query,
};
@@ -150,7 +150,7 @@ export const getAlbumArtistSongsById = async (args: {
albumArtistIds: id || [],
sortBy: SongListSort.ALBUM_ARTIST,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
...query,
};
@@ -187,7 +187,7 @@ export const getArtistSongsById = async (args: {
artistIds: id,
sortBy: SongListSort.ALBUM,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
...query,
};
@@ -222,7 +222,7 @@ export const getSongsByQuery = async (args: {
const queryFilter: SongListQuery = {
sortBy: SongListSort.ALBUM,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
...query,
};
@@ -279,7 +279,7 @@ export const getSongById = async (args: {
return {
items: [res],
startIndex: 0,
offset: 0,
totalRecordCount: 1,
};
};
@@ -46,7 +46,7 @@ export const AddToPlaylistContextModal = ({
},
sortBy: PlaylistListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
});
@@ -72,7 +72,7 @@ export const AddToPlaylistContextModal = ({
albumIds: [albumId],
sortBy: SongListSort.ALBUM,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
};
const queryKey = queryKeys.songs.list(server?.id || '', query);
@@ -90,7 +90,7 @@ export const AddToPlaylistContextModal = ({
artistIds: [artistId],
sortBy: SongListSort.ARTIST,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
};
const queryKey = queryKeys.songs.list(server?.id || '', query);
@@ -88,15 +88,11 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
const itemData: Playlist[] = [];
for (const [, data] of queriesFromCache) {
const { items, startIndex } = data || {};
const { items, offset } = data || {};
if (items && items.length !== 1 && startIndex !== undefined) {
if (items && items.length !== 1 && offset !== undefined) {
let itemIndex = 0;
for (
let rowIndex = startIndex;
rowIndex < startIndex + items.length;
rowIndex += 1
) {
for (let rowIndex = offset; rowIndex < offset + items.length; rowIndex += 1) {
itemData[rowIndex] = items[itemIndex];
itemIndex += 1;
}
@@ -116,7 +112,7 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
limit: take,
...filter,
_custom: {},
startIndex: skip,
offset: skip,
};
const queryKey = queryKeys.playlists.list(server?.id || '', query);
@@ -160,7 +160,7 @@ export const PlaylistListHeaderFilters = ({
},
},
limit: take,
startIndex: skip,
offset: skip,
...filters,
};
@@ -213,7 +213,7 @@ export const PlaylistListHeaderFilters = ({
},
query: {
limit,
startIndex,
offset,
...pageFilters,
},
}),
@@ -111,7 +111,7 @@ export const PlaylistQueryBuilder = forwardRef(
);
const { data: playlists } = usePlaylistList({
query: { sortBy: PlaylistListSort.NAME, sortOrder: ListSortOrder.ASC, startIndex: 0 },
query: { sortBy: PlaylistListSort.NAME, sortOrder: ListSortOrder.ASC, offset: 0 },
serverId: server?.id,
});
@@ -167,7 +167,7 @@ export const openUpdatePlaylistModal = async (args: {
const query: UserListQuery = {
sortBy: UserListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
};
if (!server) return;
@@ -4,7 +4,7 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import {
AddToPlaylistArgs,
AddToPlaylistResponse,
@@ -21,7 +21,7 @@ export const useAddToPlaylist = (args: MutationHookArgs) => {
null
>({
mutationFn: (args) => {
const server = getServerById(args.serverId);
const server = useServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.addToPlaylist({ ...args, apiClientProps: { server } });
},
@@ -4,7 +4,7 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { CreatePlaylistResponse } from '/@/shared/types/domain/playlist-domain-types';
export const useCreatePlaylist = (args: MutationHookArgs) => {
@@ -18,12 +18,12 @@ export const useCreatePlaylist = (args: MutationHookArgs) => {
null
>({
mutationFn: (args) => {
const server = getServerById(args.serverId);
const server = useServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.createPlaylist({ ...args, apiClientProps: { server } });
},
onSuccess: (_args, variables) => {
const server = getServerById(variables.serverId);
const server = useServerById(variables.serverId);
if (server) {
queryClient.invalidateQueries(queryKeys.playlists.list(server.id));
}
@@ -4,7 +4,7 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { getServerById, useCurrentServer } from '/@/renderer/store';
import { useServerById, useCurrentServer } from '/@/renderer/store';
import { DeletePlaylistResponse } from '/@/shared/types/domain/playlist-domain-types';
export const useDeletePlaylist = (args: MutationHookArgs) => {
@@ -19,7 +19,7 @@ export const useDeletePlaylist = (args: MutationHookArgs) => {
null
>({
mutationFn: (args) => {
const server = getServerById(args.serverId);
const server = useServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.deletePlaylist({ ...args, apiClientProps: { server } });
},
@@ -4,7 +4,7 @@ import { AxiosError, AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationOptions } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { RemoveFromPlaylistResponse } from '/@/shared/types/domain/playlist-domain-types';
export const useRemoveFromPlaylist = (options?: MutationOptions) => {
@@ -17,7 +17,7 @@ export const useRemoveFromPlaylist = (options?: MutationOptions) => {
null
>({
mutationFn: (args) => {
const server = getServerById(args.serverId);
const server = useServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.removeFromPlaylist({ ...args, apiClientProps: { server } });
},
@@ -4,7 +4,7 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { UpdatePlaylistResponse } from '/@/shared/types/domain/playlist-domain-types';
export const useUpdatePlaylist = (args: MutationHookArgs) => {
@@ -18,7 +18,7 @@ export const useUpdatePlaylist = (args: MutationHookArgs) => {
null
>({
mutationFn: (args) => {
const server = getServerById(args.serverId);
const server = useServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.updatePlaylist({ ...args, apiClientProps: { server } });
},
@@ -4,12 +4,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { PlaylistDetailQuery } from '/@/shared/types/domain/playlist-domain-types';
export const usePlaylistDetail = (args: QueryHookArgs<PlaylistDetailQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!server?.id,
@@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { PlaylistListQuery } from '/@/shared/types/domain/playlist-domain-types';
export const usePlaylistList = (args: {
@@ -13,7 +13,7 @@ export const usePlaylistList = (args: {
serverId?: string;
}) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
cacheTime: 1000 * 60 * 60,
@@ -4,12 +4,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { PlaylistSongListQuery } from '/@/shared/types/domain/playlist-domain-types';
export const usePlaylistSongList = (args: QueryHookArgs<PlaylistSongListQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!server,
@@ -26,7 +26,7 @@ const PlaylistListRoute = () => {
const itemCountCheck = usePlaylistList({
options: {
cacheTime: 1000 * 60 * 60 * 2,
gcTime: 1000 * 60 * 60 * 2,
staleTime: 1000 * 60 * 60 * 2,
},
query: {
@@ -34,7 +34,7 @@ const PlaylistListRoute = () => {
limit: 1,
sortBy: PlaylistListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
});
@@ -3,12 +3,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { SearchQuery } from '/@/shared/types/domain/search-domain-types';
export const useSearch = (args: QueryHookArgs<SearchQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!serverId,
@@ -6,7 +6,8 @@ import { nanoid } from 'nanoid/non-secure';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { api } from '/@/renderer/api';
import { useSignIn } from '../mutations/sign-in-mutation';
import JellyfinIcon from '/@/renderer/features/servers/assets/jellyfin.png';
import NavidromeIcon from '/@/renderer/features/servers/assets/navidrome.png';
import SubsonicIcon from '/@/renderer/features/servers/assets/opensubsonic.png';
@@ -20,7 +21,6 @@ import { Stack } from '/@/shared/components/stack/stack';
import { TextInput } from '/@/shared/components/text-input/text-input';
import { Text } from '/@/shared/components/text/text';
import { toast } from '/@/shared/components/toast/toast';
import { AuthenticationResponse } from '/@/shared/types/domain/auth-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { toServerType } from '/@/shared/types/types';
@@ -59,6 +59,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
const focusTrapRef = useFocusTrap(true);
const [isLoading, setIsLoading] = useState(false);
const { addServer, setCurrentServer } = useAuthStoreActions();
const { isPending, mutateAsync: signIn } = useSignIn();
const form = useForm({
initialValues: {
@@ -87,7 +88,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
const isSubmitDisabled = !form.values.name || !form.values.url || !form.values.username;
const handleSubmit = form.onSubmit(async (values) => {
const authFunction = api.controller.authenticate;
const authFunction = signIn;
if (!authFunction) {
return toast.error({
@@ -97,35 +98,33 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
try {
setIsLoading(true);
const data: AuthenticationResponse | undefined = await authFunction(
values.url,
{
legacy: values.legacyAuth,
password: values.password,
username: values.username,
const [err, response] = await signIn({
request: {
body: {
password: values.password,
username: values.username,
},
url: values.url,
},
values.type as ServerType,
);
serverType: values.type as ServerType,
});
if (!data) {
if (err || !response) {
return toast.error({
message: t('error.authenticationFailed', { postProcess: 'sentenceCase' }),
});
}
const serverItem = {
credential: data.credential,
credential: response.credential,
id: nanoid(),
name: values.name,
ndCredential: data.ndCredential,
type: values.type as ServerType,
url: values.url.replace(/\/$/, ''),
userId: data.userId,
username: data.username,
username: response.username,
};
addServer(serverItem);
setCurrentServer(serverItem);
closeAllModals();
toast.success({
@@ -0,0 +1,16 @@
import { useMutation } from '@tanstack/react-query';
import { api } from '/@/renderer/api/api-controller';
import { AuthenticationRequest } from '/@/shared/types/domain/auth-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
export function useSignIn() {
const mutation = useMutation({
mutationFn: (args: { request: AuthenticationRequest; serverType: ServerType }) => {
const result = api.authenticate(args.serverType)(args.request);
return result;
},
});
return mutation;
}
@@ -5,10 +5,10 @@ import isElectron from 'is-electron';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { getServerById, useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store';
import { useServerById, useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store';
import { useFavoriteEvent } from '/@/renderer/store/event.store';
import { AlbumDetailResponse } from '/@/shared/types/domain/album-domain-types';
import { AlbumArtistDetailResponse } from '/@/shared/types/domain/artist-domain-types';
import { ArtistDetailResponse } from '/@/shared/types/domain/artist-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import { FavoriteResponse } from '/@/shared/types/domain/user-domain-types';
@@ -28,7 +28,7 @@ export const useCreateFavorite = (args: MutationHookArgs) => {
null
>({
mutationFn: (args) => {
const server = getServerById(args.serverId);
const server = useServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.createFavorite({ ...args, apiClientProps: { server } });
},
@@ -72,10 +72,10 @@ export const useCreateFavorite = (args: MutationHookArgs) => {
id: variables.query.id[0],
});
const previous = queryClient.getQueryData<AlbumArtistDetailResponse>(queryKey);
const previous = queryClient.getQueryData<ArtistDetailResponse>(queryKey);
if (previous) {
queryClient.setQueryData<AlbumArtistDetailResponse>(queryKey, {
queryClient.setQueryData<ArtistDetailResponse>(queryKey, {
...previous,
userFavorite: true,
});
@@ -5,10 +5,10 @@ import isElectron from 'is-electron';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { getServerById, useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store';
import { useServerById, useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store';
import { useFavoriteEvent } from '/@/renderer/store/event.store';
import { AlbumDetailResponse } from '/@/shared/types/domain/album-domain-types';
import { AlbumArtistDetailResponse } from '/@/shared/types/domain/artist-domain-types';
import { ArtistDetailResponse } from '/@/shared/types/domain/artist-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import { FavoriteResponse } from '/@/shared/types/domain/user-domain-types';
@@ -28,7 +28,7 @@ export const useDeleteFavorite = (args: MutationHookArgs) => {
null
>({
mutationFn: (args) => {
const server = getServerById(args.serverId);
const server = useServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.deleteFavorite({ ...args, apiClientProps: { server } });
},
@@ -71,10 +71,10 @@ export const useDeleteFavorite = (args: MutationHookArgs) => {
const queryKey = queryKeys.albumArtists.detail(serverId, {
id: variables.query.id[0],
});
const previous = queryClient.getQueryData<AlbumArtistDetailResponse>(queryKey);
const previous = queryClient.getQueryData<ArtistDetailResponse>(queryKey);
if (previous) {
queryClient.setQueryData<AlbumArtistDetailResponse>(queryKey, {
queryClient.setQueryData<ArtistDetailResponse>(queryKey, {
...previous,
userFavorite: false,
});
@@ -5,10 +5,10 @@ import isElectron from 'is-electron';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { getServerById, useSetAlbumListItemDataById, useSetQueueRating } from '/@/renderer/store';
import { useServerById, useSetAlbumListItemDataById, useSetQueueRating } from '/@/renderer/store';
import { useRatingEvent } from '/@/renderer/store/event.store';
import { Album, AlbumDetailResponse } from '/@/shared/types/domain/album-domain-types';
import { AlbumArtistDetailResponse } from '/@/shared/types/domain/artist-domain-types';
import { ArtistDetailResponse } from '/@/shared/types/domain/artist-domain-types';
import { AnyLibraryItems, LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import { RatingResponse, SetRatingRequest } from '/@/shared/types/domain/user-domain-types';
@@ -28,7 +28,7 @@ export const useSetRating = (args: MutationHookArgs) => {
{ previous: undefined | { items: AnyLibraryItems } }
>({
mutationFn: (args) => {
const server = getServerById(args.serverId);
const server = useServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.setRating({ ...args, apiClientProps: { server } });
},
@@ -104,9 +104,9 @@ export const useSetRating = (args: MutationHookArgs) => {
const queryKey = queryKeys.albumArtists.detail(serverId || '', {
id: albumArtistId,
});
const previous = queryClient.getQueryData<AlbumArtistDetailResponse>(queryKey);
const previous = queryClient.getQueryData<ArtistDetailResponse>(queryKey);
if (previous) {
queryClient.setQueryData<AlbumArtistDetailResponse>(queryKey, {
queryClient.setQueryData<ArtistDetailResponse>(queryKey, {
...previous,
userRating: variables.query.rating,
});
@@ -3,12 +3,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { ServerMusicFolderListQuery } from '/@/shared/types/domain/server-domain-types';
export const useMusicFolders = (args: QueryHookArgs<ServerMusicFolderListQuery>) => {
const { options, serverId } = args || {};
const server = getServerById(serverId);
const server = useServerById(serverId);
const query = useQuery({
enabled: !!server,
@@ -3,7 +3,7 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { AnyLibraryItems } from '/@/shared/types/domain/shared-domain-types';
import { ShareItemRequest, ShareItemResponse } from '/@/shared/types/domain/user-domain-types';
@@ -17,7 +17,7 @@ export const useShareItem = (args: MutationHookArgs) => {
{ previous: undefined | { items: AnyLibraryItems } }
>({
mutationFn: (args) => {
const server = getServerById(args.serverId);
const server = useServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.shareItem({ ...args, apiClientProps: { server } });
},
@@ -142,7 +142,7 @@ export const SidebarPlaylistList = () => {
query: {
sortBy: PlaylistListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
});
@@ -258,7 +258,7 @@ export const SidebarSharedPlaylistList = () => {
query: {
sortBy: PlaylistListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
});
@@ -29,7 +29,7 @@ export const SimilarSongsList = ({ count, fullScreen, song }: SimilarSongsListPr
const songQuery = useSimilarSongs({
options: {
cacheTime: 1000 * 60 * 2,
gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: { albumArtistIds: song.albumArtists.map((art) => art.id), count, songId: song.id },
@@ -3,12 +3,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { SimilarSongsQuery } from '/@/shared/types/domain/song-domain-types';
export const useSimilarSongs = (args: QueryHookArgs<SimilarSongsQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!server,
@@ -42,7 +42,7 @@ export const JellyfinSongFilters = ({
musicFolderId: filter?.musicFolderId,
sortBy: GenreListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
serverId,
});
@@ -40,7 +40,7 @@ export const NavidromeSongFilters = ({
query: {
sortBy: GenreListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
serverId,
});
@@ -148,15 +148,11 @@ export const SongListGridView = ({ gridRef, itemCount }: SongListGridViewProps)
const itemData: Song[] = [];
for (const [, data] of queriesFromCache) {
const { items, startIndex } = data || {};
const { items, offset } = data || {};
if (items && items.length !== 1 && startIndex !== undefined) {
if (items && items.length !== 1 && offset !== undefined) {
let itemIndex = 0;
for (
let rowIndex = startIndex;
rowIndex < startIndex + items.length;
rowIndex += 1
) {
for (let rowIndex = offset; rowIndex < offset + items.length; rowIndex += 1) {
itemData[rowIndex] = items[itemIndex];
itemIndex += 1;
}
@@ -177,7 +173,7 @@ export const SongListGridView = ({ gridRef, itemCount }: SongListGridViewProps)
limit: take,
...filter,
...customFilters,
startIndex: skip,
offset: skip,
};
const queryKey = queryKeys.songs.list(server?.id || '', query, id);
@@ -37,7 +37,7 @@ export const SubsonicSongFilters = ({
query: {
sortBy: GenreListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
serverId,
});
@@ -4,12 +4,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { SongListQuery } from '/@/shared/types/domain/song-domain-types';
export const useSongListCount = (args: QueryHookArgs<SongListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!serverId,
@@ -4,12 +4,12 @@ import { useQuery } from '@tanstack/react-query';
import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { SongListQuery } from '/@/shared/types/domain/song-domain-types';
export const useSongList = (args: QueryHookArgs<SongListQuery>, imageSize?: number) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!server?.id,
@@ -50,13 +50,13 @@ const TrackListRoute = () => {
const genreList = useGenreList({
options: {
cacheTime: 1000 * 60 * 60,
gcTime: 1000 * 60 * 60,
enabled: !!genreId,
},
query: {
sortBy: GenreListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
serverId: server?.id,
});
@@ -72,7 +72,7 @@ const TrackListRoute = () => {
const itemCountCheck = useSongListCount({
options: {
cacheTime: 1000 * 60,
gcTime: 1000 * 60,
staleTime: 1000 * 60,
},
query: songListFilter,
@@ -85,7 +85,7 @@ const TrackListRoute = () => {
async (args: { initialSongId?: string; playType: Play }) => {
if (!itemCount || itemCount === 0) return;
const { initialSongId, playType } = args;
const query: SongListQuery = { ...songListFilter, limit: itemCount, startIndex: 0 };
const query: SongListQuery = { ...songListFilter, limit: itemCount, offset: 0 };
if (albumArtistId) {
handlePlayQueueAdd?.({
@@ -3,14 +3,14 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { hasFeature } from '/@/shared/api/utils';
import { ServerFeature } from '/@/shared/types/domain/server-domain-types';
import { TagQuery } from '/@/shared/types/domain/tag-domain-types';
export const useTagList = (args: QueryHookArgs<TagQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!server && hasFeature(server, ServerFeature.TAGS),
@@ -4,12 +4,12 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
import { useServerById } from '/@/renderer/store';
import { UserListQuery } from '/@/shared/types/domain/user-domain-types';
export const useUserList = (args: QueryHookArgs<UserListQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
const server = useServerById(serverId);
return useQuery({
enabled: !!server,
@@ -6,9 +6,9 @@ import { api } from '/@/renderer/api';
import { useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
import { toast } from '/@/shared/components/toast/toast';
import { ServerListItem, ServerType } from '/@/shared/types/domain/server-domain-types';
import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { SongListSort } from '/@/shared/types/domain/song-domain-types';
import { AuthState } from '/@/shared/types/types';
import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
const localSettings = isElectron() ? window.api.localSettings : null;
@@ -33,7 +33,7 @@ export const useServerAuthenticated = () => {
limit: 1,
sortBy: SongListSort.NAME,
sortOrder: ListSortOrder.ASC,
startIndex: 0,
offset: 0,
},
});
+5 -13
View File
@@ -22,16 +22,12 @@ const queryConfig: DefaultOptions = {
retry: process.env.NODE_ENV === 'production',
},
queries: {
cacheTime: 1000 * 60 * 3,
onError: (err) => {
console.error('react query error:', err);
},
gcTime: 1000 * 60 * 3,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
retry: process.env.NODE_ENV === 'production',
staleTime: 1000 * 5,
useErrorBoundary: (error: any) => {
return error?.response?.status >= 500;
},
},
};
@@ -41,9 +37,8 @@ export const queryClient = new QueryClient({
});
export type InfiniteQueryOptions = {
cacheTime?: UseInfiniteQueryOptions['cacheTime'];
enabled?: UseInfiniteQueryOptions['enabled'];
keepPreviousData?: UseInfiniteQueryOptions['keepPreviousData'];
gcTime?: UseInfiniteQueryOptions['gcTime'];
meta?: UseInfiniteQueryOptions['meta'];
onError?: (err: any) => void;
onSettled?: any;
@@ -55,7 +50,6 @@ export type InfiniteQueryOptions = {
retry?: UseInfiniteQueryOptions['retry'];
retryDelay?: UseInfiniteQueryOptions['retryDelay'];
staleTime?: UseInfiniteQueryOptions['staleTime'];
suspense?: UseInfiniteQueryOptions['suspense'];
useErrorBoundary?: boolean;
};
@@ -80,9 +74,8 @@ export type QueryHookArgs<T> = {
};
export type QueryOptions = {
cacheTime?: UseQueryOptions['cacheTime'];
enabled?: UseQueryOptions['enabled'];
keepPreviousData?: UseQueryOptions['keepPreviousData'];
gcTime?: UseQueryOptions['gcTime'];
meta?: UseQueryOptions['meta'];
onError?: (err: any) => void;
onSettled?: any;
@@ -94,6 +87,5 @@ export type QueryOptions = {
retry?: UseQueryOptions['retry'];
retryDelay?: UseQueryOptions['retryDelay'];
staleTime?: UseQueryOptions['staleTime'];
suspense?: UseQueryOptions['suspense'];
useErrorBoundary?: boolean;
};
+86 -65
View File
@@ -1,84 +1,83 @@
import merge from 'lodash/merge';
import { nanoid } from 'nanoid/non-secure';
import { devtools, persist } from 'zustand/middleware';
import { create } from 'zustand';
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { createWithEqualityFn } from 'zustand/traditional';
import { useAlbumArtistListDataStore } from '/@/renderer/store/album-artist-list-data.store';
import { useAlbumListDataStore } from '/@/renderer/store/album-list-data.store';
import { useListStore } from '/@/renderer/store/list.store';
import { createSelectors } from '/@/renderer/store/utils';
import { ServerListItem } from '/@/shared/types/domain/server-domain-types';
export interface AuthSlice extends AuthState {
actions: {
addServer: (args: ServerListItem) => void;
deleteServer: (id: string) => void;
getServer: (id: string) => null | ServerListItem;
setCurrentServer: (server: null | ServerListItem) => void;
updateServer: (id: string, args: Partial<ServerListItem>) => void;
};
actions: Actions;
}
export interface AuthState {
currentServer: null | ServerListItem;
currentServerId: null | string;
deviceId: string;
serverList: Record<string, ServerListItem>;
}
export const useAuthStore = createWithEqualityFn<AuthSlice>()(
interface Actions {
addServer: (args: ServerListItem) => void;
deleteServer: (id: string) => void;
setCurrentServer: (server: null | ServerListItem) => void;
updateServer: (id: string, args: Partial<ServerListItem>) => void;
}
const authStoreBase = create<AuthSlice>()(
persist(
devtools(
immer((set, get) => ({
actions: {
addServer: (args) => {
set((state) => {
state.serverList[args.id] = args;
});
},
deleteServer: (id) => {
set((state) => {
delete state.serverList[id];
subscribeWithSelector(
immer((set) => ({
actions: {
addServer: (args) => {
set((state) => {
state.serverList[args.id] = args;
state.currentServerId = args.id;
});
},
deleteServer: (id) => {
set((state) => {
delete state.serverList[id];
if (state.currentServer?.id === id) {
state.currentServer = null;
}
});
},
getServer: (id) => {
const server = get().serverList[id];
if (server) return server;
return null;
},
setCurrentServer: (server) => {
set((state) => {
state.currentServer = server;
if (state.currentServerId === id) {
state.currentServerId = null;
}
});
},
setCurrentServer: (server) => {
set((state) => {
state.currentServerId = server?.id || null;
if (server) {
// Reset list filters
useListStore.getState()._actions.resetFilter();
if (server) {
// Reset list filters
useListStore.getState()._actions.resetFilter();
// Reset persisted grid list stores
useAlbumListDataStore.getState().actions.setItemData([]);
useAlbumArtistListDataStore.getState().actions.setItemData([]);
}
});
},
updateServer: (id: string, args: Partial<ServerListItem>) => {
set((state) => {
const updatedServer = {
...state.serverList[id],
...args,
};
// Reset persisted grid list stores
useAlbumListDataStore.getState().actions.setItemData([]);
useAlbumArtistListDataStore.getState().actions.setItemData([]);
}
});
},
updateServer: (id: string, args: Partial<ServerListItem>) => {
set((state) => {
const updatedServer = {
...state.serverList[id],
...args,
};
state.serverList[id] = updatedServer as ServerListItem;
state.currentServer = updatedServer as ServerListItem;
});
state.serverList[id] = updatedServer as ServerListItem;
});
},
},
},
currentServer: null,
deviceId: nanoid(),
serverList: {},
})),
currentServerId: null,
deviceId: nanoid(),
serverList: {},
})),
),
{ name: 'store_authentication' },
),
{
@@ -89,18 +88,40 @@ export const useAuthStore = createWithEqualityFn<AuthSlice>()(
),
);
export const useCurrentServerId = () => useAuthStore((state) => state.currentServer)?.id || '';
export const useAuthStore = createSelectors(authStoreBase);
export const useCurrentServer = () => useAuthStore((state) => state.currentServer);
export const useCurrentServerId = () => {
return useAuthStore.use.currentServerId();
};
export const useServerList = () => useAuthStore((state) => state.serverList);
export const useCurrentServer = () => {
const currentServerId = useCurrentServerId();
export const useAuthStoreActions = () => useAuthStore((state) => state.actions);
export const getServerById = (id?: string) => {
if (!id) {
if (!currentServerId) {
return null;
}
return useAuthStore.getState().actions.getServer(id);
const servers = useAuthStore.use.serverList();
const server = servers[currentServerId];
if (!server) {
return null;
}
return server;
};
export const useServerList = () => useAuthStore.use.serverList();
export const useAuthStoreActions = () => useAuthStore.use.actions();
export const useServerById = (id: string) => {
const servers = useAuthStore.use.serverList();
const server = servers[id];
if (!server) {
return null;
}
return server;
};
+15
View File
@@ -1,4 +1,5 @@
import mergeWith from 'lodash/mergeWith';
import { StoreApi, UseBoundStore } from 'zustand';
/**
* A custom deep merger that will replace all 'columns' items with the persistent
@@ -17,3 +18,17 @@ export const mergeOverridingColumns = <T>(persistedState: unknown, currentState:
return undefined;
});
};
type WithSelectors<S> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never;
export function createSelectors<S extends UseBoundStore<StoreApi<object>>>(_store: S) {
const store = _store as WithSelectors<typeof _store>;
store.use = {};
for (const k of Object.keys(store.getState())) {
(store.use as any)[k] = () => store((s) => s[k as keyof typeof s]);
}
return store;
}
@@ -1,7 +1,7 @@
import isElectron from 'is-electron';
import { api } from '/@/renderer/api';
import { getServerById, useSettingsStore } from '/@/renderer/store';
import { useServerById, useSettingsStore } from '/@/renderer/store';
import { PlayerData, QueueSong, QueueSong } from '/@/shared/types/domain/player-domain-types';
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
@@ -11,7 +11,7 @@ const modifyUrl = (song: QueueSong): string => {
if (transcode.enabled) {
const streamUrl = api.controller.getTranscodingUrl({
apiClientProps: {
server: getServerById(song.serverId),
server: useServerById(song.serverId),
},
query: {
base: song.streamUrl,
@@ -384,7 +384,7 @@ const normalizeUser = (item: z.infer<typeof ndType._response.user>): User => {
id: item.id,
isAdmin: item.isAdmin,
lastLoginAt: item.lastLoginAt,
name: item.userName,
username: item.userName,
updatedAt: item.updatedAt,
};
};
File diff suppressed because it is too large Load Diff
+210 -55
View File
@@ -1,18 +1,49 @@
import { components } from './subsonic-schema.d';
import { components, paths } from './subsonic-schema.d';
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
import { Album } from '/@/shared/types/domain/album-domain-types';
import { Album, AlbumListSortOptions } from '/@/shared/types/domain/album-domain-types';
import { Artist, RelatedArtist } from '/@/shared/types/domain/artist-domain-types';
import { Genre, RelatedGenre } from '/@/shared/types/domain/genre-domain-types';
import { Playlist } from '/@/shared/types/domain/playlist-domain-types';
import { ServerListItem, ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import { Song } from '/@/shared/types/domain/song-domain-types';
import { User } from '/@/shared/types/domain/user-domain-types';
import { formatDate } from '/@/shared/utils/format-date';
type AlbumSortType = paths['/rest/getAlbumList2']['get']['parameters']['query']['type'];
const defaultSortType: AlbumSortType = 'alphabeticalByName';
const albumSortByMap: Record<AlbumListSortOptions, AlbumSortType> = {
[AlbumListSortOptions.ALBUM_ARTIST]: 'alphabeticalByArtist',
[AlbumListSortOptions.ARTIST]: defaultSortType,
[AlbumListSortOptions.COMMUNITY_RATING]: defaultSortType,
[AlbumListSortOptions.CRITIC_RATING]: defaultSortType,
[AlbumListSortOptions.DATE_ADDED]: 'newest',
[AlbumListSortOptions.DATE_PLAYED]: 'recent',
[AlbumListSortOptions.DURATION]: defaultSortType,
[AlbumListSortOptions.IS_FAVORITE]: defaultSortType,
[AlbumListSortOptions.NAME]: 'alphabeticalByName',
[AlbumListSortOptions.PLAY_COUNT]: 'frequent',
[AlbumListSortOptions.RANDOM]: defaultSortType,
[AlbumListSortOptions.RATING]: 'highest',
[AlbumListSortOptions.RELEASE_DATE]: defaultSortType,
[AlbumListSortOptions.TRACK_COUNT]: defaultSortType,
[AlbumListSortOptions.YEAR]: defaultSortType,
};
export const normalize = {
_sort: {
album: (option: AlbumListSortOptions) => {
return albumSortByMap[option] || defaultSortType;
},
},
album: (
item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
item:
| components['schemas']['AlbumID3']
| components['schemas']['AlbumID3WithSongs']
| components['schemas']['Child'],
server: ServerListItem,
): Album => {
const imageUrl = item.coverArt ? getCoverArtUrl(item.coverArt, server) : null;
@@ -24,7 +55,7 @@ export const normalize = {
artistName: item.artist || null,
artists: getArtistList(item.artists, item.artistId, item.artist),
comment: null,
createdDate: item.created,
createdDate: item.created || null,
discTitles: getDiscTitles(item),
displayArtist: null,
duration: getDuration(item.duration),
@@ -33,12 +64,12 @@ export const normalize = {
id: item.id.toString(),
imagePlaceholderUrl: null,
imageUrl,
isCompilation: item.isCompilation || null,
isCompilation: getIsCompilation(item),
mbzId: item.musicBrainzId || null,
mbzReleaseGroupId: null,
missing: null,
moods: getMoods(item),
name: item.name,
name: getName(item),
originalReleaseDate: getOriginalReleaseDate(item),
participants: {},
recordLabels: getRecordLabels(item),
@@ -46,8 +77,8 @@ export const normalize = {
releaseTypes: getReleaseTypes(item),
releaseYear: item.year || null,
size: null,
songCount: item.songCount,
sortName: item.sortName || item.name,
songCount: 'songCount' in item ? item.songCount : null,
sortName: getSortName(item),
tags: {},
updatedDate: null,
userFavorite: Boolean(item.starred),
@@ -55,7 +86,7 @@ export const normalize = {
userLastPlayedDate: item.played || null,
userPlayCount: item.playCount ?? null,
userRating: item.userRating || null,
version: item.version || null,
version: 'version' in item ? item.version || null : null,
};
},
albumArtist: (
@@ -167,6 +198,28 @@ export const normalize = {
userRating: item.userRating || null,
};
},
user: (item: components['schemas']['User'], server: ServerListItem): User => {
return {
_itemType: LibraryItem.USER,
_serverId: server.id,
_serverType: ServerType.SUBSONIC,
id: item.username,
permissions: {
'jukebox.manage': item.jukeboxRole,
'media.download': item.downloadRole,
'media.folder': item.folder?.map((folder) => folder.toString()) || [],
'media.share': item.shareRole,
'media.stream': item.streamRole,
'media.upload': item.uploadRole,
'playlist.create': item.playlistRole,
'playlist.delete': item.playlistRole,
'playlist.edit': item.playlistRole,
'server.admin': item.adminRole,
'user.edit': item.settingsRole,
},
username: item.username,
};
},
};
function getArtistList(
@@ -200,12 +253,19 @@ function getCoverArtUrl(id: string, server: ServerListItem) {
}
function getDiscTitles(
item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
item:
| components['schemas']['AlbumID3']
| components['schemas']['AlbumID3WithSongs']
| components['schemas']['Child'],
) {
return (item.discTitles || []).map((discTitle) => ({
disc: discTitle.disc,
title: discTitle.title,
}));
if ('discTitles' in item) {
return (item.discTitles || []).map((discTitle) => ({
disc: discTitle.disc,
title: discTitle.title,
}));
}
return [];
}
function getDuration(duration?: number) {
@@ -214,12 +274,16 @@ function getDuration(duration?: number) {
}
function getGainInfo(item: components['schemas']['Child']) {
return item.replayGain && (item.replayGain.albumGain || item.replayGain.trackGain)
? {
album: item.replayGain.albumGain,
track: item.replayGain.trackGain,
}
: null;
if ('replayGain' in item) {
return item.replayGain && (item.replayGain.albumGain || item.replayGain.trackGain)
? {
album: item.replayGain.albumGain,
track: item.replayGain.trackGain,
}
: null;
}
return null;
}
function getGenres(
@@ -228,15 +292,19 @@ function getGenres(
| components['schemas']['AlbumID3WithSongs']
| components['schemas']['Child'],
): RelatedGenre[] {
if (item.genres) {
return item.genres.map((genre) => ({
if ('genres' in item) {
return (item.genres || []).map((genre) => ({
id: genre.name,
imageUrl: null,
name: genre.name,
}));
}
if (item.genre) {
if ('genre' in item) {
if (!item.genre) {
return [];
}
return [
{
id: item.genre,
@@ -249,26 +317,67 @@ function getGenres(
return [];
}
function getIsCompilation(
item:
| components['schemas']['AlbumID3']
| components['schemas']['AlbumID3WithSongs']
| components['schemas']['Child'],
) {
if ('isCompilation' in item) {
return item.isCompilation || null;
}
return null;
}
function getMoods(
item:
| components['schemas']['AlbumID3']
| components['schemas']['AlbumID3WithSongs']
| components['schemas']['Child'],
) {
return (item.moods || []).map((mood) => ({
id: mood,
name: mood,
}));
if ('moods' in item) {
return (item.moods || []).map((mood) => ({
id: mood,
name: mood,
}));
}
return [];
}
function getName(
item:
| components['schemas']['AlbumID3']
| components['schemas']['AlbumID3WithSongs']
| components['schemas']['Child'],
) {
if ('name' in item) {
return item.name || '';
}
if ('title' in item) {
return item.title || '';
}
return '';
}
function getOriginalReleaseDate(
item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
item:
| components['schemas']['AlbumID3']
| components['schemas']['AlbumID3WithSongs']
| components['schemas']['Child'],
) {
return item.originalReleaseDate
? formatDate.toUTCDate(
`${item.originalReleaseDate.year}-${item.originalReleaseDate.month}-${item.originalReleaseDate.day}`,
)
: null;
if ('originalReleaseDate' in item) {
return item.originalReleaseDate
? formatDate.toUTCDate(
`${item.originalReleaseDate.year}-${item.originalReleaseDate.month}-${item.originalReleaseDate.day}`,
)
: null;
}
return null;
}
function getParticipants(item: components['schemas']['Child']) {
@@ -298,40 +407,86 @@ function getParticipants(item: components['schemas']['Child']) {
}
function getPeakInfo(item: components['schemas']['Child']) {
return item.replayGain && (item.replayGain.albumPeak || item.replayGain.trackPeak)
? {
album: item.replayGain.albumPeak,
track: item.replayGain.trackPeak,
}
: null;
if ('replayGain' in item) {
return item.replayGain && (item.replayGain.albumPeak || item.replayGain.trackPeak)
? {
album: item.replayGain.albumPeak,
track: item.replayGain.trackPeak,
}
: null;
}
return null;
}
function getRecordLabels(
item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
item:
| components['schemas']['AlbumID3']
| components['schemas']['AlbumID3WithSongs']
| components['schemas']['Child'],
) {
return (item.recordLabels || []).map((recordLabel) => ({
id: recordLabel.name,
name: recordLabel.name,
}));
if ('recordLabels' in item) {
return (item.recordLabels || []).map((recordLabel) => ({
id: recordLabel.name,
name: recordLabel.name,
}));
}
return [];
}
function getReleaseDate(
item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
item:
| components['schemas']['AlbumID3']
| components['schemas']['AlbumID3WithSongs']
| components['schemas']['Child'],
) {
return item.releaseDate
? formatDate.toUTCDate(
`${item.releaseDate.year}-${item.releaseDate.month}-${item.releaseDate.day}`,
)
: null;
if ('releaseDate' in item) {
return item.releaseDate
? formatDate.toUTCDate(
`${item.releaseDate.year}-${item.releaseDate.month}-${item.releaseDate.day}`,
)
: null;
}
return null;
}
function getReleaseTypes(
item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
item:
| components['schemas']['AlbumID3']
| components['schemas']['AlbumID3WithSongs']
| components['schemas']['Child'],
) {
return (item.releaseTypes || []).map((releaseType) => ({
id: releaseType,
name: releaseType,
}));
if ('releaseTypes' in item) {
return (item.releaseTypes || []).map((releaseType) => ({
id: releaseType,
name: releaseType,
}));
}
return [];
}
function getSortName(
item:
| components['schemas']['AlbumID3']
| components['schemas']['AlbumID3WithSongs']
| components['schemas']['Child'],
) {
if ('sortName' in item) {
return item.sortName || '';
}
if ('name' in item) {
return item.name || '';
}
if ('title' in item) {
return item.title || '';
}
return '';
}
function getStreamUrl(id: string, server: ServerListItem) {
+533
View File
@@ -1,10 +1,24 @@
import { AxiosHeaders } from 'axios';
import dayjs from 'dayjs';
import isElectron from 'is-electron';
import { orderBy, shuffle } from 'lodash';
import { stringify } from 'querystring';
import semverCoerce from 'semver/functions/coerce';
import semverGte from 'semver/functions/gte';
import { z } from 'zod';
import { Album, AlbumListSortOptions } from '/@/shared/types/domain/album-domain-types';
import { Artist, ArtistListSortOptions } from '/@/shared/types/domain/artist-domain-types';
import { Genre, GenreListSortOptions } from '/@/shared/types/domain/genre-domain-types';
import {
Playlist,
PlaylistListSortOptions,
PlaylistSong,
} from '/@/shared/types/domain/playlist-domain-types';
import { ServerFeature, ServerListItem } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { Song, SongListSortOptions } from '/@/shared/types/domain/song-domain-types';
import { User, UserListSortOptions } from '/@/shared/types/domain/user-domain-types';
// Since ts-rest client returns a strict response type, we need to add the headers to the body object
export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => {
@@ -110,3 +124,522 @@ export const getClientType = (): string => {
};
export const SEPARATOR_STRING = ' · ';
export async function estimateTotalRecordCount(args: {
fetcher: (page: number, limit: number) => Promise<number>;
limit: number;
}) {
const { fetcher, limit } = args;
// Recursive binary search across all pages to estimate total rows
async function estimateTotalRowsRecursive(
low: number,
high: number,
limit: number,
): Promise<number> {
if (low > high) {
return 0; // This condition is just a safeguard and shouldn't be reached
}
const mid = Math.floor((low + high) / 2);
const data = await fetcher(mid, limit);
if (data < limit) {
// If the current page contains fewer than 500 items, it's close to the last page
const itemCount = (mid - 1) * limit + data;
return itemCount;
} else {
// If the current page is full, search in the higher half
return estimateTotalRowsRecursive(mid + 1, high, limit);
}
}
// Function to estimate total rows with limited page size
async function estimateTotalRows(): Promise<number> {
let low = 1;
let high = 2;
// Step 1: Exponentially grow the number of pages to get an upper bound for the total pages
while (true) {
const data = await fetcher(high, limit);
if (data < limit) {
// If we encounter the last page, break out of the loop
break;
}
// Double the upper bound for the number of pages
low = high;
high *= 2;
}
// Step 2: Perform binary search across all pages to find the exact number of rows
return estimateTotalRowsRecursive(low, high, limit);
}
return estimateTotalRows();
}
export async function exactTotalRecordCount(args: {
fetcher: (page: number, limit: number) => Promise<number>;
limit: number;
startPage: number;
}) {
const { fetcher, limit, startPage } = args;
// Add early return for page 1 with no results
if (startPage === 1) {
const firstPageCount = await fetcher(1, limit);
if (firstPageCount === 0) {
return 0;
}
}
const fetchCountRecursive = async (
page: number,
limit: number,
reverse: boolean,
totalRecordCount: number,
previousPageRecordCount?: number,
): Promise<number> => {
// Add guard against negative page numbers
if (page < 1) {
return totalRecordCount;
}
const currentPageRecordCount = await fetcher(page, limit);
if (currentPageRecordCount !== limit && currentPageRecordCount !== 0) {
totalRecordCount += currentPageRecordCount;
return totalRecordCount;
}
// Handle the case when the last page is equal to the limit and is ascending
if (
!reverse &&
currentPageRecordCount !== limit &&
currentPageRecordCount === 0 &&
currentPageRecordCount === previousPageRecordCount
) {
return totalRecordCount;
}
if (reverse) {
totalRecordCount -= limit;
return fetchCountRecursive(
page - 1,
limit,
true,
totalRecordCount,
currentPageRecordCount,
);
} else {
totalRecordCount += currentPageRecordCount;
return fetchCountRecursive(
page + 1,
limit,
false,
totalRecordCount,
currentPageRecordCount,
);
}
};
const estimatedStartRecordCount = startPage * limit;
const startPageRecordCount = await fetcher(startPage, limit);
const isLastPage = startPageRecordCount < limit && startPageRecordCount !== 0;
if (isLastPage) {
if (estimatedStartRecordCount < limit) {
return estimatedStartRecordCount + startPageRecordCount;
}
return estimatedStartRecordCount - limit + startPageRecordCount;
}
const shouldReverse = startPageRecordCount < limit;
const count = await fetchCountRecursive(
startPage,
limit,
shouldReverse,
estimatedStartRecordCount,
);
return count;
}
export async function fetchAllRecords<T>(args: {
fetcher: (page: number, limit: number) => Promise<T[]>;
fetchLimit?: number;
items?: T[];
page?: number;
}) {
const limit = args.fetchLimit || 500;
const page = args.page || 0;
const items = args.items || [];
const result = await args.fetcher(page, limit);
// If we get an empty array, we've reached the end
if (result.length === 0) {
return items;
}
// If we get less than the limit, we've reached the end
if (result.length < limit) {
return [...result, ...items];
}
return fetchAllRecords({
fetcher: args.fetcher,
fetchLimit: args.fetchLimit,
items: [...items, ...result],
page: page + 1,
});
}
export async function fetchTotalRecordCount(args: {
fetcher: (page: number, limit: number) => Promise<number>;
fetchLimit?: number;
}) {
const limit = args.fetchLimit || 500;
const estimatedCount = await estimateTotalRecordCount({
fetcher: args.fetcher,
limit,
});
const estimatedPages = Math.ceil(estimatedCount / limit);
const totalRecordCount = await exactTotalRecordCount({
fetcher: args.fetcher,
limit,
startPage: estimatedPages,
});
return totalRecordCount;
}
function paginate<T>(array: T[], offset: number, limit: number) {
let result: T[];
if (limit === -1) {
result = array.slice(offset);
} else {
result = array.slice(offset, offset + limit);
}
return {
items: result,
offset,
totalRecordCount: array.length,
};
}
function search<T>(array: T[], searchTerm: string, keys: (keyof T)[]) {
return array.filter((item) =>
keys.some((key) => {
const value = item[key];
return String(value ?? '')
.toLocaleLowerCase()
.includes(searchTerm.toLocaleLowerCase());
}),
);
}
const counts = new Map<string, { count: number; expires: number }>();
setInterval(
() => {
counts.forEach((value, key) => {
if (value.expires < dayjs().unix()) {
counts.delete(key);
}
});
},
1000 * 60 * 10,
); // 10 minutes
async function getListCount(
options: {
expiration?: number; // Expiration in minutes
fetchLimit?: number;
query: Record<string, unknown>;
serverId: string;
type: LibraryItem | string;
},
fetcher?: (page: number, limit: number) => Promise<number>,
) {
const key = getListCountKey(options);
const value = counts.get(key);
if (fetcher && (!value || value.expires < dayjs().unix())) {
const totalRecordCount = await fetchTotalRecordCount({
fetcher,
fetchLimit: options.fetchLimit,
});
setListCount(key, totalRecordCount, options.expiration ?? 1440);
return totalRecordCount;
}
return value?.count;
}
function getListCountKey(options: {
query: Record<string, unknown>;
serverId: string;
type: LibraryItem | string;
}) {
const hash = stringify(options.query as Record<string, boolean | null | number | string>);
return `${options.serverId}::${options.type}::${hash}`;
}
function invalidateListCount(key?: string) {
if (key) {
return counts.delete(key);
}
return counts.clear();
}
function setListCount(key: string, count: number, expiration = 1440) {
counts.set(key, { count, expires: dayjs().unix() + expiration * 1000 * 60 });
}
const sortBy = {
album: (array: Album[], key: AlbumListSortOptions, order: ListSortOrder) => {
let value = array;
switch (key) {
case AlbumListSortOptions.ALBUM_ARTIST: {
value = orderBy(value, ['artistId'], [order]);
break;
}
case AlbumListSortOptions.ARTIST: {
value = orderBy(value, ['artistId'], [order]);
break;
}
case AlbumListSortOptions.COMMUNITY_RATING: {
value = orderBy(value, ['userRating'], [order]);
break;
}
case AlbumListSortOptions.CRITIC_RATING: {
value = orderBy(value, ['userRating'], [order]);
break;
}
case AlbumListSortOptions.DATE_ADDED: {
value = orderBy(value, ['createdDate'], [order]);
break;
}
case AlbumListSortOptions.DATE_PLAYED: {
value = orderBy(value, ['userLastPlayedDate'], [order]);
break;
}
case AlbumListSortOptions.DURATION: {
value = orderBy(value, ['duration'], [order]);
break;
}
case AlbumListSortOptions.IS_FAVORITE: {
value = orderBy(value, ['userFavoriteDate', 'userFavorite'], [order, order]);
break;
}
case AlbumListSortOptions.NAME: {
value = orderBy(value, ['name'], [order]);
break;
}
case AlbumListSortOptions.PLAY_COUNT: {
value = orderBy(value, ['userPlayCount'], [order]);
break;
}
case AlbumListSortOptions.RANDOM: {
value = shuffle(value);
break;
}
case AlbumListSortOptions.RELEASE_DATE: {
value = orderBy(value, ['releaseDate'], [order]);
break;
}
case AlbumListSortOptions.TRACK_COUNT: {
value = orderBy(value, ['trackCount'], [order]);
break;
}
case AlbumListSortOptions.YEAR: {
value = orderBy(value, ['releaseYear'], [order]);
break;
}
}
return value;
},
artist: (array: Artist[], key: ArtistListSortOptions, order: ListSortOrder) => {
let value = array;
switch (key) {
case ArtistListSortOptions.ALBUM_COUNT: {
value = orderBy(value, ['albumCount'], [order]);
break;
}
case ArtistListSortOptions.NAME: {
value = orderBy(value, ['name'], [order]);
break;
}
case ArtistListSortOptions.TRACK_COUNT: {
value = orderBy(value, ['trackCount'], [order]);
break;
}
}
return value;
},
genre: (array: Genre[], key: GenreListSortOptions, order: ListSortOrder) => {
let value = array;
switch (key) {
case GenreListSortOptions.ALBUM_COUNT: {
value = orderBy(value, ['albumCount'], [order]);
break;
}
case GenreListSortOptions.NAME: {
value = orderBy(value, ['name'], [order]);
break;
}
case GenreListSortOptions.TRACK_COUNT: {
value = orderBy(value, ['trackCount'], [order]);
break;
}
}
return value;
},
playlist: (array: Playlist[], key: PlaylistListSortOptions, order: ListSortOrder) => {
let value = array;
switch (key) {
case PlaylistListSortOptions.DURATION: {
value = orderBy(value, ['duration'], [order]);
break;
}
case PlaylistListSortOptions.NAME: {
value = orderBy(value, ['name'], [order]);
break;
}
case PlaylistListSortOptions.OWNER: {
value = orderBy(value, ['owner'], [order]);
break;
}
case PlaylistListSortOptions.PUBLIC: {
value = orderBy(value, ['isPublic'], [order]);
break;
}
case PlaylistListSortOptions.TRACK_COUNT: {
value = orderBy(value, ['trackCount'], [order]);
break;
}
}
return value;
},
song: (array: PlaylistSong[] | Song[], key: SongListSortOptions, order: ListSortOrder) => {
let value = array;
switch (key) {
case SongListSortOptions.ALBUM: {
value = orderBy(value, ['album'], [order]);
break;
}
case SongListSortOptions.ALBUM_ARTIST: {
value = orderBy(value, ['artistId'], [order]);
break;
}
case SongListSortOptions.ARTIST: {
value = orderBy(value, ['artistId'], [order]);
break;
}
case SongListSortOptions.BPM: {
value = orderBy(value, ['bpm'], [order]);
break;
}
case SongListSortOptions.CHANNELS: {
value = orderBy(value, ['channels'], [order]);
break;
}
case SongListSortOptions.COMMENT: {
value = orderBy(value, ['comment'], [order]);
break;
}
case SongListSortOptions.DURATION: {
value = orderBy(value, ['duration'], [order]);
break;
}
case SongListSortOptions.GENRE: {
value = orderBy(value, ['genre'], [order]);
break;
}
case SongListSortOptions.ID: {
break;
}
case SongListSortOptions.IS_FAVORITE: {
value = orderBy(value, ['userFavoriteDate', 'userFavorite'], [order, order]);
break;
}
case SongListSortOptions.NAME: {
value = orderBy(value, ['name'], [order]);
break;
}
case SongListSortOptions.PLAY_COUNT: {
value = orderBy(value, ['userPlayCount'], [order]);
break;
}
case SongListSortOptions.RANDOM: {
value = shuffle(value);
break;
}
case SongListSortOptions.RATING: {
value = orderBy(value, ['userRating'], [order]);
break;
}
case SongListSortOptions.RECENTLY_ADDED: {
value = orderBy(value, ['recentlyAdded'], [order]);
break;
}
case SongListSortOptions.RECENTLY_PLAYED: {
value = orderBy(value, ['userLastPlayedDate'], [order]);
break;
}
case SongListSortOptions.RELEASE_DATE: {
value = orderBy(value, ['releaseYear'], [order]);
break;
}
case SongListSortOptions.YEAR: {
value = orderBy(value, ['releaseYear'], [order]);
break;
}
}
return value;
},
user: (array: User[], key: UserListSortOptions, order: ListSortOrder) => {
let value = array;
switch (key) {
case UserListSortOptions.NAME: {
value = orderBy(value, ['name'], [order]);
break;
}
}
return value;
},
};
export const helpers = {
estimateTotalRecordCount,
exactTotalRecordCount,
fetchAllRecords,
fetchTotalRecordCount,
getListCount,
getListCountKey,
invalidateListCount,
paginate,
search,
setListCount,
sortBy,
};
@@ -6,14 +6,15 @@ import {
AlbumListResponse,
} from '/@/shared/types/domain/album-domain-types';
import {
AlbumArtistDetailRequest,
AlbumArtistDetailResponse,
AlbumArtistListRequest,
AlbumArtistListResponse,
ArtistDetailRequest,
ArtistDetailResponse,
ArtistListRequest,
ArtistListResponse,
} from '/@/shared/types/domain/artist-domain-types';
import { AuthenticationResponse } from '/@/shared/types/domain/auth-domain-types';
import {
AuthenticationRequest,
AuthenticationResponse,
} from '/@/shared/types/domain/auth-domain-types';
import { GenreListRequest, GenreListResponse } from '/@/shared/types/domain/genre-domain-types';
import {
LyricsRequest,
@@ -21,6 +22,12 @@ import {
StructuredLyric,
StructuredLyricsRequest,
} from '/@/shared/types/domain/lyric-domain-types';
import {
FavoriteRequest,
FavoriteResponse,
RatingResponse,
SetRatingRequest,
} from '/@/shared/types/domain/metadata-domain-types';
import { TranscodingRequest } from '/@/shared/types/domain/player-domain-types';
import {
AddToPlaylistArgs,
@@ -64,18 +71,16 @@ import {
import { TagRequest, TagsResponse } from '/@/shared/types/domain/tag-domain-types';
import {
DownloadRequest,
FavoriteRequest,
FavoriteResponse,
RatingResponse,
ScrobbleRequest,
ScrobbleResponse,
SetRatingRequest,
ShareItemRequest,
ShareItemResponse,
UserListRequest,
UserListResponse,
} from '/@/shared/types/domain/user-domain-types';
export type ApiAuthentication = (args: AuthenticationRequest) => Promise<AuthenticationResponse>;
export type ApiClientProps = {
baseUrl?: string;
cache?: 'default' | 'force-cache' | 'no-cache' | 'no-store' | 'only-if-cached' | 'reload';
@@ -119,21 +124,23 @@ export type ApiController = {
getListCount?: ApiControllerFn<AlbumListRequest, number>;
};
albumArtist: {
getDetail?: ApiControllerFn<AlbumArtistDetailRequest, AlbumArtistDetailResponse>;
getList?: ApiControllerFn<AlbumArtistListRequest, AlbumArtistListResponse>;
getListCount?: ApiControllerFn<AlbumArtistListRequest, number>;
};
artist: {
getDetail?: ApiControllerFn<ArtistDetailRequest, ArtistDetailResponse>;
getList?: ApiControllerFn<ArtistListRequest, ArtistListResponse>;
getListCount?: ApiControllerFn<ArtistListRequest, number>;
};
favorite: {
create?: ApiControllerFn<FavoriteRequest, FavoriteResponse>;
delete?: ApiControllerFn<FavoriteRequest, FavoriteResponse>;
artist: {
getDetail?: ApiControllerFn<ArtistDetailRequest, ArtistDetailResponse>;
getList?: ApiControllerFn<ArtistListRequest, ArtistListResponse>;
getListCount?: ApiControllerFn<ArtistListRequest, number>;
};
genre: {
getList?: ApiControllerFn<GenreListRequest, GenreListResponse>;
};
metadata: {
addFavorite?: ApiControllerFn<FavoriteRequest, FavoriteResponse>;
removeFavorite?: ApiControllerFn<FavoriteRequest, FavoriteResponse>;
setRating?: ApiControllerFn<SetRatingRequest, RatingResponse>;
};
musicFolder: {
getList?: ApiControllerFn<ServerMusicFolderListRequest, ServerMusicFolderListResponse>;
};
@@ -150,14 +157,6 @@ export type ApiController = {
update?: ApiControllerFn<UpdatePlaylistRequest, UpdatePlaylistResponse>;
};
server: {
authenticate: (
url: string,
body: { legacy?: boolean; password: string; username: string },
) => Promise<AuthenticationResponse>;
getRoles?: ApiControllerFn<
BaseEndpointArgs,
Array<string | { label: string; value: string }>
>;
getServerInfo?: ApiControllerFn<ServerInfoRequest, ServerInfo>;
getTags?: ApiControllerFn<TagRequest, TagsResponse>;
getTranscodingUrl?: ApiControllerFn<TranscodingRequest, string>;
@@ -177,7 +176,7 @@ export type ApiController = {
};
user: {
getList?: ApiControllerFn<UserListRequest, UserListResponse>;
setRating?: ApiControllerFn<SetRatingRequest, RatingResponse>;
getListCount?: ApiControllerFn<UserListRequest, number>;
shareItem?: ApiControllerFn<ShareItemRequest, ShareItemResponse>;
};
};
@@ -189,7 +188,6 @@ export interface ApiControllerError {
export type ApiControllerFn<TRequest, TResponse> = (
request: TRequest,
server: ServerListItem,
options?: ApiClientProps,
) => Promise<[ApiControllerError, null] | [null, TResponse]>;
@@ -198,18 +196,19 @@ export type BaseEndpointArgs = {
signal?: AbortSignal;
};
};
export interface BasePaginatedResponse<T> {
error?: any | string;
items: T;
startIndex: number;
totalRecordCount: null | number;
}
export interface BaseQuery<T> {
export interface BasePaginatedQuery<T> {
limit: number;
offset: number;
sortBy: T;
sortOrder: ListSortOrder;
}
export interface BasePaginatedResponse<T> {
items: T;
offset: number;
totalRecordCount: null | number;
}
export type ExtractControllerResponse<T> = T extends ApiControllerFn<any, infer R> ? R : never;
export const API_CLIENT_NAME = 'Feishin';
@@ -6,7 +6,10 @@ import { JFAlbumListSort } from '/@/shared/api/jellyfin.types';
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { NDAlbumListSort } from '/@/shared/api/navidrome.types';
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
import { BasePaginatedResponse, BaseQuery } from '/@/shared/types/adapter/api-controller-types';
import {
BasePaginatedQuery,
BasePaginatedResponse,
} from '/@/shared/types/adapter/api-controller-types';
import { RelatedArtist } from '/@/shared/types/domain/artist-domain-types';
import { RelatedGenre } from '/@/shared/types/domain/genre-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
@@ -75,24 +78,18 @@ export enum AlbumListSort {
YEAR = 'year',
}
export interface AlbumListQuery extends BaseQuery<AlbumListSort> {
_custom?: {
jellyfin?: Partial<z.infer<typeof jfType._parameters.albumList>>;
navidrome?: Partial<z.infer<typeof ndType._parameters.albumList>>;
};
export interface AlbumListQuery extends BasePaginatedQuery<AlbumListSortOptions> {
artistIds?: string[];
compilation?: boolean;
favorite?: boolean;
genres?: string[];
limit?: number;
maxYear?: number;
minYear?: number;
musicFolderId?: string;
searchTerm?: string;
startIndex: number;
}
export type AlbumListRequest = { query: AlbumListQuery };
export type AlbumListRequest = { query: AlbumListQuery; totalRecordCount?: number };
export type AlbumListResponse = BasePaginatedResponse<Album[]> | null | undefined;
+9 -7
View File
@@ -7,8 +7,8 @@ import {
AlbumListResponse,
} from '/@/shared/types/domain/album-domain-types';
import {
AlbumArtistDetailRequest,
AlbumArtistDetailResponse,
ArtistDetailRequest,
ArtistDetailResponse,
AlbumArtistListRequest,
AlbumArtistListResponse,
ArtistListRequest,
@@ -62,17 +62,19 @@ import {
import { TagRequest, TagsResponse } from '/@/shared/types/domain/tag-domain-types';
import {
DownloadRequest,
FavoriteRequest,
FavoriteResponse,
RatingResponse,
ScrobbleRequest,
ScrobbleResponse,
SetRatingRequest,
ShareItemRequest,
ShareItemResponse,
UserListRequest,
UserListResponse,
} from '/@/shared/types/domain/user-domain-types';
import {
FavoriteRequest,
FavoriteResponse,
RatingResponse,
SetRatingRequest,
} from './metadata-domain-types';
export const instanceOfCancellationError = (error: any) => {
return 'revert' in error;
@@ -88,7 +90,7 @@ export type ControllerEndpoint = {
createPlaylist: (args: CreatePlaylistRequest) => Promise<CreatePlaylistResponse>;
deleteFavorite: (args: FavoriteRequest) => Promise<FavoriteResponse>;
deletePlaylist: (args: DeletePlaylistRequest) => Promise<DeletePlaylistResponse>;
getAlbumArtistDetail: (args: AlbumArtistDetailRequest) => Promise<AlbumArtistDetailResponse>;
getAlbumArtistDetail: (args: ArtistDetailRequest) => Promise<ArtistDetailResponse>;
getAlbumArtistList: (args: AlbumArtistListRequest) => Promise<AlbumArtistListResponse>;
getAlbumArtistListCount: (args: AlbumArtistListRequest) => Promise<number>;
getAlbumDetail: (args: AlbumDetailRequest) => Promise<AlbumDetailResponse>;
+8 -29
View File
@@ -1,12 +1,12 @@
import { orderBy } from 'lodash';
import { z } from 'zod';
import i18n from '/@/i18n/i18n';
import { JFAlbumArtistListSort, JFArtistListSort } from '/@/shared/api/jellyfin.types';
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { NDAlbumArtistListSort } from '/@/shared/api/navidrome.types';
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
import { BasePaginatedResponse, BaseQuery } from '/@/shared/types/adapter/api-controller-types';
import {
BasePaginatedQuery,
BasePaginatedResponse,
} from '/@/shared/types/adapter/api-controller-types';
import { RelatedGenre } from '/@/shared/types/domain/genre-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
@@ -121,23 +121,17 @@ export enum ArtistListSort {
export type AlbumArtistDetailQuery = { id: string };
export type AlbumArtistDetailRequest = { query: AlbumArtistDetailQuery };
export type ArtistDetailRequest = { query: AlbumArtistDetailQuery };
export type AlbumArtistDetailResponse = Artist | null;
export type ArtistDetailResponse = Artist | null;
export interface ArtistListQuery extends BaseQuery<ArtistListSort> {
_custom?: {
jellyfin?: Partial<z.infer<typeof jfType._parameters.albumArtistList>>;
navidrome?: Partial<z.infer<typeof ndType._parameters.albumArtistList>>;
};
limit?: number;
export interface ArtistListQuery extends BasePaginatedQuery<ArtistListSortOptions> {
musicFolderId?: string;
role?: string;
searchTerm?: string;
startIndex: number;
}
export type ArtistListRequest = { query: ArtistListQuery };
export type ArtistListRequest = { query: ArtistListQuery; totalRecordCount?: number };
export type ArtistListResponse = BasePaginatedResponse<Artist[]> | null | undefined;
type ArtistListSortMap = {
@@ -201,21 +195,6 @@ export enum AlbumArtistListSort {
SONG_COUNT = 'songCount',
}
export interface AlbumArtistListQuery extends BaseQuery<AlbumArtistListSort> {
_custom?: {
jellyfin?: Partial<z.infer<typeof jfType._parameters.albumArtistList>>;
navidrome?: Partial<z.infer<typeof ndType._parameters.albumArtistList>>;
};
limit?: number;
musicFolderId?: string;
searchTerm?: string;
startIndex: number;
}
export type AlbumArtistListRequest = { query: AlbumArtistListQuery };
export type AlbumArtistListResponse = BasePaginatedResponse<Artist[]> | null | undefined;
export type ArtistInfoQuery = {
artistId: string;
limit: number;
+8 -2
View File
@@ -1,6 +1,12 @@
import { UserPermissions } from '/@/shared/types/domain/user-domain-types';
export type AuthenticationRequest = {
body: { password: string; username: string };
url: string;
};
export type AuthenticationResponse = {
credential: string;
ndCredential?: string;
userId: null | string;
permissions: UserPermissions;
username: string;
};
+6 -33
View File
@@ -1,10 +1,10 @@
import i18n from '/@/i18n/i18n';
import { JFGenreListSort } from '/@/shared/api/jellyfin.types';
import { NDGenreListSort } from '/@/shared/api/navidrome.types';
import { BasePaginatedResponse, BaseQuery } from '/@/shared/types/adapter/api-controller-types';
import {
BasePaginatedQuery,
BasePaginatedResponse,
} from '/@/shared/types/adapter/api-controller-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import { UserListSort } from '/@/shared/types/domain/user-domain-types';
export enum GenreListSortOptions {
ALBUM_COUNT = 'albumCount',
@@ -29,18 +29,12 @@ export type Genre = {
songCount: null | number;
};
export interface GenreListQuery extends BaseQuery<GenreListSort> {
_custom?: {
jellyfin?: null;
navidrome?: null;
};
limit?: number;
export interface GenreListQuery extends BasePaginatedQuery<GenreListSortOptions> {
musicFolderId?: string;
searchTerm?: string;
startIndex: number;
}
export type GenreListRequest = { query: GenreListQuery };
export type GenreListRequest = { query: GenreListQuery; totalRecordCount?: number };
export type GenreListResponse = BasePaginatedResponse<Genre[]> | null | undefined;
@@ -49,24 +43,3 @@ export type RelatedGenre = {
imageUrl: null | string;
name: string;
};
type GenreListSortMap = {
jellyfin: Record<GenreListSort, JFGenreListSort | undefined>;
navidrome: Record<GenreListSort, NDGenreListSort | undefined>;
subsonic: Record<UserListSort, undefined>;
};
export const genreListSortMap: GenreListSortMap = {
jellyfin: {
name: JFGenreListSort.NAME,
},
navidrome: {
name: NDGenreListSort.NAME,
},
subsonic: {
name: undefined,
},
};
export enum GenreListSort {
NAME = 'name',
}
@@ -0,0 +1,19 @@
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
export type FavoriteQuery = {
id: string[];
type: LibraryItem;
};
export type FavoriteRequest = { query: FavoriteQuery; serverId?: string };
export type FavoriteResponse = null;
export type RatingQuery = {
id: string[];
rating: number;
};
export type RatingResponse = null;
export type SetRatingRequest = { query: RatingQuery; serverId?: string };
@@ -1,19 +1,13 @@
import { z } from 'zod';
import i18n from '/@/i18n/i18n';
import { JFPlaylistListSort } from '/@/shared/api/jellyfin.types';
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { NDPlaylistListSort } from '/@/shared/api/navidrome.types';
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
import {
BaseEndpointArgs,
BasePaginatedQuery,
BasePaginatedResponse,
BaseQuery,
} from '/@/shared/types/adapter/api-controller-types';
import { Genre, RelatedGenre } from '/@/shared/types/domain/genre-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { Song, SongListSort } from '/@/shared/types/domain/song-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import { Song, SongListSortOptions } from '/@/shared/types/domain/song-domain-types';
export enum PlaylistListSortOptions {
DURATION = 'duration',
@@ -33,15 +27,6 @@ export const PlaylistListSortOptionsLabels = {
[PlaylistListSortOptions.UPDATED_AT]: i18n.t('filter.updatedAt'),
};
export enum PlaylistListSort {
DURATION = 'duration',
NAME = 'name',
OWNER = 'owner',
PUBLIC = 'public',
SONG_COUNT = 'songCount',
UPDATED_AT = 'updatedAt',
}
export type AddToPlaylistArgs = BaseEndpointArgs & {
body: AddToPlaylistBody;
query: AddToPlaylistQuery;
@@ -88,6 +73,17 @@ export type DeletePlaylistRequest = {
export type DeletePlaylistResponse = null | undefined;
export type MoveItemQuery = {
endingIndex: number;
playlistId: string;
startingIndex: number;
trackId: string;
};
export type MoveItemRequest = {
query: MoveItemQuery;
};
export type Playlist = {
_itemType: LibraryItem.PLAYLIST;
_serverId: string;
@@ -109,17 +105,19 @@ export type Playlist = {
updatedDate: null | string;
};
export interface PlaylistListQuery extends BaseQuery<PlaylistListSort> {
_custom?: {
jellyfin?: Partial<z.infer<typeof jfType._parameters.playlistList>>;
navidrome?: Partial<z.infer<typeof ndType._parameters.playlistList>>;
};
limit?: number;
export type PlaylistDetailQuery = {
id: string;
};
export type PlaylistDetailRequest = { query: PlaylistDetailQuery };
export type PlaylistDetailResponse = Playlist;
export interface PlaylistListQuery extends BasePaginatedQuery<PlaylistListSortOptions> {
searchTerm?: string;
startIndex: number;
}
export type PlaylistListRequest = { query: PlaylistListQuery };
export type PlaylistListRequest = { query: PlaylistListQuery; totalRecordCount?: number };
export type PlaylistListResponse = BasePaginatedResponse<Playlist[]> | null | undefined;
@@ -127,6 +125,41 @@ export type PlaylistSong = Song & {
playlistItemId: string;
};
export type PlaylistSongListQuery = BasePaginatedQuery<SongListSortOptions> & {
id: string;
};
export type PlaylistSongListRequest = { query: PlaylistSongListQuery; totalRecordCount?: number };
// export const playlistListSortMap: PlaylistListSortMap = {
// jellyfin: {
// duration: JFPlaylistListSort.DURATION,
// name: JFPlaylistListSort.NAME,
// owner: undefined,
// public: undefined,
// songCount: JFPlaylistListSort.SONG_COUNT,
// updatedAt: undefined,
// },
// navidrome: {
// duration: NDPlaylistListSort.DURATION,
// name: NDPlaylistListSort.NAME,
// owner: NDPlaylistListSort.OWNER,
// public: NDPlaylistListSort.PUBLIC,
// songCount: NDPlaylistListSort.SONG_COUNT,
// updatedAt: NDPlaylistListSort.UPDATED_AT,
// },
// subsonic: {
// duration: undefined,
// name: undefined,
// owner: undefined,
// public: undefined,
// songCount: undefined,
// updatedAt: undefined,
// },
// };
export type PlaylistSongListResponse = BasePaginatedResponse<PlaylistSong[]>;
export type RemoveFromPlaylistQuery = {
id: string;
songId: string[];
@@ -165,66 +198,3 @@ export type UpdatePlaylistRequest = {
};
export type UpdatePlaylistResponse = null | undefined;
type PlaylistListSortMap = {
jellyfin: Record<PlaylistListSort, JFPlaylistListSort | undefined>;
navidrome: Record<PlaylistListSort, NDPlaylistListSort | undefined>;
subsonic: Record<PlaylistListSort, undefined>;
};
export const playlistListSortMap: PlaylistListSortMap = {
jellyfin: {
duration: JFPlaylistListSort.DURATION,
name: JFPlaylistListSort.NAME,
owner: undefined,
public: undefined,
songCount: JFPlaylistListSort.SONG_COUNT,
updatedAt: undefined,
},
navidrome: {
duration: NDPlaylistListSort.DURATION,
name: NDPlaylistListSort.NAME,
owner: NDPlaylistListSort.OWNER,
public: NDPlaylistListSort.PUBLIC,
songCount: NDPlaylistListSort.SONG_COUNT,
updatedAt: NDPlaylistListSort.UPDATED_AT,
},
subsonic: {
duration: undefined,
name: undefined,
owner: undefined,
public: undefined,
songCount: undefined,
updatedAt: undefined,
},
};
export type MoveItemQuery = {
endingIndex: number;
playlistId: string;
startingIndex: number;
trackId: string;
};
export type MoveItemRequest = {
query: MoveItemQuery;
};
export type PlaylistDetailQuery = {
id: string;
};
export type PlaylistDetailRequest = { query: PlaylistDetailQuery };
export type PlaylistDetailResponse = Playlist;
export type PlaylistSongListQuery = {
id: string;
limit?: number;
sortBy?: SongListSort;
sortOrder?: ListSortOrder;
startIndex: number;
};
export type PlaylistSongListRequest = { query: PlaylistSongListQuery };
export type PlaylistSongListResponse = BasePaginatedResponse<Song[]> | null | undefined;
@@ -46,11 +46,9 @@ export type ServerListItem = {
features?: ServerFeatures;
id: string;
name: string;
ndCredential?: string;
savePassword?: boolean;
type: ServerType;
url: string;
userId: null | string;
username: string;
version?: string;
};
@@ -13,11 +13,12 @@ export enum LibraryItem {
GENRE = 'genre',
PLAYLIST = 'playlist',
SONG = 'song',
USER = 'user',
}
export enum ListSortOrder {
ASC = 'ASC',
DESC = 'DESC',
ASC = 'asc',
DESC = 'desc',
}
export type AnyLibraryItem = Album | Artist | Artist | Playlist | QueueSong | Song;
+8 -11
View File
@@ -6,7 +6,10 @@ import { JFSongListSort } from '/@/shared/api/jellyfin.types';
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { NDSongListSort } from '/@/shared/api/navidrome.types';
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
import { BasePaginatedResponse, BaseQuery } from '/@/shared/types/adapter/api-controller-types';
import {
BasePaginatedQuery,
BasePaginatedResponse,
} from '/@/shared/types/adapter/api-controller-types';
import { RelatedArtist } from '/@/shared/types/domain/artist-domain-types';
import { RelatedGenre } from '/@/shared/types/domain/genre-domain-types';
import { Played, QueueSong } from '/@/shared/types/domain/player-domain-types';
@@ -134,27 +137,21 @@ export type Song = {
userRating: null | number;
};
export interface SongListQuery extends BaseQuery<SongListSort> {
_custom?: {
jellyfin?: Partial<z.infer<typeof jfType._parameters.songList>>;
navidrome?: Partial<z.infer<typeof ndType._parameters.songList>>;
};
export interface SongListQuery extends BasePaginatedQuery<SongListSort> {
albumArtistIds?: string[];
albumIds?: string[];
artistIds?: string[];
favorite?: boolean;
genreIds?: string[];
imageSize?: number;
limit?: number;
maxYear?: number;
minYear?: number;
musicFolderId?: string;
role?: string;
searchTerm?: string;
startIndex: number;
}
export type SongListRequest = { query: SongListQuery };
export type SongListRequest = { query: SongListQuery; totalRecordCount?: number };
export type SongListResponse = BasePaginatedResponse<Song[]> | null | undefined;
type SongListSortMap = {
@@ -240,7 +237,7 @@ export type RandomSongListQuery = {
played: Played;
};
export type RandomSongListRequest = { query: RandomSongListQuery };
export type RandomSongListRequest = { query: RandomSongListQuery; totalRecordCount?: number };
export type RandomSongListResponse = SongListResponse;
@@ -264,7 +261,7 @@ export type TopSongListQuery = {
limit?: number;
};
export type TopSongListRequest = { query: TopSongListQuery };
export type TopSongListRequest = { query: TopSongListQuery; totalRecordCount?: number };
export type TopSongListResponse = BasePaginatedResponse<Song[]> | null | undefined;
+35 -68
View File
@@ -1,71 +1,17 @@
import i18n from '/@/i18n/i18n';
import { NDUserListSort } from '/@/shared/api/navidrome.types';
import { BasePaginatedResponse, BaseQuery } from '/@/shared/types/adapter/api-controller-types';
import { AnyLibraryItems, LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import {
BasePaginatedQuery,
BasePaginatedResponse,
} from '/@/shared/types/adapter/api-controller-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
export enum UserListSortOptions {
CREATED_AT = 'createdAt',
EMAIL = 'email',
NAME = 'name',
UPDATED_AT = 'updatedAt',
}
export const UserListSortOptionsLabels = {
[UserListSortOptions.CREATED_AT]: i18n.t('filter.createdAt'),
[UserListSortOptions.EMAIL]: i18n.t('filter.email'),
[UserListSortOptions.NAME]: i18n.t('filter.name'),
[UserListSortOptions.UPDATED_AT]: i18n.t('filter.updatedAt'),
};
export type FavoriteQuery = {
id: string[];
type: LibraryItem;
};
export type FavoriteRequest = { query: FavoriteQuery; serverId?: string };
export type FavoriteResponse = null;
export type RatingQuery = {
item: AnyLibraryItems;
rating: number;
};
export type RatingResponse = null;
export type SetRatingRequest = { query: RatingQuery; serverId?: string };
export interface UserListQuery extends BaseQuery<UserListSort> {
_custom?: {
navidrome?: {
owner_id?: string;
};
};
limit?: number;
searchTerm?: string;
startIndex: number;
}
export type UserListRequest = { query: UserListQuery };
export type UserListResponse = BasePaginatedResponse<User[]> | null | undefined;
type UserListSortMap = {
jellyfin: Record<UserListSort, undefined>;
navidrome: Record<UserListSort, NDUserListSort | undefined>;
subsonic: Record<UserListSort, undefined>;
};
export const userListSortMap: UserListSortMap = {
jellyfin: {
name: undefined,
},
navidrome: {
name: NDUserListSort.NAME,
},
subsonic: {
name: undefined,
},
};
export enum UserListSort {
@@ -93,20 +39,41 @@ export type ShareItemBody = {
description: string;
downloadable: boolean;
expires: number;
resourceIds: string;
resourceIds: string[];
resourceType: string;
};
export type ShareItemRequest = { body: ShareItemBody; serverId?: string };
export type ShareItemResponse = null | { id: string };
export type ShareItemResponse = null | { id: string; url: string };
export type User = {
createdAt: null | string;
email: null | string;
_itemType: LibraryItem.USER;
_serverId: string;
_serverType: ServerType;
id: string;
isAdmin: boolean | null;
lastLoginAt: null | string;
name: string;
updatedAt: null | string;
permissions: UserPermissions;
username: string;
};
export interface UserListQuery extends BasePaginatedQuery<UserListSortOptions> {
searchTerm?: string;
}
export type UserListRequest = { query: UserListQuery; totalRecordCount?: number };
export type UserListResponse = BasePaginatedResponse<User[]>;
export type UserPermissions = {
'jukebox.manage': boolean; // Allow managing the jukebox
'media.download': boolean; // Allow downloading media
'media.folder': string[]; // Viewable folders
'media.share': boolean; // Allow sharing media
'media.stream': boolean; // Allow streaming media
'media.upload': boolean; // Allow uploading media
'playlist.create': boolean; // Allow creating playlists
'playlist.delete': boolean; // Allow deleting playlists
'playlist.edit': boolean; // Allow editing playlists
'server.admin': boolean; // Allow managing the server (user management / server settings)
'user.edit': boolean; // Allow editing own user account
};
+9
View File
@@ -0,0 +1,9 @@
export const randomString = (length?: number) => {
const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let string = '';
for (let i = 0; i < (length || 12); i += 1) {
const randomPoz = Math.floor(Math.random() * charSet.length);
string += charSet.substring(randomPoz, randomPoz + 1);
}
return string;
};