Update frontend API structure

This commit is contained in:
jeffvli
2022-10-24 21:47:03 -07:00
parent 76b6eed4bb
commit d5bbff5eb6
14 changed files with 561 additions and 111 deletions
+56
View File
@@ -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<AlbumDetailResponse>(
`/servers/${query.serverId}/albums/${query.albumId}`,
{ signal }
);
return data;
};
const getAlbumList = async (
query: { serverId: string },
params: AlbumListParams,
signal?: AbortSignal
) => {
const { data } = await ax.get<AlbumListResponse>(
`/servers/${query.serverId}/albums`,
{
params,
signal,
}
);
return data;
};
export const albumsApi = {
getAlbumDetail,
getAlbumList,
};
-28
View File
@@ -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<AlbumResponse>(`/albums/${params.id}`, {
signal,
});
return data;
};
const getAlbums = async (params: AlbumsRequest, signal?: AbortSignal) => {
const { data } = await api.get<AlbumsResponse>(`/albums`, {
params,
signal,
});
return data;
};
export const albumsApi = {
getAlbum,
getAlbums,
};
+11
View File
@@ -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,
};
-10
View File
@@ -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),
});
};
+19
View File
@@ -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'],
},
};
-8
View File
@@ -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,
};
+89
View File
@@ -0,0 +1,89 @@
import {
BaseResponse,
NullResponse,
Server,
ServerType,
ServerUrl,
} from '@/renderer/api/types';
import { ax } from '@/renderer/lib/axios';
export type ServerListResponse = BaseResponse<Server[]>;
const getServerList = async (signal?: AbortSignal) => {
const { data } = await ax.get<ServerListResponse>('/servers', { signal });
return data;
};
export type CreateServerBody = {
legacy?: boolean;
name: string;
password: string;
type: ServerType;
url: string;
username: string;
};
export type ServerResponse = BaseResponse<Server>;
const createServer = async (body: CreateServerBody) => {
const { data } = await ax.post<ServerResponse>('/servers', body);
return data;
};
const updateServer = async (
query: { serverId: string },
body: Partial<CreateServerBody>
) => {
const { data } = await ax.patch<ServerResponse>(
`/servers/${query.serverId}`,
body
);
return data;
};
export type CreateUrlBody = {
url: string;
};
export type UrlResponse = BaseResponse<ServerUrl>;
const createUrl = async (query: { serverId: string }, body: CreateUrlBody) => {
const { data } = await ax.post<UrlResponse>(
`/servers/${query.serverId}/url`,
body
);
return data;
};
const deleteUrl = async (query: { serverId: string; urlId: string }) => {
const { data } = await ax.delete<NullResponse>(
`/servers/${query.serverId}/url/${query.urlId}`
);
return data;
};
const enableUrl = async (query: { serverId: string; urlId: string }) => {
const { data } = await ax.post<NullResponse>(
`/servers/${query.serverId}/url/${query.urlId}/enable`,
{}
);
return data;
};
const disableUrl = async (query: { serverId: string; urlId: string }) => {
const { data } = await ax.post<NullResponse>(
`/servers/${query.serverId}/url/${query.urlId}/disable`,
{}
);
return data;
};
export const serversApi = {
createServer,
createUrl,
deleteUrl,
disableUrl,
enableUrl,
getServerList,
updateServer,
};
-22
View File
@@ -1,22 +0,0 @@
import { api } from '../lib';
const getServers = async () => {
const { data } = await api.get<any[]>('/servers');
return data;
};
const createServer = async (body: {
name: string;
remoteUserId: string;
token: string;
url: string;
username: string;
}) => {
const { data } = await api.post<any>('/servers', body);
return data;
};
export const serversApi = {
createServer,
getServers,
};
+158
View File
@@ -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<JFAuthenticate>(
`${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<NDAuthenticate>(
`${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' };
};
View File
+208 -32
View File
@@ -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<T> {
data: T;
@@ -20,45 +58,108 @@ export interface BasePaginatedResponse<T> {
statusCode: number;
}
export interface BasePaginationRequest {
export type ApiError = {
error: {
message: string;
path: string;
trace: string[];
};
response: string;
statusCode: number;
};
export type NullResponse = BaseResponse<null>;
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<Ping>;
export type LoginResponse = BaseResponse<Login>;
export type UserResponse = BaseResponse<User>;
export type AlbumDetailResponse = BaseResponse<Album>;
export type AlbumResponse = BaseResponse<Album>;
export type AlbumsResponse = BasePaginatedResponse<Album[]>;
export type AlbumListResponse = BasePaginatedResponse<Album[]>;
export type Count = {
artists?: number;
+20
View File
@@ -0,0 +1,20 @@
import { BaseResponse, User } from '@/renderer/api/types';
import { ax } from '@/renderer/lib/axios';
export type UserDetailResponse = BaseResponse<User>;
export type UserListResponse = BaseResponse<User[]>;
const getUserDetail = async (query: { userId: string }) => {
const { data } = await ax.get<UserDetailResponse>(`/users/${query.userId}`);
return data;
};
const getUserList = async () => {
const { data } = await ax.get<UserListResponse>('/users');
return data;
};
export const usersApi = {
getUserDetail,
getUserList,
};
-11
View File
@@ -1,11 +0,0 @@
import { api } from '../lib';
import { UserResponse } from './types';
const getUsers = async () => {
const { data } = await api.get<UserResponse>('/users');
return data;
};
export const usersApi = {
getUsers,
};