download working
This commit is contained in:
@@ -39,7 +39,7 @@ function AppContent() {
|
|||||||
const settings = useSelector((state: RootState) => state.settings)
|
const settings = useSelector((state: RootState) => state.settings)
|
||||||
const status = useSelector((state: RootState) => state.status)
|
const status = useSelector((state: RootState) => state.status)
|
||||||
|
|
||||||
const socket = useMemo(() => io(getWebSocketEndpoint()), [])
|
const socket = useMemo(() => new WebSocket(getWebSocketEndpoint()), [])
|
||||||
|
|
||||||
const mode = settings.theme
|
const mode = settings.theme
|
||||||
|
|
||||||
@@ -60,9 +60,7 @@ function AppContent() {
|
|||||||
|
|
||||||
/* Get disk free space */
|
/* Get disk free space */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on('free-space', (res: string) => {
|
|
||||||
setFreeDiskSpace(res)
|
|
||||||
})
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,492 +1,455 @@
|
|||||||
import { FileUpload } from "@mui/icons-material";
|
import { FileUpload } from "@mui/icons-material";
|
||||||
import {
|
import {
|
||||||
Backdrop,
|
Backdrop,
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Container,
|
Container,
|
||||||
FormControl,
|
FormControl,
|
||||||
Grid,
|
Grid,
|
||||||
IconButton,
|
IconButton,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Paper,
|
Paper,
|
||||||
Select,
|
Select,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
styled,
|
styled,
|
||||||
TextField,
|
TextField,
|
||||||
Typography
|
Typography
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
import { Fragment, useEffect, useMemo, useState } from "react";
|
import { Fragment, useEffect, useMemo, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { Socket } from "socket.io-client";
|
|
||||||
import { CliArguments } from "./classes";
|
import { CliArguments } from "./classes";
|
||||||
import { StackableResult } from "./components/StackableResult";
|
import { StackableResult } from "./components/StackableResult";
|
||||||
import { serverStates } from "./events";
|
import { serverStates } from "./events";
|
||||||
import { connected, downloading, finished } from "./features/status/statusSlice";
|
import { connected } from "./features/status/statusSlice";
|
||||||
import { I18nBuilder } from "./i18n";
|
import { I18nBuilder } from "./i18n";
|
||||||
import { IDLMetadata, IDLMetadataAndPID, IMessage } from "./interfaces";
|
import { IDLMetadata, IMessage } from "./interfaces";
|
||||||
|
import { RPCClient } from "./rpcClient";
|
||||||
import { RootState } from "./stores/store";
|
import { RootState } from "./stores/store";
|
||||||
|
import { RPCResult } from "./types";
|
||||||
import { isValidURL, toFormatArgs, updateInStateMap } from "./utils";
|
import { isValidURL, toFormatArgs, updateInStateMap } from "./utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
socket: Socket
|
socket: WebSocket
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({ socket }: Props) {
|
export default function Home({ socket }: Props) {
|
||||||
// redux state
|
// redux state
|
||||||
const settings = useSelector((state: RootState) => state.settings)
|
const settings = useSelector((state: RootState) => state.settings)
|
||||||
const status = useSelector((state: RootState) => state.status)
|
const status = useSelector((state: RootState) => state.status)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
// ephemeral state
|
// ephemeral state
|
||||||
const [progressMap, setProgressMap] = useState(new Map<number, number>());
|
const [progressMap, setProgressMap] = useState(new Map<number, number>());
|
||||||
const [messageMap, setMessageMap] = useState(new Map<number, IMessage>());
|
const [messageMap, setMessageMap] = useState(new Map<number, IMessage>());
|
||||||
const [downloadInfoMap, setDownloadInfoMap] = useState(new Map<number, IDLMetadata>());
|
|
||||||
const [downloadFormats, setDownloadFormats] = useState<IDLMetadata>();
|
|
||||||
const [pickedVideoFormat, setPickedVideoFormat] = useState('');
|
|
||||||
const [pickedAudioFormat, setPickedAudioFormat] = useState('');
|
|
||||||
const [pickedBestFormat, setPickedBestFormat] = useState('');
|
|
||||||
|
|
||||||
const [downloadPath, setDownloadPath] = useState(0);
|
const [activeDownloads, setActiveDownloads] = useState(new Array<RPCResult>());
|
||||||
const [availableDownloadPaths, setAvailableDownloadPaths] = useState<string[]>([]);
|
const [downloadInfoMap, setDownloadInfoMap] = useState(new Map<number, IDLMetadata>());
|
||||||
|
const [downloadFormats, setDownloadFormats] = useState<IDLMetadata>();
|
||||||
|
const [pickedVideoFormat, setPickedVideoFormat] = useState('');
|
||||||
|
const [pickedAudioFormat, setPickedAudioFormat] = useState('');
|
||||||
|
const [pickedBestFormat, setPickedBestFormat] = useState('');
|
||||||
|
|
||||||
const [fileNameOverride, setFilenameOverride] = useState('');
|
const [downloadPath, setDownloadPath] = useState(0);
|
||||||
|
const [availableDownloadPaths, setAvailableDownloadPaths] = useState<string[]>([]);
|
||||||
|
|
||||||
const [url, setUrl] = useState('');
|
const [fileNameOverride, setFilenameOverride] = useState('');
|
||||||
const [workingUrl, setWorkingUrl] = useState('');
|
|
||||||
const [showBackdrop, setShowBackdrop] = useState(false);
|
|
||||||
const [showToast, setShowToast] = useState(true);
|
|
||||||
|
|
||||||
// memos
|
const [url, setUrl] = useState('');
|
||||||
const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language])
|
const [workingUrl, setWorkingUrl] = useState('');
|
||||||
const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [settings.cliArgs])
|
const [showBackdrop, setShowBackdrop] = useState(false);
|
||||||
|
const [showToast, setShowToast] = useState(true);
|
||||||
|
|
||||||
/* -------------------- Effects -------------------- */
|
// memos
|
||||||
/* WebSocket connect event handler*/
|
const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language])
|
||||||
useEffect(() => {
|
const client = useMemo(() => new RPCClient(socket), [settings.serverAddr, settings.serverPort])
|
||||||
socket.on('connect', () => {
|
const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [settings.cliArgs])
|
||||||
dispatch(connected())
|
|
||||||
socket.emit('fetch-jobs')
|
|
||||||
socket.emit('disk-space')
|
|
||||||
socket.emit('retrieve-jobs')
|
|
||||||
});
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
/* Ask server for pending jobs / background jobs */
|
/* -------------------- Effects -------------------- */
|
||||||
useEffect(() => {
|
/* WebSocket connect event handler*/
|
||||||
socket.on('pending-jobs', (count: number) => {
|
useEffect(() => {
|
||||||
count === 0 ? setShowBackdrop(false) : setShowBackdrop(true)
|
socket.onopen = () => {
|
||||||
})
|
dispatch(connected())
|
||||||
}, [])
|
console.log('oke')
|
||||||
|
socket.send('fetch-jobs')
|
||||||
/* Handle download information sent by server */
|
socket.send('disk-space')
|
||||||
useEffect(() => {
|
socket.send('retrieve-jobs')
|
||||||
socket.on('available-formats', (data: IDLMetadata) => {
|
|
||||||
setShowBackdrop(false)
|
|
||||||
setDownloadFormats(data);
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
/* Handle download information sent by server */
|
|
||||||
useEffect(() => {
|
|
||||||
socket.on('metadata', (data: IDLMetadataAndPID) => {
|
|
||||||
setShowBackdrop(false)
|
|
||||||
dispatch(downloading())
|
|
||||||
updateInStateMap<number, IDLMetadata>(data.pid, data.metadata, downloadInfoMap, setDownloadInfoMap);
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
/* Handle per-download progress */
|
|
||||||
useEffect(() => {
|
|
||||||
socket.on('progress', (data: IMessage) => {
|
|
||||||
if (data.status === serverStates.PROG_DONE || data.status === serverStates.PROC_ABORT) {
|
|
||||||
setShowBackdrop(false)
|
|
||||||
updateInStateMap<number, IMessage>(data.pid, serverStates.PROG_DONE, messageMap, setMessageMap);
|
|
||||||
updateInStateMap<number, number>(data.pid, 0, progressMap, setProgressMap);
|
|
||||||
socket.emit('disk-space')
|
|
||||||
dispatch(finished())
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateInStateMap<number, IMessage>(data.pid, data, messageMap, setMessageMap);
|
|
||||||
if (data.progress) {
|
|
||||||
updateInStateMap<number, number>(data.pid,
|
|
||||||
Math.ceil(Number(data.progress.replace('%', ''))),
|
|
||||||
progressMap,
|
|
||||||
setProgressMap
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch(`${window.location.protocol}//${settings.serverAddr}:${settings.serverPort}/tree`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
setAvailableDownloadPaths(data.flat)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
/* -------------------- component functions -------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrive url from input, cli-arguments from checkboxes and emits via WebSocket
|
|
||||||
*/
|
|
||||||
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: immediate || url || workingUrl,
|
|
||||||
path: availableDownloadPaths[downloadPath],
|
|
||||||
params: cliArgs.toString() + toFormatArgs(codes),
|
|
||||||
renameTo: fileNameOverride,
|
|
||||||
})
|
|
||||||
|
|
||||||
setUrl('')
|
|
||||||
setWorkingUrl('')
|
|
||||||
setFilenameOverride('')
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
resetInput()
|
|
||||||
setShowBackdrop(true)
|
|
||||||
setDownloadFormats(undefined)
|
|
||||||
}, 250);
|
|
||||||
}
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
/**
|
useEffect(() => {
|
||||||
* Retrive url from input and display the formats selection view
|
const interval = setInterval(() => client.running(), 1000)
|
||||||
*/
|
return () => clearInterval(interval)
|
||||||
const sendUrlFormatSelection = () => {
|
}, [])
|
||||||
socket.emit('send-url-format-selection', {
|
|
||||||
url: url,
|
|
||||||
})
|
|
||||||
|
|
||||||
setWorkingUrl(url)
|
useEffect(() => {
|
||||||
setUrl('')
|
socket.onmessage = (event) => {
|
||||||
setPickedAudioFormat('')
|
const res = client.decode(event.data)
|
||||||
setPickedVideoFormat('')
|
if (showBackdrop) {
|
||||||
setPickedBestFormat('')
|
setShowBackdrop(false)
|
||||||
|
}
|
||||||
setTimeout(() => {
|
switch (typeof res.result) {
|
||||||
resetInput()
|
case 'object':
|
||||||
setShowBackdrop(true)
|
setActiveDownloads(
|
||||||
}, 250)
|
res.result
|
||||||
|
.filter((r: RPCResult) => !!r.info.url)
|
||||||
|
.sort((a: RPCResult, b: RPCResult) => a.info.title.localeCompare(b.info.title))
|
||||||
|
)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
/**
|
useEffect(() => {
|
||||||
* Update the url state whenever the input value changes
|
fetch(`${window.location.protocol}//${settings.serverAddr}:${settings.serverPort}/tree`)
|
||||||
* @param e Input change event
|
.then(res => res.json())
|
||||||
*/
|
.then(data => {
|
||||||
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
setAvailableDownloadPaths(data.flat)
|
||||||
setUrl(e.target.value)
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/* -------------------- component functions -------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrive url from input, cli-arguments from checkboxes and emits via WebSocket
|
||||||
|
*/
|
||||||
|
const sendUrl = (immediate?: string) => {
|
||||||
|
const codes = new Array<string>();
|
||||||
|
if (pickedVideoFormat !== '') codes.push(pickedVideoFormat);
|
||||||
|
if (pickedAudioFormat !== '') codes.push(pickedAudioFormat);
|
||||||
|
if (pickedBestFormat !== '') codes.push(pickedBestFormat);
|
||||||
|
|
||||||
|
client.download(
|
||||||
|
immediate || url || workingUrl,
|
||||||
|
cliArgs.toString() + toFormatArgs(codes),
|
||||||
|
)
|
||||||
|
|
||||||
|
setUrl('')
|
||||||
|
setWorkingUrl('')
|
||||||
|
setFilenameOverride('')
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
resetInput()
|
||||||
|
setShowBackdrop(true)
|
||||||
|
setDownloadFormats(undefined)
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrive url from input and display the formats selection view
|
||||||
|
*/
|
||||||
|
const sendUrlFormatSelection = () => {
|
||||||
|
setWorkingUrl(url)
|
||||||
|
setUrl('')
|
||||||
|
setPickedAudioFormat('')
|
||||||
|
setPickedVideoFormat('')
|
||||||
|
setPickedBestFormat('')
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
resetInput()
|
||||||
|
setShowBackdrop(true)
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the url state whenever the input value changes
|
||||||
|
* @param e Input change event
|
||||||
|
*/
|
||||||
|
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setUrl(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the filename override state whenever the input value changes
|
||||||
|
* @param e Input change event
|
||||||
|
*/
|
||||||
|
const handleFilenameOverrideChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFilenameOverride(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abort a specific download if id's provided, other wise abort all running ones.
|
||||||
|
* @param id The download id / pid
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
const abort = (id?: string) => {
|
||||||
|
if (id) {
|
||||||
|
client.kill(id)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
client.killAll()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
const parseUrlListFile = (event: any) => {
|
||||||
* Update the filename override state whenever the input value changes
|
const urlList = event.target.files
|
||||||
* @param e Input change event
|
const reader = new FileReader()
|
||||||
*/
|
reader.addEventListener('load', $event => {
|
||||||
const handleFilenameOverrideChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const base64 = $event.target?.result!.toString().split(',')[1]
|
||||||
setFilenameOverride(e.target.value)
|
Buffer.from(base64!, 'base64')
|
||||||
|
.toString()
|
||||||
|
.trimEnd()
|
||||||
|
.split('\n')
|
||||||
|
.filter(_url => isValidURL(_url))
|
||||||
|
.forEach(_url => sendUrl(_url))
|
||||||
|
})
|
||||||
|
reader.readAsDataURL(urlList[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetInput = () => {
|
||||||
|
const input = document.getElementById('urlInput') as HTMLInputElement;
|
||||||
|
input.value = '';
|
||||||
|
|
||||||
|
const filename = document.getElementById('customFilenameInput') as HTMLInputElement;
|
||||||
|
if (filename) {
|
||||||
|
filename.value = '';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/* -------------------- styled components -------------------- */
|
||||||
* Abort a specific download if id's provided, other wise abort all running ones.
|
|
||||||
* @param id The download id / pid
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
const abort = (id?: number) => {
|
|
||||||
if (id) {
|
|
||||||
updateInStateMap(id, null, downloadInfoMap, setDownloadInfoMap, true)
|
|
||||||
socket.emit('abort', { pid: id })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setDownloadFormats(undefined)
|
|
||||||
socket.emit('abort-all')
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseUrlListFile = (event: any) => {
|
const Input = styled('input')({
|
||||||
const urlList = event.target.files
|
display: 'none',
|
||||||
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])
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetInput = () => {
|
return (
|
||||||
const input = document.getElementById('urlInput') as HTMLInputElement;
|
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
||||||
input.value = '';
|
<Backdrop
|
||||||
|
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||||
const filename = document.getElementById('customFilenameInput') as HTMLInputElement;
|
open={showBackdrop}
|
||||||
if (filename) {
|
>
|
||||||
filename.value = '';
|
<CircularProgress color="primary" />
|
||||||
}
|
</Backdrop>
|
||||||
}
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12}>
|
||||||
/* -------------------- styled components -------------------- */
|
<Paper
|
||||||
|
sx={{
|
||||||
const Input = styled('input')({
|
p: 2,
|
||||||
display: 'none',
|
display: 'flex',
|
||||||
});
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
return (
|
>
|
||||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
<Grid container>
|
||||||
<Backdrop
|
<TextField
|
||||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
fullWidth
|
||||||
open={showBackdrop}
|
id="urlInput"
|
||||||
>
|
label={i18n.t('urlInput')}
|
||||||
<CircularProgress color="primary" />
|
variant="outlined"
|
||||||
</Backdrop>
|
onChange={handleUrlChange}
|
||||||
<Grid container spacing={2}>
|
disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
|
||||||
<Grid item xs={12}>
|
InputProps={{
|
||||||
<Paper
|
endAdornment: (
|
||||||
sx={{
|
<InputAdornment position="end">
|
||||||
p: 2,
|
<label htmlFor="icon-button-file">
|
||||||
display: 'flex',
|
<Input id="icon-button-file" type="file" accept=".txt" onChange={parseUrlListFile} />
|
||||||
flexDirection: 'column',
|
<IconButton color="primary" aria-label="upload file" component="span">
|
||||||
}}
|
<FileUpload />
|
||||||
>
|
</IconButton>
|
||||||
<Grid container>
|
</label>
|
||||||
<TextField
|
</InputAdornment>
|
||||||
fullWidth
|
),
|
||||||
id="urlInput"
|
}}
|
||||||
label={i18n.t('urlInput')}
|
/>
|
||||||
variant="outlined"
|
|
||||||
onChange={handleUrlChange}
|
|
||||||
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>
|
|
||||||
<Grid container spacing={1} sx={{ mt: 1 }}>
|
|
||||||
{
|
|
||||||
settings.fileRenaming ?
|
|
||||||
<Grid item xs={8}>
|
|
||||||
<TextField
|
|
||||||
id="customFilenameInput"
|
|
||||||
fullWidth
|
|
||||||
label={i18n.t('customFilename')}
|
|
||||||
variant="outlined"
|
|
||||||
onChange={handleFilenameOverrideChange}
|
|
||||||
disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
|
|
||||||
/>
|
|
||||||
</Grid> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
settings.pathOverriding ?
|
|
||||||
<Grid item xs={4}>
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel>{i18n.t('customPath')}</InputLabel>
|
|
||||||
<Select
|
|
||||||
label={i18n.t('customPath')}
|
|
||||||
defaultValue={0}
|
|
||||||
variant={'outlined'}
|
|
||||||
value={downloadPath}
|
|
||||||
onChange={(e) => setDownloadPath(Number(e.target.value))}
|
|
||||||
>
|
|
||||||
{availableDownloadPaths.map((val: string, idx: number) => (
|
|
||||||
<MenuItem key={idx} value={idx}>{val}</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</Grid> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</Grid>
|
|
||||||
<Grid container spacing={1} pt={2}>
|
|
||||||
<Grid item>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
disabled={url === ''}
|
|
||||||
onClick={() => settings.formatSelection ? sendUrlFormatSelection() : sendUrl()}
|
|
||||||
>
|
|
||||||
{i18n.t('startButton')}
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={() => abort()}
|
|
||||||
>
|
|
||||||
{i18n.t('abortAllButton')}
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
</Grid >
|
|
||||||
{/* Format Selection grid */}
|
|
||||||
{
|
|
||||||
downloadFormats ? <Grid container spacing={2} mt={2}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Paper
|
|
||||||
sx={{
|
|
||||||
p: 2,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Grid container>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Typography variant="h6" component="div" pb={1}>
|
|
||||||
{downloadFormats.title}
|
|
||||||
</Typography>
|
|
||||||
{/* <Skeleton variant="rectangular" height={180} /> */}
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} pb={1}>
|
|
||||||
<img src={downloadFormats.thumbnail} height={260} width="100%" style={{ objectFit: 'cover' }} />
|
|
||||||
</Grid>
|
|
||||||
{/* video only */}
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Typography variant="body1" component="div">
|
|
||||||
Best quality
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item pr={2} py={1}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
disabled={pickedBestFormat !== ''}
|
|
||||||
onClick={() => {
|
|
||||||
setPickedBestFormat(downloadFormats.best.format_id)
|
|
||||||
setPickedVideoFormat('')
|
|
||||||
setPickedAudioFormat('')
|
|
||||||
}}>
|
|
||||||
{downloadFormats.best.format_note || downloadFormats.best.format_id} - {downloadFormats.best.vcodec}+{downloadFormats.best.acodec}
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
{/* video only */}
|
|
||||||
{downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ?
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Typography variant="body1" component="div">
|
|
||||||
Video data {downloadFormats.formats[1].acodec}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
{downloadFormats.formats
|
|
||||||
.filter(format => format.acodec === 'none' && format.vcodec !== 'none')
|
|
||||||
.map((format, idx) => (
|
|
||||||
<Grid item pr={2} py={1} key={idx}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={() => {
|
|
||||||
setPickedVideoFormat(format.format_id)
|
|
||||||
setPickedBestFormat('')
|
|
||||||
}}
|
|
||||||
disabled={pickedVideoFormat === format.format_id}
|
|
||||||
>
|
|
||||||
{format.format_note} - {format.vcodec === 'none' ? format.acodec : format.vcodec}
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
{downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ?
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Typography variant="body1" component="div">
|
|
||||||
Audio data
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
{downloadFormats.formats
|
|
||||||
.filter(format => format.acodec !== 'none' && format.vcodec === 'none')
|
|
||||||
.map((format, idx) => (
|
|
||||||
<Grid item pr={2} py={1} key={idx}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={() => {
|
|
||||||
setPickedAudioFormat(format.format_id)
|
|
||||||
setPickedBestFormat('')
|
|
||||||
}}
|
|
||||||
disabled={pickedAudioFormat === format.format_id}
|
|
||||||
>
|
|
||||||
{format.format_note} - {format.vcodec === 'none' ? format.acodec : format.vcodec}
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
<Grid item xs={12} pt={2}>
|
|
||||||
<ButtonGroup disableElevation variant="contained">
|
|
||||||
<Button
|
|
||||||
onClick={() => sendUrl()}
|
|
||||||
disabled={!pickedBestFormat && !(pickedAudioFormat || pickedVideoFormat)}
|
|
||||||
> Download
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setPickedAudioFormat('');
|
|
||||||
setPickedVideoFormat('');
|
|
||||||
setPickedBestFormat('');
|
|
||||||
}}
|
|
||||||
> Clear
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
</Grid> : null
|
|
||||||
}
|
|
||||||
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
|
|
||||||
{
|
|
||||||
Array
|
|
||||||
.from<any>(messageMap)
|
|
||||||
.filter(flattened => [...flattened][0])
|
|
||||||
.filter(flattened => [...flattened][1].toString() !== serverStates.PROG_DONE)
|
|
||||||
.flatMap(message => (
|
|
||||||
<Grid item xs={4} sm={8} md={6} key={message[0]}>
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
Message[0] => key, the pid which is shared with the progress and download Maps
|
|
||||||
Message[1] => value, the actual formatted message sent from server
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
<Fragment>
|
|
||||||
<StackableResult
|
|
||||||
formattedLog={message[1]}
|
|
||||||
title={downloadInfoMap.get(message[0])?.title ?? ''}
|
|
||||||
thumbnail={downloadInfoMap.get(message[0])?.thumbnail ?? ''}
|
|
||||||
progress={progressMap.get(message[0]) ?? 0}
|
|
||||||
stopCallback={() => abort(message[0])}
|
|
||||||
resolution={
|
|
||||||
settings.formatSelection
|
|
||||||
? ''
|
|
||||||
: downloadInfoMap.get(message[0])?.best.resolution ?? ''
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
</Grid>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Snackbar
|
<Grid container spacing={1} sx={{ mt: 1 }}>
|
||||||
open={showToast === status.connected}
|
{
|
||||||
autoHideDuration={1500}
|
settings.fileRenaming ?
|
||||||
message="Connected"
|
<Grid item xs={8}>
|
||||||
onClose={() => setShowToast(false)}
|
<TextField
|
||||||
/>
|
id="customFilenameInput"
|
||||||
</Container >
|
fullWidth
|
||||||
);
|
label={i18n.t('customFilename')}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={handleFilenameOverrideChange}
|
||||||
|
disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
|
||||||
|
/>
|
||||||
|
</Grid> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
settings.pathOverriding ?
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>{i18n.t('customPath')}</InputLabel>
|
||||||
|
<Select
|
||||||
|
label={i18n.t('customPath')}
|
||||||
|
defaultValue={0}
|
||||||
|
variant={'outlined'}
|
||||||
|
value={downloadPath}
|
||||||
|
onChange={(e) => setDownloadPath(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
{availableDownloadPaths.map((val: string, idx: number) => (
|
||||||
|
<MenuItem key={idx} value={idx}>{val}</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</Grid>
|
||||||
|
<Grid container spacing={1} pt={2}>
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={url === ''}
|
||||||
|
onClick={() => settings.formatSelection ? sendUrlFormatSelection() : sendUrl()}
|
||||||
|
>
|
||||||
|
{i18n.t('startButton')}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => abort()}
|
||||||
|
>
|
||||||
|
{i18n.t('abortAllButton')}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
</Grid >
|
||||||
|
{/* Format Selection grid */}
|
||||||
|
{
|
||||||
|
downloadFormats ? <Grid container spacing={2} mt={2}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography variant="h6" component="div" pb={1}>
|
||||||
|
{downloadFormats.title}
|
||||||
|
</Typography>
|
||||||
|
{/* <Skeleton variant="rectangular" height={180} /> */}
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} pb={1}>
|
||||||
|
<img src={downloadFormats.thumbnail} height={260} width="100%" style={{ objectFit: 'cover' }} />
|
||||||
|
</Grid>
|
||||||
|
{/* video only */}
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography variant="body1" component="div">
|
||||||
|
Best quality
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item pr={2} py={1}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={pickedBestFormat !== ''}
|
||||||
|
onClick={() => {
|
||||||
|
setPickedBestFormat(downloadFormats.best.format_id)
|
||||||
|
setPickedVideoFormat('')
|
||||||
|
setPickedAudioFormat('')
|
||||||
|
}}>
|
||||||
|
{downloadFormats.best.format_note || downloadFormats.best.format_id} - {downloadFormats.best.vcodec}+{downloadFormats.best.acodec}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
{/* video only */}
|
||||||
|
{downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ?
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography variant="body1" component="div">
|
||||||
|
Video data {downloadFormats.formats[1].acodec}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
{downloadFormats.formats
|
||||||
|
.filter(format => format.acodec === 'none' && format.vcodec !== 'none')
|
||||||
|
.map((format, idx) => (
|
||||||
|
<Grid item pr={2} py={1} key={idx}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
setPickedVideoFormat(format.format_id)
|
||||||
|
setPickedBestFormat('')
|
||||||
|
}}
|
||||||
|
disabled={pickedVideoFormat === format.format_id}
|
||||||
|
>
|
||||||
|
{format.format_note} - {format.vcodec === 'none' ? format.acodec : format.vcodec}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
{downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ?
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography variant="body1" component="div">
|
||||||
|
Audio data
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
{downloadFormats.formats
|
||||||
|
.filter(format => format.acodec !== 'none' && format.vcodec === 'none')
|
||||||
|
.map((format, idx) => (
|
||||||
|
<Grid item pr={2} py={1} key={idx}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
setPickedAudioFormat(format.format_id)
|
||||||
|
setPickedBestFormat('')
|
||||||
|
}}
|
||||||
|
disabled={pickedAudioFormat === format.format_id}
|
||||||
|
>
|
||||||
|
{format.format_note} - {format.vcodec === 'none' ? format.acodec : format.vcodec}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<Grid item xs={12} pt={2}>
|
||||||
|
<ButtonGroup disableElevation variant="contained">
|
||||||
|
<Button
|
||||||
|
onClick={() => sendUrl()}
|
||||||
|
disabled={!pickedBestFormat && !(pickedAudioFormat || pickedVideoFormat)}
|
||||||
|
> Download
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setPickedAudioFormat('');
|
||||||
|
setPickedVideoFormat('');
|
||||||
|
setPickedBestFormat('');
|
||||||
|
}}
|
||||||
|
> Clear
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
</Grid> : null
|
||||||
|
}
|
||||||
|
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
|
||||||
|
{
|
||||||
|
activeDownloads.map(download => (
|
||||||
|
<Grid item xs={4} sm={8} md={6} key={download.id}>
|
||||||
|
<Fragment>
|
||||||
|
<StackableResult
|
||||||
|
title={download.info.title}
|
||||||
|
thumbnail={download.info.thumbnail}
|
||||||
|
percentage={download.progress.percentage}
|
||||||
|
stopCallback={() => abort(download.id)}
|
||||||
|
resolution={download.info.resolution ?? ''}
|
||||||
|
speed={download.progress.speed}
|
||||||
|
size={download.info.filesize_approx ?? 0}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
</Grid>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Grid>
|
||||||
|
<Snackbar
|
||||||
|
open={showToast === status.connected}
|
||||||
|
autoHideDuration={1500}
|
||||||
|
message="Connected"
|
||||||
|
onClose={() => setShowToast(false)}
|
||||||
|
/>
|
||||||
|
</Container >
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ import { RootState } from "./stores/store";
|
|||||||
import { validateDomain, validateIP } from "./utils";
|
import { validateDomain, validateIP } from "./utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
socket: Socket
|
socket: WebSocket
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Settings({ socket }: Props) {
|
export default function Settings({ socket }: Props) {
|
||||||
@@ -112,8 +112,7 @@ export default function Settings({ socket }: Props) {
|
|||||||
* Send via WebSocket a message in order to update the yt-dlp binary from server
|
* Send via WebSocket a message in order to update the yt-dlp binary from server
|
||||||
*/
|
*/
|
||||||
const updateBinary = () => {
|
const updateBinary = () => {
|
||||||
socket.emit('update-bin')
|
|
||||||
dispatch(alreadyUpdated())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,15 +4,24 @@ import { IMessage } from "../interfaces";
|
|||||||
import { ellipsis } from "../utils";
|
import { ellipsis } from "../utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
formattedLog: IMessage,
|
|
||||||
title: string,
|
title: string,
|
||||||
thumbnail: string,
|
thumbnail: string,
|
||||||
resolution: string
|
resolution: string
|
||||||
progress: number,
|
percentage: string,
|
||||||
|
size: number,
|
||||||
|
speed: number,
|
||||||
stopCallback: VoidFunction,
|
stopCallback: VoidFunction,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StackableResult({ formattedLog, title, thumbnail, resolution, progress, stopCallback }: Props) {
|
export function StackableResult({
|
||||||
|
title,
|
||||||
|
thumbnail,
|
||||||
|
resolution,
|
||||||
|
percentage,
|
||||||
|
speed,
|
||||||
|
size,
|
||||||
|
stopCallback
|
||||||
|
}: Props) {
|
||||||
const guessResolution = (xByY: string): any => {
|
const guessResolution = (xByY: string): any => {
|
||||||
if (!xByY) return null;
|
if (!xByY) return null;
|
||||||
if (xByY.includes('4320')) return (<EightK color="primary" />);
|
if (xByY.includes('4320')) return (<EightK color="primary" />);
|
||||||
@@ -22,6 +31,8 @@ export function StackableResult({ formattedLog, title, thumbnail, resolution, pr
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const percentageToNumber = () => Number(percentage.replace('%', ''))
|
||||||
|
|
||||||
const roundMB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)}MiB`
|
const roundMB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)}MiB`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,14 +54,14 @@ export function StackableResult({ formattedLog, title, thumbnail, resolution, pr
|
|||||||
<Skeleton />
|
<Skeleton />
|
||||||
}
|
}
|
||||||
<Stack direction="row" spacing={1} py={2}>
|
<Stack direction="row" spacing={1} py={2}>
|
||||||
<Chip label={formattedLog.status} color="primary" />
|
<Chip label={'Downloading'} color="primary" />
|
||||||
<Typography>{formattedLog.progress}</Typography>
|
<Typography>{percentage}</Typography>
|
||||||
<Typography>{formattedLog.dlSpeed}</Typography>
|
<Typography>{speed}</Typography>
|
||||||
<Typography>{roundMB(formattedLog.size ?? 0)}</Typography>
|
<Typography>{roundMB(size ?? 0)}</Typography>
|
||||||
{guessResolution(resolution)}
|
{guessResolution(resolution)}
|
||||||
</Stack>
|
</Stack>
|
||||||
{progress ?
|
{percentage ?
|
||||||
<LinearProgress variant="determinate" value={progress} /> :
|
<LinearProgress variant="determinate" value={percentageToNumber()} /> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
81
frontend/src/rpcClient.ts
Normal file
81
frontend/src/rpcClient.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import type { RPCRequest, RPCResponse } from "./types"
|
||||||
|
import type { IDLMetadata } from './interfaces'
|
||||||
|
|
||||||
|
export class RPCClient {
|
||||||
|
private socket: WebSocket
|
||||||
|
private seq: number
|
||||||
|
|
||||||
|
constructor(socket: WebSocket) {
|
||||||
|
this.socket = socket
|
||||||
|
this.seq = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private incrementSeq() {
|
||||||
|
return String(this.seq++)
|
||||||
|
}
|
||||||
|
|
||||||
|
private send(req: RPCRequest) {
|
||||||
|
this.socket.send(JSON.stringify(req))
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendHTTP<T>(req: RPCRequest) {
|
||||||
|
return new Promise<RPCResponse<T>>((resolve, reject) => {
|
||||||
|
fetch('/rpc-http', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(req)
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => resolve(data))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public download(url: string, args: string) {
|
||||||
|
if (url) {
|
||||||
|
this.send({
|
||||||
|
id: this.incrementSeq(),
|
||||||
|
method: 'Service.Exec',
|
||||||
|
params: [{
|
||||||
|
URL: url.split("?list").at(0)!,
|
||||||
|
Params: args.split(" ").map(a => a.trim()),
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public formats(url: string) {
|
||||||
|
if (url) {
|
||||||
|
return this.sendHTTP<IDLMetadata>({
|
||||||
|
id: this.incrementSeq(),
|
||||||
|
method: 'Service.Formats',
|
||||||
|
params: [{
|
||||||
|
URL: url.split("?list").at(0)!,
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public running() {
|
||||||
|
this.send({
|
||||||
|
method: 'Service.Running',
|
||||||
|
params: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public kill(id: string) {
|
||||||
|
this.send({
|
||||||
|
method: 'Service.Kill',
|
||||||
|
params: [id],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public killAll() {
|
||||||
|
this.send({
|
||||||
|
method: 'Service.KillAll',
|
||||||
|
params: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public decode(data: any): RPCResponse<any> {
|
||||||
|
return JSON.parse(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
frontend/src/types.d.ts
vendored
Normal file
43
frontend/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
export type RPCMethods =
|
||||||
|
| "Service.Exec"
|
||||||
|
| "Service.Kill"
|
||||||
|
| "Service.Running"
|
||||||
|
| "Service.KillAll"
|
||||||
|
| "Service.FreeSpace"
|
||||||
|
| "Service.Formats"
|
||||||
|
|
||||||
|
export type RPCRequest = {
|
||||||
|
method: RPCMethods,
|
||||||
|
params?: any[],
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RPCResponse<T> = {
|
||||||
|
result: T,
|
||||||
|
error: number | null
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RPCResult = {
|
||||||
|
id: string
|
||||||
|
progress: {
|
||||||
|
speed: number
|
||||||
|
eta: number
|
||||||
|
percentage: string
|
||||||
|
}
|
||||||
|
info: {
|
||||||
|
url: string
|
||||||
|
filesize_approx?: number
|
||||||
|
resolution?: string
|
||||||
|
thumbnail: string
|
||||||
|
title: string
|
||||||
|
vcodec?: string
|
||||||
|
acodec?: string
|
||||||
|
ext?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RPCParams = {
|
||||||
|
URL: string
|
||||||
|
Params?: string
|
||||||
|
}
|
||||||
@@ -106,5 +106,9 @@ export function toFormatArgs(codes: string[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getWebSocketEndpoint() {
|
export function getWebSocketEndpoint() {
|
||||||
return `${window.location.protocol}//${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}`
|
return `ws://${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}/ws-rpc`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHttpRPCEndpoint() {
|
||||||
|
return `${window.location.protocol}//${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}/http-rpc`
|
||||||
}
|
}
|
||||||
@@ -33,20 +33,31 @@ func RunBlocking(ctx context.Context) {
|
|||||||
Root: http.FS(fe),
|
Root: http.FS(fe),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
app.Get("/ws", websocket.New(func(c *websocket.Conn) {
|
app.Get("/ws-rpc", websocket.New(func(c *websocket.Conn) {
|
||||||
for {
|
for {
|
||||||
mtype, reader, err := c.NextReader()
|
mtype, reader, err := c.NextReader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
res := NewRPCRequest(reader).Call()
|
||||||
|
|
||||||
writer, err := c.NextWriter(mtype)
|
writer, err := c.NextWriter(mtype)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
res := NewRPCRequest(reader).Call()
|
|
||||||
io.Copy(writer, res)
|
io.Copy(writer, res)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
app.Post("/http-rpc", func(c *fiber.Ctx) error {
|
||||||
|
reader := c.Context().RequestBodyStream()
|
||||||
|
writer := c.Response().BodyWriter()
|
||||||
|
res := NewRPCRequest(reader).Call()
|
||||||
|
io.Copy(writer, res)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Server().StreamRequestBody = true
|
||||||
|
|
||||||
log.Fatal(app.Listen(fmt.Sprintf(":%s", port)))
|
log.Fatal(app.Listen(fmt.Sprintf(":%s", port)))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user