diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 499b6ebe2..624b2a177 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -24,7 +24,7 @@ body:
请在这里提供你所使用/调用 yutto 的方式。如果与特定 url 有关,请直接在命令中提供该 url。
为了节省彼此交流的时间,麻烦在提交 issue 前多次测试该命令是能够反复复现的(非网络问题),
如果可以,麻烦在提交 issue 前对其他的情况进行测试,并将相关信息详细描述在问题简述中,
- 这里仅提供**最小可复现**的命令
+ 这里仅提供**最小可复现**的命令(注意,如果使用了自定义的配置文件,请将配置文件内容一并提供)。
placeholder: "注意在粘贴的命令中隐去所有隐私信息哦(*/ω\*)"
validations:
required: true
diff --git a/.github/workflows/biliass-build-and-release.yml b/.github/workflows/biliass-build-and-release.yml
index ed2493f99..708de7537 100644
--- a/.github/workflows/biliass-build-and-release.yml
+++ b/.github/workflows/biliass-build-and-release.yml
@@ -2,12 +2,21 @@ name: Build and Release (biliass)
on:
push:
- branches:
- - main
tags:
- "biliass*" # Push events to matching biliass*, i.e. biliass@1.0.0
+ branches:
+ - main
+ paths:
+ - "packages/biliass/**"
+ - ".github/**"
pull_request:
+ paths:
+ - "packages/biliass/**"
+ - ".github/**"
merge_group:
+ paths:
+ - "packages/biliass/**"
+ - ".github/**"
workflow_dispatch:
permissions:
@@ -40,7 +49,7 @@ jobs:
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
- args: --release --out dist
+ args: --release --out dist --interpreter '3.13 3.13t'
sccache: "true"
manylinux: auto
working-directory: packages/biliass
@@ -72,7 +81,7 @@ jobs:
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
- args: --release --out dist
+ args: --release --out dist --interpreter '3.13 3.13t'
sccache: "true"
manylinux: musllinux_1_2
working-directory: packages/biliass
@@ -95,7 +104,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
- python-version: 3.x
+ python-version: "3.13"
architecture: ${{ matrix.platform.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
@@ -110,12 +119,44 @@ jobs:
name: wheels-windows-${{ matrix.platform.target }}
path: packages/biliass/dist
+ # Python 3.13 standard and free-threaded versions cannot be
+ # available at the same time on Windows machines, so we
+ # split it into two jobs.
+ # https://github.com/Quansight-Labs/setup-python/issues/5
+ windows-free-threaded:
+ runs-on: ${{ matrix.platform.runner }}
+ strategy:
+ matrix:
+ platform:
+ - runner: windows-latest
+ target: x64
+ - runner: windows-latest
+ target: x86
+ steps:
+ - uses: actions/checkout@v4
+ - uses: Quansight-Labs/setup-python@v5
+ with:
+ python-version: 3.13t
+ architecture: ${{ matrix.platform.target }}
+ - name: Build wheels
+ uses: PyO3/maturin-action@v1
+ with:
+ target: ${{ matrix.platform.target }}
+ args: --release --out dist
+ sccache: "true"
+ working-directory: packages/biliass
+ - name: Upload wheels
+ uses: actions/upload-artifact@v4
+ with:
+ name: wheels-windows-free-threaded-${{ matrix.platform.target }}
+ path: packages/biliass/dist
+
macos:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- - runner: macos-12
+ - runner: macos-13
target: x86_64
- runner: macos-14
target: aarch64
@@ -128,7 +169,7 @@ jobs:
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
- args: --release --out dist
+ args: --release --out dist --interpreter '3.13 3.13t'
sccache: "true"
working-directory: packages/biliass
- name: Upload wheels
@@ -161,6 +202,7 @@ jobs:
- linux
- musllinux
- windows
+ - windows-free-threaded
- macos
- sdist
permissions:
@@ -174,8 +216,11 @@ jobs:
merge-multiple: true
path: dist/
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+
- name: Publish release distributions to PyPI
- uses: pypa/gh-action-pypi-publish@release/v1
+ run: uv publish -v
publish-release:
runs-on: ubuntu-latest
@@ -185,6 +230,7 @@ jobs:
- linux
- musllinux
- windows
+ - windows-free-threaded
- macos
- sdist
permissions:
diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml
index 43d2ea2f6..23ac6ddfd 100644
--- a/.github/workflows/e2e-test.yml
+++ b/.github/workflows/e2e-test.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.9", "3.10", "3.11", "3.12", "3.13-dev"]
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
architecture: ["x64"]
name: Python ${{ matrix.python-version }} on ${{ matrix.architecture }} e2e test
steps:
@@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@v4
- name: Install uv
- uses: astral-sh/setup-uv@v3
+ uses: astral-sh/setup-uv@v5
- name: Install python
uses: actions/setup-python@v5
diff --git a/.github/workflows/latest-release-test.yml b/.github/workflows/latest-release-test.yml
index 5391f1034..82ace2f0c 100644
--- a/.github/workflows/latest-release-test.yml
+++ b/.github/workflows/latest-release-test.yml
@@ -9,10 +9,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.9", "3.10", "3.11", "3.12", "3.13-dev"]
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
architecture: ["x64"]
name: Python ${{ matrix.python-version }} on ${{ matrix.architecture }} latest release test
steps:
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+
- name: Install python
uses: actions/setup-python@v5
with:
@@ -24,12 +27,36 @@ jobs:
sudo apt update
sudo apt install ffmpeg
- - name: Install yutto latest
+ - name: Test yutto from source
+ run: |
+ uv cache clean
+ uvx --no-binary yutto@latest -v
+ uvx --no-binary yutto@latest -h
+ uvx --no-binary yutto@latest https://www.bilibili.com/video/BV1AZ4y147Yg -w --no-progress
+
+ - name: Test yutto from wheel
+ run: |
+ uv cache clean
+ uvx yutto@latest -v
+ uvx yutto@latest -h
+ uvx yutto@latest https://www.bilibili.com/video/BV1AZ4y147Yg -w --no-progress
+
+ - name: Prepare data for biliass
+ run: |
+ git clone https://github.com/yutto-dev/biliass-corpus.git --depth 1
+
+ - name: Test biliass from source
run: |
- pip install yutto
+ uv cache clean
+ uvx --no-binary biliass@latest -v
+ uvx --no-binary biliass@latest -h
+ uvx --no-binary biliass@latest biliass-corpus/corpus/xml/18678311.xml -s 1920x1080 -f xml -o xml.ass
+ uvx --no-binary biliass@latest biliass-corpus/corpus/protobuf/18678311-0.pb biliass-corpus/corpus/protobuf/18678311-1.pb biliass-corpus/corpus/protobuf/18678311-2.pb biliass-corpus/corpus/protobuf/18678311-3.pb -s 1920x1080 -f protobuf -o protobuf.ass
- - name: Test yutto
+ - name: Test biliass from wheel
run: |
- yutto -v
- yutto -h
- yutto https://www.bilibili.com/video/BV1AZ4y147Yg -w
+ uv cache clean
+ uvx biliass@latest -v
+ uvx biliass@latest -h
+ uvx biliass@latest biliass-corpus/corpus/xml/18678311.xml -s 1920x1080 -f xml -o xml.ass
+ uvx biliass@latest biliass-corpus/corpus/protobuf/18678311-0.pb biliass-corpus/corpus/protobuf/18678311-1.pb biliass-corpus/corpus/protobuf/18678311-2.pb biliass-corpus/corpus/protobuf/18678311-3.pb -s 1920x1080 -f protobuf -o protobuf.ass
diff --git a/.github/workflows/lint-and-fmt.yml b/.github/workflows/lint-and-fmt.yml
index e7ba95901..421b95359 100644
--- a/.github/workflows/lint-and-fmt.yml
+++ b/.github/workflows/lint-and-fmt.yml
@@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@v4
- name: Install uv
- uses: astral-sh/setup-uv@v3
+ uses: astral-sh/setup-uv@v5
- name: Install python
uses: actions/setup-python@v5
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e459ce577..00422dcb0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v4
- name: Install uv
- uses: astral-sh/setup-uv@v3
+ uses: astral-sh/setup-uv@v5
- name: Install python
uses: actions/setup-python@v5
@@ -53,8 +53,11 @@ jobs:
name: release-dists
path: dist/
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+
- name: Publish release distributions to PyPI
- uses: pypa/gh-action-pypi-publish@release/v1
+ run: uv publish -v
publish-release:
runs-on: ubuntu-latest
diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml
index 37853e39e..cdde79c53 100644
--- a/.github/workflows/unit-test.yml
+++ b/.github/workflows/unit-test.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.9", "3.10", "3.11", "3.12", "3.13-dev"]
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
architecture: ["x64"]
name: Python ${{ matrix.python-version }} on ${{ matrix.architecture }} unit test
steps:
@@ -22,7 +22,7 @@ jobs:
submodules: recursive
- name: Install uv
- uses: astral-sh/setup-uv@v3
+ uses: astral-sh/setup-uv@v5
- name: Install python
uses: actions/setup-python@v5
@@ -39,6 +39,12 @@ jobs:
run: |
just ci-install
- - name: unit test
+ - name: Run unit tests
run: |
just ci-test
+
+ - name: Run benchmarks
+ uses: CodSpeedHQ/action@v3
+ if: "matrix.python-version == '3.13' && github.event_name != 'merge_group'"
+ with:
+ run: uv run pytest --codspeed
diff --git a/.gitignore b/.gitignore
index 70737b9c6..b9e1dfbca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -118,6 +118,7 @@ dmypy.json
.DS_Store
# Media files
+*.m4a
*.aac
*.mp3
*.flac
@@ -141,3 +142,6 @@ log
# test files
__test_files__
+
+# config file
+yutto.toml
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3a6e1aef0..a70e6cc86 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -8,5 +8,10 @@
},
"rust-analyzer.linkedProjects": [
"${workspaceFolder}/packages/biliass/rust/Cargo.toml"
- ]
+ ],
+ "search.exclude": {
+ "**/*.ambr": true,
+ "**/*.xml": true,
+ "**/*.pb": true
+ }
}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0ab09d308..1825f7b57 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -24,6 +24,12 @@
当然,如果你有更熟悉的编辑器或 IDE 的话,也是完全可以的。
+### Rust 开发工具链(可选)
+
+本 repo 是一个 monorepo,同时包含 yutto 和 biliass 两个包,其中 biliass 采用 Rust 编写,如果你有 biliass 联调的需求,则需要安装 Rust 工具链,安装方法请参考 [Rust 官方文档](https://www.rust-lang.org/tools/install)
+
+如果你不需要联调 biliass,那么可以通过注释掉 [pyproject.toml](./pyproject.toml) 中的 `tool.uv.sources` 和 `tool.uv.workspace`,避免 uv 将其当作一个子项目来处理。此时 uv 会安装 pypi 上预编译的 biliass wheel 包,而不会编译源码。这在大多数情况下是没有问题的,除非 yutto 使用了 biliass 的最新特性。
+
## 本地调试
如果你想要本地调试,最佳实践是从 GitHub 上下载最新的源码来运行
diff --git a/Dockerfile b/Dockerfile
index b224e247d..d573ca1c0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,13 +1,13 @@
-FROM alpine:3.20
+FROM alpine:3.21
LABEL maintainer="siguremo" \
- version="2.0.0-rc.1" \
+ version="2.0.0" \
description="light-weight container based on alpine for yutto"
RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
&& apk add -q --progress --update --no-cache ffmpeg python3 tzdata \
&& python3 -m venv /opt/venv \
- && /opt/venv/bin/pip install --no-cache-dir --pre --compile yutto \
+ && /opt/venv/bin/pip install --no-cache-dir --compile yutto \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
WORKDIR /app
diff --git a/README.md b/README.md
index eb7574018..40b22dd26 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,8 @@
-# yutto2.0.0-beta
+# yutto2.0.0
+
+
+
+
@@ -12,9 +16,11 @@
-yutto,一个可爱且任性的 B 站下载器(CLI)
+🧊 yutto,一个可爱且任性的 B 站下载器(CLI)
-当前 yutto 尚处于 beta 阶段,有任何建议尽管在 [Discussions](https://github.com/yutto-dev/yutto/discussions) 提出~~~
+> [!TIP]
+>
+> 如果在使用过程中遇到问题,请通过 [Issues](https://github.com/yutto-dev/yutto/issues) 反馈功能正确性问题和功能请求,其他问题请通过 [Discussions](https://github.com/yutto-dev/yutto/discussions) 反馈~
## 版本号为什么是 2.0
@@ -56,14 +62,14 @@ docker run --rm -it -v /path/to/download:/app siguremo/yutto [options]
> 在此之前请确保安装 Python3.9 及以上版本,并配置好 FFmpeg(参照 [bilili 文档](https://bilili.nyakku.moe/guide/getting-started.html))
```bash
-pip install --pre yutto
+pip install yutto
```
当然,你也可以通过 [pipx](https://github.com/pypa/pipx)/[uv](https://github.com/astral-sh/uv) 来安装 yutto(当然,前提是你要自己先安装它)
```bash
-pipx install --pre yutto # 使用 pipx
-uv tool install --pre yutto # 或者使用 uv
+pipx install yutto # 使用 pipx
+uv tool install yutto # 或者使用 uv
```
pipx/uv 会类似 Homebrew 无感地为 yutto 创建一个虚拟环境,与其余环境隔离开,避免污染 pip 的环境,因此相对于 pip,pipx/uv 是更推荐的安装方式(uv 会比 pipx 更快些~)。
@@ -99,8 +105,8 @@ uv tool install git+https://github.com/yutto-dev/yutto.git@main # 通过
| 当前用户稍后再看 批量 | :x: | `https://www.bilibili.com/watchlater` | `稍后再看/{title}/{name}` |
| 用户全部收藏夹 批量 | :x: | `https://space.bilibili.com/100969474/favlist` | `{username}的收藏夹/{series_title}/{title}/{name}` |
| UP 主个人空间 批量 | :x: | `https://space.bilibili.com/100969474/video` | `{username}的全部投稿视频/{title}/{name}` |
-| 合集 批量 | :white_check_mark: | `https://space.bilibili.com/361469957/channel/collectiondetail?sid=23195`
`https://www.bilibili.com/medialist/play/361469957?business=space_collection&business_id=23195` | `{series_title}/{title}` |
-| 视频列表 批量 | :x: | `https://space.bilibili.com/100969474/channel/seriesdetail?sid=1947439`
`https://www.bilibili.com/medialist/play/100969474?business=space_series&business_id=1947439`
`https://space.bilibili.com/100969474/favlist?fid=270359&ftype=collect` | `{series_title}/{title}/{name}` |
+| 合集 批量 | :white_check_mark: | `https://space.bilibili.com/3546619314178489/lists?sid=3221717?type=season`
`https://space.bilibili.com/3546619314178489/channel/collectiondetail?sid=3221717`旧版页面
`https://space.bilibili.com/100969474/favlist?fid=3221717&ftype=collect&ctype=21` | `{series_title}/{title}` |
+| 视频列表 批量 | :x: | `https://space.bilibili.com/100969474/lists/1947439?type=series`
`https://space.bilibili.com/100969474/channel/seriesdetail?sid=1947439`旧版页面
`https://www.bilibili.com/list/100969474?sid=1947439` | `{series_title}/{title}/{name}` |
> [!NOTE]
>
@@ -256,13 +262,13 @@ https://github.com/orgs/community/discussions/16925#discussioncomment-7571187
#### 指定在仅包含音频流时的输出格式
- 参数 `--output-format-audio-only`
-- 可选值 `"infer" | "aac" | "mp3" | "flac" | "mp4" | "mkv" | "mov"`
+- 可选值 `"infer" | "m4a" | "aac" | "mp3" | "flac" | "mp4" | "mkv" | "mov"`
- 默认值 `"infer"`
在仅包含音频流时所使用的输出格式,默认选值 `"infer"` 表示自动根据情况进行推导以保证输出的可用,推导规则如下:
- 如果音频流编码为 `"fLaC"`,则输出格式为 `"flac"`
-- 否则为 `"aac"`
+- 否则为 `"m4a"`
> **Note**
>
@@ -356,12 +362,14 @@ yutto -c "d8bc7493%2C2843925707%2C08c3e*81"
| - | - | - |
| title | 系列视频总标题(番剧名/投稿视频标题) | 全部 |
| id | 系列视频单 p 顺序标号 | 全部 |
+| aid | 视频 AV 号,早期使用的视频 ID,不建议使用,详见 [AV 号全面升级公告](https://www.bilibili.com/blackboard/activity-BV-PC.html) | 全部 |
+| bvid | 视频 BV 号,即视频 ID | 全部 |
| name | 系列视频单 p 标题 | 全部 |
| username | UP 主用户名 | 个人空间、收藏夹、稍后再看、合集、视频列表下载 |
| series_title | 合集标题 | 收藏夹、视频合集、视频列表下载 |
| pubdate🕛 | 投稿日期 | 仅投稿视频 |
| download_date🕛 | 下载日期 | 全部 |
-| owner_uid | UP 主UID | 个人空间、收藏夹、稍后再看、合集、视频列表下载 |
+| owner_uid | UP 主 UID | 个人空间、收藏夹、稍后再看、合集、视频列表下载 |
> **Note**
>
@@ -392,6 +400,62 @@ yutto tensura1 --batch --alias-file='~/.yutto_alias'
cat ~/.yutto_alias | yutto tensura-nikki --batch --alias-file -
```
+#### 指定媒体元数据值的格式
+
+当前仅支持 `premiered`
+
+- 参数 `--metadata-format-premiered`
+- 默认值 `"%Y-%m-%d"`
+- 常用值 `"%Y-%m-%d %H:%M:%S"`
+
+#### 严格校验大会员状态有效
+
+- 参数 `--vip-strict`
+- 默认值 `False`
+
+#### 严格校验登录状态有效
+
+- 参数 `--login-strict`
+- 默认值 `False`
+
+#### 设置下载间隔
+
+- 参数 `--download-interval`
+- 默认值 `0`
+
+设置两话之间的下载间隔(单位为秒),避免短时间內下载大量视频导致账号被封禁
+
+#### 禁用下载镜像
+
+- 参数 `--banned-mirrors-pattern`
+- 默认值 `None`
+
+使用正则禁用特定镜像,比如 `--banned-mirrors-pattern "mirrorali"` 将禁用 url 中包含 `mirrorali` 的镜像
+
+#### 不显示颜色
+
+- 参数 `--no-color`
+- 默认值 `False`
+
+#### 不显示进度条
+
+- 参数 `--no-progress`
+- 默认值 `False`
+
+#### 启用 Debug 模式
+
+- 参数 `--debug`
+- 默认值 `False`
+
+
+
+### 资源选择参数
+
+此外有一些参数专用于资源选择,比如选择是否下载弹幕、音频、视频等等。
+
+
+点击展开详细参数
+
#### 仅下载视频流
- 参数 `--video-only`
@@ -408,20 +472,13 @@ cat ~/.yutto_alias | yutto tensura-nikki --batch --alias-file -
- 参数 `--audio-only`
- 默认值 `False`
-仅下载其中的音频流,保存为 `.aac` 文件。
+仅下载其中的音频流,保存为 `.m4a` 文件。
#### 不生成弹幕文件
- 参数 `--no-danmaku`
- 默认值 `False`
-#### 不生成章节信息
-
-- 参数 `--no-chapter-info`
-- 默认值 `False`
-
-不生成章节信息,包含 MetaData 和嵌入视频流的章节信息。
-
#### 仅生成弹幕文件
- 参数 `--danmaku-only`
@@ -458,53 +515,99 @@ cat ~/.yutto_alias | yutto tensura-nikki --batch --alias-file -
>
> 当前仅支持为包含视频流的视频生成封面。
-#### 指定媒体元数据值的格式
+#### 生成视频流封面时单独保存封面
-当前仅支持 `premiered`
+- 参数 `--save-cover`
+- 默认值 `False`
-- 参数 `--metadata-format-premiered`
-- 默认值 `"%Y-%m-%d"`
-- 常用值 `"%Y-%m-%d %H:%M:%S"`
+#### 仅生成视频封面
-#### 严格校验大会员状态有效
+- 参数 `--cover-only`
+- 默认值 `False`
-- 参数 `--vip-strict`
+#### 不生成章节信息
+
+- 参数 `--no-chapter-info`
- 默认值 `False`
-#### 严格校验登录状态有效
+不生成章节信息,包含 MetaData 和嵌入视频流的章节信息。
-- 参数 `--login-strict`
+
+
+### 弹幕设置参数Experimental
+
+yutto 通过与 biliass 的集成,提供了一些 ASS 弹幕选项,包括字号、字体、速度等~
+
+
+点击展开详细参数
+
+#### 弹幕字体大小
+
+- 参数 `--danmaku-font-size`
+- 默认值 `video_width / 40`
+
+#### 弹幕字体
+
+- 参数 `--danmaku-font`
+- 默认值 `"SimHei"`
+
+#### 弹幕不透明度
+
+- 参数 `--danmaku-opacity`
+- 默认值 `0.8`
+
+#### 弹幕显示区域与视频高度的比例
+
+- 参数 `--danmaku-display-region-ratio`
+- 默认值 `1.0`
+
+#### 弹幕速度
+
+- 参数 `--danmaku-speed`
+- 默认值 `1.0`
+
+#### 屏蔽顶部弹幕
+
+- 参数 `--danmaku-block-top`
- 默认值 `False`
-#### 设置下载间隔
+#### 屏蔽底部弹幕
-- 参数 `--download-interval`
-- 默认值 `0`
+- 参数 `--danmaku-block-bottom`
+- 默认值 `False`
-设置两话之间的下载间隔(单位为秒),避免短时间內下载大量视频导致账号被封禁
+#### 屏蔽滚动弹幕
-#### 禁用下载镜像
+- 参数 `--danmaku-block-scroll`
+- 默认值 `False`
-- 参数 `--banned-mirrors-pattern`
-- 默认值 `None`
+#### 屏蔽逆向弹幕
-使用正则禁用特定镜像,比如 `--banned-mirrors-pattern "mirrorali"` 将禁用 url 中包含 `mirrorali` 的镜像
+- 参数 `--danmaku-block-reverse`
+- 默认值 `False`
-#### 不显示颜色
+#### 屏蔽固定弹幕(顶部、底部)
-- 参数 `--no-color`
+- 参数 `--danmaku-block-fixed`
- 默认值 `False`
-#### 不显示进度条
+#### 屏蔽高级弹幕
-- 参数 `--no-progress`
+- 参数 `--danmaku-block-special`
- 默认值 `False`
-#### 启用 Debug 模式
+#### 屏蔽彩色弹幕
-- 参数 `--debug`
+- 参数 `--danmaku-block-colorful`
- 默认值 `False`
+#### 屏蔽关键词
+
+- 参数 `--danmaku-block-keyword-patterns`
+- 默认值 `None`
+
+按关键词屏蔽,支持正则,使用 `,` 分隔
+
### 批量参数
@@ -591,6 +694,57 @@ yutto -b -p "~3,10,12~14,16,-4~"
+### 配置文件Experimental
+
+yutto 自 `2.0.0-rc.3` 起增加了实验性的配置文件功能,你可以通过 `--config` 选项来指定配置文件路径,比如
+
+```bash
+yutto --config /path/to/config.toml
+```
+
+如果不指定配置文件路径,yutto 也支持配置自动发现,根据优先级,搜索路径如下:
+
+- 当前目录下的 `yutto.toml`
+- 搜索 [`XDG_CONFIG_HOME`](https://specifications.freedesktop.org/basedir-spec/latest/) 下的 `yutto/yutto.toml` 文件
+- 非 Windows 系统下的 `~/.config/yutto/yutto.toml`,Windows 系统下的 `~/AppData/Roaming/yutto/yutto.toml`
+
+你可以通过配置文件来设置一些默认参数,整体上与命令行参数基本一致,下面以一些示例来展示配置文件的写法:
+
+```toml
+# yutto.toml
+#:schema https://raw.githubusercontent.com/yutto-dev/yutto/refs/heads/main/schemas/config.json
+[basic]
+# 设置下载目录
+dir = "/path/to/download"
+# 设置临时文件目录
+tmp_dir = "/path/to/tmp"
+# 设置 SESSDATA
+sessdata = "***************"
+# 设置大会员严格校验
+vip_strict = true
+# 设置登录严格校验
+login_strict = true
+
+[resource]
+# 不下载字幕
+require_subtitle = false
+
+[danmaku]
+# 设置弹幕速度
+speed = 2.0
+# 设置弹幕屏蔽关键词
+block_keyword_patterns = [
+ ".*keyword1.*",
+ ".*keyword2.*",
+]
+
+[batch]
+# 下载额外剧集
+with_section = true
+```
+
+如果你使用 VS Code 对配置文件编辑,强烈建议使用 [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml) 扩展,配合 yutto 提供的 schema,可以获得最佳的提示体验。
+
## 从 bilili1.x 迁移
### 取消的功能
@@ -633,6 +787,41 @@ yutto -b -p "~3,10,12~14,16,-4~"
yutto --no-color --no-progress > log
```
+### 使用配置自定义默认参数
+
+如果你希望修改 yutto 的部分参数,那么可能每次运行都需要在后面加上长长一串选项,为了避免这个问题,你可以尝试使用配置文件
+
+```toml
+# ~/.config/yutto/yutto.toml
+#:schema https://raw.githubusercontent.com/yutto-dev/yutto/refs/heads/main/schemas/config.json
+[basic]
+dir = "~/Movies/yutto"
+sessdata = "***************"
+num_workers = 16
+vcodec = "av1:copy"
+```
+
+当然,请手动修改 `sessdata` 内容为自己的 `SESSDATA` 哦~
+
+> [!TIP]
+>
+> 本方案可替代原有的「自定义命令别名」方式~
+>
+>
+> 原「自定义命令别名」方案
+>
+> 在 `~/.zshrc` / `~/.bashrc` 中自定义一条 alias,像这样
+>
+> ```bash
+> alias ytt='yutto -d ~/Movies/yutto/ -c `cat ~/.sessdata` -n 16 --vcodec="av1:copy"'
+> ```
+>
+> 这样我每次只需要 `ytt ` 就可以直接使用这些参数进行下载啦~
+>
+> 由于我提前在 `~/.sessdata` 存储了我的 `SESSDATA`,所以避免每次都要手动输入 cookie 的问题。
+>
+>
+
### 使用 url alias
yutto 新增的 url alias 可以让你下载正在追的番剧时不必每次都打开浏览器复制 url,只需要将追番列表存储在一个文件中,并为这些 url 起一个别名即可
@@ -647,6 +836,15 @@ tensura-nikki=https://www.bilibili.com/bangumi/play/ss38221/
yutto --batch tensura-nikki --alias-file=/path/to/alias-file
```
+你同样可以通过配置文件来实现这一点(推荐)
+
+```toml
+# ~/.config/yutto/yutto.toml
+#:schema https://raw.githubusercontent.com/yutto-dev/yutto/refs/heads/main/schemas/config.json
+[basic.aliases]
+tensura-nikki = "https://www.bilibili.com/bangumi/play/ss38221/"
+```
+
### 使用任务列表
现在 url 不仅支持 http/https 链接与裸 id,还支持使用文件路径与 file scheme 来用于表示文件列表,文件列表以行分隔,每行写一次命令的参数,该参数会覆盖掉主程序中所使用的参数,示例如下:
@@ -691,20 +889,6 @@ yutto file:///path/to/list --vcodec="avc:copy"
最后,列表也是支持嵌套的哦(虽然没什么用 2333)
-### 自定义命令别名
-
-如果你不习惯于 yutto 的默认参数,那么可能每次运行都需要在后面加上长长一串参数,为了避免这一点,我是这样做的:
-
-在 `~/.zshrc` / `~/.bashrc` 中自定义一条 alias,像这样
-
-```bash
-alias ytt='yutto -d ~/Movies/yutto/ -c `cat ~/.sessdata` -n 16 --vcodec="av1:copy"'
-```
-
-这样我每次只需要 `ytt ` 就可以直接使用这些参数进行下载啦~
-
-由于我提前在 `~/.sessdata` 存储了我的 `SESSDATA`,所以避免每次都要手动输入 cookie 的问题。
-
## FAQ
### 名字的由来
@@ -717,7 +901,7 @@ yutto 添加任何特性都需要以保证可维护性为前提,因此 yutto
### yutto 会替代 bilili 吗
-yutto 自诞生以来已经过去三年多了,功能上基本可以替代 bilili 了,因此 bilili 将会在 yutto 正式版发布后正式停止维护~(咳,正式版还要再过段时间~ ○ω●)
+yutto 自诞生以来已经过去三年多了,功能上基本可以替代 bilili 了,由于 B 站接口的不断变化,bilili 也不再适用于现在的环境,因此请 bilili 用户尽快迁移到 yutto ~
## 其他应用
@@ -727,21 +911,21 @@ yutto 自诞生以来已经过去三年多了,功能上基本可以替代 bili
## Roadmap
-### 2.0.0-rc
-
-- [x] feat: 投稿视频描述文件支持
-- [x] refactor: 整理路径变量名
-- [x] feat: 视频合集选集支持(合集貌似有取代分 p 的趋势,需要对其进行合适的处理)
-- [x] refactor: 重写 biliass
-
### 2.0.0
-- [ ] refactor: 针对视频合集优化路径变量
-- [ ] refactor: 优化杜比视界/音效/全景声选取逻辑(Discussing in [#62](https://github.com/yutto-dev/yutto/discussions/62))
-- [ ] docs: 可爱的静态文档(WIP in [#86](https://github.com/yutto-dev/yutto/pull/86))
+- [x] feat: 支持弹幕字体、字号、速度等设置
+- [x] feat: 配置文件支持
+- [x] feat: 配置文件功能优化,支持自定义配置路径
+- [x] docs: issue template 添加配置引导
+- [x] docs: 优化 biliass rust 重构后的贡献指南
### future
+- [ ] docs: 可爱的静态文档(WIP in [#86](https://github.com/yutto-dev/yutto/pull/86))
+- [ ] feat: 新的基于 toml 的任务列表
+- [ ] refactor: 配置参数复用 pydantic 验证
+- [ ] refactor: 针对视频合集优化路径变量
+- [ ] refactor: 优化杜比视界/音效/全景声选取逻辑(Discussing in [#62](https://github.com/yutto-dev/yutto/discussions/62))
- [ ] refactor: 直接使用 rich 替代内置的终端显示模块
- [ ] feat: 更多批下载支持
- [ ] feat: 以及更加可爱~
diff --git a/justfile b/justfile
index 7eaeb4023..8605c6c7f 100644
--- a/justfile
+++ b/justfile
@@ -18,7 +18,7 @@ fmt:
uv run ruff format .
lint:
- uv run pyright src/yutto tests
+ uv run pyright src/yutto packages/biliass/src/biliass tests
uv run ruff check .
uv run typos
@@ -45,6 +45,7 @@ clean:
-e mp4 \
-e mkv \
-e mov \
+ -e m4a \
-e aac \
-e mp3 \
-e flac \
@@ -66,6 +67,9 @@ clean-builds:
rm -rf dist/
rm -rf yutto.egg-info/
+generate-schema:
+ uv run scripts/generate-schema.py
+
# CI specific
ci-install:
uv sync --all-extras --dev
diff --git a/logo/logo.png b/logo/logo.png
new file mode 100644
index 000000000..c88a718cd
Binary files /dev/null and b/logo/logo.png differ
diff --git a/packages/biliass/README.md b/packages/biliass/README.md
index 1c26cb4dc..cce8d25da 100644
--- a/packages/biliass/README.md
+++ b/packages/biliass/README.md
@@ -7,13 +7,10 @@
+
-biliass,只是 Danmaku2ASS 的 bilili 与 yutto 适配版
-
-原版:
-
-仅支持 bilibili 弹幕,支持 XML 弹幕和 Protobuf 弹幕
+biliass,高性能且易于使用的 bilibili 弹幕转换工具(XML/Protobuf 格式转 ASS),基于 [Danmaku2ASS](https://github.com/m13253/danmaku2ass),使用 rust 重写
## Install
@@ -36,36 +33,32 @@ from biliass import convert_to_ass
# xml
convert_to_ass(
xml_text_or_bytes,
- width,
- height,
+ 1920,
+ 1080,
input_format="xml",
- reserve_blank=0,
+ display_region_ratio=1.0,
font_face="sans-serif",
- font_size=width / 40,
+ font_size=25,
text_opacity=0.8,
duration_marquee=15.0,
duration_still=10.0,
- comment_filter=None,
- is_reduce_comments=False,
+ block_options=None,
+ reduce_comments=False,
)
# protobuf
convert_to_ass(
protobuf_bytes, # only bytes
- width,
- height,
+ 1920,
+ 1080,
input_format="protobuf",
- reserve_blank=0,
+ display_region_ratio=1.0,
font_face="sans-serif",
- font_size=width / 40,
+ font_size=25,
text_opacity=0.8,
duration_marquee=15.0,
duration_still=10.0,
- comment_filter=None,
- is_reduce_comments=False,
+ block_options=None,
+ reduce_comments=False,
)
```
-
-## TODO
-
-- 导出 bilibili 网页上的弹幕设置,并导入到 biliass
diff --git a/packages/biliass/pyproject.toml b/packages/biliass/pyproject.toml
index 679c8f9c2..a41459a74 100644
--- a/packages/biliass/pyproject.toml
+++ b/packages/biliass/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "biliass"
-description = "将 B 站弹幕转换为 ASS 弹幕"
+description = "💬 将 B 站 XML/protobuf 弹幕转换为 ASS 弹幕"
readme = "README.md"
requires-python = ">=3.9"
authors = [
diff --git a/packages/biliass/rust/Cargo.lock b/packages/biliass/rust/Cargo.lock
index 1e3255173..eb0d41e49 100644
--- a/packages/biliass/rust/Cargo.lock
+++ b/packages/biliass/rust/Cargo.lock
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 3
+version = 4
[[package]]
name = "ahash"
@@ -49,7 +49,7 @@ checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
[[package]]
name = "biliass-core"
-version = "2.0.0"
+version = "2.2.0"
dependencies = [
"bytes",
"cached",
@@ -58,10 +58,11 @@ dependencies = [
"protox",
"pyo3",
"quick-xml",
+ "rayon",
"regex",
"serde",
"serde_json",
- "thiserror",
+ "thiserror 2.0.11",
"tracing",
"tracing-subscriber",
]
@@ -80,22 +81,22 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytes"
-version = "1.7.2"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "cached"
-version = "0.53.1"
+version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4d73155ae6b28cf5de4cfc29aeb02b8a1c6dab883cb015d15cd514e42766846"
+checksum = "9718806c4a2fe9e8a56fd736f97b340dd10ed1be8ed733ed50449f351dc33cae"
dependencies = [
"ahash",
"cached_proc_macro",
"cached_proc_macro_types",
"hashbrown",
"once_cell",
- "thiserror",
+ "thiserror 1.0.68",
"web-time",
]
@@ -123,6 +124,31 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
+
[[package]]
name = "darling"
version = "0.20.10"
@@ -340,7 +366,7 @@ checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1"
dependencies = [
"cfg-if",
"miette-derive",
- "thiserror",
+ "thiserror 1.0.68",
"unicode-width",
]
@@ -426,9 +452,9 @@ dependencies = [
[[package]]
name = "prost"
-version = "0.13.3"
+version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f"
+checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec"
dependencies = [
"bytes",
"prost-derive",
@@ -436,11 +462,10 @@ dependencies = [
[[package]]
name = "prost-build"
-version = "0.13.3"
+version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15"
+checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b"
dependencies = [
- "bytes",
"heck",
"itertools",
"log",
@@ -457,9 +482,9 @@ dependencies = [
[[package]]
name = "prost-derive"
-version = "0.13.3"
+version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5"
+checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
dependencies = [
"anyhow",
"itertools",
@@ -483,18 +508,18 @@ dependencies = [
[[package]]
name = "prost-types"
-version = "0.13.3"
+version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670"
+checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc"
dependencies = [
"prost",
]
[[package]]
name = "protox"
-version = "0.7.1"
+version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "873f359bdecdfe6e353752f97cb9ee69368df55b16363ed2216da85e03232a58"
+checksum = "6f352af331bf637b8ecc720f7c87bf903d2571fa2e14a66e9b2558846864b54a"
dependencies = [
"bytes",
"miette",
@@ -502,7 +527,7 @@ dependencies = [
"prost-reflect",
"prost-types",
"protox-parse",
- "thiserror",
+ "thiserror 1.0.68",
]
[[package]]
@@ -514,14 +539,14 @@ dependencies = [
"logos",
"miette",
"prost-types",
- "thiserror",
+ "thiserror 1.0.68",
]
[[package]]
name = "pyo3"
-version = "0.22.3"
+version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15ee168e30649f7f234c3d49ef5a7a6cbf5134289bc46c29ff3155fa3221c225"
+checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc"
dependencies = [
"cfg-if",
"indoc",
@@ -537,9 +562,9 @@ dependencies = [
[[package]]
name = "pyo3-build-config"
-version = "0.22.3"
+version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e61cef80755fe9e46bb8a0b8f20752ca7676dcc07a5277d8b7768c6172e529b3"
+checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7"
dependencies = [
"once_cell",
"target-lexicon",
@@ -547,9 +572,9 @@ dependencies = [
[[package]]
name = "pyo3-ffi"
-version = "0.22.3"
+version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67ce096073ec5405f5ee2b8b31f03a68e02aa10d5d4f565eca04acc41931fa1c"
+checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d"
dependencies = [
"libc",
"pyo3-build-config",
@@ -557,9 +582,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
-version = "0.22.3"
+version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2440c6d12bc8f3ae39f1e775266fa5122fd0c8891ce7520fa6048e683ad3de28"
+checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@@ -569,9 +594,9 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
-version = "0.22.3"
+version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1be962f0e06da8f8465729ea2cb71a416d2257dff56cbe40a70d3e62a93ae5d1"
+checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4"
dependencies = [
"heck",
"proc-macro2",
@@ -582,9 +607,9 @@ dependencies = [
[[package]]
name = "quick-xml"
-version = "0.36.2"
+version = "0.37.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
+checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003"
dependencies = [
"memchr",
]
@@ -598,11 +623,31 @@ dependencies = [
"proc-macro2",
]
+[[package]]
+name = "rayon"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
[[package]]
name = "regex"
-version = "1.11.0"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
@@ -648,18 +693,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
-version = "1.0.210"
+version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
+checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.210"
+version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
+checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
@@ -668,9 +713,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.128"
+version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
+checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
dependencies = [
"itoa",
"memchr",
@@ -701,9 +746,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
-version = "2.0.77"
+version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
+checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
@@ -731,18 +776,38 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "1.0.64"
+version = "1.0.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892"
+dependencies = [
+ "thiserror-impl 1.0.68",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
+dependencies = [
+ "thiserror-impl 2.0.11",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
+checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
dependencies = [
- "thiserror-impl",
+ "proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.64"
+version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
+checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [
"proc-macro2",
"quote",
@@ -761,9 +826,9 @@ dependencies = [
[[package]]
name = "tracing"
-version = "0.1.40"
+version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
@@ -772,9 +837,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.27"
+version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
@@ -783,9 +848,9 @@ dependencies = [
[[package]]
name = "tracing-core"
-version = "0.1.32"
+version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
"valuable",
@@ -804,9 +869,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
-version = "0.3.18"
+version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
+checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"nu-ansi-term",
"sharded-slab",
diff --git a/packages/biliass/rust/Cargo.toml b/packages/biliass/rust/Cargo.toml
index 3f4261259..55e000574 100644
--- a/packages/biliass/rust/Cargo.toml
+++ b/packages/biliass/rust/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "biliass-core"
-version = "2.0.0"
+version = "2.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -9,18 +9,23 @@ name = "biliass_core"
crate-type = ["cdylib"]
[dependencies]
-pyo3 = { version = "0.22.3", features = ["abi3-py39"] }
-bytes = "1.7.2"
+pyo3 = { version = "0.23.2", features = ["abi3-py39"] }
+bytes = "1.9.0"
prost = "0.13.3"
-thiserror = "1.0.63"
-quick-xml = "0.36.2"
-cached = "0.53.1"
-serde = "1.0.210"
-serde_json = "1.0.128"
-regex = "1.10.6"
-tracing = "0.1.40"
+thiserror = "2.0.11"
+quick-xml = "0.37.1"
+cached = "0.54.0"
+serde = "1.0.215"
+serde_json = "1.0.133"
+regex = "1.11.1"
+tracing = "0.1.41"
tracing-subscriber = "0.3.18"
+rayon = "1.10.0"
[build-dependencies]
prost-build = "0.13.3"
protox = "0.7.1"
+
+[profile.release]
+lto = true # Enables link to optimizations
+opt-level = "s" # Optimize for binary size
diff --git a/packages/biliass/rust/src/comment.rs b/packages/biliass/rust/src/comment.rs
index 8386ba6d4..6ab7b87e0 100644
--- a/packages/biliass/rust/src/comment.rs
+++ b/packages/biliass/rust/src/comment.rs
@@ -12,6 +12,55 @@ pub enum CommentPosition {
Special,
}
+#[derive(Debug, PartialEq, Clone)]
+pub struct NormalCommentData {
+ /// The estimated height in pixels
+ /// i.e. (comment.count('\n')+1)*size
+ pub height: f32,
+ /// The estimated width in pixels
+ /// i.e. calculate_length(comment)*size
+ pub width: f32,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct SpecialCommentData {
+ pub rotate_y: i64,
+ pub rotate_z: i64,
+ pub from_x: f64,
+ pub from_y: f64,
+ pub to_x: f64,
+ pub to_y: f64,
+ pub from_alpha: u8,
+ pub to_alpha: u8,
+ pub delay: i64,
+ pub lifetime: f64,
+ pub duration: i64,
+ pub fontface: String,
+ pub is_border: bool,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum CommentData {
+ Normal(NormalCommentData),
+ Special(SpecialCommentData),
+}
+
+impl CommentData {
+ pub fn as_normal(&self) -> Result<&NormalCommentData, &str> {
+ match self {
+ CommentData::Normal(data) => Ok(data),
+ CommentData::Special(_) => Err("CommentData is Special"),
+ }
+ }
+
+ pub fn as_special(&self) -> Result<&SpecialCommentData, &str> {
+ match self {
+ CommentData::Normal(_) => Err("CommentData is Normal"),
+ CommentData::Special(data) => Ok(data),
+ }
+ }
+}
+
#[derive(Debug, PartialEq, Clone)]
pub struct Comment {
/// The position when the comment is replayed
@@ -21,7 +70,7 @@ pub struct Comment {
/// A sequence of 1, 2, 3, ..., used for sorting
pub no: u64,
/// The content of the comment
- pub comment: String,
+ pub content: String,
/// The comment position
pub pos: CommentPosition,
/// Font color represented in 0xRRGGBB,
@@ -29,10 +78,6 @@ pub struct Comment {
pub color: u32,
/// Font size
pub size: f32,
- /// The estimated height in pixels
- /// i.e. (comment.count('\n')+1)*size
- pub height: f32,
- /// The estimated width in pixels
- /// i.e. calculate_length(comment)*size
- pub width: f32,
+ /// The comment data
+ pub data: CommentData,
}
diff --git a/packages/biliass/rust/src/convert.rs b/packages/biliass/rust/src/convert.rs
index 2aca59844..fd5aba3b5 100644
--- a/packages/biliass/rust/src/convert.rs
+++ b/packages/biliass/rust/src/convert.rs
@@ -1,44 +1,36 @@
use crate::comment::{Comment, CommentPosition};
use crate::error::BiliassError;
+use crate::filter::BlockOptions;
use crate::writer;
use crate::writer::rows;
-use regex::Regex;
+use rayon::prelude::*;
#[allow(clippy::too_many_arguments)]
pub fn process_comments(
comments: &Vec,
width: u32,
height: u32,
- bottom_reserved: u32,
+ zoom_factor: (f32, f32, f32),
+ display_region_ratio: f32,
fontface: &str,
fontsize: f32,
alpha: f32,
duration_marquee: f64,
duration_still: f64,
- filters_regex: Vec,
reduced: bool,
) -> Result {
let styleid = "biliass";
let mut ass_result = "".to_owned();
ass_result += &writer::ass::write_head(width, height, fontface, fontsize, alpha, styleid);
+ let bottom_reserved = ((height as f32) * (1. - display_region_ratio)) as u32;
let mut rows = rows::init_rows(4, (height - bottom_reserved + 1) as usize);
- let compiled_regexes_res: Result, regex::Error> = filters_regex
- .into_iter()
- .map(|pattern| Regex::new(&pattern))
- .collect();
- let compiled_regexes = compiled_regexes_res.map_err(BiliassError::from)?;
+
for comment in comments {
match comment.pos {
CommentPosition::Scroll
| CommentPosition::Bottom
| CommentPosition::Top
| CommentPosition::Reversed => {
- if compiled_regexes
- .iter()
- .any(|regex| regex.is_match(&comment.comment))
- {
- continue;
- };
ass_result += &writer::ass::write_normal_comment(
rows.as_mut(),
comment,
@@ -53,7 +45,13 @@ pub fn process_comments(
);
}
CommentPosition::Special => {
- ass_result += &writer::ass::write_special_comment(comment, width, height, styleid);
+ ass_result += &writer::ass::write_special_comment(
+ comment,
+ width,
+ height,
+ zoom_factor,
+ styleid,
+ );
}
}
}
@@ -66,46 +64,61 @@ pub fn convert_to_ass(
reader: Reader,
stage_width: u32,
stage_height: u32,
- reserve_blank: u32,
+ display_region_ratio: f32,
font_face: &str,
font_size: f32,
text_opacity: f32,
duration_marquee: f64,
duration_still: f64,
- comment_filters: Vec,
is_reduce_comments: bool,
+ block_options: &BlockOptions,
) -> Result
where
- Reader: Fn(Input, f32) -> Result, BiliassError>,
+ Reader: Fn(Input, f32, (f32, f32, f32), &BlockOptions) -> Result, BiliassError>
+ + Send
+ + Sync,
+ Input: Send,
{
+ let zoom_factor = crate::writer::utils::get_zoom_factor(
+ crate::reader::special::BILI_PLAYER_SIZE,
+ (stage_width, stage_height),
+ );
let comments_result: Result>, BiliassError> = inputs
- .into_iter()
- .map(|input| reader(input, font_size))
+ .into_par_iter()
+ .map(|input| reader(input, font_size, zoom_factor, block_options))
.collect();
+
let comments = comments_result?;
let mut comments = comments.concat();
+ if !block_options.block_keyword_patterns.is_empty() {
+ comments.retain(|comment| {
+ !block_options
+ .block_keyword_patterns
+ .iter()
+ .any(|regex| regex.is_match(&comment.content))
+ });
+ }
+ if block_options.block_colorful {
+ comments.retain(|comment| comment.color == 0xffffff);
+ }
comments.sort_by(|a, b| {
(
a.timeline,
a.timestamp,
a.no,
- &a.comment,
+ &a.content,
&a.pos,
a.color,
a.size,
- a.height,
- a.width,
)
.partial_cmp(&(
b.timeline,
b.timestamp,
b.no,
- &b.comment,
+ &b.content,
&b.pos,
b.color,
a.size,
- a.height,
- a.width,
))
.unwrap_or(std::cmp::Ordering::Less)
});
@@ -113,13 +126,13 @@ where
&comments,
stage_width,
stage_height,
- reserve_blank,
+ zoom_factor,
+ display_region_ratio,
font_face,
font_size,
text_opacity,
duration_marquee,
duration_still,
- comment_filters,
is_reduce_comments,
)
}
diff --git a/packages/biliass/rust/src/filter.rs b/packages/biliass/rust/src/filter.rs
new file mode 100644
index 000000000..45d570f3d
--- /dev/null
+++ b/packages/biliass/rust/src/filter.rs
@@ -0,0 +1,20 @@
+use crate::comment::CommentPosition;
+use regex::Regex;
+
+#[derive(Default, Clone)]
+pub struct BlockOptions {
+ pub block_top: bool,
+ pub block_bottom: bool,
+ pub block_scroll: bool,
+ pub block_reverse: bool,
+ pub block_special: bool,
+ pub block_colorful: bool,
+ pub block_keyword_patterns: Vec,
+}
+
+pub fn should_skip_parse(pos: &CommentPosition, block_options: &BlockOptions) -> bool {
+ matches!(pos, CommentPosition::Top) && block_options.block_top
+ || matches!(pos, CommentPosition::Bottom) && block_options.block_bottom
+ || matches!(pos, CommentPosition::Scroll) && block_options.block_scroll
+ || matches!(pos, CommentPosition::Special) && block_options.block_reverse
+}
diff --git a/packages/biliass/rust/src/lib.rs b/packages/biliass/rust/src/lib.rs
index 7c81e8e92..ca7de1413 100644
--- a/packages/biliass/rust/src/lib.rs
+++ b/packages/biliass/rust/src/lib.rs
@@ -1,6 +1,7 @@
mod comment;
mod convert;
mod error;
+mod filter;
mod logging;
mod proto;
mod python;
@@ -18,12 +19,14 @@ impl std::convert::From for PyErr {
}
/// Bindings for biliass core.
-#[pymodule]
+#[pymodule(gil_used = false)]
#[pyo3(name = "_core")]
fn biliass_pyo3(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(python::py_get_danmaku_meta_size, m)?)?;
m.add_function(wrap_pyfunction!(python::py_xml_to_ass, m)?)?;
m.add_function(wrap_pyfunction!(python::py_protobuf_to_ass, m)?)?;
m.add_function(wrap_pyfunction!(python::py_enable_tracing, m)?)?;
+ m.add_class::()?;
+ m.add_class::()?;
Ok(())
}
diff --git a/packages/biliass/rust/src/python/convert.rs b/packages/biliass/rust/src/python/convert.rs
index 89065c238..6b27dc2d0 100644
--- a/packages/biliass/rust/src/python/convert.rs
+++ b/packages/biliass/rust/src/python/convert.rs
@@ -1,68 +1,163 @@
+use crate::error::BiliassError;
+use crate::filter::BlockOptions;
use crate::{convert, reader};
-
use pyo3::{
prelude::*,
pybacked::{PyBackedBytes, PyBackedStr},
};
+use regex::Regex;
+
+#[pyclass(name = "BlockOptions")]
+#[derive(Clone)]
+pub struct PyBlockOptions {
+ pub block_top: bool,
+ pub block_bottom: bool,
+ pub block_scroll: bool,
+ pub block_reverse: bool,
+ pub block_special: bool,
+ pub block_colorful: bool,
+ pub block_keyword_patterns: Vec,
+}
+impl PyBlockOptions {
+ pub fn to_block_options(&self) -> Result {
+ let block_keyword_patterns_res: Result, regex::Error> = self
+ .block_keyword_patterns
+ .iter()
+ .map(|pattern| regex::Regex::new(pattern))
+ .collect();
+ let block_keyword_patterns = block_keyword_patterns_res.map_err(BiliassError::from)?;
+ Ok(BlockOptions {
+ block_top: self.block_top,
+ block_bottom: self.block_bottom,
+ block_scroll: self.block_scroll,
+ block_reverse: self.block_reverse,
+ block_special: self.block_special,
+ block_colorful: self.block_colorful,
+ block_keyword_patterns,
+ })
+ }
+}
+
+#[pymethods]
+impl PyBlockOptions {
+ #[new]
+ fn new(
+ block_top: bool,
+ block_bottom: bool,
+ block_scroll: bool,
+ block_reverse: bool,
+ block_special: bool,
+ block_colorful: bool,
+ block_keyword_patterns: Vec,
+ ) -> PyResult {
+ Ok(PyBlockOptions {
+ block_top,
+ block_bottom,
+ block_scroll,
+ block_reverse,
+ block_special,
+ block_colorful,
+ block_keyword_patterns,
+ })
+ }
+
+ #[staticmethod]
+ pub fn default() -> Self {
+ PyBlockOptions {
+ block_top: false,
+ block_bottom: false,
+ block_scroll: false,
+ block_reverse: false,
+ block_special: false,
+ block_colorful: false,
+ block_keyword_patterns: vec![],
+ }
+ }
+}
+
+#[pyclass(name = "ConversionOptions")]
+pub struct PyConversionOptions {
+ pub stage_width: u32,
+ pub stage_height: u32,
+ pub display_region_ratio: f32,
+ pub font_face: String,
+ pub font_size: f32,
+ pub text_opacity: f32,
+ pub duration_marquee: f64,
+ pub duration_still: f64,
+ pub is_reduce_comments: bool,
+}
+
+#[pymethods]
#[allow(clippy::too_many_arguments)]
+impl PyConversionOptions {
+ #[new]
+ fn new(
+ stage_width: u32,
+ stage_height: u32,
+ display_region_ratio: f32,
+ font_face: String,
+ font_size: f32,
+ text_opacity: f32,
+ duration_marquee: f64,
+ duration_still: f64,
+ is_reduce_comments: bool,
+ ) -> Self {
+ PyConversionOptions {
+ stage_width,
+ stage_height,
+ display_region_ratio,
+ font_face,
+ font_size,
+ text_opacity,
+ duration_marquee,
+ duration_still,
+ is_reduce_comments,
+ }
+ }
+}
+
#[pyfunction(name = "xml_to_ass")]
pub fn py_xml_to_ass(
inputs: Vec,
- stage_width: u32,
- stage_height: u32,
- reserve_blank: u32,
- font_face: &str,
- font_size: f32,
- text_opacity: f32,
- duration_marquee: f64,
- duration_still: f64,
- comment_filters: Vec,
- is_reduce_comments: bool,
+ conversion_options: &PyConversionOptions,
+ block_options: &PyBlockOptions,
) -> PyResult {
Ok(convert::convert_to_ass(
inputs,
crate::reader::xml::read_comments_from_xml,
- stage_width,
- stage_height,
- reserve_blank,
- font_face,
- font_size,
- text_opacity,
- duration_marquee,
- duration_still,
- comment_filters,
- is_reduce_comments,
+ conversion_options.stage_width,
+ conversion_options.stage_height,
+ conversion_options.display_region_ratio,
+ &conversion_options.font_face,
+ conversion_options.font_size,
+ conversion_options.text_opacity,
+ conversion_options.duration_marquee,
+ conversion_options.duration_still,
+ conversion_options.is_reduce_comments,
+ &block_options.to_block_options()?,
)?)
}
-#[allow(clippy::too_many_arguments)]
#[pyfunction(name = "protobuf_to_ass")]
pub fn py_protobuf_to_ass(
inputs: Vec,
- stage_width: u32,
- stage_height: u32,
- reserve_blank: u32,
- font_face: &str,
- font_size: f32,
- text_opacity: f32,
- duration_marquee: f64,
- duration_still: f64,
- comment_filters: Vec,
- is_reduce_comments: bool,
+ conversion_options: &PyConversionOptions,
+ block_options: &PyBlockOptions,
) -> PyResult {
Ok(convert::convert_to_ass(
inputs,
reader::protobuf::read_comments_from_protobuf,
- stage_width,
- stage_height,
- reserve_blank,
- font_face,
- font_size,
- text_opacity,
- duration_marquee,
- duration_still,
- comment_filters,
- is_reduce_comments,
+ conversion_options.stage_width,
+ conversion_options.stage_height,
+ conversion_options.display_region_ratio,
+ &conversion_options.font_face,
+ conversion_options.font_size,
+ conversion_options.text_opacity,
+ conversion_options.duration_marquee,
+ conversion_options.duration_still,
+ conversion_options.is_reduce_comments,
+ &block_options.to_block_options()?,
)?)
}
diff --git a/packages/biliass/rust/src/python/mod.rs b/packages/biliass/rust/src/python/mod.rs
index b3cfa870d..3cc45fe2a 100644
--- a/packages/biliass/rust/src/python/mod.rs
+++ b/packages/biliass/rust/src/python/mod.rs
@@ -1,6 +1,6 @@
mod convert;
mod logging;
mod proto;
-pub use convert::{py_protobuf_to_ass, py_xml_to_ass};
+pub use convert::{py_protobuf_to_ass, py_xml_to_ass, PyBlockOptions, PyConversionOptions};
pub use logging::py_enable_tracing;
pub use proto::py_get_danmaku_meta_size;
diff --git a/packages/biliass/rust/src/python/proto.rs b/packages/biliass/rust/src/python/proto.rs
index b4665f6aa..c2a6ce888 100644
--- a/packages/biliass/rust/src/python/proto.rs
+++ b/packages/biliass/rust/src/python/proto.rs
@@ -4,133 +4,6 @@ use prost::Message;
use pyo3::prelude::*;
use std::io::Cursor;
-#[pyclass(name = "DanmakuElem")]
-pub struct PyDanmakuElem {
- inner: proto::danmaku::DanmakuElem,
-}
-
-impl PyDanmakuElem {
- fn new(inner: proto::danmaku::DanmakuElem) -> Self {
- PyDanmakuElem { inner }
- }
-}
-
-#[pymethods]
-impl PyDanmakuElem {
- #[getter]
- fn id(&self) -> PyResult {
- Ok(self.inner.id)
- }
-
- #[getter]
- fn progress(&self) -> PyResult {
- Ok(self.inner.progress)
- }
-
- #[getter]
- fn mode(&self) -> PyResult {
- Ok(self.inner.mode)
- }
-
- #[getter]
- fn fontsize(&self) -> PyResult {
- Ok(self.inner.fontsize)
- }
-
- #[getter]
- fn color(&self) -> PyResult {
- Ok(self.inner.color)
- }
-
- #[getter]
- fn mid_hash(&self) -> PyResult {
- Ok(self.inner.mid_hash.clone())
- }
-
- #[getter]
- fn content(&self) -> PyResult {
- Ok(self.inner.content.clone())
- }
-
- #[getter]
- fn ctime(&self) -> PyResult {
- Ok(self.inner.ctime)
- }
-
- #[getter]
- fn weight(&self) -> PyResult {
- Ok(self.inner.weight)
- }
-
- #[getter]
- fn action(&self) -> PyResult {
- Ok(self.inner.action.clone())
- }
-
- #[getter]
- fn pool(&self) -> PyResult {
- Ok(self.inner.pool)
- }
-
- #[getter]
- fn id_str(&self) -> PyResult {
- Ok(self.inner.id_str.clone())
- }
-
- #[getter]
- fn attr(&self) -> PyResult {
- Ok(self.inner.attr)
- }
-
- #[getter]
- fn animation(&self) -> PyResult {
- Ok(self.inner.animation.clone())
- }
-
- fn __repr__(&self) -> PyResult {
- Ok(format!("DanmakuElem({:?})", self.inner))
- }
-}
-
-#[pyclass(name = "DmSegMobileReply")]
-pub struct PyDmSegMobileReply {
- inner: proto::danmaku::DmSegMobileReply,
-}
-
-impl PyDmSegMobileReply {
- fn new(inner: proto::danmaku::DmSegMobileReply) -> Self {
- PyDmSegMobileReply { inner }
- }
-}
-
-#[pymethods]
-impl PyDmSegMobileReply {
- #[getter]
- fn elems(&self) -> PyResult {
- Python::with_gil(|py| {
- let list = pyo3::types::PyList::empty_bound(py);
- for item in &self.inner.elems {
- let item = PyDanmakuElem::new(item.clone());
- list.append(item.into_py(py))?;
- }
- Ok(list.into())
- })
- }
-
- fn __repr__(&self) -> PyResult {
- Ok(format!("DmSegMobileReply({:?})", self.inner))
- }
-
- #[staticmethod]
- fn decode(buffer: &[u8]) -> PyResult {
- Ok(PyDmSegMobileReply::new(
- proto::danmaku::DmSegMobileReply::decode(&mut Cursor::new(buffer))
- .map_err(error::DecodeError::from)
- .map_err(error::BiliassError::from)?,
- ))
- }
-}
-
#[pyfunction(name = "get_danmaku_meta_size")]
pub fn py_get_danmaku_meta_size(buffer: &[u8]) -> PyResult {
let dm_sge_opt = proto::danmaku_view::DmWebViewReply::decode(&mut Cursor::new(buffer))
diff --git a/packages/biliass/rust/src/reader/protobuf.rs b/packages/biliass/rust/src/reader/protobuf.rs
index 1704f279f..8c1101213 100644
--- a/packages/biliass/rust/src/reader/protobuf.rs
+++ b/packages/biliass/rust/src/reader/protobuf.rs
@@ -1,11 +1,18 @@
-use crate::comment::{Comment, CommentPosition};
+use crate::comment::{Comment, CommentData, CommentPosition, NormalCommentData};
use crate::error::{BiliassError, DecodeError};
+use crate::filter::{should_skip_parse, BlockOptions};
use crate::proto::danmaku::DmSegMobileReply;
-use crate::reader::utils;
+use crate::reader::{special, utils};
use prost::Message;
use std::io::Cursor;
+use tracing::warn;
-pub fn read_comments_from_protobuf(data: T, fontsize: f32) -> Result, BiliassError>
+pub fn read_comments_from_protobuf(
+ data: T,
+ fontsize: f32,
+ zoom_factor: (f32, f32, f32),
+ block_options: &BlockOptions,
+) -> Result, BiliassError>
where
T: AsRef<[u8]>,
{
@@ -26,9 +33,12 @@ where
7 => CommentPosition::Special,
_ => unreachable!("Impossible danmaku type"),
};
+ if should_skip_parse(&comment_pos, block_options) {
+ continue;
+ }
let color = elem.color;
let size = elem.fontsize;
- let (comment_content, size, height, width) =
+ let (comment_content, size, comment_data) =
if comment_pos != CommentPosition::Special {
let comment_content =
utils::unescape_newline(&utils::filter_bad_chars(&elem.content));
@@ -37,20 +47,36 @@ where
(comment_content.chars().filter(|&c| c == '\n').count() as f32 + 1.0)
* size;
let width = utils::calculate_length(&comment_content) * size;
- (comment_content, size, height, width)
+ (
+ comment_content,
+ size,
+ CommentData::Normal(NormalCommentData { height, width }),
+ )
} else {
- (utils::filter_bad_chars(&elem.content), size as f32, 0., 0.)
+ let parsed_data = special::parse_special_comment(
+ &utils::filter_bad_chars(&elem.content),
+ zoom_factor,
+ );
+ if parsed_data.is_err() {
+ warn!("Failed to parse special comment: {:?}", parsed_data);
+ continue;
+ }
+ let (content, special_comment_data) = parsed_data.unwrap();
+ (
+ content,
+ size as f32,
+ CommentData::Special(special_comment_data),
+ )
};
comments.push(Comment {
timeline,
timestamp,
no: i as u64,
- comment: comment_content,
+ content: comment_content,
pos: comment_pos,
color,
size,
- height,
- width,
+ data: comment_data,
})
}
8 => {
diff --git a/packages/biliass/rust/src/reader/special.rs b/packages/biliass/rust/src/reader/special.rs
index 8579ca570..ed59b9804 100644
--- a/packages/biliass/rust/src/reader/special.rs
+++ b/packages/biliass/rust/src/reader/special.rs
@@ -1,4 +1,5 @@
-use crate::error::{BiliassError, DecodeError, ParseError};
+use crate::comment::SpecialCommentData;
+use crate::error::ParseError;
use crate::reader::utils;
// pub const BILI_PLAYER_SIZE: (u32, u32) = (512, 384); // Bilibili player version 2010
@@ -23,26 +24,17 @@ fn get_position(input_pos: f64, is_height: bool, zoom_factor: (f32, f32, f32)) -
pub fn parse_special_comment(
content: &str,
zoom_factor: (f32, f32, f32),
-) -> Result<
- (
- (i64, i64, f64, f64, f64, f64),
- u8,
- u8,
- String,
- i64,
- f64,
- i64,
- String,
- bool,
- ),
- BiliassError,
-> {
+) -> Result<(String, SpecialCommentData), ParseError> {
let special_comment_parsed_data =
- serde_json::from_str::(content).map_err(DecodeError::from)?;
+ serde_json::from_str::(content).map_err(|e| {
+ ParseError::SpecialComment(format!(
+ "Error occurred while parsing special comment: {e}, content: {content}",
+ ))
+ })?;
if !special_comment_parsed_data.is_array() {
- return Err(
- ParseError::SpecialComment("Special comment is not an array".to_owned()).into(),
- );
+ return Err(ParseError::SpecialComment(
+ "Special comment is not an array".to_owned(),
+ ));
}
let special_comment_array = special_comment_parsed_data.as_array().unwrap();
let text = utils::unescape_newline(special_comment_array[4].as_str().ok_or(
@@ -93,15 +85,22 @@ pub fn parse_special_comment(
// let is_border = parse_array_item_at_index(special_comment_array, 11, true, parse_bool_value)?;
let is_border = true;
Ok((
- (rotate_y, rotate_z, from_x, from_y, to_x, to_y),
- from_alpha,
- to_alpha,
text.to_owned(),
- delay,
- lifetime,
- duration,
- fontface,
- is_border,
+ SpecialCommentData {
+ rotate_y,
+ rotate_z,
+ from_x,
+ from_y,
+ to_x,
+ to_y,
+ from_alpha,
+ to_alpha,
+ delay,
+ lifetime,
+ duration,
+ fontface,
+ is_border,
+ },
))
}
@@ -110,44 +109,52 @@ fn parse_array_item_at_index(
array: &[serde_json::Value],
index: usize,
default: T,
- item_parser: fn(&serde_json::Value, T) -> Result,
-) -> Result {
+ item_parser: fn(&serde_json::Value, T) -> Result,
+) -> Result {
match array.get(index) {
Some(value) => item_parser(value, default),
None => Ok(default),
}
}
-fn parse_float_value(value: &serde_json::Value, default: f64) -> Result {
+fn parse_float_value(value: &serde_json::Value, default: f64) -> Result {
match value {
serde_json::Value::Number(num) => Ok(num.as_f64().unwrap_or(default)),
serde_json::Value::String(str) => Ok(str.parse::().unwrap_or(default)),
serde_json::Value::Null => Ok(default),
- _ => Err(ParseError::SpecialComment("Value is not a number".to_owned()).into()),
+ _ => Err(ParseError::SpecialComment(
+ "Value is not a number".to_owned(),
+ )),
}
}
-fn parse_int_value(value: &serde_json::Value, default: i64) -> Result {
+fn parse_int_value(value: &serde_json::Value, default: i64) -> Result {
match value {
serde_json::Value::Number(num) => Ok(num.as_f64().unwrap_or(default as f64) as i64),
serde_json::Value::String(str) => Ok(str.parse::().unwrap_or(default as f64) as i64),
serde_json::Value::Null => Ok(default),
- _ => Err(ParseError::SpecialComment("Value is not a number".to_owned()).into()),
+ _ => Err(ParseError::SpecialComment(
+ "Value is not a number".to_owned(),
+ )),
}
}
-fn parse_string_value(value: &serde_json::Value, _: String) -> Result {
+fn parse_string_value(value: &serde_json::Value, _: String) -> Result {
match value {
serde_json::Value::String(str) => Ok(str.to_owned()),
- _ => Err(ParseError::SpecialComment("Value is not a string".to_owned()).into()),
+ _ => Err(ParseError::SpecialComment(
+ "Value is not a string".to_owned(),
+ )),
}
}
#[allow(unused)]
-fn parse_bool_value(value: &serde_json::Value, default: bool) -> Result {
+fn parse_bool_value(value: &serde_json::Value, default: bool) -> Result {
match value {
serde_json::Value::Bool(b) => Ok(*b),
serde_json::Value::Number(num) => Ok(num.as_i64().unwrap_or(default as i64) != 0),
- _ => Err(ParseError::SpecialComment("Value is not a boolean".to_owned()).into()),
+ _ => Err(ParseError::SpecialComment(
+ "Value is not a boolean".to_owned(),
+ )),
}
}
diff --git a/packages/biliass/rust/src/reader/xml.rs b/packages/biliass/rust/src/reader/xml.rs
index f3e41829b..f4d5fb21d 100644
--- a/packages/biliass/rust/src/reader/xml.rs
+++ b/packages/biliass/rust/src/reader/xml.rs
@@ -1,8 +1,10 @@
-use crate::comment::{Comment, CommentPosition};
+use crate::comment::{Comment, CommentData, CommentPosition, NormalCommentData};
use crate::error::{BiliassError, DecodeError, ParseError};
-use crate::reader::utils;
+use crate::filter::{should_skip_parse, BlockOptions};
+use crate::reader::{special, utils};
use quick_xml::events::{BytesStart, Event};
use quick_xml::reader::Reader;
+use tracing::warn;
#[derive(PartialEq, Clone)]
enum XmlVersion {
@@ -44,7 +46,9 @@ fn parse_comment_item(
content: &str,
version: XmlVersion,
fontsize: f32,
+ zoom_factor: (f32, f32, f32),
id: u64,
+ block_options: &BlockOptions,
) -> Result