download working

This commit is contained in:
2023-01-11 20:49:25 +01:00
parent b29cdf802d
commit 4d4582b3f7
8 changed files with 584 additions and 474 deletions

View File

@@ -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 (

View File

@@ -21,18 +21,19 @@ import {
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) {
@@ -44,6 +45,8 @@ export default function Home({ socket }: Props) {
// 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 [activeDownloads, setActiveDownloads] = useState(new Array<RPCResult>());
const [downloadInfoMap, setDownloadInfoMap] = useState(new Map<number, IDLMetadata>()); const [downloadInfoMap, setDownloadInfoMap] = useState(new Map<number, IDLMetadata>());
const [downloadFormats, setDownloadFormats] = useState<IDLMetadata>(); const [downloadFormats, setDownloadFormats] = useState<IDLMetadata>();
const [pickedVideoFormat, setPickedVideoFormat] = useState(''); const [pickedVideoFormat, setPickedVideoFormat] = useState('');
@@ -62,63 +65,44 @@ export default function Home({ socket }: Props) {
// memos // memos
const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language]) const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language])
const client = useMemo(() => new RPCClient(socket), [settings.serverAddr, settings.serverPort])
const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [settings.cliArgs]) const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [settings.cliArgs])
/* -------------------- Effects -------------------- */ /* -------------------- Effects -------------------- */
/* WebSocket connect event handler*/ /* WebSocket connect event handler*/
useEffect(() => { useEffect(() => {
socket.on('connect', () => { socket.onopen = () => {
dispatch(connected()) dispatch(connected())
socket.emit('fetch-jobs') console.log('oke')
socket.emit('disk-space') socket.send('fetch-jobs')
socket.emit('retrieve-jobs') socket.send('disk-space')
}); socket.send('retrieve-jobs')
}, []) }
}, [])
/* Ask server for pending jobs / background jobs */
useEffect(() => { useEffect(() => {
socket.on('pending-jobs', (count: number) => { const interval = setInterval(() => client.running(), 1000)
count === 0 ? setShowBackdrop(false) : setShowBackdrop(true) return () => clearInterval(interval)
}) }, [])
}, [])
useEffect(() => {
/* Handle download information sent by server */ socket.onmessage = (event) => {
useEffect(() => { const res = client.decode(event.data)
socket.on('available-formats', (data: IDLMetadata) => { if (showBackdrop) {
setShowBackdrop(false) setShowBackdrop(false)
setDownloadFormats(data); }
}) switch (typeof res.result) {
}, []) case 'object':
setActiveDownloads(
/* Handle download information sent by server */ res.result
useEffect(() => { .filter((r: RPCResult) => !!r.info.url)
socket.on('metadata', (data: IDLMetadataAndPID) => { .sort((a: RPCResult, b: RPCResult) => a.info.title.localeCompare(b.info.title))
setShowBackdrop(false) )
dispatch(downloading()) break
updateInStateMap<number, IDLMetadata>(data.pid, data.metadata, downloadInfoMap, setDownloadInfoMap); default:
}) break
}, [])
/* 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(() => { useEffect(() => {
@@ -140,12 +124,10 @@ export default function Home({ socket }: Props) {
if (pickedAudioFormat !== '') codes.push(pickedAudioFormat); if (pickedAudioFormat !== '') codes.push(pickedAudioFormat);
if (pickedBestFormat !== '') codes.push(pickedBestFormat); if (pickedBestFormat !== '') codes.push(pickedBestFormat);
socket.emit('send-url', { client.download(
url: immediate || url || workingUrl, immediate || url || workingUrl,
path: availableDownloadPaths[downloadPath], cliArgs.toString() + toFormatArgs(codes),
params: cliArgs.toString() + toFormatArgs(codes), )
renameTo: fileNameOverride,
})
setUrl('') setUrl('')
setWorkingUrl('') setWorkingUrl('')
@@ -162,10 +144,6 @@ export default function Home({ socket }: Props) {
* Retrive url from input and display the formats selection view * Retrive url from input and display the formats selection view
*/ */
const sendUrlFormatSelection = () => { const sendUrlFormatSelection = () => {
socket.emit('send-url-format-selection', {
url: url,
})
setWorkingUrl(url) setWorkingUrl(url)
setUrl('') setUrl('')
setPickedAudioFormat('') setPickedAudioFormat('')
@@ -199,14 +177,12 @@ export default function Home({ socket }: Props) {
* @param id The download id / pid * @param id The download id / pid
* @returns void * @returns void
*/ */
const abort = (id?: number) => { const abort = (id?: string) => {
if (id) { if (id) {
updateInStateMap(id, null, downloadInfoMap, setDownloadInfoMap, true) client.kill(id)
socket.emit('abort', { pid: id })
return return
} }
setDownloadFormats(undefined) client.killAll()
socket.emit('abort-all')
} }
const parseUrlListFile = (event: any) => { const parseUrlListFile = (event: any) => {
@@ -451,30 +427,17 @@ export default function Home({ socket }: Props) {
} }
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}> <Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
{ {
Array activeDownloads.map(download => (
.from<any>(messageMap) <Grid item xs={4} sm={8} md={6} key={download.id}>
.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> <Fragment>
<StackableResult <StackableResult
formattedLog={message[1]} title={download.info.title}
title={downloadInfoMap.get(message[0])?.title ?? ''} thumbnail={download.info.thumbnail}
thumbnail={downloadInfoMap.get(message[0])?.thumbnail ?? ''} percentage={download.progress.percentage}
progress={progressMap.get(message[0]) ?? 0} stopCallback={() => abort(download.id)}
stopCallback={() => abort(message[0])} resolution={download.info.resolution ?? ''}
resolution={ speed={download.progress.speed}
settings.formatSelection size={download.info.filesize_approx ?? 0}
? ''
: downloadInfoMap.get(message[0])?.best.resolution ?? ''
}
/> />
</Fragment> </Fragment>
</Grid> </Grid>

View File

@@ -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 (

View File

@@ -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
View 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
View 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
}

View File

@@ -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`
} }

View File

@@ -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)))
} }