Queue on server

This commit is contained in:
AnidlSupport 2023-02-28 23:13:00 +01:00
parent 50d48ca7cd
commit 87e6990c7b
16 changed files with 178 additions and 127 deletions

View file

@ -10,14 +10,19 @@ export interface MessageHandler {
availableDubCodes: () => Promise<string[]>,
availableSubCodes: () => Promise<string[]>,
handleDefault: (name: string) => Promise<any>,
resolveItems: (data: ResolveItemsData) => Promise<ResponseBase<QueueItem[]>>,
resolveItems: (data: ResolveItemsData) => Promise<boolean>,
listEpisodes: (id: string) => Promise<EpisodeListResponse>,
downloadItem: (data) => void,
downloadItem: (data: QueueItem) => void,
isDownloading: () => Promise<boolean>,
writeToClipboard: (text: string) => void,
openFolder: (path: FolderTypes) => void,
openFile: (data: [FolderTypes, string]) => void,
openURL: (data: string) => void;
getQueue: () => Promise<QueueItem[]>,
removeFromQueue: (index: number) => void,
clearQueue: () => void,
setDownloadQueue: (data: boolean) => void,
getDownloadQueue: () => Promise<boolean>
}
export type FolderTypes = 'content' | 'config';
@ -25,7 +30,6 @@ export type FolderTypes = 'content' | 'config';
export type QueueItem = {
title: string,
episode: string,
ids: string[],
fileName: string,
dlsubs: string[],
parent: {
@ -36,13 +40,15 @@ export type QueueItem = {
dlVideoOnce: boolean,
dubLang: string[],
image: string
}
} & ResolveItemsData
export type ResolveItemsData = {
id: string,
dubLang: string[],
all: boolean,
but: boolean,
novids: boolean,
noaudio: boolean
dlVideoOnce: boolean,
e: string,
fileName: string,

View file

@ -1,8 +1,9 @@
import { ExtendedProgress } from './messageHandler';
import { ExtendedProgress, QueueItem } from './messageHandler';
export type RandomEvents = {
progress: ExtendedProgress,
finish: undefined
finish: undefined,
queueChange: QueueItem[]
}
export interface RandomEvent<T extends keyof RandomEvents> {

11
@types/ws.d.ts vendored
View file

@ -23,9 +23,9 @@ export type MessageTypes = {
'default': [string, unknown],
'availableDubCodes': [undefined, string[]],
'availableSubCodes': [undefined, string[]],
'resolveItems': [ResolveItemsData, ResponseBase<QueueItem[]>],
'resolveItems': [ResolveItemsData, boolean],
'listEpisodes': [string, EpisodeListResponse],
'downloadItem': [unknown, undefined],
'downloadItem': [QueueItem, undefined],
'isDownloading': [undefined, boolean],
'writeToClipboard': [string, undefined],
'openFolder': [FolderTypes, undefined],
@ -36,5 +36,10 @@ export type MessageTypes = {
'openURL': [string, undefined],
'setuped': [undefined, boolean],
'setupServer': [GUIConfig, boolean],
'requirePassword': [undefined, boolean]
'requirePassword': [undefined, boolean],
'getQueue': [undefined, QueueItem[]],
'removeFromQueue': [number, undefined],
'clearQueue': [undefined, undefined],
'setDownloadQueue': [boolean, undefined],
'getDownloadQueue': [undefined, boolean]
}

View file

@ -22,7 +22,7 @@ const Layout: React.FC = () => {
<AuthButton />
<Box sx={{ display: 'flex', gap: 1, height: 36 }}>
<Button variant="contained" startIcon={<Folder />} onClick={() => messageHandler?.openFolder('content')}>Open Output Directory</Button>
<Button variant="contained" startIcon={<ClearAll />} onClick={() => dispatch({ type: 'queue', payload: [], extraInfo: { force: true } })}>Clear Queue</Button>
<Button variant="contained" startIcon={<ClearAll />} onClick={() => messageHandler?.clearQueue() }>Clear Queue</Button>
</Box>
<AddToQueue />
<StartQueueButton />

View file

@ -48,18 +48,10 @@ const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
const addToQueue = async () => {
setLoading(true);
const res = await messageHandler?.resolveItems(store.downloadOptions);
if (!res || !res.isOk) {
console.error(res);
setLoading(false);
if (!res)
return enqueueSnackbar('The request failed. Please check if the ID is correct.', {
variant: 'error'
});
} else {
dispatch({
type: 'queue',
payload: res.value
});
}
setLoading(false);
if (onFinish)
onFinish();

View file

@ -5,7 +5,6 @@ import useStore from "../../../hooks/useStore";
import { messageChannelContext } from "../../../provider/MessageChannel";
const useDownloadManager = () => {
const [ { currentDownload }, dispatch ] = useStore();
const messageHandler = React.useContext(messageChannelContext);
const [progressData, setProgressData] = React.useState<ExtendedProgress|undefined>();
@ -19,10 +18,6 @@ const useDownloadManager = () => {
const finishHandler = () => {
setProgressData(undefined);
dispatch({
type: 'finish',
payload: undefined
})
}
messageHandler?.randomEvents.on('finish', finishHandler);
@ -30,20 +25,8 @@ const useDownloadManager = () => {
messageHandler?.randomEvents.removeListener('progress', handler);
messageHandler?.randomEvents.removeListener('finish', finishHandler)
};
}, [messageHandler, dispatch]);
React.useEffect(() => {
(async () => {
if (!currentDownload)
return;
if (await messageHandler?.isDownloading())
return;
console.log('start download');
messageHandler?.downloadItem(currentDownload);
})();
}, [currentDownload, messageHandler]);
}, [messageHandler]);
return progressData;
}

View file

@ -1,12 +1,19 @@
import { Box, Button, Divider, LinearProgress, Skeleton, Typography } from "@mui/material";
import React from "react";
import useStore from "../../../hooks/useStore";
import { messageChannelContext } from "../../../provider/MessageChannel";
import { queueContext } from "../../../provider/QueueProvider";
import useDownloadManager from "../DownloadManager/DownloadManager";
const Queue: React.FC = () => {
const data = useDownloadManager();
const [{ queue, currentDownload }, dispatch] = useStore();
const queue = React.useContext(queueContext);
const msg = React.useContext(messageChannelContext);
if (!msg)
return <>Never</>
return data || queue.length > 0 ? <>
{data && <>
<Box sx={{ height: 200, display: 'grid', gridTemplateColumns: '20% 1fr', gap: 1, mb: 1, mt: 1 }}>
@ -35,34 +42,6 @@ const Queue: React.FC = () => {
</Box>
</>
}
{
!data && currentDownload && <>
<Box sx={{ height: 200, display: 'grid', gridTemplateColumns: '20% 1fr', gap: 1, mb: 1, mt: 1 }}>
<img src={currentDownload.image} height='200px' width='100%' alt="Thumbnail" />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr max-content' }}>
<Typography variant='h5' color='text.primary'>
{currentDownload.title}
</Typography>
<Typography variant='h5' color='text.primary'>
Languages: {currentDownload.dubLang}
</Typography>
</Box>
<Typography variant='h6' color='text.primary'>
{currentDownload.parent.title}
</Typography>
</Box>
<LinearProgress variant='indeterminate' sx={{ height: '10px' }} />
<Box>
<Typography variant="body1" color='text.primary'>
Waiting for download to start
</Typography>
</Box>
</Box>
</Box>
</>
}
{queue.length && data && <Divider variant="fullWidth" />}
{queue.map((queueItem, index, { length }) => {
return <Box key={`queue_item_${index}`}>
@ -87,15 +66,7 @@ const Queue: React.FC = () => {
Quality: {queueItem.q}
</Typography>
<Button onClick={() => {
const override = [...queue];
override.splice(index, 1);
dispatch({
type: 'queue',
payload: override,
extraInfo: {
force: true
}
});
msg.removeFromQueue(index);
}} sx={{ position: 'relative', left: '50%', transform: 'translateX(-50%)', width: '60%' }} variant="outlined" color="warning">
Remove from Queue
</Button>

View file

@ -7,25 +7,32 @@ import Require from "./Require";
const StartQueueButton: React.FC = () => {
const messageChannel = React.useContext(messageChannelContext);
const [store, dispatch] = useStore();
const [start, setStart] = React.useState(false);
const msg = React.useContext(messageChannelContext);
React.useEffect(() => {
(async () => {
if (!msg)
return alert('Invalid state: msg not found');
setStart(await msg.getDownloadQueue());
})();
}, []);
const change = async () => {
if (await messageChannel?.isDownloading() && store.downloadQueue)
if (await messageChannel?.isDownloading())
alert("The current download will be finished before the queue stops")
dispatch({
type: 'downloadQueue',
payload: !store.downloadQueue
})
msg?.setDownloadQueue(!start);
setStart(!start);
}
return <Require value={messageChannel}>
<Button
startIcon={store.downloadQueue ? <PauseCircleFilled /> : <PlayCircleFilled /> }
startIcon={start ? <PauseCircleFilled /> : <PlayCircleFilled /> }
variant='contained'
onClick={change}
>
{
store.downloadQueue ? 'Stop Queue' : 'Start Queue'
start ? 'Stop Queue' : 'Start Queue'
}
</Button>
</Require>

View file

@ -9,6 +9,7 @@ import { CloseOutlined } from "@mui/icons-material";
import { SnackbarProvider, SnackbarKey } from 'notistack';
import Store from './provider/Store';
import ErrorHandler from './provider/ErrorHandler';
import QueueProvider from './provider/QueueProvider';
const notistackRef = React.createRef<SnackbarProvider>();
const onClickDismiss = (key: SnackbarKey | undefined) => () => {
@ -32,7 +33,9 @@ root.render(
<Style>
<MessageChannel>
<ServiceProvider>
<App />
<QueueProvider>
<App />
</QueueProvider>
</ServiceProvider>
</MessageChannel>
</Style>

View file

@ -1,5 +1,5 @@
import React from 'react';
import type { MessageHandler } from '../../../../@types/messageHandler';
import { MessageHandler, QueueItem } from '../../../../@types/messageHandler';
import useStore from '../hooks/useStore';
import type { MessageTypes, WSMessage, WSMessageWithID } from '../../../../@types/ws';
import type { Handler, RandomEvent, RandomEvents } from '../../../../@types/randomEvents';
@ -16,7 +16,8 @@ export class RandomEventHandler {
[eventName in keyof RandomEvents]: Handler<eventName>[]
} = {
progress: [],
finish: []
finish: [],
queueChange: []
};
public on<T extends keyof RandomEvents>(name: T, listener: Handler<T>) {
@ -225,7 +226,12 @@ const MessageChannelProvider: FCWithChildren = ({ children }) => {
openFolder: async (data) => messageAndResponse(socket, { name: 'openFolder', data }),
logout: async () => (await messageAndResponse(socket, { name: 'changeProvider', data: undefined })).data,
openFile: async (data) => await messageAndResponse(socket, { name: 'openFile', data }),
openURL: async (data) => await messageAndResponse(socket, { name: 'openURL', data })
openURL: async (data) => await messageAndResponse(socket, { name: 'openURL', data }),
getQueue: async () => (await messageAndResponse(socket, { name: 'getQueue', data: undefined })).data,
removeFromQueue: async (data) => await messageAndResponse(socket, { name: 'removeFromQueue', data }),
clearQueue: async () => await messageAndResponse(socket, { name: 'clearQueue', data: undefined }),
setDownloadQueue: async (data) => await messageAndResponse(socket, { name: 'setDownloadQueue', data }),
getDownloadQueue: async () => (await messageAndResponse(socket, { name: 'getDownloadQueue', data: undefined })).data,
}
return <messageChannelContext.Provider value={messageHandler}>

View file

@ -0,0 +1,35 @@
import React from 'react';
import { QueueItem } from '../../../../@types/messageHandler';
import { messageChannelContext } from './MessageChannel';
import { RandomEvent } from '../../../../@types/randomEvents';
export const queueContext = React.createContext<QueueItem[]>([]);
const QueueProvider: FCWithChildren = ({ children }) => {
const msg = React.useContext(messageChannelContext);
const [ready, setReady] = React.useState(false);
const [queue, setQueue] = React.useState<QueueItem[]>([]);
React.useEffect(() => {
if (msg && !ready) {
msg.getQueue().then(data => {
setQueue(data);
setReady(true);
});
}
const listener = (ev: RandomEvent<'queueChange'>) => {
setQueue(ev.data);
}
msg?.randomEvents.on('queueChange', listener);
return () => {
msg?.randomEvents.removeListener('queueChange', listener);
}
}, [ msg ]);
return <queueContext.Provider value={queue}>
{children}
</queueContext.Provider>;
};
export default QueueProvider;

View file

@ -17,13 +17,9 @@ export type DownloadOptions = {
}
export type StoreState = {
downloadQueue: boolean,
queue: QueueItem[],
episodeListing: Episode[];
downloadOptions: DownloadOptions,
service: 'crunchy'|'funi'|undefined,
currentDownload?: QueueItem,
finish?: undefined
}
export type StoreAction<T extends (keyof StoreState)> = {
@ -34,36 +30,12 @@ export type StoreAction<T extends (keyof StoreState)> = {
const Reducer = <T extends keyof StoreState,>(state: StoreState, action: StoreAction<T>): StoreState => {
switch(action.type) {
case "queue":
state.queue = action.extraInfo?.force ? action.payload as QueueItem[] : state.queue.concat(action.payload as QueueItem[]);
if (state.currentDownload === undefined && state.queue.length > 0 && state.downloadQueue) {
state.currentDownload = state.queue[0];
state.queue = state.queue.slice(1);
}
return { ...state };
case "finish":
if (state.queue.length > 0 && state.downloadQueue) {
state.currentDownload = state.queue[0];
state.queue = state.queue.slice(1);
} else {
state.currentDownload = undefined;
}
return { ...state }
case 'downloadQueue':
state.downloadQueue = action.payload as boolean;
if (state.queue.length > 0 && state.downloadQueue && state.currentDownload === undefined) {
state.currentDownload = state.queue[0];
state.queue = state.queue.slice(1);
}
return {...state}
default:
return { ...state, [action.type]: action.payload }
}
};
const initialState: StoreState = {
downloadQueue: false,
queue: [],
downloadOptions: {
id: '',
q: 0,
@ -110,5 +82,5 @@ const Store: FCWithChildren = ({children}) => {
};
/* Importent Notice -- The 'queue' generic will be overriden */
export const StoreContext = React.createContext<[StoreState, React.Dispatch<StoreAction<'queue'>>]>([initialState, undefined as any]);
export const StoreContext = React.createContext<[StoreState, React.Dispatch<StoreAction<'downloadOptions'>>]>([initialState, undefined as any]);
export default Store;

View file

@ -74,7 +74,7 @@ export default class ServiceHandler {
});
this.ws.events.on('resolveItems', async ({ data }, respond) => {
if (this.service === undefined)
return respond({ isOk: false, reason: new Error('No service selected') });
return respond(false);
respond(await this.service.resolveItems(data));
});
this.ws.events.on('listEpisodes', async ({ data }, respond) => {
@ -102,6 +102,24 @@ export default class ServiceHandler {
this.service!.openURL(data);
respond(undefined);
});
this.ws.events.on('getQueue', async ({ }, respond) => {
respond(await this.service?.getQueue() ?? []);
});
this.ws.events.on('removeFromQueue', async ({ data }, respond) => {
this.service?.removeFromQueue(data);
respond(undefined);
});
this.ws.events.on('clearQueue', async ({ }, respond) => {
this.service?.clearQueue();
respond(undefined);
});
this.ws.events.on('setDownloadQueue', async ({ data }, respond) => {
this.service?.setDownloadQueue(data);
respond(undefined);
});
this.ws.events.on('getDownloadQueue', async ({ }, respond) => {
respond(await this.service?.getDownloadQueue() ?? false);
});
this.ws.events.on('isDownloading', async ({}, respond) => respond(await this.service!.isDownloading()));
}

View file

@ -1,4 +1,4 @@
import { DownloadInfo, FolderTypes, ProgressData } from '../../../@types/messageHandler';
import { DownloadInfo, FolderTypes, ProgressData, QueueItem } from '../../../@types/messageHandler';
import { RandomEvent, RandomEvents } from '../../../@types/randomEvents';
import WebSocketHandler from '../websocket';
import copy from "copy-to-clipboard";
@ -12,6 +12,9 @@ export default class Base {
private downloading = false;
private queue: QueueItem[] = [];
private workOnQueue = false;
setDownloading(downloading: boolean) {
this.downloading = downloading;
}
@ -74,4 +77,48 @@ export default class Base {
open(data);
}
public async getQueue(): Promise<QueueItem[]> {
return this.queue;
}
public async removeFromQueue(index: number) {
this.queue.splice(index, 1);
this.queueChange();
}
public async clearQueue() {
this.queue = [];
this.queueChange();
}
public addToQueue(data: QueueItem[]) {
this.queue = this.queue.concat(...data);
this.queueChange();
}
public setDownloadQueue(data: boolean) {
this.workOnQueue = data;
this.queueChange();
}
public async getDownloadQueue(): Promise<boolean> {
return this.workOnQueue;
}
private async queueChange() {
this.sendMessage({ name: 'queueChange', data: this.queue });
if (this.workOnQueue && this.queue.length > 0 && !await this.isDownloading()) {
this.setDownloading(true);
this.downloadItem(this.queue[0]);
this.queue = this.queue.slice(1);
this.queueChange();
}
}
public async onFinish() {
this.queueChange();
}
//Overriten
public async downloadItem(data: QueueItem) {}
}

View file

@ -36,15 +36,16 @@ class CrunchyHandler extends Base implements MessageHandler {
return subtitleLanguagesFilter;
}
public async resolveItems(data: ResolveItemsData): Promise<ResponseBase<QueueItem[]>> {
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
await this.crunchy.refreshToken(true);
console.log(`[DEBUG] Got resolve options: ${JSON.stringify(data)}`);
const res = await this.crunchy.downloadFromSeriesID(data.id, data);
if (!res.isOk)
return res;
return { isOk: true, value: res.value.map(a => {
return res.isOk;
this.addToQueue(res.value.map(a => {
return {
...data,
ids: a.data.map(a => a.mediaId),
title: a.episodeTitle,
parent: {
@ -55,7 +56,8 @@ class CrunchyHandler extends Base implements MessageHandler {
image: a.image,
episode: a.episodeNumber
};
}) };
}));
return true;
}
public async search(data: SearchData): Promise<SearchResponse> {
@ -104,6 +106,7 @@ class CrunchyHandler extends Base implements MessageHandler {
}
this.sendMessage({ name: 'finish', data: undefined });
this.setDownloading(false);
this.onFinish();
}
}

View file

@ -51,12 +51,12 @@ class FunimationHandler extends Base implements MessageHandler {
return subtitleLanguagesFilter;
}
public async resolveItems(data: ResolveItemsData): Promise<ResponseBase<QueueItem[]>> {
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
console.log(`[DEBUG] Got resolve options: ${JSON.stringify(data)}`);
const res = await this.funi.getShow(false, { ...data, id: parseInt(data.id) });
if (!res.isOk)
return res;
return { isOk: true, value: res.value.map(a => {
return res.isOk;
this.addToQueue(res.value.map(a => {
return {
...data,
ids: [a.episodeID],
@ -69,7 +69,8 @@ class FunimationHandler extends Base implements MessageHandler {
e: a.episodeID,
episode: a.epsiodeNumber,
};
}) };
}));
return true;
}
public async search(data: SearchData): Promise<SearchResponse> {
@ -95,7 +96,7 @@ class FunimationHandler extends Base implements MessageHandler {
return this.funi.auth(data);
}
public async downloadItem(data: DownloadData) {
public async downloadItem(data: QueueItem) {
this.setDownloading(true);
console.log(`[DEBUG] Got download options: ${JSON.stringify(data)}`);
const res = await this.funi.getShow(false, { all: false, but: false, id: parseInt(data.id), e: data.e });
@ -109,6 +110,7 @@ class FunimationHandler extends Base implements MessageHandler {
}
this.sendMessage({ name: 'finish', data: undefined });
this.setDownloading(false);
this.onFinish();
}
}