diff --git a/README.md b/README.md index 8ed60cb..574cb29 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ I will eventually make this better as soon as I can. Not in the immediate. Changelog: ``` +27/01/22: Multidownload implemented! + 26/01/22: Multiple downloads are being implemented. Maybe by next release they will be there. Refactoring and JSDoc. @@ -26,7 +28,7 @@ The avaible settings are currently only: - Extract audio Future releases will have: -- Multi download *on its way* +- ~~Multi download~~ *experimental* - ~~Exctract audio~~ *done* - Format selection @@ -56,10 +58,6 @@ node server.js ``` -## Regarding multiple downloads -There's a way to circumvent the single download restriction **BUT IT LEADS TO UNDEFINED BEHAVIOUR**. -Fire up multiple tabs and make a download for each tab. I know that's horrible but it's gonna be fixed by next release. - ## Todo list - ~~retrieve background tasks~~ - better ui/ux diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f31ba7b..8f29404 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { io } from "socket.io-client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, Fragment } from "react"; import { Container, Row, @@ -9,27 +9,37 @@ import { FormControl, Button, ButtonGroup, - Toast, } from "react-bootstrap"; import { validateDomain, validateIP } from "./utils"; -import { IDLInfo, IMessage } from "./interfaces"; +import { IDLInfo, IDLInfoBase, IMessage } from "./interfaces"; +import { MessageToast } from "./components/MessageToast"; import './App.css'; const socket = io(`http://${localStorage.getItem('server-addr') || 'localhost'}:3022`) export function App() { - const [progress, setProgress] = useState(0) - const [message, setMessage] = useState('') - const [halt, setHalt] = useState(false) - const [url, setUrl] = useState('') - const [showToast, setShowToast] = useState(false) - const [invalidIP, setInvalidIP] = useState(false) - const [updatedBin, setUpdatedBin] = useState(false) - const [showSettings, setShowSettings] = useState(false) - const [darkMode, setDarkMode] = useState(localStorage.getItem('theme') === 'dark') - const [extractAudio, setExtractAudio] = useState(localStorage.getItem('-x') === 'true') - const [downloadInfo, setDownloadInfo] = useState() + const [progressMap, setProgressMap] = useState(new Map()); + const [messageMap, setMessageMap] = useState(new Map()); + const [downloadInfoMap, setDownloadInfoMap] = useState(new Map()); + + const [halt, setHalt] = useState(false); + const [url, setUrl] = useState(''); + const [showToast, setShowToast] = useState(false); + const [invalidIP, setInvalidIP] = useState(false); + const [updatedBin, setUpdatedBin] = useState(false); + const [showSettings, setShowSettings] = useState(false); + const [darkMode, setDarkMode] = useState(localStorage.getItem('theme') === 'dark'); + const [extractAudio, setExtractAudio] = useState(localStorage.getItem('-x') === 'true'); + + const updateInStateMap = (k: number, v: any, target: Map, callback: Function, remove: boolean = false) => { + if (remove) { + target.delete(k) + callback(new Map(target)) + return; + } + callback(new Map(target.set(k, v))); + } useEffect(() => { socket.on('connect', () => { @@ -42,10 +52,8 @@ export function App() { }, []) useEffect(() => { - socket.on('pending-jobs', (jobs: Array) => { - //if (jobs.length > 0) { + socket.on('pending-jobs', () => { socket.emit('retrieve-jobs') - //} }) }, []) @@ -57,21 +65,32 @@ export function App() { useEffect(() => { socket.on('info', (data: IDLInfo) => { - setDownloadInfo(data) + updateInStateMap(data.pid, data.info, downloadInfoMap, setDownloadInfoMap) }) }, []) + useEffect(() => { socket.on('progress', (data: IMessage) => { - setMessage(`operation: ${data.status || '...'} \nprogress: ${data.progress || '?'} \nsize: ${data.size || '?'} \nspeed: ${data.dlSpeed || '?'}`) if (data.status === 'Done!' || data.status === 'Aborted') { setHalt(false) - setMessage('Done!') - setProgress(0) + updateInStateMap( + data.pid, + 'Done!', + messageMap, + setMessageMap + ) + updateInStateMap(data.pid, 0, progressMap, setProgressMap) return } + updateInStateMap( + data.pid, + `operation: ${data.status || '...'} \nprogress: ${data.progress || '?'} \nsize: ${data.size || '?'} \nspeed: ${data.dlSpeed || '?'}`, + messageMap, + setMessageMap + ) if (data.progress) { - setProgress(Math.ceil(Number(data.progress.replace('%', '')))) + updateInStateMap(data.pid, Math.ceil(Number(data.progress.replace('%', ''))), progressMap, setProgressMap) } // if (data.dlSpeed) { // const event = new CustomEvent("dlSpeed", { "detail": detectSpeed(data.dlSpeed) }); @@ -95,6 +114,9 @@ export function App() { xa: extractAudio }, }) + setUrl('') + const input: HTMLInputElement = document.getElementById('urlInput') as HTMLInputElement; + input.value = ''; } const handleUrlChange = (e: React.ChangeEvent) => { @@ -114,12 +136,13 @@ export function App() { } } - const abort = () => { - setDownloadInfo({ - title: '', - thumbnail: '' - }) - socket.emit('abort') + const abort = (id?: number) => { + if (id) { + updateInStateMap(id, null, downloadInfoMap, setDownloadInfoMap, true) + socket.emit('abort', { pid: id }) + return + } + socket.emit('abort-all') setHalt(false) } @@ -149,7 +172,7 @@ export function App() { } return ( - +
@@ -160,39 +183,85 @@ export function App() {
- -
- - {downloadInfo ?

{downloadInfo.title}

: null} - -
Status
- {!message ?
Ready
: null} -
{message}
- - -
- - -
- {/* - - */} -
+ { + Array.from(messageMap).length === 0 ? +
+ + +
Status
+
Ready
+ +
+
: null + } + { /*Super big brain flatMap moment*/ + Array.from(messageMap).flatMap(message => ( + + { + /* + Message[0] => key, the pid which is shared with the progress Map + Message[1] => value, the actual formatted message sent from server + */ + } + {message[1] && message[1] !== 'Done!' ? +
+ + { + downloadInfoMap.get(message[0]) ? +

{downloadInfoMap.get(message[0]).title}

: + null + } + +
Status
+ {!message[1] ?
Ready
: null} +
{message[1]}
+ + +
+ + +
+
: null + } + { + /* + Gets the progress by indexing the map with the pid + */ + } + {progressMap.get(message[0]) ? + : + null + } + {message[1] && message[1] !== 'Done!' ? + + +
+ +
+ +
: null + } +
+ )) + } - - + + - {progress ? : null}
-
setShowSettings(!showSettings)}>Settings
@@ -237,36 +306,18 @@ export function App() { - setShowToast(false)} - bg={'primary'} - delay={1500} - autohide - className="mt-5" - > - - {`Connected to ${localStorage.getItem('server-addr') || 'localhost'}`} - - - setUpdatedBin(false)} - bg={'primary'} - delay={1500} - autohide - className="mt-5" - > - - Updated yt-dlp binary! - - + + {`Connected to ${localStorage.getItem('server-addr') || 'localhost'}`} + + + Updated yt-dlp binary! +
Made with ❤️ by Marcobaobao
- +
) } \ No newline at end of file diff --git a/frontend/src/components/MessageToast.tsx b/frontend/src/components/MessageToast.tsx new file mode 100644 index 0000000..c2c57fb --- /dev/null +++ b/frontend/src/components/MessageToast.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { + Toast, +} from "react-bootstrap"; + +type Props = { + flag: boolean, + callback: Function, + children: + | JSX.Element + | string +} + +export function MessageToast({ flag, callback, children }: Props) { + return ( + callback(false)} + bg={'primary'} + delay={1500} + autohide + className="mt-5" + > + + {children} + + + ); +} \ No newline at end of file diff --git a/frontend/src/interfaces.ts b/frontend/src/interfaces.ts index 5f0ef57..544651d 100644 --- a/frontend/src/interfaces.ts +++ b/frontend/src/interfaces.ts @@ -3,13 +3,20 @@ export interface IMessage { progress?: string, size?: string, dlSpeed?: string + pid: number } -export interface IDLInfo { +export interface IDLInfoBase { title: string, thumbnail: string, upload_date?: string | Date, duration?: number + resolution?: string +} + +export interface IDLInfo { + pid: number, + info: IDLInfoBase } export interface IDLSpeed { diff --git a/lib/Process.js b/lib/Process.js index 184a902..aa60f7c 100644 --- a/lib/Process.js +++ b/lib/Process.js @@ -7,7 +7,7 @@ const { logger } = require('./logger'); * @constructor * @param {string} url - The downlaod url. * @param {string} params - The cli arguments passed by the frontend. - * @param {object} settings - The download settings passed by the frontend. + * @param {*} settings - The download settings passed by the frontend. */ class Process { @@ -26,7 +26,7 @@ class Process { * @returns {Promise} the process instance */ async start(callback) { - await this.__internalGetInfo(); + await this.#__internalGetInfo(); const ytldp = spawn('./lib/yt-dlp', [ @@ -51,7 +51,7 @@ class Process { * function used internally by the download process to fetch information, usually thumbnail and title * @returns Promise to the lock */ - async __internalGetInfo() { + async #__internalGetInfo() { let lock = true; let stdoutChunks = []; const ytdlpInfo = spawn('./lib/yt-dlp', ['-s', '-j', this.url]); diff --git a/lib/downloader.js b/lib/downloader.js index 54a0bcc..1e35e0e 100644 --- a/lib/downloader.js +++ b/lib/downloader.js @@ -40,6 +40,7 @@ async function download(socket, payload) { const p = new Process(url, params, settings); p.start().then(downloader => { + pool.add(p) let infoLock = true; let pid = downloader.getPid(); @@ -52,7 +53,9 @@ async function download(socket, payload) { if (downloader.getInfo() === null) { return; } - socket.emit('info', downloader.getInfo()); + socket.emit('info', { + pid: pid, info: downloader.getInfo() + }); infoLock = false; } socket.emit('progress', formatter(String(stdout), pid)) // finally, emit @@ -61,13 +64,15 @@ async function download(socket, payload) { downloader.kill().then(() => { socket.emit('progress', { status: 'Done!', - process: pid, + pid: pid, }) pool.remove(downloader); }) }, error: () => { - socket.emit('progress', { status: 'Done!' }); + socket.emit('progress', { + status: 'Done!', pid: pid + }); } }); }) @@ -99,14 +104,23 @@ async function retriveDownload(socket) { // it's an hot-reload the server it's running and the frontend ask for // the pending job: retrieve them from the "in-memory database" (ProcessPool) logger('dl', `Retrieving jobs from pool`) - const it = pool.iterator(); + const it = pool.iterator(); + tempWorkQueue = new Array(); + + // sanitize for (const entry of it) { const [pid, process] = entry; + pool.removeByPid(pid); await killProcess(pid); + tempWorkQueue.push(process); + } + + // resume the jobs + for (const entry of tempWorkQueue) { await download(socket, { - url: process.url, - params: process.params + url: entry.url, + params: entry.params, }); } } diff --git a/server.js b/server.js index f6778c6..71345a7 100644 --- a/server.js +++ b/server.js @@ -10,7 +10,7 @@ const Koa = require('koa'), download, abortDownload, retriveDownload, - abortAllDownloads + abortAllDownloads, } = require('./lib/downloader'), db = require('./lib/db'); @@ -23,10 +23,13 @@ const io = new Server(server, { } }) +/* + WebSocket listeners +*/ io.on('connection', socket => { logger('ws', `${socket.handshake.address} connected!`) - // message listeners - socket.on('send-url', args => { + + socket.on('send-url', (args) => { logger('ws', args?.url) download(socket, args) }) @@ -39,10 +42,10 @@ io.on('connection', socket => { socket.on('update-bin', () => { ytdlpUpdater(socket) }) - socket.on('fetch-jobs', async () => { - socket.emit('pending-jobs', await db.retrieveAll()) + socket.on('fetch-jobs', () => { + socket.emit('pending-jobs', db.retrieveAll()) }) - socket.on('retrieve-jobs', async () => { + socket.on('retrieve-jobs', () => { retriveDownload(socket) }) })