Add music folders query

This commit is contained in:
jeffvli 2022-12-20 19:11:33 -08:00
parent 3399fc6bf6
commit a354cab797
11 changed files with 143 additions and 55 deletions

View file

@ -114,7 +114,7 @@ const endpoints: ApiController = {
getFolderList: undefined, getFolderList: undefined,
getFolderSongs: undefined, getFolderSongs: undefined,
getGenreList: navidromeApi.getGenreList, getGenreList: navidromeApi.getGenreList,
getMusicFolderList: undefined, getMusicFolderList: subsonicApi.getMusicFolderList,
getPlaylistDetail: navidromeApi.getPlaylistDetail, getPlaylistDetail: navidromeApi.getPlaylistDetail,
getPlaylistList: navidromeApi.getPlaylistList, getPlaylistList: navidromeApi.getPlaylistList,
getPlaylistSongList: navidromeApi.getPlaylistSongList, getPlaylistSongList: navidromeApi.getPlaylistSongList,
@ -140,7 +140,7 @@ const endpoints: ApiController = {
getFolderList: undefined, getFolderList: undefined,
getFolderSongs: undefined, getFolderSongs: undefined,
getGenreList: undefined, getGenreList: undefined,
getMusicFolderList: undefined, getMusicFolderList: subsonicApi.getMusicFolderList,
getPlaylistDetail: undefined, getPlaylistDetail: undefined,
getPlaylistList: undefined, getPlaylistList: undefined,
getSongDetail: undefined, getSongDetail: undefined,
@ -183,8 +183,13 @@ const getSongList = async (args: SongListArgs) => {
return (apiController('getSongList') as ControllerEndpoint['getSongList'])?.(args); return (apiController('getSongList') as ControllerEndpoint['getSongList'])?.(args);
}; };
const getMusicFolderList = async (args: MusicFolderListArgs) => {
return (apiController('getMusicFolderList') as ControllerEndpoint['getMusicFolderList'])?.(args);
};
export const controller = { export const controller = {
getAlbumDetail, getAlbumDetail,
getAlbumList, getAlbumList,
getMusicFolderList,
getSongList, getSongList,
}; };

View file

@ -95,11 +95,13 @@ const authenticate = async (
}; };
const getMusicFolderList = async (args: MusicFolderListArgs): Promise<JFMusicFolderList> => { const getMusicFolderList = async (args: MusicFolderListArgs): Promise<JFMusicFolderList> => {
const { signal } = args; const { server, signal } = args;
const userId = useAuthStore.getState().currentServer?.userId; const userId = useAuthStore.getState().currentServer?.userId;
const data = await api const data = await api
.get(`users/${userId}/items`, { .get(`users/${userId}/items`, {
headers: { 'X-MediaBrowser-Token': server?.credential },
prefixUrl: server?.url,
signal, signal,
}) })
.json<JFMusicFolderListResponse>(); .json<JFMusicFolderListResponse>();
@ -108,11 +110,7 @@ const getMusicFolderList = async (args: MusicFolderListArgs): Promise<JFMusicFol
(folder) => folder.CollectionType === JFCollectionType.MUSIC, (folder) => folder.CollectionType === JFCollectionType.MUSIC,
); );
return { return musicFolders;
items: musicFolders,
startIndex: data.StartIndex,
totalRecordCount: data.TotalRecordCount,
};
}; };
const getGenreList = async (args: GenreListArgs): Promise<JFGenreList> => { const getGenreList = async (args: GenreListArgs): Promise<JFGenreList> => {

View file

@ -7,11 +7,7 @@ export interface JFMusicFolderListResponse extends JFBasePaginatedResponse {
Items: JFMusicFolder[]; Items: JFMusicFolder[];
} }
export type JFMusicFolderList = { export type JFMusicFolderList = JFMusicFolder[];
items: JFMusicFolder[];
startIndex: number;
totalRecordCount: number;
};
export interface JFGenreListResponse extends JFBasePaginatedResponse { export interface JFGenreListResponse extends JFBasePaginatedResponse {
Items: JFGenre[]; Items: JFGenre[];
@ -506,6 +502,7 @@ export type JFAlbumListParams = {
filters?: string; filters?: string;
genres?: string; genres?: string;
includeItemTypes: 'MusicAlbum'; includeItemTypes: 'MusicAlbum';
searchTerm?: string;
sortBy?: JFAlbumListSort; sortBy?: JFAlbumListSort;
years?: string; years?: string;
} & JFBaseParams & } & JFBaseParams &
@ -528,6 +525,7 @@ export type JFSongListParams = {
filters?: string; filters?: string;
genres?: string; genres?: string;
includeItemTypes: 'Audio'; includeItemTypes: 'Audio';
searchTerm?: string;
sortBy?: JFSongListSort; sortBy?: JFSongListSort;
years?: string; years?: string;
} & JFBaseParams & } & JFBaseParams &

View file

@ -1,8 +1,13 @@
import { jfNormalize } from '/@/renderer/api/jellyfin.api'; import { jfNormalize } from '/@/renderer/api/jellyfin.api';
import type { JFAlbum, JFSong } from '/@/renderer/api/jellyfin.types'; import type { JFAlbum, JFMusicFolderList, JFSong } from '/@/renderer/api/jellyfin.types';
import { ndNormalize } from '/@/renderer/api/navidrome.api'; import { ndNormalize } from '/@/renderer/api/navidrome.api';
import type { NDAlbum, NDSong } from '/@/renderer/api/navidrome.types'; import type { NDAlbum, NDSong } from '/@/renderer/api/navidrome.types';
import type { RawAlbumListResponse, RawSongListResponse } from '/@/renderer/api/types'; import { SSMusicFolderList } from '/@/renderer/api/subsonic.types';
import type {
RawAlbumListResponse,
RawMusicFolderListResponse,
RawSongListResponse,
} from '/@/renderer/api/types';
import { ServerListItem } from '/@/renderer/types'; import { ServerListItem } from '/@/renderer/types';
const albumList = (data: RawAlbumListResponse | undefined, server: ServerListItem | null) => { const albumList = (data: RawAlbumListResponse | undefined, server: ServerListItem | null) => {
@ -45,7 +50,37 @@ const songList = (data: RawSongListResponse | undefined, server: ServerListItem
}; };
}; };
const musicFolderList = (
data: RawMusicFolderListResponse | undefined,
server: ServerListItem | null,
) => {
let musicFolders;
switch (server?.type) {
case 'jellyfin':
musicFolders = (data as JFMusicFolderList)?.map((item) => ({
id: String(item.Id),
name: item.Name,
}));
break;
case 'navidrome':
musicFolders = (data as SSMusicFolderList)?.map((item) => ({
id: String(item.id),
name: item.name,
}));
break;
case 'subsonic':
musicFolders = (data as SSMusicFolderList)?.map((item) => ({
id: String(item.id),
name: item.name,
}));
break;
}
return musicFolders;
};
export const normalize = { export const normalize = {
albumList, albumList,
musicFolderList,
songList, songList,
}; };

View file

@ -1,5 +1,4 @@
import type { AlbumListQuery, SongListQuery } from './types'; import type { AlbumListQuery, SongListQuery, AlbumDetailQuery } from './types';
import type { AlbumDetailQuery } from './types';
export const queryKeys = { export const queryKeys = {
albums: { albums: {
@ -15,6 +14,9 @@ export const queryKeys = {
list: (serverId: string) => [serverId, 'genres', 'list'] as const, list: (serverId: string) => [serverId, 'genres', 'list'] as const,
root: (serverId: string) => [serverId, 'genres'] as const, root: (serverId: string) => [serverId, 'genres'] as const,
}, },
musicFolders: {
list: (serverId: string) => [serverId, 'musicFolders', 'list'] as const,
},
server: { server: {
root: (serverId: string) => [serverId] as const, root: (serverId: string) => [serverId] as const,
}, },

View file

@ -31,6 +31,7 @@ import type {
FavoriteArgs, FavoriteArgs,
FavoriteResponse, FavoriteResponse,
GenreListArgs, GenreListArgs,
MusicFolderListArgs,
RatingArgs, RatingArgs,
} from '/@/renderer/api/types'; } from '/@/renderer/api/types';
import { useAuthStore } from '/@/renderer/store'; import { useAuthStore } from '/@/renderer/store';
@ -126,13 +127,12 @@ const authenticate = async (
}; };
}; };
const getMusicFolderList = async ( const getMusicFolderList = async (args: MusicFolderListArgs): Promise<SSMusicFolderList> => {
server: any, const { signal, server } = args;
signal?: AbortSignal,
): Promise<SSMusicFolderList> => {
const data = await api const data = await api
.get('rest/getMusicFolders.view', { .get('rest/getMusicFolders.view', {
prefixUrl: server.url, prefixUrl: server?.url,
signal, signal,
}) })
.json<SSMusicFolderListResponse>(); .json<SSMusicFolderListResponse>();
@ -143,7 +143,7 @@ const getMusicFolderList = async (
export const getAlbumArtistDetail = async ( export const getAlbumArtistDetail = async (
args: AlbumArtistDetailArgs, args: AlbumArtistDetailArgs,
): Promise<SSAlbumArtistDetail> => { ): Promise<SSAlbumArtistDetail> => {
const { signal, query } = args; const { server, signal, query } = args;
const searchParams: SSAlbumArtistDetailParams = { const searchParams: SSAlbumArtistDetailParams = {
id: query.id, id: query.id,
@ -151,6 +151,7 @@ export const getAlbumArtistDetail = async (
const data = await api const data = await api
.get('/getArtist.view', { .get('/getArtist.view', {
prefixUrl: server?.url,
searchParams, searchParams,
signal, signal,
}) })
@ -160,14 +161,15 @@ export const getAlbumArtistDetail = async (
}; };
const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<SSAlbumArtistList> => { const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<SSAlbumArtistList> => {
const { signal, query } = args; const { signal, server, query } = args;
const searchParams: SSAlbumArtistListParams = { const searchParams: SSAlbumArtistListParams = {
musicFolderId: query.musicFolderId, musicFolderId: query.musicFolderId,
}; };
const data = await api const data = await api
.get('/rest/getArtists.view', { .get('rest/getArtists.view', {
prefixUrl: server?.url,
searchParams, searchParams,
signal, signal,
}) })
@ -179,10 +181,11 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<SSAlbumArt
}; };
const getGenreList = async (args: GenreListArgs): Promise<SSGenreList> => { const getGenreList = async (args: GenreListArgs): Promise<SSGenreList> => {
const { signal } = args; const { server, signal } = args;
const data = await api const data = await api
.get('/rest/getGenres.view', { .get('rest/getGenres.view', {
prefixUrl: server?.url,
signal, signal,
}) })
.json<SSGenreListResponse>(); .json<SSGenreListResponse>();
@ -191,10 +194,11 @@ const getGenreList = async (args: GenreListArgs): Promise<SSGenreList> => {
}; };
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<SSAlbumDetail> => { const getAlbumDetail = async (args: AlbumDetailArgs): Promise<SSAlbumDetail> => {
const { query, signal } = args; const { server, query, signal } = args;
const data = await api const data = await api
.get('/rest/getAlbum.view', { .get('rest/getAlbum.view', {
prefixUrl: server?.url,
searchParams: { id: query.id }, searchParams: { id: query.id },
signal, signal,
}) })
@ -205,11 +209,12 @@ const getAlbumDetail = async (args: AlbumDetailArgs): Promise<SSAlbumDetail> =>
}; };
const getAlbumList = async (args: AlbumListArgs): Promise<SSAlbumList> => { const getAlbumList = async (args: AlbumListArgs): Promise<SSAlbumList> => {
const { query, signal } = args; const { server, query, signal } = args;
const normalizedParams = {}; const normalizedParams = {};
const data = await api const data = await api
.get('/rest/getAlbumList2.view', { .get('rest/getAlbumList2.view', {
prefixUrl: server?.url,
searchParams: normalizedParams, searchParams: normalizedParams,
signal, signal,
}) })
@ -223,7 +228,7 @@ const getAlbumList = async (args: AlbumListArgs): Promise<SSAlbumList> => {
}; };
const createFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => { const createFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
const { query, signal } = args; const { server, query, signal } = args;
const searchParams: SSFavoriteParams = { const searchParams: SSFavoriteParams = {
albumId: query.type === 'album' ? query.id : undefined, albumId: query.type === 'album' ? query.id : undefined,
@ -232,7 +237,8 @@ const createFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> =>
}; };
await api await api
.get('/rest/star.view', { .get('rest/star.view', {
prefixUrl: server?.url,
searchParams, searchParams,
signal, signal,
}) })
@ -244,7 +250,7 @@ const createFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> =>
}; };
const deleteFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => { const deleteFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
const { query, signal } = args; const { server, query, signal } = args;
const searchParams: SSFavoriteParams = { const searchParams: SSFavoriteParams = {
albumId: query.type === 'album' ? query.id : undefined, albumId: query.type === 'album' ? query.id : undefined,
@ -253,7 +259,8 @@ const deleteFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> =>
}; };
await api await api
.get('/rest/unstar.view', { .get('rest/unstar.view', {
prefixUrl: server?.url,
searchParams, searchParams,
signal, signal,
}) })
@ -265,7 +272,7 @@ const deleteFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> =>
}; };
const updateRating = async (args: RatingArgs) => { const updateRating = async (args: RatingArgs) => {
const { query, signal } = args; const { server, query, signal } = args;
const searchParams: SSRatingParams = { const searchParams: SSRatingParams = {
id: query.id, id: query.id,
@ -273,7 +280,8 @@ const updateRating = async (args: RatingArgs) => {
}; };
const data = await api const data = await api
.get('/rest/setRating.view', { .get('rest/setRating.view', {
prefixUrl: server?.url,
searchParams, searchParams,
signal, signal,
}) })

View file

@ -779,11 +779,7 @@ export type RawMusicFolderListResponse = SSMusicFolderList | JFMusicFolderList |
export type MusicFolderListResponse = BasePaginatedResponse<Playlist[]>; export type MusicFolderListResponse = BasePaginatedResponse<Playlist[]>;
export type MusicFolderListQuery = { export type MusicFolderListArgs = BaseEndpointArgs;
id: string;
};
export type MusicFolderListArgs = { query: MusicFolderListQuery } & BaseEndpointArgs;
// Create Favorite // Create Favorite
export type RawCreateFavoriteResponse = CreateFavoriteResponse | undefined; export type RawCreateFavoriteResponse = CreateFavoriteResponse | undefined;

View file

@ -7,6 +7,7 @@ import { AlbumListSort, SortOrder } from '/@/renderer/api/types';
import { Button, DropdownMenu, PageHeader } from '/@/renderer/components'; import { Button, DropdownMenu, PageHeader } from '/@/renderer/components';
import { useCurrentServer, useAppStoreActions, useAlbumRouteStore } from '/@/renderer/store'; import { useCurrentServer, useAppStoreActions, useAlbumRouteStore } from '/@/renderer/store';
import { CardDisplayType } from '/@/renderer/types'; import { CardDisplayType } from '/@/renderer/types';
import { useMusicFolders } from '/@/renderer/features/shared';
const FILTERS = { const FILTERS = {
jellyfin: [ jellyfin: [
@ -44,6 +45,8 @@ export const AlbumListHeader = () => {
const page = useAlbumRouteStore(); const page = useAlbumRouteStore();
const filters = page.list.filter; const filters = page.list.filter;
const musicFoldersQuery = useMusicFolders();
const sortByLabel = const sortByLabel =
(server?.type && (server?.type &&
(FILTERS[server.type as keyof typeof FILTERS] as { name: string; value: string }[]).find( (FILTERS[server.type as keyof typeof FILTERS] as { name: string; value: string }[]).find(
@ -78,6 +81,22 @@ export const AlbumListHeader = () => {
[page.list, setPage], [page.list, setPage],
); );
const handleSetMusicFolder = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
if (!e.currentTarget?.value) return;
setPage('albums', {
list: {
...page.list,
filter: {
...page.list.filter,
musicFolderId: e.currentTarget.value,
},
},
});
},
[page.list, setPage],
);
const handleSetOrder = useCallback( const handleSetOrder = useCallback(
(e: MouseEvent<HTMLButtonElement>) => { (e: MouseEvent<HTMLButtonElement>) => {
if (!e.currentTarget?.value) return; if (!e.currentTarget?.value) return;
@ -236,19 +255,18 @@ export const AlbumListHeader = () => {
Folder Folder
</Button> </Button>
</DropdownMenu.Target> </DropdownMenu.Target>
{/* <DropdownMenu.Dropdown> <DropdownMenu.Dropdown>
{serverFolders?.map((folder) => ( {musicFoldersQuery.data?.map((folder) => (
<DropdownMenu.Item <DropdownMenu.Item
key={folder.id} key={`musicFolder-${folder.id}`}
$isActive={filters.serverFolderId.includes(folder.id)} $isActive={filters.musicFolderId === folder.id}
closeMenuOnClick={false} value={folder.id}
value={folder.id} onClick={handleSetMusicFolder}
onClick={handleSetServerFolder} >
> {folder.name}
{folder.name} </DropdownMenu.Item>
</DropdownMenu.Item> ))}
))} </DropdownMenu.Dropdown>
</DropdownMenu.Dropdown> */}
</DropdownMenu> </DropdownMenu>
</Group> </Group>
</PageHeader> </PageHeader>

View file

@ -29,6 +29,7 @@ const AlbumListRoute = () => {
const albumListQuery = useAlbumList({ const albumListQuery = useAlbumList({
limit: 1, limit: 1,
musicFolderId: filters.musicFolderId,
sortBy: filters.sortBy, sortBy: filters.sortBy,
sortOrder: filters.sortOrder, sortOrder: filters.sortOrder,
startIndex: 0, startIndex: 0,
@ -46,6 +47,7 @@ const AlbumListRoute = () => {
controller.getAlbumList({ controller.getAlbumList({
query: { query: {
limit: take, limit: take,
musicFolderId: filters.musicFolderId,
sortBy: filters.sortBy, sortBy: filters.sortBy,
sortOrder: filters.sortOrder, sortOrder: filters.sortOrder,
startIndex: skip, startIndex: skip,
@ -111,6 +113,7 @@ const AlbumListRoute = () => {
itemSize={150 + page.list?.size} itemSize={150 + page.list?.size}
itemType={LibraryItem.ALBUM} itemType={LibraryItem.ALBUM}
minimumBatchSize={40} minimumBatchSize={40}
refresh={filters.musicFolderId}
route={{ route={{
route: AppRoute.LIBRARY_ALBUMS_DETAIL, route: AppRoute.LIBRARY_ALBUMS_DETAIL,
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }], slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],

View file

@ -1 +1,2 @@
export * from './components/animated-page'; export * from './components/animated-page';
export * from './queries/music-folders-query';

View file

@ -0,0 +1,24 @@
import { useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { useCurrentServer } from '/@/renderer/store';
import { RawMusicFolderListResponse } from '/@/renderer/api/types';
export const useMusicFolders = () => {
const server = useCurrentServer();
const query = useQuery({
enabled: !!server?.id,
queryFn: ({ signal }) => api.controller.getMusicFolderList({ server, signal }),
queryKey: queryKeys.musicFolders.list(server?.id || ''),
select: useCallback(
(data: RawMusicFolderListResponse | undefined) => {
return api.normalize.musicFolderList(data, server);
},
[server],
),
});
return query;
};