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 + +[![image](https://github.com/user-attachments/assets/dae292e1-3a99-4f6b-b423-4b973ef0d49b)](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 @@ + + + + + + + + + + + + + + + + + + +
+
Youtube&BiliBili 在线解析
+
+ + + +
+
+ +
+
+ +
+
+ + + +
+

《更新日志》

+

1:下载引擎替换为yt-dlp

+

2:支持解析BiliBili字幕和弹幕

+

3:支持显示标题和封面

+ +
+

✩   您的Star和Fork是对开发者最大的支持!

+
+ +
+

服务器正在处理...

+
+ + + + + diff --git a/webapps/js/jquery.js b/static/js/jquery.js similarity index 100% rename from webapps/js/jquery.js rename to static/js/jquery.js diff --git a/webapps/js/libjrt.js b/static/js/libjrt.js similarity index 95% rename from webapps/js/libjrt.js rename to static/js/libjrt.js index 9488656..2ee3f24 100755 --- a/webapps/js/libjrt.js +++ b/static/js/libjrt.js @@ -1,5 +1,9 @@ // library with jQuery - +/** 修改文档标题 */ +function title(msg) { + if (msg == undefined) title(window.defaultTitle || ''); + $('title').text(msg); +} // Creating a namespace for Develon (fun => { Develon = window.Develon || { @@ -51,6 +55,7 @@ } ui.setDefaultTitle = fun => { + window.defaultTitle = fun; if ($('title')[0] === undefined) { $('' + fun + '').appendTo($('head')) } @@ -74,11 +79,13 @@ }, removeNotify: id => { + title(); $(document.body)[0].removeChild($('div#notify' + id)[0]) }, notifyID: 1, notify: function (msg, callback) { + title(msg); var id = this.notifyID ++ $('
\
\ @@ -104,6 +111,7 @@ }, notifyWait: function (msg, callback) { + title(msg); var id = this.notifyID ++ $('
\
\ diff --git a/webapps/js/loadbar.js b/static/js/loadbar.js similarity index 100% rename from webapps/js/loadbar.js rename to static/js/loadbar.js diff --git a/tea.yaml b/tea.yaml new file mode 100755 index 0000000..abf98fc --- /dev/null +++ b/tea.yaml @@ -0,0 +1,6 @@ +# https://tea.xyz/what-is-this-file +--- +version: 1.0.0 +codeOwners: + - '0xAf66dE743a481394cA9CdFa1cA0f8635E6B75Ab0' +quorum: 1 diff --git a/utils.js b/utils.js new file mode 100755 index 0000000..96e41f7 --- /dev/null +++ b/utils.js @@ -0,0 +1,17 @@ +function getRemoteIP(request) { + return request.header('cf-connecting-ip') || request.ip || '未知IP'; +} + +function getWebsiteUrl(website, id, p) { + switch (website) { + case 'y2b': + return `https://youtu.be/${id}`; + case 'bilibili': + return `https://www.bilibili.com/video/${id}${p ? `?p=${p}` : ''}`; + } +} + +module.exports = { + getRemoteIP, + getWebsiteUrl, +} diff --git a/webapps/index.html b/webapps/index.html deleted file mode 100755 index a97c08a..0000000 --- a/webapps/index.html +++ /dev/null @@ -1,282 +0,0 @@ - - - - - - - - - - - - - - -
-
Youtube在线解析
-
- - - -
-
- -
-
- -
-
- - - -
-

《更新日志》

-

1:使用Node.js重构

-

2:自动清理空间

-

3:支持视频标题作为文件名

-

4:关闭在线转码,推荐自行部署

-

✩   您的Star和Fork是对开发者最大的支持!

-
- - -