From a354cab797160810c58e46db4a845d43645e401c Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 20 Dec 2022 19:11:33 -0800 Subject: [PATCH] Add music folders query --- src/renderer/api/controller.ts | 9 +++- src/renderer/api/jellyfin.api.ts | 10 ++-- src/renderer/api/jellyfin.types.ts | 8 ++-- src/renderer/api/normalize.ts | 39 ++++++++++++++- src/renderer/api/query-keys.ts | 6 ++- src/renderer/api/subsonic.api.ts | 48 +++++++++++-------- src/renderer/api/types.ts | 6 +-- .../albums/components/album-list-header.tsx | 44 ++++++++++++----- .../albums/routes/album-list-route.tsx | 3 ++ src/renderer/features/shared/index.ts | 1 + .../shared/queries/music-folders-query.ts | 24 ++++++++++ 11 files changed, 143 insertions(+), 55 deletions(-) create mode 100644 src/renderer/features/shared/queries/music-folders-query.ts diff --git a/src/renderer/api/controller.ts b/src/renderer/api/controller.ts index 7f3e1e7b..aa0a1b46 100644 --- a/src/renderer/api/controller.ts +++ b/src/renderer/api/controller.ts @@ -114,7 +114,7 @@ const endpoints: ApiController = { getFolderList: undefined, getFolderSongs: undefined, getGenreList: navidromeApi.getGenreList, - getMusicFolderList: undefined, + getMusicFolderList: subsonicApi.getMusicFolderList, getPlaylistDetail: navidromeApi.getPlaylistDetail, getPlaylistList: navidromeApi.getPlaylistList, getPlaylistSongList: navidromeApi.getPlaylistSongList, @@ -140,7 +140,7 @@ const endpoints: ApiController = { getFolderList: undefined, getFolderSongs: undefined, getGenreList: undefined, - getMusicFolderList: undefined, + getMusicFolderList: subsonicApi.getMusicFolderList, getPlaylistDetail: undefined, getPlaylistList: undefined, getSongDetail: undefined, @@ -183,8 +183,13 @@ const getSongList = async (args: SongListArgs) => { return (apiController('getSongList') as ControllerEndpoint['getSongList'])?.(args); }; +const getMusicFolderList = async (args: MusicFolderListArgs) => { + return (apiController('getMusicFolderList') as ControllerEndpoint['getMusicFolderList'])?.(args); +}; + export const controller = { getAlbumDetail, getAlbumList, + getMusicFolderList, getSongList, }; diff --git a/src/renderer/api/jellyfin.api.ts b/src/renderer/api/jellyfin.api.ts index b9396d8d..fbdfae95 100644 --- a/src/renderer/api/jellyfin.api.ts +++ b/src/renderer/api/jellyfin.api.ts @@ -95,11 +95,13 @@ const authenticate = async ( }; const getMusicFolderList = async (args: MusicFolderListArgs): Promise => { - const { signal } = args; + const { server, signal } = args; const userId = useAuthStore.getState().currentServer?.userId; const data = await api .get(`users/${userId}/items`, { + headers: { 'X-MediaBrowser-Token': server?.credential }, + prefixUrl: server?.url, signal, }) .json(); @@ -108,11 +110,7 @@ const getMusicFolderList = async (args: MusicFolderListArgs): Promise folder.CollectionType === JFCollectionType.MUSIC, ); - return { - items: musicFolders, - startIndex: data.StartIndex, - totalRecordCount: data.TotalRecordCount, - }; + return musicFolders; }; const getGenreList = async (args: GenreListArgs): Promise => { diff --git a/src/renderer/api/jellyfin.types.ts b/src/renderer/api/jellyfin.types.ts index 9938e2c9..6b484f1d 100644 --- a/src/renderer/api/jellyfin.types.ts +++ b/src/renderer/api/jellyfin.types.ts @@ -7,11 +7,7 @@ export interface JFMusicFolderListResponse extends JFBasePaginatedResponse { Items: JFMusicFolder[]; } -export type JFMusicFolderList = { - items: JFMusicFolder[]; - startIndex: number; - totalRecordCount: number; -}; +export type JFMusicFolderList = JFMusicFolder[]; export interface JFGenreListResponse extends JFBasePaginatedResponse { Items: JFGenre[]; @@ -506,6 +502,7 @@ export type JFAlbumListParams = { filters?: string; genres?: string; includeItemTypes: 'MusicAlbum'; + searchTerm?: string; sortBy?: JFAlbumListSort; years?: string; } & JFBaseParams & @@ -528,6 +525,7 @@ export type JFSongListParams = { filters?: string; genres?: string; includeItemTypes: 'Audio'; + searchTerm?: string; sortBy?: JFSongListSort; years?: string; } & JFBaseParams & diff --git a/src/renderer/api/normalize.ts b/src/renderer/api/normalize.ts index de20dffa..75cc0d8c 100644 --- a/src/renderer/api/normalize.ts +++ b/src/renderer/api/normalize.ts @@ -1,8 +1,13 @@ 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 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'; 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 = { albumList, + musicFolderList, songList, }; diff --git a/src/renderer/api/query-keys.ts b/src/renderer/api/query-keys.ts index 7b96cd97..e46cfeea 100644 --- a/src/renderer/api/query-keys.ts +++ b/src/renderer/api/query-keys.ts @@ -1,5 +1,4 @@ -import type { AlbumListQuery, SongListQuery } from './types'; -import type { AlbumDetailQuery } from './types'; +import type { AlbumListQuery, SongListQuery, AlbumDetailQuery } from './types'; export const queryKeys = { albums: { @@ -15,6 +14,9 @@ export const queryKeys = { list: (serverId: string) => [serverId, 'genres', 'list'] as const, root: (serverId: string) => [serverId, 'genres'] as const, }, + musicFolders: { + list: (serverId: string) => [serverId, 'musicFolders', 'list'] as const, + }, server: { root: (serverId: string) => [serverId] as const, }, diff --git a/src/renderer/api/subsonic.api.ts b/src/renderer/api/subsonic.api.ts index 5898a29e..31ff9c22 100644 --- a/src/renderer/api/subsonic.api.ts +++ b/src/renderer/api/subsonic.api.ts @@ -31,6 +31,7 @@ import type { FavoriteArgs, FavoriteResponse, GenreListArgs, + MusicFolderListArgs, RatingArgs, } from '/@/renderer/api/types'; import { useAuthStore } from '/@/renderer/store'; @@ -126,13 +127,12 @@ const authenticate = async ( }; }; -const getMusicFolderList = async ( - server: any, - signal?: AbortSignal, -): Promise => { +const getMusicFolderList = async (args: MusicFolderListArgs): Promise => { + const { signal, server } = args; + const data = await api .get('rest/getMusicFolders.view', { - prefixUrl: server.url, + prefixUrl: server?.url, signal, }) .json(); @@ -143,7 +143,7 @@ const getMusicFolderList = async ( export const getAlbumArtistDetail = async ( args: AlbumArtistDetailArgs, ): Promise => { - const { signal, query } = args; + const { server, signal, query } = args; const searchParams: SSAlbumArtistDetailParams = { id: query.id, @@ -151,6 +151,7 @@ export const getAlbumArtistDetail = async ( const data = await api .get('/getArtist.view', { + prefixUrl: server?.url, searchParams, signal, }) @@ -160,14 +161,15 @@ export const getAlbumArtistDetail = async ( }; const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise => { - const { signal, query } = args; + const { signal, server, query } = args; const searchParams: SSAlbumArtistListParams = { musicFolderId: query.musicFolderId, }; const data = await api - .get('/rest/getArtists.view', { + .get('rest/getArtists.view', { + prefixUrl: server?.url, searchParams, signal, }) @@ -179,10 +181,11 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise => { - const { signal } = args; + const { server, signal } = args; const data = await api - .get('/rest/getGenres.view', { + .get('rest/getGenres.view', { + prefixUrl: server?.url, signal, }) .json(); @@ -191,10 +194,11 @@ const getGenreList = async (args: GenreListArgs): Promise => { }; const getAlbumDetail = async (args: AlbumDetailArgs): Promise => { - const { query, signal } = args; + const { server, query, signal } = args; const data = await api - .get('/rest/getAlbum.view', { + .get('rest/getAlbum.view', { + prefixUrl: server?.url, searchParams: { id: query.id }, signal, }) @@ -205,11 +209,12 @@ const getAlbumDetail = async (args: AlbumDetailArgs): Promise => }; const getAlbumList = async (args: AlbumListArgs): Promise => { - const { query, signal } = args; + const { server, query, signal } = args; const normalizedParams = {}; const data = await api - .get('/rest/getAlbumList2.view', { + .get('rest/getAlbumList2.view', { + prefixUrl: server?.url, searchParams: normalizedParams, signal, }) @@ -223,7 +228,7 @@ const getAlbumList = async (args: AlbumListArgs): Promise => { }; const createFavorite = async (args: FavoriteArgs): Promise => { - const { query, signal } = args; + const { server, query, signal } = args; const searchParams: SSFavoriteParams = { albumId: query.type === 'album' ? query.id : undefined, @@ -232,7 +237,8 @@ const createFavorite = async (args: FavoriteArgs): Promise => }; await api - .get('/rest/star.view', { + .get('rest/star.view', { + prefixUrl: server?.url, searchParams, signal, }) @@ -244,7 +250,7 @@ const createFavorite = async (args: FavoriteArgs): Promise => }; const deleteFavorite = async (args: FavoriteArgs): Promise => { - const { query, signal } = args; + const { server, query, signal } = args; const searchParams: SSFavoriteParams = { albumId: query.type === 'album' ? query.id : undefined, @@ -253,7 +259,8 @@ const deleteFavorite = async (args: FavoriteArgs): Promise => }; await api - .get('/rest/unstar.view', { + .get('rest/unstar.view', { + prefixUrl: server?.url, searchParams, signal, }) @@ -265,7 +272,7 @@ const deleteFavorite = async (args: FavoriteArgs): Promise => }; const updateRating = async (args: RatingArgs) => { - const { query, signal } = args; + const { server, query, signal } = args; const searchParams: SSRatingParams = { id: query.id, @@ -273,7 +280,8 @@ const updateRating = async (args: RatingArgs) => { }; const data = await api - .get('/rest/setRating.view', { + .get('rest/setRating.view', { + prefixUrl: server?.url, searchParams, signal, }) diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index d175377f..adb01bcd 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -779,11 +779,7 @@ export type RawMusicFolderListResponse = SSMusicFolderList | JFMusicFolderList | export type MusicFolderListResponse = BasePaginatedResponse; -export type MusicFolderListQuery = { - id: string; -}; - -export type MusicFolderListArgs = { query: MusicFolderListQuery } & BaseEndpointArgs; +export type MusicFolderListArgs = BaseEndpointArgs; // Create Favorite export type RawCreateFavoriteResponse = CreateFavoriteResponse | undefined; diff --git a/src/renderer/features/albums/components/album-list-header.tsx b/src/renderer/features/albums/components/album-list-header.tsx index eba9d5c7..f01ff0fa 100644 --- a/src/renderer/features/albums/components/album-list-header.tsx +++ b/src/renderer/features/albums/components/album-list-header.tsx @@ -7,6 +7,7 @@ import { AlbumListSort, SortOrder } from '/@/renderer/api/types'; import { Button, DropdownMenu, PageHeader } from '/@/renderer/components'; import { useCurrentServer, useAppStoreActions, useAlbumRouteStore } from '/@/renderer/store'; import { CardDisplayType } from '/@/renderer/types'; +import { useMusicFolders } from '/@/renderer/features/shared'; const FILTERS = { jellyfin: [ @@ -44,6 +45,8 @@ export const AlbumListHeader = () => { const page = useAlbumRouteStore(); const filters = page.list.filter; + const musicFoldersQuery = useMusicFolders(); + const sortByLabel = (server?.type && (FILTERS[server.type as keyof typeof FILTERS] as { name: string; value: string }[]).find( @@ -78,6 +81,22 @@ export const AlbumListHeader = () => { [page.list, setPage], ); + const handleSetMusicFolder = useCallback( + (e: MouseEvent) => { + if (!e.currentTarget?.value) return; + setPage('albums', { + list: { + ...page.list, + filter: { + ...page.list.filter, + musicFolderId: e.currentTarget.value, + }, + }, + }); + }, + [page.list, setPage], + ); + const handleSetOrder = useCallback( (e: MouseEvent) => { if (!e.currentTarget?.value) return; @@ -236,19 +255,18 @@ export const AlbumListHeader = () => { Folder - {/* - {serverFolders?.map((folder) => ( - - {folder.name} - - ))} - */} + + {musicFoldersQuery.data?.map((folder) => ( + + {folder.name} + + ))} + diff --git a/src/renderer/features/albums/routes/album-list-route.tsx b/src/renderer/features/albums/routes/album-list-route.tsx index bbd8a93d..e646f231 100644 --- a/src/renderer/features/albums/routes/album-list-route.tsx +++ b/src/renderer/features/albums/routes/album-list-route.tsx @@ -29,6 +29,7 @@ const AlbumListRoute = () => { const albumListQuery = useAlbumList({ limit: 1, + musicFolderId: filters.musicFolderId, sortBy: filters.sortBy, sortOrder: filters.sortOrder, startIndex: 0, @@ -46,6 +47,7 @@ const AlbumListRoute = () => { controller.getAlbumList({ query: { limit: take, + musicFolderId: filters.musicFolderId, sortBy: filters.sortBy, sortOrder: filters.sortOrder, startIndex: skip, @@ -111,6 +113,7 @@ const AlbumListRoute = () => { itemSize={150 + page.list?.size} itemType={LibraryItem.ALBUM} minimumBatchSize={40} + refresh={filters.musicFolderId} route={{ route: AppRoute.LIBRARY_ALBUMS_DETAIL, slugs: [{ idProperty: 'id', slugProperty: 'albumId' }], diff --git a/src/renderer/features/shared/index.ts b/src/renderer/features/shared/index.ts index 3be457f4..7a034b9a 100644 --- a/src/renderer/features/shared/index.ts +++ b/src/renderer/features/shared/index.ts @@ -1 +1,2 @@ export * from './components/animated-page'; +export * from './queries/music-folders-query'; diff --git a/src/renderer/features/shared/queries/music-folders-query.ts b/src/renderer/features/shared/queries/music-folders-query.ts new file mode 100644 index 00000000..cca19f03 --- /dev/null +++ b/src/renderer/features/shared/queries/music-folders-query.ts @@ -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; +};