├── .github ├── RELEASE-TEMPLATE.md └── workflows │ ├── build.yml │ ├── deploy.yml │ ├── dev.yml │ ├── draft.yml │ ├── pre-release.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── arguments-builder.config.ts ├── biome.json ├── modules └── BiliBili.Redirect.V2.beta.sgmodule ├── package-lock.json ├── package.json ├── rspack.config.js ├── rspack.dev.config.js ├── src ├── assets │ ├── icon.png │ ├── icon_108x.png │ ├── icon_circled.png │ ├── icon_circled_108x.png │ ├── icon_rounded.png │ └── icon_rounded_108x.png ├── function │ ├── database.mjs │ └── setENV.mjs ├── request.dev.js ├── request.js ├── response.dev.js └── types.d.ts └── template ├── boxjs.settings.json ├── loon.handlebars ├── stash.handlebars └── surge.handlebars /.github/RELEASE-TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### 🆕 New Features 2 | * none 3 | 4 | ### 🛠️ Bug Fixes 5 | * none 6 | 7 | ### 🔣 Dependencies 8 | * none 9 | 10 | ### ‼️ Breaking Changes 11 | * none 12 | 13 | ### 🔄 Other Changes 14 | * none 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | workflow_call: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | actions: read 8 | contents: read 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@main 16 | with: 17 | submodules: recursive 18 | token: ${{ secrets.SUBMODULE_TOKEN }} 19 | - name: Set up Node.js 20 | uses: actions/setup-node@main 21 | with: 22 | node-version: 'latest' 23 | cache: 'npm' 24 | - name: Install dependencies 25 | run: npm install 26 | env: 27 | NODE_AUTH_TOKEN: ${{ secrets.PACKAGE_TOKEN }} 28 | - name: Update local package.json version from release tag 29 | if: github.ref_type == 'tag' 30 | uses: BellCubeDev/update-package-version-by-release-tag@v2 31 | with: 32 | version: ${{ github.ref_name }} 33 | keep-v: "false" # If set to "true", will not remove any 'v' prefix from the version number. 34 | ignore-semver-check: "false" # If set to "true", will not check if the version number is a valid semver version. 35 | - name: Build scripts 36 | run: npm run build 37 | - name: Generate modules 38 | run: npm run build:args 39 | - name: Upload artifact 40 | uses: actions/upload-artifact@master 41 | with: 42 | name: artifact 43 | path: | 44 | CHANGELOG.md 45 | rulesets 46 | dist 47 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | # Sequence of patterns matched against refs/heads 5 | branches: 6 | - dev 7 | 8 | permissions: 9 | actions: read 10 | contents: read 11 | 12 | jobs: 13 | dev: 14 | uses: ./.github/workflows/dev.yml 15 | secrets: inherit 16 | deploy: 17 | needs: dev 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Download artifact 21 | uses: actions/download-artifact@master 22 | with: 23 | name: artifact 24 | - name: Deploy Request 25 | uses: exuanbo/actions-deploy-gist@main 26 | with: 27 | token: ${{ secrets.GIST_TOKEN }} 28 | gist_id: 0c193c4ff4b930d765b1ca4b406ec1b9 29 | gist_description: "📺 BiliBili: 🔀 Redirect β" 30 | file_path: dist/request.bundle.js 31 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: Dev 2 | on: 3 | workflow_call: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | actions: read 8 | contents: read 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@main 16 | with: 17 | submodules: recursive 18 | token: ${{ secrets.SUBMODULE_TOKEN }} 19 | ref: dev 20 | - name: Set up Node.js 21 | uses: actions/setup-node@main 22 | with: 23 | node-version: 'latest' 24 | cache: 'npm' 25 | - name: Install dependencies 26 | run: npm install 27 | env: 28 | NODE_AUTH_TOKEN: ${{ secrets.PACKAGE_TOKEN }} 29 | - name: Build 30 | run: npm run build:dev 31 | - name: Upload artifact 32 | uses: actions/upload-artifact@master 33 | with: 34 | name: artifact 35 | path: | 36 | CHANGELOG.md 37 | rulesets 38 | dist 39 | -------------------------------------------------------------------------------- /.github/workflows/draft.yml: -------------------------------------------------------------------------------- 1 | name: Draft 2 | on: 3 | push: 4 | # Sequence of patterns matched against refs/heads 5 | branches: 6 | - main 7 | 8 | permissions: 9 | actions: read 10 | contents: write 11 | 12 | jobs: 13 | build: 14 | uses: ./.github/workflows/build.yml 15 | secrets: inherit 16 | draft: 17 | needs: build 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Download artifact 21 | uses: actions/download-artifact@master 22 | with: 23 | name: artifact 24 | - name: Publish Draft 25 | uses: softprops/action-gh-release@v2 26 | with: 27 | body_path: CHANGELOG.md 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | files: | 30 | rulesets/* 31 | dist/* 32 | draft: true 33 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: Pre-Release 2 | on: 3 | push: 4 | # Sequence of patterns matched against refs/tags 5 | tags: 6 | - v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+ 7 | - v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+ 8 | 9 | permissions: 10 | actions: read 11 | contents: write 12 | 13 | jobs: 14 | build: 15 | uses: ./.github/workflows/build.yml 16 | secrets: inherit 17 | pre-release: 18 | needs: build 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Download artifact 22 | uses: actions/download-artifact@master 23 | with: 24 | name: artifact 25 | - name: Publish Pre-Release 26 | uses: softprops/action-gh-release@v2 27 | with: 28 | body_path: CHANGELOG.md 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | files: | 31 | rulesets/* 32 | dist/* 33 | prerelease: true 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | # Sequence of patterns matched against refs/tags 5 | tags: 6 | - v[0-9]+.[0-9]+.[0-9]+ 7 | 8 | permissions: 9 | actions: read 10 | contents: write 11 | 12 | jobs: 13 | build: 14 | uses: ./.github/workflows/build.yml 15 | secrets: inherit 16 | release: 17 | needs: build 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Download artifact 21 | uses: actions/download-artifact@master 22 | with: 23 | name: artifact 24 | - name: Publish Release 25 | uses: softprops/action-gh-release@v2 26 | with: 27 | body_path: CHANGELOG.md 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | files: | 30 | rulesets/* 31 | dist/* 32 | make_latest: "true" 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional stylelint cache 62 | .stylelintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variable files 80 | .env 81 | .env.development.local 82 | .env.test.local 83 | .env.production.local 84 | .env.local 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | .parcel-cache 89 | 90 | # Next.js build output 91 | .next 92 | out 93 | 94 | # Nuxt.js build / generate output 95 | .nuxt 96 | dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | ### Node Patch ### 136 | # Serverless Webpack directories 137 | .webpack/ 138 | 139 | # Optional stylelint cache 140 | 141 | # SvelteKit build / generate output 142 | .svelte-kit 143 | 144 | # End of https://www.toptal.com/developers/gitignore/api/node 145 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/protobuf"] 2 | path = src/protobuf 3 | url = https://github.com/BiliUniverse/protobuf.git 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @nsnanocat:registry=https://npm.pkg.github.com 2 | //npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN} 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 🛠️ Bug Fixes 2 | * 修复`MCDN 重定向`功能错误覆盖`缓存与下载资源`的问题 3 | * `缓存与下载资源`无法重定向,所以已被排除在脚本作用范围外 4 | * `MCDN 重定向`功能现在会跳过带有 `originalUrl` 参数的`静态资源`请求 5 | * 修复 `MCDN 重定向` 脚本作用范围,现在可以正确覆盖 IPv6 地址的 MCDN 连接了 6 | * 修复 BiliBili MCDN 请求头不规范,导致端口号解析错误的问题 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🪐 BiliUniverse: 🔀 Redirect 2 | 自动化重定向 CDN,让播放更流畅 3 | -------------------------------------------------------------------------------- /arguments-builder.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@iringo/arguments-builder"; 2 | 3 | export default defineConfig({ 4 | output: { 5 | surge: { 6 | path: "./dist/BiliBili.Redirect.sgmodule", 7 | transformEgern: { 8 | enable: true, 9 | path: "./dist/BiliBili.Redirect.yaml", 10 | }, 11 | }, 12 | loon: { 13 | path: "./dist/BiliBili.Redirect.plugin", 14 | }, 15 | customItems: [ 16 | { 17 | path: "./dist/BiliBili.Redirect.stoverride", 18 | template: "./template/stash.handlebars", 19 | }, 20 | ], 21 | dts: { 22 | isExported: true, 23 | path: "./src/types.d.ts", 24 | }, 25 | boxjsSettings: { 26 | path: "./template/boxjs.settings.json", 27 | scope: "@BiliBili.Redirect.Settings", 28 | }, 29 | }, 30 | args: [ 31 | { 32 | key: "Host.Akamaized", 33 | name: "[主机名] 重定向 Akamaized CDN (港澳台)", 34 | defaultValue: "upos-sz-mirrorali.bilivideo.com", 35 | type: "string", 36 | boxJsType: "selects", 37 | description: "请选择 Akamaized 要重定向的主机名。", 38 | options: [ 39 | { 40 | "key": "upos-sz-mirrorali.bilivideo.com", 41 | "label": "阿里云 CDN" 42 | }, 43 | { 44 | "key": "upos-sz-mirrorcos.bilivideo.com", 45 | "label": "腾讯云 CDN" 46 | }, 47 | { 48 | "key": "upos-sz-mirrorhw.bilivideo.com", 49 | "label": "华为云 CDN,融合 CDN" 50 | }, 51 | { 52 | "key": "upos-sz-mirroraliov.bilivideo.com", 53 | "label": "阿里云 CDN,海外" 54 | }, 55 | { 56 | "key": "upos-sz-mirrorcosov.bilivideo.com", 57 | "label": "腾讯云 CDN,海外" 58 | }, 59 | { 60 | "key": "upos-sz-mirrorhwov.bilivideo.com", 61 | "label": "华为云 CDN,海外" 62 | } 63 | ], 64 | }, 65 | { 66 | key: "Host.BStar", 67 | name: "[主机名] 重定向 BStar CDN (国际版)", 68 | defaultValue: "upos-sz-mirrorali.bilivideo.com", 69 | type: "string", 70 | boxJsType: "selects", 71 | description: "请选择 BStar 要重定向的主机名。", 72 | options: [ 73 | { 74 | "key": "upos-sz-mirrorali.bilivideo.com", 75 | "label": "阿里云 CDN" 76 | }, 77 | { 78 | "key": "upos-sz-mirrorcos.bilivideo.com", 79 | "label": "腾讯云 CDN" 80 | }, 81 | { 82 | "key": "upos-sz-mirrorhw.bilivideo.com", 83 | "label": "华为云 CDN,融合 CDN" 84 | }, 85 | { 86 | "key": "upos-sz-mirroraliov.bilivideo.com", 87 | "label": "阿里云 CDN,海外" 88 | }, 89 | { 90 | "key": "upos-sz-mirrorcosov.bilivideo.com", 91 | "label": "腾讯云 CDN,海外" 92 | }, 93 | { 94 | "key": "upos-sz-mirrorhwov.bilivideo.com", 95 | "label": "华为云 CDN,海外" 96 | } 97 | ], 98 | }, 99 | { 100 | key: "Host.PCDN", 101 | name: "[主机名] 重定向 PCDN 主机名 (中国大陆)", 102 | defaultValue: "upos-sz-mirrorali.bilivideo.com", 103 | type: "string", 104 | boxJsType: "selects", 105 | description: "请选择 PCDN 要重定向的主机名。", 106 | options: [ 107 | { 108 | "key": "upos-sz-mirrorali.bilivideo.com", 109 | "label": "阿里云 CDN" 110 | }, 111 | { 112 | "key": "upos-sz-mirrorcos.bilivideo.com", 113 | "label": "腾讯云 CDN" 114 | }, 115 | { 116 | "key": "upos-sz-mirrorhw.bilivideo.com", 117 | "label": "华为云 CDN,融合 CDN" 118 | }, 119 | { 120 | "key": "upos-sz-mirroraliov.bilivideo.com", 121 | "label": "阿里云 CDN,海外" 122 | }, 123 | { 124 | "key": "upos-sz-mirrorcosov.bilivideo.com", 125 | "label": "腾讯云 CDN,海外" 126 | }, 127 | { 128 | "key": "upos-sz-mirrorhwov.bilivideo.com", 129 | "label": "华为云 CDN,海外" 130 | } 131 | ], 132 | }, 133 | { 134 | key: "Host.MCDN", 135 | name: "[主机名] 重定向 MCDN 主机名 (中国大陆)", 136 | defaultValue: "proxy-tf-all-ws.bilivideo.com", 137 | type: "string", 138 | boxJsType: "selects", 139 | description: "请选择 MCDN 要重定向的主机名。", 140 | options: [ 141 | { 142 | "key": "proxy-tf-all-ws.bilivideo.com", 143 | "label": "proxy-tf-all-ws.bilivideo.com" 144 | } 145 | ], 146 | }, 147 | { 148 | key: "LogLevel", 149 | name: "[调试] 日志等级", 150 | type: "string", 151 | defaultValue: "WARN", 152 | description: "选择脚本日志的输出等级,低于所选等级的日志将全部输出。", 153 | options: [ 154 | { key: "OFF", label: "关闭" }, 155 | { key: "ERROR", label: "❌ 错误" }, 156 | { key: "WARN", label: "⚠️ 警告" }, 157 | { key: "INFO", label: "ℹ️ 信息" }, 158 | { key: "DEBUG", label: "🅱️ 调试" }, 159 | { key: "ALL", label: "全部" }, 160 | ], 161 | }, 162 | ], 163 | }); 164 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "files": { 4 | "ignore": [ 5 | "**/*.bundle.js" 6 | ], 7 | "ignoreUnknown": false 8 | }, 9 | "formatter": { 10 | "enabled": true, 11 | "indentStyle": "tab", 12 | "indentWidth": 2, 13 | "lineEnding": "lf", 14 | "lineWidth": 320 15 | }, 16 | "javascript": { 17 | "formatter": { 18 | "arrowParentheses": "asNeeded", 19 | "bracketSameLine": true, 20 | "quoteStyle": "double" 21 | } 22 | }, 23 | "json": { 24 | "parser": { 25 | "allowComments": true, 26 | "allowTrailingCommas": true 27 | } 28 | }, 29 | "linter": { 30 | "enabled": true, 31 | "rules": { 32 | "complexity": { 33 | "noForEach": "off", 34 | "noStaticOnlyClass": "off", 35 | "noUselessSwitchCase": "off", 36 | "useArrowFunction": "info", 37 | "useFlatMap": "off", 38 | "useLiteralKeys": "info" 39 | }, 40 | "correctness": { 41 | "noInnerDeclarations": "info", 42 | "noSelfAssign": "off", 43 | "noSwitchDeclarations": "info", 44 | "noUnsafeOptionalChaining": "info" 45 | }, 46 | "performance": { 47 | "noDelete": "info" 48 | }, 49 | "recommended": true, 50 | "style": { 51 | "noNegationElse": "off", 52 | "noParameterAssign": "off", 53 | "noUselessElse": "off", 54 | "noVar": "info", 55 | "useDefaultParameterLast": "info", 56 | "useForOf": "error", 57 | "useNodejsImportProtocol": "error", 58 | "useNumberNamespace": "error", 59 | "useSingleVarDeclarator": "off" 60 | }, 61 | "suspicious": { 62 | "noAssignInExpressions": "info", 63 | "noDoubleEquals": "info", 64 | "noFallthroughSwitchClause": "info", 65 | "noGlobalIsNan": "off", 66 | "useDefaultSwitchClauseLast": "off" 67 | } 68 | } 69 | }, 70 | "organizeImports": { 71 | "enabled": true 72 | }, 73 | "vcs": { 74 | "clientKind": "git", 75 | "enabled": true, 76 | "useIgnoreFile": true 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /modules/BiliBili.Redirect.V2.beta.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=📺 BiliBili: 🔀 Redirect β 2 | #!desc=(BETA)\n哔哩哔哩:重定向(自定义 CDN) 3 | #!openUrl=http://boxjs.com/#/app/BiliBili.Redirect 4 | #!author=VirgilClyne 5 | #!homepage=https://redirect.biliuniverse.io 6 | #!manual=https://manual.biliuniverse.io 7 | #!icon=https://github.com/BiliUniverse/Redirect/raw/main/src/assets/icon_rounded_108x.png 8 | #!category=📺 BiliUniverse 9 | #!arguments=Host.Akamaized:upos-sz-mirrorali.bilivideo.com,Host.BStar:upos-sz-mirrorali.bilivideo.com,Host.PCDN:upos-sz-mirrorali.bilivideo.com 10 | #!arguments-desc=Host.Akamaized: 重定向港澳台 CDN 主机名\nHost.BStar: 重定向国际版 CDN 主机名\nHost.PCDN: 重定向 PCDN 主机名 11 | 12 | [Script] 13 | BiliBili.Global.playerunite.v1.Player.grpc = type=http-response, pattern=^https?:\/\/(grpc\.biliapi\.net|app\.bilibili\.com)\/bilibili\.app\.playerunite\.v1\.Player\/PlayViewUnite$, requires-body=1, binary-body-mode=1, engine=webview, debug=1, script-path=https://raw.githubusercontent.com/BiliUniverse/Redirect/beta/js/BiliBili.Redirect.response.beta.js, argument= 14 | BiliBili.Global.player.v2.PlayURL.grpc = type=http-response, pattern=^https?:\/\/(grpc\.biliapi\.net|app\.bilibili\.com)\/bilibili\.pgc\.gateway\.player\.v2\.PlayURL\/PlayView$, requires-body=1, binary-body-mode=1, engine=webview, debug=1, script-path=https://raw.githubusercontent.com/BiliUniverse/Redirect/beta/js/BiliBili.Redirect.response.beta.js, argument= 15 | BiliBili.Global.player.playurl.json = type=http-response, pattern=^https?:\/\/api\.bili(bili\.com|api\.net)\/pgc\/player\/(api|web)\/playurl(\/html5)?\?, requires-body=1, engine=webview, debug=1, script-path=https://raw.githubusercontent.com/BiliUniverse/Redirect/beta/js/BiliBili.Redirect.response.beta.js, argument= 16 | BiliBili.Global.player.v2.playurl.json = type=http-response, pattern=^https?:\/\/api\.bili(bili\.com|api\.net)\/pgc\/player\/web\/v2\/playurl\?, requires-body=1, engine=webview, debug=1, script-path=https://raw.githubusercontent.com/BiliUniverse/Redirect/beta/js/BiliBili.Redirect.response.beta.js, argument= 17 | 18 | [MITM] 19 | hostname = %APPEND% app.bilibili.com, app.biliapi.net, api.bilibili.com, api.biliapi.net, grpc.biliapi.net 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@biliuniverse/redirect", 3 | "organizationName": "🪐 BiliUniverse", 4 | "displayName": "📺 BiliBili: 🔀 Redirect", 5 | "description": "哔哩哔哩:重定向\n中国站CDN自定义", 6 | "homepage": "https://Redirect.BiliUniverse.io", 7 | "openUrl": "http://boxjs.com/#/app/BiliBili.Redirect", 8 | "icon": "https://github.com/BiliUniverse/Redirect/raw/main/src/assets/icon_rounded.png", 9 | "keywords": [], 10 | "contributors": [ 11 | "VirgilClyne[https://github.com/VirgilClyne]" 12 | ], 13 | "system": [ 14 | "iOS", 15 | "iPadOS", 16 | "macOS", 17 | "tvOS" 18 | ], 19 | "systemVersion": 15, 20 | "license": "Apache-2.0", 21 | "type": "module", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/BiliUniverse/Redirect.git" 25 | }, 26 | "directories": { 27 | "example": "example" 28 | }, 29 | "scripts": { 30 | "serve": "webpack serve", 31 | "build": "rspack build", 32 | "build:dev": "rspack build -c rspack.dev.config.js --mode=development", 33 | "build:args": "arguments-builder build", 34 | "dts": "arguments-builder dts" 35 | }, 36 | "browserslist": [ 37 | "iOS >= 15" 38 | ], 39 | "devDependencies": { 40 | "@iringo/arguments-builder": "^1.8.5", 41 | "@protobuf-ts/runtime": "^2.9.4", 42 | "@rspack/cli": "^1.0.14", 43 | "@rspack/core": "^1.0.14", 44 | "node-polyfill-webpack-plugin": "^4.0.0" 45 | }, 46 | "dependencies": { 47 | "@nsnanocat/url": "^1.2.4", 48 | "@nsnanocat/util": "^1.7.6" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rspack.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@rspack/cli"; 2 | import rspack from "@rspack/core"; 3 | import NodePolyfillPlugin from "node-polyfill-webpack-plugin"; 4 | import pkg from "./package.json" with { type: "json" }; 5 | 6 | export default defineConfig({ 7 | entry: { 8 | "request": "./src/request.js", 9 | }, 10 | output: { 11 | filename: "[name].bundle.js", 12 | }, 13 | plugins: [ 14 | new NodePolyfillPlugin({ 15 | //additionalAliases: ['console'], 16 | }), 17 | new rspack.BannerPlugin({ 18 | banner: `console.log('Date: ${new Date().toLocaleString('zh-CN', {timeZone: 'PRC'})}');`, 19 | raw: true, 20 | }), 21 | new rspack.BannerPlugin({ 22 | banner: `console.log('Version: ${pkg.version}');`, 23 | raw: true, 24 | }), 25 | new rspack.BannerPlugin({ 26 | banner: "console.log('[file]');", 27 | raw: true, 28 | }), 29 | new rspack.BannerPlugin({ 30 | banner: `console.log('${pkg.displayName}');`, 31 | raw: true, 32 | }), 33 | new rspack.BannerPlugin({ 34 | banner: pkg.homepage, 35 | }), 36 | ], 37 | devtool: false, 38 | performance: false, 39 | }); 40 | -------------------------------------------------------------------------------- /rspack.dev.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@rspack/cli"; 2 | import rspack from "@rspack/core"; 3 | import NodePolyfillPlugin from "node-polyfill-webpack-plugin"; 4 | import pkg from "./package.json" with { type: "json" }; 5 | 6 | export default defineConfig({ 7 | entry: { 8 | "request": "./src/request.dev.js", 9 | "response": "./src/response.dev.js", 10 | }, 11 | output: { 12 | filename: "[name].bundle.js", 13 | }, 14 | plugins: [ 15 | new NodePolyfillPlugin({ 16 | //additionalAliases: ['console'], 17 | }), 18 | new rspack.BannerPlugin({ 19 | banner: `console.log('Date: ${new Date().toLocaleString('zh-CN', {timeZone: 'PRC'})}');`, 20 | raw: true, 21 | }), 22 | new rspack.BannerPlugin({ 23 | banner: `console.log('Version: ${pkg.version}');`, 24 | raw: true, 25 | }), 26 | new rspack.BannerPlugin({ 27 | banner: "console.log('[file]');", 28 | raw: true, 29 | }), 30 | new rspack.BannerPlugin({ 31 | banner: `console.log('${pkg.displayName} β');`, 32 | raw: true, 33 | }), 34 | new rspack.BannerPlugin({ 35 | banner: pkg.homepage, 36 | }), 37 | ], 38 | devtool: false, 39 | performance: false, 40 | }); 41 | -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BiliUniverse/Redirect/a5cca6f1e2b6cafbc1192649fb7c20cb474c323e/src/assets/icon.png -------------------------------------------------------------------------------- /src/assets/icon_108x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BiliUniverse/Redirect/a5cca6f1e2b6cafbc1192649fb7c20cb474c323e/src/assets/icon_108x.png -------------------------------------------------------------------------------- /src/assets/icon_circled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BiliUniverse/Redirect/a5cca6f1e2b6cafbc1192649fb7c20cb474c323e/src/assets/icon_circled.png -------------------------------------------------------------------------------- /src/assets/icon_circled_108x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BiliUniverse/Redirect/a5cca6f1e2b6cafbc1192649fb7c20cb474c323e/src/assets/icon_circled_108x.png -------------------------------------------------------------------------------- /src/assets/icon_rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BiliUniverse/Redirect/a5cca6f1e2b6cafbc1192649fb7c20cb474c323e/src/assets/icon_rounded.png -------------------------------------------------------------------------------- /src/assets/icon_rounded_108x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BiliUniverse/Redirect/a5cca6f1e2b6cafbc1192649fb7c20cb474c323e/src/assets/icon_rounded_108x.png -------------------------------------------------------------------------------- /src/function/database.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | Redirect: { 3 | Settings: { 4 | Host: { 5 | Akamaized: "upos-sz-mirrorali.bilivideo.com", 6 | BStar: "upos-sz-mirrorali.bilivideo.com", 7 | PCDN: "upos-sz-mirrorali.bilivideo.com", 8 | MCDN: "proxy-tf-all-ws.bilivideo.com", 9 | }, 10 | }, 11 | }, 12 | Default: { 13 | Settings: { 14 | LogLevel: "WARN", 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/function/setENV.mjs: -------------------------------------------------------------------------------- 1 | import { Console, getStorage, Lodash as _ } from "@nsnanocat/util"; 2 | 3 | /** 4 | * Set Environment Variables 5 | * @author VirgilClyne 6 | * @param {String} name - Persistent Store Key 7 | * @param {Array} platforms - Platform Names 8 | * @param {Object} database - Default DataBase 9 | * @return {Object} { Settings, Caches, Configs } 10 | */ 11 | export default function setENV(name, platforms, database) { 12 | Console.log("☑️ Set Environment Variables"); 13 | const { Settings, Caches, Configs } = getStorage(name, platforms, database); 14 | /***************** Settings *****************/ 15 | Console.info(`typeof Settings: ${typeof Settings}`, `Settings: ${JSON.stringify(Settings, null, 2)}`); 16 | /***************** Caches *****************/ 17 | //Console.debug(`typeof Caches: ${typeof Caches}`, `Caches: ${JSON.stringify(Caches, null, 2)}`); 18 | /***************** Configs *****************/ 19 | Console.log("✅ Set Environment Variables"); 20 | return { Settings, Caches, Configs }; 21 | } 22 | -------------------------------------------------------------------------------- /src/request.dev.js: -------------------------------------------------------------------------------- 1 | import { $app, Console, done, gRPC, Lodash as _ } from "@nsnanocat/util"; 2 | import { URL } from "@nsnanocat/url"; 3 | import database from "./function/database.mjs"; 4 | import setENV from "./function/setENV.mjs"; 5 | // 构造回复数据 6 | // biome-ignore lint/style/useConst: 7 | let $response = undefined; 8 | /***************** Processing *****************/ 9 | // 解构URL 10 | const url = new URL($request.url); 11 | Console.info(`url: ${url.toJSON()}`); 12 | // 获取连接参数 13 | const PATHs = url.pathname.split("/").filter(Boolean); 14 | Console.info(`PATHs: ${PATHs}`); 15 | // 解析格式 16 | const FORMAT = ($request.headers?.["Content-Type"] ?? $request.headers?.["content-type"])?.split(";")?.[0]; 17 | Console.info(`FORMAT: ${FORMAT}`); 18 | (async () => { 19 | /** 20 | * 设置 21 | * @type {{Settings: import('./types').Settings}} 22 | */ 23 | const { Settings, Caches, Configs } = setENV("BiliBili", "Redirect", database); 24 | Console.logLevel = Settings.LogLevel; 25 | // 创建空数据 26 | const body = {}; 27 | // 方法判断 28 | switch ($request.method) { 29 | case "POST": 30 | case "PUT": 31 | case "PATCH": 32 | // biome-ignore lint/suspicious/noFallthroughSwitchClause: 33 | case "DELETE": 34 | // 格式判断 35 | switch (FORMAT) { 36 | case undefined: // 视为无body 37 | break; 38 | case "application/x-www-form-urlencoded": 39 | case "text/plain": 40 | default: 41 | break; 42 | case "application/x-mpegURL": 43 | case "application/x-mpegurl": 44 | case "application/vnd.apple.mpegurl": 45 | case "audio/mpegurl": 46 | //body = M3U8.parse($request.body); 47 | //Console.debug(`body: ${JSON.stringify(body)}`); 48 | //$request.body = M3U8.stringify(body); 49 | break; 50 | case "text/xml": 51 | case "text/html": 52 | case "text/plist": 53 | case "application/xml": 54 | case "application/plist": 55 | case "application/x-plist": 56 | //body = XML.parse($request.body); 57 | //Console.debug(`body: ${JSON.stringify(body)}`); 58 | //$request.body = XML.stringify(body); 59 | break; 60 | case "text/vtt": 61 | case "application/vtt": 62 | //body = VTT.parse($request.body); 63 | //Console.debug(`body: ${JSON.stringify(body)}`); 64 | //$request.body = VTT.stringify(body); 65 | break; 66 | case "text/json": 67 | case "application/json": 68 | //body = JSON.parse($request.body ?? "{}"); 69 | //Console.debug(`body: ${JSON.stringify(body)}`); 70 | //$request.body = JSON.stringify(body); 71 | break; 72 | case "application/protobuf": 73 | case "application/x-protobuf": 74 | case "application/vnd.google.protobuf": 75 | case "application/grpc": 76 | case "application/grpc+proto": 77 | case "application/vnd.apple.flatbuffer": 78 | case "application/octet-stream": { 79 | //Console.debug(`$request.body: ${JSON.stringify($request.body)}`); 80 | let rawBody = $app === "Quantumult X" ? new Uint8Array($request.bodyBytes ?? []) : ($request.body ?? new Uint8Array()); 81 | //Console.debug(`isBuffer? ${ArrayBuffer.isView(rawBody)}: ${JSON.stringify(rawBody)}`); 82 | switch (FORMAT) { 83 | case "application/protobuf": 84 | case "application/x-protobuf": 85 | case "application/vnd.google.protobuf": 86 | break; 87 | case "application/grpc": 88 | case "application/grpc+proto": 89 | rawBody = gRPC.decode(rawBody); 90 | rawBody = gRPC.encode(rawBody); 91 | break; 92 | } 93 | // 写入二进制数据 94 | $request.body = rawBody; 95 | break; 96 | } 97 | } 98 | //break; // 不中断,继续处理URL 99 | case "GET": 100 | case "HEAD": 101 | case "OPTIONS": 102 | default: 103 | // 主机判断 104 | switch (url.hostname) { 105 | case "upos-sz-mirrorali.bilivideo.com": // 阿里云 CDN 106 | case "upos-sz-mirroralib.bilivideo.com": // 阿里云 CDN 107 | case "upos-sz-mirroralio1.bilivideo.com": // 阿里云 CDN 108 | case "upos-sz-mirrorcos.bilivideo.com": // 腾讯云 CDN 109 | case "upos-sz-mirrorcosb.bilivideo.com": // 腾讯云 CDN,VOD 加速类型 110 | case "upos-sz-mirrorcoso1.bilivideo.com": // 腾讯云 CDN 111 | case "upos-sz-mirrorhw.bilivideo.com": // 华为云 CDN,融合 CDN 112 | case "upos-sz-mirrorhwb.bilivideo.com": // 华为云 CDN,融合 CDN 113 | case "upos-sz-mirrorhwo1.bilivideo.com": // 华为云 CDN,融合 CDN 114 | case "upos-sz-mirror08c.bilivideo.com": // 华为云 CDN,融合 CDN 115 | case "upos-sz-mirror08h.bilivideo.com": // 华为云 CDN,融合 CDN 116 | case "upos-sz-mirror08ct.bilivideo.com": // 华为云 CDN,融合 CDN 117 | case "upos-sz-mirroraliov.bilivideo.com": // 阿里云 CDN,海外 118 | case "upos-sz-mirrorcosov.bilivideo.com": // 腾讯云 CDN,海外 119 | case "upos-sz-mirrorhwov.bilivideo.com": // 华为云 CDN,海外 120 | break; 121 | case "upos-hz-mirrorakam.akamaized.net": // Akamai CDN,海外,有参数校验,其他类型的 CDN 不能直接替换为此 Host。但反过来可以。 122 | url.hostname = Settings.Host.Akamaized; 123 | break; 124 | case "upos-sz-mirroralibstar1.bilivideo.com": // 阿里云 CDN,海外(东南亚),其他类型的 CDN 应该不能替换为此 Host,但反过来可以。 125 | case "upos-sz-mirrorcosbstar1.bilivideo.com": // 腾讯云 CDN,海外(东南亚),其他类型的 CDN 应该不能替换为此 Host,但反过来可以。 126 | case "upos-sz-mirrorhwbstar1.bilivideo.com": // 华为云 CDN,海外(东南亚),其他类型的 CDN 应该不能替换为此 Host,但反过来可以。 127 | case "upos-bstar1-mirrorakam.akamaized.net": // Akamai CDN,海外(东南亚),有参数校验,其他类型的 CDN 不能直接替换为此 Host。但反过来可以。 128 | url.hostname = Settings.Host.BStar; 129 | break; 130 | default: 131 | switch (url.port) { 132 | case "": { 133 | switch (true) { 134 | case url.hostname.endsWith(".mcdn.bilivideo.cn"): 135 | switch (true) { 136 | case url.pathname.startsWith("/v1/resource/"): 137 | switch (url.protocol) { 138 | case "http:": 139 | url.port = "8000"; 140 | break; 141 | case "https:": 142 | url.port = "8082"; 143 | break; 144 | } 145 | break; 146 | case url.pathname.startsWith("/upgcxcode/"): 147 | switch (url.protocol) { 148 | case "http:": 149 | url.port = "9102"; 150 | break; 151 | case "https:": 152 | url.port = "4483"; 153 | break; 154 | } 155 | break; 156 | } 157 | break; 158 | } 159 | break; 160 | } 161 | case "486": { 162 | // MCDN 163 | const cdn = url.searchParams.get("cdn"); 164 | const sid = url.searchParams.get("sid"); 165 | if (cdn) { 166 | url.hostname = `d1--${cdn}.bilivideo.com`; 167 | url.port = ""; 168 | } else if (sid) { 169 | url.hostname = `${sid}.bilivideo.com`; 170 | url.port = ""; 171 | } 172 | break; 173 | } 174 | case "4480": // PCDN 175 | url.protocol = "http:"; 176 | url.hostname = url.searchParams.get("xy_usource") || Settings.Host.PCDN; 177 | url.port = ""; 178 | break; 179 | case "8000": // MCDN.v1.resource 180 | case "8082": // MCDN.v1.resource 181 | // 不可修改 182 | //url.protocol = "http"; 183 | //url.hostname = Settings.Host.CDN; 184 | //url.port = ""; 185 | break; 186 | case "4483": // MCDN.upgcxcode 187 | case "9102": // MCDN.upgcxcode 188 | if (url.searchParams.has("originalUrl")) break; // 跳过 MCDN 重定向 189 | url.protocol = "http:"; 190 | url.hostname = Settings.Host.MCDN; 191 | url.port = ""; 192 | url.pathname = ""; 193 | for (const key of url.searchParams.keys()) url.searchParams.delete(key); 194 | url.searchParams.set("url", $request.url); 195 | break; 196 | case "9305": // PCDN 197 | url.protocol = "http:"; 198 | url.hostname = url.PATHs.shift(); 199 | url.port = ""; 200 | url.pathname = url.PATHs.join("/"); 201 | break; 202 | } 203 | break; 204 | } 205 | break; 206 | case "CONNECT": 207 | case "TRACE": 208 | break; 209 | } 210 | if ($request.headers?.Host) $request.headers.Host = url.host; 211 | if ($request.headers?.[":authority"]) $request.headers[":authority"] = url.host; 212 | $request.url = url.toString(); 213 | Console.debug(`$request.url: ${$request.url}`); 214 | })() 215 | .catch(e => Console.error(e)) 216 | .finally(() => { 217 | switch (typeof $response) { 218 | case "object": // 有构造回复数据,返回构造的回复数据 219 | //Console.debug("finally", `echo $response: ${JSON.stringify($response, null, 2)}`); 220 | if ($response.headers?.["Content-Encoding"]) $response.headers["Content-Encoding"] = "identity"; 221 | if ($response.headers?.["content-encoding"]) $response.headers["content-encoding"] = "identity"; 222 | switch ($app) { 223 | default: 224 | done({ response: $response }); 225 | break; 226 | case "Quantumult X": 227 | if (!$response.status) $response.status = "HTTP/1.1 200 OK"; 228 | delete $response.headers?.["Content-Length"]; 229 | delete $response.headers?.["content-length"]; 230 | delete $response.headers?.["Transfer-Encoding"]; 231 | done($response); 232 | break; 233 | } 234 | break; 235 | case "undefined": // 无构造回复数据,发送修改的请求数据 236 | //Console.debug("finally", `$request: ${JSON.stringify($request, null, 2)}`); 237 | done($request); 238 | break; 239 | default: 240 | Console.error(`不合法的 $response 类型: ${typeof $response}`); 241 | done(); 242 | break; 243 | } 244 | }); 245 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | import { $app, Console, done, Lodash as _ } from "@nsnanocat/util"; 2 | import { URL } from "@nsnanocat/url"; 3 | import database from "./function/database.mjs"; 4 | import setENV from "./function/setENV.mjs"; 5 | // 构造回复数据 6 | // biome-ignore lint/style/useConst: 7 | let $response = undefined; 8 | /***************** Processing *****************/ 9 | // 解构URL 10 | const url = new URL($request.url); 11 | Console.info(`url: ${url.toJSON()}`); 12 | // 获取连接参数 13 | const PATHs = url.pathname.split("/").filter(Boolean); 14 | Console.info(`PATHs: ${PATHs}`); 15 | // 解析格式 16 | const FORMAT = ($request.headers?.["Content-Type"] ?? $request.headers?.["content-type"])?.split(";")?.[0]; 17 | Console.info(`FORMAT: ${FORMAT}`); 18 | (async () => { 19 | /** 20 | * 设置 21 | * @type {{Settings: import('./types').Settings}} 22 | */ 23 | const { Settings, Caches, Configs } = setENV("BiliBili", "Redirect", database); 24 | Console.logLevel = Settings.LogLevel; 25 | // 创建空数据 26 | const body = {}; 27 | // 方法判断 28 | switch ($request.method) { 29 | case "POST": 30 | case "PUT": 31 | case "PATCH": 32 | // biome-ignore lint/suspicious/noFallthroughSwitchClause: 33 | case "DELETE": 34 | // 格式判断 35 | switch (FORMAT) { 36 | case undefined: // 视为无body 37 | break; 38 | case "application/x-www-form-urlencoded": 39 | case "text/plain": 40 | default: 41 | break; 42 | case "application/x-mpegURL": 43 | case "application/x-mpegurl": 44 | case "application/vnd.apple.mpegurl": 45 | case "audio/mpegurl": 46 | //body = M3U8.parse($request.body); 47 | //Console.debug(`body: ${JSON.stringify(body)}`); 48 | //$request.body = M3U8.stringify(body); 49 | break; 50 | case "text/xml": 51 | case "text/html": 52 | case "text/plist": 53 | case "application/xml": 54 | case "application/plist": 55 | case "application/x-plist": 56 | //body = XML.parse($request.body); 57 | //Console.debug(`body: ${JSON.stringify(body)}`); 58 | //$request.body = XML.stringify(body); 59 | break; 60 | case "text/vtt": 61 | case "application/vtt": 62 | //body = VTT.parse($request.body); 63 | //Console.debug(`body: ${JSON.stringify(body)}`); 64 | //$request.body = VTT.stringify(body); 65 | break; 66 | case "text/json": 67 | case "application/json": 68 | //body = JSON.parse($request.body ?? "{}"); 69 | //Console.debug(`body: ${JSON.stringify(body)}`); 70 | //$request.body = JSON.stringify(body); 71 | break; 72 | case "application/protobuf": 73 | case "application/x-protobuf": 74 | case "application/vnd.google.protobuf": 75 | case "application/grpc": 76 | case "application/grpc+proto": 77 | case "application/vnd.apple.flatbuffer": 78 | case "application/octet-stream": { 79 | //Console.debug(`$request.body: ${JSON.stringify($request.body)}`); 80 | // biome-ignore lint/style/useConst: 81 | let rawBody = $app === "Quantumult X" ? new Uint8Array($request.bodyBytes ?? []) : ($request.body ?? new Uint8Array()); 82 | //Console.debug(`isBuffer? ${ArrayBuffer.isView(rawBody)}: ${JSON.stringify(rawBody)}`); 83 | switch (FORMAT) { 84 | case "application/protobuf": 85 | case "application/x-protobuf": 86 | case "application/vnd.google.protobuf": 87 | break; 88 | case "application/grpc": 89 | case "application/grpc+proto": 90 | break; 91 | } 92 | // 写入二进制数据 93 | $request.body = rawBody; 94 | break; 95 | } 96 | } 97 | //break; // 不中断,继续处理URL 98 | case "GET": 99 | case "HEAD": 100 | case "OPTIONS": 101 | default: 102 | // 主机判断 103 | switch (url.hostname) { 104 | case "upos-sz-mirrorali.bilivideo.com": // 阿里云 CDN 105 | case "upos-sz-mirroralib.bilivideo.com": // 阿里云 CDN 106 | case "upos-sz-mirroralio1.bilivideo.com": // 阿里云 CDN 107 | case "upos-sz-mirrorcos.bilivideo.com": // 腾讯云 CDN 108 | case "upos-sz-mirrorcosb.bilivideo.com": // 腾讯云 CDN,VOD 加速类型 109 | case "upos-sz-mirrorcoso1.bilivideo.com": // 腾讯云 CDN 110 | case "upos-sz-mirrorhw.bilivideo.com": // 华为云 CDN,融合 CDN 111 | case "upos-sz-mirrorhwb.bilivideo.com": // 华为云 CDN,融合 CDN 112 | case "upos-sz-mirrorhwo1.bilivideo.com": // 华为云 CDN,融合 CDN 113 | case "upos-sz-mirror08c.bilivideo.com": // 华为云 CDN,融合 CDN 114 | case "upos-sz-mirror08h.bilivideo.com": // 华为云 CDN,融合 CDN 115 | case "upos-sz-mirror08ct.bilivideo.com": // 华为云 CDN,融合 CDN 116 | case "upos-sz-mirroraliov.bilivideo.com": // 阿里云 CDN,海外 117 | case "upos-sz-mirrorcosov.bilivideo.com": // 腾讯云 CDN,海外 118 | case "upos-sz-mirrorhwov.bilivideo.com": // 华为云 CDN,海外 119 | break; 120 | case "upos-hz-mirrorakam.akamaized.net": // Akamai CDN,海外,有参数校验,其他类型的 CDN 不能直接替换为此 Host。但反过来可以。 121 | url.hostname = Settings.Host.Akamaized; 122 | break; 123 | case "upos-sz-mirroralibstar1.bilivideo.com": // 阿里云 CDN,海外(东南亚),其他类型的 CDN 应该不能替换为此 Host,但反过来可以。 124 | case "upos-sz-mirrorcosbstar1.bilivideo.com": // 腾讯云 CDN,海外(东南亚),其他类型的 CDN 应该不能替换为此 Host,但反过来可以。 125 | case "upos-sz-mirrorhwbstar1.bilivideo.com": // 华为云 CDN,海外(东南亚),其他类型的 CDN 应该不能替换为此 Host,但反过来可以。 126 | case "upos-bstar1-mirrorakam.akamaized.net": // Akamai CDN,海外(东南亚),有参数校验,其他类型的 CDN 不能直接替换为此 Host。但反过来可以。 127 | url.hostname = Settings.Host.BStar; 128 | break; 129 | default: 130 | switch (url.port) { 131 | case "": { 132 | switch (true) { 133 | case url.hostname.endsWith(".mcdn.bilivideo.cn"): 134 | switch (true) { 135 | case url.pathname.startsWith("/v1/resource/"): 136 | switch (url.protocol) { 137 | case "http:": 138 | url.port = "8000"; 139 | break; 140 | case "https:": 141 | url.port = "8082"; 142 | break; 143 | } 144 | break; 145 | case url.pathname.startsWith("/upgcxcode/"): 146 | switch (url.protocol) { 147 | case "http:": 148 | url.port = "9102"; 149 | break; 150 | case "https:": 151 | url.port = "4483"; 152 | break; 153 | } 154 | break; 155 | } 156 | break; 157 | } 158 | break; 159 | } 160 | case "486": { 161 | // MCDN 162 | const cdn = url.searchParams.get("cdn"); 163 | const sid = url.searchParams.get("sid"); 164 | if (cdn) { 165 | url.hostname = `d1--${cdn}.bilivideo.com`; 166 | url.port = ""; 167 | } else if (sid) { 168 | url.hostname = `${sid}.bilivideo.com`; 169 | url.port = ""; 170 | } 171 | break; 172 | } 173 | case "4480": // PCDN 174 | url.protocol = "http:"; 175 | url.hostname = url.searchParams.get("xy_usource") || Settings.Host.PCDN; 176 | url.port = ""; 177 | break; 178 | case "8000": // MCDN.v1.resource 179 | case "8082": // MCDN.v1.resource 180 | break; 181 | case "4483": // MCDN.upgcxcode 182 | case "9102": // MCDN.upgcxcode 183 | if (url.searchParams.has("originalUrl")) break; // 跳过 MCDN 重定向 184 | url.protocol = "http:"; 185 | url.hostname = Settings.Host.MCDN; 186 | url.port = ""; 187 | url.pathname = ""; 188 | for (const key of url.searchParams.keys()) url.searchParams.delete(key); 189 | url.searchParams.set("url", $request.url); 190 | break; 191 | case "9305": // PCDN 192 | url.protocol = "http:"; 193 | url.hostname = url.PATHs.shift(); 194 | url.port = ""; 195 | url.pathname = url.PATHs.join("/"); 196 | break; 197 | } 198 | break; 199 | } 200 | break; 201 | case "CONNECT": 202 | case "TRACE": 203 | break; 204 | } 205 | if ($request.headers?.Host) $request.headers.Host = url.host; 206 | if ($request.headers?.[":authority"]) $request.headers[":authority"] = url.host; 207 | $request.url = url.toString(); 208 | Console.debug(`$request.url: ${$request.url}`); 209 | })() 210 | .catch(e => Console.error(e)) 211 | .finally(() => { 212 | switch (typeof $response) { 213 | case "object": // 有构造回复数据,返回构造的回复数据 214 | //Console.debug("finally", `echo $response: ${JSON.stringify($response, null, 2)}`); 215 | if ($response.headers?.["Content-Encoding"]) $response.headers["Content-Encoding"] = "identity"; 216 | if ($response.headers?.["content-encoding"]) $response.headers["content-encoding"] = "identity"; 217 | switch ($app) { 218 | default: 219 | done({ response: $response }); 220 | break; 221 | case "Quantumult X": 222 | if (!$response.status) $response.status = "HTTP/1.1 200 OK"; 223 | delete $response.headers?.["Content-Length"]; 224 | delete $response.headers?.["content-length"]; 225 | delete $response.headers?.["Transfer-Encoding"]; 226 | done($response); 227 | break; 228 | } 229 | break; 230 | case "undefined": // 无构造回复数据,发送修改的请求数据 231 | //Console.debug("finally", `$request: ${JSON.stringify($request, null, 2)}`); 232 | done($request); 233 | break; 234 | default: 235 | Console.error(`不合法的 $response 类型: ${typeof $response}`); 236 | done(); 237 | break; 238 | } 239 | }); 240 | -------------------------------------------------------------------------------- /src/response.dev.js: -------------------------------------------------------------------------------- 1 | import { $app, Console, done, gRPC, Lodash as _ } from "@nsnanocat/util"; 2 | import database from "./function/database.mjs"; 3 | import setENV from "./function/setENV.mjs"; 4 | import { WireType, UnknownFieldHandler, reflectionMergePartial, MESSAGE_TYPE, MessageType, BinaryReader, isJsonObject, typeofJsonValue, jsonWriteOptions } from "@protobuf-ts/runtime/build/es2015/index.js"; 5 | import { Any } from "./protobuf/google/protobuf/any.js"; 6 | /***************** Processing *****************/ 7 | // 解构URL 8 | const url = new URL($request.url); 9 | Console.info(`url: ${url.toJSON()}`, ""); 10 | // 获取连接参数 11 | const PATHs = url.pathname.split("/").filter(Boolean); 12 | Console.info(`PATHs: ${PATHs}`, ""); 13 | // 解析格式 14 | const FORMAT = ($response.headers?.["Content-Type"] ?? $response.headers?.["content-type"])?.split(";")?.[0]; 15 | Console.info(`FORMAT: ${FORMAT}`, ""); 16 | (async () => { 17 | /** 18 | * 设置 19 | * @type {{Settings: import('./types').Settings}} 20 | */ 21 | const { Settings, Caches, Configs } = setENV("BiliBili", "Redirect", database); 22 | Console.logLevel = Settings.LogLevel; 23 | // 创建空数据 24 | let body = { code: 0, message: "0", data: {} }; 25 | // 信息组 26 | const infoGroup = { 27 | seasonTitle: url.searchParams.get("season_title"), 28 | seasonId: Number.parseInt(url.searchParams.get("season_id"), 10) || undefined, 29 | epId: Number.parseInt(url.searchParams.get("ep_id"), 10) || undefined, 30 | mId: Number.parseInt(url.searchParams.get("mid") || url.searchParams.get("vmid"), 10) || undefined, 31 | evaluate: undefined, 32 | keyword: url.searchParams.get("keyword"), 33 | locale: url.searchParams.get("locale"), 34 | locales: [], 35 | type: "UGC", 36 | }; 37 | // 格式判断 38 | switch (FORMAT) { 39 | case undefined: // 视为无body 40 | break; 41 | case "application/x-www-form-urlencoded": 42 | case "text/plain": 43 | default: 44 | break; 45 | case "application/x-mpegURL": 46 | case "application/x-mpegurl": 47 | case "application/vnd.apple.mpegurl": 48 | case "audio/mpegurl": 49 | //body = M3U8.parse($response.body); 50 | //Console.debug(`body: ${JSON.stringify(body)}`, ""); 51 | //$response.body = M3U8.stringify(body); 52 | break; 53 | case "text/xml": 54 | case "text/html": 55 | case "text/plist": 56 | case "application/xml": 57 | case "application/plist": 58 | case "application/x-plist": 59 | //body = XML.parse($response.body); 60 | //Console.debug(`body: ${JSON.stringify(body)}`, ""); 61 | //$response.body = XML.stringify(body); 62 | break; 63 | case "text/vtt": 64 | case "application/vtt": 65 | //body = VTT.parse($response.body); 66 | //Console.debug(`body: ${JSON.stringify(body)}`, ""); 67 | //$response.body = VTT.stringify(body); 68 | break; 69 | case "text/json": 70 | case "application/json": 71 | body = JSON.parse($response.body ?? "{}"); 72 | // 解析链接 73 | switch (url.hostname) { 74 | case "www.bilibili.com": 75 | break; 76 | case "app.bilibili.com": 77 | case "app.biliapi.net": 78 | break; 79 | case "api.bilibili.com": 80 | case "api.biliapi.net": 81 | switch (url.pathname) { 82 | case "/pgc/player/api/playurl": // 番剧-播放地址-api 83 | case "/pgc/player/web/playurl": // 番剧-播放地址-web 84 | case "/pgc/player/web/playurl/html5": // 番剧-播放地址-web-HTML5 85 | infoGroup.type = "PGC"; 86 | break; 87 | case "/pgc/page/bangumi": // 追番页 88 | case "/pgc/page/cinema/tab": // 观影页 89 | infoGroup.type = "PGC"; 90 | break; 91 | case "/x/player/wbi/playurl": // UGC-用户生产内容-播放地址 92 | break; 93 | case "/x/space/acc/info": // 用户空间-账号信息-pc 94 | case "/x/space/wbi/acc/info": // 用户空间-账号信息-wbi 95 | switch (infoGroup.mId) { 96 | case 11783021: // 哔哩哔哩番剧出差 97 | case 1988098633: // b站_戲劇咖 98 | case 2042149112: // b站_綜藝咖 99 | break; 100 | default: 101 | break; 102 | } 103 | break; 104 | case "/pgc/view/v2/app/season": // 番剧页面-内容-app 105 | break; 106 | case "/pgc/view/web/season": // 番剧-内容-web 107 | case "/pgc/view/pc/season": // 番剧-内容-pc 108 | break; 109 | } 110 | break; 111 | } 112 | $response.body = JSON.stringify(body); 113 | break; 114 | case "application/protobuf": 115 | case "application/x-protobuf": 116 | case "application/vnd.google.protobuf": 117 | case "application/grpc": 118 | case "application/grpc+proto": 119 | case "application/vnd.apple.flatbuffer": 120 | case "application/octet-stream": { 121 | //Console.debug(`$response.body: ${JSON.stringify($response.body)}`, ""); 122 | let rawBody = $app === "Quantumult X" ? new Uint8Array($response.bodyBytes ?? []) : ($response.body ?? new Uint8Array()); 123 | //Console.debug(`isBuffer? ${ArrayBuffer.isView(rawBody)}: ${JSON.stringify(rawBody)}`, ""); 124 | switch (FORMAT) { 125 | case "application/protobuf": 126 | case "application/x-protobuf": 127 | case "application/vnd.google.protobuf": 128 | break; 129 | case "application/grpc": 130 | case "application/grpc+proto": 131 | rawBody = gRPC.decode(rawBody); 132 | // 解析链接并处理protobuf数据 133 | // 主机判断 134 | switch (url.hostname) { 135 | case "grpc.biliapi.net": // HTTP/2 136 | case "app.biliapi.net": // HTTP/1.1 137 | case "app.bilibili.com": // HTTP/1.1 138 | switch (PATHs?.[0]) { 139 | case "bilibili.app.viewunite.v1.View": 140 | /****************** initialization start *******************/ 141 | /****************** initialization finish *******************/ 142 | switch (PATHs?.[1]) { 143 | case "View": // 播放页 144 | break; 145 | } 146 | break; 147 | case "bilibili.app.playerunite.v1.Player": { 148 | /****************** initialization start *******************/ 149 | class Stream$Type extends MessageType { 150 | constructor() { 151 | super("bilibili.playershared.Stream", [ 152 | { no: 1, name: "stream_info", kind: "message", T: () => StreamInfo }, 153 | { no: 2, name: "dash_video", kind: "message", oneof: "content", T: () => DashVideo }, 154 | { no: 3, name: "segment_video", kind: "message", oneof: "content", T: () => SegmentVideo }, 155 | ]); 156 | } 157 | } 158 | const Stream = new Stream$Type(); 159 | class StreamInfo$Type extends MessageType { 160 | constructor() { 161 | super("bilibili.playershared.StreamInfo", [ 162 | { no: 1, name: "quality", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, 163 | { no: 2, name: "format", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 164 | { no: 3, name: "description", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 165 | { no: 4, name: "err_code", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, 166 | //{ no: 5, name: "limit", kind: "message", T: () => StreamLimit }, 167 | { no: 6, name: "need_vip", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, 168 | { no: 7, name: "need_login", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, 169 | { no: 8, name: "intact", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, 170 | { no: 9, name: "no_rexcode", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, 171 | { no: 10, name: "attribute", kind: "scalar", T: 3 /*ScalarType.INT64*/, L: 2 /*LongType.NUMBER*/ }, 172 | { no: 11, name: "new_description", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 173 | { no: 12, name: "display_desc", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 174 | { no: 13, name: "superscript", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 175 | { no: 14, name: "vip_free", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, 176 | { no: 15, name: "subtitle", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 177 | //{ no: 16, name: "scheme", kind: "message", T: () => Scheme }, 178 | { no: 17, name: "support_drm", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, 179 | ]); 180 | } 181 | } 182 | const StreamInfo = new StreamInfo$Type(); 183 | class DashVideo$Type extends MessageType { 184 | constructor() { 185 | super("bilibili.playershared.DashVideo", [ 186 | { no: 1, name: "base_url", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 187 | { no: 2, name: "backup_url", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, 188 | { no: 3, name: "bandwidth", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, 189 | { no: 4, name: "codecid", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, 190 | { no: 5, name: "md5", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 191 | { no: 6, name: "size", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 2 /*LongType.NUMBER*/ }, 192 | { no: 7, name: "audio_id", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, 193 | { no: 8, name: "no_rexcode", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, 194 | { no: 9, name: "frame_rate", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 195 | { no: 10, name: "width", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, 196 | { no: 11, name: "height", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, 197 | { no: 12, name: "widevine_pssh", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 198 | ]); 199 | } 200 | } 201 | const DashVideo = new DashVideo$Type(); 202 | class FragmentVideo$Type extends MessageType { 203 | constructor() { 204 | super("bilibili.playershared.FragmentVideo", [{ no: 1, name: "videos", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => FragmentVideoInfo }]); 205 | } 206 | } 207 | const FragmentVideo = new FragmentVideo$Type(); 208 | class FragmentVideoInfo$Type extends MessageType { 209 | constructor() { 210 | super("bilibili.playershared.FragmentVideoInfo", [ 211 | //{ no: 1, name: "fragment_info", kind: "message", T: () => FragmentInfo }, 212 | { no: 2, name: "vod_info", kind: "message", T: () => VodInfo }, 213 | //{ no: 3, name: "play_arc_conf", kind: "message", T: () => PlayArcConf }, 214 | //{ no: 4, name: "dimension", kind: "message", T: () => Dimension }, 215 | { no: 5, name: "timelength", kind: "scalar", T: 3 /*ScalarType.INT64*/, L: 2 /*LongType.NUMBER*/ }, 216 | //{ no: 6, name: "video_type", kind: "enum", T: () => ["bilibili.playershared.BizType", BizType, "BIZ_TYPE_"] }, 217 | { no: 7, name: "playable_status", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, 218 | ]); 219 | } 220 | } 221 | const FragmentVideoInfo = new FragmentVideoInfo$Type(); 222 | class ResponseUrl$Type extends MessageType { 223 | constructor() { 224 | super("bilibili.playershared.ResponseUrl", [ 225 | { no: 1, name: "order", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, 226 | { no: 2, name: "length", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 2 /*LongType.NUMBER*/ }, 227 | { no: 3, name: "size", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 2 /*LongType.NUMBER*/ }, 228 | { no: 4, name: "url", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 229 | { no: 5, name: "backup_url", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, 230 | { no: 6, name: "md5", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 231 | ]); 232 | } 233 | } 234 | const ResponseUrl = new ResponseUrl$Type(); 235 | class SegmentVideo$Type extends MessageType { 236 | constructor() { 237 | super("bilibili.playershared.SegmentVideo", [{ no: 1, name: "segment", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => ResponseUrl }]); 238 | } 239 | } 240 | const SegmentVideo = new SegmentVideo$Type(); 241 | class VodInfo$Type extends MessageType { 242 | constructor() { 243 | super("bilibili.playershared.VodInfo", [ 244 | { no: 1, name: "quality", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, 245 | { no: 2, name: "format", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 246 | { no: 3, name: "timelength", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 2 /*LongType.NUMBER*/ }, 247 | { no: 4, name: "video_codecid", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, 248 | { no: 5, name: "stream_list", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => Stream }, 249 | //{ no: 6, name: "dash_audio", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => DashItem }, 250 | //{ no: 7, name: "dolby", kind: "message", T: () => DolbyItem }, 251 | //{ no: 8, name: "volume", kind: "message", T: () => VolumeInfo }, 252 | //{ no: 9, name: "loss_less_item", kind: "message", T: () => LossLessItem }, 253 | //{ no: 10, name: "support_project", kind: "scalar", T: 8 /*ScalarType.BOOL*/ } 254 | ]); 255 | } 256 | } 257 | const VodInfo = new VodInfo$Type(); 258 | /****************** initialization finish *******************/ 259 | switch (PATHs?.[1]) { 260 | case "PlayViewUnite": { 261 | // 播放页 262 | /****************** initialization start *******************/ 263 | class PlayViewUniteReply$Type extends MessageType { 264 | constructor() { 265 | super("bilibili.app.playerunite.v1.PlayViewUniteReply", [ 266 | { no: 1, name: "vod_info", kind: "message", T: () => VodInfo }, 267 | //{ no: 2, name: "play_arc_conf", kind: "message", T: () => PlayArcConf }, 268 | //{ no: 3, name: "play_device_conf", kind: "message", T: () => PlayDeviceConf }, 269 | //{ no: 4, name: "event", kind: "message", T: () => Event }, 270 | //{ no: 5, name: "supplement", kind: "message", T: () => Any }, 271 | //{ no: 6, name: "play_arc", kind: "message", T: () => PlayArc }, 272 | //{ no: 7, name: "qn_trial_info", kind: "message", T: () => QnTrialInfo }, 273 | //{ no: 8, name: "history", kind: "message", T: () => History }, 274 | //{ no: 9, name: "view_info", kind: "message", T: () => ViewInfo }, 275 | { no: 10, name: "fragment_video", kind: "message", T: () => FragmentVideo }, 276 | ]); 277 | } 278 | } 279 | const PlayViewUniteReply = new PlayViewUniteReply$Type(); 280 | /****************** initialization finish *******************/ 281 | let data = PlayViewUniteReply.fromBinary(body); 282 | Console.debug(`data: ${JSON.stringify(data)}`, ""); 283 | let UF = UnknownFieldHandler.list(data); 284 | //Console.debug(`UF: ${JSON.stringify(UF)}`, ""); 285 | if (UF) { 286 | UF = UF.map(uf => { 287 | //uf.no; // 22 288 | //uf.wireType; // WireType.Varint 289 | // use the binary reader to decode the raw data: 290 | let reader = new BinaryReader(uf.data); 291 | let addedNumber = reader.int32(); // 7777 292 | Console.debug(`no: ${uf.no}, wireType: ${uf.wireType}, addedNumber: ${addedNumber}`, ""); 293 | }); 294 | } 295 | data.vodInfo.streamList = data.vodInfo.streamList.map(stream => { 296 | switch (stream?.content?.oneofKind) { 297 | case "dashVideo": 298 | stream.content.dashVideo.baseUrl = stream.content.dashVideo.backupUrl.at(-1); 299 | break; 300 | case "SegmentVideo": 301 | stream.content.segmentVideo.segment = stream.content.segmentVideo.segment.map(segment => { 302 | segment.url = segment.backupUrl.at(-1); 303 | return segment; 304 | }); 305 | break; 306 | } 307 | return stream; 308 | }); 309 | Console.debug(`data: ${JSON.stringify(data)}`, ""); 310 | body = PlayViewUniteReply.toBinary(data); 311 | break; 312 | } 313 | } 314 | break; 315 | } 316 | case "bilibili.app.playurl.v1.PlayURL": // 普通视频 317 | /****************** initialization start *******************/ 318 | /****************** initialization finish *******************/ 319 | switch (PATHs?.[1]) { 320 | case "PlayView": // 播放地址 321 | break; 322 | case "PlayConf": // 播放配置 323 | break; 324 | } 325 | break; 326 | case "bilibili.pgc.gateway.player.v2.PlayURL": // 番剧 327 | /****************** initialization start *******************/ 328 | /****************** initialization finish *******************/ 329 | infoGroup.type = "PGC"; 330 | switch (PATHs?.[1]) { 331 | case "PlayView": // 播放地址 332 | /****************** initialization start *******************/ 333 | /****************** initialization finish *******************/ 334 | break; 335 | case "PlayConf": // 播放配置 336 | break; 337 | } 338 | break; 339 | case "bilibili.app.nativeact.v1.NativeAct": // 活动-节目、动画、韩综(港澳台) 340 | switch (PATHs?.[1]) { 341 | case "Index": // 首页 342 | break; 343 | } 344 | break; 345 | case "bilibili.app.interface.v1.Search": // 搜索框 346 | switch (PATHs?.[1]) { 347 | case "Suggest3": // 搜索建议 348 | break; 349 | } 350 | break; 351 | case "bilibili.polymer.app.search.v1.Search": // 搜索结果 352 | /****************** initialization start *******************/ 353 | /****************** initialization finish *******************/ 354 | switch (PATHs?.[1]) { 355 | case "SearchAll": { 356 | // 全部结果(综合) 357 | /****************** initialization start *******************/ 358 | /****************** initialization finish *******************/ 359 | break; 360 | } 361 | case "SearchByType": { 362 | // 分类结果(番剧、用户、影视、专栏) 363 | break; 364 | } 365 | } 366 | break; 367 | } 368 | break; 369 | } 370 | rawBody = gRPC.encode(rawBody); 371 | break; 372 | } 373 | // 写入二进制数据 374 | $response.body = rawBody; 375 | break; 376 | } 377 | } 378 | Console.debug(`信息组, infoGroup: ${JSON.stringify(infoGroup)}`, ""); 379 | })() 380 | .catch(e => Console.error(e)) 381 | .finally(() => done($response)); 382 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Settings { 2 | Host?: { 3 | /** 4 | * [主机名] 重定向 Akamaized CDN (港澳台) 5 | * 6 | * 请选择 Akamaized 要重定向的主机名。 7 | * 8 | * @remarks 9 | * 10 | * Possible values: 11 | * - `'upos-sz-mirrorali.bilivideo.com'` - 阿里云 CDN 12 | * - `'upos-sz-mirrorcos.bilivideo.com'` - 腾讯云 CDN 13 | * - `'upos-sz-mirrorhw.bilivideo.com'` - 华为云 CDN,融合 CDN 14 | * - `'upos-sz-mirroraliov.bilivideo.com'` - 阿里云 CDN,海外 15 | * - `'upos-sz-mirrorcosov.bilivideo.com'` - 腾讯云 CDN,海外 16 | * - `'upos-sz-mirrorhwov.bilivideo.com'` - 华为云 CDN,海外 17 | * 18 | * @defaultValue "upos-sz-mirrorali.bilivideo.com" 19 | */ 20 | Akamaized?: 'upos-sz-mirrorali.bilivideo.com' | 'upos-sz-mirrorcos.bilivideo.com' | 'upos-sz-mirrorhw.bilivideo.com' | 'upos-sz-mirroraliov.bilivideo.com' | 'upos-sz-mirrorcosov.bilivideo.com' | 'upos-sz-mirrorhwov.bilivideo.com'; 21 | /** 22 | * [主机名] 重定向 BStar CDN (国际版) 23 | * 24 | * 请选择 BStar 要重定向的主机名。 25 | * 26 | * @remarks 27 | * 28 | * Possible values: 29 | * - `'upos-sz-mirrorali.bilivideo.com'` - 阿里云 CDN 30 | * - `'upos-sz-mirrorcos.bilivideo.com'` - 腾讯云 CDN 31 | * - `'upos-sz-mirrorhw.bilivideo.com'` - 华为云 CDN,融合 CDN 32 | * - `'upos-sz-mirroraliov.bilivideo.com'` - 阿里云 CDN,海外 33 | * - `'upos-sz-mirrorcosov.bilivideo.com'` - 腾讯云 CDN,海外 34 | * - `'upos-sz-mirrorhwov.bilivideo.com'` - 华为云 CDN,海外 35 | * 36 | * @defaultValue "upos-sz-mirrorali.bilivideo.com" 37 | */ 38 | BStar?: 'upos-sz-mirrorali.bilivideo.com' | 'upos-sz-mirrorcos.bilivideo.com' | 'upos-sz-mirrorhw.bilivideo.com' | 'upos-sz-mirroraliov.bilivideo.com' | 'upos-sz-mirrorcosov.bilivideo.com' | 'upos-sz-mirrorhwov.bilivideo.com'; 39 | /** 40 | * [主机名] 重定向 PCDN 主机名 (中国大陆) 41 | * 42 | * 请选择 PCDN 要重定向的主机名。 43 | * 44 | * @remarks 45 | * 46 | * Possible values: 47 | * - `'upos-sz-mirrorali.bilivideo.com'` - 阿里云 CDN 48 | * - `'upos-sz-mirrorcos.bilivideo.com'` - 腾讯云 CDN 49 | * - `'upos-sz-mirrorhw.bilivideo.com'` - 华为云 CDN,融合 CDN 50 | * - `'upos-sz-mirroraliov.bilivideo.com'` - 阿里云 CDN,海外 51 | * - `'upos-sz-mirrorcosov.bilivideo.com'` - 腾讯云 CDN,海外 52 | * - `'upos-sz-mirrorhwov.bilivideo.com'` - 华为云 CDN,海外 53 | * 54 | * @defaultValue "upos-sz-mirrorali.bilivideo.com" 55 | */ 56 | PCDN?: 'upos-sz-mirrorali.bilivideo.com' | 'upos-sz-mirrorcos.bilivideo.com' | 'upos-sz-mirrorhw.bilivideo.com' | 'upos-sz-mirroraliov.bilivideo.com' | 'upos-sz-mirrorcosov.bilivideo.com' | 'upos-sz-mirrorhwov.bilivideo.com'; 57 | /** 58 | * [主机名] 重定向 MCDN 主机名 (中国大陆) 59 | * 60 | * 请选择 MCDN 要重定向的主机名。 61 | * 62 | * @remarks 63 | * 64 | * Possible values: 65 | * - `'proxy-tf-all-ws.bilivideo.com'` - proxy-tf-all-ws.bilivideo.com 66 | * 67 | * @defaultValue "proxy-tf-all-ws.bilivideo.com" 68 | */ 69 | MCDN?: 'proxy-tf-all-ws.bilivideo.com'; 70 | }; 71 | /** 72 | * [调试] 日志等级 73 | * 74 | * 选择脚本日志的输出等级,低于所选等级的日志将全部输出。 75 | * 76 | * @remarks 77 | * 78 | * Possible values: 79 | * - `'OFF'` - 关闭 80 | * - `'ERROR'` - ❌ 错误 81 | * - `'WARN'` - ⚠️ 警告 82 | * - `'INFO'` - ℹ️ 信息 83 | * - `'DEBUG'` - 🅱️ 调试 84 | * - `'ALL'` - 全部 85 | * 86 | * @defaultValue "WARN" 87 | */ 88 | LogLevel?: 'OFF' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'ALL'; 89 | } 90 | -------------------------------------------------------------------------------- /template/boxjs.settings.json: -------------------------------------------------------------------------------- 1 | [{"id":"@BiliBili.Redirect.Settings.Host.Akamaized","name":"[主机名] 重定向 Akamaized CDN (港澳台)","type":"selects","val":"upos-sz-mirrorali.bilivideo.com","items":[{"key":"upos-sz-mirrorali.bilivideo.com","label":"阿里云 CDN"},{"key":"upos-sz-mirrorcos.bilivideo.com","label":"腾讯云 CDN"},{"key":"upos-sz-mirrorhw.bilivideo.com","label":"华为云 CDN,融合 CDN"},{"key":"upos-sz-mirroraliov.bilivideo.com","label":"阿里云 CDN,海外"},{"key":"upos-sz-mirrorcosov.bilivideo.com","label":"腾讯云 CDN,海外"},{"key":"upos-sz-mirrorhwov.bilivideo.com","label":"华为云 CDN,海外"}],"desc":"请选择 Akamaized 要重定向的主机名。"},{"id":"@BiliBili.Redirect.Settings.Host.BStar","name":"[主机名] 重定向 BStar CDN (国际版)","type":"selects","val":"upos-sz-mirrorali.bilivideo.com","items":[{"key":"upos-sz-mirrorali.bilivideo.com","label":"阿里云 CDN"},{"key":"upos-sz-mirrorcos.bilivideo.com","label":"腾讯云 CDN"},{"key":"upos-sz-mirrorhw.bilivideo.com","label":"华为云 CDN,融合 CDN"},{"key":"upos-sz-mirroraliov.bilivideo.com","label":"阿里云 CDN,海外"},{"key":"upos-sz-mirrorcosov.bilivideo.com","label":"腾讯云 CDN,海外"},{"key":"upos-sz-mirrorhwov.bilivideo.com","label":"华为云 CDN,海外"}],"desc":"请选择 BStar 要重定向的主机名。"},{"id":"@BiliBili.Redirect.Settings.Host.PCDN","name":"[主机名] 重定向 PCDN 主机名 (中国大陆)","type":"selects","val":"upos-sz-mirrorali.bilivideo.com","items":[{"key":"upos-sz-mirrorali.bilivideo.com","label":"阿里云 CDN"},{"key":"upos-sz-mirrorcos.bilivideo.com","label":"腾讯云 CDN"},{"key":"upos-sz-mirrorhw.bilivideo.com","label":"华为云 CDN,融合 CDN"},{"key":"upos-sz-mirroraliov.bilivideo.com","label":"阿里云 CDN,海外"},{"key":"upos-sz-mirrorcosov.bilivideo.com","label":"腾讯云 CDN,海外"},{"key":"upos-sz-mirrorhwov.bilivideo.com","label":"华为云 CDN,海外"}],"desc":"请选择 PCDN 要重定向的主机名。"},{"id":"@BiliBili.Redirect.Settings.Host.MCDN","name":"[主机名] 重定向 MCDN 主机名 (中国大陆)","type":"selects","val":"proxy-tf-all-ws.bilivideo.com","items":[{"key":"proxy-tf-all-ws.bilivideo.com","label":"proxy-tf-all-ws.bilivideo.com"}],"desc":"请选择 MCDN 要重定向的主机名。"},{"id":"@BiliBili.Redirect.Settings.LogLevel","name":"[调试] 日志等级","type":"selects","val":"WARN","items":[{"key":"OFF","label":"关闭"},{"key":"ERROR","label":"❌ 错误"},{"key":"WARN","label":"⚠️ 警告"},{"key":"INFO","label":"ℹ️ 信息"},{"key":"DEBUG","label":"🅱️ 调试"},{"key":"ALL","label":"全部"}],"desc":"选择脚本日志的输出等级,低于所选等级的日志将全部输出。"}] -------------------------------------------------------------------------------- /template/loon.handlebars: -------------------------------------------------------------------------------- 1 | #!name = {{@package 'displayName'}} 2 | #!desc = {{inline (@package 'description')}} 3 | #!openUrl = {{@package 'openUrl'}} 4 | #!author = {{@package 'contributors'}} 5 | #!homepage = {{@package 'homepage'}} 6 | #!icon = {{@package 'icon'}} 7 | #!tag = {{@package 'organizationName'}} 8 | #!system = {{@package 'system'}} 9 | #!date = {{now "yyyy-MM-dd HH:mm:ss"}} 10 | #!version = {{@package 'version'}} 11 | #!system_version = {{ @package 'systemVersion'}} 12 | 13 | [Argument] 14 | {{{arguments}}} 15 | 16 | [General] 17 | force-http-engine-hosts = *:4480, *:4483, *:8000, *:8082, *:9102 18 | 19 | [Script] 20 | http-request ^https?:\/\/.+\.bilivideo\.com\/upgcxcode\/ script-path=https://github.com/BiliUniverse/Redirect/releases/download/v{{@package 'version'}}/request.bundle.js, tag=📺 BiliBili.Redirect.CDN.upgcxcode.m4s, argument={{{scriptParams}}} 21 | http-request ^https?:\/\/[adbcefxy0-9]+\.mcdn\.bilivideo\.cn(:(8000|8082))?\/v1\/resource\/ script-path=https://github.com/BiliUniverse/Redirect/releases/download/v{{@package 'version'}}/request.bundle.js, tag=📺 BiliBili.Redirect.MCDN.v1.resource.m4s, argument={{{scriptParams}}} 22 | http-request ^https?:\/\/[adbcefxy0-9]+\.mcdn\.bilivideo\.cn:(4483|9102)\/upgcxcode\/ script-path=https://github.com/BiliUniverse/Redirect/releases/download/v{{@package 'version'}}/request.bundle.js, tag=📺 BiliBili.Redirect.MCDN.upgcxcode.m4s, argument={{{scriptParams}}} 23 | http-request ^https?:\/\/(.+):4480\/upgcxcode\/ script-path=https://github.com/BiliUniverse/Redirect/releases/download/v{{@package 'version'}}/request.bundle.js, tag=📺 BiliBili.Redirect.PCDN.upgcxcode.m4s, argument={{{scriptParams}}} 24 | http-request ^https?:\/\/upos-(hz|bstar1)-mirrorakam\.akamaized\.net/upgcxcode\/ script-path=https://github.com/BiliUniverse/Redirect/releases/download/v{{@package 'version'}}/request.bundle.js, tag=📺 BiliBili.Redirect.Akamaized.upgcxcode.m4s, argument={{{scriptParams}}} 25 | 26 | [MITM] 27 | hostname = *.mcdn.bilivideo.cn, upos-sz-mirror*bstar1.bilivideo.com, upos-*-mirrorakam.akamaized.net 28 | -------------------------------------------------------------------------------- /template/stash.handlebars: -------------------------------------------------------------------------------- 1 | name: "{{@package 'displayName'}}" 2 | desc: |- 3 | {{#each (split (@package 'description') "\n")}} 4 | {{{this}}} 5 | {{/each}} 6 | openUrl: "{{@package 'openUrl'}}" 7 | author: |- 8 | {{#each (@package 'contributors')}} 9 | {{this}} 10 | {{/each}} 11 | homepage: "{{@package 'homepage'}}" 12 | icon: "{{@package 'icon'}}" 13 | category: "{{@package 'organizationName'}}" 14 | date: "{{now "yyyy-MM-dd HH:mm:ss"}}" 15 | version: "{{@package 'version'}}" 16 | 17 | http: 18 | force-http-engine: 19 | - "*:4480" 20 | - "*:8000" 21 | - "*:9102" 22 | mitm: 23 | - "*.mcdn.bilivideo.cn" 24 | - "*.mcdn.bilivideo.cn:4483" 25 | - "*.mcdn.bilivideo.cn:8082" 26 | - "upos-sz-mirror*bstar1.bilivideo.com" 27 | - "upos-*-mirrorakam.akamaized.net" 28 | script: 29 | - match: ^https?:\/\/.+\.bilivideo\.com\/upgcxcode\/ 30 | name: "📺 BiliBili.Redirect.request" 31 | type: request 32 | - match: ^https?:\/\/[adbcefxy0-9]+\.mcdn\.bilivideo\.cn(:(8000|8082))?\/v1\/resource\/ 33 | name: "📺 BiliBili.Redirect.request" 34 | type: request 35 | argument: 36 | - match: ^https?:\/\/[adbcefxy0-9]+\.mcdn\.bilivideo\.cn:(4483|9102)\/upgcxcode\/ 37 | name: "📺 BiliBili.Redirect.request" 38 | type: request 39 | argument: 40 | - match: ^https?:\/\/(.+):4480\/upgcxcode\/ 41 | name: "📺 BiliBili.Redirect.request" 42 | type: request 43 | argument: 44 | - match: ^https?:\/\/upos-(hz|bstar1)-mirrorakam\.akamaized\.net/upgcxcode\/ 45 | name: "📺 BiliBili.Redirect.request" 46 | type: request 47 | argument: 48 | script-providers: 49 | "📺 BiliBili.Redirect.request": 50 | url: https://github.com/BiliUniverse/Redirect/releases/download/v{{@package 'version'}}/request.bundle.js 51 | interval: 86400 52 | 53 | -------------------------------------------------------------------------------- /template/surge.handlebars: -------------------------------------------------------------------------------- 1 | #!name = {{@package 'displayName'}} 2 | #!desc = {{inline (@package 'description')}} 3 | #!openUrl = {{@package 'openUrl'}} 4 | #!author = {{@package 'contributors'}} 5 | #!homepage = {{@package 'homepage'}} 6 | #!icon = {{@package 'icon'}} 7 | #!category = {{@package 'organizationName'}} 8 | #!date = {{now "yyyy-MM-dd HH:mm:ss"}} 9 | #!version = {{@package 'version'}} 10 | #!arguments = {{{arguments}}} 11 | #!arguments-desc = {{{argumentsDesc}}} 12 | 13 | [General] 14 | # > TCP Force HTTP Hosts 15 | # 让 Surge 把 TCP 连接当作 HTTP 请求来处理。Surge HTTP 引擎将处理这些请求,所有的高级功能,如捕获、重写和脚本等都可以使用。 16 | force-http-engine-hosts = %APPEND% *:4480, *:4483, *:8000, *:8082, *:9102 17 | 18 | [Script] 19 | 📺 BiliBili.Redirect.CDN.upgcxcode.m4s = type=http-request, pattern=^https?:\/\/.+\.bilivideo\.com\/upgcxcode\/, script-path=https://github.com/BiliUniverse/Redirect/releases/download/v{{@package 'version'}}/request.bundle.js, argument={{{scriptParams}}} 20 | 📺 BiliBili.Redirect.MCDN.v1.resource.m4s = type=http-request, pattern=^https?:\/\/[adbcefxy0-9]+\.mcdn\.bilivideo\.cn(:(8000|8082))?\/v1\/resource\/, script-path=https://github.com/BiliUniverse/Redirect/releases/download/v{{@package 'version'}}/request.bundle.js, argument={{{scriptParams}}} 21 | 📺 BiliBili.Redirect.MCDN.upgcxcode.m4s = type=http-request, pattern=^https?:\/\/[adbcefxy0-9]+\.mcdn\.bilivideo\.cn:(4483|9102)\/upgcxcode\/, script-path=https://github.com/BiliUniverse/Redirect/releases/download/v{{@package 'version'}}/request.bundle.js, argument={{{scriptParams}}} 22 | 📺 BiliBili.Redirect.PCDN.upgcxcode.m4s = type=http-request, pattern=^https?:\/\/(.+):4480\/upgcxcode\/, script-path=https://github.com/BiliUniverse/Redirect/releases/download/v{{@package 'version'}}/request.bundle.js, argument={{{scriptParams}}} 23 | 📺 BiliBili.Redirect.Akamaized.upgcxcode.m4s = type=http-request, pattern=^https?:\/\/upos-(hz|bstar1)-mirrorakam\.akamaized\.net/upgcxcode\/, script-path=https://github.com/BiliUniverse/Redirect/releases/download/v{{@package 'version'}}/request.bundle.js, argument={{{scriptParams}}} 24 | 25 | [MITM] 26 | hostname = %APPEND% *.mcdn.bilivideo.cn, upos-sz-mirror*bstar1.bilivideo.com, upos-*-mirrorakam.akamaized.net 27 | --------------------------------------------------------------------------------