Add GUI States

This adds states for the GUI that get saved to a file, notably this means that queue's of services are no longer lost when changing services, or closing and reopening the program.

Also changed was a few spelling fixes
This commit is contained in:
AnimeDL 2023-07-15 11:16:55 -07:00
parent f7ddaf1176
commit c3dab33f6b
12 changed files with 93 additions and 53 deletions

View file

@ -4,6 +4,7 @@ import type { AvailableMuxer } from '../modules/module.args';
import { LanguageItem } from '../modules/module.langsData'; import { LanguageItem } from '../modules/module.langsData';
export interface MessageHandler { export interface MessageHandler {
name: string
auth: (data: AuthData) => Promise<AuthResponse>; auth: (data: AuthData) => Promise<AuthResponse>;
checkToken: () => Promise<CheckTokenResponse>; checkToken: () => Promise<CheckTokenResponse>;
search: (data: SearchData) => Promise<SearchResponse>, search: (data: SearchData) => Promise<SearchResponse>,
@ -21,8 +22,7 @@ export interface MessageHandler {
removeFromQueue: (index: number) => void, removeFromQueue: (index: number) => void,
clearQueue: () => void, clearQueue: () => void,
setDownloadQueue: (data: boolean) => void, setDownloadQueue: (data: boolean) => void,
getDownloadQueue: () => Promise<boolean>, getDownloadQueue: () => Promise<boolean>
name: string
} }
export type FolderTypes = 'content' | 'config'; export type FolderTypes = 'content' | 'config';
@ -131,7 +131,7 @@ export type ProgressData = {
bytes: number bytes: number
}; };
export type PossibleMessanges = keyof ServiceHandler; export type PossibleMessages = keyof ServiceHandler;
export type DownloadInfo = { export type DownloadInfo = {
image: string, image: string,
@ -146,4 +146,13 @@ export type DownloadInfo = {
export type ExtendedProgress = { export type ExtendedProgress = {
progress: ProgressData, progress: ProgressData,
downloadInfo: DownloadInfo downloadInfo: DownloadInfo
}
export type GuiState = {
setup: Boolean,
services: Record<string, GuiStateService>
}
export type GuiStateService = {
queue: QueueItem[]
} }

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

@ -33,7 +33,7 @@ export type MessageTypes = {
'setup': ['funi'|'crunchy'|'hidive'|undefined, undefined], 'setup': ['funi'|'crunchy'|'hidive'|undefined, undefined],
'openFile': [[FolderTypes, string], undefined], 'openFile': [[FolderTypes, string], undefined],
'openURL': [string, undefined], 'openURL': [string, undefined],
'setuped': [undefined, boolean], 'isSetup': [undefined, boolean],
'setupServer': [GUIConfig, boolean], 'setupServer': [GUIConfig, boolean],
'requirePassword': [undefined, boolean], 'requirePassword': [undefined, boolean],
'getQueue': [undefined, QueueItem[]], 'getQueue': [undefined, QueueItem[]],

View file

@ -1,3 +0,0 @@
{
"setuped": true
}

View file

@ -9,7 +9,7 @@ import { useSnackbar } from 'notistack';
import { LockOutlined, PowerSettingsNew } from '@mui/icons-material'; import { LockOutlined, PowerSettingsNew } from '@mui/icons-material';
import { GUIConfig } from '../../../../modules/module.cfg-loader'; import { GUIConfig } from '../../../../modules/module.cfg-loader';
export type FrontEndMessanges = (MessageHandler & { randomEvents: RandomEventHandler, logout: () => Promise<boolean> }); export type FrontEndMessages = (MessageHandler & { randomEvents: RandomEventHandler, logout: () => Promise<boolean> });
export class RandomEventHandler { export class RandomEventHandler {
private handler: { private handler: {
@ -38,7 +38,7 @@ export class RandomEventHandler {
} }
} }
export const messageChannelContext = React.createContext<FrontEndMessanges|undefined>(undefined); export const messageChannelContext = React.createContext<FrontEndMessages|undefined>(undefined);
async function messageAndResponse<T extends keyof MessageTypes>(socket: WebSocket, msg: WSMessage<T>): Promise<WSMessage<T, 1>> { async function messageAndResponse<T extends keyof MessageTypes>(socket: WebSocket, msg: WSMessage<T>): Promise<WSMessage<T, 1>> {
const id = v4(); const id = v4();
@ -65,7 +65,7 @@ const MessageChannelProvider: FCWithChildren = ({ children }) => {
const [socket, setSocket] = React.useState<undefined|WebSocket>(); const [socket, setSocket] = React.useState<undefined|WebSocket>();
const [publicWS, setPublicWS] = React.useState<undefined|WebSocket>(); const [publicWS, setPublicWS] = React.useState<undefined|WebSocket>();
const [usePassword, setUsePassword] = React.useState<'waiting'|'yes'|'no'>('waiting'); const [usePassword, setUsePassword] = React.useState<'waiting'|'yes'|'no'>('waiting');
const [isSetuped, setIsSetuped] = React.useState<'waiting'|'yes'|'no'>('waiting'); const [isSetup, setIsSetup] = React.useState<'waiting'|'yes'|'no'>('waiting');
const { enqueueSnackbar } = useSnackbar(); const { enqueueSnackbar } = useSnackbar();
@ -84,7 +84,7 @@ const MessageChannelProvider: FCWithChildren = ({ children }) => {
if (!publicWS) if (!publicWS)
return; return;
setUsePassword((await messageAndResponse(publicWS, { name: 'requirePassword', data: undefined })).data ? 'yes' : 'no'); setUsePassword((await messageAndResponse(publicWS, { name: 'requirePassword', data: undefined })).data ? 'yes' : 'no');
setIsSetuped((await messageAndResponse(publicWS, { name: 'setuped', data: undefined })).data ? 'yes' : 'no'); setIsSetup((await messageAndResponse(publicWS, { name: 'isSetup', data: undefined })).data ? 'yes' : 'no');
})(); })();
}, [publicWS]); }, [publicWS]);
@ -190,7 +190,7 @@ const MessageChannelProvider: FCWithChildren = ({ children }) => {
</Box>; </Box>;
} }
if (isSetuped === 'no') { if (isSetup === 'no') {
return <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', justifyItems: 'center', alignItems: 'center' }}> return <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', justifyItems: 'center', alignItems: 'center' }}>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}> <Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<PowerSettingsNew /> <PowerSettingsNew />
@ -211,7 +211,8 @@ const MessageChannelProvider: FCWithChildren = ({ children }) => {
</Box>; </Box>;
} }
const messageHandler: FrontEndMessanges = { const messageHandler: FrontEndMessages = {
name: "default",
auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data, auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data,
checkToken: async () => (await messageAndResponse(socket, { name: 'checkToken', data: undefined })).data, checkToken: async () => (await messageAndResponse(socket, { name: 'checkToken', data: undefined })).data,
search: async (data) => (await messageAndResponse(socket, { name: 'search', data })).data, search: async (data) => (await messageAndResponse(socket, { name: 'search', data })).data,

View file

@ -1,8 +1,8 @@
import { ServerResponse } from 'http'; import { ServerResponse } from 'http';
import { Server } from 'http'; import { Server } from 'http';
import { IncomingMessage } from 'http'; import { IncomingMessage } from 'http';
import { MessageHandler } from '../../@types/messageHandler'; import { MessageHandler, GuiState } from '../../@types/messageHandler';
import { setSetuped, writeYamlCfgFile } from '../../modules/module.cfg-loader'; import { setState, getState, writeYamlCfgFile } from '../../modules/module.cfg-loader';
import CrunchyHandler from './services/crunchyroll'; import CrunchyHandler from './services/crunchyroll';
import FunimationHandler from './services/funimation'; import FunimationHandler from './services/funimation';
import HidiveHandler from './services/hidive'; import HidiveHandler from './services/hidive';
@ -12,16 +12,19 @@ export default class ServiceHandler {
private service: MessageHandler|undefined = undefined; private service: MessageHandler|undefined = undefined;
private ws: WebSocketHandler; private ws: WebSocketHandler;
private state: GuiState;
constructor(server: Server<typeof IncomingMessage, typeof ServerResponse>) { constructor(server: Server<typeof IncomingMessage, typeof ServerResponse>) {
this.ws = new WebSocketHandler(server); this.ws = new WebSocketHandler(server);
this.handleMessanges(); this.handleMessages();
this.state = getState();
} }
private handleMessanges() { private handleMessages() {
this.ws.events.on('setupServer', ({ data }, respond) => { this.ws.events.on('setupServer', ({ data }, respond) => {
writeYamlCfgFile('gui', data); writeYamlCfgFile('gui', data);
setSetuped(true); this.state.setup = true;
setState(this.state);
respond(true); respond(true);
process.exit(0); process.exit(0);
}); });

View file

@ -1,20 +1,35 @@
import { DownloadInfo, FolderTypes, ProgressData, QueueItem } from '../../../@types/messageHandler'; import { DownloadInfo, FolderTypes, GuiState, ProgressData, QueueItem } from '../../../@types/messageHandler';
import { RandomEvent, RandomEvents } from '../../../@types/randomEvents'; import { RandomEvent, RandomEvents } from '../../../@types/randomEvents';
import WebSocketHandler from '../websocket'; import WebSocketHandler from '../websocket';
import open from 'open'; import open from 'open';
import { cfg } from '..'; import { cfg } from '..';
import path from 'path'; import path from 'path';
import { console } from '../../../modules/log'; import { console } from '../../../modules/log';
import { getState, setState } from '../../../modules/module.cfg-loader';
export default class Base { export default class Base {
private state: GuiState;
constructor(private ws: WebSocketHandler) {} public name = 'default';
constructor(private ws: WebSocketHandler) {
this.state = getState();
}
private downloading = false; private downloading = false;
private queue: QueueItem[] = []; private queue: QueueItem[] = [];
private workOnQueue = false; private workOnQueue = false;
initState() {
if (this.state.services[this.name]) {
this.queue = this.state.services[this.name].queue;
this.queueChange();
} else {
this.state.services[this.name] = {
'queue': []
}
}
}
setDownloading(downloading: boolean) { setDownloading(downloading: boolean) {
this.downloading = downloading; this.downloading = downloading;
} }
@ -109,6 +124,8 @@ export default class Base {
this.queue = this.queue.slice(1); this.queue = this.queue.slice(1);
this.queueChange(); this.queueChange();
} }
this.state.services[this.name].queue = this.queue;
setState(this.state);
} }
public async onFinish() { public async onFinish() {

View file

@ -14,6 +14,7 @@ class CrunchyHandler extends Base implements MessageHandler {
super(ws); super(ws);
this.crunchy = new Crunchy(); this.crunchy = new Crunchy();
this.crunchy.refreshToken(); this.crunchy.refreshToken();
this.initState();
} }
public async listEpisodes (id: string): Promise<EpisodeListResponse> { public async listEpisodes (id: string): Promise<EpisodeListResponse> {

View file

@ -13,6 +13,7 @@ class FunimationHandler extends Base implements MessageHandler {
constructor(ws: WebSocketHandler) { constructor(ws: WebSocketHandler) {
super(ws); super(ws);
this.funi = new Funimation(); this.funi = new Funimation();
this.initState();
} }
public async listEpisodes (id: string) : Promise<EpisodeListResponse> { public async listEpisodes (id: string) : Promise<EpisodeListResponse> {

View file

@ -14,6 +14,7 @@ class HidiveHandler extends Base implements MessageHandler {
super(ws); super(ws);
this.hidive = new Hidive(); this.hidive = new Hidive();
this.hidive.doInit(); this.hidive.doInit();
this.initState();
} }
public auth(data: AuthData) { public auth(data: AuthData) {

View file

@ -4,7 +4,7 @@ import { RandomEvent, RandomEvents } from '../../@types/randomEvents';
import { MessageTypes, UnknownWSMessage, WSMessage } from '../../@types/ws'; import { MessageTypes, UnknownWSMessage, WSMessage } from '../../@types/ws';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { cfg } from '.'; import { cfg } from '.';
import { isSetuped } from '../../modules/module.cfg-loader'; import { getState } from '../../modules/module.cfg-loader';
import { console } from '../../modules/log'; import { console } from '../../modules/log';
declare interface ExternalEvent { declare interface ExternalEvent {
@ -81,6 +81,7 @@ export default class WebSocketHandler {
export class PublicWebSocket { export class PublicWebSocket {
private wsServer: ws.Server; private wsServer: ws.Server;
private state = getState();
constructor(server: Server) { constructor(server: Server) {
this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/public' }); this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/public' });
@ -90,8 +91,8 @@ export class PublicWebSocket {
socket.on('message', (msg) => { socket.on('message', (msg) => {
const data = JSON.parse(msg.toString()) as UnknownWSMessage; const data = JSON.parse(msg.toString()) as UnknownWSMessage;
switch (data.name) { switch (data.name) {
case 'setuped': case 'isSetup':
this.send(socket, data.id, data.name, isSetuped()); this.send(socket, data.id, data.name, this.state.setup);
break; break;
case 'requirePassword': case 'requirePassword':
this.send(socket, data.id, data.name, cfg.gui.password !== undefined); this.send(socket, data.id, data.name, cfg.gui.password !== undefined);

View file

@ -3,6 +3,7 @@ import yaml from 'yaml';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { lookpath } from 'lookpath'; import { lookpath } from 'lookpath';
import { console } from './log'; import { console } from './log';
import { GuiState } from '../@types/messageHandler';
// new-cfg // new-cfg
const workingDir = (process as NodeJS.Process & { const workingDir = (process as NodeJS.Process & {
@ -15,17 +16,17 @@ const binCfgFile = path.join(workingDir, 'config', 'bin-path');
const dirCfgFile = path.join(workingDir, 'config', 'dir-path'); const dirCfgFile = path.join(workingDir, 'config', 'dir-path');
const guiCfgFile = path.join(workingDir, 'config', 'gui'); const guiCfgFile = path.join(workingDir, 'config', 'gui');
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults'); const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
const hdProfileCfgFile = path.join(workingDir, 'config', 'hd_profile'); const hdPflCfgFile = path.join(workingDir, 'config', 'hd_profile');
const sessCfgFile = { const sessCfgFile = {
funi: path.join(workingDir, 'config', 'funi_sess'), funi: path.join(workingDir, 'config', 'funi_sess'),
cr: path.join(workingDir, 'config', 'cr_sess'), cr: path.join(workingDir, 'config', 'cr_sess'),
hd: path.join(workingDir, 'config', 'hd_sess') hd: path.join(workingDir, 'config', 'hd_sess')
}; };
const setupFile = path.join(workingDir, 'config', 'setup'); const stateFile = path.join(workingDir, 'config', 'guistate');
const tokenFile = { const tokenFile = {
funi: path.join(workingDir, 'config', 'funi_token'), funi: path.join(workingDir, 'config', 'funi_token'),
cr: path.join(workingDir, 'config', 'cr_token'), cr: path.join(workingDir, 'config', 'cr_token'),
hd: path.join(workingDir, 'config', 'hd_token') hd: path.join(workingDir, 'config', 'hd_token')
}; };
export const ensureConfig = () => { export const ensureConfig = () => {
@ -256,10 +257,10 @@ const saveHDToken = (data: Record<string, unknown>) => {
}; };
const saveHDProfile = (data: Record<string, unknown>) => { const saveHDProfile = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(hdProfileCfgFile); const cfgFolder = path.dirname(hdPflCfgFile);
try{ try{
fs.ensureDirSync(cfgFolder); fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${hdProfileCfgFile}.yml`, yaml.stringify(data)); fs.writeFileSync(`${hdPflCfgFile}.yml`, yaml.stringify(data));
} }
catch(e){ catch(e){
console.error('Can\'t save profile file to disk!'); console.error('Can\'t save profile file to disk!');
@ -267,7 +268,7 @@ const saveHDProfile = (data: Record<string, unknown>) => {
}; };
const loadHDProfile = () => { const loadHDProfile = () => {
let profile = loadYamlCfgFile(hdProfileCfgFile, true); let profile = loadYamlCfgFile(hdPflCfgFile, true);
if(typeof profile !== 'object' || profile === null || Array.isArray(profile) || Object.keys(profile).length === 0){ if(typeof profile !== 'object' || profile === null || Array.isArray(profile) || Object.keys(profile).length === 0){
profile = { profile = {
// base // base
@ -316,23 +317,31 @@ const saveFuniToken = (data: {
const cfgDir = path.join(workingDir, 'config'); const cfgDir = path.join(workingDir, 'config');
const isSetuped = (): boolean => { const getState = (): GuiState => {
const fn = `${setupFile}.json`; const fn = `${stateFile}.json`;
if (!fs.existsSync(fn)) if (!fs.existsSync(fn)) {
return false; return {
return JSON.parse(fs.readFileSync(fn).toString()).setuped; "setup": false,
"services": {}
}
}
try {
return JSON.parse(fs.readFileSync(fn).toString());
} catch(e) {
console.error('Invalid state file, regenerating');
return {
"setup": false,
"services": {}
}
}
}; };
const setSetuped = (bool: boolean) => { const setState = (state: GuiState) => {
const fn = `${setupFile}.json`; const fn = `${stateFile}.json`;
if (bool) { try {
fs.writeFileSync(fn, JSON.stringify({ fs.writeFileSync(fn, JSON.stringify(state, null, 2));
setuped: true } catch(e) {
}, null, 2)); console.error('Failed to write state file.');
} else {
if (fs.existsSync(fn)) {
fs.removeSync(fn);
}
} }
}; };
@ -352,10 +361,10 @@ export {
loadHDToken, loadHDToken,
saveHDProfile, saveHDProfile,
loadHDProfile, loadHDProfile,
isSetuped, getState,
setSetuped, setState,
writeYamlCfgFile, writeYamlCfgFile,
sessCfgFile, sessCfgFile,
hdProfileCfgFile, hdPflCfgFile,
cfgDir cfgDir
}; };

View file

@ -1,7 +1,7 @@
{ {
"name": "multi-downloader-nx", "name": "multi-downloader-nx",
"short_name": "aniDL", "short_name": "aniDL",
"version": "4.2.1b1", "version": "4.3.0b1",
"description": "Download videos from Funimation, Crunchyroll, or Hidive via cli", "description": "Download videos from Funimation, Crunchyroll, or Hidive via cli",
"keywords": [ "keywords": [
"download", "download",