From d5bbff5eb6aba669244e2b585acf5cbcfbbce9cd Mon Sep 17 00:00:00 2001 From: jeffvli Date: Mon, 24 Oct 2022 21:47:03 -0700 Subject: [PATCH] Update frontend API structure --- src/renderer/api/albums.api.ts | 56 +++++ src/renderer/api/albumsApi.ts | 28 --- src/renderer/api/{authApi.ts => auth.api.ts} | 0 src/renderer/api/index.ts | 11 + src/renderer/api/queries/useAlbum.ts | 10 - src/renderer/api/query-keys.ts | 19 ++ src/renderer/api/queryKeys.ts | 8 - src/renderer/api/servers.api.ts | 89 +++++++ src/renderer/api/serversApi.ts | 22 -- src/renderer/api/shared.api.ts | 158 ++++++++++++ src/renderer/api/subsonic.ts | 0 src/renderer/api/types.ts | 240 ++++++++++++++++--- src/renderer/api/users.api.ts | 20 ++ src/renderer/api/usersApi.ts | 11 - 14 files changed, 561 insertions(+), 111 deletions(-) create mode 100644 src/renderer/api/albums.api.ts delete mode 100644 src/renderer/api/albumsApi.ts rename src/renderer/api/{authApi.ts => auth.api.ts} (100%) create mode 100644 src/renderer/api/index.ts delete mode 100644 src/renderer/api/queries/useAlbum.ts create mode 100644 src/renderer/api/query-keys.ts delete mode 100644 src/renderer/api/queryKeys.ts create mode 100644 src/renderer/api/servers.api.ts delete mode 100644 src/renderer/api/serversApi.ts create mode 100644 src/renderer/api/shared.api.ts delete mode 100644 src/renderer/api/subsonic.ts create mode 100644 src/renderer/api/users.api.ts delete mode 100644 src/renderer/api/usersApi.ts diff --git a/src/renderer/api/albums.api.ts b/src/renderer/api/albums.api.ts new file mode 100644 index 000000000..99c91bfe5 --- /dev/null +++ b/src/renderer/api/albums.api.ts @@ -0,0 +1,56 @@ +import { ax } from '@/renderer/lib/axios'; +import { SortOrder } from '@/types'; +import { + AlbumDetailResponse, + AlbumListResponse, + PaginationParams, +} from './types'; + +export enum AlbumSort { + DATE_ADDED = 'added', + DATE_ADDED_REMOTE = 'addedRemote', + DATE_RELEASED = 'released', + DATE_RELEASED_YEAR = 'year', + FAVORITE = 'favorite', + NAME = 'name', + RANDOM = 'random', + RATING = 'rating', +} + +export type AlbumListParams = PaginationParams & { + orderBy: SortOrder; + serverFolderId?: string[]; + serverUrlId?: string; + sortBy: AlbumSort; +}; + +const getAlbumDetail = async ( + query: { albumId: number; serverId: string }, + signal?: AbortSignal +) => { + const { data } = await ax.get( + `/servers/${query.serverId}/albums/${query.albumId}`, + { signal } + ); + return data; +}; + +const getAlbumList = async ( + query: { serverId: string }, + params: AlbumListParams, + signal?: AbortSignal +) => { + const { data } = await ax.get( + `/servers/${query.serverId}/albums`, + { + params, + signal, + } + ); + return data; +}; + +export const albumsApi = { + getAlbumDetail, + getAlbumList, +}; diff --git a/src/renderer/api/albumsApi.ts b/src/renderer/api/albumsApi.ts deleted file mode 100644 index bc15d19e4..000000000 --- a/src/renderer/api/albumsApi.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { api } from '../lib'; -import { AlbumResponse, AlbumsResponse, BasePaginationRequest } from './types'; - -export interface AlbumsRequest extends BasePaginationRequest { - orderBy: string; - serverFolderIds?: string; - sortBy: string; -} - -const getAlbum = async (params: { id: number }, signal?: AbortSignal) => { - const { data } = await api.get(`/albums/${params.id}`, { - signal, - }); - return data; -}; - -const getAlbums = async (params: AlbumsRequest, signal?: AbortSignal) => { - const { data } = await api.get(`/albums`, { - params, - signal, - }); - return data; -}; - -export const albumsApi = { - getAlbum, - getAlbums, -}; diff --git a/src/renderer/api/authApi.ts b/src/renderer/api/auth.api.ts similarity index 100% rename from src/renderer/api/authApi.ts rename to src/renderer/api/auth.api.ts diff --git a/src/renderer/api/index.ts b/src/renderer/api/index.ts new file mode 100644 index 000000000..ec92fb921 --- /dev/null +++ b/src/renderer/api/index.ts @@ -0,0 +1,11 @@ +import { albumsApi } from './albums.api'; +import { authApi } from './auth.api'; +import { serversApi } from './servers.api'; +import { usersApi } from './users.api'; + +export const api = { + albums: albumsApi, + auth: authApi, + servers: serversApi, + users: usersApi, +}; diff --git a/src/renderer/api/queries/useAlbum.ts b/src/renderer/api/queries/useAlbum.ts deleted file mode 100644 index 2919d1599..000000000 --- a/src/renderer/api/queries/useAlbum.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useQuery } from 'react-query'; -import { albumsApi } from '../albumsApi'; -import { queryKeys } from '../queryKeys'; - -export const useAlbum = (albumId: number) => { - return useQuery({ - queryFn: () => albumsApi.getAlbum(albumId), - queryKey: queryKeys.album(albumId), - }); -}; diff --git a/src/renderer/api/query-keys.ts b/src/renderer/api/query-keys.ts new file mode 100644 index 000000000..8fc3ab8cb --- /dev/null +++ b/src/renderer/api/query-keys.ts @@ -0,0 +1,19 @@ +import { AlbumListParams } from './albums.api'; + +export const queryKeys = { + albums: { + detail: (albumId: string) => ['albums', albumId] as const, + list: (params: AlbumListParams) => ['albums', 'list', params] as const, + root: ['albums'], + songList: (albumId: string) => ['albums', albumId, 'songs'] as const, + }, + ping: (url: string) => ['ping', url] as const, + servers: { + list: () => ['servers', 'list'] as const, + }, + users: { + detail: (userId: string) => ['users', userId] as const, + list: (params: any) => ['users', 'list', params] as const, + root: ['users'], + }, +}; diff --git a/src/renderer/api/queryKeys.ts b/src/renderer/api/queryKeys.ts deleted file mode 100644 index 8fefab9e9..000000000 --- a/src/renderer/api/queryKeys.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { AlbumsRequest } from './albumsApi'; - -export const queryKeys = { - album: (albumId: number) => ['album', albumId] as const, - albums: (params: AlbumsRequest) => ['albums', params] as const, - ping: (url: string) => ['ping', url] as const, - servers: ['servers'] as const, -}; diff --git a/src/renderer/api/servers.api.ts b/src/renderer/api/servers.api.ts new file mode 100644 index 000000000..74104aec0 --- /dev/null +++ b/src/renderer/api/servers.api.ts @@ -0,0 +1,89 @@ +import { + BaseResponse, + NullResponse, + Server, + ServerType, + ServerUrl, +} from '@/renderer/api/types'; +import { ax } from '@/renderer/lib/axios'; + +export type ServerListResponse = BaseResponse; + +const getServerList = async (signal?: AbortSignal) => { + const { data } = await ax.get('/servers', { signal }); + return data; +}; + +export type CreateServerBody = { + legacy?: boolean; + name: string; + password: string; + type: ServerType; + url: string; + username: string; +}; + +export type ServerResponse = BaseResponse; + +const createServer = async (body: CreateServerBody) => { + const { data } = await ax.post('/servers', body); + return data; +}; + +const updateServer = async ( + query: { serverId: string }, + body: Partial +) => { + const { data } = await ax.patch( + `/servers/${query.serverId}`, + body + ); + return data; +}; + +export type CreateUrlBody = { + url: string; +}; + +export type UrlResponse = BaseResponse; + +const createUrl = async (query: { serverId: string }, body: CreateUrlBody) => { + const { data } = await ax.post( + `/servers/${query.serverId}/url`, + body + ); + return data; +}; + +const deleteUrl = async (query: { serverId: string; urlId: string }) => { + const { data } = await ax.delete( + `/servers/${query.serverId}/url/${query.urlId}` + ); + return data; +}; + +const enableUrl = async (query: { serverId: string; urlId: string }) => { + const { data } = await ax.post( + `/servers/${query.serverId}/url/${query.urlId}/enable`, + {} + ); + return data; +}; + +const disableUrl = async (query: { serverId: string; urlId: string }) => { + const { data } = await ax.post( + `/servers/${query.serverId}/url/${query.urlId}/disable`, + {} + ); + return data; +}; + +export const serversApi = { + createServer, + createUrl, + deleteUrl, + disableUrl, + enableUrl, + getServerList, + updateServer, +}; diff --git a/src/renderer/api/serversApi.ts b/src/renderer/api/serversApi.ts deleted file mode 100644 index 9dfcfdac3..000000000 --- a/src/renderer/api/serversApi.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { api } from '../lib'; - -const getServers = async () => { - const { data } = await api.get('/servers'); - return data; -}; - -const createServer = async (body: { - name: string; - remoteUserId: string; - token: string; - url: string; - username: string; -}) => { - const { data } = await api.post('/servers', body); - return data; -}; - -export const serversApi = { - createServer, - getServers, -}; diff --git a/src/renderer/api/shared.api.ts b/src/renderer/api/shared.api.ts new file mode 100644 index 000000000..567bd208f --- /dev/null +++ b/src/renderer/api/shared.api.ts @@ -0,0 +1,158 @@ +import axios from 'axios'; +import md5 from 'md5'; +import { ServerType } from '@/renderer/api/types'; +import { randomString } from '@/renderer/utils'; + +type JFAuthenticate = { + AccessToken: string; + ServerId: string; + SessionInfo: any; + User: any; +}; + +export const jfAuthenticate = async (options: { + password: string; + url: string; + username: string; +}) => { + const { password, url, username } = options; + const cleanServerUrl = url.replace(/\/$/, ''); + + const { data } = await axios.post( + `${cleanServerUrl}/users/authenticatebyname`, + { pw: password, username }, + { + headers: { + 'X-Emby-Authorization': `MediaBrowser Client="Sonixd", Device="PC", DeviceId="Sonixd", Version="1.0.0-alpha1"`, + }, + } + ); + + return data; +}; + +type NDAuthenticate = { + id: string; + isAdmin: boolean; + name: string; + subsonicSalt: string; + subsonicToken: string; + token: string; + username: string; +}; + +const ndAuthenticate = async (options: { + password: string; + url: string; + username: string; +}) => { + const { password, url, username } = options; + const cleanServerUrl = url.replace(/\/$/, ''); + + const { data } = await axios.post( + `${cleanServerUrl}/auth/login`, + { password, username } + ); + + return data; +}; + +const ssAuthenticate = async (options: { + legacy?: boolean; + password: string; + url: string; + username: string; +}) => { + let token; + + const cleanServerUrl = options.url.replace(/\/$/, ''); + + if (options.legacy) { + token = `u=${options.username}&p=${options.password}`; + } else { + const salt = randomString(); + const hash = md5(options.password + salt); + token = `u=${options.username}&s=${salt}&t=${hash}`; + } + + const { data } = await axios.get( + `${cleanServerUrl}/rest/ping.view?v=1.13.0&c=sonixd&f=json&${token}` + ); + + return { token, ...data }; +}; + +export const remoteServerLogin = async (options: { + legacy?: boolean; + password: string; + type: ServerType; + url: string; + username: string; +}) => { + if (options.type === ServerType.JELLYFIN) { + try { + const res = await jfAuthenticate({ + password: options.password, + url: options.url, + username: options.username, + }); + + return { + remoteUserId: res.User.Id, + token: res.AccessToken, + type: ServerType.JELLYFIN, + url: options.url, + username: options.username, + }; + } catch (err: any) { + return { message: err.message, type: 'error' }; + } + } + + if (options.type === ServerType.SUBSONIC) { + const res = await ssAuthenticate({ + legacy: options.legacy, + password: options.password, + url: options.url, + username: options.username, + }); + + if (res.status === 'failed') { + return { + message: 'Could not validate username and password', + type: 'error', + }; + } + + return { + remoteUserId: '', + token: res.token, + type: ServerType.SUBSONIC, + url: options.url, + username: options.username, + }; + } + + if (options.type === ServerType.NAVIDROME) { + try { + const res = await ndAuthenticate({ + password: options.password, + url: options.url, + username: options.username, + }); + + return { + remoteUserId: res.id, + token: `u=${res.name}&s=${res.subsonicSalt}&t=${res.subsonicToken}`, + // token: res.token, + type: ServerType.NAVIDROME, + url: options.url, + username: options.username, + }; + } catch (err: any) { + return { message: err.message, type: 'error' }; + } + } + + return { message: 'Not found', type: 'error' }; +}; diff --git a/src/renderer/api/subsonic.ts b/src/renderer/api/subsonic.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index 5a0aadd24..38352246c 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -1,4 +1,42 @@ -import { Album } from '../../types'; +export enum ServerType { + JELLYFIN = 'JELLYFIN', + NAVIDROME = 'NAVIDROME', + SUBSONIC = 'SUBSONIC', +} + +export enum ServerPermissionType { + ADMIN = 'ADMIN', + EDITOR = 'EDITOR', + VIEWER = 'VIEWER', +} + +export enum ExternalSource { + LASTFM = 'LASTFM', + MUSICBRAINZ = 'MUSICBRAINZ', + SPOTIFY = 'SPOTIFY', + THEAUDIODB = 'THEAUDIODB', +} + +export enum ExternalType { + ID = 'ID', + LINK = 'LINK', +} + +export enum ImageType { + BACKDROP = 'BACKDROP', + LOGO = 'LOGO', + PRIMARY = 'PRIMARY', + SCREENSHOT = 'SCREENSHOT', +} + +export enum TaskType { + FULL_SCAN = 'FULL_SCAN', + LASTFM = 'LASTFM', + MUSICBRAINZ = 'MUSICBRAINZ', + QUICK_SCAN = 'QUICK_SCAN', + REFRESH = 'REFRESH', + SPOTIFY = 'SPOTIFY', +} export interface BaseResponse { data: T; @@ -20,45 +58,108 @@ export interface BasePaginatedResponse { statusCode: number; } -export interface BasePaginationRequest { +export type ApiError = { + error: { + message: string; + path: string; + trace: string[]; + }; + response: string; + statusCode: number; +}; + +export type NullResponse = BaseResponse; + +export type PaginationParams = { skip: number; take: number; +}; + +export enum SortOrder { + ASC = 'asc', + DESC = 'desc', } -export type ServerResponse = { +export type Server = { createdAt: string; - id: number; + id: string; name: string; remoteUserId: string; - serverFolder?: ServerFolderResponse[]; - serverType: string; - token: string; + serverFolders?: RelatedServerFolder[]; + serverPermissions?: RelatedServerPermission[]; + serverUrls?: RelatedServerUrl[]; + token?: string; + type: ServerType; updatedAt: string; url: string; username: string; }; -export type ServerFolderResponse = { - createdAt: string; - enabled: boolean; - id: number; - isPublic: boolean; +export type RelatedServerFolder = { + id: string; + lastScannedAt: string | null; name: string; remoteId: string; - serverId: number; +}; + +export type ServerFolder = { + createdAt: string; + enabled: boolean; + id: string; + lastScannedAt: string | null; + name: string; + remoteId: string; + serverId: string; updatedAt: string; }; +export type ServerUrl = { + createdAt: string; + id: string; + serverId: string; + updatedAt: string; + url: string; +}; + +export type RelatedServerUrl = { + enabled: boolean; + id: string; + url: string; +}; + +export type RelatedServerPermission = { + id: string; + type: ServerPermissionType; +}; + export type User = { createdAt: string; enabled: boolean; - id: number; + flatServerPermissions: string[]; + id: string; isAdmin: boolean; - password: string; + password?: string; + serverFolderPermissions: ServerFolderPermission[]; + serverPermissions: ServerPermission[]; updatedAt: string; username: string; }; +export type ServerFolderPermission = { + createdAt: string; + id: string; + serverFolderId: string; + updatedAt: string; +}; + +export type ServerPermission = { + createdAt: string; + id: string; + serverId: string; + type: ServerPermissionType; + updatedAt: string; +}; + export type Login = { accessToken: string; refreshToken: string; @@ -70,49 +171,124 @@ export type Ping = { version: string; }; -export type GenreResponse = { +export type Genre = { createdAt: string; - id: number; + id: string; name: string; updatedAt: string; }; -export type ArtistResponse = { +export type RelatedGenre = { + id: string; + name: string; +}; + +export type External = { + createdAt: string; + id: string; + name: string; + updatedAt: string; + url: string; +}; + +export type Image = { + createdAt: string; + id: string; + name: string; + updatedAt: string; + url: string; +}; + +export type Album = { + albumArtists: RelatedArtist[]; + artists: RelatedArtist[]; + backdropImageUrl: string | null; + createdAt: string; + deleted: boolean; + genres: RelatedGenre[]; + id: string; + imageUrl: string | null; + isFavorite: boolean; + name: string; + rating: number | null; + releaseDate: string | null; + releaseYear: number | null; + remoteCreatedAt: string; + remoteId: string; + serverFolders: RelatedServerFolder[]; + songcount: number; + songs?: Song[]; + sortName: string; + type: ServerType; + updatedAt: string; +}; + +export type Song = { + album: Album; + artistName: string; + artists: RelatedArtist[]; + bitRate: number; + container: string; + createdAt: string; + deleted: boolean; + discNumber: number; + duration: number; + genres: RelatedGenre[]; + id: string; + imageUrl: string; + name: string; + releaseDate: string; + releaseYear: string; + remoteCreatedAt: string; + remoteId: string; + serverFolderId: string; + serverId: string; + streamUrl: string; + trackNumber: number; + updatedAt: string; +}; + +export type AlbumArtist = { biography: string | null; createdAt: string; - id: number; + id: string; name: string; remoteCreatedAt: string | null; remoteId: string; - serverFolderId: number; + serverFolderId: string; updatedAt: string; }; -export type ExternalResponse = { - createdAt: string; - id: number; +export type RelatedAlbumArtist = { + id: string; name: string; - updatedAt: string; - url: string; + remoteId: string; }; -export type ImageResponse = { +export type Artist = { + biography: string | null; createdAt: string; - id: number; + id: string; name: string; + remoteCreatedAt: string | null; + remoteId: string; + serverFolderId: string; updatedAt: string; - url: string; +}; + +export type RelatedArtist = { + id: string; + name: string; + remoteId: string; }; export type PingResponse = BaseResponse; export type LoginResponse = BaseResponse; -export type UserResponse = BaseResponse; +export type AlbumDetailResponse = BaseResponse; -export type AlbumResponse = BaseResponse; - -export type AlbumsResponse = BasePaginatedResponse; +export type AlbumListResponse = BasePaginatedResponse; export type Count = { artists?: number; diff --git a/src/renderer/api/users.api.ts b/src/renderer/api/users.api.ts new file mode 100644 index 000000000..c631d439d --- /dev/null +++ b/src/renderer/api/users.api.ts @@ -0,0 +1,20 @@ +import { BaseResponse, User } from '@/renderer/api/types'; +import { ax } from '@/renderer/lib/axios'; + +export type UserDetailResponse = BaseResponse; +export type UserListResponse = BaseResponse; + +const getUserDetail = async (query: { userId: string }) => { + const { data } = await ax.get(`/users/${query.userId}`); + return data; +}; + +const getUserList = async () => { + const { data } = await ax.get('/users'); + return data; +}; + +export const usersApi = { + getUserDetail, + getUserList, +}; diff --git a/src/renderer/api/usersApi.ts b/src/renderer/api/usersApi.ts deleted file mode 100644 index 03f9b9336..000000000 --- a/src/renderer/api/usersApi.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { api } from '../lib'; -import { UserResponse } from './types'; - -const getUsers = async () => { - const { data } = await api.get('/users'); - return data; -}; - -export const usersApi = { - getUsers, -};