update 30
This commit is contained in:
@@ -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
4
.gitignore
vendored
@@ -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
|
||||||
28
README.md
28
README.md
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
12
frontend/src/components/Statistics.tsx
Normal file
12
frontend/src/components/Statistics.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 ''
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -25,4 +25,4 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"parcel": "^2.0.1"
|
"parcel": "^2.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
10
server.js
10
server.js
@@ -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)
|
||||||
Reference in New Issue
Block a user