File upload and code refactoring

This commit is contained in:
2022-06-15 12:02:50 +02:00
parent c0982d7098
commit b6ff444526
6 changed files with 100 additions and 28 deletions

View File

@@ -6,8 +6,11 @@ import {
CircularProgress, CircularProgress,
Container, Container,
Grid, Grid,
IconButton,
InputAdornment,
Paper, Paper,
Snackbar, Snackbar,
styled,
TextField, TextField,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
@@ -17,7 +20,9 @@ import { StackableResult } from "./components/StackableResult";
import { connected, downloading, finished } from "./features/status/statusSlice"; import { connected, downloading, finished } from "./features/status/statusSlice";
import { IDLInfo, IDLInfoBase, IDownloadInfo, IMessage } from "./interfaces"; import { IDLInfo, IDLInfoBase, IDownloadInfo, IMessage } from "./interfaces";
import { RootState } from "./stores/store"; import { RootState } from "./stores/store";
import { toFormatArgs, updateInStateMap, } from "./utils"; import { isValidURL, toFormatArgs, updateInStateMap, } from "./utils";
import { FileUpload } from "@mui/icons-material";
import { Buffer } from 'buffer';
type Props = { type Props = {
socket: Socket socket: Socket
@@ -74,7 +79,7 @@ export default function Home({ socket }: Props) {
socket.on('info', (data: IDLInfo) => { socket.on('info', (data: IDLInfo) => {
setShowBackdrop(false) setShowBackdrop(false)
dispatch(downloading()) dispatch(downloading())
updateInStateMap(data.pid, data.info, downloadInfoMap, setDownloadInfoMap); updateInStateMap<number, IDLInfoBase>(data.pid, data.info, downloadInfoMap, setDownloadInfoMap);
}) })
}, []) }, [])
@@ -83,15 +88,19 @@ export default function Home({ socket }: Props) {
socket.on('progress', (data: IMessage) => { socket.on('progress', (data: IMessage) => {
if (data.status === 'Done!' || data.status === 'Aborted') { if (data.status === 'Done!' || data.status === 'Aborted') {
setShowBackdrop(false) setShowBackdrop(false)
updateInStateMap(data.pid, 'Done!', messageMap, setMessageMap); updateInStateMap<number, IMessage>(data.pid, 'Done!', messageMap, setMessageMap);
updateInStateMap(data.pid, 0, progressMap, setProgressMap); updateInStateMap<number, number>(data.pid, 0, progressMap, setProgressMap);
socket.emit('disk-space') socket.emit('disk-space')
dispatch(finished()) dispatch(finished())
return; return;
} }
updateInStateMap(data.pid, data, messageMap, setMessageMap); updateInStateMap<number, IMessage>(data.pid, data, messageMap, setMessageMap);
if (data.progress) { if (data.progress) {
updateInStateMap(data.pid, Math.ceil(Number(data.progress.replace('%', ''))), progressMap, setProgressMap) updateInStateMap<number, number>(data.pid,
Math.ceil(Number(data.progress.replace('%', ''))),
progressMap,
setProgressMap
);
} }
}) })
}, []) }, [])
@@ -101,14 +110,14 @@ export default function Home({ socket }: Props) {
/** /**
* Retrive url from input, cli-arguments from checkboxes and emits via WebSocket * Retrive url from input, cli-arguments from checkboxes and emits via WebSocket
*/ */
const sendUrl = () => { const sendUrl = (immediate?: string) => {
const codes = new Array<string>(); const codes = new Array<string>();
if (pickedVideoFormat !== '') codes.push(pickedVideoFormat); if (pickedVideoFormat !== '') codes.push(pickedVideoFormat);
if (pickedAudioFormat !== '') codes.push(pickedAudioFormat); if (pickedAudioFormat !== '') codes.push(pickedAudioFormat);
if (pickedBestFormat !== '') codes.push(pickedBestFormat); if (pickedBestFormat !== '') codes.push(pickedBestFormat);
socket.emit('send-url', { socket.emit('send-url', {
url: url || workingUrl, url: immediate || url || workingUrl,
params: settings.cliArgs.toString() + toFormatArgs(codes), params: settings.cliArgs.toString() + toFormatArgs(codes),
}) })
setUrl('') setUrl('')
@@ -163,6 +172,27 @@ export default function Home({ socket }: Props) {
socket.emit('abort-all') socket.emit('abort-all')
} }
const parseUrlListFile = (event: any) => {
const urlList = event.target.files
const reader = new FileReader()
reader.addEventListener('load', $event => {
const base64 = $event.target?.result!.toString().split(',')[1]
Buffer.from(base64!, 'base64')
.toString()
.trimEnd()
.split('\n')
.filter(_url => isValidURL(_url))
.forEach(_url => sendUrl(_url))
})
reader.readAsDataURL(urlList[0])
}
/* -------------------- styled components -------------------- */
const Input = styled('input')({
display: 'none',
});
return ( return (
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}> <Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
<Backdrop <Backdrop
@@ -185,7 +215,19 @@ export default function Home({ socket }: Props) {
label={settings.i18n.t('urlInput')} label={settings.i18n.t('urlInput')}
variant="outlined" variant="outlined"
onChange={handleUrlChange} onChange={handleUrlChange}
disabled={settings.formatSelection && downloadFormats != null} disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<label htmlFor="icon-button-file">
<Input id="icon-button-file" type="file" accept=".txt" onChange={parseUrlListFile} />
<IconButton color="primary" aria-label="upload file" component="span">
<FileUpload />
</IconButton>
</label>
</InputAdornment>
),
}}
/> />
<Grid container spacing={1} pt={2}> <Grid container spacing={1} pt={2}>
<Grid item> <Grid item>

View File

@@ -19,6 +19,7 @@ import {
} from "@mui/material"; } from "@mui/material";
import React, { useState } from "react"; import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { debounceTime, distinctUntilChanged, map, of } from "rxjs";
import { Socket } from "socket.io-client"; import { Socket } from "socket.io-client";
import { LanguageUnion, setCliArgs, setFormatSelection, setLanguage, setServerAddr, setTheme, ThemeUnion } from "./features/settings/settingsSlice"; import { LanguageUnion, setCliArgs, setFormatSelection, setLanguage, setServerAddr, setTheme, ThemeUnion } from "./features/settings/settingsSlice";
import { alreadyUpdated, updated } from "./features/status/statusSlice"; import { alreadyUpdated, updated } from "./features/status/statusSlice";
@@ -38,20 +39,28 @@ export default function Settings({ socket }: Props) {
/** /**
* Update the server ip address state and localstorage whenever the input value changes. * Update the server ip address state and localstorage whenever the input value changes.
* Validate the ip-addr then set. * Validate the ip-addr then set.s
* @param e Input change event * @param e Input change event
*/ */
const handleAddrChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleAddrChange = (event: any) => {
const input = e.target.value; const $serverAddr = of(event)
if (validateIP(input)) { .pipe(
map(event => event.target.value),
debounceTime(500),
distinctUntilChanged()
)
.subscribe(addr => {
if (validateIP(addr)) {
setInvalidIP(false) setInvalidIP(false)
dispatch(setServerAddr(input)) dispatch(setServerAddr(addr))
} else if (validateDomain(input)) { } else if (validateDomain(addr)) {
setInvalidIP(false) setInvalidIP(false)
dispatch(setServerAddr(input)) dispatch(setServerAddr(addr))
} else { } else {
setInvalidIP(true) setInvalidIP(true)
} }
})
return $serverAddr.unsubscribe()
} }
/** /**
@@ -79,7 +88,6 @@ export default function Settings({ socket }: Props) {
return ( return (
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}> <Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
<Grid container spacing={3}> <Grid container spacing={3}>
{/* Chart */}
<Grid item xs={12} md={12} lg={12}> <Grid item xs={12} md={12} lg={12}>
<Paper <Paper
sx={{ sx={{
@@ -99,8 +107,8 @@ export default function Settings({ socket }: Props) {
fullWidth fullWidth
label={settings.i18n.t('serverAddressTitle')} label={settings.i18n.t('serverAddressTitle')}
defaultValue={settings.serverAddr} defaultValue={settings.serverAddr}
onChange={handleAddrChange}
error={invalidIP} error={invalidIP}
onChange={handleAddrChange}
InputProps={{ InputProps={{
startAdornment: <InputAdornment position="start">ws://</InputAdornment>, startAdornment: <InputAdornment position="start">ws://</InputAdornment>,
}} }}
@@ -165,6 +173,7 @@ export default function Settings({ socket }: Props) {
<Switch <Switch
defaultChecked={settings.cliArgs.extractAudio} defaultChecked={settings.cliArgs.extractAudio}
onChange={() => dispatch(setCliArgs(settings.cliArgs.toggleExtractAudio()))} onChange={() => dispatch(setCliArgs(settings.cliArgs.toggleExtractAudio()))}
disabled={settings.formatSelection}
/> />
} }
label={settings.i18n.t('extractAudioCheckbox')} label={settings.i18n.t('extractAudioCheckbox')}

View File

@@ -16,6 +16,11 @@ export class CliArguments {
return this; return this;
} }
public disableExtractAudio() {
this._extractAudio = false;
return this;
}
public get noMTime(): boolean { public get noMTime(): boolean {
return this._noMTime; return this._noMTime;
} }

View File

@@ -15,10 +15,10 @@ export interface SettingsState {
} }
const initialState: SettingsState = { const initialState: SettingsState = {
serverAddr: localStorage.getItem("server-addr") || "localhost", serverAddr: localStorage.getItem("server-addr") || window.location.hostname,
language: (localStorage.getItem("language") || "english") as LanguageUnion, language: (localStorage.getItem("language") || "english") as LanguageUnion,
theme: (localStorage.getItem("theme") || "light") as ThemeUnion, theme: (localStorage.getItem("theme") || "light") as ThemeUnion,
cliArgs: localStorage.getItem("cli-args") ? new CliArguments().fromString(localStorage.getItem("cli-args")) : new CliArguments(false, true), cliArgs: localStorage.getItem("cli-args") ? new CliArguments().fromString(localStorage.getItem("cli-args") ?? "") : new CliArguments(false, true),
i18n: new I18nBuilder((localStorage.getItem("language") || "english")), i18n: new I18nBuilder((localStorage.getItem("language") || "english")),
formatSelection: localStorage.getItem("format-selection") === "true", formatSelection: localStorage.getItem("format-selection") === "true",
} }

View File

@@ -24,6 +24,22 @@ export function validateDomain(domainName: string): boolean {
return domainRegex.test(domainName) || domainName === 'localhost' return domainRegex.test(domainName) || domainName === 'localhost'
} }
/**
* Validate a domain via regex.
* Exapmples
* - http://example.com
* - https://example.com
* - http://www.example.com
* - https://www.example.com
* - http://10.0.0.1/[something]/[something-else]
* @param url
* @returns url validity test
*/
export function isValidURL(url: string): boolean {
let urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/
return urlRegex.test(url)
}
export function ellipsis(str: string, lim: number): string { export function ellipsis(str: string, lim: number): string {
if (str) { if (str) {
return str.length > lim ? `${str.substring(0, lim)}...` : str return str.length > lim ? `${str.substring(0, lim)}...` : str
@@ -37,7 +53,7 @@ export function ellipsis(str: string, lim: number): string {
* @returns download speed in KiB/s * @returns download speed in KiB/s
*/ */
export function detectSpeed(str: string): number { export function detectSpeed(str: string): number {
let effective = str.match(/[\d,]+(\.\d+)?/)[0] let effective = str.match(/[\d,]+(\.\d+)?/)![0]
const unit = str.replace(effective, '') const unit = str.replace(effective, '')
switch (unit) { switch (unit) {
case 'MiB/s': case 'MiB/s':
@@ -57,7 +73,7 @@ export function detectSpeed(str: string): number {
* @param callback calls React's StateAction function with the newly created Map * @param callback calls React's StateAction function with the newly created Map
* @param remove -optional- is it an update or a deletion operation? * @param remove -optional- is it an update or a deletion operation?
*/ */
export const updateInStateMap = (k: number, v: any, target: Map<number, any>, callback: Function, remove: boolean = false) => { export function updateInStateMap<K, V>(k: K, v: any, target: Map<K, V>, callback: Function, remove: boolean = false) {
if (remove) { if (remove) {
const _target = target const _target = target
_target.delete(k) _target.delete(k)

View File

@@ -110,7 +110,7 @@ class Process {
*/ */
async kill() { async kill() {
spawn('kill', [String(this.pid)]).on('exit', () => { spawn('kill', [String(this.pid)]).on('exit', () => {
log.info('db', `Deleted ${this.pid} because SIGKILL`) log.info('proc', `Stopped ${this.pid} because SIGKILL`)
}); });
} }