├── .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 | bilitool 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 | bilitool 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 | ![](https://cdn.jsdelivr.net/gh/timerring/scratchpad2023/2024/2025-01-08-11-54-34.png) 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 | --------------------------------------------------------------------------------