mirror of
https://github.com/szdytom/nyanpasu.git
synced 2025-10-19 17:10:16 +00:00
optimize for win32
This commit is contained in:
parent
4e01cd5fbd
commit
692756aaf7
38
README.md
38
README.md
@ -1,12 +1,12 @@
|
|||||||
# 喵帕斯解析器 (nyanpasu)
|
# 喵帕斯解析器 (nyanpasu)
|
||||||
|
|
||||||
喵帕斯解析器是一个 BiliBili 番剧视频和弹幕元数据的解析脚本,可以根据链接自动下载和解析视频和弹幕元数据,并创建下载列表。
|
喵帕斯解析器是一个 BiliBili 番剧视频和弹幕元数据的解析脚本,可用于快速缓存 BiliBili 番剧各集的数据。喵帕斯解析器可以根据链接自动下载和解析视频和弹幕元数据,并创建下载列表。
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
喵帕斯解析器可以根据一个链接解析相关的资源数据和链接,它被设计为自动化工具链中的一环,在需要大量解析缓存番剧数据时可能十分有用。但是,请注意,喵帕斯解析器**不是**一个:
|
喵帕斯解析器可以根据一个链接解析相关的资源数据和链接,它被设计为自动化工具链中的一环,在需要大量解析缓存番剧数据时可能十分有用。但是,请注意,喵帕斯解析器**不是**一个:
|
||||||
|
|
||||||
- 弹幕下载器:喵帕斯解析器可以找到下载弹幕的链接,并生成用于批量下载弹幕的脚本。但是实际的下载过程不是由喵帕斯解析器完成的(而是依赖于 [curl](https://github.com/curl/curl)),如果你仅仅希望下载某一视频的弹幕,你可能希望使用 [Bilibili-Evolved](https://github.com/the1812/Bilibili-Evolved) 这一浏览器插件。
|
- 弹幕下载器:喵帕斯解析器可以找到下载弹幕的链接,并生成用于批量下载弹幕的脚本。但是实际的下载过程不是由喵帕斯解析器完成的(而是依赖于 [curl](https://github.com/curl/curl) 或其他命令行下载器),如果你仅仅希望下载某一视频的弹幕,你可能希望使用 [Bilibili-Evolved](https://github.com/the1812/Bilibili-Evolved) 这一浏览器插件。
|
||||||
- 视频下载器:喵帕斯解析器可以找到视频播放的链接,并生成用于批量下载视频的链接文件。但是实际的下载过程不是由喵帕斯解析器完成的(而是依赖于 [yt-dlp](https://github.com/yt-dlp/yt-dlp)),如果你仅仅希望下载某一视频,你可能希望直接使用 [yt-dlp](https://github.com/yt-dlp/yt-dlp) 脚本。
|
- 视频下载器:喵帕斯解析器可以找到视频播放的链接,并生成用于批量下载视频的链接文件。但是实际的下载过程不是由喵帕斯解析器完成的(而是依赖于 [yt-dlp](https://github.com/yt-dlp/yt-dlp)),如果你仅仅希望下载某一视频,你可能希望直接使用 [yt-dlp](https://github.com/yt-dlp/yt-dlp) 脚本。
|
||||||
- 弹幕播放器:喵帕斯解析器可以帮助你生成用于正确保存视频和弹幕的脚本,但是喵帕斯解析器不能理解视频或弹幕文件,如果你希望播放视频和弹幕,你可能希望使用 [KikoPlay](https://github.com/KikoPlayProject/KikoPlay) 或者其他类似的软件。
|
- 弹幕播放器:喵帕斯解析器可以帮助你生成用于正确保存视频和弹幕的脚本,但是喵帕斯解析器不能理解视频或弹幕文件,如果你希望播放视频和弹幕,你可能希望使用 [KikoPlay](https://github.com/KikoPlayProject/KikoPlay) 或者其他类似的软件。
|
||||||
- DRM破解器:喵帕斯解析器没有任何魔法帮助你查看或破解你无权查看的数据和视频,喵帕斯解析器只能下载和分析公开的数据。
|
- DRM破解器:喵帕斯解析器没有任何魔法帮助你查看或破解你无权查看的数据和视频,喵帕斯解析器只能下载和分析公开的数据。
|
||||||
@ -16,7 +16,11 @@
|
|||||||
|
|
||||||
此脚本没有使用任何平台特定的代码,应当可以适用于所有主流操作系统,但是它只在 Linux 下测试过。
|
此脚本没有使用任何平台特定的代码,应当可以适用于所有主流操作系统,但是它只在 Linux 下测试过。
|
||||||
|
|
||||||
你需要首先安装 Node.JS 以运行此脚本。你可以从 Release 页面下载到一个压缩后脚本(`nyanpasu.mjs`),该文件已经将 NPM 依赖内嵌在文件中,因此不依赖第三方库。该脚本包含正确的“井号注释”(shebang),可以直接赋予可执行权限并执行。你可以考虑将其复制到 PATH 目录下(例如 `~/.local/bin` 或者 `/usr/local/bin`)以便于使用。
|
你需要首先安装 Node.JS 以运行此脚本。你可以从 Release 页面下载到一个压缩后脚本(`nyanpasu.mjs`),该文件已经将 NPM 依赖内嵌在文件中,因此不依赖第三方库。
|
||||||
|
|
||||||
|
对于 Linux 操作系统,该脚本包含正确的“井号注释”(shebang),可以直接赋予可执行权限以便执行。你可以考虑将其复制到 PATH 目录下(例如 `~/.local/bin` 或者 `/usr/local/bin`)以便于使用。
|
||||||
|
|
||||||
|
对于 Windows 操作系统,你可能需要将 `.mjs` 设置为默认使用 Node.JS 运行,或者每次运行时使用 `node --experimental-modules nyanpasu.mjs`。(由于本人对 Windows 的使用经验不足,这里的描述可能与实际情况有偏差,如果你有更好的打包方式,请随时打开一个 Issue 或 PR 讨论。)
|
||||||
|
|
||||||
你可能希望安装 [yt-dlp](https://github.com/yt-dlp/yt-dlp) 及其依赖 [ffmpeg](https://ffmpeg.org/) 以便于下载视频。你可能还希望使用 [Tmux](https://github.com/tmux/tmux) 或者 [GNU Screen](https://www.gnu.org/software/screen/) 以防止因为关闭终端而导致下载中断。如果你下载了视频和弹幕文件,你可能希望安装 [KikoPlay](https://github.com/KikoPlayProject/KikoPlay) 弹幕播放器。
|
你可能希望安装 [yt-dlp](https://github.com/yt-dlp/yt-dlp) 及其依赖 [ffmpeg](https://ffmpeg.org/) 以便于下载视频。你可能还希望使用 [Tmux](https://github.com/tmux/tmux) 或者 [GNU Screen](https://www.gnu.org/software/screen/) 以防止因为关闭终端而导致下载中断。如果你下载了视频和弹幕文件,你可能希望安装 [KikoPlay](https://github.com/KikoPlayProject/KikoPlay) 弹幕播放器。
|
||||||
|
|
||||||
@ -39,7 +43,7 @@ npm run build
|
|||||||
|
|
||||||
即可在 `dist/nyanpasu.mjs` 找到输出。
|
即可在 `dist/nyanpasu.mjs` 找到输出。
|
||||||
|
|
||||||
请注意,此脚本的打包脚本 `build.sh` 不适用于 Windows(依赖于 `/bin/sh`),但是可以应当很容易的适配到 Windows(如果对此需要帮助,请随时提交一个 Issue)。
|
此脚本的打包脚本通过 `build.cjs` 根据操作系统自动选择使用 `build.sh` 还是 `build.bat`,值得注意的是,适用于 Windows 的构建脚本没有经过充分测试,如果你遇到问题,请随时打开一个 Issue。
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
|
||||||
@ -89,7 +93,7 @@ Command hint: yt-dlp -a vlist.txt -o "%(autonumber)s.%(ext)s"
|
|||||||
cache.json descriptor.xml download-danmu.sh vlist.txt
|
cache.json descriptor.xml download-danmu.sh vlist.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
其中 `cache.json` 是番剧元数据的缓存,`descriptor.xml` 是简化后的番剧元数据,可供人阅读或其他软件解析。`download-danmu.sh` 是用于下载弹幕数据和番剧封面图的脚本(内部使用 [curl](https://github.com/curl/curl))。`vlist.txt` 是各集视频的链接,你可以进一步使用如下命令下载弹幕数据和番剧封面图:
|
其中 `cache.json` 是番剧元数据的缓存,`descriptor.xml` 是简化后的番剧元数据,可供人阅读或其他软件解析。`download-danmu.sh` 是用于下载弹幕数据和番剧封面图的脚本(内部使用 [curl](https://github.com/curl/curl),如果你使用的是 Windows,你应该会得到内部使用 `Invoke-WebRequest` 的 PowerShell 脚本)。`vlist.txt` 是各集视频的链接,你可以进一步使用如下命令下载弹幕数据和番剧封面图:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./download-danmu.sh
|
./download-danmu.sh
|
||||||
@ -103,12 +107,34 @@ yt-dlp -a vlist.txt -o "%(autonumber)s.%(ext)s"
|
|||||||
|
|
||||||
根据需要,你可能需要添加 `--cookies`、`--cookies-from-browser`、`--abort-on-error` 等命令选项,注意可能无法下载付费视频和地区限制视频,可能会报错。关于更多信息,请参考 [yt-dlp](https://github.com/yt-dlp/yt-dlp) 的文档。
|
根据需要,你可能需要添加 `--cookies`、`--cookies-from-browser`、`--abort-on-error` 等命令选项,注意可能无法下载付费视频和地区限制视频,可能会报错。关于更多信息,请参考 [yt-dlp](https://github.com/yt-dlp/yt-dlp) 的文档。
|
||||||
|
|
||||||
你可以通过 `--help` 选项或阅读 `src/index.mjs` 源代码以查看更多命令行选项。
|
### 关于缓存
|
||||||
|
|
||||||
|
在执行脚本后,会在当前目录下生成 `cache.json`,内部包含 BiliBili 关于番剧的元数据缓存。在第二次执行脚本时,如果检测到 `cache.json` 存在,则不再会重新请求,而是直接使用 `cache.json` 内部缓存的数据,**即使两次执行使用的 URL 参数不相同**。
|
||||||
|
|
||||||
|
`cache.json` 内部的格式可能并不稳定,请尽可能地使用 `descriptor.xml` 中的数据而不是 `cache.json`。
|
||||||
|
|
||||||
|
### 命令行选项
|
||||||
|
|
||||||
|
你也可以通过 `--help` 选项或阅读 `src/index.mjs` 源代码以查看喵帕斯解析器的命令行选项。
|
||||||
|
|
||||||
|
- `--skip-cache`:不读取/写入本地缓存 `cache.json`。
|
||||||
|
- `--skip-url`:不使用 URL 参数,而是读取本地缓存 `cache.json`。
|
||||||
|
- `--include-pv`:不将标记为“预告”的视频排除在外。
|
||||||
|
- `--min-duration <seconds>`:将时长小于给定值的视频排除在外,可用于筛选掉PV和MV。
|
||||||
|
- `--danmu-source`:选择使用 `comment.bilibili.com` 还是 `api.bilibili.com` 的弹幕链接,理论上没有区别。
|
||||||
|
- `--downloader-command`:选择脚本内使用 [curl](https://github.com/curl/curl) 还是 [Invoke-WebRequest](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-webrequest) 作为命令行下载器。
|
||||||
|
- `--script-format`:选择脚本的格式(可选 `bash`、`cmd` 或 `powershell`,亦可指定 `none` 以抑制解析器生成弹幕下载脚本)。
|
||||||
|
- `--downloader-args`:为命令行下载器指定额外的参数,注意该选项的值需要以引号包裹,并以空格开头,例如 `--downloader-args " -A -B --example"`。
|
||||||
|
- `--downloader-args-override`:覆盖命令行下载器的参数,注意该选项的值需要以引号包裹,并以空格开头,例如 `--downloader-args-override " -A -B --example"`。
|
||||||
|
|
||||||
## 社区
|
## 社区
|
||||||
|
|
||||||
我们期待来自社区的贡献。如果您遇到了错误,请随时提出问题。还有许多可以添加的功能。如果您实现了任何增强功能,欢迎打开一个拉取请求。
|
我们期待来自社区的贡献。如果您遇到了错误,请随时提出问题。还有许多可以添加的功能。如果您实现了任何增强功能,欢迎打开一个拉取请求。
|
||||||
|
|
||||||
|
## 名称
|
||||||
|
|
||||||
|
喵帕斯这个词出自 [悠哉日常大王](https://zh.moegirl.org.cn/%E6%82%A0%E5%93%89%E6%97%A5%E5%B8%B8%E5%A4%A7%E7%8E%8B),请参见[百科条目](https://zh.moegirl.org.cn/%E5%96%B5%E5%B8%95%E6%96%AF)。
|
||||||
|
|
||||||
## 声明
|
## 声明
|
||||||
|
|
||||||
请注意,本项目不代表上海宽娱数码科技有限公司或番剧版权方的意见,本项目按照“按其原样”的原则提供,不提供任何附带保证,使用者需承担可能的风险。
|
请注意,本项目不代表上海宽娱数码科技有限公司或番剧版权方的意见,本项目按照“按其原样”的原则提供,不提供任何附带保证,使用者需承担可能的风险。
|
||||||
|
6
build.bat
Normal file
6
build.bat
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@echo off
|
||||||
|
FOR /F "tokens=*" %%i IN ('git rev-parse --show-toplevel') DO SET "GIT_ROOT=%%i"
|
||||||
|
cd /d %GIT_ROOT%
|
||||||
|
if exist dist rmdir /s /q dist
|
||||||
|
npx ncc build -m src\index.mjs
|
||||||
|
type nyanpasu.js dist\index.mjs > dist\nyanpasu.mjs
|
1
build.cjs
Normal file
1
build.cjs
Normal file
@ -0,0 +1 @@
|
|||||||
|
void(require('child_process').execSync(require('os').platform() === 'win32' ? 'build.bat' : './build.sh'));
|
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "nyanpasu",
|
"name": "nyanpasu",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"description": "BiliBili 番剧视频和弹幕元数据解析脚本",
|
"description": "BiliBili 番剧视频和弹幕元数据解析脚本",
|
||||||
"main": "src/index.mjs",
|
"main": "src/index.mjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "./build.sh"
|
"build": "node build.cjs"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
307
src/index.mjs
307
src/index.mjs
@ -2,6 +2,7 @@ import fetch from 'node-fetch';
|
|||||||
import xmldom from '@xmldom/xmldom';
|
import xmldom from '@xmldom/xmldom';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import xmlFormat from 'xml-formatter';
|
import xmlFormat from 'xml-formatter';
|
||||||
|
import { platform } from 'node:os';
|
||||||
import { hideBin } from 'yargs/helpers';
|
import { hideBin } from 'yargs/helpers';
|
||||||
import yargs from 'yargs';
|
import yargs from 'yargs';
|
||||||
|
|
||||||
@ -11,7 +12,7 @@ async function fetchDescriptor(url) {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const regex = /<script id="__NEXT_DATA__" type="application\/json">(.*?)<\/script>/s;;
|
const regex = /<script id="__NEXT_DATA__" type="application\/json">(.*?)<\/script>/s;
|
||||||
const match = html.match(regex);
|
const match = html.match(regex);
|
||||||
|
|
||||||
if (match && match[1]) {
|
if (match && match[1]) {
|
||||||
@ -125,6 +126,205 @@ class AnimeEpisode {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class DownloadTarget {
|
||||||
|
constructor(uri, compressed) {
|
||||||
|
this.uri = uri;
|
||||||
|
this.compressed = compressed ?? false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class DownloadCommand {
|
||||||
|
constructor(defaultArgs, supportFlags, { customArgs, overrideArgs }) {
|
||||||
|
if (overrideArgs != null) {
|
||||||
|
this.args = overrideArgs;
|
||||||
|
} else {
|
||||||
|
this.args = defaultArgs + customArgs;
|
||||||
|
}
|
||||||
|
if (this.args.length && this.args[0] != ' ') {
|
||||||
|
this.args = ' ' + this.args;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.supportFlags = supportFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
support(flag) {
|
||||||
|
return this.supportFlags.contains(flag);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class CurlDownloadCommand extends DownloadCommand {
|
||||||
|
constructor(config) {
|
||||||
|
super('-Lgf', ['Win32Cmd', 'Linux', 'PowerShell'], config);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(target, filename) {
|
||||||
|
if (target.compressed) {
|
||||||
|
return `curl${this.args} --compressed -o "${filename}" "${target.uri}"`;
|
||||||
|
}
|
||||||
|
return `curl${this.args} -o "${filename}" "${target.uri}"`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class PowerShellDownloadCommand extends DownloadCommand {
|
||||||
|
constructor(config) {
|
||||||
|
super('', ['PowerShell'], config);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(target, filename) {
|
||||||
|
return `Invoke-WebRequest${this.args} -Uri "${target.uri}" -OutFile "${filename}"`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class DownloadScriptBuilder {
|
||||||
|
constructor(flag, downloader) {
|
||||||
|
this.dmLinks = [];
|
||||||
|
this.cvLinks = [];
|
||||||
|
this.cvLinksURI = new Set();
|
||||||
|
this.downloader = downloader;
|
||||||
|
this.flag = flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
addDm(link) {
|
||||||
|
this.dmLinks.push(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
addCover(link) {
|
||||||
|
if (this.cvLinksURI.has(link.uri)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cvLinksURI.add(link.uri);
|
||||||
|
this.cvLinks.push(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(target, file) {
|
||||||
|
return this.downloader.downloadFile(target, file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class BashDownloadScriptBuilder extends DownloadScriptBuilder {
|
||||||
|
constructor(downloader) {
|
||||||
|
super('Linux', downloader);
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulate() {
|
||||||
|
let script = [];
|
||||||
|
script.push('#!/bin/sh');
|
||||||
|
script.push('set -e');
|
||||||
|
|
||||||
|
let i = 1;
|
||||||
|
for (let link of this.dmLinks) {
|
||||||
|
const iName = i.toString().padStart(5, '0');
|
||||||
|
script.push(this.downloadFile(link, `${iName}.xml`));
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
i = 1;
|
||||||
|
for (let link of this.cvLinks) {
|
||||||
|
script.push(this.downloadFile(link, `cover-${i}.jpg`));
|
||||||
|
}
|
||||||
|
return script.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
extension() {
|
||||||
|
return '.sh';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class WinCmdDownloadScriptBuilder extends DownloadScriptBuilder {
|
||||||
|
constructor(downloader) {
|
||||||
|
super('Win32', downloader);
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulate() {
|
||||||
|
let cmd = [];
|
||||||
|
let i = 1;
|
||||||
|
for (let link of this.dmLinks) {
|
||||||
|
const iName = i.toString().padStart(5, '0');
|
||||||
|
cmd.push(this.downloadFile(link, `${iName}.xml`));
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
i = 1;
|
||||||
|
for (let link of this.cvLinks) {
|
||||||
|
cmd.push(this.downloadFile(link, `cover-${i}.jpg`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return `@echo off\r\n${cmd.join(' && ')}\r\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension() {
|
||||||
|
return '.bat';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class PowerShellDownloadScriptBuilder extends DownloadScriptBuilder {
|
||||||
|
constructor(downloader) {
|
||||||
|
super('PowerShell', downloader);
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulate() {
|
||||||
|
let script = [];
|
||||||
|
script.push('$ErrorActionPreference = "Stop"');
|
||||||
|
|
||||||
|
let i = 1;
|
||||||
|
for (let link of this.dmLinks) {
|
||||||
|
const iName = i.toString().padStart(5, '0');
|
||||||
|
script.push(this.downloadFile(link, `${iName}.xml`));
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
i = 1;
|
||||||
|
for (let link of this.cvLinks) {
|
||||||
|
script.push(this.downloadFile(link, `cover-${i}.jpg`));
|
||||||
|
}
|
||||||
|
return script.join('\r\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
extension() {
|
||||||
|
return '.ps1';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class NullDownloadScriptBuilder extends DownloadScriptBuilder {
|
||||||
|
constructor(downloader) {
|
||||||
|
super(null, downloader);
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulate() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class DanmuDownloadSource {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class CommentDanmuDownloadSource extends DanmuDownloadSource {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
convert(cid) {
|
||||||
|
return new DownloadTarget(`https://comment.bilibili.com/${cid}.xml`, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ApiCurrentDanmuDownloadSource extends DanmuDownloadSource {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
convert(cid) {
|
||||||
|
return new DownloadTarget(`https://api.bilibili.com/x/v1/dm/list.so?oid=${cid}`, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let enableLogging = true;
|
let enableLogging = true;
|
||||||
function info(msg) {
|
function info(msg) {
|
||||||
if (enableLogging) {
|
if (enableLogging) {
|
||||||
@ -175,10 +375,11 @@ function parseDescriptor(source, includePv, minDuration) {
|
|||||||
if (!includePv && edata.badge != null && edata.badge.search('预告') != -1) {
|
if (!includePv && edata.badge != null && edata.badge.search('预告') != -1) {
|
||||||
info(`Filtered pv (specify --include-pv to keep): ${episode.toString()}`);
|
info(`Filtered pv (specify --include-pv to keep): ${episode.toString()}`);
|
||||||
} else if (edata.duration < minDuration * 1000) {
|
} else if (edata.duration < minDuration * 1000) {
|
||||||
info(`Filtered duration: ${episode.toString()}`);
|
info(`Filtered by duration: ${episode.toString()}`);
|
||||||
} else if (edata.duration < 180000 && minDuration === 0) { // 3 minute
|
|
||||||
duration_warn = true;
|
|
||||||
} else {
|
} else {
|
||||||
|
if (edata.duration < 180000 && minDuration === 0) { // 3 minute
|
||||||
|
duration_warn = true;
|
||||||
|
}
|
||||||
episode_id += 1;
|
episode_id += 1;
|
||||||
anime.episodes.push(episode);
|
anime.episodes.push(episode);
|
||||||
}
|
}
|
||||||
@ -191,7 +392,7 @@ function parseDescriptor(source, includePv, minDuration) {
|
|||||||
return anime;
|
return anime;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processDescriptor(rawDesc, includePv, minDuration) {
|
async function processDescriptor(rawDesc, includePv, minDuration, scriptBuilder, danmuSource) {
|
||||||
let anime = parseDescriptor(rawDesc, includePv, minDuration);
|
let anime = parseDescriptor(rawDesc, includePv, minDuration);
|
||||||
info(`Title: ${anime.title}`);
|
info(`Title: ${anime.title}`);
|
||||||
info(`Count: ${anime.episodes.length} episodes`);
|
info(`Count: ${anime.episodes.length} episodes`);
|
||||||
@ -211,25 +412,24 @@ async function processDescriptor(rawDesc, includePv, minDuration) {
|
|||||||
tasks.push(fs.writeFile('descriptor.xml', xmlContent));
|
tasks.push(fs.writeFile('descriptor.xml', xmlContent));
|
||||||
|
|
||||||
info('Command hint: yt-dlp -a vlist.txt -o "%(autonumber)s.%(ext)s"');
|
info('Command hint: yt-dlp -a vlist.txt -o "%(autonumber)s.%(ext)s"');
|
||||||
let d_script = '#!/bin/sh\nset -e\n'
|
let v_list = [];
|
||||||
let v_list = [], covers = new Set();
|
|
||||||
for (let episode of anime.episodes) {
|
for (let episode of anime.episodes) {
|
||||||
const filename = `${episode.id.toString().padStart(5, '0')}.xml`;
|
scriptBuilder.addDm(danmuSource.convert(episode.cid));
|
||||||
const download_link = `https://comment.bilibili.com/${episode.cid}.xml`;
|
scriptBuilder.addCover(new DownloadTarget('https:' + episode.cover));
|
||||||
d_script += `curl --compressed -Lgf --retry 3 --retry-delay 3 -o ${filename} ${download_link}\n`;
|
|
||||||
v_list.push(episode.link);
|
v_list.push(episode.link);
|
||||||
covers.add(episode.cover);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let i = 1;
|
const scriptFilename = `download-danmu${scriptBuilder.extension()}`;
|
||||||
for (let cover of covers) {
|
let script = scriptBuilder.accumulate();
|
||||||
d_script += `curl -Lgf --retry 3 --retry-delay 3 -o cover-${i}.jpg https:${cover}\n`;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
tasks.push(fs.writeFile('download-danmu.sh', d_script));
|
|
||||||
tasks.push(fs.writeFile('vlist.txt', v_list.join('\n')));
|
tasks.push(fs.writeFile('vlist.txt', v_list.join('\n')));
|
||||||
|
if (script != null) {
|
||||||
|
tasks.push(fs.writeFile(scriptFilename, script));
|
||||||
|
}
|
||||||
await Promise.all(tasks);
|
await Promise.all(tasks);
|
||||||
await fs.chmod('download-danmu.sh', 0o755);
|
if (script != null) {
|
||||||
|
await fs.chmod(scriptFilename, 0o755);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@ -255,22 +455,83 @@ async function main() {
|
|||||||
description: 'Filter episodes with duration too small (unit: second)',
|
description: 'Filter episodes with duration too small (unit: second)',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
default: 0,
|
default: 0,
|
||||||
|
}).option('danmu-source', {
|
||||||
|
description: 'Source of danmu download (<value>.bilibili.com)',
|
||||||
|
alias: 'D',
|
||||||
|
choices: ['comment', 'api'],
|
||||||
|
default: 'comment',
|
||||||
|
}).option('downloader-command', {
|
||||||
|
description: 'Command to use in the download script',
|
||||||
|
alias: 'c',
|
||||||
|
choices: ['curl', 'invoke-webrequest', 'auto'],
|
||||||
|
default: 'auto',
|
||||||
|
}).option('script-format', {
|
||||||
|
description: 'Format of the download script',
|
||||||
|
alias: 'F',
|
||||||
|
choices: ['none', 'bash', 'cmd', 'powershell'],
|
||||||
|
default: platform() === 'win32' ? 'powershell' : 'bash',
|
||||||
|
}).option('downloader-args', {
|
||||||
|
description: 'Extra arguments/flags passed to the downloader command',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
}).option('downloader-args-override', {
|
||||||
|
description: 'Override the arguments/flags passed to the downloader command',
|
||||||
|
type: 'string',
|
||||||
|
default: null,
|
||||||
}).option('suppress-tmux-warning', {
|
}).option('suppress-tmux-warning', {
|
||||||
description: 'Suppress warning of not inside a TMUX session',
|
description: 'Suppress warning of not inside a TMUX session',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
}).usage('Uasge: <url>').version('0.1.3').help().alias('help', 'h').argv;
|
}).usage('Uasge: <url>').version('0.1.4').help().alias('help', 'h').argv;
|
||||||
|
|
||||||
const url = args._[0];
|
const url = args._[0];
|
||||||
enableLogging = !args.quiet;
|
enableLogging = !args.quiet;
|
||||||
if (args.skipUrl && args.skipCache) {
|
if (args.skipUrl && args.skipCache) {
|
||||||
console.error('There is nothing to do.');
|
console.error('There is nothing to do.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args.skipUrl && url == null) {
|
if (!args.skipUrl && url == null) {
|
||||||
console.error('No url provided, please specify --skip-url.');
|
console.error('No url provided, please specify --skip-url.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.downloaderArgsOverride != null && args.downloaderArgs != '') {
|
||||||
|
info('Flag --downloader-args will not take effect because --downloader-args-override specified');
|
||||||
|
}
|
||||||
|
|
||||||
|
let danmuSourceMap = {
|
||||||
|
'api': ApiCurrentDanmuDownloadSource,
|
||||||
|
'comment': CommentDanmuDownloadSource,
|
||||||
|
};
|
||||||
|
let danmuSource = new danmuSourceMap[args.danmuSource]();
|
||||||
|
|
||||||
|
let downloaderType = args.downloaderCommand;
|
||||||
|
if (downloaderType === 'auto') {
|
||||||
|
if (args.scriptFormat === 'powershell') {
|
||||||
|
downloaderType = 'invoke-webrequest';
|
||||||
|
} else {
|
||||||
|
downloaderType = 'curl';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloaderMap = {
|
||||||
|
'curl' : CurlDownloadCommand,
|
||||||
|
'invoke-webrequest': PowerShellDownloadCommand,
|
||||||
|
};
|
||||||
|
let downloader = new downloaderMap[downloaderType]({
|
||||||
|
customArgs: args.downloaderArgs,
|
||||||
|
overrideArgs: args.downloaderArgsOverride,
|
||||||
|
});
|
||||||
|
|
||||||
|
const scriptBuilderMap = {
|
||||||
|
'none': NullDownloadScriptBuilder,
|
||||||
|
'bash': BashDownloadScriptBuilder,
|
||||||
|
'cmd': WinCmdDownloadScriptBuilder,
|
||||||
|
'powershell': PowerShellDownloadScriptBuilder,
|
||||||
|
};
|
||||||
|
let scriptBuilder = new scriptBuilderMap[args.scriptFormat](downloader);
|
||||||
|
|
||||||
let rawDesc;
|
let rawDesc;
|
||||||
if (args.skipCache) {
|
if (args.skipCache) {
|
||||||
info('Downloading descriptor info (no cache)');
|
info('Downloading descriptor info (no cache)');
|
||||||
@ -291,11 +552,15 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await processDescriptor(rawDesc, args.includePv, args.minDuration);
|
await processDescriptor(rawDesc, args.includePv, args.minDuration, scriptBuilder, danmuSource);
|
||||||
|
|
||||||
if (!args.suppressTmuxWarning && process.env.TMUX == null && process.env.STY == null) {
|
if (!args.suppressTmuxWarning && process.env.TMUX == null && process.env.STY == null) {
|
||||||
info('It seems that you are NOT inside a tmux or screen session!!');
|
info('It seems that you are NOT inside a tmux or screen session!!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main().catch(err => {
|
||||||
|
console.error(`An exception happened: ${err}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user