monthly update
This commit is contained in:
@@ -6,4 +6,5 @@ package-lock.json
|
||||
lib/*.exe
|
||||
lib/yt-dlp
|
||||
.env
|
||||
*.mp4
|
||||
*.mp4
|
||||
*.ytdl
|
||||
18
.github/workflows/docker-image.yml
vendored
Normal file
18
.github/workflows/docker-image.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the Docker image
|
||||
run: docker build . --file Dockerfile --tag my-image-name:$(date +%s)
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,4 +5,6 @@ node_modules
|
||||
lib/*.exe
|
||||
lib/yt-dlp
|
||||
.env
|
||||
*.mp4
|
||||
*.mp4
|
||||
*.ytdl
|
||||
*.part
|
||||
@@ -5,6 +5,7 @@ WORKDIR /usr/src/yt-dlp-webui
|
||||
COPY package*.json ./
|
||||
RUN apt-get update
|
||||
RUN apt-get install curl ffmpeg -y
|
||||
RUN apt-get install psmisc
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
@@ -12,4 +13,4 @@ RUN chmod +x ./lib/fetch-yt-dlp.sh
|
||||
RUN ./lib/fetch-yt-dlp.sh && mv yt-dlp ./lib
|
||||
RUN rm -rf .parcel-cache
|
||||
EXPOSE 3022
|
||||
CMD [ "node" , "./server.js" ]
|
||||
CMD [ "node" , "./server.js" ]
|
||||
|
||||
11
README.md
11
README.md
@@ -1,9 +1,12 @@
|
||||
# yt-dlp Web UI
|
||||
|
||||
A terrible web ui for yt-dlp.
|
||||
A not so terrible web ui for yt-dlp.
|
||||
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.
|
||||
|
||||
**Background jobs now are retrieved. It's still rudimentary but it leverages**
|
||||
**on yt-dlp resume feature**
|
||||
|
||||
<img src="https://i.ibb.co/7VBK1PY/1.png">
|
||||
|
||||
## Now with dark mode
|
||||
@@ -15,9 +18,11 @@ I will eventually make this better as soon as I can. Not in the immediate.
|
||||
The avaible settings are currently only:
|
||||
- Server address
|
||||
- Switch theme
|
||||
- Retrieve background jobs
|
||||
|
||||
Future releases will have:
|
||||
- Exctract audio
|
||||
- Multi download
|
||||
- ~~Exctract audio~~
|
||||
- Format selection
|
||||
|
||||
## Docker installation
|
||||
@@ -46,5 +51,5 @@ node server.js
|
||||
```
|
||||
|
||||
## Todo list
|
||||
- retrieve background tasks
|
||||
- ~~retrieve background tasks~~
|
||||
- better ui/ux
|
||||
|
||||
BIN
downloads.db
Normal file
BIN
downloads.db
Normal file
Binary file not shown.
@@ -28,17 +28,27 @@ export function App() {
|
||||
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<IDLInfo>()
|
||||
|
||||
useEffect(() => {
|
||||
socket.on('connect', () => {
|
||||
setShowToast(true)
|
||||
socket.emit('fetch-jobs')
|
||||
})
|
||||
return () => {
|
||||
socket.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
socket.on('pending-jobs', (jobs: Array<any>) => {
|
||||
if (jobs.length > 0) {
|
||||
socket.emit('retrieve-jobs')
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
darkMode ?
|
||||
document.body.classList.add('dark') :
|
||||
@@ -54,15 +64,19 @@ export function App() {
|
||||
useEffect(() => {
|
||||
socket.on('progress', (data: IMessage) => {
|
||||
setMessage(`operation: ${data.status || '...'} \nprogress: ${data.progress || '?'} \nsize: ${data.size || '?'} \nspeed: ${data.dlSpeed || '?'}`)
|
||||
if (data.status === 'Done!') {
|
||||
if (data.status === 'Done!' || data.status === 'Aborted') {
|
||||
setHalt(false)
|
||||
setMessage('Done!')
|
||||
setProgress(0)
|
||||
return
|
||||
}
|
||||
setProgress(
|
||||
Math.ceil(Number(data.progress.replace('%', '')))
|
||||
)
|
||||
if (data.progress) {
|
||||
setProgress(Math.ceil(Number(data.progress.replace('%', ''))))
|
||||
}
|
||||
// if (data.dlSpeed) {
|
||||
// const event = new CustomEvent<number>("dlSpeed", { "detail": detectSpeed(data.dlSpeed) });
|
||||
// document.dispatchEvent(event);
|
||||
// }
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -75,7 +89,12 @@ export function App() {
|
||||
|
||||
const sendUrl = () => {
|
||||
setHalt(true)
|
||||
socket.emit('send-url', url)
|
||||
socket.emit('send-url', {
|
||||
url: url,
|
||||
params: {
|
||||
xa: extractAudio
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -119,111 +138,135 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const toggleExtractAudio = () => {
|
||||
if (extractAudio) {
|
||||
localStorage.setItem('-x', 'false')
|
||||
setExtractAudio(false)
|
||||
} else {
|
||||
localStorage.setItem('-x', 'true')
|
||||
setExtractAudio(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col lg={7} xs={12}>
|
||||
<div className="mt-5" />
|
||||
<h1 className="fw-bold">yt-dlp WebUI</h1>
|
||||
<div className="mt-5" />
|
||||
<React.Fragment>
|
||||
<Container className="pb-5">
|
||||
<Row>
|
||||
<Col lg={7} xs={12}>
|
||||
<div className="mt-5" />
|
||||
<h1 className="fw-bold">yt-dlp WebUI</h1>
|
||||
<div className="mt-5" />
|
||||
|
||||
<div className="p-3 stack-box shadow">
|
||||
<InputGroup>
|
||||
<FormControl
|
||||
className="url-input"
|
||||
placeholder="YouTube or other supported service video url"
|
||||
onChange={handleUrlChange}
|
||||
/>
|
||||
</InputGroup>
|
||||
<div className="p-3 stack-box shadow">
|
||||
<InputGroup>
|
||||
<FormControl
|
||||
className="url-input"
|
||||
placeholder="YouTube or other supported service video url"
|
||||
onChange={handleUrlChange}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
<div className="mt-2 status-box">
|
||||
<Row>
|
||||
{downloadInfo ? <p>{downloadInfo.title}</p> : null}
|
||||
<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 className="mt-2 status-box">
|
||||
<Row>
|
||||
{downloadInfo ? <p>{downloadInfo.title}</p> : null}
|
||||
<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>
|
||||
{/* <Col>
|
||||
<Statistics></Statistics>
|
||||
</Col> */}
|
||||
</div>
|
||||
|
||||
<ButtonGroup className="mt-2">
|
||||
<Button onClick={() => sendUrl()} disabled={halt}>Start</Button>
|
||||
<Button active onClick={() => abort()}>Abort</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
{progress ? <ProgressBar className="container-padding mt-2" now={progress} variant="primary" /> : null}
|
||||
</div>
|
||||
|
||||
<ButtonGroup className="mt-2">
|
||||
<Button onClick={() => sendUrl()} disabled={halt}>Start</Button>
|
||||
<Button active onClick={() => abort()}>Abort</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
{progress ? <ProgressBar className="container-padding mt-2" now={progress} variant="primary" /> : null}
|
||||
</div>
|
||||
<div className="my-4">
|
||||
<span className="settings" onClick={() => setShowSettings(!showSettings)}>Settings</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="my-4">
|
||||
<span className="settings" onClick={() => setShowSettings(!showSettings)}>Settings</span>
|
||||
</div>
|
||||
|
||||
{showSettings ?
|
||||
<div className="p-3 stack-box shadow">
|
||||
<h6>Server address</h6>
|
||||
<InputGroup className="mb-3 url-input" hasValidation>
|
||||
<InputGroup.Text>ws://</InputGroup.Text>
|
||||
<FormControl
|
||||
defaultValue={localStorage.getItem('server-addr') || 'localhost'}
|
||||
placeholder="Server address"
|
||||
aria-label="Server address"
|
||||
onChange={handleAddrChange}
|
||||
isInvalid={invalidIP}
|
||||
isValid={!invalidIP}
|
||||
/>
|
||||
<InputGroup.Text>:3022</InputGroup.Text>
|
||||
</InputGroup>
|
||||
<Button onClick={() => updateBinary()} disabled={halt}>
|
||||
Update yt-dlp binary
|
||||
</Button>{' '}
|
||||
<Button
|
||||
variant={darkMode ? 'light' : 'dark'}
|
||||
onClick={() => toggleTheme()}>
|
||||
{darkMode ? 'Light theme' : 'Dark theme'}
|
||||
</Button>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<div className="mt-5" />
|
||||
<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 className="mt-5" />
|
||||
<small>Made with ❤️ by Marcobaobao</small>
|
||||
</Col>
|
||||
<Col>
|
||||
<Toast
|
||||
show={showToast}
|
||||
onClose={() => setShowToast(false)}
|
||||
bg={'primary'}
|
||||
delay={1500}
|
||||
autohide
|
||||
className="mt-5"
|
||||
>
|
||||
<Toast.Body className="text-light">
|
||||
{`Connected to ${localStorage.getItem('server-addr') || 'localhost'}`}
|
||||
</Toast.Body>
|
||||
</Toast>
|
||||
<Toast
|
||||
show={updatedBin}
|
||||
onClose={() => setUpdatedBin(false)}
|
||||
bg={'primary'}
|
||||
delay={1500}
|
||||
autohide
|
||||
className="mt-5"
|
||||
>
|
||||
<Toast.Body className="text-light">
|
||||
Updated yt-dlp binary!
|
||||
</Toast.Body>
|
||||
</Toast>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
{showSettings ?
|
||||
<div className="p-3 stack-box shadow">
|
||||
<h6>Server address</h6>
|
||||
<InputGroup className="mb-3 url-input" hasValidation>
|
||||
<InputGroup.Text>ws://</InputGroup.Text>
|
||||
<FormControl
|
||||
defaultValue={localStorage.getItem('server-addr') || 'localhost'}
|
||||
placeholder="Server address"
|
||||
aria-label="Server address"
|
||||
onChange={handleAddrChange}
|
||||
isInvalid={invalidIP}
|
||||
isValid={!invalidIP}
|
||||
/>
|
||||
<InputGroup.Text>:3022</InputGroup.Text>
|
||||
</InputGroup>
|
||||
<Button onClick={() => updateBinary()} disabled={halt}>
|
||||
Update yt-dlp binary
|
||||
</Button>{' '}
|
||||
<Button
|
||||
variant={darkMode ? 'light' : 'dark'}
|
||||
onClick={() => toggleTheme()}
|
||||
>
|
||||
{darkMode ? 'Light theme' : 'Dark theme'}
|
||||
</Button>
|
||||
<div className="pt-2">
|
||||
<input type="checkbox" name="-x" id="-x"
|
||||
onClick={() => toggleExtractAudio()} checked={extractAudio} />
|
||||
<label htmlFor="-x"> Extract audio</label>
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
<div className="mt-5" />
|
||||
<div>
|
||||
<small>
|
||||
Once you close this page the download will continue in the background.
|
||||
</small>
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<Toast
|
||||
show={showToast}
|
||||
onClose={() => setShowToast(false)}
|
||||
bg={'primary'}
|
||||
delay={1500}
|
||||
autohide
|
||||
className="mt-5"
|
||||
>
|
||||
<Toast.Body className="text-light">
|
||||
{`Connected to ${localStorage.getItem('server-addr') || 'localhost'}`}
|
||||
</Toast.Body>
|
||||
</Toast>
|
||||
<Toast
|
||||
show={updatedBin}
|
||||
onClose={() => setUpdatedBin(false)}
|
||||
bg={'primary'}
|
||||
delay={1500}
|
||||
autohide
|
||||
className="mt-5"
|
||||
>
|
||||
<Toast.Body className="text-light">
|
||||
Updated yt-dlp binary!
|
||||
</Toast.Body>
|
||||
</Toast>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<div className="container pb-5">
|
||||
<small>Made with ❤️ by Marcobaobao</small>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
34
frontend/src/components/StackableInput.tsx
Normal file
34
frontend/src/components/StackableInput.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import {
|
||||
InputGroup,
|
||||
FormControl,
|
||||
Button,
|
||||
ProgressBar
|
||||
} from "react-bootstrap";
|
||||
|
||||
export function StackableInput(props: any) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<InputGroup className="mt-5">
|
||||
<FormControl
|
||||
className="url-input"
|
||||
placeholder="YouTube or other supported service video url"
|
||||
onChange={props.handleUrlChange}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
<div className="mt-2 status-box">
|
||||
<h6>Status</h6>
|
||||
<pre id='status'>{props.message}</pre>
|
||||
</div>
|
||||
|
||||
{props.progress ?
|
||||
<ProgressBar className="container-padding" now={props.progress} variant="danger" /> :
|
||||
null
|
||||
}
|
||||
|
||||
{/* <Button className="my-5" variant="danger" onClick={() => sendUrl()} disabled={props.halt}>Go!</Button>{' '} */}
|
||||
{/* <Button variant="danger" active onClick={() => abort()}>Abort</Button>{' '} */}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,52 @@
|
||||
import React, { useState } from "react";
|
||||
import { IDLSpeed } from "../interfaces";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from 'chart.js';
|
||||
import { on } from "../events";
|
||||
|
||||
export function Statistics(props: any) {
|
||||
const [dataset, setDataset] = useState<Array<IDLSpeed>>()
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
);
|
||||
|
||||
export function Statistics() {
|
||||
const dataset = new Array<number>();
|
||||
const chartRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
on('dlSpeed', (data: CustomEvent<any>) => {
|
||||
dataset.push(data.detail)
|
||||
chartRef.current.update()
|
||||
})
|
||||
}, [])
|
||||
|
||||
const data = {
|
||||
labels: dataset.map(() => ''),
|
||||
datasets: [
|
||||
{
|
||||
data: dataset,
|
||||
label: 'download speed',
|
||||
borderColor: 'rgb(53, 162, 235)',
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chart">
|
||||
|
||||
<Line data={data} ref={chartRef} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
frontend/src/events.ts
Normal file
3
frontend/src/events.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function on(eventType: string, listener: any) {
|
||||
document.addEventListener(eventType, listener)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ export interface IMessage {
|
||||
status: string,
|
||||
progress?: string,
|
||||
size?: string,
|
||||
dlSpeed?: string | IDLSpeed
|
||||
dlSpeed?: string
|
||||
}
|
||||
|
||||
export interface IDLInfo {
|
||||
@@ -15,4 +15,4 @@ export interface IDLInfo {
|
||||
export interface IDLSpeed {
|
||||
effective: number,
|
||||
unit: string,
|
||||
}
|
||||
}
|
||||
@@ -13,4 +13,17 @@ export function ellipsis(str: string, lim: number): string {
|
||||
return str.length > lim ? `${str.substr(0, lim)}...` : str
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function detectSpeed(str: string): number {
|
||||
let effective = str.match(/[\d,]+(\.\d+)?/)[0]
|
||||
const unit = str.replace(effective, '')
|
||||
switch (unit) {
|
||||
case 'MiB/s':
|
||||
return Number(effective) * 1000
|
||||
case 'KiB/s':
|
||||
return Number(effective)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
74
lib/db.js
Normal file
74
lib/db.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const uuid = require('uuid')
|
||||
const { logger } = require('./logger')
|
||||
const { existsInProc } = require('./procUtils')
|
||||
|
||||
const db = require('better-sqlite3')('downloads.db')
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
db.exec(`CREATE TABLE downloads (
|
||||
uid varchar(36) NOT NULL,
|
||||
url text NOT NULL,
|
||||
title text,
|
||||
thumbnail text,
|
||||
created date,
|
||||
size text,
|
||||
process_pid int NOT NULL,
|
||||
PRIMARY KEY (uid)
|
||||
)`)
|
||||
} catch (e) {
|
||||
logger('db', 'Table already created, ignoring')
|
||||
}
|
||||
}
|
||||
|
||||
async function get_db() {
|
||||
return db
|
||||
}
|
||||
|
||||
async function insertDownload(url, title, thumbnail, size, PID) {
|
||||
const uid = uuid.v1()
|
||||
db
|
||||
.prepare(`
|
||||
INSERT INTO downloads
|
||||
(uid, url, title, thumbnail, size, process_pid)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
.run(uid, url, title, thumbnail, size, PID)
|
||||
|
||||
return uid
|
||||
}
|
||||
|
||||
async function retrieveAll() {
|
||||
return db
|
||||
.prepare('SELECT * FROM downloads')
|
||||
.all()
|
||||
}
|
||||
|
||||
async function deleteDownloadById(uid) {
|
||||
db.prepare(`DELETE FROM downloads WHERE uid=${uid}`).run()
|
||||
}
|
||||
|
||||
async function deleteDownloadByPID(PID) {
|
||||
db.prepare(`DELETE FROM downloads WHERE process_pid=${PID}`).run()
|
||||
}
|
||||
|
||||
async function pruneDownloads() {
|
||||
const all = await retrieveAll()
|
||||
return all.map(job => {
|
||||
if (existsInProc(job.process_pid)) {
|
||||
return job
|
||||
} else {
|
||||
deleteDownloadByPID(job.process_pid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: init,
|
||||
getDB: get_db,
|
||||
insertDownload: insertDownload,
|
||||
retrieveAll: retrieveAll,
|
||||
deleteDownloadById: deleteDownloadById,
|
||||
deleteDownloadByPID: deleteDownloadByPID,
|
||||
pruneDownloads: pruneDownloads,
|
||||
}
|
||||
@@ -1,29 +1,41 @@
|
||||
const { spawn } = require('child_process');
|
||||
const logger = require('./logger');
|
||||
const { from, interval } = require('rxjs');
|
||||
const { throttle } = require('rxjs/operators');
|
||||
const { insertDownload, deleteDownloadByPID, pruneDownloads } = require('./db');
|
||||
const { logger } = require('./logger');
|
||||
const { retriveStdoutFromProcFd, killProcess } = require('./procUtils');
|
||||
let settings;
|
||||
|
||||
try {
|
||||
settings = require('../settings.json')
|
||||
settings = require('../settings.json');
|
||||
}
|
||||
catch (e) {
|
||||
console.warn("settings.json not found")
|
||||
console.warn("settings.json not found");
|
||||
}
|
||||
|
||||
const isWindows = process.platform === 'win32'
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
const download = (socket, url) => {
|
||||
if (url === '' || url === null) {
|
||||
socket.emit('progress', { status: 'Done!' })
|
||||
return
|
||||
async function download(socket, payload) {
|
||||
|
||||
if (!payload || payload.url === '' || payload.url === null) {
|
||||
socket.emit('progress', { status: 'Done!' });
|
||||
return;
|
||||
}
|
||||
|
||||
getDownloadInfo(socket, url)
|
||||
const url = payload.url
|
||||
const params = payload.params?.xa ? '-x' : '';
|
||||
|
||||
await getDownloadInfo(socket, url);
|
||||
|
||||
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`,
|
||||
params,
|
||||
url
|
||||
]
|
||||
);
|
||||
|
||||
await insertDownload(url, null, null, null, ytldp.pid);
|
||||
|
||||
from(ytldp.stdout) // stdout as observable
|
||||
.pipe(throttle(() => interval(500))) // discard events closer than 500ms
|
||||
@@ -31,49 +43,69 @@ const download = (socket, url) => {
|
||||
next: (stdout) => {
|
||||
//let _stdout = String(stdout)
|
||||
socket.emit('progress', formatter(String(stdout))) // finally, emit
|
||||
//logger('download', `Fetching ${stdout}`)
|
||||
//logger('download', `Fetching ${_stdout}`)
|
||||
},
|
||||
complete: () => {
|
||||
socket.emit('progress', { status: 'Done!' })
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
ytldp.on('exit', () => {
|
||||
socket.emit('progress', { status: 'Done!' })
|
||||
logger('download', 'Done!')
|
||||
|
||||
deleteDownloadByPID(ytldp.pid).then(() => {
|
||||
logger('db', `Deleted ${ytldp.pid} because SIGKILL`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const getDownloadInfo = (socket, url) => {
|
||||
async function retriveDownload(socket) {
|
||||
const downloads = await pruneDownloads();
|
||||
|
||||
if (downloads.length > 0) {
|
||||
for (const _download of downloads) {
|
||||
await killProcess(_download.process_pid);
|
||||
await download(socket, _download.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getDownloadInfo(socket, url) {
|
||||
let stdoutChunks = [];
|
||||
const ytdlpInfo = spawn(`./lib/yt-dlp${isWindows ? '.exe' : ''}`, ['-s', '-j', url]);
|
||||
|
||||
ytdlpInfo.stdout.on('data', (data) => {
|
||||
stdoutChunks.push(data)
|
||||
})
|
||||
stdoutChunks.push(data);
|
||||
});
|
||||
|
||||
ytdlpInfo.on('exit', () => {
|
||||
const buffer = Buffer.concat(stdoutChunks)
|
||||
const json = JSON.parse(buffer.toString())
|
||||
socket.emit('info', json)
|
||||
try {
|
||||
const buffer = Buffer.concat(stdoutChunks);
|
||||
const json = JSON.parse(buffer.toString());
|
||||
socket.emit('info', json);
|
||||
} catch (e) {
|
||||
socket.emit('progress', { status: 'Aborted' });
|
||||
logger('download', 'Done!');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const abortDownload = (socket) => {
|
||||
function abortDownload(socket) {
|
||||
const res = process.platform === 'win32' ?
|
||||
spawn('taskkill', ['/IM', 'yt-dlp.exe', '/F', '/T']) :
|
||||
spawn('killall', ['yt-dlp'])
|
||||
spawn('killall', ['yt-dlp']);
|
||||
res.on('exit', () => {
|
||||
socket.emit('progress', 'Aborted!')
|
||||
logger('download', 'Aborting downloads')
|
||||
})
|
||||
socket.emit('progress', { status: 'Aborted' });
|
||||
logger('download', 'Aborting downloads');
|
||||
});
|
||||
}
|
||||
|
||||
const formatter = (stdout) => {
|
||||
const cleanStdout = stdout
|
||||
.replace(/\s\s+/g, ' ')
|
||||
.split(' ')
|
||||
const status = cleanStdout[0].replace(/\[|\]|\r/g, '')
|
||||
.split(' ');
|
||||
const status = cleanStdout[0].replace(/\[|\]|\r/g, '');
|
||||
switch (status) {
|
||||
case 'download':
|
||||
return {
|
||||
@@ -82,7 +114,7 @@ const formatter = (stdout) => {
|
||||
size: cleanStdout[3],
|
||||
dlSpeed: cleanStdout[5],
|
||||
}
|
||||
case 'merger':
|
||||
case 'merge':
|
||||
return {
|
||||
status: 'merging',
|
||||
progress: '100',
|
||||
@@ -95,4 +127,5 @@ const formatter = (stdout) => {
|
||||
module.exports = {
|
||||
download: download,
|
||||
abortDownload: abortDownload,
|
||||
retriveDownload: retriveDownload,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
const logger = (proto, args) => {
|
||||
console.log(`[${proto}] ${args}`)
|
||||
console.log(`[${proto}]\t${args}`)
|
||||
}
|
||||
|
||||
module.exports = logger
|
||||
const splash = () => {
|
||||
console.log("-------------------------------------------------")
|
||||
console.log("yt-dlp-webUI - A web-ui for yt-dlp, simply enough")
|
||||
console.log("-------------------------------------------------")
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
logger: logger,
|
||||
splash: splash,
|
||||
}
|
||||
38
lib/procUtils.js
Normal file
38
lib/procUtils.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const net = require('net');
|
||||
const { logger } = require('./logger');
|
||||
|
||||
function existsInProc(pid) {
|
||||
try {
|
||||
return fs.statSync(`/proc/${pid}`)
|
||||
} catch (e) {
|
||||
logger('proc', `pid ${pid} not found in procfs`)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
function retriveStdoutFromProcFd(pid) {
|
||||
if (existsInProc(pid)) {
|
||||
const unixSocket = fs.readlinkSync(`/proc/${pid}/fd/1`).replace('socket:[', '127.0.0.1:').replace(']', '')
|
||||
if (unixSocket) {
|
||||
console.log(unixSocket)
|
||||
logger('proc', `found pending job on pid: ${pid} attached to UNIX socket: ${unixSocket}`)
|
||||
return net.createConnection(unixSocket)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
async function killProcess(pid) {
|
||||
const res = spawn('kill', [pid])
|
||||
res.on('exit', () => {
|
||||
logger('proc', `Successfully killed yt-dlp process, pid: ${pid}`)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
existsInProc: existsInProc,
|
||||
//retriveStdoutFromProcFd: retriveStdoutFromProcFd,
|
||||
killProcess: killProcess,
|
||||
}
|
||||
15
package.json
15
package.json
@@ -3,7 +3,7 @@
|
||||
"version": "1.0.0",
|
||||
"description": "A terrible webUI for yt-dlp, all-in-one solution.",
|
||||
"scripts": {
|
||||
"start": "node --harmony server.js",
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon app.js",
|
||||
"build": "parcel build ./frontend/index.html",
|
||||
"fe": "parcel ./frontend/index.html --open",
|
||||
@@ -13,16 +13,23 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@koa/cors": "^3.1.0",
|
||||
"better-sqlite3": "^7.4.5",
|
||||
"chart.js": "^3.6.0",
|
||||
"koa": "^2.13.4",
|
||||
"koa-static": "^5.0.0",
|
||||
"ordered-binary": "^1.2.1",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^2.0.2",
|
||||
"react-bootstrap": "2.0.2",
|
||||
"react-chartjs-2": "^4.0.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"rxjs": "^7.4.0",
|
||||
"socket.io": "^4.3.2",
|
||||
"socket.io-client": "^4.3.2"
|
||||
"socket.io-client": "^4.3.2",
|
||||
"sqlite": "^4.0.23",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"parcel": "^2.0.1"
|
||||
"parcel": "^2.0.1",
|
||||
"typescript": "^4.5.2"
|
||||
}
|
||||
}
|
||||
31
server.js
31
server.js
@@ -1,12 +1,13 @@
|
||||
const Koa = require('koa');
|
||||
const serve = require('koa-static');
|
||||
const path = require('path');
|
||||
const { Server } = require('socket.io');
|
||||
const { createServer } = require('http');
|
||||
const cors = require('@koa/cors');
|
||||
const logger = require('./lib/logger');
|
||||
const { download, abortDownload } = require('./lib/downloader');
|
||||
const { ytdlpUpdater } = require('./lib/updater');
|
||||
const Koa = require('koa'),
|
||||
serve = require('koa-static'),
|
||||
cors = require('@koa/cors'),
|
||||
{ logger, splash } = require('./lib/logger'),
|
||||
{ join } = require('path'),
|
||||
{ Server } = require('socket.io'),
|
||||
{ createServer } = require('http'),
|
||||
{ download, abortDownload, retriveDownload } = require('./lib/downloader'),
|
||||
{ ytdlpUpdater } = require('./lib/updater'),
|
||||
db = require('./lib/db');
|
||||
|
||||
const app = new Koa()
|
||||
const server = createServer(app.callback())
|
||||
@@ -30,6 +31,12 @@ io.on('connection', socket => {
|
||||
socket.on('update-bin', () => {
|
||||
ytdlpUpdater(socket)
|
||||
})
|
||||
socket.on('fetch-jobs', async () => {
|
||||
socket.emit('pending-jobs', await db.pruneDownloads())
|
||||
})
|
||||
socket.on('retrieve-jobs', async () => {
|
||||
retriveDownload(socket)
|
||||
})
|
||||
})
|
||||
|
||||
io.on('disconnect', (socket) => {
|
||||
@@ -38,8 +45,10 @@ io.on('disconnect', (socket) => {
|
||||
|
||||
app
|
||||
.use(cors())
|
||||
.use(serve(path.join(__dirname, 'dist')))
|
||||
.use(serve(join(__dirname, 'dist')))
|
||||
|
||||
splash()
|
||||
logger('koa', `Server started on port ${process.env.PORT || 3022}`)
|
||||
|
||||
server.listen(process.env.PORT || 3022)
|
||||
db.init()
|
||||
.then(() => server.listen(process.env.PORT || 3022))
|
||||
Reference in New Issue
Block a user