update 30

This commit is contained in:
2021-11-30 13:07:22 +01:00
parent 8f204f7cbd
commit 29d23144e7
11 changed files with 124 additions and 62 deletions

View File

@@ -2,4 +2,8 @@ node_modules
dist dist
package-lock.json package-lock.json
.parcel-cache .parcel-cache
.git .git
lib/*.exe
lib/yt-dlp
.env
*.mp4

4
.gitignore vendored
View File

@@ -3,4 +3,6 @@ dist
package-lock.json package-lock.json
node_modules node_modules
lib/*.exe lib/*.exe
.env lib/yt-dlp
.env
*.mp4

View File

@@ -1,12 +1,26 @@
# yt-dlp Web UI # yt-dlp Web UI
A terrible web ui for yt-dlp. A terrible web ui for yt-dlp.
Created for the only purpose of *cough cough* k-pop videos from my server/nas. Created for the only purpose of *consume* videos from my server/nas.
I will eventually make this better as soon as I can. Not in the immediate. I will eventually make this better as soon as I can. Not in the immediate.
<img src="https://i.ibb.co/drt0LWc/Screenshot-2021-11-24-at-13-11-09-yt-dlp-Web-UI.png" alt="ytdlpwebui"> <img src="https://i.ibb.co/7VBK1PY/1.png">
## Docker install ## Now with dark mode
<img src="https://i.ibb.co/h8S5vKg/2.png">
## Settings
The avaible settings are currently only:
- Server address
- Switch theme
Future releases will have:
- Exctract audio
- Format selection
## Docker installation
``` ```
docker pull marcobaobao/yt-dlp-webui:latest docker pull marcobaobao/yt-dlp-webui:latest
docker run -d -p 3022:3022 -v <your dir>:/usr/src/yt-dlp-webui/downloads marcobaobao/yt-dlp-webui docker run -d -p 3022:3022 -v <your dir>:/usr/src/yt-dlp-webui/downloads marcobaobao/yt-dlp-webui
@@ -17,16 +31,14 @@ docker build -t yt-dlp-webui .
docker run -d -p 3022:3022 -v <your dir>:/usr/src/yt-dlp-webui/downloads yt-dlp-webui docker run -d -p 3022:3022 -v <your dir>:/usr/src/yt-dlp-webui/downloads yt-dlp-webui
``` ```
## Manual install ## Manual installation
``` ```
// download the yt-dl build and put it in the lib folder and make it executable
npm i npm i
npm run build npm run build
npm run fetch npm run fetch
// edit the settings.json specifying the download path or // edit the settings.json specifying the download path or
// it will use the following folder // it will default to the following created folder
mkdir downloads mkdir downloads
@@ -34,5 +46,5 @@ node server.js
``` ```
## Todo list ## Todo list
- retrieve background task - retrieve background tasks
- better ui/ux - better ui/ux

View File

@@ -12,7 +12,7 @@ import {
Toast, Toast,
} from "react-bootstrap"; } from "react-bootstrap";
import { validateDomain, validateIP } from "./utils"; import { validateDomain, validateIP } from "./utils";
import { IMessage } from "./interfaces"; import { IDLInfo, IMessage } from "./interfaces";
import './App.css'; import './App.css';
const socket = io(`http://${localStorage.getItem('server-addr') || 'localhost'}:3022`) const socket = io(`http://${localStorage.getItem('server-addr') || 'localhost'}:3022`)
@@ -28,11 +28,15 @@ export function App() {
const [updatedBin, setUpdatedBin] = useState(false) const [updatedBin, setUpdatedBin] = useState(false)
const [showSettings, setShowSettings] = useState(false) const [showSettings, setShowSettings] = useState(false)
const [darkMode, setDarkMode] = useState(localStorage.getItem('theme') === 'dark') const [darkMode, setDarkMode] = useState(localStorage.getItem('theme') === 'dark')
const [downloadInfo, setDownloadInfo] = useState<IDLInfo>()
useEffect(() => { useEffect(() => {
socket.on('connect', () => { socket.on('connect', () => {
setShowToast(true) setShowToast(true)
}) })
return () => {
socket.disconnect()
}
}, []) }, [])
useEffect(() => { useEffect(() => {
@@ -41,9 +45,15 @@ export function App() {
document.body.classList.remove('dark') document.body.classList.remove('dark')
}, [darkMode]) }, [darkMode])
useEffect(() => {
socket.on('info', (data: IDLInfo) => {
setDownloadInfo(data)
})
}, [])
useEffect(() => { useEffect(() => {
socket.on('progress', (data: IMessage) => { socket.on('progress', (data: IMessage) => {
setMessage(`${data.status || '...'} | progress: ${data.progress || '?'} | size: ${data.size || '?'} | speed: ${data.dlSpeed || '?'}`) setMessage(`operation: ${data.status || '...'} \nprogress: ${data.progress || '?'} \nsize: ${data.size || '?'} \nspeed: ${data.dlSpeed || '?'}`)
if (data.status === 'Done!') { if (data.status === 'Done!') {
setHalt(false) setHalt(false)
setMessage('Done!') setMessage('Done!')
@@ -86,6 +96,10 @@ export function App() {
} }
const abort = () => { const abort = () => {
setDownloadInfo({
title: '',
thumbnail: ''
})
socket.emit('abort') socket.emit('abort')
setHalt(false) setHalt(false)
} }
@@ -123,13 +137,23 @@ export function App() {
</InputGroup> </InputGroup>
<div className="mt-2 status-box"> <div className="mt-2 status-box">
<h6>Status</h6> <Row>
{!message ? <pre>Ready</pre> : null} {downloadInfo ? <p>{downloadInfo.title}</p> : null}
<pre id='status'>{message}</pre> <Col sm={9}>
<h6>Status</h6>
{!message ? <pre>Ready</pre> : null}
<pre id='status'>{message}</pre>
</Col>
<Col sm={3}>
<br />
<img className="img-fluid rounded" src={downloadInfo?.thumbnail} />
</Col>
</Row>
</div> </div>
<ButtonGroup>
<Button className="mt-2" onClick={() => sendUrl()} disabled={halt}>Start</Button>{' '} <ButtonGroup className="mt-2">
<Button className="mt-2" active onClick={() => abort()}>Abort</Button>{' '} <Button onClick={() => sendUrl()} disabled={halt}>Start</Button>
<Button active onClick={() => abort()}>Abort</Button>
</ButtonGroup> </ButtonGroup>
{progress ? <ProgressBar className="container-padding mt-2" now={progress} variant="primary" /> : null} {progress ? <ProgressBar className="container-padding mt-2" now={progress} variant="primary" /> : null}
@@ -161,14 +185,14 @@ export function App() {
<Button <Button
variant={darkMode ? 'light' : 'dark'} variant={darkMode ? 'light' : 'dark'}
onClick={() => toggleTheme()}> onClick={() => toggleTheme()}>
{darkMode ? 'Dark theme' : 'Light theme'} {darkMode ? 'Light theme' : 'Dark theme'}
</Button> </Button>
</div> : </div> :
null null
} }
<div className="mt-5" /> <div className="mt-5" />
<div>Once you close the page the download will continue in the background.</div> <div>Once you close this page the download will continue in the background.</div>
<div>It won't be possible retriving the progress though.</div> <div>It won't be possible retriving the progress though.</div>
<div className="mt-5" /> <div className="mt-5" />
<small>Made with ❤️ by Marcobaobao</small> <small>Made with ❤️ by Marcobaobao</small>

View File

@@ -0,0 +1,12 @@
import React, { useState } from "react";
import { IDLSpeed } from "../interfaces";
export function Statistics(props: any) {
const [dataset, setDataset] = useState<Array<IDLSpeed>>()
return (
<div className="chart">
</div>
)
}

View File

@@ -2,7 +2,14 @@ export interface IMessage {
status: string, status: string,
progress?: string, progress?: string,
size?: string, size?: string,
dlSpeed?: string dlSpeed?: string | IDLSpeed
}
export interface IDLInfo {
title: string,
thumbnail: string,
upload_date?: string | Date,
duration?: number
} }
export interface IDLSpeed { export interface IDLSpeed {

View File

@@ -6,4 +6,11 @@ export function validateIP(ipAddr: string): boolean {
export function validateDomain(domainName: string): boolean { export function validateDomain(domainName: string): boolean {
let domainRegex = /[^@ \t\r\n]+.[^@ \t\r\n]+\.[^@ \t\r\n]+/ let domainRegex = /[^@ \t\r\n]+.[^@ \t\r\n]+\.[^@ \t\r\n]+/
return domainRegex.test(domainName) || domainName === 'localhost' return domainRegex.test(domainName) || domainName === 'localhost'
}
export function ellipsis(str: string, lim: number): string {
if (str) {
return str.length > lim ? `${str.substr(0, lim)}...` : str
}
return ''
} }

View File

@@ -14,22 +14,27 @@ catch (e) {
const isWindows = process.platform === 'win32' const isWindows = process.platform === 'win32'
const download = (socket, url) => { const download = (socket, url) => {
if (url === '' || url === null) {
socket.emit('progress', { status: 'Done!' })
return
}
getDownloadInfo(socket, url)
const ytldp = spawn(`./lib/yt-dlp${isWindows ? '.exe' : ''}`, const ytldp = spawn(`./lib/yt-dlp${isWindows ? '.exe' : ''}`,
['-o', `${settings.download_path || 'downloads/'}%(title)s.%(ext)s`, url] ['-o', `${settings.download_path || 'downloads/'}%(title)s.%(ext)s`, url]
) )
from(ytldp.stdout) // stout as observable from(ytldp.stdout) // stdout as observable
.pipe(throttle(() => interval(500))) // discard events closer than 500ms .pipe(throttle(() => interval(500))) // discard events closer than 500ms
.subscribe({ .subscribe({
next: (stdout) => { next: (stdout) => {
let _stdout = String(stdout) //let _stdout = String(stdout)
socket.emit('progress', formatter(_stdout)) // finally, emit socket.emit('progress', formatter(String(stdout))) // finally, emit
//logger('download', `Fetching ${stdout}`) //logger('download', `Fetching ${stdout}`)
console.log(formatter(_stdout))
}, },
complete: () => { complete: () => {
socket.emit('progress', { status: 'Done!' }) socket.emit('progress', { status: 'Done!' })
logger('download', 'Done!')
} }
}) })
@@ -39,15 +44,29 @@ const download = (socket, url) => {
}) })
} }
const getDownloadInfo = (socket, url) => {
let stdoutChunks = [];
const ytdlpInfo = spawn(`./lib/yt-dlp${isWindows ? '.exe' : ''}`, ['-s', '-j', url]);
ytdlpInfo.stdout.on('data', (data) => {
stdoutChunks.push(data)
})
ytdlpInfo.on('exit', () => {
const buffer = Buffer.concat(stdoutChunks)
const json = JSON.parse(buffer.toString())
socket.emit('info', json)
})
}
const abortDownload = (socket) => { const abortDownload = (socket) => {
const res = process.platform === 'win32' ? const res = process.platform === 'win32' ?
spawn('taskkill', ['/IM', 'yt-dlp.exe', '/F', '/T']) : spawn('taskkill', ['/IM', 'yt-dlp.exe', '/F', '/T']) :
spawn('killall', ['yt-dlp']) spawn('killall', ['yt-dlp'])
res.stdout.on('data', data => { res.on('exit', () => {
socket.emit('progress', 'Aborted!') socket.emit('progress', 'Aborted!')
logger('download', `Aborting ${data.toString()}`) logger('download', 'Aborting downloads')
}) })
logger('download', 'Aborted')
} }
const formatter = (stdout) => { const formatter = (stdout) => {
@@ -71,10 +90,9 @@ const formatter = (stdout) => {
default: default:
return { progress: '0' } return { progress: '0' }
} }
} }
module.exports = { module.exports = {
download: download, download: download,
abortDownload: abortDownload abortDownload: abortDownload,
} }

View File

@@ -1,24 +0,0 @@
const Readable = require('stream').Readable;
class Subscription extends Readable{
constructor(options) {
super();
if (!(this instanceof Subscription))
return new Subscription(options);
options = options || {};
Readable.call(this, options);
this.value = 0;
}
_read() {
while(this.value <= 100){
this.push(String(this.value++));
}
}
}
exports.subscribe = function(event, options){
return new Subscription(options);
}

View File

@@ -25,4 +25,4 @@
"devDependencies": { "devDependencies": {
"parcel": "^2.0.1" "parcel": "^2.0.1"
} }
} }

View File

@@ -18,8 +18,8 @@ const io = new Server(server, {
}) })
io.on('connection', socket => { io.on('connection', socket => {
logger('ws', 'connesso') logger('ws', `${socket.handshake.address} connected!`)
// message listeners
socket.on('send-url', args => { socket.on('send-url', args => {
logger('ws', args) logger('ws', args)
download(socket, args) download(socket, args)
@@ -32,14 +32,14 @@ io.on('connection', socket => {
}) })
}) })
io.on('disconnect', () => { io.on('disconnect', (socket) => {
logger('ws', 'disconnesso') logger('ws', `${socket.handshake.address} disconnected`)
}) })
app app
.use(cors()) .use(cors())
.use(serve(path.join(__dirname, 'dist'))) .use(serve(path.join(__dirname, 'dist')))
console.log('[koa] Server started port', process.env.PORT || 3022) logger('koa', `Server started on port ${process.env.PORT || 3022}`)
server.listen(process.env.PORT || 3022) server.listen(process.env.PORT || 3022)