download working
This commit is contained in:
@@ -39,7 +39,7 @@ function AppContent() {
|
||||
const settings = useSelector((state: RootState) => state.settings)
|
||||
const status = useSelector((state: RootState) => state.status)
|
||||
|
||||
const socket = useMemo(() => io(getWebSocketEndpoint()), [])
|
||||
const socket = useMemo(() => new WebSocket(getWebSocketEndpoint()), [])
|
||||
|
||||
const mode = settings.theme
|
||||
|
||||
@@ -60,9 +60,7 @@ function AppContent() {
|
||||
|
||||
/* Get disk free space */
|
||||
useEffect(() => {
|
||||
socket.on('free-space', (res: string) => {
|
||||
setFreeDiskSpace(res)
|
||||
})
|
||||
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
||||
@@ -21,18 +21,19 @@ import {
|
||||
import { Buffer } from 'buffer';
|
||||
import { Fragment, useEffect, useMemo, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Socket } from "socket.io-client";
|
||||
import { CliArguments } from "./classes";
|
||||
import { StackableResult } from "./components/StackableResult";
|
||||
import { serverStates } from "./events";
|
||||
import { connected, downloading, finished } from "./features/status/statusSlice";
|
||||
import { connected } from "./features/status/statusSlice";
|
||||
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 { RPCResult } from "./types";
|
||||
import { isValidURL, toFormatArgs, updateInStateMap } from "./utils";
|
||||
|
||||
type Props = {
|
||||
socket: Socket
|
||||
socket: WebSocket
|
||||
}
|
||||
|
||||
export default function Home({ socket }: Props) {
|
||||
@@ -44,6 +45,8 @@ export default function Home({ socket }: Props) {
|
||||
// ephemeral state
|
||||
const [progressMap, setProgressMap] = useState(new Map<number, number>());
|
||||
const [messageMap, setMessageMap] = useState(new Map<number, IMessage>());
|
||||
|
||||
const [activeDownloads, setActiveDownloads] = useState(new Array<RPCResult>());
|
||||
const [downloadInfoMap, setDownloadInfoMap] = useState(new Map<number, IDLMetadata>());
|
||||
const [downloadFormats, setDownloadFormats] = useState<IDLMetadata>();
|
||||
const [pickedVideoFormat, setPickedVideoFormat] = useState('');
|
||||
@@ -62,63 +65,44 @@ export default function Home({ socket }: Props) {
|
||||
|
||||
// memos
|
||||
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])
|
||||
|
||||
/* -------------------- Effects -------------------- */
|
||||
/* WebSocket connect event handler*/
|
||||
useEffect(() => {
|
||||
socket.on('connect', () => {
|
||||
socket.onopen = () => {
|
||||
dispatch(connected())
|
||||
socket.emit('fetch-jobs')
|
||||
socket.emit('disk-space')
|
||||
socket.emit('retrieve-jobs')
|
||||
});
|
||||
}, [])
|
||||
|
||||
/* Ask server for pending jobs / background jobs */
|
||||
useEffect(() => {
|
||||
socket.on('pending-jobs', (count: number) => {
|
||||
count === 0 ? setShowBackdrop(false) : setShowBackdrop(true)
|
||||
})
|
||||
}, [])
|
||||
|
||||
/* Handle download information sent by server */
|
||||
useEffect(() => {
|
||||
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;
|
||||
console.log('oke')
|
||||
socket.send('fetch-jobs')
|
||||
socket.send('disk-space')
|
||||
socket.send('retrieve-jobs')
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => client.running(), 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
socket.onmessage = (event) => {
|
||||
const res = client.decode(event.data)
|
||||
if (showBackdrop) {
|
||||
setShowBackdrop(false)
|
||||
}
|
||||
switch (typeof res.result) {
|
||||
case 'object':
|
||||
setActiveDownloads(
|
||||
res.result
|
||||
.filter((r: RPCResult) => !!r.info.url)
|
||||
.sort((a: RPCResult, b: RPCResult) => a.info.title.localeCompare(b.info.title))
|
||||
)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
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(() => {
|
||||
@@ -140,12 +124,10 @@ export default function Home({ socket }: Props) {
|
||||
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,
|
||||
})
|
||||
client.download(
|
||||
immediate || url || workingUrl,
|
||||
cliArgs.toString() + toFormatArgs(codes),
|
||||
)
|
||||
|
||||
setUrl('')
|
||||
setWorkingUrl('')
|
||||
@@ -162,10 +144,6 @@ export default function Home({ socket }: Props) {
|
||||
* Retrive url from input and display the formats selection view
|
||||
*/
|
||||
const sendUrlFormatSelection = () => {
|
||||
socket.emit('send-url-format-selection', {
|
||||
url: url,
|
||||
})
|
||||
|
||||
setWorkingUrl(url)
|
||||
setUrl('')
|
||||
setPickedAudioFormat('')
|
||||
@@ -199,14 +177,12 @@ export default function Home({ socket }: Props) {
|
||||
* @param id The download id / pid
|
||||
* @returns void
|
||||
*/
|
||||
const abort = (id?: number) => {
|
||||
const abort = (id?: string) => {
|
||||
if (id) {
|
||||
updateInStateMap(id, null, downloadInfoMap, setDownloadInfoMap, true)
|
||||
socket.emit('abort', { pid: id })
|
||||
client.kill(id)
|
||||
return
|
||||
}
|
||||
setDownloadFormats(undefined)
|
||||
socket.emit('abort-all')
|
||||
client.killAll()
|
||||
}
|
||||
|
||||
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}>
|
||||
{
|
||||
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
|
||||
*/
|
||||
}
|
||||
activeDownloads.map(download => (
|
||||
<Grid item xs={4} sm={8} md={6} key={download.id}>
|
||||
<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 ?? ''
|
||||
}
|
||||
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>
|
||||
|
||||
@@ -40,7 +40,7 @@ import { RootState } from "./stores/store";
|
||||
import { validateDomain, validateIP } from "./utils";
|
||||
|
||||
type Props = {
|
||||
socket: Socket
|
||||
socket: WebSocket
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
const updateBinary = () => {
|
||||
socket.emit('update-bin')
|
||||
dispatch(alreadyUpdated())
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,15 +4,24 @@ import { IMessage } from "../interfaces";
|
||||
import { ellipsis } from "../utils";
|
||||
|
||||
type Props = {
|
||||
formattedLog: IMessage,
|
||||
title: string,
|
||||
thumbnail: string,
|
||||
resolution: string
|
||||
progress: number,
|
||||
percentage: string,
|
||||
size: number,
|
||||
speed: number,
|
||||
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 => {
|
||||
if (!xByY) return null;
|
||||
if (xByY.includes('4320')) return (<EightK color="primary" />);
|
||||
@@ -22,6 +31,8 @@ export function StackableResult({ formattedLog, title, thumbnail, resolution, pr
|
||||
return null;
|
||||
}
|
||||
|
||||
const percentageToNumber = () => Number(percentage.replace('%', ''))
|
||||
|
||||
const roundMB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)}MiB`
|
||||
|
||||
return (
|
||||
@@ -43,14 +54,14 @@ export function StackableResult({ formattedLog, title, thumbnail, resolution, pr
|
||||
<Skeleton />
|
||||
}
|
||||
<Stack direction="row" spacing={1} py={2}>
|
||||
<Chip label={formattedLog.status} color="primary" />
|
||||
<Typography>{formattedLog.progress}</Typography>
|
||||
<Typography>{formattedLog.dlSpeed}</Typography>
|
||||
<Typography>{roundMB(formattedLog.size ?? 0)}</Typography>
|
||||
<Chip label={'Downloading'} color="primary" />
|
||||
<Typography>{percentage}</Typography>
|
||||
<Typography>{speed}</Typography>
|
||||
<Typography>{roundMB(size ?? 0)}</Typography>
|
||||
{guessResolution(resolution)}
|
||||
</Stack>
|
||||
{progress ?
|
||||
<LinearProgress variant="determinate" value={progress} /> :
|
||||
{percentage ?
|
||||
<LinearProgress variant="determinate" value={percentageToNumber()} /> :
|
||||
null
|
||||
}
|
||||
</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() {
|
||||
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),
|
||||
}))
|
||||
|
||||
app.Get("/ws", websocket.New(func(c *websocket.Conn) {
|
||||
app.Get("/ws-rpc", websocket.New(func(c *websocket.Conn) {
|
||||
for {
|
||||
mtype, reader, err := c.NextReader()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
res := NewRPCRequest(reader).Call()
|
||||
|
||||
writer, err := c.NextWriter(mtype)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
res := NewRPCRequest(reader).Call()
|
||||
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)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user