mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Update scanner (frontend)
This commit is contained in:
@@ -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,4 +1,3 @@
|
||||
// import axios from 'axios';
|
||||
import axios from 'axios';
|
||||
import { LoginResponse, PingResponse } from './types';
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user