server migration to TypeScript

This commit is contained in:
2022-01-30 00:53:08 +01:00
parent 7d745ec4cc
commit 9dcfade3fd
18 changed files with 346 additions and 133 deletions

View File

@@ -1,10 +1,11 @@
node_modules node_modules
dist dist
package-lock.json package-lock.json
pnpm-lock.yaml
.parcel-cache .parcel-cache
.git .git
server/*.exe src/server/core/*.exe
server/yt-dlp src/server/core/yt-dlp
.env .env
*.mp4 *.mp4
*.ytdl *.ytdl

5
.gitignore vendored
View File

@@ -1,9 +1,10 @@
.parcel-cache .parcel-cache
dist dist
package-lock.json package-lock.json
pnpm-lock.yaml
node_modules node_modules
server/*.exe src/server/core/*.exe
server/yt-dlp src/server/core/yt-dlp
.env .env
*.mp4 *.mp4
*.ytdl *.ytdl

View File

@@ -8,9 +8,8 @@ RUN apt-get install curl ffmpeg -y
RUN apt-get install psmisc RUN apt-get install psmisc
RUN npm install RUN npm install
COPY . . COPY . .
RUN npm run build RUN chmod +x ./fetch-yt-dlp.sh
RUN chmod +x ./server/fetch-yt-dlp.sh RUN npm run build-all
RUN ./server/fetch-yt-dlp.sh && mv yt-dlp ./server
RUN rm -rf .parcel-cache RUN rm -rf .parcel-cache
EXPOSE 3022 EXPOSE 3022
CMD [ "node" , "./server.js" ] CMD [ "node" , "./dist/main.js" ]

View File

@@ -1,18 +1,29 @@
{ {
"name": "youtube-dlp-web", "name": "yt-dlp-webui",
"version": "1.0.0", "version": "1.1.0",
"description": "A terrible webUI for yt-dlp, all-in-one solution.", "description": "A terrible webUI for yt-dlp, all-in-one solution.",
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"dev": "nodemon app.js", "dev": "nodemon app.js",
"build": "parcel build ./frontend/index.html", "build": "parcel build ./frontend/index.html --dist-dir ./dist/frontend",
"build-server": "tsc --build",
"build-all": "tsc --build && npm run build && npm run fetch",
"clean": "tsc --build --clean",
"clean-all": "rm -r dist",
"fe": "parcel ./frontend/index.html --open", "fe": "parcel ./frontend/index.html --open",
"fetch": "./server/fetch-yt-dlp.sh && mv yt-dlp ./server" "fetch-dev": "./fetch-yt-dlp.sh && mv yt-dlp ./server/core",
"fetch": "./fetch-yt-dlp.sh && mv yt-dlp ./dist/core"
}, },
"author": "marcobaobao", "author": "marcobaobao",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@koa/cors": "^3.1.0", "@koa/cors": "^3.1.0",
"@types/better-sqlite3": "^7.4.2",
"@types/koa": "^2.13.4",
"@types/koa-static": "^4.0.2",
"@types/koa__cors": "^3.1.1",
"@types/node": "^17.0.13",
"@types/uuid": "^8.3.4",
"better-sqlite3": "^7.4.5", "better-sqlite3": "^7.4.5",
"chart.js": "^3.6.0", "chart.js": "^3.6.0",
"koa": "^2.13.4", "koa": "^2.13.4",

148
pnpm-lock.yaml generated
View File

@@ -2,6 +2,12 @@ lockfileVersion: 5.3
specifiers: specifiers:
'@koa/cors': ^3.1.0 '@koa/cors': ^3.1.0
'@types/better-sqlite3': ^7.4.2
'@types/koa': ^2.13.4
'@types/koa-static': ^4.0.2
'@types/koa__cors': ^3.1.1
'@types/node': ^17.0.13
'@types/uuid': ^8.3.4
better-sqlite3: ^7.4.5 better-sqlite3: ^7.4.5
chart.js: ^3.6.0 chart.js: ^3.6.0
koa: ^2.13.4 koa: ^2.13.4
@@ -22,6 +28,12 @@ specifiers:
dependencies: dependencies:
'@koa/cors': 3.1.0 '@koa/cors': 3.1.0
'@types/better-sqlite3': 7.4.2
'@types/koa': 2.13.4
'@types/koa-static': 4.0.2
'@types/koa__cors': 3.1.1
'@types/node': 17.0.13
'@types/uuid': 8.3.4
better-sqlite3: 7.5.0 better-sqlite3: 7.5.0
chart.js: 3.7.0 chart.js: 3.7.0
koa: 2.13.4 koa: 2.13.4
@@ -1162,30 +1174,139 @@ packages:
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
dev: true dev: true
/@types/accepts/1.3.5:
resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==}
dependencies:
'@types/node': 17.0.13
dev: false
/@types/better-sqlite3/7.4.2:
resolution: {integrity: sha512-HUXWMOmRgOrXJ0SKt6kxqUaZtGkr0HCuaEt/76LojT6bkTu0lb0uhr3K1su9T09mskDKyQwNMvT7WithFN10PQ==}
dependencies:
'@types/node': 17.0.13
dev: false
/@types/body-parser/1.19.2:
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
dependencies:
'@types/connect': 3.4.35
'@types/node': 17.0.13
dev: false
/@types/component-emitter/1.2.11: /@types/component-emitter/1.2.11:
resolution: {integrity: sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==} resolution: {integrity: sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==}
dev: false dev: false
/@types/connect/3.4.35:
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
dependencies:
'@types/node': 17.0.13
dev: false
/@types/content-disposition/0.5.4:
resolution: {integrity: sha512-0mPF08jn9zYI0n0Q/Pnz7C4kThdSt+6LD4amsrYDDpgBfrVWa3TcCOxKX1zkGgYniGagRv8heN2cbh+CAn+uuQ==}
dev: false
/@types/cookie/0.4.1: /@types/cookie/0.4.1:
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
dev: false dev: false
/@types/cookies/0.7.7:
resolution: {integrity: sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==}
dependencies:
'@types/connect': 3.4.35
'@types/express': 4.17.13
'@types/keygrip': 1.0.2
'@types/node': 17.0.13
dev: false
/@types/cors/2.8.12: /@types/cors/2.8.12:
resolution: {integrity: sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==} resolution: {integrity: sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==}
dev: false dev: false
/@types/express-serve-static-core/4.17.28:
resolution: {integrity: sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==}
dependencies:
'@types/node': 17.0.13
'@types/qs': 6.9.7
'@types/range-parser': 1.2.4
dev: false
/@types/express/4.17.13:
resolution: {integrity: sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==}
dependencies:
'@types/body-parser': 1.19.2
'@types/express-serve-static-core': 4.17.28
'@types/qs': 6.9.7
'@types/serve-static': 1.13.10
dev: false
/@types/http-assert/1.5.3:
resolution: {integrity: sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==}
dev: false
/@types/http-errors/1.8.2:
resolution: {integrity: sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==}
dev: false
/@types/http-proxy/1.17.8: /@types/http-proxy/1.17.8:
resolution: {integrity: sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA==} resolution: {integrity: sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA==}
dependencies: dependencies:
'@types/node': 17.0.12 '@types/node': 17.0.13
dev: true dev: true
/@types/invariant/2.2.35: /@types/invariant/2.2.35:
resolution: {integrity: sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==} resolution: {integrity: sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==}
dev: false dev: false
/@types/node/17.0.12: /@types/keygrip/1.0.2:
resolution: {integrity: sha512-4YpbAsnJXWYK/fpTVFlMIcUIho2AYCi4wg5aNPrG1ng7fn/1/RZfCIpRCiBX+12RVa34RluilnvCqD+g3KiSiA==} resolution: {integrity: sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==}
dev: false
/@types/koa-compose/3.2.5:
resolution: {integrity: sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==}
dependencies:
'@types/koa': 2.13.4
dev: false
/@types/koa-send/4.1.3:
resolution: {integrity: sha512-daaTqPZlgjIJycSTNjKpHYuKhXYP30atFc1pBcy6HHqB9+vcymDgYTguPdx9tO4HMOqNyz6bz/zqpxt5eLR+VA==}
dependencies:
'@types/koa': 2.13.4
dev: false
/@types/koa-static/4.0.2:
resolution: {integrity: sha512-ns/zHg+K6XVPMuohjpOlpkR1WLa4VJ9czgUP9bxkCDn0JZBtUWbD/wKDZzPGDclkQK1bpAEScufCHOy8cbfL0w==}
dependencies:
'@types/koa': 2.13.4
'@types/koa-send': 4.1.3
dev: false
/@types/koa/2.13.4:
resolution: {integrity: sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==}
dependencies:
'@types/accepts': 1.3.5
'@types/content-disposition': 0.5.4
'@types/cookies': 0.7.7
'@types/http-assert': 1.5.3
'@types/http-errors': 1.8.2
'@types/keygrip': 1.0.2
'@types/koa-compose': 3.2.5
'@types/node': 17.0.13
dev: false
/@types/koa__cors/3.1.1:
resolution: {integrity: sha512-O7MBkCocnLrpEvkMrYAp17arUDS+KuS5bXMG/Z4aPSbrO7vrYB6YrqcsTD3Dp2OnAL3j4WME2k/x2kOcyzwNUw==}
dependencies:
'@types/koa': 2.13.4
dev: false
/@types/mime/1.3.2:
resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==}
dev: false
/@types/node/17.0.13:
resolution: {integrity: sha512-Y86MAxASe25hNzlDbsviXl8jQHb0RDvKt4c40ZJQ1Don0AAL0STLZSs4N+6gLEO55pedy7r2cLwS+ZDxPm/2Bw==}
/@types/parse-json/4.0.0: /@types/parse-json/4.0.0:
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
@@ -1195,6 +1316,14 @@ packages:
resolution: {integrity: sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==} resolution: {integrity: sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==}
dev: false dev: false
/@types/qs/6.9.7:
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
dev: false
/@types/range-parser/1.2.4:
resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==}
dev: false
/@types/react-transition-group/4.4.4: /@types/react-transition-group/4.4.4:
resolution: {integrity: sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==} resolution: {integrity: sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==}
dependencies: dependencies:
@@ -1213,6 +1342,17 @@ packages:
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
dev: false dev: false
/@types/serve-static/1.13.10:
resolution: {integrity: sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==}
dependencies:
'@types/mime': 1.3.2
'@types/node': 17.0.13
dev: false
/@types/uuid/8.3.4:
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
dev: false
/@types/warning/3.0.0: /@types/warning/3.0.0:
resolution: {integrity: sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=} resolution: {integrity: sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=}
dev: false dev: false
@@ -2101,7 +2241,7 @@ packages:
dependencies: dependencies:
'@types/cookie': 0.4.1 '@types/cookie': 0.4.1
'@types/cors': 2.8.12 '@types/cors': 2.8.12
'@types/node': 17.0.12 '@types/node': 17.0.13
accepts: 1.3.7 accepts: 1.3.7
base64id: 2.0.0 base64id: 2.0.0
cookie: 0.4.1 cookie: 0.4.1

View File

@@ -1,6 +1,10 @@
const { spawn } = require('child_process'); import { spawn } from 'child_process';
const { deleteDownloadByPID, insertDownload } = require('./db'); import { join } from 'path';
const { logger } = require('./logger'); import { Readable } from 'stream';
import { deleteDownloadByPID, insertDownload } from '../db/db';
import Logger from '../utils/BetterLogger';
const log = new Logger();
/** /**
* Represents a download process that spawns yt-dlp. * Represents a download process that spawns yt-dlp.
@@ -11,9 +15,18 @@ const { logger } = require('./logger');
*/ */
class Process { class Process {
constructor(url, params, settings) { private url: string;
private params: Array<string>;
private settings: any;
private stdout: Readable;
private pid: number;
private info: any;
private lock: boolean;
private exePath = join(__dirname, 'yt-dlp');
constructor(url: string, params: Array<string>, settings: any) {
this.url = url; this.url = url;
this.params = params || ' '; this.params = params || [];
this.settings = settings this.settings = settings
this.stdout = undefined; this.stdout = undefined;
this.pid = undefined; this.pid = undefined;
@@ -25,10 +38,10 @@ class Process {
* @param {Function} callback not yet implemented * @param {Function} callback not yet implemented
* @returns {Promise<this>} the process instance * @returns {Promise<this>} the process instance
*/ */
async start(callback) { async start(callback?: Function): Promise<this> {
await this.#__internalGetInfo(); await this.#__internalGetInfo();
const ytldp = spawn('./server/yt-dlp', const ytldp = spawn(this.exePath,
['-o', `${this.settings?.download_path || 'downloads/'}%(title)s.%(ext)s`] ['-o', `${this.settings?.download_path || 'downloads/'}%(title)s.%(ext)s`]
.concat(this.params) .concat(this.params)
.concat([this.url]) .concat([this.url])
@@ -37,9 +50,9 @@ class Process {
this.pid = ytldp.pid; this.pid = ytldp.pid;
this.stdout = ytldp.stdout; this.stdout = ytldp.stdout;
logger('proc', `Spawned a new process, pid: ${this.pid}`) log.info('proc', `Spawned a new process, pid: ${this.pid}`)
await insertDownload(this.url, null, null, null, this.pid); await insertDownload(this.url, this.info?.title, this.info?.thumbnail, null, this.pid);
return this; return this;
} }
@@ -52,7 +65,7 @@ class Process {
async #__internalGetInfo() { async #__internalGetInfo() {
let lock = true; let lock = true;
let stdoutChunks = []; let stdoutChunks = [];
const ytdlpInfo = spawn('./server/yt-dlp', ['-s', '-j', this.url]); const ytdlpInfo = spawn(this.exePath, ['-s', '-j', this.url]);
ytdlpInfo.stdout.on('data', (data) => { ytdlpInfo.stdout.on('data', (data) => {
stdoutChunks.push(data); stdoutChunks.push(data);
@@ -82,9 +95,9 @@ class Process {
* function that kills the current process * function that kills the current process
*/ */
async kill() { async kill() {
spawn('kill', [this.pid]).on('exit', () => { spawn('kill', [String(this.pid)]).on('exit', () => {
deleteDownloadByPID(this.pid).then(() => { deleteDownloadByPID(this.pid).then(() => {
logger('db', `Deleted ${this.pid} because SIGKILL`) log.info('db', `Deleted ${this.pid} because SIGKILL`)
}) })
}); });
} }
@@ -93,7 +106,7 @@ class Process {
* pid getter function * pid getter function
* @returns {number} pid * @returns {number} pid
*/ */
getPid() { getPid(): number {
if (!this.pid) { if (!this.pid) {
throw "Process isn't started" throw "Process isn't started"
} }
@@ -102,9 +115,9 @@ class Process {
/** /**
* stdout getter function * stdout getter function
* @returns {ReadableStream} stdout as stream * @returns {Readable} stdout as stream
*/ */
getStdout() { getStdout(): Readable {
return this.stdout return this.stdout
} }
@@ -112,9 +125,9 @@ class Process {
* download info getter function * download info getter function
* @returns {object} * @returns {object}
*/ */
getInfo() { getInfo(): object {
return this.info return this.info
} }
} }
module.exports = Process; export default Process;

View File

@@ -3,7 +3,12 @@
* Represents a download process that spawns yt-dlp. * Represents a download process that spawns yt-dlp.
*/ */
import Process from "./Process";
class ProcessPool { class ProcessPool {
private _pool: Map<number, Process>;
private _size: number;
constructor() { constructor() {
this._pool = new Map(); this._pool = new Map();
this._size = 0; this._size = 0;
@@ -13,7 +18,7 @@ class ProcessPool {
* Pool size getter * Pool size getter
* @returns {number} pool's size * @returns {number} pool's size
*/ */
size() { size(): number {
return this._size; return this._size;
} }
@@ -21,7 +26,7 @@ class ProcessPool {
* Add a process to the pool * Add a process to the pool
* @param {Process} process * @param {Process} process
*/ */
add(process) { add(process: Process) {
this._pool.set(process.getPid(), process) this._pool.set(process.getPid(), process)
} }
@@ -29,7 +34,7 @@ class ProcessPool {
* Delete a process from the pool * Delete a process from the pool
* @param {Process} process * @param {Process} process
*/ */
remove(process) { remove(process: Process) {
this._pool.delete(process.getPid()) this._pool.delete(process.getPid())
} }
@@ -37,7 +42,7 @@ class ProcessPool {
* Delete a process from the pool by its pid * Delete a process from the pool by its pid
* @param {number} pid * @param {number} pid
*/ */
removeByPid(pid) { removeByPid(pid: number) {
this._pool.delete(pid) this._pool.delete(pid)
} }
@@ -45,7 +50,7 @@ class ProcessPool {
* get an iterator for the pool * get an iterator for the pool
* @returns {IterableIterator} iterator * @returns {IterableIterator} iterator
*/ */
iterator() { iterator(): IterableIterator<[number, Process]> {
return this._pool.entries() return this._pool.entries()
} }
@@ -54,9 +59,9 @@ class ProcessPool {
* @param {number} pid * @param {number} pid
* @returns {Process} * @returns {Process}
*/ */
getByPid(pid) { getByPid(pid: number): Process {
return this._pool.get(pid) return this._pool.get(pid)
} }
} }
module.exports = ProcessPool; export default ProcessPool;

View File

@@ -1,16 +1,18 @@
const { spawn } = require('child_process'); import { spawn } from 'child_process';
const { from, interval } = require('rxjs'); import { from, interval } from 'rxjs';
const { throttle } = require('rxjs/operators'); import { throttle } from 'rxjs/operators';
const { Socket } = require('socket.io'); import { pruneDownloads } from '../db/db';
const { pruneDownloads } = require('./db'); import { killProcess } from '../utils/procUtils';
const { logger } = require('./logger'); import Logger from '../utils/BetterLogger';
const Process = require('./Process'); import Process from './Process';
const ProcessPool = require('./ProcessPool'); import ProcessPool from './ProcessPool';
const { killProcess } = require('./procUtils'); import { Socket } from 'socket.io';
import { IPayload } from '../interfaces/IPayload';
// settings read from settings.json // settings read from settings.json
let settings; let settings;
let coldRestart = true; let coldRestart = true;
const log = new Logger();
const pool = new ProcessPool(); const pool = new ProcessPool();
@@ -28,7 +30,7 @@ catch (e) {
* @param {object} payload frontend download payload * @param {object} payload frontend download payload
* @returns * @returns
*/ */
async function download(socket, payload) { export async function download(socket: Socket, payload: IPayload) {
if (!payload || payload.url === '' || payload.url === null) { if (!payload || payload.url === '' || payload.url === null) {
socket.emit('progress', { status: 'Done!' }); socket.emit('progress', { status: 'Done!' });
return; return;
@@ -87,14 +89,14 @@ async function download(socket, payload) {
* @param {Socket} socket current connection socket * @param {Socket} socket current connection socket
* @returns * @returns
*/ */
async function retriveDownload(socket) { export async function retriveDownload(socket: Socket) {
// it's a cold restart: the server has just been started with pending // it's a cold restart: the server has just been started with pending
// downloads, so fetch them from the database and resume. // downloads, so fetch them from the database and resume.
if (coldRestart) { if (coldRestart) {
coldRestart = false; coldRestart = false;
let downloads = await pruneDownloads(); let downloads = await pruneDownloads();
downloads = [... new Set(downloads)]; downloads = [... new Set(downloads)];
logger('dl', `Cold restart, retrieving ${downloads.length} jobs`) log.info('dl', `Cold restart, retrieving ${downloads.length} jobs`)
for (const entry of downloads) { for (const entry of downloads) {
if (entry) { if (entry) {
await download(socket, entry); await download(socket, entry);
@@ -105,10 +107,10 @@ async function retriveDownload(socket) {
// it's an hot-reload the server it's running and the frontend ask for // 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) // the pending job: retrieve them from the "in-memory database" (ProcessPool)
logger('dl', `Retrieving jobs ${pool.size()} from pool`) log.info('dl', `Retrieving ${pool.size()} jobs from pool`)
const it = pool.iterator(); const it = pool.iterator();
tempWorkQueue = new Array(); const tempWorkQueue = new Array();
// sanitize // sanitize
for (const entry of it) { for (const entry of it) {
@@ -135,7 +137,7 @@ async function retriveDownload(socket) {
* @param {*} args args sent by the frontend. MUST contain the PID. * @param {*} args args sent by the frontend. MUST contain the PID.
* @returns * @returns
*/ */
function abortDownload(socket, args) { export function abortDownload(socket: Socket, args: any) {
if (!args) { if (!args) {
abortAllDownloads(socket); abortAllDownloads(socket);
return; return;
@@ -148,7 +150,7 @@ function abortDownload(socket, args) {
status: 'Aborted', status: 'Aborted',
process: pid, process: pid,
}); });
logger('dl', `Aborting download ${pid}`); log.warn('dl', `Aborting download ${pid}`);
}); });
} }
@@ -156,11 +158,11 @@ function abortDownload(socket, args) {
* Unconditionally kills all yt-dlp process. * Unconditionally kills all yt-dlp process.
* @param {Socket} socket currenct connection socket * @param {Socket} socket currenct connection socket
*/ */
function abortAllDownloads(socket) { export function abortAllDownloads(socket: Socket) {
spawn('killall', ['yt-dlp']) spawn('killall', ['yt-dlp'])
.on('exit', () => { .on('exit', () => {
socket.emit('progress', { status: 'Aborted' }); socket.emit('progress', { status: 'Aborted' });
logger('dl', 'Aborting downloads'); log.info('dl', 'Aborting downloads');
}); });
} }
@@ -170,7 +172,7 @@ function abortAllDownloads(socket) {
* @param {number} pid current process id relative to stdout * @param {number} pid current process id relative to stdout
* @returns * @returns
*/ */
const formatter = (stdout, pid) => { const formatter = (stdout: string, pid: number) => {
const cleanStdout = stdout const cleanStdout = stdout
.replace(/\s\s+/g, ' ') .replace(/\s\s+/g, ' ')
.split(' '); .split(' ');
@@ -193,10 +195,3 @@ const formatter = (stdout, pid) => {
return { progress: '0' } return { progress: '0' }
} }
} }
module.exports = {
download: download,
abortDownload: abortDownload,
abortAllDownloads: abortAllDownloads,
retriveDownload: retriveDownload,
}

View File

@@ -1,13 +1,14 @@
const uuid = require('uuid') import { v1 } from 'uuid';
const { logger } = require('./logger') import { existsInProc } from '../utils/procUtils';
const { existsInProc } = require('./procUtils') import Logger from '../utils/BetterLogger';
const db = require('better-sqlite3')('downloads.db');
const db = require('better-sqlite3')('downloads.db') const log = new Logger();
/** /**
* Inits the repository, the tables. * Inits the repository, the tables.
*/ */
async function init() { export async function init() {
try { try {
db.exec(`CREATE TABLE downloads ( db.exec(`CREATE TABLE downloads (
uid varchar(36) NOT NULL, uid varchar(36) NOT NULL,
@@ -20,7 +21,7 @@ async function init() {
PRIMARY KEY (uid) PRIMARY KEY (uid)
)`) )`)
} catch (e) { } catch (e) {
logger('db', 'Table already created, ignoring') log.warn('db', 'Table already created, ignoring')
} }
} }
@@ -28,7 +29,7 @@ async function init() {
* Get an instance of the db. * Get an instance of the db.
* @returns {BetterSqlite3.Database} Current database instance * @returns {BetterSqlite3.Database} Current database instance
*/ */
async function get_db() { export async function get_db(): Promise<any> {
return db return db
} }
@@ -41,8 +42,8 @@ async function get_db() {
* @param {number} PID the pid of the downloader * @param {number} PID the pid of the downloader
* @returns {Promise<string>} the download UUID * @returns {Promise<string>} the download UUID
*/ */
async function insertDownload(url, title, thumbnail, size, PID) { export async function insertDownload(url: string, title: string, thumbnail: string, size: string, PID: number): Promise<string> {
const uid = uuid.v1() const uid = v1()
try { try {
db db
.prepare(` .prepare(`
@@ -52,7 +53,7 @@ async function insertDownload(url, title, thumbnail, size, PID) {
) )
.run(uid, url, title, thumbnail, size, PID) .run(uid, url, title, thumbnail, size, PID)
} catch (error) { } catch (error) {
logger('db', 'some error occourred') log.err('db', 'some error occourred')
} }
return uid return uid
@@ -62,7 +63,7 @@ async function insertDownload(url, title, thumbnail, size, PID) {
* Retrieve all downloads from the database * Retrieve all downloads from the database
* @returns {ArrayLike} a collection of results * @returns {ArrayLike} a collection of results
*/ */
async function retrieveAll() { export async function retrieveAll(): Promise<any> {
return db return db
.prepare('SELECT * FROM downloads') .prepare('SELECT * FROM downloads')
.all() .all()
@@ -72,7 +73,7 @@ async function retrieveAll() {
* Delete a download by its uuid * Delete a download by its uuid
* @param {string} uid the to-be-deleted download uuid * @param {string} uid the to-be-deleted download uuid
*/ */
async function deleteDownloadById(uid) { export async function deleteDownloadById(uid: string) {
db.prepare(`DELETE FROM downloads WHERE uid=${uid}`).run() db.prepare(`DELETE FROM downloads WHERE uid=${uid}`).run()
} }
@@ -80,7 +81,7 @@ async function deleteDownloadById(uid) {
* Delete a download by its pid * Delete a download by its pid
* @param {string} pid the to-be-deleted download pid * @param {string} pid the to-be-deleted download pid
*/ */
async function deleteDownloadByPID(PID) { export async function deleteDownloadByPID(PID) {
db.prepare(`DELETE FROM downloads WHERE process_pid=${PID}`).run() db.prepare(`DELETE FROM downloads WHERE process_pid=${PID}`).run()
} }
@@ -88,7 +89,7 @@ async function deleteDownloadByPID(PID) {
* Deletes the downloads that aren't active anymore * Deletes the downloads that aren't active anymore
* @returns {Promise<ArrayLike>} * @returns {Promise<ArrayLike>}
*/ */
async function pruneDownloads() { export async function pruneDownloads(): Promise<any> {
const all = await retrieveAll() const all = await retrieveAll()
return all.map(job => { return all.map(job => {
if (existsInProc(job.process_pid)) { if (existsInProc(job.process_pid)) {
@@ -96,14 +97,4 @@ async function pruneDownloads() {
} }
deleteDownloadByPID(job.process_pid) deleteDownloadByPID(job.process_pid)
}) })
}
module.exports = {
init: init,
getDB: get_db,
insertDownload: insertDownload,
retrieveAll: retrieveAll,
deleteDownloadById: deleteDownloadById,
deleteDownloadByPID: deleteDownloadByPID,
pruneDownloads: pruneDownloads,
} }

View File

@@ -0,0 +1,7 @@
export interface IPayload {
url: string
params: Array<string> | string,
title?: string,
thumbnail?: string,
size?: string,
}

View File

View File

@@ -1,20 +1,17 @@
const Koa = require('koa'), import Koa from 'koa';
serve = require('koa-static'), import serve from 'koa-static';
cors = require('@koa/cors'), import cors from '@koa/cors';
{ logger, splash } = require('./server/logger'), import { logger, splash } from './utils/logger';
{ join } = require('path'), import { join } from 'path';
{ Server } = require('socket.io'), import { Server } from 'socket.io';
{ createServer } = require('http'), import { createServer } from 'http';
{ ytdlpUpdater } = require('./server/updater'), import { ytdlpUpdater } from './utils/updater';
{ import { download, abortDownload, retriveDownload, abortAllDownloads } from './core/downloader';
download, import Logger from './utils/BetterLogger';
abortDownload, import { retrieveAll, init } from './db/db';
retriveDownload,
abortAllDownloads,
} = require('./server/downloader'),
db = require('./server/db');
const app = new Koa() const app = new Koa()
const log = new Logger()
const server = createServer(app.callback()) const server = createServer(app.callback())
const io = new Server(server, { const io = new Server(server, {
cors: { cors: {
@@ -43,7 +40,7 @@ io.on('connection', socket => {
ytdlpUpdater(socket) ytdlpUpdater(socket)
}) })
socket.on('fetch-jobs', () => { socket.on('fetch-jobs', () => {
socket.emit('pending-jobs', db.retrieveAll()) socket.emit('pending-jobs', retrieveAll())
}) })
socket.on('retrieve-jobs', () => { socket.on('retrieve-jobs', () => {
retriveDownload(socket) retriveDownload(socket)
@@ -56,10 +53,11 @@ io.on('disconnect', (socket) => {
app app
.use(cors()) .use(cors())
.use(serve(join(__dirname, 'dist'))) .use(serve(join(__dirname, 'frontend')))
splash() splash()
logger('koa', `Server started on port ${process.env.PORT || 3022}`) log.info('koa', `Server started on port ${process.env.PORT || 3022}`)
db.init() init()
.then(() => server.listen(process.env.PORT || 3022)) .then(() => server.listen(process.env.PORT || 3022))
.catch(err => log.err('db', err))

View File

@@ -0,0 +1,46 @@
const ansi = {
reset: '\u001b[0m',
red: '\u001b[31m',
cyan: '\u001b[36m',
green: '\u001b[32m',
yellow: '\u001b[93m',
}
class Logger {
/**
* Print a standard info message
* @param {string} proto the context/protocol/section outputting the message
* @param {string} args the acutal message
*/
info(proto: string, args: string) {
process.stdout.write(
this.#__formatter(proto, args)
)
}
/**
* Print a warn message
* @param {string} proto the context/protocol/section outputting the message
* @param {string} args the acutal message
*/
warn(proto: string, args: string) {
process.stdout.write(
`${ansi.yellow}${this.#__formatter(proto, args)}${ansi.reset}`
)
}
/**
* Print an error message
* @param {string} proto the context/protocol/section outputting the message
* @param {string} args the acutal message
*/
err(proto: string, args: string) {
process.stdout.write(
`${ansi.red}${this.#__formatter(proto, args)}${ansi.reset}`
)
}
#__formatter(proto: any, args: any) {
return `[${proto}]\t${args}\n`
}
}
export default Logger;

View File

@@ -4,20 +4,15 @@
* @param {string} proto protocol * @param {string} proto protocol
* @param {string} args message * @param {string} args message
*/ */
const logger = (proto, args) => { export const logger = (proto: string, args: string) => {
console.log(`[${proto}]\t${args}`) console.log(`[${proto}]\t${args}`)
} }
/** /**
* CLI splash * CLI splash
*/ */
const splash = () => { export const splash = () => {
console.log("-------------------------------------------------") console.log("-------------------------------------------------")
console.log("yt-dlp-webUI - A web-ui for yt-dlp, simply enough") console.log("yt-dlp-webUI - A web-ui for yt-dlp, simply enough")
console.log("-------------------------------------------------") console.log("-------------------------------------------------")
}
module.exports = {
logger: logger,
splash: splash,
} }

View File

@@ -1,14 +1,14 @@
const { spawn } = require('child_process'); import { spawn } from 'child_process';
const fs = require('fs'); import fs = require('fs');
const net = require('net'); import net = require('net');
const { logger } = require('./logger'); import { logger } from './logger';
/** /**
* Browse /proc in order to find the specific pid * Browse /proc in order to find the specific pid
* @param {number} pid * @param {number} pid
* @returns {*} process stats if any * @returns {*} process stats if any
*/ */
function existsInProc(pid) { export function existsInProc(pid: number): any {
try { try {
return fs.statSync(`/proc/${pid}`) return fs.statSync(`/proc/${pid}`)
} catch (e) { } catch (e) {
@@ -33,15 +33,9 @@ function retriveStdoutFromProcFd(pid) {
* Kills a process with a sys-call * Kills a process with a sys-call
* @param {number} pid the killed process pid * @param {number} pid the killed process pid
*/ */
async function killProcess(pid) { export async function killProcess(pid: number) {
const res = spawn('kill', [pid]) const res = spawn('kill', [String(pid)])
res.on('exit', () => { res.on('exit', () => {
logger('proc', `Successfully killed yt-dlp process, pid: ${pid}`) logger('proc', `Successfully killed yt-dlp process, pid: ${pid}`)
}) })
} }
module.exports = {
existsInProc: existsInProc,
//retriveStdoutFromProcFd: retriveStdoutFromProcFd,
killProcess: killProcess,
}

View File

@@ -37,7 +37,7 @@ function buildDonwloadOptions(release) {
async function update() { async function update() {
// ensure that the binary has been removed // ensure that the binary has been removed
try { try {
fs.rmSync(path.join(__dirname, 'yt-dlp')) fs.rmSync(path.join(__dirname, '..', 'core', 'yt-dlp'))
} }
catch (e) { catch (e) {
console.log('file not found!') console.log('file not found!')
@@ -70,11 +70,11 @@ function downloadBinary(url) {
if (res.statusCode === 301 || res.statusCode === 302) { if (res.statusCode === 301 || res.statusCode === 302) {
return downloadBinary(res.headers.location) return downloadBinary(res.headers.location)
} }
let bin = fs.createWriteStream(path.join(__dirname, 'yt-dlp')) let bin = fs.createWriteStream(path.join(__dirname, '..', 'core', 'yt-dlp'))
res.pipe(bin) res.pipe(bin)
// once the connection has ended make the file executable // once the connection has ended make the file executable
res.on('end', () => { res.on('end', () => {
fs.chmod(path.join(__dirname, 'yt-dlp'), 0o775, err => { fs.chmod(path.join(__dirname, '..', 'core', 'yt-dlp'), 0o775, err => {
err ? console.error('failed updating!') : console.log('done!') err ? console.error('failed updating!') : console.log('done!')
}) })
}) })

17
tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"outDir": "./dist",
"allowJs": true,
"target": "ES2018",
"module": "commonjs",
"esModuleInterop": true,
"strict": false,
"noEmit": false
},
"exclude": [
"node_modules"
],
"include": [
"./server/src/**/*"
]
}