From 19090a0ed86877c551cf32ca2661d8ae67e67761 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 29 Oct 2022 19:13:40 -0700 Subject: [PATCH] Update scanner (frontend) --- src/renderer/api/albums.api.ts | 10 +- src/renderer/api/auth.api.ts | 1 - src/renderer/api/index.ts | 4 + src/renderer/api/query-keys.ts | 10 +- src/renderer/api/servers.api.ts | 76 ++++++++++++++- src/renderer/api/shared.api.ts | 4 +- src/renderer/api/sockets.api.ts | 9 ++ src/renderer/api/tasks.api.ts | 35 +++++++ src/renderer/api/types.ts | 28 ++++++ src/renderer/features/tasks/index.ts | 3 + .../tasks/mutations/cancel-all-tasks.ts | 39 ++++++++ .../features/tasks/mutations/cancel-task.ts | 39 ++++++++ .../features/tasks/queries/task-list.ts | 22 +++++ .../features/titlebar/components/Titlebar.tsx | 13 +-- .../titlebar/components/activity-menu.tsx | 93 +++++++++++++++++++ .../features/titlebar/components/app-menu.tsx | 63 +++++++++---- 16 files changed, 411 insertions(+), 38 deletions(-) create mode 100644 src/renderer/api/sockets.api.ts create mode 100644 src/renderer/api/tasks.api.ts create mode 100644 src/renderer/features/tasks/index.ts create mode 100644 src/renderer/features/tasks/mutations/cancel-all-tasks.ts create mode 100644 src/renderer/features/tasks/mutations/cancel-task.ts create mode 100644 src/renderer/features/tasks/queries/task-list.ts create mode 100644 src/renderer/features/titlebar/components/activity-menu.tsx diff --git a/src/renderer/api/albums.api.ts b/src/renderer/api/albums.api.ts index 99c91bfe5..109e7c41d 100644 --- a/src/renderer/api/albums.api.ts +++ b/src/renderer/api/albums.api.ts @@ -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( `/servers/${query.serverId}/albums/${query.albumId}`, { signal } ); + + // const songs = data.data.songs?.map((s) => ({ + // ...s, + // streamUrl: + // })); + return data; }; diff --git a/src/renderer/api/auth.api.ts b/src/renderer/api/auth.api.ts index 63c54c754..7d04b52bd 100644 --- a/src/renderer/api/auth.api.ts +++ b/src/renderer/api/auth.api.ts @@ -1,4 +1,3 @@ -// import axios from 'axios'; import axios from 'axios'; import { LoginResponse, PingResponse } from './types'; diff --git a/src/renderer/api/index.ts b/src/renderer/api/index.ts index ec92fb921..d405f37f8 100644 --- a/src/renderer/api/index.ts +++ b/src/renderer/api/index.ts @@ -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, }; diff --git a/src/renderer/api/query-keys.ts b/src/renderer/api/query-keys.ts index 8fc3ab8cb..25f9d27ea 100644 --- a/src/renderer/api/query-keys.ts +++ b/src/renderer/api/query-keys.ts @@ -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, diff --git a/src/renderer/api/servers.api.ts b/src/renderer/api/servers.api.ts index 74104aec0..87972a084 100644 --- a/src/renderer/api/servers.api.ts +++ b/src/renderer/api/servers.api.ts @@ -9,14 +9,21 @@ import { ax } from '@/renderer/lib/axios'; export type ServerListResponse = BaseResponse; -const getServerList = async (signal?: AbortSignal) => { - const { data } = await ax.get('/servers', { signal }); +const getServerList = async ( + params?: { enabled?: boolean }, + signal?: AbortSignal +) => { + const { data } = await ax.get('/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( + `/servers/${query.serverId}`, + {} + ); + return data; +}; + const updateServer = async ( query: { serverId: string }, body: Partial @@ -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( +// `/servers/${query.serverId}/folder/${query.folderId}` +// ); +// return data; +// }; + +const enableFolder = async (query: { folderId: string; serverId: string }) => { + const { data } = await ax.post( + `/servers/${query.serverId}/folder/${query.folderId}/enable`, + {} + ); + return data; +}; + +const disableFolder = async (query: { folderId: string; serverId: string }) => { + const { data } = await ax.post( + `/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( + `/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( + `/servers/${query.serverId}/full-scan`, + body + ); + return data; +}; + export const serversApi = { createServer, createUrl, + deleteServer, deleteUrl, + disableFolder, disableUrl, + enableFolder, enableUrl, + fullScan, getServerList, + quickScan, updateServer, }; diff --git a/src/renderer/api/shared.api.ts b/src/renderer/api/shared.api.ts index 567bd208f..6ceeeeb8e 100644 --- a/src/renderer/api/shared.api.ts +++ b/src/renderer/api/shared.api.ts @@ -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 }; diff --git a/src/renderer/api/sockets.api.ts b/src/renderer/api/sockets.api.ts new file mode 100644 index 000000000..a1fec1d58 --- /dev/null +++ b/src/renderer/api/sockets.api.ts @@ -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 }, +}); diff --git a/src/renderer/api/tasks.api.ts b/src/renderer/api/tasks.api.ts new file mode 100644 index 000000000..0fc8660b6 --- /dev/null +++ b/src/renderer/api/tasks.api.ts @@ -0,0 +1,35 @@ +import { BaseResponse, NullResponse, Task } from '@/renderer/api/types'; +import { ax } from '@/renderer/lib/axios'; + +export type TaskListResponse = BaseResponse; + +const getActiveTasks = async (signal?: AbortSignal) => { + const { data } = await ax.get('/tasks', { + signal, + }); + + return data; +}; + +const cancelAllTasks = async () => { + const { data } = await ax.post('/tasks/cancel', {}); + + return data; +}; + +export type TaskResponse = BaseResponse; + +const cancelTask = async (query: { taskId: string }) => { + const { data } = await ax.post( + `/tasks/${query.taskId}/cancel`, + {} + ); + + return data; +}; + +export const tasksApi = { + cancelAllTasks, + cancelTask, + getActiveTasks, +}; diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index 38352246c..70c9aea49 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -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; export type LoginResponse = BaseResponse; diff --git a/src/renderer/features/tasks/index.ts b/src/renderer/features/tasks/index.ts new file mode 100644 index 000000000..a0363afc2 --- /dev/null +++ b/src/renderer/features/tasks/index.ts @@ -0,0 +1,3 @@ +export * from './queries/task-list'; +export * from './mutations/cancel-all-tasks'; +export * from './mutations/cancel-task'; diff --git a/src/renderer/features/tasks/mutations/cancel-all-tasks.ts b/src/renderer/features/tasks/mutations/cancel-all-tasks.ts new file mode 100644 index 000000000..dd14bf496 --- /dev/null +++ b/src/renderer/features/tasks/mutations/cancel-all-tasks.ts @@ -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, + 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(queryKey); + + if (!previous) return undefined; + + queryClient.setQueryData(queryKey, { ...previous, data: [] }); + + return { previous }; + }, + onSuccess: () => { + queryClient.invalidateQueries(queryKeys.tasks.list()); + }, + }); + + return mutation; +}; diff --git a/src/renderer/features/tasks/mutations/cancel-task.ts b/src/renderer/features/tasks/mutations/cancel-task.ts new file mode 100644 index 000000000..cbc0cad0c --- /dev/null +++ b/src/renderer/features/tasks/mutations/cancel-task.ts @@ -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, + { 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(queryKey); + + if (!previous) return undefined; + + queryClient.setQueryData(queryKey, { ...previous, data: [] }); + + return { previous }; + }, + onSuccess: () => { + queryClient.invalidateQueries(queryKeys.tasks.list()); + }, + }); + + return mutation; +}; diff --git a/src/renderer/features/tasks/queries/task-list.ts b/src/renderer/features/tasks/queries/task-list.ts new file mode 100644 index 000000000..91c7a6637 --- /dev/null +++ b/src/renderer/features/tasks/queries/task-list.ts @@ -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) => { + 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, + }); +}; diff --git a/src/renderer/features/titlebar/components/Titlebar.tsx b/src/renderer/features/titlebar/components/Titlebar.tsx index 48933e52f..4e9e70677 100644 --- a/src/renderer/features/titlebar/components/Titlebar.tsx +++ b/src/renderer/features/titlebar/components/Titlebar.tsx @@ -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) => { {isAuthenticated && ( <> - + )} diff --git a/src/renderer/features/titlebar/components/activity-menu.tsx b/src/renderer/features/titlebar/components/activity-menu.tsx new file mode 100644 index 000000000..aa8eacab1 --- /dev/null +++ b/src/renderer/features/titlebar/components/activity-menu.tsx @@ -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 ( + <> + + + + + + {isTaskRunning ? ( + tasks?.data?.map((task) => ( + + {task.note} + + )) + ) : ( + No tasks running + )} + + + + ); +}; diff --git a/src/renderer/features/titlebar/components/app-menu.tsx b/src/renderer/features/titlebar/components/app-menu.tsx index 7d4c2c966..1c425a42c 100644 --- a/src/renderer/features/titlebar/components/app-menu.tsx +++ b/src/renderer/features/titlebar/components/app-menu.tsx @@ -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 ( - +