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,
Container,
Grid,
IconButton,
InputAdornment,
Paper,
Snackbar,
styled,
TextField,
Typography,
} from "@mui/material";
@@ -17,7 +20,9 @@ import { StackableResult } from "./components/StackableResult";
import { connected, downloading, finished } from "./features/status/statusSlice";
import { IDLInfo, IDLInfoBase, IDownloadInfo, IMessage } from "./interfaces";
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 = {
socket: Socket
@@ -74,7 +79,7 @@ export default function Home({ socket }: Props) {
socket.on('info', (data: IDLInfo) => {
setShowBackdrop(false)
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) => {
if (data.status === 'Done!' || data.status === 'Aborted') {
setShowBackdrop(false)
updateInStateMap(data.pid, 'Done!', messageMap, setMessageMap);
updateInStateMap(data.pid, 0, progressMap, setProgressMap);
updateInStateMap<number, IMessage>(data.pid, 'Done!', messageMap, setMessageMap);
updateInStateMap<number, number>(data.pid, 0, progressMap, setProgressMap);
socket.emit('disk-space')
dispatch(finished())
return;
}
updateInStateMap(data.pid, data, messageMap, setMessageMap);
updateInStateMap<number, IMessage>(data.pid, data, messageMap, setMessageMap);
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
*/
const sendUrl = () => {
const sendUrl = (immediate?: string) => {
const codes = new Array<string>();
if (pickedVideoFormat !== '') codes.push(pickedVideoFormat);
if (pickedAudioFormat !== '') codes.push(pickedAudioFormat);
if (pickedBestFormat !== '') codes.push(pickedBestFormat);
socket.emit('send-url', {
url: url || workingUrl,
url: immediate || url || workingUrl,
params: settings.cliArgs.toString() + toFormatArgs(codes),
})
setUrl('')
@@ -163,6 +172,27 @@ export default function Home({ socket }: Props) {
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 (
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
<Backdrop
@@ -185,7 +215,19 @@ export default function Home({ socket }: Props) {
label={settings.i18n.t('urlInput')}
variant="outlined"
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 item>

View File

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

View File

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

View File

@@ -15,10 +15,10 @@ export interface 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,
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")),
formatSelection: localStorage.getItem("format-selection") === "true",
}

View File

@@ -24,6 +24,22 @@ export function validateDomain(domainName: string): boolean {
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 {
if (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
*/
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, '')
switch (unit) {
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 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) {
const _target = target
_target.delete(k)

View File

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