Update scanner (frontend)

This commit is contained in:
jeffvli
2022-10-29 19:13:40 -07:00
parent 0200b92860
commit 19090a0ed8
16 changed files with 411 additions and 38 deletions
+8 -2
View File
@@ -1,5 +1,5 @@
import { ax } from '@/renderer/lib/axios'; import { ax } from '@/renderer/lib/axios';
import { SortOrder } from '@/types'; import { SortOrder } from '@/renderer/types';
import { import {
AlbumDetailResponse, AlbumDetailResponse,
AlbumListResponse, AlbumListResponse,
@@ -25,13 +25,19 @@ export type AlbumListParams = PaginationParams & {
}; };
const getAlbumDetail = async ( const getAlbumDetail = async (
query: { albumId: number; serverId: string }, query: { albumId: string; serverId: string },
signal?: AbortSignal signal?: AbortSignal
) => { ) => {
const { data } = await ax.get<AlbumDetailResponse>( const { data } = await ax.get<AlbumDetailResponse>(
`/servers/${query.serverId}/albums/${query.albumId}`, `/servers/${query.serverId}/albums/${query.albumId}`,
{ signal } { signal }
); );
// const songs = data.data.songs?.map((s) => ({
// ...s,
// streamUrl:
// }));
return data; return data;
}; };
-1
View File
@@ -1,4 +1,3 @@
// import axios from 'axios';
import axios from 'axios'; import axios from 'axios';
import { LoginResponse, PingResponse } from './types'; import { LoginResponse, PingResponse } from './types';
+4
View File
@@ -1,11 +1,15 @@
import { tasksApi } from '@/renderer/api/tasks.api';
import { albumsApi } from './albums.api'; import { albumsApi } from './albums.api';
import { authApi } from './auth.api'; import { authApi } from './auth.api';
import { serversApi } from './servers.api'; import { serversApi } from './servers.api';
import { usersApi } from './users.api'; import { usersApi } from './users.api';
export * from './sockets.api';
export const api = { export const api = {
albums: albumsApi, albums: albumsApi,
auth: authApi, auth: authApi,
servers: serversApi, servers: serversApi,
tasks: tasksApi,
users: usersApi, users: usersApi,
}; };
+8 -2
View File
@@ -3,13 +3,19 @@ import { AlbumListParams } from './albums.api';
export const queryKeys = { export const queryKeys = {
albums: { albums: {
detail: (albumId: string) => ['albums', albumId] as const, detail: (albumId: string) => ['albums', albumId] as const,
list: (params: AlbumListParams) => ['albums', 'list', params] as const, list: (serverId: string, params: AlbumListParams) =>
['albums', 'list', serverId, params] as const,
root: ['albums'], root: ['albums'],
songList: (albumId: string) => ['albums', albumId, 'songs'] as const, songList: (albumId: string) => ['albums', albumId, 'songs'] as const,
}, },
ping: (url: string) => ['ping', url] as const, ping: (url: string) => ['ping', url] as const,
servers: { servers: {
list: () => ['servers', 'list'] as const, list: (params?: any) => ['servers', 'list', params] as const,
root: ['servers'],
},
tasks: {
list: () => ['tasks', 'list'] as const,
root: ['tasks'],
}, },
users: { users: {
detail: (userId: string) => ['users', userId] as const, detail: (userId: string) => ['users', userId] as const,
+74 -2
View File
@@ -9,14 +9,21 @@ import { ax } from '@/renderer/lib/axios';
export type ServerListResponse = BaseResponse<Server[]>; export type ServerListResponse = BaseResponse<Server[]>;
const getServerList = async (signal?: AbortSignal) => { const getServerList = async (
const { data } = await ax.get<ServerListResponse>('/servers', { signal }); params?: { enabled?: boolean },
signal?: AbortSignal
) => {
const { data } = await ax.get<ServerListResponse>('/servers', {
params,
signal,
});
return data; return data;
}; };
export type CreateServerBody = { export type CreateServerBody = {
legacy?: boolean; legacy?: boolean;
name: string; name: string;
noCredential?: boolean;
password: string; password: string;
type: ServerType; type: ServerType;
url: string; url: string;
@@ -30,6 +37,15 @@ const createServer = async (body: CreateServerBody) => {
return data; return data;
}; };
const deleteServer = async (options: { query: { serverId: string } }) => {
const { query } = options;
const { data } = await ax.post<NullResponse>(
`/servers/${query.serverId}`,
{}
);
return data;
};
const updateServer = async ( const updateServer = async (
query: { serverId: string }, query: { serverId: string },
body: Partial<CreateServerBody> body: Partial<CreateServerBody>
@@ -78,12 +94,68 @@ const disableUrl = async (query: { serverId: string; urlId: string }) => {
return data; return data;
}; };
// const deleteFolder = async (query: { serverId: string; folderId: string }) => {
// const { data } = await ax.delete<NullResponse>(
// `/servers/${query.serverId}/folder/${query.folderId}`
// );
// return data;
// };
const enableFolder = async (query: { folderId: string; serverId: string }) => {
const { data } = await ax.post<NullResponse>(
`/servers/${query.serverId}/folder/${query.folderId}/enable`,
{}
);
return data;
};
const disableFolder = async (query: { folderId: string; serverId: string }) => {
const { data } = await ax.post<NullResponse>(
`/servers/${query.serverId}/folder/${query.folderId}/disable`,
{}
);
return data;
};
export type ScanServerBody = {
serverFolderId?: string[];
};
const quickScan = async (options: {
body: ScanServerBody;
query: { serverId: string };
}) => {
const { body, query } = options;
const { data } = await ax.post<NullResponse>(
`/servers/${query.serverId}/scan`,
body
);
return data;
};
const fullScan = async (options: {
body: ScanServerBody;
query: { serverId: string };
}) => {
const { body, query } = options;
const { data } = await ax.post<NullResponse>(
`/servers/${query.serverId}/full-scan`,
body
);
return data;
};
export const serversApi = { export const serversApi = {
createServer, createServer,
createUrl, createUrl,
deleteServer,
deleteUrl, deleteUrl,
disableFolder,
disableUrl, disableUrl,
enableFolder,
enableUrl, enableUrl,
fullScan,
getServerList, getServerList,
quickScan,
updateServer, updateServer,
}; };
+2 -2
View File
@@ -23,7 +23,7 @@ export const jfAuthenticate = async (options: {
{ pw: password, username }, { pw: password, username },
{ {
headers: { headers: {
'X-Emby-Authorization': `MediaBrowser Client="Sonixd", Device="PC", DeviceId="Sonixd", Version="1.0.0-alpha1"`, 'X-Emby-Authorization': `MediaBrowser Client="Feishin", Device="PC", DeviceId="Feishin", Version="1.0.0-alpha1"`,
}, },
} }
); );
@@ -76,7 +76,7 @@ const ssAuthenticate = async (options: {
} }
const { data } = await axios.get( const { data } = await axios.get(
`${cleanServerUrl}/rest/ping.view?v=1.13.0&c=sonixd&f=json&${token}` `${cleanServerUrl}/rest/ping.view?v=1.13.0&c=Feishin&f=json&${token}`
); );
return { token, ...data }; return { token, ...data };
+9
View File
@@ -0,0 +1,9 @@
import { io } from 'socket.io-client';
const { username } = JSON.parse(
localStorage.getItem('store_authentication') || '{}'
).state.permissions;
export const socket = io('http://localhost:8843', {
query: { username },
});
+35
View File
@@ -0,0 +1,35 @@
import { BaseResponse, NullResponse, Task } from '@/renderer/api/types';
import { ax } from '@/renderer/lib/axios';
export type TaskListResponse = BaseResponse<Task[]>;
const getActiveTasks = async (signal?: AbortSignal) => {
const { data } = await ax.get<TaskListResponse>('/tasks', {
signal,
});
return data;
};
const cancelAllTasks = async () => {
const { data } = await ax.post<NullResponse>('/tasks/cancel', {});
return data;
};
export type TaskResponse = BaseResponse<Task>;
const cancelTask = async (query: { taskId: string }) => {
const { data } = await ax.post<TaskResponse>(
`/tasks/${query.taskId}/cancel`,
{}
);
return data;
};
export const tasksApi = {
cancelAllTasks,
cancelTask,
getActiveTasks,
};
+28
View File
@@ -84,6 +84,7 @@ export type Server = {
createdAt: string; createdAt: string;
id: string; id: string;
name: string; name: string;
noCredential: boolean;
remoteUserId: string; remoteUserId: string;
serverFolders?: RelatedServerFolder[]; serverFolders?: RelatedServerFolder[];
serverPermissions?: RelatedServerPermission[]; serverPermissions?: RelatedServerPermission[];
@@ -96,6 +97,7 @@ export type Server = {
}; };
export type RelatedServerFolder = { export type RelatedServerFolder = {
enabled: boolean;
id: string; id: string;
lastScannedAt: string | null; lastScannedAt: string | null;
name: string; name: string;
@@ -282,6 +284,32 @@ export type RelatedArtist = {
remoteId: string; remoteId: string;
}; };
export type RelatedServer = {
id: string;
name: string;
type: ServerType;
url: string;
};
export type RelatedUser = {
enabled: boolean;
id: string;
isAdmin: boolean;
username: string;
};
export type Task = {
createdAt: string;
id: string;
isCompleted: boolean;
isError: boolean;
message: string;
server: RelatedServer | null;
type: TaskType;
updatedAt: string;
user: RelatedUser | null;
};
export type PingResponse = BaseResponse<Ping>; export type PingResponse = BaseResponse<Ping>;
export type LoginResponse = BaseResponse<Login>; export type LoginResponse = BaseResponse<Login>;
+3
View File
@@ -0,0 +1,3 @@
export * from './queries/task-list';
export * from './mutations/cancel-all-tasks';
export * from './mutations/cancel-task';
@@ -0,0 +1,39 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys';
import { TaskListResponse } from '@/renderer/api/tasks.api';
import { ApiError, NullResponse } from '@/renderer/api/types';
export const useCancelAllTasks = () => {
const queryClient = useQueryClient();
const mutation = useMutation<
NullResponse,
AxiosError<ApiError>,
null,
{ previous: TaskListResponse | undefined }
>({
mutationFn: () => api.tasks.cancelAllTasks(),
onError: (_err, _variables, context) => {
if (!context?.previous) return;
queryClient.setQueryData(queryKeys.servers.list(), context.previous);
},
onMutate: () => {
const queryKey = queryKeys.tasks.list();
queryClient.cancelQueries(queryKey);
const previous = queryClient.getQueryData<TaskListResponse>(queryKey);
if (!previous) return undefined;
queryClient.setQueryData(queryKey, { ...previous, data: [] });
return { previous };
},
onSuccess: () => {
queryClient.invalidateQueries(queryKeys.tasks.list());
},
});
return mutation;
};
@@ -0,0 +1,39 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys';
import { TaskListResponse, TaskResponse } from '@/renderer/api/tasks.api';
import { ApiError } from '@/renderer/api/types';
export const useCancelTask = () => {
const queryClient = useQueryClient();
const mutation = useMutation<
TaskResponse,
AxiosError<ApiError>,
{ query: { taskId: string } },
{ previous: TaskListResponse | undefined }
>({
mutationFn: ({ query }) => api.tasks.cancelTask(query),
onError: (_err, _variables, context) => {
if (!context?.previous) return;
queryClient.setQueryData(queryKeys.servers.list(), context.previous);
},
onMutate: () => {
const queryKey = queryKeys.tasks.list();
queryClient.cancelQueries(queryKey);
const previous = queryClient.getQueryData<TaskListResponse>(queryKey);
if (!previous) return undefined;
queryClient.setQueryData(queryKey, { ...previous, data: [] });
return { previous };
},
onSuccess: () => {
queryClient.invalidateQueries(queryKeys.tasks.list());
},
});
return mutation;
};
@@ -0,0 +1,22 @@
import { useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys';
import { TaskListResponse } from '@/renderer/api/tasks.api';
import { QueryOptions } from '@/renderer/lib/react-query';
export const useTaskList = (options?: QueryOptions<TaskListResponse>) => {
return useQuery({
queryFn: ({ signal }) => api.tasks.getActiveTasks(signal),
queryKey: queryKeys.tasks.list(),
select: useCallback((data: TaskListResponse) => {
return {
...data,
data: data.data.map((task) => {
return { ...task, note: `${task.server?.name} - ${task.message}` };
}),
};
}, []),
...options,
});
};
@@ -1,8 +1,8 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Group } from '@mantine/core'; import { Group } from '@mantine/core';
import { FiActivity } from 'react-icons/fi'; import { Text } from '@/renderer/components';
import { Button, Text } from '@/renderer/components'; import { ActivityMenu } from '@/renderer/features/titlebar/components/activity-menu';
import { AppMenu } from '@/renderer/features/titlebar/components/app-menu'; import { AppMenu } from '@/renderer/features/titlebar/components/app-menu';
import { useAuthStore } from '@/renderer/store'; import { useAuthStore } from '@/renderer/store';
import { Font } from '@/renderer/styles'; import { Font } from '@/renderer/styles';
@@ -69,14 +69,7 @@ export const Titlebar = ({ children }: TitlebarProps) => {
<Group spacing="xs"> <Group spacing="xs">
{isAuthenticated && ( {isAuthenticated && (
<> <>
<Button <ActivityMenu />
px={5}
size="xs"
sx={{ color: 'var(--titlebar-fg)' }}
variant="subtle"
>
<FiActivity size={15} />
</Button>
<AppMenu /> <AppMenu />
</> </>
)} )}
@@ -0,0 +1,93 @@
import { useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { Group } from '@mantine/core';
import { FiActivity } from 'react-icons/fi';
import { RiRefreshLine } from 'react-icons/ri';
import { socket } from '@/renderer/api';
import { Button, Popover, Text } from '@/renderer/components';
import { useTaskList } from '@/renderer/features/tasks';
import { rotating } from '@/renderer/styles';
const StyledActivitySvg = styled(RiRefreshLine)`
${rotating}
animation: rotating 1s linear infinite;
`;
export const ActivityMenu = () => {
const [isTaskRunning, setIsTaskRunning] = useState(false);
const { data: tasks, refetch } = useTaskList({
onSuccess: (data) => {
if (data.data.length === 0) {
return setIsTaskRunning(false);
}
return setIsTaskRunning(true);
},
refetchInterval: isTaskRunning ? 5000 : undefined,
});
// const cancelTask = useCancelTask();
// const cancelAllTasks = useCancelAllTasks();
// const handleCancelTask = (taskId: string) => {
// cancelTask.mutate(
// { query: { taskId } },
// {
// onSuccess: () => {
// toast.info({ message: 'Task cancelled' });
// },
// }
// );
// };
// const handleCancelAllTasks = (taskId: string) => {
// cancelAllTasks.mutate(null, {
// onSuccess: () => {
// toast.info({ message: 'All tasks cancelled' });
// },
// });
// };
useEffect(() => {
socket.on('task:started', () => {
setTimeout(() => refetch(), 1000);
setIsTaskRunning(true);
});
return () => {
socket.off('task:started');
};
}, [refetch]);
return (
<>
<Popover withArrow withinPortal>
<Popover.Target>
<Button
px={5}
size="xs"
sx={{ color: 'var(--titlebar-fg)' }}
variant="subtle"
>
{isTaskRunning ? (
<StyledActivitySvg size={15} />
) : (
<FiActivity size={15} />
)}
</Button>
</Popover.Target>
<Popover.Dropdown>
{isTaskRunning ? (
tasks?.data?.map((task) => (
<Group key={task.id} position="apart">
<Text>{task.note}</Text>
</Group>
))
) : (
<Text>No tasks running</Text>
)}
</Popover.Dropdown>
</Popover>
</>
);
};
@@ -1,5 +1,11 @@
import { Group } from '@mantine/core';
import { openModal, closeAllModals } from '@mantine/modals'; import { openModal, closeAllModals } from '@mantine/modals';
import { RiArrowLeftLine, RiLogoutBoxLine, RiMenu3Fill } from 'react-icons/ri'; import {
RiArrowLeftLine,
RiLock2Line,
RiLogoutBoxLine,
RiMenu3Fill,
} from 'react-icons/ri';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { Button, DropdownMenu } from '@/renderer/components'; import { Button, DropdownMenu } from '@/renderer/components';
import { import {
@@ -15,11 +21,16 @@ export const AppMenu = () => {
const logout = useAuthStore((state) => state.logout); const logout = useAuthStore((state) => state.logout);
const currentServer = useAuthStore((state) => state.currentServer); const currentServer = useAuthStore((state) => state.currentServer);
const setCurrentServer = useAuthStore((state) => state.setCurrentServer); const setCurrentServer = useAuthStore((state) => state.setCurrentServer);
const serverCredentials = useAuthStore((state) => state.serverCredentials);
const permissions = usePermissions(); const permissions = usePermissions();
const { data: servers } = useServerList(); const { data: servers } = useServerList();
const serverList = const serverList =
servers?.data?.map((s) => ({ id: s.id, label: `${s.name}` })) ?? []; servers?.data?.map((s) => ({
id: s.id,
label: `${s.name}`,
noCredential: s.noCredential,
})) ?? [];
const handleLogout = () => { const handleLogout = () => {
logout(); logout();
@@ -49,7 +60,7 @@ export const AppMenu = () => {
}; };
return ( return (
<DropdownMenu withinPortal position="bottom" width={200}> <DropdownMenu withArrow withinPortal position="bottom" width={200}>
<DropdownMenu.Target> <DropdownMenu.Target>
<Button <Button
px={5} px={5}
@@ -62,24 +73,38 @@ export const AppMenu = () => {
</DropdownMenu.Target> </DropdownMenu.Target>
<DropdownMenu.Dropdown> <DropdownMenu.Dropdown>
<DropdownMenu.Label>Server switcher</DropdownMenu.Label> <DropdownMenu.Label>Server switcher</DropdownMenu.Label>
{serverList.map((s) => ( {serverList.map((s) => {
<DropdownMenu.Item const requiresCredential = !serverCredentials.some(
key={`server-${s.id}`} (c) => c.serverId === s.id && c.enabled
rightSection={ );
s.id === currentServer?.id ? <RiArrowLeftLine /> : undefined
} return (
sx={{ <DropdownMenu.Item
color: key={`server-${s.id}`}
s.id === currentServer?.id ? 'var(--primary-color)' : undefined, disabled={requiresCredential}
}} rightSection={
onClick={() => handleSetCurrentServer(s.id)} s.id === currentServer?.id ? <RiArrowLeftLine /> : undefined
> }
{s.label} sx={{
</DropdownMenu.Item> color:
))} s.id === currentServer?.id
? 'var(--primary-color)'
: undefined,
}}
onClick={() => handleSetCurrentServer(s.id)}
>
<Group>
{requiresCredential && (
<RiLock2Line color="var(--danger-color)" />
)}
{s.label}
</Group>
</DropdownMenu.Item>
);
})}
<DropdownMenu.Divider /> <DropdownMenu.Divider />
<DropdownMenu.Item disabled>Search</DropdownMenu.Item> <DropdownMenu.Item disabled>Search</DropdownMenu.Item>
<DropdownMenu.Item>Configure</DropdownMenu.Item> <DropdownMenu.Item>Settings</DropdownMenu.Item>
<DropdownMenu.Divider /> <DropdownMenu.Divider />
{permissions.createServer && ( {permissions.createServer && (
<DropdownMenu.Item onClick={handleAddServerModal}> <DropdownMenu.Item onClick={handleAddServerModal}>