├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── docs-feedback.md
│ ├── feature_request.md
│ └── help-wanted.md
├── pull_request_template.md
└── workflows
│ ├── build.yml
│ └── deploy.yml
├── .gitignore
├── LICENSE
├── README-en.md
├── README.md
├── assets
├── headerDark.svg
└── headerLight.svg
├── bilitool
├── __init__.py
├── authenticate
│ ├── __init__.py
│ └── wbi_sign.py
├── cli.py
├── controller
│ ├── __init__.py
│ ├── download_controller.py
│ ├── feed_controller.py
│ ├── login_controller.py
│ └── upload_controller.py
├── download
│ ├── __init__.py
│ └── bili_download.py
├── feed
│ ├── __init__.py
│ ├── bili_live_list.py
│ └── bili_video_list.py
├── login
│ ├── __init__.py
│ ├── check_bili_login.py
│ ├── login_bili.py
│ └── logout_bili.py
├── model
│ ├── __init__.py
│ ├── config.json
│ └── model.py
├── upload
│ ├── __init__.py
│ └── bili_upload.py
└── utils
│ ├── __init__.py
│ ├── check_format.py
│ ├── get_ip_info.py
│ ├── parse_cookies.py
│ └── parse_yaml.py
├── docs
├── .vitepress
│ ├── cache
│ │ └── deps
│ │ │ ├── @braintree_sanitize-url.js
│ │ │ ├── @braintree_sanitize-url.js.map
│ │ │ ├── @theme_index.js
│ │ │ ├── @theme_index.js.map
│ │ │ ├── _metadata.json
│ │ │ ├── chunk-2AEJUKVR.js
│ │ │ ├── chunk-2AEJUKVR.js.map
│ │ │ ├── chunk-BUSYA2B4.js
│ │ │ ├── chunk-BUSYA2B4.js.map
│ │ │ ├── chunk-U55NQ7RZ.js
│ │ │ ├── chunk-U55NQ7RZ.js.map
│ │ │ ├── cytoscape-cose-bilkent.js
│ │ │ ├── cytoscape-cose-bilkent.js.map
│ │ │ ├── cytoscape.js
│ │ │ ├── cytoscape.js.map
│ │ │ ├── dayjs.js
│ │ │ ├── dayjs.js.map
│ │ │ ├── debug.js
│ │ │ ├── debug.js.map
│ │ │ ├── package.json
│ │ │ ├── vitepress___@vue_devtools-api.js
│ │ │ ├── vitepress___@vue_devtools-api.js.map
│ │ │ ├── vitepress___@vueuse_core.js
│ │ │ ├── vitepress___@vueuse_core.js.map
│ │ │ ├── vue.js
│ │ │ └── vue.js.map
│ └── config.mts
├── CNAME
├── check.md
├── convert.md
├── download.md
├── getting-start.md
├── index.md
├── installation.md
├── ip.md
├── list.md
├── login.md
├── logout.md
├── show.md
├── tid.md
└── upload.md
├── package-lock.json
├── package.json
├── pyproject.toml
├── requirements.txt
├── sha256sum.sh
├── template
└── example-config.yaml
└── tests
├── __init__.py
├── test_feed.py
├── test_upload.py
└── test_utils.py
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve the project.
4 | title: "[BUG] "
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Describe the bug
11 | (A clear and concise description of what the bug is.)
12 |
13 | ## How To Reproduce
14 | Steps to reproduce the behavior:
15 | 1. Config/File changes: ...
16 | 2. Run command: ...
17 | 3. See error: ...
18 |
19 | ## Expected behavior
20 | (A clear and concise description of what you expected to happen.)
21 |
22 | ## Screenshots
23 | (If applicable, add screenshots to help explain your problem.)
24 |
25 | ## Environment Information
26 | - Operating System: [e.g. Ubuntu 20.04.5 LTS]
27 | - Python Version: [e.g. Python 3.9.15]
28 | - Driver & CUDA Version: [e.g. Driver 470.103.01 & CUDA 11.4]
29 | - Error Messages and Logs: [If applicable, provide any error messages or relevant log outputs]
30 |
31 | ## Additional context
32 | (Add any other context about the problem here.)
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/docs-feedback.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Docs feedback
3 | about: Improve documentation.
4 | title: "[Docs] "
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Documentation Reference
11 | (Path/Link to the documentation file)
12 |
13 | ## Feedback on documentation
14 | (Your suggestions to the documentation. e.g., accuracy, complex explanations, structural organization, practical examples, technical reliability, and consistency)
15 |
16 | ## Additional context
17 | (Add any other context or screenshots about the documentation here.)
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for the project.
4 | title: "[Feature] "
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Is your feature request related to a problem? Please describe.
11 | (A clear and concise description of what the problem is.)
12 |
13 | ## Describe the solution you'd like
14 | (A clear and concise description of what you want to happen.)
15 |
16 | ## Describe alternatives you've considered
17 | (A clear and concise description of any alternative solutions or features you've considered.)
18 |
19 | ## Additional context
20 | (Add any other context or screenshots about the feature request here.)
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/help-wanted.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Help wanted
3 | about: Want help from project team.
4 | title: "[Help] "
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Problem Overview
11 | (Briefly and clearly describe the issue you're facing and seeking help with.)
12 |
13 | ## Steps Taken
14 | (Detail your attempts to resolve the issue, including any relevant steps or processes.)
15 | 1. Config/File changes: ...
16 | 2. Run command: ...
17 | 3. See errors: ...
18 |
19 | ## Expected Outcome
20 | (A clear and concise description of what you expected to happen.)
21 |
22 | ## Screenshots
23 | (If applicable, add screenshots to help explain your problem.)
24 |
25 | ## Environment Information
26 | - Operating System: [e.g. Ubuntu 20.04.5 LTS]
27 | - Python Version: [e.g. Python 3.9.15]
28 | - Driver & CUDA Version: [e.g. Driver 470.103.01 & CUDA 11.4]
29 | - Error Messages and Logs: [If applicable, provide any error messages or relevant log outputs]
30 |
31 | ## Additional context
32 | (Add any other context about the problem here.)
33 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 | ## Description
3 |
4 | [Please describe the background, purpose, changes made, and how to test this PR]
5 |
6 | ## Related Issues
7 |
8 | [List the issue numbers related to this PR]
9 |
10 | ## Changes Proposed
11 |
12 | - [ ] change1
13 | - [ ] ...
14 |
15 | ## Who Can Review?
16 |
17 | [Please use the '@' symbol to mention any community member who is free to review the PR once the tests have passed. Feel free to tag members or contributors who might be interested in your PR.]
18 |
19 | ## TODO
20 |
21 | - [ ] task1
22 | - [ ] ...
23 |
24 | ## Checklist
25 |
26 | - [ ] Code has been reviewed
27 | - [ ] Code complies with the project's code standards and best practices
28 | - [ ] Code has passed all tests
29 | - [ ] Code does not affect the normal use of existing features
30 | - [ ] Code has been commented properly
31 | - [ ] Documentation has been updated (if applicable)
32 | - [ ] Demo/checkpoint has been attached (if applicable)
33 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: main
2 | on: push
3 | jobs:
4 | build:
5 | strategy:
6 | matrix:
7 | os: [ubuntu-20.04, windows-latest]
8 |
9 | runs-on: ${{ matrix.os }}
10 |
11 | steps:
12 | - name: Check-out repository
13 | uses: actions/checkout@v4
14 |
15 | - name: Setup Python
16 | uses: actions/setup-python@v5
17 | with:
18 | python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
19 | architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
20 | cache: 'pip'
21 | cache-dependency-path: |
22 | **/requirements*.txt
23 |
24 | - name: Install Dependencies
25 | run: |
26 | pip install -r requirements.txt
27 |
28 | - name: Build Executable
29 | uses: Nuitka/Nuitka-Action@main
30 | with:
31 | nuitka-version: main
32 | script-name: bilitool/cli.py
33 | mode: app
34 | include-data-files: |
35 | ./bilitool/model/config.json=./bilitool/model/config.json
36 |
37 | - name: Upload Artifacts
38 | uses: actions/upload-artifact@v4
39 | with:
40 | name: ${{ runner.os }} Build
41 | path: |
42 | build/*.exe
43 | build/*.bin
44 | build/*.app/**/*
45 | include-hidden-files: true
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | # Sample workflow for building and deploying a VitePress site to GitHub Pages
2 | #
3 | name: Deploy VitePress site to Pages
4 |
5 | on:
6 | # Runs on pushes targeting the `main` branch. Change this to `master` if you're
7 | # using the `master` branch as the default branch.
8 | push:
9 | branches: [main]
10 |
11 | # Allows you to run this workflow manually from the Actions tab
12 | workflow_dispatch:
13 |
14 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
15 | permissions:
16 | contents: read
17 | pages: write
18 | id-token: write
19 |
20 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
21 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
22 | concurrency:
23 | group: pages
24 | cancel-in-progress: false
25 |
26 | jobs:
27 | # Build job
28 | build:
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Checkout
32 | uses: actions/checkout@v4
33 | with:
34 | fetch-depth: 0 # Not needed if lastUpdated is not enabled
35 | # - uses: pnpm/action-setup@v3 # Uncomment this block if you're using pnpm
36 | # with:
37 | # version: 9 # Not needed if you've set "packageManager" in package.json
38 | # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
39 | - name: Setup Node
40 | uses: actions/setup-node@v4
41 | with:
42 | node-version: 20
43 | cache: npm # or pnpm / yarn
44 | - name: Setup Pages
45 | uses: actions/configure-pages@v4
46 | - name: Install dependencies
47 | run: npm ci # or pnpm install / yarn install / bun install
48 | - name: Build with VitePress
49 | run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
50 | - name: Upload artifact
51 | uses: actions/upload-pages-artifact@v3
52 | with:
53 | path: docs/.vitepress/dist
54 |
55 | # Deployment job
56 | deploy:
57 | environment:
58 | name: github-pages
59 | url: ${{ steps.deployment.outputs.page_url }}
60 | needs: build
61 | runs-on: ubuntu-latest
62 | name: Deploy
63 | steps:
64 | - name: Deploy to GitHub Pages
65 | id: deployment
66 | uses: actions/deploy-pages@v4
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Recommend to use commitizen to commit
2 | node_modules/
3 | __pycache__/
4 | cookie.json
5 | *.mp4
6 | upload.yaml
7 | dist/
8 | *.spec
9 | build/
10 | test.py
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 bilitool
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README-en.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | [简体中文](./README.md) | English
9 |
10 |
11 |
12 | > Welcome to use, provide feedback, and contribute to this project via PRs. Please do not use it for purposes that violate community guidelines.
13 |
14 | `bilitool` is a Python toolkit that provides features such as persistent login, video download, and video upload to Bilibili. It can be used via command-line interface (CLI) or as a library in other projects.
15 |
16 | The project is designed following the MVC architecture:
17 |
18 | ```mermaid
19 | graph TD
20 | subgraph Model
21 | M1[Model]
22 | end
23 |
24 | subgraph Database
25 | DB1[JSON]
26 | end
27 |
28 | subgraph Controller
29 | C1[DownloadController]
30 | C2[UploadController]
31 | C3[LoginController]
32 | C4[FeedController]
33 | end
34 |
35 | subgraph View
36 | V1[CLI]
37 | end
38 |
39 | subgraph Utility
40 | U1[CheckFormat]
41 | U2[IPInfo]
42 | end
43 |
44 | subgraph Download
45 | D1[BiliDownloader]
46 | end
47 |
48 | subgraph Upload
49 | U3[BiliUploader]
50 | end
51 |
52 | subgraph Feed
53 | F1[BiliVideoList]
54 | end
55 |
56 | subgraph Login
57 | L1[LoginBili]
58 | L2[LogoutBili]
59 | L3[CheckBiliLogin]
60 | end
61 |
62 | subgraph Authenticate
63 | A1[WbiSign]
64 | end
65 |
66 | M1 --> DB1
67 | DB1 --> M1
68 |
69 | M1 <--> C1
70 | M1 <--> C2
71 | M1 <--> C4
72 |
73 | C1 --> D1
74 | C2 --> U3
75 |
76 | V1 --> Utility
77 |
78 | C3 --> L1
79 | C3 --> L2
80 | C3 --> L3
81 |
82 | C4 --> F1
83 |
84 | V1 --> C1
85 | V1 --> C2
86 | V1 --> C3
87 | V1 --> A1 --> C4
88 |
89 | ```
90 |
91 | ## Major Features
92 |
93 | - `bilitool login` remembers and stores login status
94 | - Supports exporting `cookies.json` for use in other projects
95 | - `bilitool logout` logs out
96 | - Logs out and clears cookies to protect privacy and prevent leaks
97 | - `bilitool check` checks login status
98 | - `bilitool upload` uploads videos
99 | - Supports various custom parameters for uploading
100 | - Supports uploading videos with YAML configuration and parsing
101 | - Displays logs and upload progress
102 | - Supports **automatic speed test and selection of the best route** (default)
103 | - Supports specifying upload lines (`qn`, `bldsa`, `ws`, `bda2`, `tx`)
104 | - Supports uploading cover image
105 | - `bilitool append` appends videos to existing videos (multi-part)
106 | - `bilitool download` downloads videos
107 | - Supports downloading with `bvid` and `avid` identifiers
108 | - Supports downloading danmaku (comments)
109 | - Supports downloading in multiple qualities
110 | - Supports downloading multi-part videos
111 | - Displays logs and download progress
112 | - `bilitool list` queries the status of past uploaded videos
113 | - Supports querying videos with various statuses
114 | - Displays reasons if video review fails
115 | - `bilitool convert` converts video identifiers
116 | - Supports conversion between `bvid` and `avid`
117 | - `bilitool show` displays detailed video information
118 | - Supports viewing basic video information and interaction data
119 | - `bilitool ip` displays IP information
120 | - Supports querying specified IP addresses
121 | - Append videos to existing videos (planned support)
122 |
123 | > Add `-h` or `--help` to the above commands to view command help information.
124 | >
125 | > For more detailed commands, refer to the [project documentation](https://bilitool.timerring.com).
126 |
127 | ## Installation
128 |
129 | > Recommended Python version >= 3.10.
130 |
131 | ```bash
132 | pip install bilitool
133 | ```
134 |
135 | Alternatively, you can download the compiled CLI tool and run it directly [Download Link](https://github.com/timerring/bilitool/releases).
136 |
137 | ## Usage
138 |
139 | ### CLI Method
140 |
141 | > For more detailed commands, refer to the [project documentation](https://bilitool.timerring.com).
142 |
143 | Help information:
144 |
145 | ```
146 | usage: bilitool [-h] [-V] {login,logout,upload,check,download,list,ip} ...
147 |
148 | The Python toolkit package and cli designed for interaction with Bilibili
149 |
150 | positional arguments:
151 | {login,logout,upload,append,check,download,list,show,convert,ip}
152 | Subcommands
153 | login Login and save the cookie
154 | logout Logout the current account
155 | upload Upload the video
156 | append Append the video
157 | check Check if the user is logged in
158 | download Download the video
159 | list Get the uploaded video list
160 | show Show the video detailed info
161 | convert Convert between avid and bvid
162 | ip Get the ip info
163 |
164 | options:
165 | -h, --help show this help message and exit
166 | -V, --version Print version information
167 | ```
168 |
169 | ### API Method
170 |
171 | > For more detailed functions and documentation, refer to the [project documentation](https://bilitool.timerring.com).
172 |
173 | ```python
174 | from bilitool import LoginController, UploadController, DownloadController, FeedController, IPInfo, CheckFormat
175 |
176 | # Login
177 | LoginController().login_bilibili(export: bool)
178 | # Logout
179 | LoginController().logout_bilibili()
180 | # Check login
181 | LoginController().check_bilibili_login()
182 |
183 | # Upload
184 | UploadController().upload_video_entry(video_path: str, yaml: str, line: str, tid: int, title: str, desc: str, tag: str, source: str, cover: str, dynamic: str, cdn: str)
185 |
186 | # Append
187 | UploadController().append_video_entry(video_path: str, bvid: str, cdn: str)
188 |
189 | # Download
190 | DownloadController().download_video_entry(vid: str, danmaku: bool, quality: int, chunksize: int, multiple: bool)
191 |
192 | # Query recent video list
193 | FeedController().print_video_list_info(size: int, status: str)
194 |
195 | # Query video information
196 | FeedController().print_video_info(vid: str)
197 |
198 | # Convert video identifier
199 | CheckFormat().convert_bv_and_av(vid: str)
200 |
201 | # Query IP information
202 | IPInfo.get_ip_address(ip: str)
203 | ```
204 |
205 | ## Acknowledgments
206 |
207 | - Thanks to [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect) for providing the API collection.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 简体中文 | [English](./README-en.md)
9 |
10 |
11 |
12 | > 欢迎使用,欢迎提供更多反馈,欢迎 PR 贡献此项目,请勿用于违反社区规定的用途。
13 |
14 | `bilitool` 是一个 python 的工具库,实现持久化登录,下载视频,上传视频到 bilibili 等功能,可以使用命令行 cli 操作,也可以作为其他项目的库使用。
15 |
16 | 项目仿照 MVC 架构进行设计:
17 |
18 | ```mermaid
19 | graph TD
20 | subgraph Model
21 | M1[Model]
22 | end
23 |
24 | subgraph Database
25 | DB1[JSON]
26 | end
27 |
28 | subgraph Controller
29 | C1[DownloadController]
30 | C2[UploadController]
31 | C3[LoginController]
32 | C4[FeedController]
33 | end
34 |
35 | subgraph View
36 | V1[CLI]
37 | end
38 |
39 | subgraph Utility
40 | U1[CheckFormat]
41 | U2[IPInfo]
42 | end
43 |
44 | subgraph Download
45 | D1[BiliDownloader]
46 | end
47 |
48 | subgraph Upload
49 | U3[BiliUploader]
50 | end
51 |
52 | subgraph Feed
53 | F1[BiliVideoList]
54 | end
55 |
56 | subgraph Login
57 | L1[LoginBili]
58 | L2[LogoutBili]
59 | L3[CheckBiliLogin]
60 | end
61 |
62 | subgraph Authenticate
63 | A1[WbiSign]
64 | end
65 |
66 | M1 --> DB1
67 | DB1 --> M1
68 |
69 | M1 <--> C1
70 | M1 <--> C2
71 | M1 <--> C4
72 |
73 | C1 --> D1
74 | C2 --> U3
75 |
76 | V1 --> Utility
77 |
78 | C3 --> L1
79 | C3 --> L2
80 | C3 --> L3
81 |
82 | C4 --> F1
83 |
84 | V1 --> C1
85 | V1 --> C2
86 | V1 --> C3
87 | V1 --> A1 --> C4
88 |
89 | ```
90 |
91 | ## Major features
92 |
93 | - `bilitool login` **记忆存储登录状态**
94 | - 支持导出 `cookies.json` 用于其他项目
95 | - `bilitool logout` 退出登录
96 | - 退出登录同时注销 cookies,**保护隐私防止泄露**
97 | - `bilitool check` 检查登录状态
98 | - `bilitool upload` 上传视频
99 | - 支持多种自定义参数上传
100 | - 支持上传视频的 yaml 配置与解析
101 | - 显示日志与上传进度
102 | - 支持**自动测速并且选择最佳线路**(默认)
103 | - 支持指定上传线路(`qn`, `bldsa`, `ws`, `bda2`, `tx`)
104 | - 支持上传视频封面图片
105 | - `bilitool append` 追加视频到已有的视频(**分p投稿**)
106 | - `bilitool download` 下载视频
107 | - 支持 `bvid` 和 `avid` 两种编号下载
108 | - 支持下载弹幕
109 | - 支持下载多种画质
110 | - **支持下载多 p 视频**
111 | - 显示日志与下载进度
112 | - `bilitool list` **查询本账号过往投稿视频状态**
113 | - 支持查询多种状态的视频
114 | - 若视频审核未通过,同时会显示原因
115 | - `bilitool convert` 查询转换视频编号
116 | - 支持 `bvid` 和 `avid` 两种编号互转
117 | - `bilitool show` 显示视频详细信息
118 | - 支持查看视频基本信息以及互动状态数据
119 | - `bilitool ip` **显示请求 IP 地址**
120 | - 支持查询指定 IP 地址
121 |
122 | > 以上命令添加 `-h` 或 `--help` 参数可以查看命令帮助信息。
123 | >
124 | > 更详细的命令可以参考[项目文档](https://bilitool.timerring.com)。
125 |
126 | ## Installation
127 |
128 | > 推荐 Python 版本 >= 3.10.
129 |
130 | ```bash
131 | pip install bilitool
132 | ```
133 |
134 | 或者你也可以下载编译好的 cli 工具直接运行 [下载地址](https://github.com/timerring/bilitool/releases)。
135 |
136 | ## Usage
137 |
138 | ### cli 方式
139 |
140 | > 更详细的命令可以参考[项目文档](https://bilitool.timerring.com),这里不赘述。
141 |
142 | 帮助信息:
143 |
144 | ```
145 | usage: bilitool [-h] [-V] {login,logout,upload,check,download,list,ip} ...
146 |
147 | The Python toolkit package and cli designed for interaction with Bilibili
148 |
149 | positional arguments:
150 | {login,logout,upload,append,check,download,list,show,convert,ip}
151 | Subcommands
152 | login Login and save the cookie
153 | logout Logout the current account
154 | upload Upload the video
155 | append Append the video
156 | check Check if the user is logged in
157 | download Download the video
158 | list Get the uploaded video list
159 | show Show the video detailed info
160 | convert Convert between avid and bvid
161 | ip Get the ip info
162 |
163 | options:
164 | -h, --help show this help message and exit
165 | -V, --version Print version information
166 | ```
167 |
168 | ### 接口调用方式
169 |
170 | > 更详细的函数及文档可以参考[项目文档](https://bilitool.timerring.com)。
171 |
172 | ```python
173 | from bilitool import LoginController, UploadController, DownloadController, FeedController, IPInfo, CheckFormat
174 |
175 | # 登录
176 | LoginController().login_bilibili(export: bool)
177 | # 退出登录
178 | LoginController().logout_bilibili()
179 | # 检查登录
180 | LoginController().check_bilibili_login()
181 |
182 | # 上传
183 | UploadController().upload_video_entry(video_path: str, yaml: str, tid: int, title: str, desc: str, tag: str, source: str, cover: str, dynamic: str, cdn: str)
184 |
185 | # 追加投稿(分p)
186 | UploadController().append_video_entry(video_path: str, bvid: str, cdn: str)
187 |
188 | # 下载
189 | DownloadController().download_video_entry(vid: str, danmaku: bool, quality: int, chunksize: int, multiple: bool)
190 |
191 | # 查询近期投稿列表
192 | FeedController().print_video_list_info(size: int, status: str)
193 |
194 | # 查询视频信息
195 | FeedController().print_video_info(vid: str)
196 |
197 | # 查询视频编号
198 | CheckFormat().convert_bv_and_av(vid: str)
199 |
200 | # 查询 IP 信息
201 | IPInfo.get_ip_address(ip: str)
202 | ```
203 |
204 | ## Acknowledgments
205 |
206 | - 感谢 [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect) 提供的 API 集合。
--------------------------------------------------------------------------------
/bilitool/__init__.py:
--------------------------------------------------------------------------------
1 | from .controller.login_controller import LoginController
2 | from .controller.upload_controller import UploadController
3 | from .controller.download_controller import DownloadController
4 | from .controller.feed_controller import FeedController
5 | from .utils.get_ip_info import IPInfo
6 | from .utils.check_format import CheckFormat
7 |
8 | __all__ = [
9 | "LoginController",
10 | "UploadController",
11 | "DownloadController",
12 | "FeedController",
13 | "IPInfo",
14 | "CheckFormat",
15 | ]
16 |
--------------------------------------------------------------------------------
/bilitool/authenticate/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timerring/bilitool/aefaff618235d7f94db35213a1c938126b25cac7/bilitool/authenticate/__init__.py
--------------------------------------------------------------------------------
/bilitool/authenticate/wbi_sign.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | from functools import reduce
4 | from hashlib import md5
5 | import urllib.parse
6 | import time
7 | import requests
8 | from ..model.model import Model
9 |
10 | # https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md
11 |
12 |
13 | class WbiSign(object):
14 | def __init__(self):
15 | self.config = Model().get_config()
16 |
17 | mixinKeyEncTab = [
18 | 46,
19 | 47,
20 | 18,
21 | 2,
22 | 53,
23 | 8,
24 | 23,
25 | 32,
26 | 15,
27 | 50,
28 | 10,
29 | 31,
30 | 58,
31 | 3,
32 | 45,
33 | 35,
34 | 27,
35 | 43,
36 | 5,
37 | 49,
38 | 33,
39 | 9,
40 | 42,
41 | 19,
42 | 29,
43 | 28,
44 | 14,
45 | 39,
46 | 12,
47 | 38,
48 | 41,
49 | 13,
50 | 37,
51 | 48,
52 | 7,
53 | 16,
54 | 24,
55 | 55,
56 | 40,
57 | 61,
58 | 26,
59 | 17,
60 | 0,
61 | 1,
62 | 60,
63 | 51,
64 | 30,
65 | 4,
66 | 22,
67 | 25,
68 | 54,
69 | 21,
70 | 56,
71 | 59,
72 | 6,
73 | 63,
74 | 57,
75 | 62,
76 | 11,
77 | 36,
78 | 20,
79 | 34,
80 | 44,
81 | 52,
82 | ]
83 |
84 | def get_wbi_keys(self) -> tuple[str, str]:
85 | """Get the refresh token"""
86 | headers = Model().get_headers_with_cookies_and_refer()
87 | resp = requests.get(
88 | "https://api.bilibili.com/x/web-interface/nav", headers=headers
89 | )
90 | resp.raise_for_status()
91 | json_content = resp.json()
92 | img_url: str = json_content["data"]["wbi_img"]["img_url"]
93 | sub_url: str = json_content["data"]["wbi_img"]["sub_url"]
94 | img_key = img_url.rsplit("/", 1)[1].split(".")[0]
95 | sub_key = sub_url.rsplit("/", 1)[1].split(".")[0]
96 | return img_key, sub_key
97 |
98 | def get_mixin_key(self, orig: str):
99 | """shuffle the string"""
100 | return reduce(lambda s, i: s + orig[i], self.mixinKeyEncTab, "")[:32]
101 |
102 | def enc_wbi(self, params: dict, img_key: str, sub_key: str):
103 | """wbi sign"""
104 | mixin_key = self.get_mixin_key(img_key + sub_key)
105 | curr_time = round(time.time())
106 | params["wts"] = curr_time
107 | params = dict(sorted(params.items()))
108 | # filter the value of "!'()*"
109 | params = {
110 | k: "".join(filter(lambda char: char not in "!'()*", str(v)))
111 | for k, v in params.items()
112 | }
113 | query = urllib.parse.urlencode(params)
114 | wbi_sign = md5((query + mixin_key).encode()).hexdigest()
115 | # add `w_rid` parameter in the url
116 | params["w_rid"] = wbi_sign
117 | return params
118 |
119 | def get_wbi_signed_params(self, params):
120 | img_key, sub_key = self.get_wbi_keys()
121 |
122 | signed_params = self.enc_wbi(params=params, img_key=img_key, sub_key=sub_key)
123 | return signed_params
124 |
--------------------------------------------------------------------------------
/bilitool/cli.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | import argparse
4 | import sys
5 | import os
6 | import logging
7 | import textwrap
8 | from bilitool import (
9 | LoginController,
10 | UploadController,
11 | DownloadController,
12 | FeedController,
13 | IPInfo,
14 | CheckFormat,
15 | )
16 |
17 |
18 | def cli():
19 | logging.basicConfig(
20 | format="[%(levelname)s] - [%(asctime)s %(name)s] - %(message)s",
21 | level=logging.INFO,
22 | )
23 | parser = argparse.ArgumentParser(
24 | prog="bilitool",
25 | formatter_class=argparse.RawDescriptionHelpFormatter,
26 | description=textwrap.dedent(
27 | """
28 | The Python toolkit package and cli designed for interaction with Bilibili.
29 | Source code at https://github.com/timerring/bilitool
30 | """
31 | ),
32 | )
33 | parser.add_argument(
34 | "-V",
35 | "--version",
36 | action="version",
37 | version="bilitool 0.1.3 and source code at https://github.com/timerring/bilitool",
38 | help="Print version information",
39 | )
40 |
41 | subparsers = parser.add_subparsers(dest="subcommand", help="Subcommands")
42 |
43 | # Login subcommand
44 | login_parser = subparsers.add_parser("login", help="Login and save the cookie")
45 | login_parser.add_argument(
46 | "-f", "--file", default="", help="(default is empty) Login via cookie file"
47 | )
48 | login_parser.add_argument(
49 | "--export",
50 | action="store_true",
51 | help="(default is false) Export the login cookie file",
52 | )
53 |
54 | # Logout subcommand
55 | logout_parser = subparsers.add_parser("logout", help="Logout the current account")
56 |
57 | # Upload subcommand
58 | upload_parser = subparsers.add_parser("upload", help="Upload the video")
59 | upload_parser.add_argument("video_path", help="(required) The path to video file")
60 | upload_parser.add_argument(
61 | "-y",
62 | "--yaml",
63 | default="",
64 | help="The path to yaml file(if yaml file is provided, the arguments below will be ignored)",
65 | )
66 | upload_parser.add_argument(
67 | "--title", default="", help="(default is video name) The title of video"
68 | )
69 | upload_parser.add_argument(
70 | "--desc", default="", help="(default is empty) The description of video"
71 | )
72 | upload_parser.add_argument(
73 | "--tid",
74 | type=int,
75 | default=138,
76 | help="(default is 138) For more info to the type id, refer to https://bilitool.timerring.com/tid.html",
77 | )
78 | upload_parser.add_argument(
79 | "--tag",
80 | default="bilitool",
81 | help="(default is bilitool) Video tags, separated by comma",
82 | )
83 | upload_parser.add_argument(
84 | "--source",
85 | default="来源于网络",
86 | help="(default is 来源于网络) The source of video (if your video is re-print)",
87 | )
88 | upload_parser.add_argument(
89 | "--cover",
90 | default="",
91 | help="(default is empty) The cover of video (if you want to customize, set it as the path to your cover image)",
92 | )
93 | upload_parser.add_argument(
94 | "--dynamic", default="", help="(default is empty) The dynamic information"
95 | )
96 | upload_parser.add_argument(
97 | "--cdn", default="", help="(default is auto detect) The cdn line"
98 | )
99 |
100 | # Append subcommand
101 | append_parser = subparsers.add_parser("append", help="Append the video")
102 | append_parser.add_argument(
103 | "-v",
104 | "--vid",
105 | required=True,
106 | help="(required) The bvid or avid of appended video",
107 | )
108 | append_parser.add_argument("video_path", help="(required) The path to video file")
109 | append_parser.add_argument(
110 | "--cdn", default="", help="(default is auto detect) The cdn line"
111 | )
112 |
113 | # Check login subcommand
114 | check_login_parser = subparsers.add_parser(
115 | "check", help="Check if the user is logged in"
116 | )
117 |
118 | # Download subcommand
119 | download_parser = subparsers.add_parser("download", help="Download the video")
120 |
121 | download_parser.add_argument("vid", help="(required) the bvid or avid of video")
122 | download_parser.add_argument(
123 | "--danmaku",
124 | action="store_true",
125 | help="(default is false) download the danmaku of video",
126 | )
127 | download_parser.add_argument(
128 | "--quality",
129 | type=int,
130 | default=64,
131 | help="(default is 64) the resolution of video",
132 | )
133 | download_parser.add_argument(
134 | "--chunksize",
135 | type=int,
136 | default=1024,
137 | help="(default is 1024) the chunk size of video",
138 | )
139 | download_parser.add_argument(
140 | "--multiple",
141 | action="store_true",
142 | help="(default is false) download the multiple videos if have set",
143 | )
144 |
145 | # List subcommand
146 | list_parser = subparsers.add_parser("list", help="Get the uploaded video list")
147 | list_parser.add_argument(
148 | "--size", type=int, default=20, help="(default is 20) the size of video list"
149 | )
150 | list_parser.add_argument(
151 | "--status",
152 | default="pubed,not_pubed,is_pubing",
153 | help="(default is all) the status of video list: pubed, not_pubed, is_pubing",
154 | )
155 |
156 | # Show subcommand
157 | show_parser = subparsers.add_parser("show", help="Show the video detailed info")
158 | show_parser.add_argument("vid", help="The avid or bvid of the video")
159 |
160 | # Convert subcommand
161 | convert_parser = subparsers.add_parser(
162 | "convert", help="Convert between avid and bvid"
163 | )
164 | convert_parser.add_argument("vid", help="The avid or bvid of the video")
165 |
166 | # IP subcommand
167 | ip_parser = subparsers.add_parser("ip", help="Get the ip info")
168 | ip_parser.add_argument(
169 | "--ip", default="", help="(default is your request ip) The ip address"
170 | )
171 |
172 | args = parser.parse_args()
173 |
174 | # Check if no subcommand is provided
175 | if args.subcommand is None:
176 | print("No subcommand provided. Please specify a subcommand.")
177 | parser.print_help()
178 | sys.exit()
179 |
180 | if args.subcommand == "login":
181 | if args.file:
182 | LoginController().login_bilibili_with_cookie_file(args.file)
183 | else:
184 | LoginController().login_bilibili(args.export)
185 |
186 | if args.subcommand == "logout":
187 | LoginController().logout_bilibili()
188 |
189 | if args.subcommand == "check":
190 | LoginController().check_bilibili_login()
191 |
192 | if args.subcommand == "upload":
193 | # print(args)
194 | UploadController().upload_video_entry(
195 | args.video_path,
196 | args.yaml,
197 | args.tid,
198 | args.title,
199 | args.desc,
200 | args.tag,
201 | args.source,
202 | args.cover,
203 | args.dynamic,
204 | args.cdn,
205 | )
206 |
207 | if args.subcommand == "append":
208 | UploadController().append_video_entry(args.video_path, args.vid, args.cdn)
209 |
210 | if args.subcommand == "download":
211 | # print(args)
212 | DownloadController().download_video_entry(
213 | args.vid, args.danmaku, args.quality, args.chunksize, args.multiple
214 | )
215 |
216 | if args.subcommand == "list":
217 | FeedController().print_video_list_info(args.size, args.status)
218 |
219 | if args.subcommand == "show":
220 | FeedController().print_video_info(args.vid)
221 |
222 | if args.subcommand == "convert":
223 | CheckFormat().convert_bv_and_av(args.vid)
224 |
225 | if args.subcommand == "ip":
226 | IPInfo.get_ip_address(args.ip)
227 |
228 |
229 | if __name__ == "__main__":
230 | cli()
231 |
--------------------------------------------------------------------------------
/bilitool/controller/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timerring/bilitool/aefaff618235d7f94db35213a1c938126b25cac7/bilitool/controller/__init__.py
--------------------------------------------------------------------------------
/bilitool/controller/download_controller.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | from ..model.model import Model
4 | from ..download.bili_download import BiliDownloader
5 | from ..utils.check_format import CheckFormat
6 | import re
7 | import logging
8 |
9 |
10 | class DownloadController:
11 | def __init__(self):
12 | self.logger = logging.getLogger("bilitool")
13 | self.model = Model()
14 | self.bili_downloader = BiliDownloader(self.logger)
15 | self.config = self.model.get_config()
16 |
17 | def extract_filename(self, filename):
18 | illegal_chars = r'[\\/:"*?<>|]'
19 | filename = re.sub(illegal_chars, "", filename)
20 | return filename
21 |
22 | @staticmethod
23 | def package_download_metadata(danmaku, quality, chunksize, multiple):
24 | return {
25 | "danmaku": danmaku,
26 | "quality": quality,
27 | "chunksize": chunksize,
28 | "multiple": multiple,
29 | }
30 |
31 | def get_cid(self, bvid):
32 | return self.bili_downloader.get_cid(bvid)
33 |
34 | def download_video(self, bvid):
35 | cid_group = self.get_cid(bvid)
36 | if self.config["download"]["multiple"]:
37 | for i in range(0, len(cid_group)):
38 | cid = str(cid_group[i]["cid"])
39 | name = cid_group[i]["part"]
40 | self.logger.info(f"Begin download {name}")
41 | self.download_biv_and_danmaku(bvid, cid, name)
42 | else:
43 | cid = str(cid_group[0]["cid"])
44 | name = cid_group[0]["part"]
45 | self.logger.info(f"Begin download {name}")
46 | self.download_biv_and_danmaku(bvid, cid, name)
47 |
48 | def download_biv_and_danmaku(self, bvid, cid, name_raw="video"):
49 | name = self.extract_filename(name_raw)
50 | self.bili_downloader.get_bvid_video(bvid, cid, name)
51 | self.download_danmaku(cid, name)
52 |
53 | def download_danmaku(self, cid, name="video"):
54 | self.bili_downloader.download_danmaku(cid, name)
55 |
56 | def download_video_entry(self, vid, danmaku, quality, chunksize, multiple):
57 | download_metadata = self.package_download_metadata(
58 | danmaku, quality, chunksize, multiple
59 | )
60 | Model().update_multiple_config("download", download_metadata)
61 | bvid = CheckFormat().only_bvid(vid)
62 | self.download_video(bvid)
63 |
--------------------------------------------------------------------------------
/bilitool/controller/feed_controller.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | from ..feed.bili_video_list import BiliVideoList
4 | from ..utils.check_format import CheckFormat
5 |
6 |
7 | class FeedController(object):
8 | def __init__(self):
9 | self.bili_video_list = BiliVideoList()
10 |
11 | def print_video_list_info(
12 | self, size: int = 20, status_type: str = "pubed,not_pubed,is_pubing"
13 | ):
14 | self.bili_video_list.print_video_list_info(size, status_type)
15 |
16 | def print_video_info(self, vid: str):
17 | bvid = CheckFormat().only_bvid(vid)
18 | video_info = self.bili_video_list.get_video_info(bvid)
19 | extracted_info = self.bili_video_list.extract_video_info(video_info)
20 | self.bili_video_list.print_video_info(extracted_info)
21 |
22 | def get_video_dict_info(
23 | self, size: int = 20, status_type: str = "pubed,not_pubed,is_pubing"
24 | ):
25 | return self.bili_video_list.get_video_dict_info(size, status_type)
26 |
--------------------------------------------------------------------------------
/bilitool/controller/login_controller.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | from ..model.model import Model
4 | import qrcode
5 | from ..login.login_bili import LoginBili
6 | from ..login.logout_bili import LogoutBili
7 | from ..login.check_bili_login import CheckBiliLogin
8 |
9 |
10 | class LoginController(object):
11 | def __init__(self):
12 | self.model = Model()
13 | self.login_bili = LoginBili()
14 | self.logout_bili = LogoutBili()
15 | self.check_bili_login = CheckBiliLogin()
16 |
17 | def login_bilibili(self, export):
18 | print(
19 | "Please maximize the window to ensure the QR code is fully displayed, press Enter to continue: ",
20 | flush=True,
21 | )
22 | login_url, auth_code = self.login_bili.get_tv_qrcode_url_and_auth_code()
23 | qr = qrcode.QRCode()
24 | qr.add_data(login_url)
25 | qr.print_ascii()
26 | print("Or copy this link to your phone Bilibili:", login_url, flush=True)
27 | self.login_bili.verify_login(auth_code, export)
28 |
29 | def logout_bilibili(self):
30 | self.logout_bili.logout_bili()
31 |
32 | def check_bilibili_login(self):
33 | return self.check_bili_login.check_bili_login()
34 |
35 | def login_bilibili_with_cookie_file(self, filename):
36 | self.login_bili.get_cookie_file_login(filename)
37 |
--------------------------------------------------------------------------------
/bilitool/controller/upload_controller.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | from ..model.model import Model
4 | from ..upload.bili_upload import BiliUploader
5 | from pathlib import Path
6 | import re
7 | from math import ceil
8 | import logging
9 | from ..utils.parse_yaml import parse_yaml
10 |
11 |
12 | class UploadController:
13 | def __init__(self):
14 | self.logger = logging.getLogger("bilitool")
15 | self.bili_uploader = BiliUploader(self.logger)
16 |
17 | @staticmethod
18 | def package_upload_metadata(tid, title, desc, tag, source, cover, dynamic):
19 | return {
20 | "tid": tid,
21 | "title": title,
22 | "desc": desc,
23 | "tag": tag,
24 | "source": source,
25 | "cover": cover,
26 | "dynamic": dynamic,
27 | }
28 |
29 | def upload_video(self, file, cdn=None):
30 | """upload and publish video on bilibili"""
31 | if cdn == "qn":
32 | upos_url = "//upos-cs-upcdnqn.bilivideo.com/"
33 | probe_version = "20221109"
34 | elif cdn == "bldsa":
35 | upos_url = "//upos-cs-upcdnbldsa.bilivideo.com/"
36 | probe_version = "20221109"
37 | elif cdn == "ws":
38 | upos_url = "//upos-sz-upcdnws.bilivideo.com/"
39 | probe_version = "20221109"
40 | elif cdn == "bda2":
41 | upos_url = "//upos-cs-upcdnbda2.bilivideo.com/"
42 | probe_version = "20221109"
43 | elif cdn == "tx":
44 | upos_url = "//upos-cs-upcdntx.bilivideo.com/"
45 | probe_version = "20221109"
46 | else:
47 | upos_url, cdn, probe_version = self.bili_uploader.probe()
48 | file = Path(file)
49 | assert file.exists(), f"The file {file} does not exist"
50 | filename = file.name
51 | title = Model().get_config()["upload"]["title"] or file.stem
52 | Model().update_specific_config("upload", "title", title)
53 | filesize = file.stat().st_size
54 | self.logger.info(f"The {title} to be uploaded")
55 |
56 | # upload video
57 | self.logger.info("Start preuploading the video")
58 | pre_upload_response = self.bili_uploader.preupload(
59 | filename=filename, filesize=filesize, cdn=cdn, probe_version=probe_version
60 | )
61 | upos_uri = pre_upload_response["upos_uri"].split("//")[-1]
62 | auth = pre_upload_response["auth"]
63 | biz_id = pre_upload_response["biz_id"]
64 | chunk_size = pre_upload_response["chunk_size"]
65 | chunks = ceil(filesize / chunk_size)
66 |
67 | self.logger.info("Start uploading the video")
68 | upload_video_id_response = self.bili_uploader.get_upload_video_id(
69 | upos_uri=upos_uri, auth=auth, upos_url=upos_url
70 | )
71 | upload_id = upload_video_id_response["upload_id"]
72 | key = upload_video_id_response["key"]
73 |
74 | bilibili_filename = re.search(r"/(.*)\.", key).group(1)
75 |
76 | self.logger.info(f"Uploading the video in {chunks} batches")
77 | fileio = file.open(mode="rb")
78 | self.bili_uploader.upload_video_in_chunks(
79 | upos_uri=upos_uri,
80 | auth=auth,
81 | upload_id=upload_id,
82 | fileio=fileio,
83 | filesize=filesize,
84 | chunk_size=chunk_size,
85 | chunks=chunks,
86 | upos_url=upos_url,
87 | )
88 | fileio.close()
89 |
90 | # notify the all chunks have been uploaded
91 | self.bili_uploader.finish_upload(
92 | upos_uri=upos_uri,
93 | auth=auth,
94 | filename=filename,
95 | upload_id=upload_id,
96 | biz_id=biz_id,
97 | chunks=chunks,
98 | upos_url=upos_url,
99 | )
100 | return bilibili_filename
101 |
102 | def publish_video(self, file, cdn=None):
103 | bilibili_filename = self.upload_video(file, cdn)
104 | # publish video
105 | publish_video_response = self.bili_uploader.publish_video(
106 | bilibili_filename=bilibili_filename
107 | )
108 | if publish_video_response["code"] == 0:
109 | bvid = publish_video_response["data"]["bvid"]
110 | self.logger.info(f"upload success!\tbvid:{bvid}")
111 | return True
112 | else:
113 | self.logger.error(publish_video_response["message"])
114 | return False
115 | # reset the video title
116 | Model().reset_upload_config()
117 |
118 | def append_video_entry(self, video_path, bvid, cdn=None, video_name=None):
119 | bilibili_filename = self.upload_video(video_path, cdn)
120 | video_name = video_name if video_name else Path(video_path).name.strip(".mp4")
121 | video_data = self.bili_uploader.get_video_list_info(bvid)
122 | response = self.bili_uploader.append_video(
123 | bilibili_filename, video_name, video_data
124 | ).json()
125 | if response["code"] == 0:
126 | self.logger.info(f"append {video_name} to {bvid} success!")
127 | return True
128 | else:
129 | self.logger.error(response["message"])
130 | return False
131 | # reset the video title
132 | Model().reset_upload_config()
133 |
134 | def upload_video_entry(
135 | self,
136 | video_path,
137 | yaml,
138 | tid,
139 | title,
140 | desc,
141 | tag,
142 | source,
143 | cover,
144 | dynamic,
145 | cdn=None,
146 | ):
147 | if yaml:
148 | # * is used to unpack the tuple
149 | upload_metadata = self.package_upload_metadata(*parse_yaml(yaml))
150 | else:
151 | upload_metadata = self.package_upload_metadata(
152 | tid, title, desc, tag, source, cover, dynamic
153 | )
154 | if upload_metadata["cover"]:
155 | upload_metadata["cover"] = self.bili_uploader.cover_up(
156 | upload_metadata["cover"]
157 | )
158 | Model().update_multiple_config("upload", upload_metadata)
159 | return self.publish_video(video_path, cdn)
160 |
--------------------------------------------------------------------------------
/bilitool/download/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timerring/bilitool/aefaff618235d7f94db35213a1c938126b25cac7/bilitool/download/__init__.py
--------------------------------------------------------------------------------
/bilitool/download/bili_download.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | import requests
4 | import time
5 | import sys
6 | from tqdm import tqdm
7 | from ..model.model import Model
8 |
9 |
10 | class BiliDownloader:
11 | def __init__(self, logger) -> None:
12 | self.logger = logger
13 | self.config = Model().get_config()
14 | self.headers = Model().get_headers_with_cookies_and_refer()
15 |
16 | def get_cid(self, bvid):
17 | url = "https://api.bilibili.com/x/player/pagelist?bvid=" + bvid
18 | response = requests.get(url, headers=self.headers)
19 | return response.json()["data"]
20 |
21 | def get_bvid_video(self, bvid, cid, name_raw="video"):
22 | url = (
23 | "https://api.bilibili.com/x/player/playurl?cid="
24 | + str(cid)
25 | + "&bvid="
26 | + bvid
27 | + "&qn="
28 | + str(self.config["download"]["quality"])
29 | )
30 | name = name_raw + ".mp4"
31 | response = None
32 | response = requests.get(url, headers=self.headers)
33 | video_url = response.json()["data"]["durl"][0]["url"]
34 | self.download_video(video_url, name)
35 |
36 | def download_video(self, url, name):
37 | response = requests.get(url, headers=self.headers, stream=True)
38 | if response.status_code == 200:
39 | with open(name, "wb") as file:
40 | content_length = int(response.headers["Content-Length"])
41 | progress_bar = tqdm(
42 | total=content_length, unit="B", unit_scale=True, desc=name
43 | )
44 | for chunk in response.iter_content(
45 | chunk_size=self.config["download"]["chunksize"]
46 | ):
47 | file.write(chunk)
48 | progress_bar.update(len(chunk))
49 | progress_bar.close()
50 | self.logger.info(f"Download completed")
51 | else:
52 | self.logger.info(f"{name} Download failed")
53 |
54 | def download_danmaku(self, cid, name_raw="video"):
55 | if self.config["download"]["danmaku"]:
56 | self.logger.info(f"Begin download danmaku")
57 | dm_url = "https://comment.bilibili.com/" + cid + ".xml"
58 | response = requests.get(dm_url, headers=self.headers)
59 | with open(name_raw + ".xml", "wb") as file:
60 | file.write(response.content)
61 | self.logger.info(f"Successfully downloaded danmaku")
62 |
--------------------------------------------------------------------------------
/bilitool/feed/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 |
4 | def VideoListInfo():
5 | return {
6 | "bvid": str(),
7 | "title": "video title",
8 | "state_desc": "",
9 | # the status detail
10 | "state": 0,
11 | "reject_reason": "",
12 | # the overview of status 0: pass review 1: reviewing 2: rejected 3: clash 4: the codec issue
13 | "state_panel": 0,
14 | }
15 |
16 |
17 | # https://github.com/SocialSisterYi/bilibili-API-collect/blob/e5fbfed42807605115c6a9b96447f6328ca263c5/docs/video/attribute_data.md?plain=1#L44
18 | state_dict = {
19 | 1: "橙色通过",
20 | 0: "开放浏览",
21 | -1: "待审",
22 | -2: "被打回",
23 | -3: "网警锁定",
24 | -4: "被锁定",
25 | -5: "管理员锁定",
26 | -6: "修复待审",
27 | -7: "暂缓审核",
28 | -8: "补档待审",
29 | -9: "等待转码",
30 | -10: "延迟审核",
31 | -11: "视频源待修",
32 | -12: "转储失败",
33 | -13: "允许评论待审",
34 | -14: "临时回收站",
35 | -15: "分发中",
36 | -16: "转码失败",
37 | -20: "创建未提交",
38 | -30: "创建已提交",
39 | -40: "定时发布",
40 | -50: "仅UP主可见",
41 | -100: "用户删除",
42 | }
43 |
44 | video_info_dict = {
45 | "title": "标题",
46 | "desc": "描述",
47 | "duration": "时长",
48 | "pubdate": "发布日期",
49 | "owner_name": "作者名称",
50 | "tname": "分区",
51 | "copyright": "版权",
52 | "width": "宽",
53 | "height": "高",
54 | "stat_view": "观看数",
55 | "stat_danmaku": "弹幕数",
56 | "stat_reply": "评论数",
57 | "stat_coin": "硬币数",
58 | "stat_share": "分享数",
59 | "stat_like": "点赞数",
60 | }
61 |
--------------------------------------------------------------------------------
/bilitool/feed/bili_live_list.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | import requests
4 |
5 |
6 | class BiliLiveList:
7 | def __init__(self, headers):
8 | self.headers = headers
9 |
10 | def get_live_info(self, room) -> dict:
11 | """Get the live info of the room"""
12 |
13 | url = (
14 | "https://api.live.bilibili.com/room/v1/Room/get_info?room_id={room}".format(
15 | room=room
16 | )
17 | )
18 | response = requests.get(url=url, headers=self.headers)
19 | if response.status_code != 200:
20 | raise Exception("HTTP ERROR")
21 | response_json = response.json().get("data")
22 | if response.json().get("code") != 0:
23 | raise Exception(response.json().get("message"))
24 | return response_json
25 |
--------------------------------------------------------------------------------
/bilitool/feed/bili_video_list.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | import time
4 | import requests
5 | from ..model.model import Model
6 | from ..authenticate.wbi_sign import WbiSign
7 | from . import VideoListInfo, state_dict, video_info_dict
8 |
9 |
10 | class BiliVideoList(object):
11 | def __init__(self):
12 | self.headers = Model().get_headers_with_cookies_and_refer()
13 |
14 | @staticmethod
15 | def save_video_list_info(archive: dict):
16 | """
17 | Save the video info
18 | """
19 | info = VideoListInfo()
20 | info["bvid"] = archive.get("bvid")
21 | info["title"] = archive.get("title")
22 | info["state"] = archive.get("state")
23 | info["state_desc"] = archive.get("state_desc")
24 | info["reject_reason"] = archive.get("reject_reason")
25 | info["state_panel"] = archive.get("state_panel")
26 | return info
27 |
28 | def get_bili_video_list(
29 | self, size: int = 20, status_type: str = "pubed,not_pubed,is_pubing"
30 | ):
31 | """Query the video list
32 |
33 | :param size: page size
34 | :param status_type: pubed,not_pubed,is_pubing
35 | """
36 | url = f"https://member.bilibili.com/x/web/archives?status={status_type}&pn=1&ps={size}"
37 | resp = requests.get(url=url, headers=self.headers)
38 | if resp.status_code != 200:
39 | raise Exception(f"HTTP ERROR code {resp.status_code}")
40 | response_data = resp.json().get("data")
41 | if resp.json().get("code") != 0:
42 | raise Exception(resp.json().get("message"))
43 | arc_items = list()
44 | page_info = response_data.get("1")
45 | if response_data.get("arc_audits") is not None:
46 | for item in response_data.get("arc_audits"):
47 | archive = item["Archive"]
48 | for i, v in enumerate(item["Videos"]):
49 | if v["reject_reason"] != "":
50 | archive["reject_reason"] += "\nP{p_num}-{r}".format(
51 | p_num=i + 1, r=v["reject_reason"]
52 | )
53 | arc_items.append(self.save_video_list_info(archive))
54 | data: dict = {
55 | "page": page_info,
56 | "status": response_data.get("class"),
57 | "items": arc_items,
58 | }
59 | return data
60 |
61 | def print_video_list_info(
62 | self, size: int = 20, status_type: str = "pubed,not_pubed,is_pubing"
63 | ):
64 | video_data = self.get_bili_video_list(size, status_type)
65 | for item in video_data["items"]:
66 | info = f"{item['state_desc']} | {item['bvid']} | {item['title']}"
67 | extra_info = []
68 | if item["reject_reason"]:
69 | extra_info.append(f"拒绝原因: {item['reject_reason']}")
70 | if extra_info:
71 | info += f" | {' | '.join(extra_info)}"
72 | print(info)
73 |
74 | def get_video_dict_info(
75 | self, size: int = 20, status_type: str = "pubed,not_pubed,is_pubing"
76 | ):
77 | video_data = self.get_bili_video_list(size, status_type)
78 | data = dict()
79 | for item in video_data["items"]:
80 | data[item["title"]] = item["bvid"]
81 | return data
82 |
83 | def get_video_info(self, bvid: str) -> dict:
84 | """Get the video info of the bvid"""
85 | url = f"https://api.bilibili.com/x/web-interface/view?bvid={bvid}"
86 | resp = requests.get(url=url, headers=self.headers)
87 | if resp.status_code != 200:
88 | raise Exception("HTTP ERROR")
89 | return resp.json()
90 |
91 | @staticmethod
92 | def extract_video_info(response_data):
93 | data = response_data.get("data", {})
94 |
95 | video_info = {
96 | # video info
97 | "title": data.get("title"),
98 | "desc": data.get("desc"),
99 | "duration": data.get("duration"),
100 | "pubdate": data.get("pubdate"),
101 | "owner_name": data.get("owner", {}).get("name"),
102 | "tname": data.get("tname"),
103 | "copyright": data.get("copyright"),
104 | "width": data.get("dimension", {}).get("width"),
105 | "height": data.get("dimension", {}).get("height"),
106 | # video status
107 | "stat_view": data.get("stat", {}).get("view"),
108 | "stat_danmaku": data.get("stat", {}).get("danmaku"),
109 | "stat_reply": data.get("stat", {}).get("reply"),
110 | "stat_coin": data.get("stat", {}).get("coin"),
111 | "stat_share": data.get("stat", {}).get("share"),
112 | "stat_like": data.get("stat", {}).get("like"),
113 | }
114 |
115 | return video_info
116 |
117 | @staticmethod
118 | def print_video_info(video_info):
119 | for key, value in video_info.items():
120 | if key == "duration":
121 | value = f"{value // 60}:{value % 60}"
122 | elif key == "pubdate":
123 | value = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(value))
124 | elif key == "copyright":
125 | value = "原创" if value == 1 else "转载"
126 | label = video_info_dict.get(key, key)
127 | print(f"{label}: {value}")
128 |
--------------------------------------------------------------------------------
/bilitool/login/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timerring/bilitool/aefaff618235d7f94db35213a1c938126b25cac7/bilitool/login/__init__.py
--------------------------------------------------------------------------------
/bilitool/login/check_bili_login.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | from ..model.model import Model
4 | import requests
5 | import json
6 |
7 |
8 | class CheckBiliLogin(object):
9 | def __init__(self):
10 | self.config = Model().get_config()
11 |
12 | def check_bili_login(self):
13 | url = "https://api.bilibili.com/x/web-interface/nav"
14 | with requests.Session() as session:
15 | session.headers = self.config["headers"]
16 | session.cookies = requests.utils.cookiejar_from_dict(self.config["cookies"])
17 | response = session.get(url)
18 | if response.status_code == 200:
19 | response_data = json.loads(response.text)
20 | if response_data["data"]["isLogin"] == True:
21 | self.obtain_bili_login_info(response_data)
22 | return True
23 | else:
24 | print(
25 | "There is currently no login account, some functions may not work"
26 | )
27 | # print(response.text)
28 | return False
29 | else:
30 | print("Check failed, please check the info")
31 | print(response.text)
32 | return False
33 |
34 | def obtain_bili_login_info(self, response_data):
35 | current_level = response_data["data"]["level_info"]["current_level"]
36 | uname = response_data["data"]["uname"]
37 | vip_status = response_data["data"]["vipStatus"]
38 |
39 | print(f"Current account: {uname}")
40 | print(f"Current level: {current_level}")
41 | if vip_status == 1:
42 | print(f"Status: 大会员")
43 | else:
44 | print(f"Status: 非大会员")
45 |
--------------------------------------------------------------------------------
/bilitool/login/login_bili.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | import hashlib
4 | import subprocess
5 | import time
6 | import json
7 | from urllib.parse import urlencode
8 | from ..model.model import Model
9 | from .check_bili_login import CheckBiliLogin
10 |
11 |
12 | class LoginBili(object):
13 | def __init__(self):
14 | self.APP_KEY = "4409e2ce8ffd12b8"
15 | self.APP_SEC = "59b43e04ad6965f34319062b478f83dd"
16 |
17 | def signature(self, params):
18 | params["appkey"] = self.APP_KEY
19 | keys = sorted(params.keys())
20 | query = "&".join(f"{k}={params[k]}" for k in keys)
21 | query += self.APP_SEC
22 | md5_hash = hashlib.md5(query.encode("utf-8")).hexdigest()
23 | params["sign"] = md5_hash
24 |
25 | @staticmethod
26 | def map_to_string(params):
27 | return urlencode(params)
28 |
29 | def execute_curl_command(self, api, data):
30 | data_string = LoginBili.map_to_string(data)
31 | headers = "Content-Type: application/x-www-form-urlencoded"
32 | curl_command = f'curl -X POST -H "{headers}" -d "{data_string}" {api}'
33 | result = subprocess.run(
34 | curl_command, shell=True, capture_output=True, text=True, encoding="utf-8"
35 | )
36 | if result.returncode != 0:
37 | raise Exception(f"curl command failed: {result.stderr}")
38 | return json.loads(result.stdout)
39 |
40 | def get_tv_qrcode_url_and_auth_code(self):
41 | api = "https://passport.bilibili.com/x/passport-tv-login/qrcode/auth_code"
42 | data = {"local_id": "0", "ts": str(int(time.time()))}
43 | self.signature(data)
44 | body = self.execute_curl_command(api, data)
45 | if body["code"] == 0:
46 | qrcode_url = body["data"]["url"]
47 | auth_code = body["data"]["auth_code"]
48 | return qrcode_url, auth_code
49 | else:
50 | raise Exception("get_tv_qrcode_url_and_auth_code error")
51 |
52 | def verify_login(self, auth_code, export):
53 | api = "https://passport.bilibili.com/x/passport-tv-login/qrcode/poll"
54 | data = {"auth_code": auth_code, "local_id": "0", "ts": str(int(time.time()))}
55 | self.signature(data)
56 | while True:
57 | body = self.execute_curl_command(api, data)
58 | if body["code"] == 0:
59 | filename = "cookie.json"
60 | if export:
61 | with open(filename, "w", encoding="utf-8") as f:
62 | json.dump(body, f, ensure_ascii=False, indent=4)
63 | print(f"cookie has been saved to {filename}")
64 |
65 | access_key_value = body["data"]["access_token"]
66 | sessdata_value = body["data"]["cookie_info"]["cookies"][0]["value"]
67 | bili_jct_value = body["data"]["cookie_info"]["cookies"][1]["value"]
68 | dede_user_id_value = body["data"]["cookie_info"]["cookies"][2]["value"]
69 | dede_user_id_ckmd5_value = body["data"]["cookie_info"]["cookies"][3][
70 | "value"
71 | ]
72 | sid_value = body["data"]["cookie_info"]["cookies"][4]["value"]
73 | Model().save_cookies_info(
74 | access_key_value,
75 | sessdata_value,
76 | bili_jct_value,
77 | dede_user_id_value,
78 | dede_user_id_ckmd5_value,
79 | sid_value,
80 | )
81 | print("Login success!")
82 | break
83 | else:
84 | time.sleep(3)
85 |
86 | def get_cookie_file_login(self, filename):
87 | with open(filename, "r", encoding="utf-8") as f:
88 | body = json.load(f)
89 | access_key_value = body["data"]["access_token"]
90 | sessdata_value = body["data"]["cookie_info"]["cookies"][0]["value"]
91 | bili_jct_value = body["data"]["cookie_info"]["cookies"][1]["value"]
92 | dede_user_id_value = body["data"]["cookie_info"]["cookies"][2]["value"]
93 | dede_user_id_ckmd5_value = body["data"]["cookie_info"]["cookies"][3][
94 | "value"
95 | ]
96 | sid_value = body["data"]["cookie_info"]["cookies"][4]["value"]
97 | Model().save_cookies_info(
98 | access_key_value,
99 | sessdata_value,
100 | bili_jct_value,
101 | dede_user_id_value,
102 | dede_user_id_ckmd5_value,
103 | sid_value,
104 | )
105 | if CheckBiliLogin().check_bili_login():
106 | print("Login success!", flush=True)
107 | else:
108 | print("Login failed, please check the cookie file again", flush=True)
109 |
--------------------------------------------------------------------------------
/bilitool/login/logout_bili.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | import http.client
4 | import urllib.parse
5 | from ..model.model import Model
6 | import json
7 |
8 |
9 | # The requests lib here was suspended by the official, so use http.client to make the request
10 | class LogoutBili(object):
11 | def __init__(self):
12 | self.config = Model().get_config()
13 |
14 | def logout_bili(self):
15 | host = "passport.bilibili.com"
16 | path = "/login/exit/v2"
17 |
18 | headers = {
19 | "Cookie": f'DedeUserID={self.config["cookies"]["DedeUserID"]}; bili_jct={self.config["cookies"]["bili_jct"]}; SESSDATA={self.config["cookies"]["SESSDATA"]}',
20 | "Content-Type": "application/x-www-form-urlencoded",
21 | }
22 |
23 | data = {"biliCSRF": self.config["cookies"]["bili_jct"]}
24 | encoded_data = urllib.parse.urlencode(data)
25 | connection = http.client.HTTPSConnection(host)
26 |
27 | connection.request("POST", path, body=encoded_data, headers=headers)
28 |
29 | response = connection.getresponse()
30 | response_json = json.loads(response.read().decode("utf-8"))
31 | if response_json["code"] == 0:
32 | print("Logout successfully, the cookie has expired")
33 | Model().reset_cookies()
34 | else:
35 | print("Logout failed, check the info:")
36 | print(response_json)
37 | connection.close()
38 |
--------------------------------------------------------------------------------
/bilitool/model/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timerring/bilitool/aefaff618235d7f94db35213a1c938126b25cac7/bilitool/model/__init__.py
--------------------------------------------------------------------------------
/bilitool/model/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "headers": {
3 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/63.0.3239.108"
4 | },
5 | "cookies": {
6 | "SESSDATA": "",
7 | "bili_jct": "",
8 | "DedeUserID": "",
9 | "DedeUserID__ckMd5": "",
10 | "access_key": "",
11 | "sid": ""
12 | },
13 | "upload": {
14 | "copyright": 2,
15 | "title": "",
16 | "desc": "",
17 | "tid": 138,
18 | "tag": "bilitool",
19 | "source": "\u6765\u6e90\u4e8e\u7f51\u7edc",
20 | "cover": "",
21 | "dynamic": ""
22 | },
23 | "download": {
24 | "danmaku": false,
25 | "quality": 64,
26 | "chunksize": 1024,
27 | "multiple": false
28 | }
29 | }
--------------------------------------------------------------------------------
/bilitool/model/model.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | import json
4 | import os
5 |
6 |
7 | def add_headers_info(referer=None):
8 | def decorator(func):
9 | def wrapper(self, *args, **kwargs):
10 | headers = func(self, *args, **kwargs)
11 | config_info = self.get_config()
12 | cookies = config_info["cookies"]
13 | cookie_string = "; ".join(
14 | [f"{key}={value}" for key, value in cookies.items() if value]
15 | )
16 | headers["Cookie"] = cookie_string
17 | if referer:
18 | headers["Referer"] = referer
19 | return headers
20 |
21 | return wrapper
22 |
23 | return decorator
24 |
25 |
26 | class Model:
27 | def __init__(self, path=None) -> None:
28 | if path is None:
29 | self.path = os.path.join(os.path.dirname(__file__), "config.json")
30 | else:
31 | self.path = path
32 | self.default_config = {
33 | "headers": {
34 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
35 | },
36 | "cookies": {
37 | "SESSDATA": "",
38 | "bili_jct": "",
39 | "DedeUserID": "",
40 | "DedeUserID__ckMd5": "",
41 | "access_key": "",
42 | "sid": "",
43 | },
44 | "upload": {
45 | "copyright": 2,
46 | "title": "",
47 | "desc": "",
48 | "tid": 138,
49 | "tag": "bilitool",
50 | "source": "\u6765\u6e90\u4e8e\u4e92\u8054\u7f51",
51 | "cover": "",
52 | "dynamic": "",
53 | },
54 | "download": {
55 | "danmaku": 1,
56 | "quality": 64,
57 | "chunksize": 1024,
58 | "multiple": False,
59 | },
60 | }
61 |
62 | def get_default_config(self):
63 | return self.default_config
64 |
65 | def reset_config(self):
66 | self.write(self.default_config)
67 |
68 | @add_headers_info()
69 | def get_headers_with_cookies(self):
70 | return self.get_config()["headers"]
71 |
72 | @add_headers_info(referer="https://www.bilibili.com/")
73 | def get_headers_with_cookies_and_refer(self):
74 | return self.get_config()["headers"]
75 |
76 | def save_cookies_info(
77 | self, access_key, sessdata, bili_jct, dede_user_id, dede_user_id_ckmd5, sid
78 | ):
79 | config_info = self.get_config()
80 | config_info["cookies"]["access_key"] = access_key
81 | config_info["cookies"]["SESSDATA"] = sessdata
82 | config_info["cookies"]["bili_jct"] = bili_jct
83 | config_info["cookies"]["DedeUserID"] = dede_user_id
84 | config_info["cookies"]["DedeUserID__ckMd5"] = dede_user_id_ckmd5
85 | config_info["cookies"]["sid"] = sid
86 | self.write(config_info)
87 |
88 | def update_specific_config(self, action, key, value):
89 | config_info = self.get_config()
90 | config_info[action][key] = value
91 | self.write(config_info)
92 |
93 | def update_multiple_config(self, action, updates: dict):
94 | config_info = self.get_config()
95 | for key, value in updates.items():
96 | config_info[action][key] = value
97 | self.write(config_info)
98 |
99 | def reset_upload_config(self):
100 | config_info = self.get_config()
101 | config_info["upload"]["copyright"] = 2
102 | config_info["upload"]["title"] = ""
103 | config_info["upload"]["desc"] = ""
104 | config_info["upload"]["tid"] = 138
105 | config_info["upload"]["tag"] = "bilitool"
106 | config_info["upload"]["source"] = "\u6765\u6e90\u4e8e\u4e92\u8054\u7f51"
107 | config_info["upload"]["cover"] = ""
108 | config_info["upload"]["dynamic"] = ""
109 | self.write(config_info)
110 |
111 | def reset_cookies(self):
112 | config_info = self.get_config()
113 | config_info["cookies"]["access_key"] = ""
114 | config_info["cookies"]["SESSDATA"] = ""
115 | config_info["cookies"]["bili_jct"] = ""
116 | config_info["cookies"]["DedeUserID"] = ""
117 | config_info["cookies"]["DedeUserID__ckMd5"] = ""
118 | config_info["cookies"]["sid"] = ""
119 | self.write(config_info)
120 |
121 | def get_config(self):
122 | if not os.path.exists(self.path):
123 | self.reset_config()
124 | return self.read()
125 |
126 | def read(self):
127 | with open(self.path, "r") as f:
128 | return json.load(f)
129 |
130 | def write(self, config):
131 | with open(self.path, "w") as f:
132 | json.dump(config, f, indent=4)
133 |
--------------------------------------------------------------------------------
/bilitool/upload/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timerring/bilitool/aefaff618235d7f94db35213a1c938126b25cac7/bilitool/upload/__init__.py
--------------------------------------------------------------------------------
/bilitool/upload/bili_upload.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | import re
4 | import sys
5 | import logging
6 | import argparse
7 | from math import ceil
8 | import json
9 | from pathlib import Path
10 | import requests
11 | from tqdm import tqdm
12 | from ..utils.parse_cookies import parse_cookies
13 | from ..model.model import Model
14 | import hashlib
15 | import time
16 | import base64
17 |
18 |
19 | class BiliUploader(object):
20 | def __init__(self, logger):
21 | self.logger = logger
22 | self.config = Model().get_config()
23 | self.session = requests.Session()
24 | self.session.headers = self.config["headers"]
25 | self.session.cookies = requests.utils.cookiejar_from_dict(
26 | self.config["cookies"]
27 | )
28 | self.headers = Model().get_headers_with_cookies_and_refer()
29 |
30 | def cover_up(self, img: str):
31 | """Upload the cover image
32 | Parameters
33 | ----------
34 | - img: img path or stream
35 | Returns
36 | -------
37 | - url: str
38 | the url of the cover image in bili server
39 | """
40 | from PIL import Image
41 | from io import BytesIO
42 |
43 | with Image.open(img) as im:
44 | # you should keep the image ratio 16:10
45 | xsize, ysize = im.size
46 | if xsize / ysize > 1.6:
47 | delta = xsize - ysize * 1.6
48 | region = im.crop((delta / 2, 0, xsize - delta / 2, ysize))
49 | else:
50 | delta = ysize - xsize * 10 / 16
51 | region = im.crop((0, delta / 2, xsize, ysize - delta / 2))
52 | buffered = BytesIO()
53 | region.save(buffered, format=im.format)
54 | r = self.session.post(
55 | url="https://member.bilibili.com/x/vu/web/cover/up",
56 | data={
57 | "cover": b"data:image/jpeg;base64,"
58 | + (base64.b64encode(buffered.getvalue())),
59 | "csrf": self.config["cookies"]["bili_jct"],
60 | },
61 | timeout=30,
62 | )
63 | buffered.close()
64 | res = r.json()
65 | if res.get("data") is None:
66 | raise Exception(res)
67 | self.logger.info(f"the cover image has been uploaded as {res['data']['url']}")
68 | return res["data"]["url"]
69 |
70 | def probe(self):
71 | self.logger.info("begin to probe the best cdn line")
72 | ret = requests.get(
73 | "https://member.bilibili.com/preupload?r=probe",
74 | headers=self.headers,
75 | timeout=5,
76 | ).json()
77 | data, auto_os = None, None
78 | min_cost = 0
79 | if ret["probe"].get("get"):
80 | method = "get"
81 | else:
82 | method = "post"
83 | data = bytes(int(1024 * 0.1 * 1024))
84 | for line in ret["lines"]:
85 | start = time.perf_counter()
86 | test = requests.request(
87 | method, f"https:{line['probe_url']}", data=data, timeout=30
88 | )
89 | cost = time.perf_counter() - start
90 | print(line["query"], cost)
91 | if test.status_code != 200:
92 | return
93 | if not min_cost or min_cost > cost:
94 | auto_os = line
95 | min_cost = cost
96 | auto_os["cost"] = min_cost
97 | self.logger.info(f"the best cdn line is:{auto_os}")
98 | upos_url = auto_os["probe_url"].rstrip("OK")
99 | self.logger.info(f"the upos_url is:{upos_url}")
100 | query_params = dict(param.split("=") for param in auto_os["query"].split("&"))
101 | cdn = query_params.get("upcdn")
102 | self.logger.info(f"the cdn is:{cdn}")
103 | probe_version = query_params.get("probe_version")
104 | self.logger.info(f"the probe_version is:{probe_version}")
105 | return upos_url, cdn, probe_version
106 |
107 | def preupload(self, *, filename, filesize, cdn, probe_version):
108 | """The preupload process to get `upos_uri` and `auth` information.
109 | Parameters
110 | ----------
111 | filename : str
112 | the name of the video to be uploaded
113 | filesize : int
114 | the size of the video to be uploaded
115 | biz_id : num
116 | the business id
117 |
118 | Returns
119 | -------
120 | - upos_uri: str
121 | the uri of the video will be stored in server
122 | - auth: str
123 | the auth information
124 |
125 | [Easter egg] Sometimes I'm also confused why it is called `upos`
126 | So I ask a question on the V2EX: https://v2ex.com/t/1103152
127 | Finally, the netizens reckon that may be the translation style of bilibili.
128 | """
129 | url = "https://member.bilibili.com/preupload"
130 | params = {
131 | "name": filename,
132 | "size": filesize,
133 | # The parameters below are fixed
134 | "r": "upos",
135 | "profile": "ugcupos/bup",
136 | "ssl": 0,
137 | "version": "2.8.9",
138 | "build": "2080900",
139 | "upcdn": cdn,
140 | "probe_version": probe_version,
141 | }
142 | res_json = self.session.get(
143 | url, params=params, headers={"TE": "Trailers"}
144 | ).json()
145 | assert res_json["OK"] == 1
146 | self.logger.info("Completed preupload phase")
147 | # print(res_json)
148 | return res_json
149 |
150 | def get_upload_video_id(self, *, upos_uri, auth, upos_url):
151 | """Get the `upload_id` of video.
152 |
153 | Parameters
154 | ----------
155 | - upos_uri: str
156 | get from `preupload`
157 | - auth: str
158 | get from `preupload`
159 | Returns
160 | -------
161 | - upload_id: str
162 | the id of the video to be uploaded
163 | """
164 | url = f"https:{upos_url}{upos_uri}?uploads&output=json"
165 | res_json = self.session.post(url, headers={"X-Upos-Auth": auth}).json()
166 | assert res_json["OK"] == 1
167 | self.logger.info("Completed upload_id obtaining phase")
168 | # print(res_json)
169 | return res_json
170 |
171 | def upload_video_in_chunks(
172 | self,
173 | *,
174 | upos_uri,
175 | auth,
176 | upload_id,
177 | fileio,
178 | filesize,
179 | chunk_size,
180 | chunks,
181 | upos_url,
182 | ):
183 | """Upload the video in chunks.
184 |
185 | Parameters
186 | ----------
187 | - upos_uri: str
188 | get from `preupload`
189 | - auth: str
190 | get from `preupload`
191 | - upload_id: str
192 | get from `get_upload_video_id`
193 | - fileio: io.BufferedReader
194 | the io stream of the video to be uploaded
195 | - filesize: int
196 | the size of the video to be uploaded
197 | - chunk_size: int
198 | the size of each chunk to be uploaded
199 | - chunks: int
200 | the number of chunks to be uploaded
201 | """
202 | url = f"https:{upos_url}{upos_uri}"
203 | params = {
204 | "partNumber": None, # start from 1
205 | "uploadId": upload_id,
206 | "chunk": None, # start from 0
207 | "chunks": chunks,
208 | "size": None, # current batch size
209 | "start": None,
210 | "end": None,
211 | "total": filesize,
212 | }
213 | # Single thread upload
214 | with tqdm(
215 | total=filesize, desc="Uploading video", unit="B", unit_scale=True
216 | ) as pbar:
217 | for chunknum in range(chunks):
218 | start = fileio.tell()
219 | batchbytes = fileio.read(chunk_size)
220 | params["partNumber"] = chunknum + 1
221 | params["chunk"] = chunknum
222 | params["size"] = len(batchbytes)
223 | params["start"] = start
224 | params["end"] = fileio.tell()
225 | res = self.session.put(
226 | url, params=params, data=batchbytes, headers={"X-Upos-Auth": auth}
227 | )
228 | assert res.status_code == 200
229 | self.logger.debug(f"Completed chunk{chunknum+1} uploading")
230 | pbar.update(len(batchbytes))
231 | # print(res)
232 |
233 | def finish_upload(
234 | self, *, upos_uri, auth, filename, upload_id, biz_id, chunks, upos_url
235 | ):
236 | """Notify the all chunks have been uploaded.
237 |
238 | Parameters
239 | ----------
240 | - upos_uri: str
241 | get from `preupload`
242 | - auth: str
243 | get from `preupload`
244 | - filename: str
245 | the name of the video to be uploaded
246 | - upload_id: str
247 | get from `get_upload_video_id`
248 | - biz_id: num
249 | get from `preupload`
250 | - chunks: int
251 | the number of chunks to be uploaded
252 | """
253 | url = f"https:{upos_url}{upos_uri}"
254 | params = {
255 | "output": "json",
256 | "name": filename,
257 | "profile": "ugcupos/bup",
258 | "uploadId": upload_id,
259 | "biz_id": biz_id,
260 | }
261 | data = {"parts": [{"partNumber": i, "eTag": "etag"} for i in range(chunks, 1)]}
262 | res_json = self.session.post(
263 | url, params=params, json=data, headers={"X-Upos-Auth": auth}
264 | ).json()
265 | assert res_json["OK"] == 1
266 | # print(res_json)
267 |
268 | # API docs: https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/creativecenter/upload.md
269 | def publish_video(self, bilibili_filename):
270 | """publish the uploaded video"""
271 | config = Model().get_config()
272 | url = f'https://member.bilibili.com/x/vu/client/add?access_key={config["cookies"]["access_key"]}'
273 | data = {
274 | "copyright": config["upload"]["copyright"],
275 | "videos": [
276 | {
277 | "filename": bilibili_filename,
278 | "title": config["upload"]["title"],
279 | "desc": config["upload"]["desc"],
280 | }
281 | ],
282 | "source": config["upload"]["source"],
283 | "tid": config["upload"]["tid"],
284 | "title": config["upload"]["title"],
285 | "cover": config["upload"]["cover"],
286 | "tag": config["upload"]["tag"],
287 | "desc_format_id": 0,
288 | "desc": config["upload"]["desc"],
289 | "dynamic": config["upload"]["dynamic"],
290 | "subtitle": {"open": 0, "lan": ""},
291 | }
292 | if config["upload"]["copyright"] != 2:
293 | del data["source"]
294 | res_json = self.session.post(url, json=data, headers={"TE": "Trailers"}).json()
295 | # print(res_json)
296 | return res_json
297 |
298 | def get_updated_video_info(self, bvid: str):
299 | url = f"http://member.bilibili.com/x/client/archive/view"
300 | params = {
301 | "access_key": Model().get_config()["cookies"]["access_key"],
302 | "bvid": bvid,
303 | }
304 | resp = requests.get(url=url, headers=self.headers, params=params)
305 | return resp.json()["data"]
306 |
307 | def get_video_list_info(self, bvid: str):
308 | raw_data = self.get_updated_video_info(bvid)
309 | # print(raw_data)
310 | videos = []
311 | for video in raw_data["videos"]:
312 | videos.append(
313 | {
314 | "filename": video["filename"],
315 | "title": video["title"],
316 | "desc": video["desc"],
317 | }
318 | )
319 |
320 | data = {
321 | "bvid": bvid,
322 | "build": 1054,
323 | "copyright": raw_data["archive"]["copyright"],
324 | "videos": videos,
325 | "source": raw_data["archive"]["source"],
326 | "tid": raw_data["archive"]["tid"],
327 | "title": raw_data["archive"]["title"],
328 | "cover": raw_data["archive"]["cover"],
329 | "tag": raw_data["archive"]["tag"],
330 | "no_reprint": raw_data["archive"]["no_reprint"],
331 | "open_elec": raw_data["archive_elec"]["state"],
332 | "desc": raw_data["archive"]["desc"],
333 | }
334 | return data
335 |
336 | @staticmethod
337 | def sign_dict(data: dict, app_secret: str):
338 | """sign a dictionary of request parameters
339 | Parameters
340 | ----------
341 | - data: dictionary of request parameters.
342 | - app_secret: a secret string coupled with app_key.
343 |
344 | Returns
345 | -------
346 | - A hash string. len=32
347 | """
348 | data_str = []
349 | keys = list(data.keys())
350 | keys.sort()
351 | for key in keys:
352 | data_str.append("{}={}".format(key, data[key]))
353 | data_str = "&".join(data_str)
354 | data_str = data_str + app_secret
355 | return hashlib.md5(data_str.encode("utf-8")).hexdigest()
356 |
357 | def append_video(self, bilibili_filename, video_name, data):
358 | """append the uploaded video"""
359 | # Parse JSON string to dict if data is a string
360 | video_to_be_appended = {
361 | "filename": bilibili_filename,
362 | "title": video_name,
363 | "desc": "",
364 | }
365 | data["videos"].append(video_to_be_appended)
366 | headers = {
367 | "Connection": "keep-alive",
368 | "Content-Type": "application/json",
369 | "User-Agent": "",
370 | }
371 | params = {
372 | "access_key": Model().get_config()["cookies"]["access_key"],
373 | }
374 | APPSECRET = "af125a0d5279fd576c1b4418a3e8276d"
375 | params["sign"] = BiliUploader.sign_dict(params, APPSECRET)
376 |
377 | res_json = requests.post(
378 | url="http://member.bilibili.com/x/vu/client/edit",
379 | params=params,
380 | headers=headers,
381 | verify=False,
382 | cookies={"sid": Model().get_config()["cookies"]["sid"]},
383 | json=data,
384 | )
385 | print(res_json.json())
386 | return res_json
387 |
--------------------------------------------------------------------------------
/bilitool/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timerring/bilitool/aefaff618235d7f94db35213a1c938126b25cac7/bilitool/utils/__init__.py
--------------------------------------------------------------------------------
/bilitool/utils/check_format.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 |
4 | class CheckFormat(object):
5 | def __init__(self):
6 | self.XOR_CODE = 23442827791579
7 | self.MASK_CODE = 2251799813685247
8 | self.MAX_AID = 1 << 51
9 | self.ALPHABET = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf"
10 | self.ENCODE_MAP = 8, 7, 0, 5, 1, 3, 2, 4, 6
11 | self.DECODE_MAP = tuple(reversed(self.ENCODE_MAP))
12 |
13 | self.BASE = len(self.ALPHABET)
14 | self.PREFIX = "BV1"
15 | self.PREFIX_LEN = len(self.PREFIX)
16 | self.CODE_LEN = len(self.ENCODE_MAP)
17 |
18 | @staticmethod
19 | def is_bvid(bvid: str) -> bool:
20 | if len(bvid) != 12:
21 | return False
22 | if bvid[0:2] != "BV":
23 | return False
24 | return True
25 |
26 | @staticmethod
27 | def is_chinese(word: str) -> bool:
28 | for ch in word:
29 | if "\u4e00" <= ch <= "\u9fff":
30 | return True
31 | return False
32 |
33 | # https://github.com/SocialSisterYi/bilibili-API-collect/blob/e5fbfed42807605115c6a9b96447f6328ca263c5/docs/misc/bvid_desc.md
34 |
35 | def av2bv(self, aid: int) -> str:
36 | bvid = [""] * 9
37 | tmp = (self.MAX_AID | aid) ^ self.XOR_CODE
38 | for i in range(self.CODE_LEN):
39 | bvid[self.ENCODE_MAP[i]] = self.ALPHABET[tmp % self.BASE]
40 | tmp //= self.BASE
41 | return self.PREFIX + "".join(bvid)
42 |
43 | def bv2av(self, bvid: str) -> int:
44 | assert bvid[:3] == self.PREFIX
45 |
46 | bvid = bvid[3:]
47 | tmp = 0
48 | for i in range(self.CODE_LEN):
49 | idx = self.ALPHABET.index(bvid[self.DECODE_MAP[i]])
50 | tmp = tmp * self.BASE + idx
51 | return (tmp & self.MASK_CODE) ^ self.XOR_CODE
52 |
53 | def convert_bv_and_av(self, vid: str):
54 | if self.is_bvid(str(vid)):
55 | print("The avid of the video is: ", self.bv2av(str(vid)))
56 | else:
57 | print("The bvid of the video is: ", self.av2bv(int(vid)))
58 |
59 | def only_bvid(self, vid: str):
60 | if self.is_bvid(str(vid)):
61 | return vid
62 | else:
63 | return self.av2bv(int(vid))
64 |
--------------------------------------------------------------------------------
/bilitool/utils/get_ip_info.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | import http.client
4 | import urllib.parse
5 | import json
6 | import inspect
7 |
8 |
9 | def suppress_print_in_unittest(func):
10 | def wrapper(*args, **kwargs):
11 | # Check if the caller is a unittest
12 | for frame_info in inspect.stack():
13 | if "unittest" in frame_info.filename:
14 | # If called from unittest, suppress print
15 | return func(*args, **kwargs)
16 |
17 | result = func(*args, **kwargs)
18 | if result:
19 | addr, isp, location, position = result
20 | print(f"IP: {addr}, ISP: {isp}, Location: {location}, Position: {position}")
21 | return result
22 |
23 | return wrapper
24 |
25 |
26 | class IPInfo:
27 | @staticmethod
28 | def get_ip_address(ip=None):
29 | url = "https://api.live.bilibili.com/ip_service/v1/ip_service/get_ip_addr"
30 | if ip:
31 | params = urllib.parse.urlencode({"ip": ip})
32 | full_url = f"{url}?{params}"
33 | else:
34 | full_url = url
35 |
36 | parsed_url = urllib.parse.urlparse(full_url)
37 | host = parsed_url.netloc
38 | path = parsed_url.path + ("?" + parsed_url.query if parsed_url.query else "")
39 |
40 | connection = http.client.HTTPSConnection(host)
41 | connection.request("GET", path)
42 |
43 | response = connection.getresponse()
44 | data = json.loads(response.read().decode("utf-8"))
45 | connection.close()
46 |
47 | return IPInfo.print_ip_info(data)
48 |
49 | @staticmethod
50 | @suppress_print_in_unittest
51 | def print_ip_info(ip_info):
52 | if ip_info["code"] != 0:
53 | return None
54 | else:
55 | addr = ip_info["data"]["addr"]
56 | isp = ip_info["data"]["isp"]
57 | location = (
58 | ip_info["data"]["country"]
59 | + ip_info["data"]["province"]
60 | + ip_info["data"]["city"]
61 | )
62 | position = ip_info["data"]["latitude"] + "," + ip_info["data"]["longitude"]
63 | return addr, isp, location, position
64 |
--------------------------------------------------------------------------------
/bilitool/utils/parse_cookies.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | import json
4 | import os
5 | import sys
6 |
7 |
8 | def parse_cookies(cookies_path):
9 | try:
10 | with open(cookies_path, "r") as file:
11 | data = json.load(file)
12 | except FileNotFoundError:
13 | return "Error: Cookies file not found."
14 | except json.JSONDecodeError:
15 | return "Error: Failed to decode JSON from cookies file."
16 |
17 | cookies = data.get("data", {}).get("cookie_info", {}).get("cookies", [])
18 |
19 | sessdata_value = None
20 | bili_jct_value = None
21 |
22 | for cookie in cookies:
23 | if cookie["name"] == "SESSDATA":
24 | sessdata_value = cookie["value"]
25 | elif cookie["name"] == "bili_jct":
26 | bili_jct_value = cookie["value"]
27 |
28 | if not sessdata_value or not bili_jct_value:
29 | return "Error: Required cookies not found."
30 |
31 | return sessdata_value, bili_jct_value
32 |
33 |
34 | if __name__ == "__main__":
35 | sessdata, bili_jct = parse_cookies("")
36 |
--------------------------------------------------------------------------------
/bilitool/utils/parse_yaml.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | import yaml
4 |
5 |
6 | def parse_yaml(yaml_path):
7 | with open(yaml_path, "r", encoding="utf-8") as file:
8 | data = yaml.safe_load(file)
9 |
10 | # Assuming there's only one streamer entry
11 | tid = data.get("tid")
12 | title = data.get("title")
13 | desc = data.get("desc")
14 | tag = data.get("tag")
15 | source = data.get("source")
16 | cover = data.get("cover")
17 | dynamic = data.get("dynamic")
18 | return tid, title, desc, tag, source, cover, dynamic
19 |
20 |
21 | if __name__ == "__main__":
22 | res = parse_yaml("")
23 | print(res)
24 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/@braintree_sanitize-url.js:
--------------------------------------------------------------------------------
1 | import {
2 | __commonJS
3 | } from "./chunk-BUSYA2B4.js";
4 |
5 | // ../../../node_modules/@braintree/sanitize-url/dist/constants.js
6 | var require_constants = __commonJS({
7 | "../../../node_modules/@braintree/sanitize-url/dist/constants.js"(exports) {
8 | "use strict";
9 | Object.defineProperty(exports, "__esModule", { value: true });
10 | exports.BLANK_URL = exports.relativeFirstCharacters = exports.whitespaceEscapeCharsRegex = exports.urlSchemeRegex = exports.ctrlCharactersRegex = exports.htmlCtrlEntityRegex = exports.htmlEntitiesRegex = exports.invalidProtocolRegex = void 0;
11 | exports.invalidProtocolRegex = /^([^\w]*)(javascript|data|vbscript)/im;
12 | exports.htmlEntitiesRegex = /(\w+)(^\w|;)?/g;
13 | exports.htmlCtrlEntityRegex = /&(newline|tab);/gi;
14 | exports.ctrlCharactersRegex = /[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim;
15 | exports.urlSchemeRegex = /^.+(:|:)/gim;
16 | exports.whitespaceEscapeCharsRegex = /(\\|%5[cC])((%(6[eE]|72|74))|[nrt])/g;
17 | exports.relativeFirstCharacters = [".", "/"];
18 | exports.BLANK_URL = "about:blank";
19 | }
20 | });
21 |
22 | // ../../../node_modules/@braintree/sanitize-url/dist/index.js
23 | var require_dist = __commonJS({
24 | "../../../node_modules/@braintree/sanitize-url/dist/index.js"(exports) {
25 | Object.defineProperty(exports, "__esModule", { value: true });
26 | exports.sanitizeUrl = void 0;
27 | var constants_1 = require_constants();
28 | function isRelativeUrlWithoutProtocol(url) {
29 | return constants_1.relativeFirstCharacters.indexOf(url[0]) > -1;
30 | }
31 | function decodeHtmlCharacters(str) {
32 | var removedNullByte = str.replace(constants_1.ctrlCharactersRegex, "");
33 | return removedNullByte.replace(constants_1.htmlEntitiesRegex, function(match, dec) {
34 | return String.fromCharCode(dec);
35 | });
36 | }
37 | function isValidUrl(url) {
38 | return URL.canParse(url);
39 | }
40 | function decodeURI(uri) {
41 | try {
42 | return decodeURIComponent(uri);
43 | } catch (e) {
44 | return uri;
45 | }
46 | }
47 | function sanitizeUrl(url) {
48 | if (!url) {
49 | return constants_1.BLANK_URL;
50 | }
51 | var charsToDecode;
52 | var decodedUrl = decodeURI(url.trim());
53 | do {
54 | decodedUrl = decodeHtmlCharacters(decodedUrl).replace(constants_1.htmlCtrlEntityRegex, "").replace(constants_1.ctrlCharactersRegex, "").replace(constants_1.whitespaceEscapeCharsRegex, "").trim();
55 | decodedUrl = decodeURI(decodedUrl);
56 | charsToDecode = decodedUrl.match(constants_1.ctrlCharactersRegex) || decodedUrl.match(constants_1.htmlEntitiesRegex) || decodedUrl.match(constants_1.htmlCtrlEntityRegex) || decodedUrl.match(constants_1.whitespaceEscapeCharsRegex);
57 | } while (charsToDecode && charsToDecode.length > 0);
58 | var sanitizedUrl = decodedUrl;
59 | if (!sanitizedUrl) {
60 | return constants_1.BLANK_URL;
61 | }
62 | if (isRelativeUrlWithoutProtocol(sanitizedUrl)) {
63 | return sanitizedUrl;
64 | }
65 | var trimmedUrl = sanitizedUrl.trimStart();
66 | var urlSchemeParseResults = trimmedUrl.match(constants_1.urlSchemeRegex);
67 | if (!urlSchemeParseResults) {
68 | return sanitizedUrl;
69 | }
70 | var urlScheme = urlSchemeParseResults[0].toLowerCase().trim();
71 | if (constants_1.invalidProtocolRegex.test(urlScheme)) {
72 | return constants_1.BLANK_URL;
73 | }
74 | var backSanitized = trimmedUrl.replace(/\\/g, "/");
75 | if (urlScheme === "mailto:" || urlScheme.includes("://")) {
76 | return backSanitized;
77 | }
78 | if (urlScheme === "http:" || urlScheme === "https:") {
79 | if (!isValidUrl(backSanitized)) {
80 | return constants_1.BLANK_URL;
81 | }
82 | var url_1 = new URL(backSanitized);
83 | url_1.protocol = url_1.protocol.toLowerCase();
84 | url_1.hostname = url_1.hostname.toLowerCase();
85 | return url_1.toString();
86 | }
87 | return backSanitized;
88 | }
89 | exports.sanitizeUrl = sanitizeUrl;
90 | }
91 | });
92 | export default require_dist();
93 | //# sourceMappingURL=@braintree_sanitize-url.js.map
94 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "sources": ["../../../../../../../node_modules/@braintree/sanitize-url/dist/constants.js", "../../../../../../../node_modules/@braintree/sanitize-url/dist/index.js"],
4 | "sourcesContent": ["\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.BLANK_URL = exports.relativeFirstCharacters = exports.whitespaceEscapeCharsRegex = exports.urlSchemeRegex = exports.ctrlCharactersRegex = exports.htmlCtrlEntityRegex = exports.htmlEntitiesRegex = exports.invalidProtocolRegex = void 0;\nexports.invalidProtocolRegex = /^([^\\w]*)(javascript|data|vbscript)/im;\nexports.htmlEntitiesRegex = /(\\w+)(^\\w|;)?/g;\nexports.htmlCtrlEntityRegex = /&(newline|tab);/gi;\nexports.ctrlCharactersRegex = /[\\u0000-\\u001F\\u007F-\\u009F\\u2000-\\u200D\\uFEFF]/gim;\nexports.urlSchemeRegex = /^.+(:|:)/gim;\nexports.whitespaceEscapeCharsRegex = /(\\\\|%5[cC])((%(6[eE]|72|74))|[nrt])/g;\nexports.relativeFirstCharacters = [\".\", \"/\"];\nexports.BLANK_URL = \"about:blank\";\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.sanitizeUrl = void 0;\nvar constants_1 = require(\"./constants\");\nfunction isRelativeUrlWithoutProtocol(url) {\n return constants_1.relativeFirstCharacters.indexOf(url[0]) > -1;\n}\nfunction decodeHtmlCharacters(str) {\n var removedNullByte = str.replace(constants_1.ctrlCharactersRegex, \"\");\n return removedNullByte.replace(constants_1.htmlEntitiesRegex, function (match, dec) {\n return String.fromCharCode(dec);\n });\n}\nfunction isValidUrl(url) {\n return URL.canParse(url);\n}\nfunction decodeURI(uri) {\n try {\n return decodeURIComponent(uri);\n }\n catch (e) {\n // Ignoring error\n // It is possible that the URI contains a `%` not associated\n // with URI/URL-encoding.\n return uri;\n }\n}\nfunction sanitizeUrl(url) {\n if (!url) {\n return constants_1.BLANK_URL;\n }\n var charsToDecode;\n var decodedUrl = decodeURI(url.trim());\n do {\n decodedUrl = decodeHtmlCharacters(decodedUrl)\n .replace(constants_1.htmlCtrlEntityRegex, \"\")\n .replace(constants_1.ctrlCharactersRegex, \"\")\n .replace(constants_1.whitespaceEscapeCharsRegex, \"\")\n .trim();\n decodedUrl = decodeURI(decodedUrl);\n charsToDecode =\n decodedUrl.match(constants_1.ctrlCharactersRegex) ||\n decodedUrl.match(constants_1.htmlEntitiesRegex) ||\n decodedUrl.match(constants_1.htmlCtrlEntityRegex) ||\n decodedUrl.match(constants_1.whitespaceEscapeCharsRegex);\n } while (charsToDecode && charsToDecode.length > 0);\n var sanitizedUrl = decodedUrl;\n if (!sanitizedUrl) {\n return constants_1.BLANK_URL;\n }\n if (isRelativeUrlWithoutProtocol(sanitizedUrl)) {\n return sanitizedUrl;\n }\n // Remove any leading whitespace before checking the URL scheme\n var trimmedUrl = sanitizedUrl.trimStart();\n var urlSchemeParseResults = trimmedUrl.match(constants_1.urlSchemeRegex);\n if (!urlSchemeParseResults) {\n return sanitizedUrl;\n }\n var urlScheme = urlSchemeParseResults[0].toLowerCase().trim();\n if (constants_1.invalidProtocolRegex.test(urlScheme)) {\n return constants_1.BLANK_URL;\n }\n var backSanitized = trimmedUrl.replace(/\\\\/g, \"/\");\n // Handle special cases for mailto: and custom deep-link protocols\n if (urlScheme === \"mailto:\" || urlScheme.includes(\"://\")) {\n return backSanitized;\n }\n // For http and https URLs, perform additional validation\n if (urlScheme === \"http:\" || urlScheme === \"https:\") {\n if (!isValidUrl(backSanitized)) {\n return constants_1.BLANK_URL;\n }\n var url_1 = new URL(backSanitized);\n url_1.protocol = url_1.protocol.toLowerCase();\n url_1.hostname = url_1.hostname.toLowerCase();\n return url_1.toString();\n }\n return backSanitized;\n}\nexports.sanitizeUrl = sanitizeUrl;\n"],
5 | "mappings": ";;;;;AAAA;AAAA;AAAA;AACA,WAAO,eAAe,SAAS,cAAc,EAAE,OAAO,KAAK,CAAC;AAC5D,YAAQ,YAAY,QAAQ,0BAA0B,QAAQ,6BAA6B,QAAQ,iBAAiB,QAAQ,sBAAsB,QAAQ,sBAAsB,QAAQ,oBAAoB,QAAQ,uBAAuB;AAC3O,YAAQ,uBAAuB;AAC/B,YAAQ,oBAAoB;AAC5B,YAAQ,sBAAsB;AAC9B,YAAQ,sBAAsB;AAC9B,YAAQ,iBAAiB;AACzB,YAAQ,6BAA6B;AACrC,YAAQ,0BAA0B,CAAC,KAAK,GAAG;AAC3C,YAAQ,YAAY;AAAA;AAAA;;;ACVpB;AAAA;AACA,WAAO,eAAe,SAAS,cAAc,EAAE,OAAO,KAAK,CAAC;AAC5D,YAAQ,cAAc;AACtB,QAAI,cAAc;AAClB,aAAS,6BAA6B,KAAK;AACvC,aAAO,YAAY,wBAAwB,QAAQ,IAAI,CAAC,CAAC,IAAI;AAAA,IACjE;AACA,aAAS,qBAAqB,KAAK;AAC/B,UAAI,kBAAkB,IAAI,QAAQ,YAAY,qBAAqB,EAAE;AACrE,aAAO,gBAAgB,QAAQ,YAAY,mBAAmB,SAAU,OAAO,KAAK;AAChF,eAAO,OAAO,aAAa,GAAG;AAAA,MAClC,CAAC;AAAA,IACL;AACA,aAAS,WAAW,KAAK;AACrB,aAAO,IAAI,SAAS,GAAG;AAAA,IAC3B;AACA,aAAS,UAAU,KAAK;AACpB,UAAI;AACA,eAAO,mBAAmB,GAAG;AAAA,MACjC,SACO,GAAG;AAIN,eAAO;AAAA,MACX;AAAA,IACJ;AACA,aAAS,YAAY,KAAK;AACtB,UAAI,CAAC,KAAK;AACN,eAAO,YAAY;AAAA,MACvB;AACA,UAAI;AACJ,UAAI,aAAa,UAAU,IAAI,KAAK,CAAC;AACrC,SAAG;AACC,qBAAa,qBAAqB,UAAU,EACvC,QAAQ,YAAY,qBAAqB,EAAE,EAC3C,QAAQ,YAAY,qBAAqB,EAAE,EAC3C,QAAQ,YAAY,4BAA4B,EAAE,EAClD,KAAK;AACV,qBAAa,UAAU,UAAU;AACjC,wBACI,WAAW,MAAM,YAAY,mBAAmB,KAC5C,WAAW,MAAM,YAAY,iBAAiB,KAC9C,WAAW,MAAM,YAAY,mBAAmB,KAChD,WAAW,MAAM,YAAY,0BAA0B;AAAA,MACnE,SAAS,iBAAiB,cAAc,SAAS;AACjD,UAAI,eAAe;AACnB,UAAI,CAAC,cAAc;AACf,eAAO,YAAY;AAAA,MACvB;AACA,UAAI,6BAA6B,YAAY,GAAG;AAC5C,eAAO;AAAA,MACX;AAEA,UAAI,aAAa,aAAa,UAAU;AACxC,UAAI,wBAAwB,WAAW,MAAM,YAAY,cAAc;AACvE,UAAI,CAAC,uBAAuB;AACxB,eAAO;AAAA,MACX;AACA,UAAI,YAAY,sBAAsB,CAAC,EAAE,YAAY,EAAE,KAAK;AAC5D,UAAI,YAAY,qBAAqB,KAAK,SAAS,GAAG;AAClD,eAAO,YAAY;AAAA,MACvB;AACA,UAAI,gBAAgB,WAAW,QAAQ,OAAO,GAAG;AAEjD,UAAI,cAAc,aAAa,UAAU,SAAS,KAAK,GAAG;AACtD,eAAO;AAAA,MACX;AAEA,UAAI,cAAc,WAAW,cAAc,UAAU;AACjD,YAAI,CAAC,WAAW,aAAa,GAAG;AAC5B,iBAAO,YAAY;AAAA,QACvB;AACA,YAAI,QAAQ,IAAI,IAAI,aAAa;AACjC,cAAM,WAAW,MAAM,SAAS,YAAY;AAC5C,cAAM,WAAW,MAAM,SAAS,YAAY;AAC5C,eAAO,MAAM,SAAS;AAAA,MAC1B;AACA,aAAO;AAAA,IACX;AACA,YAAQ,cAAc;AAAA;AAAA;",
6 | "names": []
7 | }
8 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/@theme_index.js:
--------------------------------------------------------------------------------
1 | import {
2 | useMediaQuery
3 | } from "./chunk-2AEJUKVR.js";
4 | import {
5 | computed,
6 | ref,
7 | shallowRef,
8 | watch
9 | } from "./chunk-U55NQ7RZ.js";
10 | import "./chunk-BUSYA2B4.js";
11 |
12 | // ../../../node_modules/vitepress/dist/client/theme-default/index.js
13 | import "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/styles/fonts.css";
14 |
15 | // ../../../node_modules/vitepress/dist/client/theme-default/without-fonts.js
16 | import "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/styles/vars.css";
17 | import "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/styles/base.css";
18 | import "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/styles/icons.css";
19 | import "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/styles/utils.css";
20 | import "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/styles/components/custom-block.css";
21 | import "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code.css";
22 | import "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code-group.css";
23 | import "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/styles/components/vp-doc.css";
24 | import "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/styles/components/vp-sponsor.css";
25 | import VPBadge from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue";
26 | import Layout from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/Layout.vue";
27 | import { default as default2 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue";
28 | import { default as default3 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPImage.vue";
29 | import { default as default4 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPButton.vue";
30 | import { default as default5 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPHomeContent.vue";
31 | import { default as default6 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPHomeHero.vue";
32 | import { default as default7 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPHomeFeatures.vue";
33 | import { default as default8 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPHomeSponsors.vue";
34 | import { default as default9 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPLink.vue";
35 | import { default as default10 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPDocAsideSponsors.vue";
36 | import { default as default11 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPSocialLink.vue";
37 | import { default as default12 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPSocialLinks.vue";
38 | import { default as default13 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPSponsors.vue";
39 | import { default as default14 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPTeamPage.vue";
40 | import { default as default15 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageTitle.vue";
41 | import { default as default16 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageSection.vue";
42 | import { default as default17 } from "/home/jh/Downloads/bilive/node_modules/vitepress/dist/client/theme-default/components/VPTeamMembers.vue";
43 |
44 | // ../../../node_modules/vitepress/dist/client/theme-default/support/utils.js
45 | import { withBase } from "vitepress";
46 |
47 | // ../../../node_modules/vitepress/dist/client/theme-default/composables/data.js
48 | import { useData as useData$ } from "vitepress";
49 | var useData = useData$;
50 |
51 | // ../../../node_modules/vitepress/dist/client/theme-default/support/utils.js
52 | function ensureStartingSlash(path) {
53 | return /^\//.test(path) ? path : `/${path}`;
54 | }
55 |
56 | // ../../../node_modules/vitepress/dist/client/theme-default/support/sidebar.js
57 | function getSidebar(_sidebar, path) {
58 | if (Array.isArray(_sidebar))
59 | return addBase(_sidebar);
60 | if (_sidebar == null)
61 | return [];
62 | path = ensureStartingSlash(path);
63 | const dir = Object.keys(_sidebar).sort((a, b) => {
64 | return b.split("/").length - a.split("/").length;
65 | }).find((dir2) => {
66 | return path.startsWith(ensureStartingSlash(dir2));
67 | });
68 | const sidebar = dir ? _sidebar[dir] : [];
69 | return Array.isArray(sidebar) ? addBase(sidebar) : addBase(sidebar.items, sidebar.base);
70 | }
71 | function getSidebarGroups(sidebar) {
72 | const groups = [];
73 | let lastGroupIndex = 0;
74 | for (const index in sidebar) {
75 | const item = sidebar[index];
76 | if (item.items) {
77 | lastGroupIndex = groups.push(item);
78 | continue;
79 | }
80 | if (!groups[lastGroupIndex]) {
81 | groups.push({ items: [] });
82 | }
83 | groups[lastGroupIndex].items.push(item);
84 | }
85 | return groups;
86 | }
87 | function addBase(items, _base) {
88 | return [...items].map((_item) => {
89 | const item = { ..._item };
90 | const base = item.base || _base;
91 | if (base && item.link)
92 | item.link = base + item.link;
93 | if (item.items)
94 | item.items = addBase(item.items, base);
95 | return item;
96 | });
97 | }
98 |
99 | // ../../../node_modules/vitepress/dist/client/theme-default/composables/sidebar.js
100 | function useSidebar() {
101 | const { frontmatter, page, theme: theme2 } = useData();
102 | const is960 = useMediaQuery("(min-width: 960px)");
103 | const isOpen = ref(false);
104 | const _sidebar = computed(() => {
105 | const sidebarConfig = theme2.value.sidebar;
106 | const relativePath = page.value.relativePath;
107 | return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : [];
108 | });
109 | const sidebar = ref(_sidebar.value);
110 | watch(_sidebar, (next, prev) => {
111 | if (JSON.stringify(next) !== JSON.stringify(prev))
112 | sidebar.value = _sidebar.value;
113 | });
114 | const hasSidebar = computed(() => {
115 | return frontmatter.value.sidebar !== false && sidebar.value.length > 0 && frontmatter.value.layout !== "home";
116 | });
117 | const leftAside = computed(() => {
118 | if (hasAside)
119 | return frontmatter.value.aside == null ? theme2.value.aside === "left" : frontmatter.value.aside === "left";
120 | return false;
121 | });
122 | const hasAside = computed(() => {
123 | if (frontmatter.value.layout === "home")
124 | return false;
125 | if (frontmatter.value.aside != null)
126 | return !!frontmatter.value.aside;
127 | return theme2.value.aside !== false;
128 | });
129 | const isSidebarEnabled = computed(() => hasSidebar.value && is960.value);
130 | const sidebarGroups = computed(() => {
131 | return hasSidebar.value ? getSidebarGroups(sidebar.value) : [];
132 | });
133 | function open() {
134 | isOpen.value = true;
135 | }
136 | function close() {
137 | isOpen.value = false;
138 | }
139 | function toggle() {
140 | isOpen.value ? close() : open();
141 | }
142 | return {
143 | isOpen,
144 | sidebar,
145 | sidebarGroups,
146 | hasSidebar,
147 | hasAside,
148 | leftAside,
149 | isSidebarEnabled,
150 | open,
151 | close,
152 | toggle
153 | };
154 | }
155 |
156 | // ../../../node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
157 | import { onContentUpdated } from "vitepress";
158 |
159 | // ../../../node_modules/vitepress/dist/client/theme-default/composables/outline.js
160 | import { getScrollOffset } from "vitepress";
161 | var resolvedHeaders = [];
162 | function getHeaders(range) {
163 | const headers = [
164 | ...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")
165 | ].filter((el) => el.id && el.hasChildNodes()).map((el) => {
166 | const level = Number(el.tagName[1]);
167 | return {
168 | element: el,
169 | title: serializeHeader(el),
170 | link: "#" + el.id,
171 | level
172 | };
173 | });
174 | return resolveHeaders(headers, range);
175 | }
176 | function serializeHeader(h) {
177 | let ret = "";
178 | for (const node of h.childNodes) {
179 | if (node.nodeType === 1) {
180 | if (node.classList.contains("VPBadge") || node.classList.contains("header-anchor") || node.classList.contains("ignore-header")) {
181 | continue;
182 | }
183 | ret += node.textContent;
184 | } else if (node.nodeType === 3) {
185 | ret += node.textContent;
186 | }
187 | }
188 | return ret.trim();
189 | }
190 | function resolveHeaders(headers, range) {
191 | if (range === false) {
192 | return [];
193 | }
194 | const levelsRange = (typeof range === "object" && !Array.isArray(range) ? range.level : range) || 2;
195 | const [high, low] = typeof levelsRange === "number" ? [levelsRange, levelsRange] : levelsRange === "deep" ? [2, 6] : levelsRange;
196 | return buildTree(headers, high, low);
197 | }
198 | function buildTree(data, min, max) {
199 | resolvedHeaders.length = 0;
200 | const result = [];
201 | const stack = [];
202 | data.forEach((item) => {
203 | const node = { ...item, children: [] };
204 | let parent = stack[stack.length - 1];
205 | while (parent && parent.level >= node.level) {
206 | stack.pop();
207 | parent = stack[stack.length - 1];
208 | }
209 | if (node.element.classList.contains("ignore-header") || parent && "shouldIgnore" in parent) {
210 | stack.push({ level: node.level, shouldIgnore: true });
211 | return;
212 | }
213 | if (node.level > max || node.level < min)
214 | return;
215 | resolvedHeaders.push({ element: node.element, link: node.link });
216 | if (parent)
217 | parent.children.push(node);
218 | else
219 | result.push(node);
220 | stack.push(node);
221 | });
222 | return result;
223 | }
224 |
225 | // ../../../node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
226 | function useLocalNav() {
227 | const { theme: theme2, frontmatter } = useData();
228 | const headers = shallowRef([]);
229 | const hasLocalNav = computed(() => {
230 | return headers.value.length > 0;
231 | });
232 | onContentUpdated(() => {
233 | headers.value = getHeaders(frontmatter.value.outline ?? theme2.value.outline);
234 | });
235 | return {
236 | headers,
237 | hasLocalNav
238 | };
239 | }
240 |
241 | // ../../../node_modules/vitepress/dist/client/theme-default/without-fonts.js
242 | var theme = {
243 | Layout,
244 | enhanceApp: ({ app }) => {
245 | app.component("Badge", VPBadge);
246 | }
247 | };
248 | var without_fonts_default = theme;
249 | export {
250 | default2 as VPBadge,
251 | default4 as VPButton,
252 | default10 as VPDocAsideSponsors,
253 | default5 as VPHomeContent,
254 | default7 as VPHomeFeatures,
255 | default6 as VPHomeHero,
256 | default8 as VPHomeSponsors,
257 | default3 as VPImage,
258 | default9 as VPLink,
259 | default11 as VPSocialLink,
260 | default12 as VPSocialLinks,
261 | default13 as VPSponsors,
262 | default17 as VPTeamMembers,
263 | default14 as VPTeamPage,
264 | default16 as VPTeamPageSection,
265 | default15 as VPTeamPageTitle,
266 | without_fonts_default as default,
267 | useLocalNav,
268 | useSidebar
269 | };
270 | //# sourceMappingURL=@theme_index.js.map
271 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/_metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "hash": "c588bacb",
3 | "configHash": "ab437a89",
4 | "lockfileHash": "246a70ff",
5 | "browserHash": "ca32368a",
6 | "optimized": {
7 | "vue": {
8 | "src": "../../../../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
9 | "file": "vue.js",
10 | "fileHash": "68a3a53a",
11 | "needsInterop": false
12 | },
13 | "vitepress > @vue/devtools-api": {
14 | "src": "../../../../../../../node_modules/@vue/devtools-api/dist/index.js",
15 | "file": "vitepress___@vue_devtools-api.js",
16 | "fileHash": "6e3c2db1",
17 | "needsInterop": false
18 | },
19 | "vitepress > @vueuse/core": {
20 | "src": "../../../../../../../node_modules/@vueuse/core/index.mjs",
21 | "file": "vitepress___@vueuse_core.js",
22 | "fileHash": "addab1f7",
23 | "needsInterop": false
24 | },
25 | "@braintree/sanitize-url": {
26 | "src": "../../../../../../../node_modules/@braintree/sanitize-url/dist/index.js",
27 | "file": "@braintree_sanitize-url.js",
28 | "fileHash": "7e0b10ff",
29 | "needsInterop": true
30 | },
31 | "dayjs": {
32 | "src": "../../../../../../../node_modules/dayjs/dayjs.min.js",
33 | "file": "dayjs.js",
34 | "fileHash": "019c6ae3",
35 | "needsInterop": true
36 | },
37 | "debug": {
38 | "src": "../../../../../../../node_modules/debug/src/browser.js",
39 | "file": "debug.js",
40 | "fileHash": "e5da3a4d",
41 | "needsInterop": true
42 | },
43 | "cytoscape-cose-bilkent": {
44 | "src": "../../../../../../../node_modules/cytoscape-cose-bilkent/cytoscape-cose-bilkent.js",
45 | "file": "cytoscape-cose-bilkent.js",
46 | "fileHash": "ebdc4d3a",
47 | "needsInterop": true
48 | },
49 | "cytoscape": {
50 | "src": "../../../../../../../node_modules/cytoscape/dist/cytoscape.esm.mjs",
51 | "file": "cytoscape.js",
52 | "fileHash": "8f892c6f",
53 | "needsInterop": false
54 | },
55 | "@theme/index": {
56 | "src": "../../../../../../../node_modules/vitepress/dist/client/theme-default/index.js",
57 | "file": "@theme_index.js",
58 | "fileHash": "899fe8c9",
59 | "needsInterop": false
60 | }
61 | },
62 | "chunks": {
63 | "chunk-2AEJUKVR": {
64 | "file": "chunk-2AEJUKVR.js"
65 | },
66 | "chunk-U55NQ7RZ": {
67 | "file": "chunk-U55NQ7RZ.js"
68 | },
69 | "chunk-BUSYA2B4": {
70 | "file": "chunk-BUSYA2B4.js"
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js:
--------------------------------------------------------------------------------
1 | var __getOwnPropNames = Object.getOwnPropertyNames;
2 | var __commonJS = (cb, mod) => function __require() {
3 | return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
4 | };
5 |
6 | export {
7 | __commonJS
8 | };
9 | //# sourceMappingURL=chunk-BUSYA2B4.js.map
10 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "sources": [],
4 | "sourcesContent": [],
5 | "mappings": "",
6 | "names": []
7 | }
8 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/dayjs.js:
--------------------------------------------------------------------------------
1 | import {
2 | __commonJS
3 | } from "./chunk-BUSYA2B4.js";
4 |
5 | // ../../../node_modules/dayjs/dayjs.min.js
6 | var require_dayjs_min = __commonJS({
7 | "../../../node_modules/dayjs/dayjs.min.js"(exports, module) {
8 | !function(t, e) {
9 | "object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define(e) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs = e();
10 | }(exports, function() {
11 | "use strict";
12 | var t = 1e3, e = 6e4, n = 36e5, r = "millisecond", i = "second", s = "minute", u = "hour", a = "day", o = "week", c = "month", f = "quarter", h = "year", d = "date", l = "Invalid Date", $ = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/, y = /\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g, M = { name: "en", weekdays: "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), months: "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), ordinal: function(t2) {
13 | var e2 = ["th", "st", "nd", "rd"], n2 = t2 % 100;
14 | return "[" + t2 + (e2[(n2 - 20) % 10] || e2[n2] || e2[0]) + "]";
15 | } }, m = function(t2, e2, n2) {
16 | var r2 = String(t2);
17 | return !r2 || r2.length >= e2 ? t2 : "" + Array(e2 + 1 - r2.length).join(n2) + t2;
18 | }, v = { s: m, z: function(t2) {
19 | var e2 = -t2.utcOffset(), n2 = Math.abs(e2), r2 = Math.floor(n2 / 60), i2 = n2 % 60;
20 | return (e2 <= 0 ? "+" : "-") + m(r2, 2, "0") + ":" + m(i2, 2, "0");
21 | }, m: function t2(e2, n2) {
22 | if (e2.date() < n2.date()) return -t2(n2, e2);
23 | var r2 = 12 * (n2.year() - e2.year()) + (n2.month() - e2.month()), i2 = e2.clone().add(r2, c), s2 = n2 - i2 < 0, u2 = e2.clone().add(r2 + (s2 ? -1 : 1), c);
24 | return +(-(r2 + (n2 - i2) / (s2 ? i2 - u2 : u2 - i2)) || 0);
25 | }, a: function(t2) {
26 | return t2 < 0 ? Math.ceil(t2) || 0 : Math.floor(t2);
27 | }, p: function(t2) {
28 | return { M: c, y: h, w: o, d: a, D: d, h: u, m: s, s: i, ms: r, Q: f }[t2] || String(t2 || "").toLowerCase().replace(/s$/, "");
29 | }, u: function(t2) {
30 | return void 0 === t2;
31 | } }, g = "en", D = {};
32 | D[g] = M;
33 | var p = "$isDayjsObject", S = function(t2) {
34 | return t2 instanceof _ || !(!t2 || !t2[p]);
35 | }, w = function t2(e2, n2, r2) {
36 | var i2;
37 | if (!e2) return g;
38 | if ("string" == typeof e2) {
39 | var s2 = e2.toLowerCase();
40 | D[s2] && (i2 = s2), n2 && (D[s2] = n2, i2 = s2);
41 | var u2 = e2.split("-");
42 | if (!i2 && u2.length > 1) return t2(u2[0]);
43 | } else {
44 | var a2 = e2.name;
45 | D[a2] = e2, i2 = a2;
46 | }
47 | return !r2 && i2 && (g = i2), i2 || !r2 && g;
48 | }, O = function(t2, e2) {
49 | if (S(t2)) return t2.clone();
50 | var n2 = "object" == typeof e2 ? e2 : {};
51 | return n2.date = t2, n2.args = arguments, new _(n2);
52 | }, b = v;
53 | b.l = w, b.i = S, b.w = function(t2, e2) {
54 | return O(t2, { locale: e2.$L, utc: e2.$u, x: e2.$x, $offset: e2.$offset });
55 | };
56 | var _ = function() {
57 | function M2(t2) {
58 | this.$L = w(t2.locale, null, true), this.parse(t2), this.$x = this.$x || t2.x || {}, this[p] = true;
59 | }
60 | var m2 = M2.prototype;
61 | return m2.parse = function(t2) {
62 | this.$d = function(t3) {
63 | var e2 = t3.date, n2 = t3.utc;
64 | if (null === e2) return /* @__PURE__ */ new Date(NaN);
65 | if (b.u(e2)) return /* @__PURE__ */ new Date();
66 | if (e2 instanceof Date) return new Date(e2);
67 | if ("string" == typeof e2 && !/Z$/i.test(e2)) {
68 | var r2 = e2.match($);
69 | if (r2) {
70 | var i2 = r2[2] - 1 || 0, s2 = (r2[7] || "0").substring(0, 3);
71 | return n2 ? new Date(Date.UTC(r2[1], i2, r2[3] || 1, r2[4] || 0, r2[5] || 0, r2[6] || 0, s2)) : new Date(r2[1], i2, r2[3] || 1, r2[4] || 0, r2[5] || 0, r2[6] || 0, s2);
72 | }
73 | }
74 | return new Date(e2);
75 | }(t2), this.init();
76 | }, m2.init = function() {
77 | var t2 = this.$d;
78 | this.$y = t2.getFullYear(), this.$M = t2.getMonth(), this.$D = t2.getDate(), this.$W = t2.getDay(), this.$H = t2.getHours(), this.$m = t2.getMinutes(), this.$s = t2.getSeconds(), this.$ms = t2.getMilliseconds();
79 | }, m2.$utils = function() {
80 | return b;
81 | }, m2.isValid = function() {
82 | return !(this.$d.toString() === l);
83 | }, m2.isSame = function(t2, e2) {
84 | var n2 = O(t2);
85 | return this.startOf(e2) <= n2 && n2 <= this.endOf(e2);
86 | }, m2.isAfter = function(t2, e2) {
87 | return O(t2) < this.startOf(e2);
88 | }, m2.isBefore = function(t2, e2) {
89 | return this.endOf(e2) < O(t2);
90 | }, m2.$g = function(t2, e2, n2) {
91 | return b.u(t2) ? this[e2] : this.set(n2, t2);
92 | }, m2.unix = function() {
93 | return Math.floor(this.valueOf() / 1e3);
94 | }, m2.valueOf = function() {
95 | return this.$d.getTime();
96 | }, m2.startOf = function(t2, e2) {
97 | var n2 = this, r2 = !!b.u(e2) || e2, f2 = b.p(t2), l2 = function(t3, e3) {
98 | var i2 = b.w(n2.$u ? Date.UTC(n2.$y, e3, t3) : new Date(n2.$y, e3, t3), n2);
99 | return r2 ? i2 : i2.endOf(a);
100 | }, $2 = function(t3, e3) {
101 | return b.w(n2.toDate()[t3].apply(n2.toDate("s"), (r2 ? [0, 0, 0, 0] : [23, 59, 59, 999]).slice(e3)), n2);
102 | }, y2 = this.$W, M3 = this.$M, m3 = this.$D, v2 = "set" + (this.$u ? "UTC" : "");
103 | switch (f2) {
104 | case h:
105 | return r2 ? l2(1, 0) : l2(31, 11);
106 | case c:
107 | return r2 ? l2(1, M3) : l2(0, M3 + 1);
108 | case o:
109 | var g2 = this.$locale().weekStart || 0, D2 = (y2 < g2 ? y2 + 7 : y2) - g2;
110 | return l2(r2 ? m3 - D2 : m3 + (6 - D2), M3);
111 | case a:
112 | case d:
113 | return $2(v2 + "Hours", 0);
114 | case u:
115 | return $2(v2 + "Minutes", 1);
116 | case s:
117 | return $2(v2 + "Seconds", 2);
118 | case i:
119 | return $2(v2 + "Milliseconds", 3);
120 | default:
121 | return this.clone();
122 | }
123 | }, m2.endOf = function(t2) {
124 | return this.startOf(t2, false);
125 | }, m2.$set = function(t2, e2) {
126 | var n2, o2 = b.p(t2), f2 = "set" + (this.$u ? "UTC" : ""), l2 = (n2 = {}, n2[a] = f2 + "Date", n2[d] = f2 + "Date", n2[c] = f2 + "Month", n2[h] = f2 + "FullYear", n2[u] = f2 + "Hours", n2[s] = f2 + "Minutes", n2[i] = f2 + "Seconds", n2[r] = f2 + "Milliseconds", n2)[o2], $2 = o2 === a ? this.$D + (e2 - this.$W) : e2;
127 | if (o2 === c || o2 === h) {
128 | var y2 = this.clone().set(d, 1);
129 | y2.$d[l2]($2), y2.init(), this.$d = y2.set(d, Math.min(this.$D, y2.daysInMonth())).$d;
130 | } else l2 && this.$d[l2]($2);
131 | return this.init(), this;
132 | }, m2.set = function(t2, e2) {
133 | return this.clone().$set(t2, e2);
134 | }, m2.get = function(t2) {
135 | return this[b.p(t2)]();
136 | }, m2.add = function(r2, f2) {
137 | var d2, l2 = this;
138 | r2 = Number(r2);
139 | var $2 = b.p(f2), y2 = function(t2) {
140 | var e2 = O(l2);
141 | return b.w(e2.date(e2.date() + Math.round(t2 * r2)), l2);
142 | };
143 | if ($2 === c) return this.set(c, this.$M + r2);
144 | if ($2 === h) return this.set(h, this.$y + r2);
145 | if ($2 === a) return y2(1);
146 | if ($2 === o) return y2(7);
147 | var M3 = (d2 = {}, d2[s] = e, d2[u] = n, d2[i] = t, d2)[$2] || 1, m3 = this.$d.getTime() + r2 * M3;
148 | return b.w(m3, this);
149 | }, m2.subtract = function(t2, e2) {
150 | return this.add(-1 * t2, e2);
151 | }, m2.format = function(t2) {
152 | var e2 = this, n2 = this.$locale();
153 | if (!this.isValid()) return n2.invalidDate || l;
154 | var r2 = t2 || "YYYY-MM-DDTHH:mm:ssZ", i2 = b.z(this), s2 = this.$H, u2 = this.$m, a2 = this.$M, o2 = n2.weekdays, c2 = n2.months, f2 = n2.meridiem, h2 = function(t3, n3, i3, s3) {
155 | return t3 && (t3[n3] || t3(e2, r2)) || i3[n3].slice(0, s3);
156 | }, d2 = function(t3) {
157 | return b.s(s2 % 12 || 12, t3, "0");
158 | }, $2 = f2 || function(t3, e3, n3) {
159 | var r3 = t3 < 12 ? "AM" : "PM";
160 | return n3 ? r3.toLowerCase() : r3;
161 | };
162 | return r2.replace(y, function(t3, r3) {
163 | return r3 || function(t4) {
164 | switch (t4) {
165 | case "YY":
166 | return String(e2.$y).slice(-2);
167 | case "YYYY":
168 | return b.s(e2.$y, 4, "0");
169 | case "M":
170 | return a2 + 1;
171 | case "MM":
172 | return b.s(a2 + 1, 2, "0");
173 | case "MMM":
174 | return h2(n2.monthsShort, a2, c2, 3);
175 | case "MMMM":
176 | return h2(c2, a2);
177 | case "D":
178 | return e2.$D;
179 | case "DD":
180 | return b.s(e2.$D, 2, "0");
181 | case "d":
182 | return String(e2.$W);
183 | case "dd":
184 | return h2(n2.weekdaysMin, e2.$W, o2, 2);
185 | case "ddd":
186 | return h2(n2.weekdaysShort, e2.$W, o2, 3);
187 | case "dddd":
188 | return o2[e2.$W];
189 | case "H":
190 | return String(s2);
191 | case "HH":
192 | return b.s(s2, 2, "0");
193 | case "h":
194 | return d2(1);
195 | case "hh":
196 | return d2(2);
197 | case "a":
198 | return $2(s2, u2, true);
199 | case "A":
200 | return $2(s2, u2, false);
201 | case "m":
202 | return String(u2);
203 | case "mm":
204 | return b.s(u2, 2, "0");
205 | case "s":
206 | return String(e2.$s);
207 | case "ss":
208 | return b.s(e2.$s, 2, "0");
209 | case "SSS":
210 | return b.s(e2.$ms, 3, "0");
211 | case "Z":
212 | return i2;
213 | }
214 | return null;
215 | }(t3) || i2.replace(":", "");
216 | });
217 | }, m2.utcOffset = function() {
218 | return 15 * -Math.round(this.$d.getTimezoneOffset() / 15);
219 | }, m2.diff = function(r2, d2, l2) {
220 | var $2, y2 = this, M3 = b.p(d2), m3 = O(r2), v2 = (m3.utcOffset() - this.utcOffset()) * e, g2 = this - m3, D2 = function() {
221 | return b.m(y2, m3);
222 | };
223 | switch (M3) {
224 | case h:
225 | $2 = D2() / 12;
226 | break;
227 | case c:
228 | $2 = D2();
229 | break;
230 | case f:
231 | $2 = D2() / 3;
232 | break;
233 | case o:
234 | $2 = (g2 - v2) / 6048e5;
235 | break;
236 | case a:
237 | $2 = (g2 - v2) / 864e5;
238 | break;
239 | case u:
240 | $2 = g2 / n;
241 | break;
242 | case s:
243 | $2 = g2 / e;
244 | break;
245 | case i:
246 | $2 = g2 / t;
247 | break;
248 | default:
249 | $2 = g2;
250 | }
251 | return l2 ? $2 : b.a($2);
252 | }, m2.daysInMonth = function() {
253 | return this.endOf(c).$D;
254 | }, m2.$locale = function() {
255 | return D[this.$L];
256 | }, m2.locale = function(t2, e2) {
257 | if (!t2) return this.$L;
258 | var n2 = this.clone(), r2 = w(t2, e2, true);
259 | return r2 && (n2.$L = r2), n2;
260 | }, m2.clone = function() {
261 | return b.w(this.$d, this);
262 | }, m2.toDate = function() {
263 | return new Date(this.valueOf());
264 | }, m2.toJSON = function() {
265 | return this.isValid() ? this.toISOString() : null;
266 | }, m2.toISOString = function() {
267 | return this.$d.toISOString();
268 | }, m2.toString = function() {
269 | return this.$d.toUTCString();
270 | }, M2;
271 | }(), k = _.prototype;
272 | return O.prototype = k, [["$ms", r], ["$s", i], ["$m", s], ["$H", u], ["$W", a], ["$M", c], ["$y", h], ["$D", d]].forEach(function(t2) {
273 | k[t2[1]] = function(e2) {
274 | return this.$g(e2, t2[0], t2[1]);
275 | };
276 | }), O.extend = function(t2, e2) {
277 | return t2.$i || (t2(e2, _, O), t2.$i = true), O;
278 | }, O.locale = w, O.isDayjs = S, O.unix = function(t2) {
279 | return O(1e3 * t2);
280 | }, O.en = D[g], O.Ls = D, O.p = {}, O;
281 | });
282 | }
283 | });
284 | export default require_dayjs_min();
285 | //# sourceMappingURL=dayjs.js.map
286 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/dayjs.js.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "sources": ["../../../../../../../node_modules/dayjs/dayjs.min.js"],
4 | "sourcesContent": ["!function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define(e):(t=\"undefined\"!=typeof globalThis?globalThis:t||self).dayjs=e()}(this,(function(){\"use strict\";var t=1e3,e=6e4,n=36e5,r=\"millisecond\",i=\"second\",s=\"minute\",u=\"hour\",a=\"day\",o=\"week\",c=\"month\",f=\"quarter\",h=\"year\",d=\"date\",l=\"Invalid Date\",$=/^(\\d{4})[-/]?(\\d{1,2})?[-/]?(\\d{0,2})[Tt\\s]*(\\d{1,2})?:?(\\d{1,2})?:?(\\d{1,2})?[.:]?(\\d+)?$/,y=/\\[([^\\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,M={name:\"en\",weekdays:\"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday\".split(\"_\"),months:\"January_February_March_April_May_June_July_August_September_October_November_December\".split(\"_\"),ordinal:function(t){var e=[\"th\",\"st\",\"nd\",\"rd\"],n=t%100;return\"[\"+t+(e[(n-20)%10]||e[n]||e[0])+\"]\"}},m=function(t,e,n){var r=String(t);return!r||r.length>=e?t:\"\"+Array(e+1-r.length).join(n)+t},v={s:m,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?\"+\":\"-\")+m(r,2,\"0\")+\":\"+m(i,2,\"0\")},m:function t(e,n){if(e.date()1)return t(u[0])}else{var a=e.name;D[a]=e,i=a}return!r&&i&&(g=i),i||!r&&g},O=function(t,e){if(S(t))return t.clone();var n=\"object\"==typeof e?e:{};return n.date=t,n.args=arguments,new _(n)},b=v;b.l=w,b.i=S,b.w=function(t,e){return O(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var _=function(){function M(t){this.$L=w(t.locale,null,!0),this.parse(t),this.$x=this.$x||t.x||{},this[p]=!0}var m=M.prototype;return m.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(b.u(e))return new Date;if(e instanceof Date)return new Date(e);if(\"string\"==typeof e&&!/Z$/i.test(e)){var r=e.match($);if(r){var i=r[2]-1||0,s=(r[7]||\"0\").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.init()},m.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},m.$utils=function(){return b},m.isValid=function(){return!(this.$d.toString()===l)},m.isSame=function(t,e){var n=O(t);return this.startOf(e)<=n&&n<=this.endOf(e)},m.isAfter=function(t,e){return O(t) 0) {
18 | return parse(val);
19 | } else if (type === "number" && isFinite(val)) {
20 | return options.long ? fmtLong(val) : fmtShort(val);
21 | }
22 | throw new Error(
23 | "val is not a non-empty string or a valid number. val=" + JSON.stringify(val)
24 | );
25 | };
26 | function parse(str) {
27 | str = String(str);
28 | if (str.length > 100) {
29 | return;
30 | }
31 | var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
32 | str
33 | );
34 | if (!match) {
35 | return;
36 | }
37 | var n = parseFloat(match[1]);
38 | var type = (match[2] || "ms").toLowerCase();
39 | switch (type) {
40 | case "years":
41 | case "year":
42 | case "yrs":
43 | case "yr":
44 | case "y":
45 | return n * y;
46 | case "weeks":
47 | case "week":
48 | case "w":
49 | return n * w;
50 | case "days":
51 | case "day":
52 | case "d":
53 | return n * d;
54 | case "hours":
55 | case "hour":
56 | case "hrs":
57 | case "hr":
58 | case "h":
59 | return n * h;
60 | case "minutes":
61 | case "minute":
62 | case "mins":
63 | case "min":
64 | case "m":
65 | return n * m;
66 | case "seconds":
67 | case "second":
68 | case "secs":
69 | case "sec":
70 | case "s":
71 | return n * s;
72 | case "milliseconds":
73 | case "millisecond":
74 | case "msecs":
75 | case "msec":
76 | case "ms":
77 | return n;
78 | default:
79 | return void 0;
80 | }
81 | }
82 | function fmtShort(ms) {
83 | var msAbs = Math.abs(ms);
84 | if (msAbs >= d) {
85 | return Math.round(ms / d) + "d";
86 | }
87 | if (msAbs >= h) {
88 | return Math.round(ms / h) + "h";
89 | }
90 | if (msAbs >= m) {
91 | return Math.round(ms / m) + "m";
92 | }
93 | if (msAbs >= s) {
94 | return Math.round(ms / s) + "s";
95 | }
96 | return ms + "ms";
97 | }
98 | function fmtLong(ms) {
99 | var msAbs = Math.abs(ms);
100 | if (msAbs >= d) {
101 | return plural(ms, msAbs, d, "day");
102 | }
103 | if (msAbs >= h) {
104 | return plural(ms, msAbs, h, "hour");
105 | }
106 | if (msAbs >= m) {
107 | return plural(ms, msAbs, m, "minute");
108 | }
109 | if (msAbs >= s) {
110 | return plural(ms, msAbs, s, "second");
111 | }
112 | return ms + " ms";
113 | }
114 | function plural(ms, msAbs, n, name) {
115 | var isPlural = msAbs >= n * 1.5;
116 | return Math.round(ms / n) + " " + name + (isPlural ? "s" : "");
117 | }
118 | }
119 | });
120 |
121 | // ../../../node_modules/debug/src/common.js
122 | var require_common = __commonJS({
123 | "../../../node_modules/debug/src/common.js"(exports, module) {
124 | function setup(env) {
125 | createDebug.debug = createDebug;
126 | createDebug.default = createDebug;
127 | createDebug.coerce = coerce;
128 | createDebug.disable = disable;
129 | createDebug.enable = enable;
130 | createDebug.enabled = enabled;
131 | createDebug.humanize = require_ms();
132 | createDebug.destroy = destroy;
133 | Object.keys(env).forEach((key) => {
134 | createDebug[key] = env[key];
135 | });
136 | createDebug.names = [];
137 | createDebug.skips = [];
138 | createDebug.formatters = {};
139 | function selectColor(namespace) {
140 | let hash = 0;
141 | for (let i = 0; i < namespace.length; i++) {
142 | hash = (hash << 5) - hash + namespace.charCodeAt(i);
143 | hash |= 0;
144 | }
145 | return createDebug.colors[Math.abs(hash) % createDebug.colors.length];
146 | }
147 | createDebug.selectColor = selectColor;
148 | function createDebug(namespace) {
149 | let prevTime;
150 | let enableOverride = null;
151 | let namespacesCache;
152 | let enabledCache;
153 | function debug(...args) {
154 | if (!debug.enabled) {
155 | return;
156 | }
157 | const self = debug;
158 | const curr = Number(/* @__PURE__ */ new Date());
159 | const ms = curr - (prevTime || curr);
160 | self.diff = ms;
161 | self.prev = prevTime;
162 | self.curr = curr;
163 | prevTime = curr;
164 | args[0] = createDebug.coerce(args[0]);
165 | if (typeof args[0] !== "string") {
166 | args.unshift("%O");
167 | }
168 | let index = 0;
169 | args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => {
170 | if (match === "%%") {
171 | return "%";
172 | }
173 | index++;
174 | const formatter = createDebug.formatters[format];
175 | if (typeof formatter === "function") {
176 | const val = args[index];
177 | match = formatter.call(self, val);
178 | args.splice(index, 1);
179 | index--;
180 | }
181 | return match;
182 | });
183 | createDebug.formatArgs.call(self, args);
184 | const logFn = self.log || createDebug.log;
185 | logFn.apply(self, args);
186 | }
187 | debug.namespace = namespace;
188 | debug.useColors = createDebug.useColors();
189 | debug.color = createDebug.selectColor(namespace);
190 | debug.extend = extend;
191 | debug.destroy = createDebug.destroy;
192 | Object.defineProperty(debug, "enabled", {
193 | enumerable: true,
194 | configurable: false,
195 | get: () => {
196 | if (enableOverride !== null) {
197 | return enableOverride;
198 | }
199 | if (namespacesCache !== createDebug.namespaces) {
200 | namespacesCache = createDebug.namespaces;
201 | enabledCache = createDebug.enabled(namespace);
202 | }
203 | return enabledCache;
204 | },
205 | set: (v) => {
206 | enableOverride = v;
207 | }
208 | });
209 | if (typeof createDebug.init === "function") {
210 | createDebug.init(debug);
211 | }
212 | return debug;
213 | }
214 | function extend(namespace, delimiter) {
215 | const newDebug = createDebug(this.namespace + (typeof delimiter === "undefined" ? ":" : delimiter) + namespace);
216 | newDebug.log = this.log;
217 | return newDebug;
218 | }
219 | function enable(namespaces) {
220 | createDebug.save(namespaces);
221 | createDebug.namespaces = namespaces;
222 | createDebug.names = [];
223 | createDebug.skips = [];
224 | const split = (typeof namespaces === "string" ? namespaces : "").trim().replace(" ", ",").split(",").filter(Boolean);
225 | for (const ns of split) {
226 | if (ns[0] === "-") {
227 | createDebug.skips.push(ns.slice(1));
228 | } else {
229 | createDebug.names.push(ns);
230 | }
231 | }
232 | }
233 | function matchesTemplate(search, template) {
234 | let searchIndex = 0;
235 | let templateIndex = 0;
236 | let starIndex = -1;
237 | let matchIndex = 0;
238 | while (searchIndex < search.length) {
239 | if (templateIndex < template.length && (template[templateIndex] === search[searchIndex] || template[templateIndex] === "*")) {
240 | if (template[templateIndex] === "*") {
241 | starIndex = templateIndex;
242 | matchIndex = searchIndex;
243 | templateIndex++;
244 | } else {
245 | searchIndex++;
246 | templateIndex++;
247 | }
248 | } else if (starIndex !== -1) {
249 | templateIndex = starIndex + 1;
250 | matchIndex++;
251 | searchIndex = matchIndex;
252 | } else {
253 | return false;
254 | }
255 | }
256 | while (templateIndex < template.length && template[templateIndex] === "*") {
257 | templateIndex++;
258 | }
259 | return templateIndex === template.length;
260 | }
261 | function disable() {
262 | const namespaces = [
263 | ...createDebug.names,
264 | ...createDebug.skips.map((namespace) => "-" + namespace)
265 | ].join(",");
266 | createDebug.enable("");
267 | return namespaces;
268 | }
269 | function enabled(name) {
270 | for (const skip of createDebug.skips) {
271 | if (matchesTemplate(name, skip)) {
272 | return false;
273 | }
274 | }
275 | for (const ns of createDebug.names) {
276 | if (matchesTemplate(name, ns)) {
277 | return true;
278 | }
279 | }
280 | return false;
281 | }
282 | function coerce(val) {
283 | if (val instanceof Error) {
284 | return val.stack || val.message;
285 | }
286 | return val;
287 | }
288 | function destroy() {
289 | console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.");
290 | }
291 | createDebug.enable(createDebug.load());
292 | return createDebug;
293 | }
294 | module.exports = setup;
295 | }
296 | });
297 |
298 | // ../../../node_modules/debug/src/browser.js
299 | var require_browser = __commonJS({
300 | "../../../node_modules/debug/src/browser.js"(exports, module) {
301 | exports.formatArgs = formatArgs;
302 | exports.save = save;
303 | exports.load = load;
304 | exports.useColors = useColors;
305 | exports.storage = localstorage();
306 | exports.destroy = /* @__PURE__ */ (() => {
307 | let warned = false;
308 | return () => {
309 | if (!warned) {
310 | warned = true;
311 | console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.");
312 | }
313 | };
314 | })();
315 | exports.colors = [
316 | "#0000CC",
317 | "#0000FF",
318 | "#0033CC",
319 | "#0033FF",
320 | "#0066CC",
321 | "#0066FF",
322 | "#0099CC",
323 | "#0099FF",
324 | "#00CC00",
325 | "#00CC33",
326 | "#00CC66",
327 | "#00CC99",
328 | "#00CCCC",
329 | "#00CCFF",
330 | "#3300CC",
331 | "#3300FF",
332 | "#3333CC",
333 | "#3333FF",
334 | "#3366CC",
335 | "#3366FF",
336 | "#3399CC",
337 | "#3399FF",
338 | "#33CC00",
339 | "#33CC33",
340 | "#33CC66",
341 | "#33CC99",
342 | "#33CCCC",
343 | "#33CCFF",
344 | "#6600CC",
345 | "#6600FF",
346 | "#6633CC",
347 | "#6633FF",
348 | "#66CC00",
349 | "#66CC33",
350 | "#9900CC",
351 | "#9900FF",
352 | "#9933CC",
353 | "#9933FF",
354 | "#99CC00",
355 | "#99CC33",
356 | "#CC0000",
357 | "#CC0033",
358 | "#CC0066",
359 | "#CC0099",
360 | "#CC00CC",
361 | "#CC00FF",
362 | "#CC3300",
363 | "#CC3333",
364 | "#CC3366",
365 | "#CC3399",
366 | "#CC33CC",
367 | "#CC33FF",
368 | "#CC6600",
369 | "#CC6633",
370 | "#CC9900",
371 | "#CC9933",
372 | "#CCCC00",
373 | "#CCCC33",
374 | "#FF0000",
375 | "#FF0033",
376 | "#FF0066",
377 | "#FF0099",
378 | "#FF00CC",
379 | "#FF00FF",
380 | "#FF3300",
381 | "#FF3333",
382 | "#FF3366",
383 | "#FF3399",
384 | "#FF33CC",
385 | "#FF33FF",
386 | "#FF6600",
387 | "#FF6633",
388 | "#FF9900",
389 | "#FF9933",
390 | "#FFCC00",
391 | "#FFCC33"
392 | ];
393 | function useColors() {
394 | if (typeof window !== "undefined" && window.process && (window.process.type === "renderer" || window.process.__nwjs)) {
395 | return true;
396 | }
397 | if (typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) {
398 | return false;
399 | }
400 | let m;
401 | return typeof document !== "undefined" && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance || // Is firebug? http://stackoverflow.com/a/398120/376773
402 | typeof window !== "undefined" && window.console && (window.console.firebug || window.console.exception && window.console.table) || // Is firefox >= v31?
403 | // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages
404 | typeof navigator !== "undefined" && navigator.userAgent && (m = navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)) && parseInt(m[1], 10) >= 31 || // Double check webkit in userAgent just in case we are in a worker
405 | typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/);
406 | }
407 | function formatArgs(args) {
408 | args[0] = (this.useColors ? "%c" : "") + this.namespace + (this.useColors ? " %c" : " ") + args[0] + (this.useColors ? "%c " : " ") + "+" + module.exports.humanize(this.diff);
409 | if (!this.useColors) {
410 | return;
411 | }
412 | const c = "color: " + this.color;
413 | args.splice(1, 0, c, "color: inherit");
414 | let index = 0;
415 | let lastC = 0;
416 | args[0].replace(/%[a-zA-Z%]/g, (match) => {
417 | if (match === "%%") {
418 | return;
419 | }
420 | index++;
421 | if (match === "%c") {
422 | lastC = index;
423 | }
424 | });
425 | args.splice(lastC, 0, c);
426 | }
427 | exports.log = console.debug || console.log || (() => {
428 | });
429 | function save(namespaces) {
430 | try {
431 | if (namespaces) {
432 | exports.storage.setItem("debug", namespaces);
433 | } else {
434 | exports.storage.removeItem("debug");
435 | }
436 | } catch (error) {
437 | }
438 | }
439 | function load() {
440 | let r;
441 | try {
442 | r = exports.storage.getItem("debug");
443 | } catch (error) {
444 | }
445 | if (!r && typeof process !== "undefined" && "env" in process) {
446 | r = process.env.DEBUG;
447 | }
448 | return r;
449 | }
450 | function localstorage() {
451 | try {
452 | return localStorage;
453 | } catch (error) {
454 | }
455 | }
456 | module.exports = require_common()(exports);
457 | var { formatters } = module.exports;
458 | formatters.j = function(v) {
459 | try {
460 | return JSON.stringify(v);
461 | } catch (error) {
462 | return "[UnexpectedJSONParseError]: " + error.message;
463 | }
464 | };
465 | }
466 | });
467 | export default require_browser();
468 | //# sourceMappingURL=debug.js.map
469 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js:
--------------------------------------------------------------------------------
1 | import {
2 | DefaultMagicKeysAliasMap,
3 | StorageSerializers,
4 | TransitionPresets,
5 | assert,
6 | breakpointsAntDesign,
7 | breakpointsBootstrapV5,
8 | breakpointsElement,
9 | breakpointsMasterCss,
10 | breakpointsPrimeFlex,
11 | breakpointsQuasar,
12 | breakpointsSematic,
13 | breakpointsTailwind,
14 | breakpointsVuetify,
15 | breakpointsVuetifyV2,
16 | breakpointsVuetifyV3,
17 | bypassFilter,
18 | camelize,
19 | clamp,
20 | cloneFnJSON,
21 | computedAsync,
22 | computedEager,
23 | computedInject,
24 | computedWithControl,
25 | containsProp,
26 | controlledRef,
27 | createEventHook,
28 | createFetch,
29 | createFilterWrapper,
30 | createGlobalState,
31 | createInjectionState,
32 | createReusableTemplate,
33 | createSharedComposable,
34 | createSingletonPromise,
35 | createTemplatePromise,
36 | createUnrefFn,
37 | customStorageEventName,
38 | debounceFilter,
39 | defaultDocument,
40 | defaultLocation,
41 | defaultNavigator,
42 | defaultWindow,
43 | directiveHooks,
44 | executeTransition,
45 | extendRef,
46 | formatDate,
47 | formatTimeAgo,
48 | get,
49 | getLifeCycleTarget,
50 | getSSRHandler,
51 | hasOwn,
52 | hyphenate,
53 | identity,
54 | increaseWithUnit,
55 | injectLocal,
56 | invoke,
57 | isClient,
58 | isDef,
59 | isDefined,
60 | isIOS,
61 | isObject,
62 | isWorker,
63 | makeDestructurable,
64 | mapGamepadToXbox360Controller,
65 | noop,
66 | normalizeDate,
67 | notNullish,
68 | now,
69 | objectEntries,
70 | objectOmit,
71 | objectPick,
72 | onClickOutside,
73 | onKeyDown,
74 | onKeyPressed,
75 | onKeyStroke,
76 | onKeyUp,
77 | onLongPress,
78 | onStartTyping,
79 | pausableFilter,
80 | promiseTimeout,
81 | provideLocal,
82 | rand,
83 | reactify,
84 | reactifyObject,
85 | reactiveComputed,
86 | reactiveOmit,
87 | reactivePick,
88 | refAutoReset,
89 | refDebounced,
90 | refDefault,
91 | refThrottled,
92 | refWithControl,
93 | resolveRef,
94 | resolveUnref,
95 | set,
96 | setSSRHandler,
97 | syncRef,
98 | syncRefs,
99 | templateRef,
100 | throttleFilter,
101 | timestamp,
102 | toReactive,
103 | toRef,
104 | toRefs,
105 | toValue,
106 | tryOnBeforeMount,
107 | tryOnBeforeUnmount,
108 | tryOnMounted,
109 | tryOnScopeDispose,
110 | tryOnUnmounted,
111 | unrefElement,
112 | until,
113 | useActiveElement,
114 | useAnimate,
115 | useArrayDifference,
116 | useArrayEvery,
117 | useArrayFilter,
118 | useArrayFind,
119 | useArrayFindIndex,
120 | useArrayFindLast,
121 | useArrayIncludes,
122 | useArrayJoin,
123 | useArrayMap,
124 | useArrayReduce,
125 | useArraySome,
126 | useArrayUnique,
127 | useAsyncQueue,
128 | useAsyncState,
129 | useBase64,
130 | useBattery,
131 | useBluetooth,
132 | useBreakpoints,
133 | useBroadcastChannel,
134 | useBrowserLocation,
135 | useCached,
136 | useClipboard,
137 | useClipboardItems,
138 | useCloned,
139 | useColorMode,
140 | useConfirmDialog,
141 | useCounter,
142 | useCssVar,
143 | useCurrentElement,
144 | useCycleList,
145 | useDark,
146 | useDateFormat,
147 | useDebounceFn,
148 | useDebouncedRefHistory,
149 | useDeviceMotion,
150 | useDeviceOrientation,
151 | useDevicePixelRatio,
152 | useDevicesList,
153 | useDisplayMedia,
154 | useDocumentVisibility,
155 | useDraggable,
156 | useDropZone,
157 | useElementBounding,
158 | useElementByPoint,
159 | useElementHover,
160 | useElementSize,
161 | useElementVisibility,
162 | useEventBus,
163 | useEventListener,
164 | useEventSource,
165 | useEyeDropper,
166 | useFavicon,
167 | useFetch,
168 | useFileDialog,
169 | useFileSystemAccess,
170 | useFocus,
171 | useFocusWithin,
172 | useFps,
173 | useFullscreen,
174 | useGamepad,
175 | useGeolocation,
176 | useIdle,
177 | useImage,
178 | useInfiniteScroll,
179 | useIntersectionObserver,
180 | useInterval,
181 | useIntervalFn,
182 | useKeyModifier,
183 | useLastChanged,
184 | useLocalStorage,
185 | useMagicKeys,
186 | useManualRefHistory,
187 | useMediaControls,
188 | useMediaQuery,
189 | useMemoize,
190 | useMemory,
191 | useMounted,
192 | useMouse,
193 | useMouseInElement,
194 | useMousePressed,
195 | useMutationObserver,
196 | useNavigatorLanguage,
197 | useNetwork,
198 | useNow,
199 | useObjectUrl,
200 | useOffsetPagination,
201 | useOnline,
202 | usePageLeave,
203 | useParallax,
204 | useParentElement,
205 | usePerformanceObserver,
206 | usePermission,
207 | usePointer,
208 | usePointerLock,
209 | usePointerSwipe,
210 | usePreferredColorScheme,
211 | usePreferredContrast,
212 | usePreferredDark,
213 | usePreferredLanguages,
214 | usePreferredReducedMotion,
215 | usePrevious,
216 | useRafFn,
217 | useRefHistory,
218 | useResizeObserver,
219 | useScreenOrientation,
220 | useScreenSafeArea,
221 | useScriptTag,
222 | useScroll,
223 | useScrollLock,
224 | useSessionStorage,
225 | useShare,
226 | useSorted,
227 | useSpeechRecognition,
228 | useSpeechSynthesis,
229 | useStepper,
230 | useStorage,
231 | useStorageAsync,
232 | useStyleTag,
233 | useSupported,
234 | useSwipe,
235 | useTemplateRefsList,
236 | useTextDirection,
237 | useTextSelection,
238 | useTextareaAutosize,
239 | useThrottleFn,
240 | useThrottledRefHistory,
241 | useTimeAgo,
242 | useTimeout,
243 | useTimeoutFn,
244 | useTimeoutPoll,
245 | useTimestamp,
246 | useTitle,
247 | useToNumber,
248 | useToString,
249 | useToggle,
250 | useTransition,
251 | useUrlSearchParams,
252 | useUserMedia,
253 | useVModel,
254 | useVModels,
255 | useVibrate,
256 | useVirtualList,
257 | useWakeLock,
258 | useWebNotification,
259 | useWebSocket,
260 | useWebWorker,
261 | useWebWorkerFn,
262 | useWindowFocus,
263 | useWindowScroll,
264 | useWindowSize,
265 | watchArray,
266 | watchAtMost,
267 | watchDebounced,
268 | watchDeep,
269 | watchIgnorable,
270 | watchImmediate,
271 | watchOnce,
272 | watchPausable,
273 | watchThrottled,
274 | watchTriggerable,
275 | watchWithFilter,
276 | whenever
277 | } from "./chunk-2AEJUKVR.js";
278 | import "./chunk-U55NQ7RZ.js";
279 | import "./chunk-BUSYA2B4.js";
280 | export {
281 | DefaultMagicKeysAliasMap,
282 | StorageSerializers,
283 | TransitionPresets,
284 | assert,
285 | computedAsync as asyncComputed,
286 | refAutoReset as autoResetRef,
287 | breakpointsAntDesign,
288 | breakpointsBootstrapV5,
289 | breakpointsElement,
290 | breakpointsMasterCss,
291 | breakpointsPrimeFlex,
292 | breakpointsQuasar,
293 | breakpointsSematic,
294 | breakpointsTailwind,
295 | breakpointsVuetify,
296 | breakpointsVuetifyV2,
297 | breakpointsVuetifyV3,
298 | bypassFilter,
299 | camelize,
300 | clamp,
301 | cloneFnJSON,
302 | computedAsync,
303 | computedEager,
304 | computedInject,
305 | computedWithControl,
306 | containsProp,
307 | computedWithControl as controlledComputed,
308 | controlledRef,
309 | createEventHook,
310 | createFetch,
311 | createFilterWrapper,
312 | createGlobalState,
313 | createInjectionState,
314 | reactify as createReactiveFn,
315 | createReusableTemplate,
316 | createSharedComposable,
317 | createSingletonPromise,
318 | createTemplatePromise,
319 | createUnrefFn,
320 | customStorageEventName,
321 | debounceFilter,
322 | refDebounced as debouncedRef,
323 | watchDebounced as debouncedWatch,
324 | defaultDocument,
325 | defaultLocation,
326 | defaultNavigator,
327 | defaultWindow,
328 | directiveHooks,
329 | computedEager as eagerComputed,
330 | executeTransition,
331 | extendRef,
332 | formatDate,
333 | formatTimeAgo,
334 | get,
335 | getLifeCycleTarget,
336 | getSSRHandler,
337 | hasOwn,
338 | hyphenate,
339 | identity,
340 | watchIgnorable as ignorableWatch,
341 | increaseWithUnit,
342 | injectLocal,
343 | invoke,
344 | isClient,
345 | isDef,
346 | isDefined,
347 | isIOS,
348 | isObject,
349 | isWorker,
350 | makeDestructurable,
351 | mapGamepadToXbox360Controller,
352 | noop,
353 | normalizeDate,
354 | notNullish,
355 | now,
356 | objectEntries,
357 | objectOmit,
358 | objectPick,
359 | onClickOutside,
360 | onKeyDown,
361 | onKeyPressed,
362 | onKeyStroke,
363 | onKeyUp,
364 | onLongPress,
365 | onStartTyping,
366 | pausableFilter,
367 | watchPausable as pausableWatch,
368 | promiseTimeout,
369 | provideLocal,
370 | rand,
371 | reactify,
372 | reactifyObject,
373 | reactiveComputed,
374 | reactiveOmit,
375 | reactivePick,
376 | refAutoReset,
377 | refDebounced,
378 | refDefault,
379 | refThrottled,
380 | refWithControl,
381 | resolveRef,
382 | resolveUnref,
383 | set,
384 | setSSRHandler,
385 | syncRef,
386 | syncRefs,
387 | templateRef,
388 | throttleFilter,
389 | refThrottled as throttledRef,
390 | watchThrottled as throttledWatch,
391 | timestamp,
392 | toReactive,
393 | toRef,
394 | toRefs,
395 | toValue,
396 | tryOnBeforeMount,
397 | tryOnBeforeUnmount,
398 | tryOnMounted,
399 | tryOnScopeDispose,
400 | tryOnUnmounted,
401 | unrefElement,
402 | until,
403 | useActiveElement,
404 | useAnimate,
405 | useArrayDifference,
406 | useArrayEvery,
407 | useArrayFilter,
408 | useArrayFind,
409 | useArrayFindIndex,
410 | useArrayFindLast,
411 | useArrayIncludes,
412 | useArrayJoin,
413 | useArrayMap,
414 | useArrayReduce,
415 | useArraySome,
416 | useArrayUnique,
417 | useAsyncQueue,
418 | useAsyncState,
419 | useBase64,
420 | useBattery,
421 | useBluetooth,
422 | useBreakpoints,
423 | useBroadcastChannel,
424 | useBrowserLocation,
425 | useCached,
426 | useClipboard,
427 | useClipboardItems,
428 | useCloned,
429 | useColorMode,
430 | useConfirmDialog,
431 | useCounter,
432 | useCssVar,
433 | useCurrentElement,
434 | useCycleList,
435 | useDark,
436 | useDateFormat,
437 | refDebounced as useDebounce,
438 | useDebounceFn,
439 | useDebouncedRefHistory,
440 | useDeviceMotion,
441 | useDeviceOrientation,
442 | useDevicePixelRatio,
443 | useDevicesList,
444 | useDisplayMedia,
445 | useDocumentVisibility,
446 | useDraggable,
447 | useDropZone,
448 | useElementBounding,
449 | useElementByPoint,
450 | useElementHover,
451 | useElementSize,
452 | useElementVisibility,
453 | useEventBus,
454 | useEventListener,
455 | useEventSource,
456 | useEyeDropper,
457 | useFavicon,
458 | useFetch,
459 | useFileDialog,
460 | useFileSystemAccess,
461 | useFocus,
462 | useFocusWithin,
463 | useFps,
464 | useFullscreen,
465 | useGamepad,
466 | useGeolocation,
467 | useIdle,
468 | useImage,
469 | useInfiniteScroll,
470 | useIntersectionObserver,
471 | useInterval,
472 | useIntervalFn,
473 | useKeyModifier,
474 | useLastChanged,
475 | useLocalStorage,
476 | useMagicKeys,
477 | useManualRefHistory,
478 | useMediaControls,
479 | useMediaQuery,
480 | useMemoize,
481 | useMemory,
482 | useMounted,
483 | useMouse,
484 | useMouseInElement,
485 | useMousePressed,
486 | useMutationObserver,
487 | useNavigatorLanguage,
488 | useNetwork,
489 | useNow,
490 | useObjectUrl,
491 | useOffsetPagination,
492 | useOnline,
493 | usePageLeave,
494 | useParallax,
495 | useParentElement,
496 | usePerformanceObserver,
497 | usePermission,
498 | usePointer,
499 | usePointerLock,
500 | usePointerSwipe,
501 | usePreferredColorScheme,
502 | usePreferredContrast,
503 | usePreferredDark,
504 | usePreferredLanguages,
505 | usePreferredReducedMotion,
506 | usePrevious,
507 | useRafFn,
508 | useRefHistory,
509 | useResizeObserver,
510 | useScreenOrientation,
511 | useScreenSafeArea,
512 | useScriptTag,
513 | useScroll,
514 | useScrollLock,
515 | useSessionStorage,
516 | useShare,
517 | useSorted,
518 | useSpeechRecognition,
519 | useSpeechSynthesis,
520 | useStepper,
521 | useStorage,
522 | useStorageAsync,
523 | useStyleTag,
524 | useSupported,
525 | useSwipe,
526 | useTemplateRefsList,
527 | useTextDirection,
528 | useTextSelection,
529 | useTextareaAutosize,
530 | refThrottled as useThrottle,
531 | useThrottleFn,
532 | useThrottledRefHistory,
533 | useTimeAgo,
534 | useTimeout,
535 | useTimeoutFn,
536 | useTimeoutPoll,
537 | useTimestamp,
538 | useTitle,
539 | useToNumber,
540 | useToString,
541 | useToggle,
542 | useTransition,
543 | useUrlSearchParams,
544 | useUserMedia,
545 | useVModel,
546 | useVModels,
547 | useVibrate,
548 | useVirtualList,
549 | useWakeLock,
550 | useWebNotification,
551 | useWebSocket,
552 | useWebWorker,
553 | useWebWorkerFn,
554 | useWindowFocus,
555 | useWindowScroll,
556 | useWindowSize,
557 | watchArray,
558 | watchAtMost,
559 | watchDebounced,
560 | watchDeep,
561 | watchIgnorable,
562 | watchImmediate,
563 | watchOnce,
564 | watchPausable,
565 | watchThrottled,
566 | watchTriggerable,
567 | watchWithFilter,
568 | whenever
569 | };
570 | //# sourceMappingURL=vitepress___@vueuse_core.js.map
571 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "sources": [],
4 | "sourcesContent": [],
5 | "mappings": "",
6 | "names": []
7 | }
8 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/vue.js:
--------------------------------------------------------------------------------
1 | import {
2 | BaseTransition,
3 | BaseTransitionPropsValidators,
4 | Comment,
5 | DeprecationTypes,
6 | EffectScope,
7 | ErrorCodes,
8 | ErrorTypeStrings,
9 | Fragment,
10 | KeepAlive,
11 | ReactiveEffect,
12 | Static,
13 | Suspense,
14 | Teleport,
15 | Text,
16 | TrackOpTypes,
17 | Transition,
18 | TransitionGroup,
19 | TriggerOpTypes,
20 | VueElement,
21 | assertNumber,
22 | callWithAsyncErrorHandling,
23 | callWithErrorHandling,
24 | camelize,
25 | capitalize,
26 | cloneVNode,
27 | compatUtils,
28 | compile,
29 | computed,
30 | createApp,
31 | createBaseVNode,
32 | createBlock,
33 | createCommentVNode,
34 | createElementBlock,
35 | createHydrationRenderer,
36 | createPropsRestProxy,
37 | createRenderer,
38 | createSSRApp,
39 | createSlots,
40 | createStaticVNode,
41 | createTextVNode,
42 | createVNode,
43 | customRef,
44 | defineAsyncComponent,
45 | defineComponent,
46 | defineCustomElement,
47 | defineEmits,
48 | defineExpose,
49 | defineModel,
50 | defineOptions,
51 | defineProps,
52 | defineSSRCustomElement,
53 | defineSlots,
54 | devtools,
55 | effect,
56 | effectScope,
57 | getCurrentInstance,
58 | getCurrentScope,
59 | getCurrentWatcher,
60 | getTransitionRawChildren,
61 | guardReactiveProps,
62 | h,
63 | handleError,
64 | hasInjectionContext,
65 | hydrate,
66 | hydrateOnIdle,
67 | hydrateOnInteraction,
68 | hydrateOnMediaQuery,
69 | hydrateOnVisible,
70 | initCustomFormatter,
71 | initDirectivesForSSR,
72 | inject,
73 | isMemoSame,
74 | isProxy,
75 | isReactive,
76 | isReadonly,
77 | isRef,
78 | isRuntimeOnly,
79 | isShallow,
80 | isVNode,
81 | markRaw,
82 | mergeDefaults,
83 | mergeModels,
84 | mergeProps,
85 | nextTick,
86 | normalizeClass,
87 | normalizeProps,
88 | normalizeStyle,
89 | onActivated,
90 | onBeforeMount,
91 | onBeforeUnmount,
92 | onBeforeUpdate,
93 | onDeactivated,
94 | onErrorCaptured,
95 | onMounted,
96 | onRenderTracked,
97 | onRenderTriggered,
98 | onScopeDispose,
99 | onServerPrefetch,
100 | onUnmounted,
101 | onUpdated,
102 | onWatcherCleanup,
103 | openBlock,
104 | popScopeId,
105 | provide,
106 | proxyRefs,
107 | pushScopeId,
108 | queuePostFlushCb,
109 | reactive,
110 | readonly,
111 | ref,
112 | registerRuntimeCompiler,
113 | render,
114 | renderList,
115 | renderSlot,
116 | resolveComponent,
117 | resolveDirective,
118 | resolveDynamicComponent,
119 | resolveFilter,
120 | resolveTransitionHooks,
121 | setBlockTracking,
122 | setDevtoolsHook,
123 | setTransitionHooks,
124 | shallowReactive,
125 | shallowReadonly,
126 | shallowRef,
127 | ssrContextKey,
128 | ssrUtils,
129 | stop,
130 | toDisplayString,
131 | toHandlerKey,
132 | toHandlers,
133 | toRaw,
134 | toRef,
135 | toRefs,
136 | toValue,
137 | transformVNodeArgs,
138 | triggerRef,
139 | unref,
140 | useAttrs,
141 | useCssModule,
142 | useCssVars,
143 | useHost,
144 | useId,
145 | useModel,
146 | useSSRContext,
147 | useShadowRoot,
148 | useSlots,
149 | useTemplateRef,
150 | useTransitionState,
151 | vModelCheckbox,
152 | vModelDynamic,
153 | vModelRadio,
154 | vModelSelect,
155 | vModelText,
156 | vShow,
157 | version,
158 | warn,
159 | watch,
160 | watchEffect,
161 | watchPostEffect,
162 | watchSyncEffect,
163 | withAsyncContext,
164 | withCtx,
165 | withDefaults,
166 | withDirectives,
167 | withKeys,
168 | withMemo,
169 | withModifiers,
170 | withScopeId
171 | } from "./chunk-U55NQ7RZ.js";
172 | import "./chunk-BUSYA2B4.js";
173 | export {
174 | BaseTransition,
175 | BaseTransitionPropsValidators,
176 | Comment,
177 | DeprecationTypes,
178 | EffectScope,
179 | ErrorCodes,
180 | ErrorTypeStrings,
181 | Fragment,
182 | KeepAlive,
183 | ReactiveEffect,
184 | Static,
185 | Suspense,
186 | Teleport,
187 | Text,
188 | TrackOpTypes,
189 | Transition,
190 | TransitionGroup,
191 | TriggerOpTypes,
192 | VueElement,
193 | assertNumber,
194 | callWithAsyncErrorHandling,
195 | callWithErrorHandling,
196 | camelize,
197 | capitalize,
198 | cloneVNode,
199 | compatUtils,
200 | compile,
201 | computed,
202 | createApp,
203 | createBlock,
204 | createCommentVNode,
205 | createElementBlock,
206 | createBaseVNode as createElementVNode,
207 | createHydrationRenderer,
208 | createPropsRestProxy,
209 | createRenderer,
210 | createSSRApp,
211 | createSlots,
212 | createStaticVNode,
213 | createTextVNode,
214 | createVNode,
215 | customRef,
216 | defineAsyncComponent,
217 | defineComponent,
218 | defineCustomElement,
219 | defineEmits,
220 | defineExpose,
221 | defineModel,
222 | defineOptions,
223 | defineProps,
224 | defineSSRCustomElement,
225 | defineSlots,
226 | devtools,
227 | effect,
228 | effectScope,
229 | getCurrentInstance,
230 | getCurrentScope,
231 | getCurrentWatcher,
232 | getTransitionRawChildren,
233 | guardReactiveProps,
234 | h,
235 | handleError,
236 | hasInjectionContext,
237 | hydrate,
238 | hydrateOnIdle,
239 | hydrateOnInteraction,
240 | hydrateOnMediaQuery,
241 | hydrateOnVisible,
242 | initCustomFormatter,
243 | initDirectivesForSSR,
244 | inject,
245 | isMemoSame,
246 | isProxy,
247 | isReactive,
248 | isReadonly,
249 | isRef,
250 | isRuntimeOnly,
251 | isShallow,
252 | isVNode,
253 | markRaw,
254 | mergeDefaults,
255 | mergeModels,
256 | mergeProps,
257 | nextTick,
258 | normalizeClass,
259 | normalizeProps,
260 | normalizeStyle,
261 | onActivated,
262 | onBeforeMount,
263 | onBeforeUnmount,
264 | onBeforeUpdate,
265 | onDeactivated,
266 | onErrorCaptured,
267 | onMounted,
268 | onRenderTracked,
269 | onRenderTriggered,
270 | onScopeDispose,
271 | onServerPrefetch,
272 | onUnmounted,
273 | onUpdated,
274 | onWatcherCleanup,
275 | openBlock,
276 | popScopeId,
277 | provide,
278 | proxyRefs,
279 | pushScopeId,
280 | queuePostFlushCb,
281 | reactive,
282 | readonly,
283 | ref,
284 | registerRuntimeCompiler,
285 | render,
286 | renderList,
287 | renderSlot,
288 | resolveComponent,
289 | resolveDirective,
290 | resolveDynamicComponent,
291 | resolveFilter,
292 | resolveTransitionHooks,
293 | setBlockTracking,
294 | setDevtoolsHook,
295 | setTransitionHooks,
296 | shallowReactive,
297 | shallowReadonly,
298 | shallowRef,
299 | ssrContextKey,
300 | ssrUtils,
301 | stop,
302 | toDisplayString,
303 | toHandlerKey,
304 | toHandlers,
305 | toRaw,
306 | toRef,
307 | toRefs,
308 | toValue,
309 | transformVNodeArgs,
310 | triggerRef,
311 | unref,
312 | useAttrs,
313 | useCssModule,
314 | useCssVars,
315 | useHost,
316 | useId,
317 | useModel,
318 | useSSRContext,
319 | useShadowRoot,
320 | useSlots,
321 | useTemplateRef,
322 | useTransitionState,
323 | vModelCheckbox,
324 | vModelDynamic,
325 | vModelRadio,
326 | vModelSelect,
327 | vModelText,
328 | vShow,
329 | version,
330 | warn,
331 | watch,
332 | watchEffect,
333 | watchPostEffect,
334 | watchSyncEffect,
335 | withAsyncContext,
336 | withCtx,
337 | withDefaults,
338 | withDirectives,
339 | withKeys,
340 | withMemo,
341 | withModifiers,
342 | withScopeId
343 | };
344 | //# sourceMappingURL=vue.js.map
345 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/vue.js.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "sources": [],
4 | "sourcesContent": [],
5 | "mappings": "",
6 | "names": []
7 | }
8 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.mts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitepress'
2 | import { withMermaid } from 'vitepress-plugin-mermaid'
3 |
4 | // https://vitepress.dev/reference/site-config
5 | let config = defineConfig({
6 | base: "/",
7 | title: "bilitool",
8 | description: "Official documentation",
9 | themeConfig: {
10 | // https://vitepress.dev/reference/default-theme-config
11 | nav: [
12 | { text: 'Home', link: '/' },
13 | { text: 'Guide', link: '/getting-start' }
14 | ],
15 |
16 | sidebar: [
17 | {
18 | text: 'Guide',
19 | items: [
20 | { text: 'Getting Started', link: '/getting-start' },
21 | { text: 'Installation', link: '/installation' },
22 | { text: 'Login', link: '/login' },
23 | { text: 'Check', link: '/check' },
24 | { text: 'Logout', link: '/logout' },
25 | { text: 'Upload', link: '/upload' },
26 | { text: 'Download', link: '/download' },
27 | { text: 'List', link: '/list' },
28 | { text: 'Show', link: '/show' },
29 | { text: 'Convert', link: '/convert' },
30 | { text: 'IP', link: '/ip' },
31 | { text: 'Tid', link: '/tid' },
32 | ]
33 | }
34 | ],
35 | outline: {
36 | level: [2, 4]
37 | },
38 | socialLinks: [
39 | { icon: 'github', link: 'https://github.com/timerring/bilitool' }
40 | ]
41 | },
42 | // optionally, you can pass MermaidConfig
43 | mermaid: {
44 | // refer for options:
45 | // https://mermaid.js.org/config/setup/modules/mermaidAPI.html#mermaidapi-configuration-defaults
46 | },
47 | // optionally set additional config for plugin itself with MermaidPluginConfig
48 | mermaidPlugin: {
49 | // set additional css class for mermaid container
50 | class: "mermaid"
51 | }
52 | })
53 |
54 | config = withMermaid(config)
55 |
56 | export default config
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | bilitool.timerring.com
--------------------------------------------------------------------------------
/docs/check.md:
--------------------------------------------------------------------------------
1 | ## 检查登录状态
2 |
3 | > 检查当前登录的账号名称
4 |
5 | ```bash
6 | bilitool check
7 |
8 | # 检查成功后会出现
9 | # Current account: Your account name
10 | # Current level: Your account level
11 | # Status: 非大会员
12 |
13 | # 如果未登录,则会出现
14 | # There is currently no login account, some functions may not work
15 | ```
--------------------------------------------------------------------------------
/docs/convert.md:
--------------------------------------------------------------------------------
1 |
2 | ## 查询转换视频编号
3 |
4 | `bilitool convert -h ` 打印帮助信息:
5 |
6 | ```bash
7 | usage: bilitool convert [-h] vid
8 |
9 | positional arguments:
10 | vid The avid or bvid of the video
11 |
12 | options:
13 | -h, --help show this help message and exit
14 | ```
15 |
16 | 示例:
17 |
18 | ```bash
19 | # 转换视频编号
20 | # 输入 bvid 输出 avid
21 | bilitool convert BV1BpcPeqE2p
22 | # 输出格式
23 | # The avid of the video is: 113811163974247
24 |
25 | # 输入 avid 输出 bvid
26 | bilitool convert 113811163974247
27 | # 输出格式
28 | # The bvid of the video is: BV1BpcPeqE2p
29 | ```
--------------------------------------------------------------------------------
/docs/download.md:
--------------------------------------------------------------------------------
1 | ## 下载
2 |
3 | > 注意:如果要下载高清以上画质的视频,需要先登录才能获取下载。
4 |
5 | `bilitool download -h ` 打印帮助信息:
6 |
7 | ```bash
8 | usage: bilitool download [-h] [--danmaku] [--quality QUALITY] [--chunksize CHUNKSIZE] [--multiple] bvid
9 |
10 | positional arguments:
11 | bvid (required) the bvid of video
12 |
13 | options:
14 | -h, --help show this help message and exit
15 | --danmaku (default is false) download the danmaku of video
16 | --quality QUALITY (default is 64) the resolution of video
17 | --chunksize CHUNKSIZE
18 | (default is 1024) the chunk size of video
19 | --multiple (default is false) download the multiple videos if have set
20 | ```
21 |
22 | 示例:
23 |
24 | ```bash
25 | # 默认下载视频
26 | bilitool download bvid
27 |
28 | # 下载序号为 bvid 的视频,并下载弹幕,设置画质为 1080p 高清,分段大小为 1024,如果有多 p,则一次性下载所有视频
29 | bilitool download bvid --danmaku --quality 80 --chunksize 1024 --multiple
30 | ```
31 |
32 | 下载过程日志格式:
33 |
34 | ```
35 | [INFO] - [2025-01-15 21:54:13,026 bilitool] - Begin download the video name
36 | the video name.mp4: 100%|███████████████████████████████████████████████████████████████████████| 6.54M/6.54M [00:00<00:00, 9.74MB/s]
37 | [INFO] - [2025-01-15 21:54:14,109 bilitool] - Download completed
38 | [INFO] - [2025-01-15 21:54:14,112 bilitool] - Begin download danmaku
39 | [INFO] - [2025-01-15 21:54:14,225 bilitool] - Successfully downloaded danmaku
40 | ```
--------------------------------------------------------------------------------
/docs/getting-start.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | `bilitool` 是一个 python 的工具库,实现持久化登录,下载视频,上传视频到 bilibili 等功能,可以使用命令行 cli 操作,也可以作为其他项目的库使用。
4 |
5 | ## Major features
6 |
7 | - `login` 记忆存储登录状态
8 | - 支持导出 `cookies.json` 用于其他项目
9 | - `logout` 退出登录
10 | - `check` 检查登录状态
11 | - `upload` 上传视频
12 | - 支持多种自定义参数上传
13 | - 支持上传视频的 yaml 配置与解析
14 | - 显示日志与上传进度
15 | - `download` 下载视频
16 | - 支持 `bvid` 和 `avid` 两种编号下载
17 | - 支持下载弹幕
18 | - 支持下载多种画质
19 | - 支持下载多 p 视频
20 | - 显示日志与下载进度
21 | - `ip` 显示请求 IP 地址
22 | - 支持查询指定 IP 地址
23 | - `list` 查询本账号过往投稿视频状态
24 | - 支持查询多种状态的视频
25 | - 若视频审核未通过,同时会显示原因
26 | - `convert` 查询转换视频编号
27 | - 支持 `bvid` 和 `avid` 两种编号互转
28 | - `show` 显示视频详细信息
29 | - 支持查看视频基本信息以及互动状态数据
30 | - 追加视频到已有的视频(预计支持)
31 |
32 | ## Architecture diagram
33 |
34 | 项目仿照 MVC 架构进行设计:
35 |
36 | ```mermaid
37 | graph TD
38 | subgraph Model
39 | M1[Model]
40 | end
41 |
42 | subgraph Database
43 | DB1[JSON]
44 | end
45 |
46 | subgraph Controller
47 | C1[DownloadController]
48 | C2[UploadController]
49 | C3[LoginController]
50 | C4[FeedController]
51 | end
52 |
53 | subgraph View
54 | V1[CLI]
55 | end
56 |
57 | subgraph Utility
58 | U1[CheckFormat]
59 | U2[IPInfo]
60 | end
61 |
62 | subgraph Download
63 | D1[BiliDownloader]
64 | end
65 |
66 | subgraph Upload
67 | U3[BiliUploader]
68 | end
69 |
70 | subgraph Feed
71 | F1[BiliVideoList]
72 | end
73 |
74 | subgraph Login
75 | L1[LoginBili]
76 | L2[LogoutBili]
77 | L3[CheckBiliLogin]
78 | end
79 |
80 | subgraph Authenticate
81 | A1[WbiSign]
82 | end
83 |
84 | M1 --> DB1
85 | DB1 --> M1
86 |
87 | M1 <--> C1
88 | M1 <--> C2
89 | M1 <--> C4
90 |
91 | C1 --> D1
92 | C2 --> U3
93 |
94 | V1 --> Utility
95 |
96 | C3 --> L1
97 | C3 --> L2
98 | C3 --> L3
99 |
100 | C4 --> F1
101 |
102 | V1 --> C1
103 | V1 --> C2
104 | V1 --> C3
105 | V1 --> A1 --> C4
106 |
107 | ```
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: "bilitool"
7 | text: "Official documentation"
8 | tagline: The Python toolkit package and cli designed for interaction with Bilibili.
9 | actions:
10 | - theme: brand
11 | text: 现在开始
12 | link: /getting-start
13 | - theme: alt
14 | text: 在 GitHub 上查看
15 | link: https://github.com/timerring/bilitool
16 |
17 | features:
18 | - title: 持久化登录
19 | details: 记忆存储登录状态,支持导出 `cookies.json` 用于其他项目
20 | - title: 保护隐私
21 | details: 退出登录同时注销 cookies,保护隐私信息防止泄露
22 | - title: 检查登录状态
23 | details: 检查当前登录的账号名称
24 | - title: 上传视频
25 | details: 支持多种自定义参数上传
26 | - title: 下载视频
27 | details: 支持多编号,多画质,弹幕,多 p 视频下载
28 | - title: 查询视频详细信息
29 | details: 支持查询视频详细信息以及互动状态数据
30 | - title: 查询转换视频编号
31 | details: 支持 `bvid` 和 `avid` 两种编号互转
32 | - title: 查询 IP 地址
33 | details: 支持查询请求 IP 地址以及指定 IP 地址
34 | ---
35 |
36 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | ## Installation
2 |
3 | ```bash
4 | pip install bilitool
5 | ```
6 |
7 | Or you can download the compiled cli tool directly [download address](https://github.com/timerring/bilitool/releases).
--------------------------------------------------------------------------------
/docs/ip.md:
--------------------------------------------------------------------------------
1 |
2 | ## 查询 IP 地址
3 |
4 | `bilitool ip -h ` 打印帮助信息:
5 |
6 | ```bash
7 | usage: bilitool ip [-h] [--ip IP]
8 |
9 | options:
10 | -h, --help show this help message and exit
11 | --ip IP the ip address you want to query
12 | ```
13 |
14 | 示例:
15 |
16 | > 由于国内网络搭建的历史原因以及复杂性,查询 IP 普遍有误差,这也是为什么 IP 结果只能精确到省的原因。详细内容可以参考 IPIP 创始人高春辉的微信公众号。
17 |
18 | ```bash
19 | # 默认查询本机 IP
20 | bilitool ip
21 | # 输出格式
22 | # IP: 183.xxx.xxx.xxx, ISP: 移动, Location: 中国广东广州, Position: xx.xxxx,xxx.xxxxxx
23 |
24 | # 查询指定 IP
25 | bilitool ip --ip 8.8.8.8
26 | # 输出格式
27 | # IP: 8.8.8.8, ISP: level3.com, Location: GOOGLE.COMGOOGLE.COM, Position: ,
28 | ```
--------------------------------------------------------------------------------
/docs/list.md:
--------------------------------------------------------------------------------
1 | ## 查询近期投稿视频状态
2 |
3 | `bilitool list -h ` 打印帮助信息:
4 |
5 | ```bash
6 | usage: bilitool list [-h] [--size SIZE] [--status STATUS]
7 |
8 | options:
9 | -h, --help show this help message and exit
10 | --size SIZE (default is 20) the size of video list
11 | --status STATUS (default is all) the status of video list: pubed, not_pubed, is_pubing
12 | ```
13 |
14 | 示例:
15 |
16 | ```bash
17 | # 默认显示近期所有投稿的 20 条视频信息
18 | bilitool list
19 |
20 | # 查询近期投稿审核未通过的 10 条视频
21 | bilitool list --size 10 --status not_pubed
22 | ```
23 |
24 | 打印返回格式
25 |
26 | ```
27 | 开放浏览 | BV1xxxxxxxxx | the video title
28 | 开放浏览 | BV1xxxxxxxxx | the video title
29 | 已退回 | BV1xxxxxxxxx | the video title | 拒绝原因: 根据相关法律法规和政策,该视频【P1(00:20:40-00:21:59)】不予审核通过
30 | ```
--------------------------------------------------------------------------------
/docs/login.md:
--------------------------------------------------------------------------------
1 | ## 登录
2 |
3 | ```bash
4 | bilitool login
5 | ```
6 |
7 | 然后你可以扫描二维码或点击链接登录,如果输入命令时加上 `--export` 参数,则会导出 `cookie.json` 文件到当前目录,该 `cookie.json` 文件可以用于其他项目。
8 |
9 | > 注意,请勿将 `cookie.json` 文件泄露给他人,否则很可能会导致账号被盗。
10 |
11 | 
12 |
13 | `bilitool login -h ` 打印帮助信息:
14 |
15 | ```
16 | usage: bilitool login [-h] [--export]
17 |
18 | options:
19 | -h, --help show this help message and exit
20 | --export (default is false) export the login cookie file
21 | ```
22 |
23 | 登录示例:
24 |
25 | ```bash
26 | # 扫码登录
27 | bilitool login
28 |
29 | # 登录成功后同时导出一份 cookie.json 文件
30 | bilitool login --export
31 |
32 | # 登录成功后会出现
33 | # Login success!
34 | ```
35 |
--------------------------------------------------------------------------------
/docs/logout.md:
--------------------------------------------------------------------------------
1 | ## 退出登录
2 |
3 | > 退出登录后,同时会使 `cookies.json` 文件失效(如果登录时使用了 `--export` 参数导出了 cookies)。
4 |
5 | ```bash
6 | bilitool logout
7 |
8 | # 成功退出后会显示
9 | # Logout successfully, the cookie has expired
10 |
11 | # 退出失败则会显示
12 | # Logout failed, check the info: XXX
13 | ```
--------------------------------------------------------------------------------
/docs/show.md:
--------------------------------------------------------------------------------
1 |
2 | ## 查询视频详细信息
3 |
4 | `bilitool show -h ` 打印帮助信息:
5 |
6 | ```bash
7 | usage: bilitool show [-h] bvid
8 |
9 | positional arguments:
10 | vid The avid or bvid of the video
11 |
12 | options:
13 | -h, --help show this help message and exit
14 | ```
15 |
16 | 示例:
17 |
18 | ```bash
19 | # 查询视频详细信息
20 | # bvid 查询
21 | bilitool show BV1BpcPeqE2p
22 |
23 | # avid 查询
24 | bilitool show 113811163974247
25 | ```
26 |
27 | 查询结果格式:
28 |
29 | ```
30 | 标题: 【MrBeast公益】我帮助2000人重新行走!
31 | 描述: 全球首发视频都在这里 记得关注哦
32 | 时长: 30:36
33 | 发布日期: 2025-01-12 04:05:00
34 | 作者名称: MrBeast官方账号
35 | 分区: 日常
36 | 版权: 原创
37 | 宽: 1920
38 | 高: 1080
39 | 观看数: 986457
40 | 弹幕数: 26129
41 | 评论数: 2612
42 | 硬币数: 90428
43 | 分享数: 1529
44 | 点赞数: 213320
45 | ```
--------------------------------------------------------------------------------
/docs/tid.md:
--------------------------------------------------------------------------------
1 | ## tid 对应分区一览
2 |
3 | ### 动画
4 | | 主分区名称 | 自分区名称 | tid 代号 |
5 | | --- | --- | --- |
6 | | 动画 | 动画(主分区) | 1 |
7 | | 动画 | MAD·AMV | 24 |
8 | | 动画 | MMD·3D | 25 |
9 | | 动画 | 短片·手书 | 47 |
10 | | 动画 | 配音 | 257 |
11 | | 动画 | 手办·模玩 | 210 |
12 | | 动画 | 特摄 | 86 |
13 | | 动画 | 动漫杂谈 | 253 |
14 | | 动画 | 综合 | 27 |
15 |
16 | ### 番剧
17 | | 主分区名称 | 自分区名称 | tid 代号 |
18 | | --- | --- | --- |
19 | | 番剧 | 番剧(主分区) | 13 |
20 | | 番剧 | 资讯 | 51 |
21 | | 番剧 | 官方延伸 | 152 |
22 | | 番剧 | 完结动画 | 32 |
23 | | 番剧 | 连载动画 | 33 |
24 |
25 | ### 国创
26 | | 主分区名称 | 自分区名称 | tid 代号 |
27 | | --- | --- | --- |
28 | | 国创 | 国创(主分区) | 167 |
29 | | 国创 | 国产动画 | 153 |
30 | | 国创 | 国产原创相关 | 168 |
31 | | 国创 | 布袋戏 | 169 |
32 | | 国创 | 资讯 | 170 |
33 | | 国创 | 动态漫·广播剧 | 195 |
34 |
35 | ### 音乐
36 | | 主分区名称 | 自分区名称 | tid 代号 |
37 | | --- | --- | --- |
38 | | 音乐 | 音乐(主分区) | 3 |
39 | | 音乐 | 原创音乐 | 28 |
40 | | 音乐 | 翻唱 | 31 |
41 | | 音乐 | VOCALOID·UTAU | 30 |
42 | | 音乐 | 演奏 | 59 |
43 | | 音乐 | MV | 193 |
44 | | 音乐 | 音乐现场 | 29 |
45 | | 音乐 | 音乐综合 | 130 |
46 | | 音乐 | 乐评盘点 | 243 |
47 | | 音乐 | 音乐教学 | 244 |
48 | | 音乐 | ~~电音~~(已下线) | ~~194~~ |
49 |
50 | ### 舞蹈
51 | | 主分区名称 | 自分区名称 | tid 代号 |
52 | | --- | --- | --- |
53 | | 舞蹈 | 舞蹈(主分区) | 129 |
54 | | 舞蹈 | 宅舞 | 20 |
55 | | 舞蹈 | 舞蹈综合 | 154 |
56 | | 舞蹈 | 舞蹈教程 | 156 |
57 | | 舞蹈 | 街舞 | 198 |
58 | | 舞蹈 | 明星舞蹈 | 199 |
59 | | 舞蹈 | 国风舞蹈 | 200 |
60 | | 舞蹈 | 手势·网红舞 | 255 |
61 |
62 | ### 游戏
63 | | 主分区名称 | 自分区名称 | tid 代号 |
64 | | --- | --- | --- |
65 | | 游戏 | 游戏(主分区) | 4 |
66 | | 游戏 | 单机游戏 | 17 |
67 | | 游戏 | 电子竞技 | 171 |
68 | | 游戏 | 手机游戏 | 172 |
69 | | 游戏 | 网络游戏 | 65 |
70 | | 游戏 | 桌游棋牌 | 173 |
71 | | 游戏 | GMV | 121 |
72 | | 游戏 | 音游 | 136 |
73 | | 游戏 | Mugen | 19 |
74 |
75 | ### 知识
76 | | 主分区名称 | 自分区名称 | tid 代号 |
77 | | --- | --- | --- |
78 | | 知识 | 知识(主分区) | 36 |
79 | | 知识 | 科学科普 | 201 |
80 | | 知识 | 社科·法律·心理(~~原社科人文、原趣味科普人文~~) | 124 |
81 | | 知识 | 人文历史 | 228 |
82 | | 知识 | 财经商业 | 207 |
83 | | 知识 | 校园学习 | 208 |
84 | | 知识 | 职业职场 | 209 |
85 | | 知识 | 设计·创意 | 229 |
86 | | 知识 | 野生技术协会 | 122 |
87 | | 知识 | ~~演讲·公开课~~(已下线) | ~~39~~ |
88 | | 知识 | ~~星海~~(已下线) | ~~96~~ |
89 | | 知识 | ~~机械~~(已下线) | ~~98~~ |
90 |
91 | ### 科技
92 | | 主分区名称 | 自分区名称 | tid 代号 |
93 | | --- | --- | --- |
94 | | 科技 | 科技(主分区) | 188 |
95 | | 科技 | 数码(~~原手机平板~~) | 95 |
96 | | 科技 | 软件应用 | 230 |
97 | | 科技 | 计算机技术 | 231 |
98 | | 科技 | 科工机械 (~~原工业·工程·机械~~) | 232 |
99 | | 科技 | 极客DIY | 233 |
100 | | 科技 | ~~电脑装机~~(已下线) | ~~189~~ |
101 | | 科技 | ~~摄影摄像~~(已下线) | ~~190~~ |
102 | | 科技 | ~~影音智能~~(已下线) | ~~191~~ |
103 |
104 | ### 运动
105 | | 主分区名称 | 自分区名称 | tid 代号 |
106 | | --- | --- | --- |
107 | | 运动 | 运动(主分区) | 234 |
108 | | 运动 | 篮球 | 235 |
109 | | 运动 | 足球 | 249 |
110 | | 运动 | 健身 | 164 |
111 | | 运动 | 竞技体育 | 236 |
112 | | 运动 | 运动文化 | 237 |
113 | | 运动 | 运动综合 | 238 |
114 |
115 | ### 汽车
116 | | 主分区名称 | 自分区名称 | tid 代号 |
117 | | --- | --- | --- |
118 | | 汽车 | 汽车(主分区) | 223 |
119 | | 汽车 | 汽车知识科普 | 258 |
120 | | 汽车 | 赛车 | 245 |
121 | | 汽车 | 改装玩车 | 246 |
122 | | 汽车 | 新能源车 | 247 |
123 | | 汽车 | 房车 | 248 |
124 | | 汽车 | 摩托车 | 240 |
125 | | 汽车 | 购车攻略 | 227 |
126 | | 汽车 | 汽车生活 | 176 |
127 | | 汽车 | ~~汽车文化~~(已下线) | ~~224~~ |
128 | | 汽车 | ~~汽车极客~~(已下线) | ~~225~~ |
129 | | 汽车 | ~~智能出行~~(已下线) | ~~226~~ |
130 |
131 | ### 生活
132 | | 主分区名称 | 自分区名称 | tid 代号 |
133 | | --- | --- | --- |
134 | | 生活 | 生活(主分区) | 160 |
135 | | 生活 | 搞笑 | 138 |
136 | | 生活 | 出行 | 250 |
137 | | 生活 | 三农 | 251 |
138 | | 生活 | 家居房产 | 239 |
139 | | 生活 | 手工 | 161 |
140 | | 生活 | 绘画 | 162 |
141 | | 生活 | 日常 | 21 |
142 | | 生活 | 亲子 | 254 |
143 | | 生活 | ~~美食圈~~(重定向) | ~~76~~ |
144 | | 生活 | ~~动物圈~~(重定向) | ~~75~~ |
145 | | 生活 | ~~运动~~(重定向) | ~~163~~ |
146 | | 生活 | ~~汽车~~(重定向) | ~~176~~ |
147 | | 生活 | ~~其他~~(已下线) | ~~174~~ |
148 |
149 | ### 美食
150 | | 主分区名称 | 自分区名称 | tid 代号 |
151 | | --- | --- | --- |
152 | | 美食 | 美食(主分区) | 211 |
153 | | 美食 | 美食制作(~~原[生活]->[美食圈]~~) | 76 |
154 | | 美食 | 美食侦探 | 212 |
155 | | 美食 | 美食测评 | 213 |
156 | | 美食 | 田园美食 | 214 |
157 | | 美食 | 美食记录 | 215 |
158 |
159 | ### 动物圈
160 | | 主分区名称 | 自分区名称 | tid 代号 |
161 | | --- | --- | --- |
162 | | 动物圈 | 动物圈(主分区) | 217 |
163 | | 动物圈 | 喵星人 | 218 |
164 | | 动物圈 | 汪星人 | 219 |
165 | | 动物圈 | 动物二创 | 220 |
166 | | 动物圈 | 野生动物 | 221 |
167 | | 动物圈 | 小宠异宠 | 222 |
168 | | 动物圈 | 动物综合 | 75 |
169 |
170 | ### 鬼畜
171 | | 主分区名称 | 自分区名称 | tid 代号 |
172 | | --- | --- | --- |
173 | | 鬼畜 | 鬼畜(主分区) | 119 |
174 | | 鬼畜 | 鬼畜调教 | 22 |
175 | | 鬼畜 | 音MAD | 26 |
176 | | 鬼畜 | 人力VOCALOID | 126 |
177 | | 鬼畜 | 鬼畜剧场 | 216 |
178 | | 鬼畜 | 教程演示 | 127 |
179 |
180 | ### 时尚
181 | | 主分区名称 | 自分区名称 | tid 代号 |
182 | | --- | --- | --- |
183 | | 时尚 | 时尚(主分区) | 155 |
184 | | 时尚 | 美妆护肤 | 157 |
185 | | 时尚 | 仿妆cos | 252 |
186 | | 时尚 | 穿搭 | 158 |
187 | | 时尚 | 时尚潮流 | 159 |
188 | | 时尚 | ~~健身~~(重定向) | ~~164~~ |
189 | | 时尚 | ~~风尚标~~(已下线) | ~~192~~ |
190 |
191 | ### 资讯
192 | | 主分区名称 | 自分区名称 | tid 代号 |
193 | | --- | --- | --- |
194 | | 资讯 | 资讯(主分区) | 202 |
195 | | 资讯 | 热点 | 203 |
196 | | 资讯 | 环球 | 204 |
197 | | 资讯 | 社会 | 205 |
198 | | 资讯 | 综合 | 206 |
199 |
200 | ### 广告(已下线)
201 | | 主分区名称 | 自分区名称 | tid 代号 |
202 | | --- | --- | --- |
203 | | ~~广告~~(已下线) | ~~广告(主分区)~~ | ~~165~~ |
204 | | ~~广告~~(已下线) | ~~广告~~(已下线) | ~~166~~ |
205 |
206 | ### 娱乐
207 | | 主分区名称 | 自分区名称 | tid 代号 |
208 | | --- | --- | --- |
209 | | 娱乐 | 娱乐(主分区) | 5 |
210 | | 娱乐 | 综艺 | 71 |
211 | | 娱乐 | 娱乐杂谈 | 241 |
212 | | 娱乐 | 粉丝创作 | 242 |
213 | | 娱乐 | 明星综合 | 137 |
214 | | 娱乐 | ~~Korea相关~~(已下线) | ~~131~~ |
215 |
216 | ### 影视
217 | | 主分区名称 | 自分区名称 | tid 代号 |
218 | | --- | --- | --- |
219 | | 影视 | 影视(主分区) | 181 |
220 | | 影视 | 影视杂谈 | 182 |
221 | | 影视 | 影视剪辑 | 183 |
222 | | 影视 | 小剧场 | 85 |
223 | | 影视 | 预告·资讯 | 184 |
224 | | 影视 | 短片 | 256 |
225 |
226 | ### 纪录片
227 | | 主分区名称 | 自分区名称 | tid 代号 |
228 | | --- | --- | --- |
229 | | 纪录片 | 纪录片(主分区) | 177 |
230 | | 纪录片 | 人文·历史 | 37 |
231 | | 纪录片 | 科学·探索·自然 | 178 |
232 | | 纪录片 | 军事 | 179 |
233 | | 纪录片 | 社会·美食·旅行 | 180 |
234 |
235 | ### 电影
236 | | 主分区名称 | 自分区名称 | tid 代号 |
237 | | --- | --- | --- |
238 | | 电影 | 电影(主分区) | 23 |
239 | | 电影 | 华语电影 | 147 |
240 | | 电影 | 欧美电影 | 145 |
241 | | 电影 | 日本电影 | 146 |
242 | | 电影 | 其他国家 | 83 |
243 |
244 | ### 电视剧
245 | | 主分区名称 | 自分区名称 | tid 代号 |
246 | | --- | --- | --- |
247 | | 电视剧 | 电视剧(主分区) | 11 |
248 | | 电视剧 | 国产剧 | 185 |
249 | | 电视剧 | 海外剧 | 187 |
250 |
251 | ## Source
252 |
253 | [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/video_zone.md)
--------------------------------------------------------------------------------
/docs/upload.md:
--------------------------------------------------------------------------------
1 | ## 上传
2 |
3 | > 注意:上传功能需要先登录,登录后会记忆登录状态,下次上传时不需要再次登录。
4 |
5 | `bilitool upload -h ` 打印帮助信息:
6 |
7 | ```bash
8 | usage: bilitool upload [-h] [-y YAML] [--title TITLE] [--desc DESC] [--tid TID] [--tag TAG] [--source SOURCE] [--cover COVER]
9 | [--dynamic DYNAMIC]
10 | video_path
11 |
12 | positional arguments:
13 | video_path (required) the path to video file
14 |
15 | options:
16 | -h, --help show this help message and exit
17 | -y YAML, --yaml YAML The path to yaml file(if yaml file is provided, the arguments below will be ignored)
18 | --title TITLE (default is video name) The title of video
19 | --desc DESC (default is empty) The description of video
20 | --tid TID (default is 138) For more info to the type id, refer to https://biliup.github.io/tid-ref.html
21 | --tag TAG (default is bilitool) video tags, separated by comma
22 | --source SOURCE (default is 来源于网络) The source of video (if your video is re-print)
23 | --cover COVER (default is empty) The cover of video (if you want to customize, set it as the path to your cover image)
24 | --dynamic DYNAMIC (default is empty) The dynamic information
25 | ```
26 |
27 | 示例:
28 |
29 | 你可以参考 [`template/example-config.yaml`](https://github.com/timerring/bilitool/tree/main/template/example-config.yaml) 了解更多的 yaml 模板。
30 |
31 | ```bash
32 | # 视频路径是必需的
33 | bilitool upload /path/to/your/video.mp4
34 |
35 | # 使用命令行参数上传视频
36 | bilitool upload /path/to/your/video.mp4 --title "test" --desc "test" --tid 138 --tag "test"
37 |
38 | # 使用 yaml 配置上传视频
39 | bilitool upload /path/to/your/video.mp4 -y /path/to/your/upload/template.yaml
40 | ```
41 |
42 | 上传日志过程日志格式:
43 |
44 | ```
45 | [INFO] - [2025-01-15 20:43:40,489 bilitool] - The video name to be uploaded
46 | [INFO] - [2025-01-15 20:43:40,489 bilitool] - Start preuploading the video
47 | [INFO] - [2025-01-15 20:43:41,860 bilitool] - Completed preupload phase
48 | [INFO] - [2025-01-15 20:43:41,860 bilitool] - Start uploading the video
49 | [INFO] - [2025-01-15 20:43:42,007 bilitool] - Completed upload_id obtaining phase
50 | [INFO] - [2025-01-15 20:43:42,007 bilitool] - Uploading the video in 4 batches
51 | Uploading video: 100%|████████████████████████████████████████████████| 7.43M/7.43M [00:01<00:00, 4.80MB/s]
52 | [INFO] - [2025-01-15 20:43:44,305 bilitool] - [video name]upload success! bvid:BV1XXXXXXXX
53 |
54 | # 如果错误,则最后一条INFO会改为错误信息等
55 | [ERROR] - [2025-01-15 20:56:56,646 bilitool] - 标题只能包含中文、英文、数字,日文等可见符号
56 | ```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "cz-conventional-changelog": "^3.3.0",
4 | "mermaid": "^11.4.1",
5 | "vitepress": "^1.5.0",
6 | "vitepress-plugin-mermaid": "^2.0.17"
7 | },
8 | "config": {
9 | "commitizen": {
10 | "path": "./node_modules/cz-conventional-changelog"
11 | }
12 | },
13 | "scripts": {
14 | "docs:dev": "vitepress dev docs",
15 | "docs:build": "vitepress build docs",
16 | "docs:preview": "vitepress preview docs"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "bilitool" # make sure your module name is unique
7 | version = "0.1.3"
8 | authors = [
9 | { name="timerring"},
10 | ]
11 | description = "The Python toolkit package and cli designed for interaction with Bilibili"
12 | readme = "README.md"
13 | license = { file="LICENSE" }
14 | requires-python = ">=3.9"
15 | classifiers = [
16 | "Programming Language :: Python :: 3",
17 | "License :: OSI Approved :: MIT License",
18 | "Operating System :: OS Independent",
19 | ]
20 | dependencies = [
21 | "PyYAML>=6.0.2",
22 | "qrcode>=8.0",
23 | "Requests>=2.32.3",
24 | "tqdm>=4.67.1",
25 | "Pillow>=5.2.0"
26 | ]
27 |
28 | [project.scripts]
29 | bilitool = "bilitool.cli:cli"
30 |
31 | [project.urls]
32 | "Homepage" = "https://github.com/timerring/bilitool"
33 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | PyYAML==6.0.2
2 | qrcode==8.0
3 | Requests==2.32.3
4 | tqdm==4.67.1
5 | Pillow>=5.2.0
--------------------------------------------------------------------------------
/sha256sum.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ $# -ne 1 ]; then
4 | echo "Usage: $0 "
5 | exit 1
6 | fi
7 |
8 | directory=$1
9 |
10 | if [! -d "$directory" ]; then
11 | echo "Error: Directory $directory does not exist."
12 | exit 1
13 | fi
14 |
15 | files=()
16 |
17 | while IFS= read -r -d $'\0' file; do
18 | files+=("$file")
19 | done < <(find "$directory" -type f -print0)
20 |
21 | for file in "${files[@]}"; do
22 | result=$(shasum -a 256 $file)
23 | echo "$result" | tee "$file.sha256"
24 | done
25 |
--------------------------------------------------------------------------------
/template/example-config.yaml:
--------------------------------------------------------------------------------
1 | source: https://live.bilibili.com/xxxxxxxx # the source of video (required if your video is re-print)
2 | title: "your video title"
3 | desc: "your video description"
4 | tid: 138 # the type id of video
5 | tag: "your, video, tags" # the tags of video, separated by comma
6 | cover: '' # the cover of video (if you want to customize, set it as the path to your cover image)
7 | dynamic: '' # the dynamic information of video
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timerring/bilitool/aefaff618235d7f94db35213a1c938126b25cac7/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_feed.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | import unittest
4 | from bilitool.feed.bili_video_list import BiliVideoList
5 | from bilitool.feed.bili_live_list import BiliLiveList
6 |
7 |
8 | class TestBiliList(unittest.TestCase):
9 | def setUp(self):
10 | self.headers = {
11 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/63.0.3239.108"
12 | }
13 |
14 | def test_get_bili_video_list(self):
15 | bili = BiliVideoList()
16 | bili.print_video_list_info(bili.get_bili_video_list(50, "not_pubed"))
17 |
18 | def test_print_video_info_via_bvid(self):
19 | bili = BiliVideoList()
20 | bili.print_video_info_via_bvid("BV1pCr6YcEgD")
21 |
22 | def test_get_live_info(self):
23 | bili = BiliLiveList(self.headers)
24 | print(bili.get_live_info(25538755))
25 |
--------------------------------------------------------------------------------
/tests/test_upload.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | import unittest
4 | import logging
5 | from bilitool.upload.bili_upload import BiliUploader
6 |
7 |
8 | class TestBiliUploader(unittest.TestCase):
9 | def test_get_updated_video_info(self):
10 | logger = logging.getLogger("bilitool")
11 | bili = BiliUploader(logger)
12 | print(bili.get_updated_video_info("BVXXXXXXXXX"))
13 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 bilitool
2 |
3 | import unittest
4 | from bilitool.utils.get_ip_info import IPInfo
5 | from bilitool.utils.check_format import CheckFormat
6 |
7 |
8 | class TestIPInfo(unittest.TestCase):
9 | def test_get_ip_address(self):
10 | self.assertEqual(
11 | IPInfo.get_ip_address("12.12.12.12"),
12 | (
13 | "12.12.12.12",
14 | "att.com",
15 | "美国阿拉斯加州安克雷奇",
16 | "61.108841,-149.373145",
17 | ),
18 | )
19 |
20 |
21 | class TestCheckFormat(unittest.TestCase):
22 | def test_av2bv(self):
23 | check_format = CheckFormat()
24 | self.assertEqual(check_format.av2bv(2), "BV1xx411c7mD")
25 |
26 | def test_bv2av(self):
27 | check_format = CheckFormat()
28 | self.assertEqual(check_format.bv2av("BV1y7411Q7Eq"), 99999999)
29 |
--------------------------------------------------------------------------------