diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..73ca78c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+node_modules
+tmp
+bilibili
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..8537251
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,12 @@
+FROM node:18.14
+EXPOSE 80
+RUN \
+ apt update && \
+ apt install -y git ffmpeg python && \
+ wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/bin/yt-dlp && \
+ chmod +x /usr/bin/yt-dlp && \
+ git clone https://github.com/develon2015/Youtube-dl-REST /Youtube-dl-REST && \
+ cd /Youtube-dl-REST && \
+ npm i
+WORKDIR /Youtube-dl-REST
+CMD npm run start
diff --git a/README.md b/README.md
index f92da69..a5e9e9f 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,28 @@
# Youtube-dl-REST
-## 概要
+通过本项目,您可以搭建一个网页,快速下载各种Youtube、Bili视频。
+在线地址:[https://y2b.455556.xyz](https://y2b.455556.xyz)
-通过本项目,您可以搭建一个网页,快速下载您中意的Youtube视频。
-在线地址:[https://y2b.123345.xyz](https://y2b.123345.xyz)
## 安装
+如果您使用docker,推荐使用以下命令运行本项目:
+
+```
+docker volume create vol
+docker run -it -d --name youtube-dl-rest -p 80:80 -v vol:/Youtube-dl-REST imgxx/youtube-dl-rest
+```
+
+你可能需要修改 config.json 、替换自己的 cookies.txt 等文件,然后重启容器:
+
+```
+vi /var/lib/docker/volumes/vol/_data/config.json
+vi /var/lib/docker/volumes/vol/_data/cookies.txt
+docker restart youtube-dl-rest
+```
+
+如果您不使用docker,则按以下步骤进行安装:
+
### 1.安装Node.js
以Ubuntu为例,使用snapd安装:
@@ -19,11 +35,11 @@ sudo snap install node --classic --channel=14
node -v
```
-### 2.安装[youtube-dl](https://github.com/ytdl-org/youtube-dl)和[FFmpeg](https://github.com/FFmpeg/FFmpeg)
+### 2.安装[yt-dlp](https://github.com/yt-dlp/yt-dlp)和[FFmpeg](https://github.com/yt-dlp/yt-dlp)
-确保`youtube-dl`命令和`ffmpeg`命令可用:
+确保`yt-dlp`命令和`ffmpeg`命令可用:
```
-sudo youtube-dl -U
+sudo yt-dlp -U
ffmpeg -version
```
@@ -44,3 +60,8 @@ npm start
```
+## Sponsors
+
+[](https://yxvm.com/)
+
+[NodeSupport](https://github.com/NodeSeekDev/NodeSupport) 赞助了本项目
diff --git a/config.json b/config.json
index 99c21ca..1d33fe5 100644
--- a/config.json
+++ b/config.json
@@ -1,8 +1,7 @@
{
- "address": "127.0.0.1",
- "port": 28888,
- "disk": "/dev/sda2",
+ "address": "0.0.0.0",
+ "port": 80,
"cookie": "cookies.txt",
"blacklist": "blacklist.txt",
- "mode": "演示模式"
+ "mode": "非演示模式(演示模式关闭转码功能)"
}
diff --git a/cookies.txt b/cookies.txt
new file mode 100644
index 0000000..b9b6654
--- /dev/null
+++ b/cookies.txt
@@ -0,0 +1,12 @@
+# Netscape HTTP Cookie File
+# This file is generated by youtube-dl. Do not edit.
+
+.youtube.com TRUE / TRUE 2146723198 CONSENT YES+JP.zh-CN+20161009-18-0
+.youtube.com TRUE / FALSE 1601903974 GPS 1
+.youtube.com TRUE / FALSE 1607086761 PREF f1=50000000&f6=8&hl=en
+.youtube.com TRUE / TRUE 1617428183 VISITOR_INFO1_LIVE O3iKRKMJC5k
+.youtube.com TRUE / TRUE 0 YSC IufRwdakY8s
+.youtube.com TRUE / TRUE 1664974174 __Secure-3PAPISID ZISjGgE8830AXgFi/AIxubKg_gvkT_hcQg
+.youtube.com TRUE / TRUE 1664974174 __Secure-3PSID 2AfUWLyxBONI8BjFvhuAig89T0Dhx0COSUkbHxkJJdfMFE5Dr4-z7hPHOVkqYWxgyT5geg.
+.youtube.com TRUE / FALSE 0 s_gl 1d69aac621b2f9c0a25dade722d6e24bcwIAAABVUw==
+.youtube.com TRUE / FALSE 0 wide 1
diff --git a/get-remote-ip.js b/get-remote-ip.js
deleted file mode 100755
index 0d4b791..0000000
--- a/get-remote-ip.js
+++ /dev/null
@@ -1,5 +0,0 @@
-function getRemoteIP(request) {
- return request.header('cf-connecting-ip') || '未知IP';
-}
-
-module.exports = getRemoteIP;
diff --git a/index.js b/index.js
index 2d05f0a..99e911f 100644
--- a/index.js
+++ b/index.js
@@ -1,13 +1,31 @@
+const DISABLE = 0
+
const express = require('express');
+const json = require('body-parser').json;
const child_process = require('child_process');
const worker_threads = require('worker_threads');
const fs = require('fs');
-const getRemoteIP = require('./get-remote-ip.js');
+const { getRemoteIP, getWebsiteUrl } = require('./utils.js');
+const https = require('https');
+const http = require('http');
-const config = require('./config.json');
+const config = require('./config.json'); // 加载配置文件
+/*======================================================================================
+main 主线程
+========================================================================================*/
function main() {
let app = new express();
+ app.use('/y2b', (req, res, next) => {
+ if (DISABLE) {
+ res.send({
+ success: false,
+ error: `暂停使用!`,
+ });
+ } else {
+ next();
+ }
+ });
app.use((req, res, next) => {
console.log(`${getRemoteIP(req)}\t=> ${req.url}`);
let isBlackIP = false;
@@ -27,71 +45,74 @@ function main() {
}
if (!isBlackIP) next();
});
- app.use('/', express.static(`${__dirname}/webapps`));
+ app.use('/', express.static(`${__dirname}/static`));
app.use('/file', (req, res, next) => {
console.log(`下载${req.url}`);
let info = fs.readFileSync(`${__dirname}/tmp/${req.url.replace(/\.\w+$/, '.info.json')}`).toString();
info = JSON.parse(info);
console.log({'标题': info.title}); // or 'fulltitle'
let ext = req.url.match(/.*(\.\w+)$/)[1];
- res.set({'Content-Disposition': `attachment; filename="${encodeURI(info.title + ext)}"; filename*=UTF-8''${encodeURI(info.title + ext)}`});
+ res.set({'Content-Disposition': `attachment; filename="${encodeURIComponent(info.title + ext)}"; filename*=UTF-8''${encodeURI(info.title + ext)}`});
next();
});
app.use('/file', express.static(`${__dirname}/tmp`));
app.use('/info', express.static(`${__dirname}/tmp`));
+ app.use('/bili_file', express.static(`${__dirname}/bilibili`));
- app.get('/youtube/parse', (req, res) => {
+ app.get('/y2b/parse', (req, res) => {
let url = req._parsedUrl.query;
+ url = decodeURIComponent(url.replace('y2b', 'youtube').replace('y2', 'youtu')); // "链接已重置"大套餐
console.log({ op: '解析', url });
- let mr = url.match(/^https?:\/\/(?:youtu.be\/|(?:www|m).youtube.com\/watch\?v=)([\w-]{11})$/);
- if (!!!mr) {
+ let y2b = url.match(/^https?:\/\/(?:youtu.be\/|(?:www|m).youtube.com\/(?:watch|shorts)(?:\/|\?v=))([\w-]{11})$/);
+ let bilibili = url.match(/^https?:\/\/(?:www\.|m\.)?bilibili\.com\/video\/([\w\d]{11,14})\/?(?:\?p=(\d+))?$/);
+ let website;
+ switch (true) {
+ case y2b != null:
+ website = 'y2b';
+ break;
+ case bilibili != null:
+ website = 'bilibili';
+ break;
+ }
+ if (!!! website) {
console.log('reject');
res.send({
- "error": "请提供一个Youtube视频URL
例如:
https://www.youtube.com/watch?v=xxxxxxxxxxx",
+ "error": "请提供一个Youtube视频URL
例如:
https://youtu.be/xxxxxxxxxxx
https://www.bilibili.com/video/xx",
"success": false
});
return;
}
+ checkDisk(); // 解析视频前先检查磁盘空间
let thread = new worker_threads.Worker(__filename);
thread.once('message', msg => {
// console.log(JSON.stringify(msg, null, 1));
res.send(msg);
});
- thread.postMessage({ op: 'parse', url, videoID: mr[1] });
+ thread.postMessage({ op: 'parse', website, url, videoID: (y2b || bilibili)[1], p: bilibili?.[2] });
});
let queue = [];
- app.get('/youtube/download', (req, res) => {
- let { v, format, recode } = req.query;
- if (!!!v.match(/^[\w-]{11}$/))
+ app.get('/y2b/download', (req, res) => {
+ let { website, v, p, format, recode, subs } = req.query;
+ if (!!!v.match(/^[\w-]{11,14}$/))
return res.send({ "error": "Qurey参数v错误: 请提供一个正确的Video ID", "success": false });
- if (!!!format.match(/^(\d+)(?:x(\d+))?$/))
+ if (p && !!!p.match(/^[\d]+$/))
+ return res.send({ "error": "Qurey参数p错误: 请提供一个正确的Part number", "success": false });
+
+ if (!!!format.match(/^([\w\d-]+)(?:x([\w\d-]+))?$/))
return res.send({ "error": "Query参数format错误: 请求的音频和视频ID必须是数字, 合并格式为'视频IDx音频ID'", "success": false });
if (config.mode === '演示模式' && !!recode)
return res.send({ "error": "演示模式,关闭转码功能
本项目已使用Node.js重写
请克隆本项目后自行部署", "success": false });
+ if (subs && subs !== '' && !subs.match(/^([a-z]{2}(-[a-zA-Z]{2,4})?,?)+$/))
+ return res.send({ "error": "字幕不正确!", "success": false });
+
if (queue[JSON.stringify(req.query)] === undefined) {
- // 检查磁盘空间
- try {
- let df = child_process.execSync(`df -h '${config.disk}'`).toString();
- df.split('\n').forEach(it => {
- console.log({'空间': it});
- // /dev/sda2 39G 19G 19G 51% /
- let mr = it.match(/.*\s(\d+)%/);
- if (!!mr && Number.parseInt(mr[1]) > 90) {
- let cmd = `rm -r '${__dirname}/tmp'`;
- console.log({'清理空间': cmd});
- child_process.execSync(cmd);
- queue = [];;
- }
- });
- } catch(error) {
- //
- }
+ checkDisk(); // 下载视频前先检查磁盘空间
queue[JSON.stringify(req.query)] = {
"success": true,
@@ -111,106 +132,295 @@ function main() {
console.log(JSON.stringify(msg, null, 1));
queue[JSON.stringify(req.query)] = msg;
});
- thread.postMessage({ op: 'download', videoID: v, format, recode });
+ thread.postMessage({ op: 'download', website, videoID: v, p, format, recode, subs });
+ } // if end
+ // 发送轮询结果
+ res.send(queue[JSON.stringify(req.query)]);
+ }); // /youtube/download end
+
+ // API: 下载字幕
+ app.use(json());
+ app.post('/y2b/subtitle', (req, res) => {
+ let { website, id, p, locale, ext, type } = req.body;
+
+ if (!id.match(/^[\w-]{11,14}$/) ||
+ !ext.match(/^.(srt|ass|vtt|lrc|xml)$/) ||
+ !type.match(/^(auto|native)$/) ||
+ (p && !p.match(/^[\d]+$/)) ||
+ // !locale.match(/^([a-z]{2}(-[a-zA-Z]{2,4})?)+$/) ||
+ false
+ ) {
+ console.log('字幕请求预检被禁止, 可疑请求:', req.body);
+ res.send({ success: false });
+ return;
}
+ // checkDisk(); // 下载字幕前先检查磁盘空间
+ let thread = new worker_threads.Worker(__filename); // 启动子线程
+ thread.once('message', msg => {
+ let { title, filename, text } = msg;
+ // 下载字幕成功或失败
+ if (msg.success) {
+ console.log('字幕下载成功');
+ res.send({ success: true, title, filename, text });
+ } else {
+ console.log('字幕下载失败');
+ res.send({ success: false });
+ }
+ });
+ thread.postMessage({ op: 'subtitle', website, id, p, locale, ext, type });
+ }); // /youtube/subtitle end
- // console.log(queue);
- res.send(queue[JSON.stringify(req.query)]);
+ app.get('/pxy', (req, res) => {
+ let url = req.query.url;
+ if (!url.startsWith('https://i.ytimg.com/') && !url.match(/^https?:\/\/i\d\.hdslb\.com\//)) {
+ res.status(403).end();
+ return;
+ }
+ (url.startsWith('https://') ? https : http).get(url, (response) => {
+ res.writeHead(response.statusCode, response.statusMessage, response.headers);
+ response.pipe(res);
+ }).on('error', (err) => {
+ console.log(err);
+ res.status(502).end();
+ });
});
app.listen(config.port, config.address, () => {
console.log('服务已启动');
});
-}
+ /**
+ * 检测磁盘空间, 必要时清理空间并清空队列queue
+ */
+ function checkDisk() {
+ let content = fs.readFileSync(config.cookie).toString();
+ if (content.trim() == '') {
+ fs.rmSync(config.cookie);
+ }
+ try {
+ let df = child_process.execSync(`df -h .`).toString();
+ df.split('\n').forEach(it => {
+ console.log({ '空间': it });
+ // /dev/sda2 39G 19G 19G 51% /
+ let mr = it.match(/.*\s(\d+)%/);
+ if (!!mr && Number.parseInt(mr[1]) > 90) {
+ let cmd = `rm -r '${__dirname}/tmp' '${__dirname}/bilibili'`;
+ console.log({ '清理空间': cmd });
+ child_process.execSync(cmd);
+ queue = [];
+ }
+ });
+ } catch (error) {
+ //
+ }
+ } // checkDisk()
+} // main()
+
+
+
+/*======================================================================================
+Worker
+========================================================================================*/
function getAudio(id, format, rate, info, size) {
- return { id, format, rate, info, size };
+ return { id, format, rate: rate == 0 ? '未知' : rate, info, size: size == 0 ? '未知' : size };
}
function getVideo(id, format, scale, frame, rate, info, size) {
- return { id, format, scale, frame, rate, info, size };
+ return { id, format, scale, frame, rate: rate == 0 ? '未知' : rate, info, size: size == 0 ? '未知' : size };
+}
+
+/**
+ * 在以下形式的字符串中捕获字幕:
+ * Language Name Formats <= 返回0, 继续
+ * gu vtt, ttml, srv3, srv2, srv1
+ * zh-Hans vtt, ttml, srv3, srv2, srv1
+ * en English vtt, ttml, srv3, srv2, srv1, json3
+ * 其它形式一律视为终结符, 返回-1, 终结
+ * @param {String} line
+ */
+function catchSubtitle(line) {
+ if (line.match(/^Language .*/)) return 0;
+ let mr = line.match(/^(danmaku|[a-z]{2}(?:-[a-zA-Z]+)?).*/);
+ if (mr) return mr[1];
+ return -1;
+}
+
+/**
+ * 同步解析字幕
+ * @param {{ op: 'parse', url: String, videoID: String }} msg
+ */
+function parseSubtitle(msg) {
+ try {
+ let cmd = `yt-dlp --list-subs ${config.cookie !== undefined ? `--cookies "${config.cookie}"` : ''} '${msg.url}' 2> /dev/null`
+ console.log(`解析字幕, 命令: ${cmd}`);
+ let rs = child_process.execSync(cmd).toString().split(/(\r\n|\n)/);
+
+ /** 是否没有自动字幕 */
+ let noAutoSub = true;
+ let officialSub = [];
+
+ for (let i = 0; i < rs.length; i ++ ) {
+ if (rs[i].trim() === '' || rs[i].trim() === '\n') continue; // 空行直接忽略
+ // console.log('=> ', rs[i]);
+ // 排除一下连自动字幕都没有的, 那一定是没有任何字幕可用
+ if (rs[i].match(/.*Available automatic captions for .*?:/)) { // ?表示非贪婪, 遇到冒号即停止
+ noAutoSub = false; // 排除即可, 全都是把整个字幕列表输出一遍, 这部分不需要捕获
+ continue;
+ }
+ // 解析官方字幕
+ if (rs[i].match(/.*Available subtitles for .*?:/)) {
+ FOR_J: // 打标签, 因为需要从switch中断
+ for (let j = i + 1; j < rs.length; j ++ ) {
+ if (rs[j].trim() === '' || rs[j].trim() === '\n') continue; // 空行直接忽略
+ sub = catchSubtitle(rs[j]);
+ switch (sub) {
+ case -1: { // 终结
+ break FOR_J;
+ }
+ case 0: { // 继续
+ continue;
+ }
+ default: { // 捕获
+ officialSub.push(sub);
+ break;
+ }
+ }
+ } // for j
+ } // if
+ } // for i
+
+ if (officialSub.length < 1) { // 没有官方字幕
+ if (noAutoSub) { // 没有任何字幕
+ console.log('没有任何字幕');
+ return [];
+ } else { // 没有官方字幕但是有自动生成字幕, 可以自动翻译为任何字幕
+ console.log('有自动生成字幕');
+ return ['auto'];
+ }
+ } else { // 有官方字幕, 同时可以自动翻译为任何字幕
+ console.log('有官方字幕');
+ console.log(JSON.stringify(officialSub, null, 0));
+ return officialSub;
+ }
+ } catch (error) {
+ console.log(error); // npm 命令无法捕获error错误流
+ }
+ return [];
}
+/**
+ * Worker线程入口
+ */
function task() {
worker_threads.parentPort.once('message', msg => {
switch (msg.op) {
+ case 'subtitle': {
+ console.log(msg);
+ let { id, p, locale, ext, type, website } = msg;
+ // 先下载字幕
+ let fullpath = `${__dirname}/tmp/${id}${ p ? `/p${p}` : '' }`; // 字幕工作路径
+ let cmd_download = '';
+ if (type === 'native') // 原生字幕
+ cmd_download = `yt-dlp --sub-lang '${locale}' -o '${fullpath}/%(id)s.%(ext)s' --write-sub --skip-download --write-info-json ${getWebsiteUrl(website, id, p)} ${config.cookie !== undefined ? `--cookies ${config.cookie}` : ''}`;
+ else if (type === 'auto') // 切换翻译通道
+ cmd_download = `yt-dlp --sub-lang '${locale}' -o '${fullpath}/%(id)s.%(ext)s' --write-auto-sub --skip-download --write-info-json ${getWebsiteUrl(website, id, p)} ${config.cookie !== undefined ? `--cookies ${config.cookie}` : ''}`;
+ console.log(`下载字幕, 命令: ${cmd_download}`);
+ try {
+ child_process.execSync(cmd_download); // 执行下载
+ // 文件前缀
+ let before = `${fullpath}/${id}${ p ? `_p${p}` : '' }`;
+ // 字幕文件路径
+ let file = `${before}.${locale}.${locale == 'danmaku' ? 'xml' : website == 'y2b' ? 'vtt' : 'srt'}`; // B站的字幕一定是srt格式, 或xml格式(B站弹幕),y2b是vtt格式
+ console.log('下载的字幕:', file);
+ let file_convert = `${before}.${locale}${ext}`; // 要转换的字幕文件
+ if (file != file_convert) {
+ console.log('转换为:', file_convert);
+ let cmd_ffmpeg = `ffmpeg -i '${file}' '${file_convert}' -y`; // -y 强制覆盖文件
+ console.log(`转换字幕, 命令: ${cmd_ffmpeg}`);
+ child_process.execSync(cmd_ffmpeg);
+ }
+ // info文件路径
+ let file_info = `${before}.info.json`;
+ console.log('info文件:', file_info);
+ // JSON of info文件
+ let info = JSON.parse(fs.readFileSync(file_info).toString());
+ let title = info.title; // 视频标题
+ console.log('视频标题:', title);
+ let text = fs.readFileSync(file_convert).toString(); // 转换后字幕文件的文本内容
+ worker_threads.parentPort.postMessage({ // 下载成功
+ success: true,
+ title, // 返回标题
+ filename: `${title}.${locale}${ext}`, // 建议文件名
+ text: Buffer.from(text).toString('base64'), // 字幕文本,Base64
+ });
+ } catch(error) { // 下载过程出错
+ console.log(error);
+ }
+ worker_threads.parentPort.postMessage({
+ success: false,
+ });
+ break;
+ } // case subtitle end
+
case 'parse': {
let audios = [], videos = [];
let bestAudio = {}, bestVideo = {};
- let rs = [];
+ let rs = { title: '', thumbnail: '', formats: [] };
try {
- if (true)
- rs = child_process.execSync(`youtube-dl ${config.cookie !== undefined ? `--cookies ${config.cookie}` : ''} -F '${msg.url}' 2> /dev/null`).toString().split('\n');
- // 测试用数据
- else
- rs = `[youtube] sbz3fOe7rog: Downloading webpage
-[youtube] sbz3fOe7rog: Downloading video info webpage
-[info] Available formats for sbz3fOe7rog:
-format code extension resolution note
-249 webm audio only tiny 59k , opus @ 50k (48000Hz), 1.50KB
-251 webm audio only tiny 150k , opus @160k (48000Hz), 3.85MiB
-250 webm audio only tiny 78k , opus @ 70k (48000Hz), 2.00MiB
-140 m4a audio only tiny 129k , m4a_dash container, mp4a.40.2@128k (44100Hz), 3.47MiB
-278 webm 256x144 144p 95k , webm container, vp9, 15fps, video only, 2.36MiB
-160 mp4 256x144 144p 111k , avc1.4d400c, 15fps, video only, 2.95MiB
-133 mp4 426x240 240p 247k , avc1.4d4015, 15fps, video only, 6.58MiB
-242 webm 426x240 240p 162k , vp9, 15fps, video only, 2.62MiB
-18 mp4 512x288 240p 355k , avc1.42001E, mp4a.40.2@ 96k (44100Hz), 9.58MiB (best)`.split('\n');
- } catch(error) {
+ let cmd = `yt-dlp --print-json --skip-download ${config.cookie !== undefined ? `--cookies ${config.cookie}` : ''} '${msg.url}' 2> /dev/null`
+ console.log('解析视频, 命令:', cmd);
+ rs = child_process.execSync(cmd).toString();
+ try {
+ rs = JSON.parse(rs);
+ } catch (error) {
+ let cmd = `yt-dlp --print-json --skip-download ${config.cookie !== undefined ? `--cookies ${config.cookie}` : ''} '${msg.url}?p=1' 2> /dev/null`;
+ console.log('尝试分P, 命令:', cmd);
+ rs = child_process.execSync(cmd).toString();
+ rs = JSON.parse(rs);
+ msg.p = '1';
+ msg.url = `${msg.url}?p=1`;
+ }
+ console.log('解析完成:', rs.title, msg.url);
+ } catch (error) {
console.log(error.toString());
worker_threads.parentPort.postMessage({
"error": "解析失败!",
"success": false
});
+ return;
}
- rs.forEach(it => {
- console.log(it);
- let videoRegex = /^(\d+)\s+(\w+)\s+(\d+x\d+)\s+(\d+)p\s+(\d+)k , (.*), video only, (.+)MiB$/;
- let mr = it.match(videoRegex);
- if (!!mr) {
- let video = getVideo(mr[1], mr[2], mr[3], mr[4], mr[5], mr[6], mr[7]);
- return videos.push(video);
- }
-
- videoRegex = /^(\d+)\s+(\w+)\s+(\d+x\d+)\s+(\d+)p\s+(\d+)k , (.*), (.+)MiB.+best.+$/;
- mr = it.match(videoRegex);
- if (!!mr) {
- let video = getVideo(mr[1], mr[2], mr[3], mr[4], mr[5], mr[6], mr[7]);
- return videos.push(video);
- }
-
- videoRegex = /^(\d+)\s+(\w+)\s+(\d+x\d+)\s+(?:[^,]+)\s+(\d+)k , (.*), video.*$/;
- mr = it.match(videoRegex);
- if (!!mr) {
- let video = getVideo(mr[1], mr[2], mr[3], 0, mr[4], mr[5], '未知');
- return videos.push(video);
- }
-
- let audioRegex = /^(\d+)\s+(\w+)\s+audio only.*\s+(\d+)k , (.*),\s+(?:(.+)MiB|.+)$/;
- mr = it.match(audioRegex);
- if (!!mr) {
- let audio = getAudio(mr[1], mr[2], mr[3], mr[4], mr[5] || '未知');
- return audios.push(audio);
+ rs.formats.forEach(it => {
+ let length = (it.filesize_approx ? '≈' : '') + ((it.filesize || it.filesize_approx || 0) / 1024 / 1024).toFixed(2);
+ if (it.audio_ext != 'none') {
+ audios.push(getAudio(it.format_id, it.ext, (it.abr || 0).toFixed(0), it.format_note || it.format || '', length));
+ } else if (it.video_ext != 'none') {
+ videos.push(getVideo(it.format_id, it.ext, it.resolution, it.height, (it.vbr || 0).toFixed(0), it.format_note || it.format || '', length));
}
});
// sort
- audios.sort((a, b) => a.rate - b.rate);
- videos.sort((a, b) => a.rate - b.rate);
- bestAudio = audios[audios.length - 1];
- bestVideo = videos[videos.length - 1];
+ // audios.sort((a, b) => a.rate - b.rate);
+ // videos.sort((a, b) => a.rate - b.rate);
+ bestAudio = Array.from(audios).sort((a, b) => a.rate - b.rate)[audios.length - 1];
+ bestVideo = Array.from(videos).sort((a, b) => a.rate - b.rate)[videos.length - 1];
+
+ let subs = parseSubtitle(msg); // 解析字幕
worker_threads.parentPort.postMessage({
"success": true,
"result": {
+ "website": msg.website,
"v": msg.videoID,
+ "p": msg.p,
+ "title": rs.title,
+ "thumbnail": rs.thumbnail,
"best": {
"audio": bestAudio,
"video": bestVideo,
},
- "available": { audios, videos }
+ "available": { audios, videos, subs }
}
});
@@ -218,13 +428,13 @@ format code extension resolution note
}
case 'download': {
- let { videoID, format, recode } = msg;
- const path = `${videoID}/${format}`;
+ let { videoID, p, format, recode, subs, website } = msg; // subs字幕内封暂未实现
+ const path = `${videoID}${ p ? `/p${p}` : '' }/${format}`;
const fullpath = `${__dirname}/tmp/${path}`;
let cmd = //`cd '${__dirname}' && (cd tmp > /dev/null || (mkdir tmp && cd tmp)) &&` +
- `youtube-dl ${config.cookie !== undefined ? `--cookies ${config.cookie}` : ''} 'https://www.youtube.com/watch?v=${videoID}' -f ${format.replace('x', '+')} ` +
+ `yt-dlp ${config.cookie !== undefined ? `--cookies ${config.cookie}` : ''} ${getWebsiteUrl(website, videoID, p)} -f ${format.replace('x', '+')} ` +
`-o '${fullpath}/${videoID}.%(ext)s' ${recode !== undefined ? `--recode ${recode}` : ''} -k --write-info-json`;
- console.log({ cmd });
+ console.log('下载视频, 命令:', cmd);
try {
let dest = 'Unknown dest';
let ps = child_process.execSync(cmd).toString().split('\n');
@@ -274,7 +484,11 @@ format code extension resolution note
});
}
+/*======================================================================================
+index.js 兵分两路
+========================================================================================*/
if (worker_threads.isMainThread)
main();
else
task();
+/*======================================================================================*/
diff --git a/package.json b/package.json
index 0019e0d..ae2116d 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,21 @@
{
- "name": "Youtube-dl-REST-node",
+ "name": "@develon/youtube-dl-rest",
"version": "1.0.0",
- "description": "Youtube-dl-REST By Node.js",
- "main": "index.js",
+ "description": "Website for downloading Youtube & BiliBili videos",
"scripts": {
- "start": "/usr/bin/env node index.js"
+ "start:dev": "nodemon index.js",
+ "start": "node index.js"
},
- "keywords": [],
- "author": "develon",
- "license": "ISC",
+ "license": "MIT",
"dependencies": {
"express": "^4.17.1"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "homepage": "https://github.com/develon2015/Youtube-dl-REST",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/develon2015/Youtube-dl-REST.git"
}
}
diff --git a/webapps/css/style.css b/static/css/style.css
similarity index 100%
rename from webapps/css/style.css
rename to static/css/style.css
diff --git a/webapps/favicon.ico b/static/favicon.ico
similarity index 100%
rename from webapps/favicon.ico
rename to static/favicon.ico
diff --git a/static/index.html b/static/index.html
new file mode 100755
index 0000000..a1be6c8
--- /dev/null
+++ b/static/index.html
@@ -0,0 +1,601 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
《更新日志》
+1:下载引擎替换为yt-dlp
+2:支持解析BiliBili字幕和弹幕
+3:支持显示标题和封面
+ +✩ 您的Star和Fork是对开发者最大的支持!
+《更新日志》
-1:使用Node.js重构
-2:自动清理空间
-3:支持视频标题作为文件名
-4:关闭在线转码,推荐自行部署
-✩ 您的Star和Fork是对开发者最大的支持!
-