Code refactoring
This commit is contained in:
20
README.md
20
README.md
@@ -9,6 +9,12 @@ Developed to be as lightweight as possible (because my server is basically an in
|
|||||||
|
|
||||||
The bottleneck remains yt-dlp startup time (until yt-dlp will provide a rpc interface).
|
The bottleneck remains yt-dlp startup time (until yt-dlp will provide a rpc interface).
|
||||||
|
|
||||||
|
**I strongly recomend the ghrc build instead of docker hub one.**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker pull ghcr.io/marcopeocchi/yt-dlp-web-ui:master
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Changelog:
|
Changelog:
|
||||||
@@ -86,11 +92,17 @@ Future releases will have:
|
|||||||
|
|
||||||
## Docker installation
|
## Docker installation
|
||||||
```shell
|
```shell
|
||||||
docker pull marcobaobao/yt-dlp-webui:latest #x86 only
|
# recomended for ARM and x86 devices
|
||||||
# or alternatively for ARM and x86 devices docker pull ghcr.io/marcopeocchi/yt-dlp-web-ui:master
|
docker pull ghcr.io/marcopeocchi/yt-dlp-web-ui:master
|
||||||
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 ghcr.io/marcopeocchi/yt-dlp-web-ui:master
|
||||||
|
|
||||||
|
# or even
|
||||||
|
docker pull ghcr.io/marcopeocchi/yt-dlp-web-ui:master
|
||||||
|
docker create --name yt-dlp-webui -p 8082:3022 -v <your dir>:/usr/src/yt-dlp-webui/ ghcr.io/marcopeocchi/yt-dlp-web-ui:master
|
||||||
```
|
```
|
||||||
|
|
||||||
or
|
or
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker build -t yt-dlp-webui .
|
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
|
||||||
@@ -103,7 +115,7 @@ docker run -d -p 3022:3022 -v <your dir>:/usr/src/yt-dlp-webui/downloads yt-dlp-
|
|||||||
npm i
|
npm i
|
||||||
npm run build-all
|
npm run build-all
|
||||||
|
|
||||||
# edit the settings.json specifying the download path or
|
# edit the settings.json specifying port and download path or
|
||||||
# it will default to the following created folder
|
# it will default to the following created folder
|
||||||
|
|
||||||
mkdir downloads
|
mkdir downloads
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { useDispatch, useSelector } from "react-redux";
|
|||||||
import { Socket } from "socket.io-client";
|
import { Socket } from "socket.io-client";
|
||||||
import { StackableResult } from "./components/StackableResult";
|
import { StackableResult } from "./components/StackableResult";
|
||||||
import { connected, downloading, finished } from "./features/status/statusSlice";
|
import { connected, downloading, finished } from "./features/status/statusSlice";
|
||||||
import { IDLInfo, IDLInfoBase, IDownloadInfo, IMessage } from "./interfaces";
|
import { IDLMetadata, IDLMetadataAndPID, IMessage } from "./interfaces";
|
||||||
import { RootState } from "./stores/store";
|
import { RootState } from "./stores/store";
|
||||||
import { isValidURL, toFormatArgs, updateInStateMap, } from "./utils";
|
import { isValidURL, toFormatArgs, updateInStateMap, } from "./utils";
|
||||||
import { FileUpload } from "@mui/icons-material";
|
import { FileUpload } from "@mui/icons-material";
|
||||||
@@ -37,8 +37,8 @@ export default function Home({ socket }: Props) {
|
|||||||
// ephemeral state
|
// ephemeral state
|
||||||
const [progressMap, setProgressMap] = useState(new Map<number, number>());
|
const [progressMap, setProgressMap] = useState(new Map<number, number>());
|
||||||
const [messageMap, setMessageMap] = useState(new Map<number, IMessage>());
|
const [messageMap, setMessageMap] = useState(new Map<number, IMessage>());
|
||||||
const [downloadInfoMap, setDownloadInfoMap] = useState(new Map<number, IDLInfoBase>());
|
const [downloadInfoMap, setDownloadInfoMap] = useState(new Map<number, IDLMetadata>());
|
||||||
const [downloadFormats, setDownloadFormats] = useState<IDownloadInfo>();
|
const [downloadFormats, setDownloadFormats] = useState<IDLMetadata>();
|
||||||
const [pickedVideoFormat, setPickedVideoFormat] = useState('');
|
const [pickedVideoFormat, setPickedVideoFormat] = useState('');
|
||||||
const [pickedAudioFormat, setPickedAudioFormat] = useState('');
|
const [pickedAudioFormat, setPickedAudioFormat] = useState('');
|
||||||
const [pickedBestFormat, setPickedBestFormat] = useState('');
|
const [pickedBestFormat, setPickedBestFormat] = useState('');
|
||||||
@@ -68,7 +68,7 @@ export default function Home({ socket }: Props) {
|
|||||||
|
|
||||||
/* Handle download information sent by server */
|
/* Handle download information sent by server */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on('available-formats', (data: IDownloadInfo) => {
|
socket.on('available-formats', (data: IDLMetadata) => {
|
||||||
setShowBackdrop(false)
|
setShowBackdrop(false)
|
||||||
setDownloadFormats(data);
|
setDownloadFormats(data);
|
||||||
})
|
})
|
||||||
@@ -76,10 +76,10 @@ export default function Home({ socket }: Props) {
|
|||||||
|
|
||||||
/* Handle download information sent by server */
|
/* Handle download information sent by server */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on('info', (data: IDLInfo) => {
|
socket.on('metadata', (data: IDLMetadataAndPID) => {
|
||||||
setShowBackdrop(false)
|
setShowBackdrop(false)
|
||||||
dispatch(downloading())
|
dispatch(downloading())
|
||||||
updateInStateMap<number, IDLInfoBase>(data.pid, data.info, downloadInfoMap, setDownloadInfoMap);
|
updateInStateMap<number, IDLMetadata>(data.pid, data.metadata, downloadInfoMap, setDownloadInfoMap);
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -374,9 +374,9 @@ export default function Home({ socket }: Props) {
|
|||||||
formattedLog={message[1]}
|
formattedLog={message[1]}
|
||||||
title={downloadInfoMap.get(message[0])?.title ?? ''}
|
title={downloadInfoMap.get(message[0])?.title ?? ''}
|
||||||
thumbnail={downloadInfoMap.get(message[0])?.thumbnail ?? ''}
|
thumbnail={downloadInfoMap.get(message[0])?.thumbnail ?? ''}
|
||||||
resolution={downloadInfoMap.get(message[0])?.resolution ?? '...'}
|
|
||||||
progress={progressMap.get(message[0]) ?? 0}
|
progress={progressMap.get(message[0]) ?? 0}
|
||||||
stopCallback={() => abort(message[0])}
|
stopCallback={() => abort(message[0])}
|
||||||
|
resolution={''}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -6,22 +6,14 @@ export interface IMessage {
|
|||||||
pid: number
|
pid: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDLInfoBase {
|
export interface IDLMetadata {
|
||||||
title: string,
|
formats: Array<IDLFormat>,
|
||||||
thumbnail: string,
|
best: IDLFormat,
|
||||||
upload_date?: string | Date,
|
|
||||||
duration?: number
|
|
||||||
resolution?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IDownloadInfo {
|
|
||||||
formats: Array<IDownloadInfoSection>,
|
|
||||||
best: IDownloadInfoSection,
|
|
||||||
thumbnail: string,
|
thumbnail: string,
|
||||||
title: string,
|
title: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDownloadInfoSection {
|
export interface IDLFormat {
|
||||||
format_id: string,
|
format_id: string,
|
||||||
format_note: string,
|
format_note: string,
|
||||||
fps: number,
|
fps: number,
|
||||||
@@ -30,9 +22,9 @@ export interface IDownloadInfoSection {
|
|||||||
acodec: string,
|
acodec: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDLInfo {
|
export interface IDLMetadataAndPID {
|
||||||
pid: number,
|
pid: number,
|
||||||
info: IDLInfoBase
|
metadata: IDLMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDLSpeed {
|
export interface IDLSpeed {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Readable } from 'stream';
|
|||||||
import { ISettings } from '../interfaces/ISettings';
|
import { ISettings } from '../interfaces/ISettings';
|
||||||
import { availableParams } from '../utils/params';
|
import { availableParams } from '../utils/params';
|
||||||
import Logger from '../utils/BetterLogger';
|
import Logger from '../utils/BetterLogger';
|
||||||
|
import { IDownloadFormat, IDownloadMetadata } from '../interfaces/IDownloadMetadata';
|
||||||
|
|
||||||
const log = Logger.instance;
|
const log = Logger.instance;
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ class Process {
|
|||||||
private settings: ISettings;
|
private settings: ISettings;
|
||||||
private stdout: Readable;
|
private stdout: Readable;
|
||||||
private pid: number;
|
private pid: number;
|
||||||
private metadata?: IDownloadInfo;
|
private metadata?: IDownloadMetadata;
|
||||||
private exePath = join(__dirname, 'yt-dlp');
|
private exePath = join(__dirname, 'yt-dlp');
|
||||||
|
|
||||||
constructor(url: string, params: Array<string>, settings: any) {
|
constructor(url: string, params: Array<string>, settings: any) {
|
||||||
@@ -59,7 +60,7 @@ class Process {
|
|||||||
* function used internally by the download process to fetch information, usually thumbnail and title
|
* function used internally by the download process to fetch information, usually thumbnail and title
|
||||||
* @returns Promise to the lock
|
* @returns Promise to the lock
|
||||||
*/
|
*/
|
||||||
public getInfo(): Promise<IDownloadInfo> {
|
public getMetadata(): Promise<IDownloadMetadata> {
|
||||||
if (!this.metadata) {
|
if (!this.metadata) {
|
||||||
let stdoutChunks = [];
|
let stdoutChunks = [];
|
||||||
const ytdlpInfo = spawn(this.exePath, ['-j', this.url]);
|
const ytdlpInfo = spawn(this.exePath, ['-j', this.url]);
|
||||||
@@ -74,7 +75,7 @@ class Process {
|
|||||||
const buffer = Buffer.concat(stdoutChunks);
|
const buffer = Buffer.concat(stdoutChunks);
|
||||||
const json = JSON.parse(buffer.toString());
|
const json = JSON.parse(buffer.toString());
|
||||||
const info = {
|
const info = {
|
||||||
formats: json.formats.map((format: IDownloadInfoSection) => {
|
formats: json.formats.map((format: IDownloadFormat) => {
|
||||||
return {
|
return {
|
||||||
format_id: format.format_id ?? '',
|
format_id: format.format_id ?? '',
|
||||||
format_note: format.format_note ?? '',
|
format_note: format.format_note ?? '',
|
||||||
@@ -83,7 +84,7 @@ class Process {
|
|||||||
vcodec: format.vcodec ?? '',
|
vcodec: format.vcodec ?? '',
|
||||||
acodec: format.acodec ?? '',
|
acodec: format.acodec ?? '',
|
||||||
}
|
}
|
||||||
}).filter((format: IDownloadInfoSection) => format.format_note !== 'storyboard'),
|
}).filter((format: IDownloadFormat) => format.format_note !== 'storyboard'),
|
||||||
best: {
|
best: {
|
||||||
format_id: json.format_id ?? '',
|
format_id: json.format_id ?? '',
|
||||||
format_note: json.format_note ?? '',
|
format_note: json.format_note ?? '',
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ catch (e) {
|
|||||||
* @param socket
|
* @param socket
|
||||||
* @param url
|
* @param url
|
||||||
*/
|
*/
|
||||||
export async function getFormatsAndInfo(socket: Socket, url: string) {
|
export async function getFormatsAndMetadata(socket: Socket, url: string) {
|
||||||
let p = new Process(url, [], settings);
|
let p = new Process(url, [], settings);
|
||||||
const formats = await p.getInfo();
|
const formats = await p.getMetadata();
|
||||||
socket.emit('available-formats', formats)
|
socket.emit('available-formats', formats)
|
||||||
p = null;
|
p = null;
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ export async function download(socket: Socket, payload: IPayload) {
|
|||||||
|
|
||||||
p.start().then(downloader => {
|
p.start().then(downloader => {
|
||||||
mem_db.add(downloader)
|
mem_db.add(downloader)
|
||||||
displayDownloadInfo(downloader, socket);
|
displayDownloadMetadata(downloader, socket);
|
||||||
streamProcess(downloader, socket);
|
streamProcess(downloader, socket);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -66,11 +66,11 @@ export async function download(socket: Socket, payload: IPayload) {
|
|||||||
* @param process
|
* @param process
|
||||||
* @param socket
|
* @param socket
|
||||||
*/
|
*/
|
||||||
function displayDownloadInfo(process: Process, socket: Socket) {
|
function displayDownloadMetadata(process: Process, socket: Socket) {
|
||||||
process.getInfo().then(info => {
|
process.getMetadata().then(metadata => {
|
||||||
socket.emit('info', {
|
socket.emit('metadata', {
|
||||||
pid: process.getPid(),
|
pid: process.getPid(),
|
||||||
info: info
|
metadata: metadata,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -87,11 +87,8 @@ function streamProcess(process: Process, socket: Socket) {
|
|||||||
pid: process.getPid(),
|
pid: process.getPid(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const stdout = process.getStdout()
|
|
||||||
|
|
||||||
stdout.removeAllListeners()
|
from(process.getStdout().removeAllListeners()) // stdout as observable
|
||||||
|
|
||||||
from(stdout) // stdout as observable
|
|
||||||
.pipe(
|
.pipe(
|
||||||
throttle(() => interval(500)), // discard events closer than 500ms
|
throttle(() => interval(500)), // discard events closer than 500ms
|
||||||
map(stdout => formatter(String(stdout), process.getPid()))
|
map(stdout => formatter(String(stdout), process.getPid()))
|
||||||
@@ -148,7 +145,7 @@ export async function retrieveDownload(socket: Socket) {
|
|||||||
// resume the jobs
|
// resume the jobs
|
||||||
for (const entry of it) {
|
for (const entry of it) {
|
||||||
const [, process] = entry
|
const [, process] = entry
|
||||||
displayDownloadInfo(process, socket);
|
displayDownloadMetadata(process, socket);
|
||||||
streamProcess(process, socket);
|
streamProcess(process, socket);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
interface IDownloadInfo {
|
export interface IDownloadMetadata {
|
||||||
formats: Array<IDownloadInfoSection>,
|
formats: Array<IDownloadFormat>,
|
||||||
best: IDownloadInfoSection,
|
best: IDownloadFormat,
|
||||||
thumbnail: string,
|
thumbnail: string,
|
||||||
title: string,
|
title: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IDownloadInfoSection {
|
export interface IDownloadFormat {
|
||||||
format_id: string,
|
format_id: string,
|
||||||
format_note: string,
|
format_note: string,
|
||||||
fps: number,
|
fps: number,
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export interface ISettings {
|
export interface ISettings {
|
||||||
download_path: string,
|
download_path: string,
|
||||||
cliArgs?: string[],
|
cliArgs?: string[],
|
||||||
|
port?: number,
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ import { splash } from './utils/logger';
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { Server } from 'socket.io';
|
import { Server } from 'socket.io';
|
||||||
import { ytdlpUpdater } from './utils/updater';
|
import { ytdlpUpdater } from './utils/updater';
|
||||||
import { download, abortDownload, retrieveDownload, abortAllDownloads, getFormatsAndInfo } from './core/downloader';
|
import { download, abortDownload, retrieveDownload, abortAllDownloads, getFormatsAndMetadata } from './core/downloader';
|
||||||
import { getFreeDiskSpace } from './utils/procUtils';
|
import { getFreeDiskSpace } from './utils/procUtils';
|
||||||
import { listDownloaded } from './core/downloadArchive';
|
import { listDownloaded } from './core/downloadArchive';
|
||||||
import { createServer } from 'http';
|
import { createServer } from 'http';
|
||||||
@@ -12,6 +12,7 @@ import * as Router from 'koa-router';
|
|||||||
import * as serve from 'koa-static';
|
import * as serve from 'koa-static';
|
||||||
import * as cors from '@koa/cors';
|
import * as cors from '@koa/cors';
|
||||||
import Logger from './utils/BetterLogger';
|
import Logger from './utils/BetterLogger';
|
||||||
|
import { ISettings } from './interfaces/ISettings';
|
||||||
|
|
||||||
const app = new Koa();
|
const app = new Koa();
|
||||||
const server = createServer(app.callback());
|
const server = createServer(app.callback());
|
||||||
@@ -24,6 +25,14 @@ const io = new Server(server, {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let settings: ISettings;
|
||||||
|
|
||||||
|
try {
|
||||||
|
settings = require('../settings.json');
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('settings', 'file not found, ignore if using Docker');
|
||||||
|
}
|
||||||
|
|
||||||
// Koa routing
|
// Koa routing
|
||||||
router.get('/settings', (ctx, next) => {
|
router.get('/settings', (ctx, next) => {
|
||||||
ctx.redirect('/')
|
ctx.redirect('/')
|
||||||
@@ -49,7 +58,6 @@ router.get('/stream/:filepath', (ctx, next) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// WebSocket listeners
|
// WebSocket listeners
|
||||||
|
|
||||||
io.on('connection', socket => {
|
io.on('connection', socket => {
|
||||||
log.info('ws', `${socket.handshake.address} connected!`)
|
log.info('ws', `${socket.handshake.address} connected!`)
|
||||||
|
|
||||||
@@ -59,7 +67,7 @@ io.on('connection', socket => {
|
|||||||
})
|
})
|
||||||
socket.on('send-url-format-selection', (args) => {
|
socket.on('send-url-format-selection', (args) => {
|
||||||
log.info('ws', `Formats ${args?.url}`)
|
log.info('ws', `Formats ${args?.url}`)
|
||||||
if (args.url) getFormatsAndInfo(socket, args?.url)
|
if (args.url) getFormatsAndMetadata(socket, args?.url)
|
||||||
})
|
})
|
||||||
socket.on('abort', (args) => {
|
socket.on('abort', (args) => {
|
||||||
abortDownload(socket, args)
|
abortDownload(socket, args)
|
||||||
@@ -86,10 +94,10 @@ app.use(serve(join(__dirname, 'frontend')))
|
|||||||
app.use(cors())
|
app.use(cors())
|
||||||
app.use(router.routes())
|
app.use(router.routes())
|
||||||
|
|
||||||
server.listen(process.env.PORT || 3022)
|
server.listen(process.env.PORT || settings.port || 3022)
|
||||||
|
|
||||||
splash()
|
splash()
|
||||||
log.info('http', `Server started on port ${process.env.PORT || 3022}`)
|
log.info('http', `Server started on port ${process.env.PORT || settings.port || 3022}`)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup handler
|
* Cleanup handler
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"port": 0,
|
||||||
"download_path": "",
|
"download_path": "",
|
||||||
"cliArgs": []
|
"cliArgs": []
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user