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 { SortOrder } from '@/types';
import { SortOrder } from '@/renderer/types';
import {
AlbumDetailResponse,
AlbumListResponse,
@@ -25,13 +25,19 @@ export type AlbumListParams = PaginationParams & {
};
const getAlbumDetail = async (
query: { albumId: number; serverId: string },
query: { albumId: string; serverId: string },
signal?: AbortSignal
) => {
const { data } = await ax.get<AlbumDetailResponse>(
`/servers/${query.serverId}/albums/${query.albumId}`,
{ signal }
);
// const songs = data.data.songs?.map((s) => ({
// ...s,
// streamUrl:
// }));
return data;
};
-1
View File
@@ -1,4 +1,3 @@
// import axios from 'axios';
import axios from 'axios';
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 { authApi } from './auth.api';
import { serversApi } from './servers.api';
import { usersApi } from './users.api';
export * from './sockets.api';
export const api = {
albums: albumsApi,
auth: authApi,
servers: serversApi,
tasks: tasksApi,
users: usersApi,
};
+8 -2
View File
@@ -3,13 +3,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,
list: (serverId: string, params: AlbumListParams) =>
['albums', 'list', serverId, 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,
list: (params?: any) => ['servers', 'list', params] as const,
root: ['servers'],
},
tasks: {
list: () => ['tasks', 'list'] as const,
root: ['tasks'],
},
users: {
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[]>;
const getServerList = async (signal?: AbortSignal) => {
const { data } = await ax.get<ServerListResponse>('/servers', { signal });
const getServerList = async (
params?: { enabled?: boolean },
signal?: AbortSignal
) => {
const { data } = await ax.get<ServerListResponse>('/servers', {
params,
signal,
});
return data;
};
export type CreateServerBody = {
legacy?: boolean;
name: string;
noCredential?: boolean;
password: string;
type: ServerType;
url: string;
@@ -30,6 +37,15 @@ const createServer = async (body: CreateServerBody) => {
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 (
query: { serverId: string },
body: Partial<CreateServerBody>
@@ -78,12 +94,68 @@ const disableUrl = async (query: { serverId: string; urlId: string }) => {
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 = {
createServer,
createUrl,
deleteServer,
deleteUrl,
disableFolder,
disableUrl,
enableFolder,
enableUrl,
fullScan,
getServerList,
quickScan,
updateServer,
};
+2 -2
View File
@@ -23,7 +23,7 @@ export const jfAuthenticate = async (options: {
{ pw: password, username },
{
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(
`${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 };
+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;
id: string;
name: string;
noCredential: boolean;
remoteUserId: string;
serverFolders?: RelatedServerFolder[];
serverPermissions?: RelatedServerPermission[];
@@ -96,6 +97,7 @@ export type Server = {
};
export type RelatedServerFolder = {
enabled: boolean;
id: string;
lastScannedAt: string | null;
name: string;
@@ -282,6 +284,32 @@ export type RelatedArtist = {
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 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 styled from '@emotion/styled';
import { Group } from '@mantine/core';
import { FiActivity } from 'react-icons/fi';
import { Button, Text } from '@/renderer/components';
import { Text } from '@/renderer/components';
import { ActivityMenu } from '@/renderer/features/titlebar/components/activity-menu';
import { AppMenu } from '@/renderer/features/titlebar/components/app-menu';
import { useAuthStore } from '@/renderer/store';
import { Font } from '@/renderer/styles';
@@ -69,14 +69,7 @@ export const Titlebar = ({ children }: TitlebarProps) => {
<Group spacing="xs">
{isAuthenticated && (
<>
<Button
px={5}
size="xs"
sx={{ color: 'var(--titlebar-fg)' }}
variant="subtle"
>
<FiActivity size={15} />
</Button>
<ActivityMenu />
<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 { RiArrowLeftLine, RiLogoutBoxLine, RiMenu3Fill } from 'react-icons/ri';
import {
RiArrowLeftLine,
RiLock2Line,
RiLogoutBoxLine,
RiMenu3Fill,
} from 'react-icons/ri';
import { useNavigate } from 'react-router';
import { Button, DropdownMenu } from '@/renderer/components';
import {
@@ -15,11 +21,16 @@ export const AppMenu = () => {
const logout = useAuthStore((state) => state.logout);
const currentServer = useAuthStore((state) => state.currentServer);
const setCurrentServer = useAuthStore((state) => state.setCurrentServer);
const serverCredentials = useAuthStore((state) => state.serverCredentials);
const permissions = usePermissions();
const { data: servers } = useServerList();
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 = () => {
logout();
@@ -49,7 +60,7 @@ export const AppMenu = () => {
};
return (
<DropdownMenu withinPortal position="bottom" width={200}>
<DropdownMenu withArrow withinPortal position="bottom" width={200}>
<DropdownMenu.Target>
<Button
px={5}
@@ -62,24 +73,38 @@ export const AppMenu = () => {
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Label>Server switcher</DropdownMenu.Label>
{serverList.map((s) => (
<DropdownMenu.Item
key={`server-${s.id}`}
rightSection={
s.id === currentServer?.id ? <RiArrowLeftLine /> : undefined
}
sx={{
color:
s.id === currentServer?.id ? 'var(--primary-color)' : undefined,
}}
onClick={() => handleSetCurrentServer(s.id)}
>
{s.label}
</DropdownMenu.Item>
))}
{serverList.map((s) => {
const requiresCredential = !serverCredentials.some(
(c) => c.serverId === s.id && c.enabled
);
return (
<DropdownMenu.Item
key={`server-${s.id}`}
disabled={requiresCredential}
rightSection={
s.id === currentServer?.id ? <RiArrowLeftLine /> : undefined
}
sx={{
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.Item disabled>Search</DropdownMenu.Item>
<DropdownMenu.Item>Configure</DropdownMenu.Item>
<DropdownMenu.Item>Settings</DropdownMenu.Item>
<DropdownMenu.Divider />
{permissions.createServer && (
<DropdownMenu.Item onClick={handleAddServerModal}>