├── .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 ├── js ├── DualSubs.Transcripts.Translate.response.beta.js └── DualSubs.Transcripts.Translate.response.js ├── modules ├── DualSubs.Spotify.Transcripts.beta.sgmodule ├── DualSubs.Spotify.Transcripts.plugin ├── DualSubs.Spotify.Transcripts.sgmodule ├── DualSubs.Spotify.Transcripts.snippet ├── DualSubs.Spotify.Transcripts.srmodule └── DualSubs.Spotify.Transcripts.stoverride ├── package-lock.json ├── package.json ├── rspack.config.js ├── rspack.dev.config.js ├── src ├── assets │ ├── icon.png │ └── icon_rounded.png ├── function │ ├── database.mjs │ ├── modifiedAccountAttributes.mjs │ ├── modifiedAssignedValues.mjs │ ├── setCache.mjs │ └── setENV.mjs ├── request.dev.js ├── request.js ├── response.dev.js ├── response.js └── types.d.ts └── template ├── boxjs.settings.json ├── loon.handlebars ├── quantumultx.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 | debug: 14 | uses: ./.github/workflows/dev.yml 15 | secrets: inherit 16 | deploy: 17 | needs: debug 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: e60d056221d3d42ef6fa30c4e3ea8a51 29 | gist_description: "🍿️ DualSubs: 🎵 Spotify β" 30 | file_path: dist/request.bundle.js 31 | - name: Deploy Reponse 32 | uses: exuanbo/actions-deploy-gist@main 33 | with: 34 | token: ${{ secrets.GIST_TOKEN }} 35 | gist_id: e60d056221d3d42ef6fa30c4e3ea8a51 36 | gist_description: "🍿️ DualSubs: 🎵 Spotify β" 37 | file_path: dist/response.bundle.js 38 | -------------------------------------------------------------------------------- /.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/DualSubs/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 | * 修复 `$argument` 和 `$persistentStore` 载入顺序颠倒的问题 3 | * 正确顺序为先读取 `$argument` 再读取 `$persistentStore (BoxJs)` 4 | * 即,有相同键名时,`$persistentStore (BoxJs)` 的值会覆盖 `$argument` 的值 5 | 6 | ### 🔣 Dependencies 7 | * 升级了 `@nsnanocat/util` 8 | * `util` 由 `submodule` 更改为 `package` 9 | * `$platform` 改为 `$app` 10 | * 使用了全新的 `Console` polyfill 11 | -------------------------------------------------------------------------------- /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 | # 🍿️ DualSubs: 🎵 Spotify 2 | -------------------------------------------------------------------------------- /arguments-builder.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@iringo/arguments-builder"; 2 | 3 | export default defineConfig({ 4 | output: { 5 | surge: { 6 | path: "./dist/DualSubs.Spotify.sgmodule", 7 | transformEgern: { 8 | enable: true, 9 | path: "./dist/DualSubs.Spotify.yaml", 10 | }, 11 | }, 12 | loon: { 13 | path: "./dist/DualSubs.Spotify.plugin", 14 | }, 15 | customItems: [ 16 | { 17 | path: "./dist/DualSubs.Spotify.snippet", 18 | template: "./template/quantumultx.handlebars", 19 | }, 20 | { 21 | path: "./dist/DualSubs.Spotify.stoverride", 22 | template: "./template/stash.handlebars", 23 | }, 24 | ], 25 | dts: { 26 | isExported: true, 27 | path: "./src/types.d.ts", 28 | }, 29 | boxjsSettings: { 30 | path: "./template/boxjs.settings.json", 31 | scope: "@DualSubs.Spotify.Settings", 32 | }, 33 | }, 34 | args: [ 35 | { 36 | key: "Types", 37 | name: "[歌词] 启用类型(多选)", 38 | defaultValue: ["Translate", "External"], 39 | type: "array", 40 | options: [ 41 | { 42 | key: "Translate", 43 | label: "翻译歌词(翻译器)", 44 | }, 45 | { 46 | key: "External", 47 | label: "外部歌词(外部源)", 48 | }, 49 | ], 50 | description: "请选择要添加的歌词选项,如果为多选,则会自动决定提供的歌词类型。", 51 | }, 52 | { 53 | key: "Languages[0]", 54 | name: "[翻译器] 主语言(源语言)", 55 | defaultValue: "AUTO", 56 | type: "string", 57 | boxJsType: "selects", 58 | description: "仅当源语言识别不准确时更改此选项。", 59 | options: [ 60 | { 61 | key: "AUTO", 62 | label: "自动 - Automatic", 63 | }, 64 | { 65 | key: "ZH", 66 | label: "中文(自动)", 67 | }, 68 | { 69 | key: "ZH-HANS", 70 | label: "中文(简体)", 71 | }, 72 | { 73 | key: "ZH-HK", 74 | label: "中文(香港)", 75 | }, 76 | { 77 | key: "ZH-HANT", 78 | label: "中文(繁体)", 79 | }, 80 | { 81 | key: "EN", 82 | label: "English - 英语(自动)", 83 | }, 84 | { 85 | key: "ES", 86 | label: "Español - 西班牙语(自动)", 87 | }, 88 | { 89 | key: "JA", 90 | label: "日本語 - 日语", 91 | }, 92 | { 93 | key: "KO", 94 | label: "한국어 - 韩语", 95 | }, 96 | { 97 | key: "DE", 98 | label: "Deutsch - 德语", 99 | }, 100 | { 101 | key: "FR", 102 | label: "Français - 法语", 103 | }, 104 | { 105 | key: "TR", 106 | label: "Türkçe - 土耳其语", 107 | }, 108 | { 109 | key: "KM", 110 | label: "ភាសាខ្មែរ - 高棉语", 111 | }, 112 | ], 113 | }, 114 | { 115 | key: "Languages[1]", 116 | name: "[翻译器] 副语言(目标语言)", 117 | defaultValue: "ZH", 118 | type: "string", 119 | boxJsType: "selects", 120 | description: "请指定翻译歌词的目标语言。", 121 | options: [ 122 | { 123 | key: "ZH", 124 | label: "中文(自动)", 125 | }, 126 | { 127 | key: "ZH-HANS", 128 | label: "中文(简体)", 129 | }, 130 | { 131 | key: "ZH-HK", 132 | label: "中文(香港)", 133 | }, 134 | { 135 | key: "ZH-HANT", 136 | label: "中文(繁体)", 137 | }, 138 | { 139 | key: "EN", 140 | label: "English - 英语(自动)", 141 | }, 142 | { 143 | key: "ES", 144 | label: "Español - 西班牙语(自动)", 145 | }, 146 | { 147 | key: "JA", 148 | label: "日本語 - 日语", 149 | }, 150 | { 151 | key: "KO", 152 | label: "한국어 - 韩语", 153 | }, 154 | { 155 | key: "DE", 156 | label: "Deutsch - 德语", 157 | }, 158 | { 159 | key: "FR", 160 | label: "Français - 法语", 161 | }, 162 | { 163 | key: "TR", 164 | label: "Türkçe - 土耳其语", 165 | }, 166 | { 167 | key: "KM", 168 | label: "ភាសាខ្មែរ - 高棉语", 169 | }, 170 | ], 171 | }, 172 | { 173 | key: "Vendor", 174 | name: "[翻译器] 服务商API", 175 | defaultValue: "Google", 176 | type: "string", 177 | options: [ 178 | { 179 | key: "Google", 180 | label: "Google Translate", 181 | }, 182 | { 183 | key: "Microsoft", 184 | label: "Microsoft Translator(需填写API)", 185 | }, 186 | ], 187 | description: "请选择翻译器所使用的服务商API,更多翻译选项请使用BoxJs。", 188 | }, 189 | { 190 | key: "LrcVendor", 191 | name: "[歌词] 服务商API", 192 | defaultValue: "NeteaseMusic", 193 | type: "string", 194 | options: [ 195 | { 196 | key: "NeteaseMusic", 197 | label: "网易云音乐(官方)", 198 | }, 199 | { 200 | key: "QQMusic", 201 | label: "QQ音乐(官方)", 202 | }, 203 | { 204 | key: "NeteaseMusicNodeJS", 205 | label: "网易云音乐 NodeJS API", 206 | }, 207 | ], 208 | description: "请选择外部源所使用的服务商API。", 209 | }, 210 | { 211 | key: "LogLevel", 212 | name: "[调试] 日志等级", 213 | type: "string", 214 | defaultValue: "WARN", 215 | description: "选择脚本日志的输出等级,低于所选等级的日志将全部输出。", 216 | options: [ 217 | { key: "OFF", label: "关闭" }, 218 | { key: "ERROR", label: "❌ 错误" }, 219 | { key: "WARN", label: "⚠️ 警告" }, 220 | { key: "INFO", label: "ℹ️ 信息" }, 221 | { key: "DEBUG", label: "🅱️ 调试" }, 222 | { key: "ALL", label: "全部" }, 223 | ], 224 | }, 225 | ], 226 | }); 227 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /js/DualSubs.Transcripts.Translate.response.js: -------------------------------------------------------------------------------- 1 | /* 2 | README: https://github.com/DualSubs/Spotify 3 | */ 4 | 5 | const $ = new Env("🍿️ DualSubs: 🎵 Spotify v1.2.4(10) Transcripts.Translate.response"); 6 | const URI = new URIs(); 7 | const DataBase = { 8 | "Default":{ 9 | "Settings":{"Switch":true,"Type":"Translate","Types":["Official","Translate"],"Languages":["EN","ZH"],"CacheSize":50} 10 | }, 11 | "Universal":{ 12 | "Settings":{"Switch":true,"Types":["Official","Translate"],"Languages":["EN","ZH"]}, 13 | "Configs":{ 14 | "Languages":{"AUTO":"","AR":["ar","ar-001"],"BG":["bg","bg-BG","bul"],"CS":["cs","cs-CZ","ces"],"DA":["da","da-DK","dan"],"DE":["de","de-DE","deu"],"EL":["el","el-GR","ell"],"EN":["en","en-US","eng","en-GB","en-UK","en-CA","en-US SDH"],"EN-CA":["en-CA","en","eng"],"EN-GB":["en-UK","en","eng"],"EN-US":["en-US","en","eng"],"EN-US SDH":["en-US SDH","en-US","en","eng"],"ES":["es","es-419","es-ES","spa","es-419 SDH"],"ES-419":["es-419","es","spa"],"ES-419 SDH":["es-419 SDH","es-419","es","spa"],"ES-ES":["es-ES","es","spa"],"ET":["et","et-EE","est"],"FI":["fi","fi-FI","fin"],"FR":["fr","fr-CA","fr-FR","fra"],"FR-CA":["fr-CA","fr","fra"],"FR-DR":["fr-FR","fr","fra"],"HU":["hu","hu-HU","hun"],"ID":["id","id-id"],"IT":["it","it-IT","ita"],"JA":["ja","ja-JP","jpn"],"KO":["ko","ko-KR","kor"],"LT":["lt","lt-LT","lit"],"LV":["lv","lv-LV","lav"],"NL":["nl","nl-NL","nld"],"NO":["no","nb-NO","nor"],"PL":["pl","pl-PL"],"PT":["pt","pt-PT","pt-BR","por"],"PT-PT":["pt-PT","pt","por"],"PT-BR":["pt-BR","pt","por"],"RO":["ro","ro-RO","ron"],"RU":["ru","ru-RU","rus"],"SK":["sk","sk-SK","slk"],"SL":["sl","sl-SI","slv"],"SV":["sv","sv-SE","swe"],"IS":["is","is-IS","isl"],"ZH":["zh","cmn","zho","zh-CN","zh-Hans","cmn-Hans","zh-TW","zh-Hant","cmn-Hant","zh-HK","yue-Hant","yue"],"ZH-CN":["zh-CN","zh-Hans","cmn-Hans","zho"],"ZH-HANS":["zh-Hans","cmn-Hans","zh-CN","zho"],"ZH-HK":["zh-HK","yue-Hant","yue","zho"],"ZH-TW":["zh-TW","zh-Hant","cmn-Hant","zho"],"ZH-HANT":["zh-Hant","cmn-Hant","zh-TW","zho"],"YUE":["yue","yue-Hant","zh-HK","zho"],"YUE-HK":["yue-Hant","yue","zh-HK","zho"]}, 15 | } 16 | }, 17 | "YouTube": { 18 | "Settings":{"Switch":true,"Type":"Official","Types":["Translate","External"],"Languages":["AUTO","ZH"],"AutoCC":true,"ShowOnly":false}, 19 | "Configs":{ 20 | "Languages":{"BG":"bg-BG","CS":"cs","DA":"da-DK","DE":"de","EL":"el","EN":"en","EN-GB":"en-GB","EN-US":"en-US","EN-US SDH":"en-US SDH","ES":"es","ES-419":"es-419","ES-ES":"es-ES","ET":"et-EE","FI":"fi","FR":"fr","HU":"hu-HU","ID":"id","IS":"is-IS","IT":"it","JA":"ja","KO":"ko","LT":"lt-LT","LV":"lv-LV","NL":"nl-NL","NO":"nb-NO","PL":"pl-PL","PT":"pt","PT-PT":"pt-PT","PT-BR":"pt-BR","RO":"ro-RO","RU":"ru-RU","SK":"sk-SK","SL":"sl-SI","SV":"sv-SE","YUE":"yue","YUE-HK":"yue-HK","ZH":"zh","ZH-HANS":"zh-Hans","ZH-HK":"zh-Hant-HK","ZH-HANT":"zh-Hant","ZH-TW":"zh-TW"}, 21 | "translationLanguages":{ 22 | "DESKTOP":[{"languageCode":"sq","languageName":{"simpleText":"Shqip - 阿尔巴尼亚语"}},{"languageCode":"ak","languageName":{"simpleText":"Ákán - 阿肯语"}},{"languageCode":"ar","languageName":{"simpleText":"العربية - 阿拉伯语"}},{"languageCode":"am","languageName":{"simpleText":"አማርኛ - 阿姆哈拉语"}},{"languageCode":"as","languageName":{"simpleText":"অসমীয়া - 阿萨姆语"}},{"languageCode":"az","languageName":{"simpleText":"آذربايجان ديلی - 阿塞拜疆语"}},{"languageCode":"ee","languageName":{"simpleText":"Èʋegbe - 埃维语"}},{"languageCode":"ay","languageName":{"simpleText":"Aymar aru - 艾马拉语"}},{"languageCode":"ga","languageName":{"simpleText":"Gaeilge - 爱尔兰语"}},{"languageCode":"et","languageName":{"simpleText":"Eesti - 爱沙尼亚语"}},{"languageCode":"or","languageName":{"simpleText":"ଓଡ଼ିଆ - 奥里亚语"}},{"languageCode":"om","languageName":{"simpleText":"Afaan Oromoo - 奥罗莫语"}},{"languageCode":"eu","languageName":{"simpleText":"Euskara - 巴斯克语"}},{"languageCode":"be","languageName":{"simpleText":"Беларуская - 白俄罗斯语"}},{"languageCode":"bg","languageName":{"simpleText":"Български - 保加利亚语"}},{"languageCode":"nso","languageName":{"simpleText":"Sesotho sa Leboa - 北索托语"}},{"languageCode":"is","languageName":{"simpleText":"Íslenska - 冰岛语"}},{"languageCode":"pl","languageName":{"simpleText":"Polski - 波兰语"}},{"languageCode":"bs","languageName":{"simpleText":"Bosanski - 波斯尼亚语"}},{"languageCode":"fa","languageName":{"simpleText":"فارسی - 波斯语"}},{"languageCode":"bho","languageName":{"simpleText":"भोजपुरी - 博杰普尔语"}},{"languageCode":"ts","languageName":{"simpleText":"Xitsonga - 聪加语"}},{"languageCode":"tt","languageName":{"simpleText":"Татарча - 鞑靼语"}},{"languageCode":"da","languageName":{"simpleText":"Dansk - 丹麦语"}},{"languageCode":"de","languageName":{"simpleText":"Deutsch - 德语"}},{"languageCode":"dv","languageName":{"simpleText":"ދިވެހިބަސް - 迪维希语"}},{"languageCode":"ru","languageName":{"simpleText":"Русский - 俄语"}},{"languageCode":"fr","languageName":{"simpleText":"français - 法语"}},{"languageCode":"sa","languageName":{"simpleText":"संस्कृतम् - 梵语"}},{"languageCode":"fil","languageName":{"simpleText":"Filipino - 菲律宾语"}},{"languageCode":"fi","languageName":{"simpleText":"suomi - 芬兰语"}},{"languageCode":"km","languageName":{"simpleText":"ភាសាខ្មែរ - 高棉语"}},{"languageCode":"ka","languageName":{"simpleText":"ქართული - 格鲁吉亚语"}},{"languageCode":"gu","languageName":{"simpleText":"ગુજરાતી - 古吉拉特语"}},{"languageCode":"gn","languageName":{"simpleText":"Avañe'ẽ - 瓜拉尼语"}},{"languageCode":"kk","languageName":{"simpleText":"Қазақ тілі - 哈萨克语"}},{"languageCode":"ht","languageName":{"simpleText":"Kreyòl ayisyen - 海地克里奥尔语"}},{"languageCode":"ko","languageName":{"simpleText":"한국어 - 韩语"}},{"languageCode":"ha","languageName":{"simpleText":"هَوُسَ - 豪萨语"}},{"languageCode":"nl","languageName":{"simpleText":"Nederlands - 荷兰语"}},{"languageCode":"gl","languageName":{"simpleText":"Galego - 加利西亚语"}},{"languageCode":"ca","languageName":{"simpleText":"català - 加泰罗尼亚语"}},{"languageCode":"cs","languageName":{"simpleText":"čeština - 捷克语"}},{"languageCode":"kn","languageName":{"simpleText":"ಕನ್ನಡ - 卡纳达语"}},{"languageCode":"ky","languageName":{"simpleText":"кыргыз тили - 吉尔吉斯语"}},{"languageCode":"xh","languageName":{"simpleText":"isiXhosa - 科萨语"}},{"languageCode":"co","languageName":{"simpleText":"corsu - 科西嘉语"}},{"languageCode":"hr","languageName":{"simpleText":"hrvatski - 克罗地亚语"}},{"languageCode":"qu","languageName":{"simpleText":"Runa Simi - 克丘亚语"}},{"languageCode":"ku","languageName":{"simpleText":"Kurdî - 库尔德语"}},{"languageCode":"la","languageName":{"simpleText":"lingua latīna - 拉丁语"}},{"languageCode":"lv","languageName":{"simpleText":"latviešu valoda - 拉脱维亚语"}},{"languageCode":"lo","languageName":{"simpleText":"ພາສາລາວ - 老挝语"}},{"languageCode":"lt","languageName":{"simpleText":"lietuvių kalba - 立陶宛语"}},{"languageCode":"ln","languageName":{"simpleText":"lingála - 林加拉语"}},{"languageCode":"lg","languageName":{"simpleText":"Luganda - 卢干达语"}},{"languageCode":"lb","languageName":{"simpleText":"Lëtzebuergesch - 卢森堡语"}},{"languageCode":"rw","languageName":{"simpleText":"Kinyarwanda - 卢旺达语"}},{"languageCode":"ro","languageName":{"simpleText":"Română - 罗马尼亚语"}},{"languageCode":"mt","languageName":{"simpleText":"Malti - 马耳他语"}},{"languageCode":"mr","languageName":{"simpleText":"मराठी - 马拉地语"}},{"languageCode":"mg","languageName":{"simpleText":"Malagasy - 马拉加斯语"}},{"languageCode":"ml","languageName":{"simpleText":"മലയാളം - 马拉雅拉姆语"}},{"languageCode":"ms","languageName":{"simpleText":"bahasa Melayu - 马来语"}},{"languageCode":"mk","languageName":{"simpleText":"македонски јазик - 马其顿语"}},{"languageCode":"mi","languageName":{"simpleText":"te reo Māori - 毛利语"}},{"languageCode":"mn","languageName":{"simpleText":"Монгол хэл - 蒙古语"}},{"languageCode":"bn","languageName":{"simpleText":"বাংলা - 孟加拉语"}},{"languageCode":"my","languageName":{"simpleText":"ဗမာစာ - 缅甸语"}},{"languageCode":"hmn","languageName":{"simpleText":"Hmoob - 苗语"}},{"languageCode":"af","languageName":{"simpleText":"Afrikaans - 南非荷兰语"}},{"languageCode":"st","languageName":{"simpleText":"Sesotho - 南索托语"}},{"languageCode":"ne","languageName":{"simpleText":"नेपाली - 尼泊尔语"}},{"languageCode":"no","languageName":{"simpleText":"Norsk - 挪威语"}},{"languageCode":"pa","languageName":{"simpleText":"ਪੰਜਾਬੀ - 旁遮普语"}},{"languageCode":"pt","languageName":{"simpleText":"Português - 葡萄牙语"}},{"languageCode":"ps","languageName":{"simpleText":"پښتو - 普什图语"}},{"languageCode":"ny","languageName":{"simpleText":"chiCheŵa - 齐切瓦语"}},{"languageCode":"ja","languageName":{"simpleText":"日本語 - 日语"}},{"languageCode":"sv","languageName":{"simpleText":"Svenska - 瑞典语"}},{"languageCode":"sm","languageName":{"simpleText":"Gagana fa'a Samoa - 萨摩亚语"}},{"languageCode":"sr","languageName":{"simpleText":"Српски језик - 塞尔维亚语"}},{"languageCode":"si","languageName":{"simpleText":"සිංහල - 僧伽罗语"}},{"languageCode":"sn","languageName":{"simpleText":"ChiShona - 绍纳语"}},{"languageCode":"eo","languageName":{"simpleText":"Esperanto - 世界语"}},{"languageCode":"sk","languageName":{"simpleText":"slovenčina - 斯洛伐克语"}},{"languageCode":"sl","languageName":{"simpleText":"slovenščina - 斯洛文尼亚语"}},{"languageCode":"sw","languageName":{"simpleText":"Kiswahili - 斯瓦希里语"}},{"languageCode":"gd","languageName":{"simpleText":"Gàidhlig - 苏格兰盖尔语"}},{"languageCode":"ceb","languageName":{"simpleText":"Binisaya - 宿务语"}},{"languageCode":"so","languageName":{"simpleText":"Soomaaliga - 索马里语"}},{"languageCode":"tg","languageName":{"simpleText":"тоҷикӣ - 塔吉克语"}},{"languageCode":"te","languageName":{"simpleText":"తెలుగు - 泰卢固语"}},{"languageCode":"ta","languageName":{"simpleText":"தமிழ் - 泰米尔语"}},{"languageCode":"th","languageName":{"simpleText":"ไทย - 泰语"}},{"languageCode":"ti","languageName":{"simpleText":"ትግርኛ - 提格利尼亚语"}},{"languageCode":"tr","languageName":{"simpleText":"Türkçe - 土耳其语"}},{"languageCode":"tk","languageName":{"simpleText":"Türkmen - 土库曼语"}},{"languageCode":"cy","languageName":{"simpleText":"Cymraeg - 威尔士语"}},{"languageCode":"ug","languageName":{"simpleText":"ئۇيغۇرچە - 维吾尔语"}},{"languageCode":"und","languageName":{"simpleText":"Unknown - 未知语言"}},{"languageCode":"ur","languageName":{"simpleText":"اردو - 乌尔都语"}},{"languageCode":"uk","languageName":{"simpleText":"українська - 乌克兰语"}},{"languageCode":"uz","languageName":{"simpleText":"O'zbek - 乌兹别克语"}},{"languageCode":"es","languageName":{"simpleText":"Español - 西班牙语"}},{"languageCode":"fy","languageName":{"simpleText":"Frysk - 西弗里西亚语"}},{"languageCode":"iw","languageName":{"simpleText":"עברית - 希伯来语"}},{"languageCode":"el","languageName":{"simpleText":"Ελληνικά - 希腊语"}},{"languageCode":"haw","languageName":{"simpleText":"ʻŌlelo Hawaiʻi - 夏威夷语"}},{"languageCode":"sd","languageName":{"simpleText":"سنڌي - 信德语"}},{"languageCode":"hu","languageName":{"simpleText":"magyar - 匈牙利语"}},{"languageCode":"su","languageName":{"simpleText":"Basa Sunda - 巽他语"}},{"languageCode":"hy","languageName":{"simpleText":"հայերեն - 亚美尼亚语"}},{"languageCode":"ig","languageName":{"simpleText":"Igbo - 伊博语"}},{"languageCode":"it","languageName":{"simpleText":"Italiano - 意大利语"}},{"languageCode":"yi","languageName":{"simpleText":"ייִדיש - 意第绪语"}},{"languageCode":"hi","languageName":{"simpleText":"हिन्दी - 印地语"}},{"languageCode":"id","languageName":{"simpleText":"Bahasa Indonesia - 印度尼西亚语"}},{"languageCode":"en","languageName":{"simpleText":"English - 英语"}},{"languageCode":"yo","languageName":{"simpleText":"Yorùbá - 约鲁巴语"}},{"languageCode":"vi","languageName":{"simpleText":"Tiếng Việt - 越南语"}},{"languageCode":"jv","languageName":{"simpleText":"Basa Jawa - 爪哇语"}},{"languageCode":"zh-Hant","languageName":{"simpleText":"中文(繁體)- 中文(繁体)"}},{"languageCode":"zh-Hans","languageName":{"simpleText":"中文(简体)"}},{"languageCode":"zu","languageName":{"simpleText":"isiZulu - 祖鲁语"}},{"languageCode":"kri","languageName":{"simpleText":"Krìì - 克里语"}}], 23 | "MOBILE":[{"languageCode":"sq","languageName":{"runs":[{"text":"Shqip - 阿尔巴尼亚语"}]}},{"languageCode":"ak","languageName":{"runs":[{"text":"Ákán - 阿肯语"}]}},{"languageCode":"ar","languageName":{"runs":[{"text":"العربية - 阿拉伯语"}]}},{"languageCode":"am","languageName":{"runs":[{"text":"አማርኛ - 阿姆哈拉语"}]}},{"languageCode":"as","languageName":{"runs":[{"text":"অসমীয়া - 阿萨姆语"}]}},{"languageCode":"az","languageName":{"runs":[{"text":"Azərbaycanca - 阿塞拜疆语"}]}},{"languageCode":"ee","languageName":{"runs":[{"text":"Eʋegbe - 埃维语"}]}},{"languageCode":"ay","languageName":{"runs":[{"text":"Aymar - 艾马拉语"}]}},{"languageCode":"ga","languageName":{"runs":[{"text":"Gaeilge - 爱尔兰语"}]}},{"languageCode":"et","languageName":{"runs":[{"text":"Eesti - 爱沙尼亚语"}]}},{"languageCode":"or","languageName":{"runs":[{"text":"ଓଡ଼ିଆ - 奥里亚语"}]}},{"languageCode":"om","languageName":{"runs":[{"text":"Oromoo - 奥罗莫语"}]}},{"languageCode":"eu","languageName":{"runs":[{"text":"Euskara - 巴斯克语"}]}},{"languageCode":"be","languageName":{"runs":[{"text":"Беларуская - 白俄罗斯语"}]}},{"languageCode":"bg","languageName":{"runs":[{"text":"Български - 保加利亚语"}]}},{"languageCode":"nso","languageName":{"runs":[{"text":"Sesotho sa Leboa - 北索托语"}]}},{"languageCode":"is","languageName":{"runs":[{"text":"Íslenska - 冰岛语"}]}},{"languageCode":"pl","languageName":{"runs":[{"text":"Polski - 波兰语"}]}},{"languageCode":"bs","languageName":{"runs":[{"text":"Bosanski - 波斯尼亚语"}]}},{"languageCode":"fa","languageName":{"runs":[{"text":"فارسی - 波斯语"}]}},{"languageCode":"bho","languageName":{"runs":[{"text":"भोजपुरी - 博杰普尔语"}]}},{"languageCode":"ts","languageName":{"runs":[{"text":"Xitsonga - 聪加语"}]}},{"languageCode":"tt","languageName":{"runs":[{"text":"Татарча - 鞑靼语"}]}},{"languageCode":"da","languageName":{"runs":[{"text":"Dansk - 丹麦语"}]}},{"languageCode":"de","languageName":{"runs":[{"text":"Deutsch - 德语"}]}},{"languageCode":"dv","languageName":{"runs":[{"text":"ދިވެހިބަސް - 迪维希语"}]}},{"languageCode":"ru","languageName":{"runs":[{"text":"Русский - 俄语"}]}},{"languageCode":"fr","languageName":{"runs":[{"text":"Français - 法语"}]}},{"languageCode":"sa","languageName":{"runs":[{"text":"संस्कृतम् - 梵语"}]}},{"languageCode":"fil","languageName":{"runs":[{"text":"Filipino - 菲律宾语"}]}},{"languageCode":"fi","languageName":{"runs":[{"text":"Suomi - 芬兰语"}]}},{"languageCode":"km","languageName":{"runs":[{"text":"ភាសាខ្មែរ - 高棉语"}]}},{"languageCode":"ka","languageName":{"runs":[{"text":"ქართული - 格鲁吉亚语"}]}},{"languageCode":"gu","languageName":{"runs":[{"text":"ગુજરાતી - 古吉拉特语"}]}},{"languageCode":"gn","languageName":{"runs":[{"text":"Avañe'ẽ - 瓜拉尼语"}]}},{"languageCode":"kk","languageName":{"runs":[{"text":"Қазақ тілі - 哈萨克语"}]}},{"languageCode":"ht","languageName":{"runs":[{"text":"海地克里奥尔语"}]}},{"languageCode":"ko","languageName":{"runs":[{"text":"한국말 - 韩语"}]}},{"languageCode":"ha","languageName":{"runs":[{"text":"هَوُسَ - 豪萨语"}]}},{"languageCode":"nl","languageName":{"runs":[{"text":"Nederlands - 荷兰语"}]}},{"languageCode":"gl","languageName":{"runs":[{"text":"Galego - 加利西亚语"}]}},{"languageCode":"ca","languageName":{"runs":[{"text":"Català - 加泰罗尼亚语"}]}},{"languageCode":"cs","languageName":{"runs":[{"text":"Čeština - 捷克语"}]}},{"languageCode":"kn","languageName":{"runs":[{"text":"ಕನ್ನಡ - 卡纳达语"}]}},{"languageCode":"ky","languageName":{"runs":[{"text":"Кыргызча - 吉尔吉斯语"}]}},{"languageCode":"xh","languageName":{"runs":[{"text":"isiXhosa - 科萨语"}]}},{"languageCode":"co","languageName":{"runs":[{"text":"Corsu - 科西嘉语"}]}},{"languageCode":"hr","languageName":{"runs":[{"text":"Hrvatski - 克罗地亚语"}]}},{"languageCode":"qu","languageName":{"runs":[{"text":"Runa Simi - 克丘亚语"}]}},{"languageCode":"ku","languageName":{"runs":[{"text":"Kurdî - 库尔德语"}]}},{"languageCode":"la","languageName":{"runs":[{"text":"lingua latīna - 拉丁语"}]}},{"languageCode":"lv","languageName":{"runs":[{"text":"Latviešu - 拉脱维亚语"}]}},{"languageCode":"lo","languageName":{"runs":[{"text":"ລາວ - 老挝语"}]}},{"languageCode":"lt","languageName":{"runs":[{"text":"Lietuvių - 立陶宛语"}]}},{"languageCode":"ln","languageName":{"runs":[{"text":"Lingála - 林加拉语"}]}},{"languageCode":"lg","languageName":{"runs":[{"text":"Luganda - 卢干达语"}]}},{"languageCode":"lb","languageName":{"runs":[{"text":"Lëtzebuergesch - 卢森堡语"}]}},{"languageCode":"rw","languageName":{"runs":[{"text":"Kinyarwanda - 卢旺达语"}]}},{"languageCode":"ro","languageName":{"runs":[{"text":"Română - 罗马尼亚语"}]}},{"languageCode":"mt","languageName":{"runs":[{"text":"Malti - 马耳他语"}]}},{"languageCode":"mr","languageName":{"runs":[{"text":"मराठी - 马拉地语"}]}},{"languageCode":"mg","languageName":{"runs":[{"text":"Malagasy - 马拉加斯语"}]}},{"languageCode":"ml","languageName":{"runs":[{"text":"മലയാളം - 马拉雅拉姆语"}]}},{"languageCode":"ms","languageName":{"runs":[{"text":"Bahasa Melayu - 马来语"}]}},{"languageCode":"mk","languageName":{"runs":[{"text":"македонски - 马其顿语"}]}},{"languageCode":"mi","languageName":{"runs":[{"text":"Māori - 毛利语"}]}},{"languageCode":"mn","languageName":{"runs":[{"text":"Монгол - 蒙古语"}]}},{"languageCode":"bn","languageName":{"runs":[{"text":"বাংলা - 孟加拉语"}]}},{"languageCode":"my","languageName":{"runs":[{"text":"ဗမာစာ - 缅甸语"}]}},{"languageCode":"hmn","languageName":{"runs":[{"text":"Hmoob - 苗语"}]}},{"languageCode":"af","languageName":{"runs":[{"text":"Afrikaans - 南非荷兰语"}]}},{"languageCode":"st","languageName":{"runs":[{"text":"Sesotho - 南索托语"}]}},{"languageCode":"ne","languageName":{"runs":[{"text":"नेपाली - 尼泊尔语"}]}},{"languageCode":"no","languageName":{"runs":[{"text":"Norsk - 挪威语"}]}},{"languageCode":"pa","languageName":{"runs":[{"text":"ਪੰਜਾਬੀ - 旁遮普语"}]}},{"languageCode":"pt","languageName":{"runs":[{"text":"Português - 葡萄牙语"}]}},{"languageCode":"ps","languageName":{"runs":[{"text":"پښتو - 普什图语"}]}},{"languageCode":"ny","languageName":{"runs":[{"text":"chiCheŵa - 齐切瓦语"}]}},{"languageCode":"ja","languageName":{"runs":[{"text":"日本語 - 日语"}]}},{"languageCode":"sv","languageName":{"runs":[{"text":"Svenska - 瑞典语"}]}},{"languageCode":"sm","languageName":{"runs":[{"text":"Gagana Samoa - 萨摩亚语"}]}},{"languageCode":"sr","languageName":{"runs":[{"text":"Српски језик - 塞尔维亚语"}]}},{"languageCode":"si","languageName":{"runs":[{"text":"සිංහල - 僧伽罗语"}]}},{"languageCode":"sn","languageName":{"runs":[{"text":"ChiShona - 绍纳语"}]}},{"languageCode":"eo","languageName":{"runs":[{"text":"Esperanto - 世界语"}]}},{"languageCode":"sk","languageName":{"runs":[{"text":"Slovenčina - 斯洛伐克语"}]}},{"languageCode":"sl","languageName":{"runs":[{"text":"Slovenščina - 斯洛文尼亚语"}]}},{"languageCode":"sw","languageName":{"runs":[{"text":"Kiswahili - 斯瓦希里语"}]}},{"languageCode":"gd","languageName":{"runs":[{"text":"Gàidhlig - 苏格兰盖尔语"}]}},{"languageCode":"ceb","languageName":{"runs":[{"text":"Cebuano - 宿务语"}]}},{"languageCode":"so","languageName":{"runs":[{"text":"Soomaaliga - 索马里语"}]}},{"languageCode":"tg","languageName":{"runs":[{"text":"тоҷикӣ - 塔吉克语"}]}},{"languageCode":"te","languageName":{"runs":[{"text":"తెలుగు - 泰卢固语"}]}},{"languageCode":"ta","languageName":{"runs":[{"text":"தமிழ் - 泰米尔语"}]}},{"languageCode":"th","languageName":{"runs":[{"text":"ไทย - 泰语"}]}},{"languageCode":"ti","languageName":{"runs":[{"text":"ትግርኛ - 提格利尼亚语"}]}},{"languageCode":"tr","languageName":{"runs":[{"text":"Türkçe - 土耳其语"}]}},{"languageCode":"tk","languageName":{"runs":[{"text":"Türkmen - 土库曼语"}]}},{"languageCode":"cy","languageName":{"runs":[{"text":"Cymraeg - 威尔士语"}]}},{"languageCode":"ug","languageName":{"runs":[{"text":"ئۇيغۇرچە - 维吾尔语"}]}},{"languageCode":"und","languageName":{"runs":[{"text":"Unknown - 未知语言"}]}},{"languageCode":"ur","languageName":{"runs":[{"text":"اردو - 乌尔都语"}]}},{"languageCode":"uk","languageName":{"runs":[{"text":"Українська - 乌克兰语"}]}},{"languageCode":"uz","languageName":{"runs":[{"text":"O‘zbek - 乌兹别克语"}]}},{"languageCode":"es","languageName":{"runs":[{"text":"Español - 西班牙语"}]}},{"languageCode":"fy","languageName":{"runs":[{"text":"Frysk - 西弗里西亚语"}]}},{"languageCode":"iw","languageName":{"runs":[{"text":"עברית - 希伯来语"}]}},{"languageCode":"el","languageName":{"runs":[{"text":"Ελληνικά - 希腊语"}]}},{"languageCode":"haw","languageName":{"runs":[{"text":"ʻŌlelo Hawaiʻi - 夏威夷语"}]}},{"languageCode":"sd","languageName":{"runs":[{"text":"سنڌي - 信德语"}]}},{"languageCode":"hu","languageName":{"runs":[{"text":"Magyar - 匈牙利语"}]}},{"languageCode":"su","languageName":{"runs":[{"text":"Basa Sunda - 巽他语"}]}},{"languageCode":"hy","languageName":{"runs":[{"text":"Հայերեն - 亚美尼亚语"}]}},{"languageCode":"ig","languageName":{"runs":[{"text":"Igbo - 伊博语"}]}},{"languageCode":"it","languageName":{"runs":[{"text":"Italiano - 意大利语"}]}},{"languageCode":"yi","languageName":{"runs":[{"text":"ייִדיש - 意第绪语"}]}},{"languageCode":"hi","languageName":{"runs":[{"text":"हिन्दी - 印地语"}]}},{"languageCode":"id","languageName":{"runs":[{"text":"Bahasa Indonesia - 印度尼西亚语"}]}},{"languageCode":"en","languageName":{"runs":[{"text":"English - 英语"}]}},{"languageCode":"yo","languageName":{"runs":[{"text":"Yorùbá - 约鲁巴语"}]}},{"languageCode":"vi","languageName":{"runs":[{"text":"Tiếng Việt - 越南语"}]}},{"languageCode":"jv","languageName":{"runs":[{"text":"Basa Jawa - 爪哇语"}]}},{"languageCode":"zh-Hant","languageName":{"runs":[{"text":"中文(繁體) - 中文(繁体)"}]}},{"languageCode":"zh-Hans","languageName":{"runs":[{"text":"中文(简体)"}]}},{"languageCode":"zu","languageName":{"runs":[{"text":"isiZulu - 祖鲁语"}]}},{"languageCode":"kri","languageName":{"runs":[{"text":"Krìì - 克里语"}]}}] 24 | } 25 | } 26 | }, 27 | "Netflix":{ 28 | "Settings":{"Switch":true,"Type":"Translate","Languages":["AUTO","ZH"]}, 29 | "Configs":{ 30 | "Languages":{"AR":"ar","CS":"cs","DA":"da","DE":"de","EN":"en","EN-GB":"en-GB","EN-US":"en-US","EN-US SDH":"en-US SDH","ES":"es","ES-419":"es-419","ES-ES":"es-ES","FI":"fi","FR":"fr","HE":"he","HR":"hr","HU":"hu","ID":"id","IT":"it","JA":"ja","KO":"ko","MS":"ms","NB":"nb","NL":"nl","PL":"pl","PT":"pt","PT-PT":"pt-PT","PT-BR":"pt-BR","RO":"ro","RU":"ru","SV":"sv","TH":"th","TR":"tr","UK":"uk","VI":"vi","IS":"is","ZH":"zh","ZH-HANS":"zh-Hans","ZH-HK":"zh-HK","ZH-HANT":"zh-Hant"} 31 | } 32 | }, 33 | "Spotify":{ 34 | "Settings":{"Switch":true,"Types":["Translate","External"],"Languages":["AUTO","ZH"]} 35 | }, 36 | "Composite":{ 37 | "Settings":{"CacheSize":20,"ShowOnly":false,"Position":"Reverse","Offset":0,"Tolerance":1000} 38 | }, 39 | "Translate":{ 40 | "Settings":{"Vendor":"Google","ShowOnly":false,"Position":"Forward","CacheSize":10,"Method":"Part","Times":3,"Interval":500,"Exponential":true}, 41 | "Configs":{ 42 | "Languages":{ 43 | "Google":{"AUTO":"auto","AF":"af","AM":"am","AR":"ar","AS":"as","AY":"ay","AZ":"az","BG":"bg","BE":"be","BM":"bm","BN":"bn","BHO":"bho","CS":"cs","DA":"da","DE":"de","EL":"el","EU":"eu","EN":"en","EN-GB":"en","EN-US":"en","EN-US SDH":"en","ES":"es","ES-419":"es","ES-ES":"es","ET":"et","FI":"fi","FR":"fr","FR-CA":"fr","HU":"hu","IS":"is","IT":"it","JA":"ja","KO":"ko","LT":"lt","LV":"lv","NL":"nl","NO":"no","PL":"pl","PT":"pt","PT-PT":"pt","PT-BR":"pt","PA":"pa","RO":"ro","RU":"ru","SK":"sk","SL":"sl","SQ":"sq","ST":"st","SV":"sv","TH":"th","TR":"tr","UK":"uk","UR":"ur","VI":"vi","ZH":"zh","ZH-HANS":"zh-CN","ZH-HK":"zh-TW","ZH-HANT":"zh-TW"}, 44 | "Microsoft":{"AUTO":"","AF":"af","AM":"am","AR":"ar","AS":"as","AY":"ay","AZ":"az","BG":"bg","BE":"be","BM":"bm","BN":"bn","BHO":"bho","CS":"cs","DA":"da","DE":"de","EL":"el","EU":"eu","EN":"en","EN-GB":"en","EN-US":"en","EN-US SDH":"en","ES":"es","ES-419":"es","ES-ES":"es","ET":"et","FI":"fi","FR":"fr","FR-CA":"fr-ca","HU":"hu","IS":"is","IT":"it","JA":"ja","KO":"ko","LT":"lt","LV":"lv","NL":"nl","NO":"no","PL":"pl","PT":"pt","PT-PT":"pt-pt","PT-BR":"pt","PA":"pa","RO":"ro","RU":"ru","SK":"sk","SL":"sl","SQ":"sq","ST":"st","SV":"sv","TH":"th","TR":"tr","UK":"uk","UR":"ur","VI":"vi","ZH":"zh-Hans","ZH-HANS":"zh-Hans","ZH-HK":"yue","ZH-HANT":"zh-Hant"}, 45 | "DeepL":{"AUTO":"","BG":"BG","CS":"CS","DA":"DA","DE":"de","EL":"el","EN":"EN-US","EN-GB":"EN-GB","EN-US":"EN-US","EN-US SDH":"EN-US","ES":"ES","ES-419":"ES","ES-ES":"ES","ET":"ET","FI":"FI","FR":"FR","HU":"HU","IT":"IT","JA":"JA","KO":"ko","LT":"LT","LV":"LV","NL":"NL","PL":"PL","PT":"PT-PT","PT-PT":"PT-PT","PT-BR":"PT-BR","RO":"RO","RU":"RU","SK":"SK","SL":"SL","SV":"SV","TR":"TR","ZH":"ZH","ZH-HANS":"ZH","ZH-HK":"ZH","ZH-HANT":"ZH"} 46 | } 47 | } 48 | }, 49 | "External":{ 50 | "Settings":{"SubVendor":"URL","LrcVendor":"QQMusic","CacheSize":50} 51 | }, 52 | "API":{ 53 | "Settings":{ 54 | "GoogleCloud":{"Version":"v2","Mode":"Key","Auth":undefined},"Microsoft":{"Version":"Azure","Mode":"Token","Region":undefined,"Auth":undefined},"DeepL":{"Version":"Free","Auth":undefined},"DeepLX":{"Endpoint":undefined,"Auth":undefined}, 55 | "URL":undefined,"NeteaseMusic":{"PhoneNumber":undefined,"Password":undefined} 56 | } 57 | } 58 | }; 59 | 60 | /***************** Processing *****************/ 61 | // 解构URL 62 | const URL = URI.parse($request.url); 63 | $.log(`⚠ ${$.name}`, `URL: ${JSON.stringify(URL)}`, ""); 64 | // 获取连接参数 65 | const METHOD = $request.method, HOST = URL.host, PATH = URL.path, PATHs = URL.paths; 66 | $.log(`⚠ ${$.name}`, `METHOD: ${METHOD}`, ""); 67 | // 获取平台 68 | const PLATFORM = detectPlatform(HOST); 69 | $.log(`⚠ ${$.name}, PLATFORM: ${PLATFORM}`, ""); 70 | // 解析格式 71 | let FORMAT = ($response.headers?.["Content-Type"] ?? $response.headers?.["content-type"])?.split(";")?.[0]; 72 | if (FORMAT === "application/octet-stream" || FORMAT === "text/plain") FORMAT = detectFormat(URL, $response?.body); 73 | $.log(`⚠ ${$.name}, FORMAT: ${FORMAT}`, ""); 74 | (async () => { 75 | // 读取设置 76 | const { Settings, Caches, Configs } = setENV("DualSubs", [(["YouTube", "Netflix", "BiliBili", "Spotify"].includes(PLATFORM)) ? PLATFORM : "Universal", "Translate", "API"], DataBase); 77 | $.log(`⚠ ${$.name}`, `Settings.Switch: ${Settings?.Switch}`, ""); 78 | switch (Settings.Switch) { 79 | case true: 80 | default: 81 | // 获取字幕类型与语言 82 | const Type = URL.query?.subtype ?? Settings.Type, Languages = [URL.query?.lang?.toUpperCase?.() ?? Settings.Languages[0], (URL.query?.tlang ?? Caches?.tlang)?.toUpperCase?.() ?? Settings.Languages[1]]; 83 | $.log(`⚠ ${$.name}, Type: ${Type}, Languages: ${Languages}`, ""); 84 | // 创建空数据 85 | let body = {}; 86 | // 格式判断 87 | switch (FORMAT) { 88 | case undefined: // 视为无body 89 | break; 90 | case "application/x-www-form-urlencoded": 91 | case "text/plain": 92 | case "text/html": 93 | default: 94 | break; 95 | case "application/x-mpegURL": 96 | case "application/x-mpegurl": 97 | case "application/vnd.apple.mpegurl": 98 | case "audio/mpegurl": 99 | break; 100 | case "text/xml": 101 | case "text/plist": 102 | case "application/xml": 103 | case "application/plist": 104 | case "application/x-plist": 105 | break; 106 | case "text/vtt": 107 | case "application/vtt": 108 | break; 109 | case "text/json": 110 | case "application/json": 111 | break; 112 | case "application/protobuf": 113 | case "application/x-protobuf": 114 | case "application/vnd.google.protobuf": 115 | case "application/grpc": 116 | case "application/grpc+proto": 117 | case "applecation/octet-stream": 118 | let rawBody = $.isQuanX() ? new Uint8Array($response.bodyBytes ?? []) : $response.body ?? new Uint8Array(); 119 | /****************** initialization start *******************/ 120 | // timostamm/protobuf-ts 2.9.0 121 | // text-decoder 122 | !function(i){"use strict";function _(n,e,i){return e<=n&&n<=i}"undefined"!=typeof module&&module.exports&&!i["encoding-indexes"]&&(i["encoding-indexes"]=require("./encoding-indexes.js")["encoding-indexes"]);var l=Math.floor;function s(n){if(void 0===n)return{};if(n===Object(n))return n;throw TypeError("Could not convert argument to dictionary")}function u(n){return 0<=n&&n<=127}var a=u,b=-1;function c(n){this.tokens=[].slice.call(n),this.tokens.reverse()}c.prototype={endOfStream:function(){return!this.tokens.length},read:function(){return this.tokens.length?this.tokens.pop():b},prepend:function(n){if(Array.isArray(n))for(var e=n;e.length;)this.tokens.push(e.pop());else this.tokens.push(n)},push:function(n){if(Array.isArray(n))for(var e=n;e.length;)this.tokens.unshift(e.shift());else this.tokens.unshift(n)}};var w=-1;function m(n,e){if(n)throw TypeError("Decoder error");return e||65533}function f(n){throw TypeError("The code point "+n+" could not be encoded.")}function r(n){return n=String(n).trim().toLowerCase(),Object.prototype.hasOwnProperty.call(d,n)?d[n]:null}var t,o,n=[{encodings:[{labels:["unicode-1-1-utf-8","utf-8","utf8"],name:"UTF-8"}],heading:"The Encoding"},{encodings:[{labels:["866","cp866","csibm866","ibm866"],name:"IBM866"},{labels:["csisolatin2","iso-8859-2","iso-ir-101","iso8859-2","iso88592","iso_8859-2","iso_8859-2:1987","l2","latin2"],name:"ISO-8859-2"},{labels:["csisolatin3","iso-8859-3","iso-ir-109","iso8859-3","iso88593","iso_8859-3","iso_8859-3:1988","l3","latin3"],name:"ISO-8859-3"},{labels:["csisolatin4","iso-8859-4","iso-ir-110","iso8859-4","iso88594","iso_8859-4","iso_8859-4:1988","l4","latin4"],name:"ISO-8859-4"},{labels:["csisolatincyrillic","cyrillic","iso-8859-5","iso-ir-144","iso8859-5","iso88595","iso_8859-5","iso_8859-5:1988"],name:"ISO-8859-5"},{labels:["arabic","asmo-708","csiso88596e","csiso88596i","csisolatinarabic","ecma-114","iso-8859-6","iso-8859-6-e","iso-8859-6-i","iso-ir-127","iso8859-6","iso88596","iso_8859-6","iso_8859-6:1987"],name:"ISO-8859-6"},{labels:["csisolatingreek","ecma-118","elot_928","greek","greek8","iso-8859-7","iso-ir-126","iso8859-7","iso88597","iso_8859-7","iso_8859-7:1987","sun_eu_greek"],name:"ISO-8859-7"},{labels:["csiso88598e","csisolatinhebrew","hebrew","iso-8859-8","iso-8859-8-e","iso-ir-138","iso8859-8","iso88598","iso_8859-8","iso_8859-8:1988","visual"],name:"ISO-8859-8"},{labels:["csiso88598i","iso-8859-8-i","logical"],name:"ISO-8859-8-I"},{labels:["csisolatin6","iso-8859-10","iso-ir-157","iso8859-10","iso885910","l6","latin6"],name:"ISO-8859-10"},{labels:["iso-8859-13","iso8859-13","iso885913"],name:"ISO-8859-13"},{labels:["iso-8859-14","iso8859-14","iso885914"],name:"ISO-8859-14"},{labels:["csisolatin9","iso-8859-15","iso8859-15","iso885915","iso_8859-15","l9"],name:"ISO-8859-15"},{labels:["iso-8859-16"],name:"ISO-8859-16"},{labels:["cskoi8r","koi","koi8","koi8-r","koi8_r"],name:"KOI8-R"},{labels:["koi8-ru","koi8-u"],name:"KOI8-U"},{labels:["csmacintosh","mac","macintosh","x-mac-roman"],name:"macintosh"},{labels:["dos-874","iso-8859-11","iso8859-11","iso885911","tis-620","windows-874"],name:"windows-874"},{labels:["cp1250","windows-1250","x-cp1250"],name:"windows-1250"},{labels:["cp1251","windows-1251","x-cp1251"],name:"windows-1251"},{labels:["ansi_x3.4-1968","ascii","cp1252","cp819","csisolatin1","ibm819","iso-8859-1","iso-ir-100","iso8859-1","iso88591","iso_8859-1","iso_8859-1:1987","l1","latin1","us-ascii","windows-1252","x-cp1252"],name:"windows-1252"},{labels:["cp1253","windows-1253","x-cp1253"],name:"windows-1253"},{labels:["cp1254","csisolatin5","iso-8859-9","iso-ir-148","iso8859-9","iso88599","iso_8859-9","iso_8859-9:1989","l5","latin5","windows-1254","x-cp1254"],name:"windows-1254"},{labels:["cp1255","windows-1255","x-cp1255"],name:"windows-1255"},{labels:["cp1256","windows-1256","x-cp1256"],name:"windows-1256"},{labels:["cp1257","windows-1257","x-cp1257"],name:"windows-1257"},{labels:["cp1258","windows-1258","x-cp1258"],name:"windows-1258"},{labels:["x-mac-cyrillic","x-mac-ukrainian"],name:"x-mac-cyrillic"}],heading:"Legacy single-byte encodings"},{encodings:[{labels:["chinese","csgb2312","csiso58gb231280","gb2312","gb_2312","gb_2312-80","gbk","iso-ir-58","x-gbk"],name:"GBK"},{labels:["gb18030"],name:"gb18030"}],heading:"Legacy multi-byte Chinese (simplified) encodings"},{encodings:[{labels:["big5","big5-hkscs","cn-big5","csbig5","x-x-big5"],name:"Big5"}],heading:"Legacy multi-byte Chinese (traditional) encodings"},{encodings:[{labels:["cseucpkdfmtjapanese","euc-jp","x-euc-jp"],name:"EUC-JP"},{labels:["csiso2022jp","iso-2022-jp"],name:"ISO-2022-JP"},{labels:["csshiftjis","ms932","ms_kanji","shift-jis","shift_jis","sjis","windows-31j","x-sjis"],name:"Shift_JIS"}],heading:"Legacy multi-byte Japanese encodings"},{encodings:[{labels:["cseuckr","csksc56011987","euc-kr","iso-ir-149","korean","ks_c_5601-1987","ks_c_5601-1989","ksc5601","ksc_5601","windows-949"],name:"EUC-KR"}],heading:"Legacy multi-byte Korean encodings"},{encodings:[{labels:["csiso2022kr","hz-gb-2312","iso-2022-cn","iso-2022-cn-ext","iso-2022-kr"],name:"replacement"},{labels:["utf-16be"],name:"UTF-16BE"},{labels:["utf-16","utf-16le"],name:"UTF-16LE"},{labels:["x-user-defined"],name:"x-user-defined"}],heading:"Legacy miscellaneous encodings"}],d={},h=(n.forEach(function(n){n.encodings.forEach(function(e){e.labels.forEach(function(n){d[n]=e})})}),{}),g={};function y(n,e){return e&&e[n]||null}function p(n,e){e=e.indexOf(n);return-1===e?null:e}function v(n){if("encoding-indexes"in i)return i["encoding-indexes"][n];throw Error("Indexes missing. Did you forget to include encoding-indexes.js first?")}var x="utf-8";function O(n,e){if(!(this instanceof O))throw TypeError("Called as a function. Did you forget 'new'?");n=void 0!==n?String(n):x,e=s(e),this._encoding=null,this._decoder=null,this._ignoreBOM=!1,this._BOMseen=!1,this._error_mode="replacement",this._do_not_flush=!1;var i=r(n);if(null===i||"replacement"===i.name)throw RangeError("Unknown encoding: "+n);if(g[i.name])return(n=this)._encoding=i,Boolean(e.fatal)&&(n._error_mode="fatal"),Boolean(e.ignoreBOM)&&(n._ignoreBOM=!0),Object.defineProperty||(this.encoding=n._encoding.name.toLowerCase(),this.fatal="fatal"===n._error_mode,this.ignoreBOM=n._ignoreBOM),n;throw Error("Decoder not present. Did you forget to include encoding-indexes.js first?")}function k(n,e){if(!(this instanceof k))throw TypeError("Called as a function. Did you forget 'new'?");e=s(e),this._encoding=null,this._encoder=null,this._do_not_flush=!1,this._fatal=Boolean(e.fatal)?"fatal":"replacement";if(Boolean(e.NONSTANDARD_allowLegacyEncoding)){e=r(n=void 0!==n?String(n):x);if(null===e||"replacement"===e.name)throw RangeError("Unknown encoding: "+n);if(!h[e.name])throw Error("Encoder not present. Did you forget to include encoding-indexes.js first?");this._encoding=e}else this._encoding=r("utf-8"),void 0!==n&&"console"in i&&console.warn("TextEncoder constructor called with encoding label, which is ignored.");return Object.defineProperty||(this.encoding=this._encoding.name.toLowerCase()),this}function e(n){var r=n.fatal,t=0,o=0,s=0,l=128,a=191;this.handler=function(n,e){if(e===b&&0!==s)return s=0,m(r);if(e===b)return w;if(0===s){if(_(e,0,127))return e;if(_(e,194,223))s=1,t=31&e;else if(_(e,224,239))224===e&&(l=160),237===e&&(a=159),s=2,t=15&e;else{if(!_(e,240,244))return m(r);240===e&&(l=144),244===e&&(a=143),s=3,t=7&e}return null}var i;return _(e,l,a)?(l=128,a=191,t=t<<6|63&e,(o+=1)!==s?null:(i=t,t=s=o=0,i)):(t=s=o=0,l=128,a=191,n.prepend(e),m(r))}}function E(n){n.fatal;this.handler=function(n,e){if(e===b)return w;if(a(e))return e;_(e,128,2047)?(i=1,r=192):_(e,2048,65535)?(i=2,r=224):_(e,65536,1114111)&&(i=3,r=240);for(var i,r,t=[(e>>6*i)+r];0>6*(i-1)),--i;return t}}function j(i,n){var r=n.fatal;this.handler=function(n,e){return e===b?w:u(e)?e:null===(e=i[e-128])?m(r):e}}function B(r,n){n.fatal;this.handler=function(n,e){var i;return e===b?w:a(e)?e:(null===(i=p(e,r))&&f(e),i+128)}}function S(n){var o=n.fatal,s=0,l=0,a=0;this.handler=function(n,e){var i,r,t;return e===b&&0===s&&0===l&&0===a?w:(e!==b||0===s&&0===l&&0===a||(a=l=s=0,m(o)),0!==a?(i=null,_(e,48,57)&&(i=function(n){if(39419>8,n=255&n;return e?[i,n]:[n,i]}function L(r,n){var t=n.fatal,o=null,s=null;this.handler=function(n,e){var i;return e!==b||null===o&&null===s?e===b&&null===o&&null===s?w:null===o?(o=e,null):(e=r?(o<<8)+e:(e<<8)+o,(o=null)!==s?(i=s,s=null,_(e,56320,57343)?65536+1024*(i-55296)+(e-56320):(n.prepend(A(e,r)),m(t))):_(e,55296,56319)?(s=e,null):_(e,56320,57343)?m(t):e):m(t)}}function M(r,n){n.fatal;this.handler=function(n,e){var i;return e===b?w:_(e,0,65535)?A(e,r):(i=A(55296+(e-65536>>10),r),e=A(56320+(e-65536&1023),r),i.concat(e))}}function N(n){n.fatal;this.handler=function(n,e){return e===b?w:u(e)?e:63360+e-128}}function q(n){n.fatal;this.handler=function(n,e){return e===b?w:a(e)?e:_(e,63360,63487)?e-63360+128:f(e)}}Object.defineProperty&&(Object.defineProperty(O.prototype,"encoding",{get:function(){return this._encoding.name.toLowerCase()}}),Object.defineProperty(O.prototype,"fatal",{get:function(){return"fatal"===this._error_mode}}),Object.defineProperty(O.prototype,"ignoreBOM",{get:function(){return this._ignoreBOM}})),O.prototype.decode=function(n,e){n="object"==typeof n&&n instanceof ArrayBuffer?new Uint8Array(n):"object"==typeof n&&"buffer"in n&&n.buffer instanceof ArrayBuffer?new Uint8Array(n.buffer,n.byteOffset,n.byteLength):new Uint8Array(0);e=s(e),this._do_not_flush||(this._decoder=g[this._encoding.name]({fatal:"fatal"===this._error_mode}),this._BOMseen=!1),this._do_not_flush=Boolean(e.stream);for(var i,r=new c(n),t=[];;){var o=r.read();if(o===b)break;if((i=this._decoder.handler(r,o))===w)break;null!==i&&(Array.isArray(i)?t.push.apply(t,i):t.push(i))}if(!this._do_not_flush){for(;(i=this._decoder.handler(r,r.read()))!==w&&(null!==i&&(Array.isArray(i)?t.push.apply(t,i):t.push(i)),!r.endOfStream()););this._decoder=null}return function(n){e=["UTF-8","UTF-16LE","UTF-16BE"],i=this._encoding.name,-1===e.indexOf(i)||this._ignoreBOM||this._BOMseen||(0>10),56320+(1023&s)))}return t}.call(this,t)},Object.defineProperty&&Object.defineProperty(k.prototype,"encoding",{get:function(){return this._encoding.name.toLowerCase()}}),k.prototype.encode=function(n,e){n=void 0===n?"":String(n),e=s(e),this._do_not_flush||(this._encoder=h[this._encoding.name]({fatal:"fatal"===this._fatal})),this._do_not_flush=Boolean(e.stream);for(var i,r=new c(function(n){for(var e=String(n),i=e.length,r=0,t=[];r{i.symbol=Symbol.for("protobuf-ts/unknown"),i.onRead=(e,r,t,a,n)=>{(s(r)?r[i.symbol]:r[i.symbol]=[]).push({no:t,wireType:a,data:n})},i.onWrite=(e,r,t)=>{for(var{no:a,wireType:n,data:s}of i.list(r))t.tag(a,n).raw(s)},i.list=(e,r)=>{return s(e)?(e=e[i.symbol],r?e.filter(e=>e.no==r):e):[]},i.last=(e,r)=>(0,i.list)(e,r).slice(-1)[0];const s=e=>e&&Array.isArray(e[i.symbol])})(UnknownFieldHandler=UnknownFieldHandler||{}); 125 | var UnknownFieldHandler,WireType=(e=>(e[e.Varint=0]="Varint",e[e.Bit64=1]="Bit64",e[e.LengthDelimited=2]="LengthDelimited",e[e.StartGroup=3]="StartGroup",e[e.EndGroup=4]="EndGroup",e[e.Bit32=5]="Bit32",e))(WireType||{});const MESSAGE_TYPE=Symbol.for("protobuf-ts/message-type");function lowerCamelCase(r){let t=!1;var a=[];for(let e=0;e(e[e.DOUBLE=1]="DOUBLE",e[e.FLOAT=2]="FLOAT",e[e.INT64=3]="INT64",e[e.UINT64=4]="UINT64",e[e.INT32=5]="INT32",e[e.FIXED64=6]="FIXED64",e[e.FIXED32=7]="FIXED32",e[e.BOOL=8]="BOOL",e[e.STRING=9]="STRING",e[e.BYTES=12]="BYTES",e[e.UINT32=13]="UINT32",e[e.SFIXED32=15]="SFIXED32",e[e.SFIXED64=16]="SFIXED64",e[e.SINT32=17]="SINT32",e[e.SINT64=18]="SINT64",e))(ScalarType||{}),LongType=(e=>(e[e.BIGINT=0]="BIGINT",e[e.STRING=1]="STRING",e[e.NUMBER=2]="NUMBER",e))(LongType||{}),RepeatType=(e=>(e[e.NO=0]="NO",e[e.PACKED=1]="PACKED",e[e.UNPACKED=2]="UNPACKED",e))(RepeatType||{});function normalizeFieldInfo(e){return e.localName=e.localName??lowerCamelCase(e.name),e.jsonName=e.jsonName??lowerCamelCase(e.name),e.repeat=e.repeat??0,e.opt=e.opt??(!e.repeat&&(!e.oneof&&"message"==e.kind)),e}function isOneofGroup(e){if("object"!=typeof e||null===e||!e.hasOwnProperty("oneofKind"))return!1;switch(typeof e.oneofKind){case"string":return void 0===e[e.oneofKind]?!1:2==Object.keys(e).length;case"undefined":return 1==Object.keys(e).length;default:return!1}}class ReflectionTypeCheck{constructor(e){this.fields=e.fields??[]}prepare(){if(!this.data){var e,r=[],t=[],a=[];for(e of this.fields)if(e.oneof)a.includes(e.oneof)||(a.push(e.oneof),r.push(e.oneof),t.push(e.oneof));else switch(t.push(e.localName),e.kind){case"scalar":case"enum":e.opt&&!e.repeat||r.push(e.localName);break;case"message":e.repeat&&r.push(e.localName);break;case"map":r.push(e.localName)}this.data={req:r,known:t,oneofs:Object.values(a)}}}is(e,a,n=!1){if(!(a<0)){if(null==e||"object"!=typeof e)return!1;this.prepare();let r=Object.keys(e),t=this.data;if(r.length!r.includes(e)))return!1;if(!n&&r.some(e=>!t.known.includes(e)))return!1;if(!(a<1)){for(const i of t.oneofs){const o=e[i];if(!isOneofGroup(o))return!1;if(void 0!==o.oneofKind){var s=this.fields.find(e=>e.localName===o.oneofKind);if(!s)return!1;if(!this.field(o[o.oneofKind],s,n,a))return!1}}for(const l of this.fields)if(void 0===l.oneof&&!this.field(e[l.localName],l,n,a))return!1}}return!0}field(e,r,t,a){var n=r.repeat;switch(r.kind){case"scalar":return void 0===e?r.opt:n?this.scalars(e,r.T,a,r.L):this.scalar(e,r.T,r.L);case"enum":return void 0===e?r.opt:n?this.scalars(e,ScalarType.INT32,a):this.scalar(e,ScalarType.INT32);case"message":return void 0===e?!0:n?this.messages(e,r.T(),t,a):this.message(e,r.T(),t,a);case"map":if("object"!=typeof e||null===e)return!1;if(a<2)return!0;if(!this.mapKeys(e,r.K,a))return!1;switch(r.V.kind){case"scalar":return this.scalars(Object.values(e),r.V.T,a,r.V.L);case"enum":return this.scalars(Object.values(e),ScalarType.INT32,a);case"message":return this.messages(Object.values(e),r.V.T(),t,a)}}return!0}message(e,r,t,a){return t?r.isAssignable(e,a):r.is(e,a)}messages(r,t,e,a){if(!Array.isArray(r))return!1;if(!(a<2))if(e){for(let e=0;eparseInt(e)),r,t);case ScalarType.BOOL:return this.scalars(a.slice(0,t).map(e=>"true"==e||"false"!=e&&e),r,t);default:return this.scalars(a,r,t,LongType.STRING)}}}function typeofJsonValue(e){var r=typeof e;if("object"==r){if(Array.isArray(e))return"array";if(null===e)return"null"}return r}function isJsonObject(e){return null!==e&&"object"==typeof e&&!Array.isArray(e)}let encTable="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split(""),decTable=[];for(let e=0;e>4,i=s,n=2;break;case 2:t[a++]=(15&i)<<4|(60&s)>>2,i=s,n=3;break;case 3:t[a++]=(3&i)<<6|s,n=0}}if(1==n)throw Error("invalid base64 string.");return t.subarray(0,a)}function base64encode(r){let t="",a=0,n,s=0;for(let e=0;e>2],s=(3&n)<<4,a=1;break;case 1:t+=encTable[s|n>>4],s=(15&n)<<2,a=2;break;case 2:t=(t+=encTable[s|n>>6])+encTable[63&n],a=0}return a&&(t=t+encTable[s]+"=",1==a&&(t+="=")),t}function varint64read(){let r=0,t=0;for(let e=0;e<28;e+=7){var a=this.buf[this.pos++];if(r|=(127&a)<>4,0==(128&e))return this.assertBounds(),[r,t];for(let e=3;e<=31;e+=7){var n=this.buf[this.pos++];if(t|=(127&n)<>>e,s=!(n>>>7==0&&0==t);if(a.push(255&(s?128|n:n)),!s)return}var e=r>>>28&15|(7&t)<<4,i=!(t>>3==0);if(a.push(255&(i?128|e:e)),i){for(let e=3;e<31;e+=7){var o=t>>>e,l=!(o>>>7==0);if(a.push(255&(l?128|o:o)),!l)return}a.push(t>>>31&1)}}decTable["-".charCodeAt(0)]=encTable.indexOf("+"),decTable["_".charCodeAt(0)]=encTable.indexOf("/");const TWO_PWR_32_DBL$1=4294967296;function int64fromString(t){var e="-"==t[0];e&&(t=t.slice(1));let a=0,n=0;function r(e,r){e=Number(t.slice(e,r));n*=1e6,(a=1e6*a+e)>=TWO_PWR_32_DBL$1&&(n+=a/TWO_PWR_32_DBL$1|0,a%=TWO_PWR_32_DBL$1)}return r(-24,-18),r(-18,-12),r(-12,-6),r(-6),[e,a,n]}function int64toString(e,r){if(r<=2097151)return""+(TWO_PWR_32_DBL$1*r+(e>>>0));var t=(e>>>24|r<<8)>>>0&16777215,r=r>>16&65535;let a=(16777215&e)+6777216*t+6710656*r,n=t+8147497*r,s=2*r;function i(e,r){e=e?String(e):"";return r?"0000000".slice(e.length)+e:e}return 1e7<=a&&(n+=Math.floor(a/1e7),a%=1e7),1e7<=n&&(s+=Math.floor(n/1e7),n%=1e7),i(s,0)+i(n,s)+i(a,1)}function varint32write(r,t){if(0<=r){for(;127>>=7;t.push(r)}else{for(let e=0;e<9;e++)t.push(127&r|128),r>>=7;t.push(1)}}function varint32read(){let r=this.buf[this.pos++];var e=127&r;if(0==(128&r))return this.assertBounds(),e;if(e|=(127&(r=this.buf[this.pos++]))<<7,0==(128&r))return this.assertBounds(),e;if(e|=(127&(r=this.buf[this.pos++]))<<14,0==(128&r))return this.assertBounds(),e;if(e|=(127&(r=this.buf[this.pos++]))<<21,0==(128&r))return this.assertBounds(),e;e|=(15&(r=this.buf[this.pos++]))<<28;for(let e=5;0!=(128&r)&&e<10;e++)r=this.buf[this.pos++];if(0!=(128&r))throw new Error("invalid varint");return this.assertBounds(),e>>>0}function detectBi(){var e=new DataView(new ArrayBuffer(8));return void 0!==globalThis.BigInt&&"function"==typeof e.getBigInt64&&"function"==typeof e.getBigUint64&&"function"==typeof e.setBigInt64&&"function"==typeof e.setBigUint64?{MIN:BigInt("-9223372036854775808"),MAX:BigInt("9223372036854775807"),UMIN:BigInt("0"),UMAX:BigInt("18446744073709551615"),C:BigInt,V:e}:void 0}const BI=detectBi();function assertBi(e){if(!e)throw new Error("BigInt unavailable, see https://github.com/timostamm/protobuf-ts/blob/v1.0.8/MANUAL.md#bigint-support")}const RE_DECIMAL_STR=/^-?[0-9]+$/,TWO_PWR_32_DBL=4294967296;class SharedPbLong{constructor(e,r){this.lo=0|e,this.hi=0|r}isZero(){return 0==this.lo&&0==this.hi}toNumber(){var e=this.hi*TWO_PWR_32_DBL+(this.lo>>>0);if(Number.isSafeInteger(e))return e;throw new Error("cannot convert to safe number")}}const _PbULong=class extends SharedPbLong{static from(e){if(BI)switch(typeof e){case"string":if("0"==e)return this.ZERO;if(""==e)throw new Error("string is no integer");e=BI.C(e);case"number":if(0===e)return this.ZERO;e=BI.C(e);case"bigint":if(!e)return this.ZERO;if(eBI.UMAX)throw new Error("ulong too large");return BI.V.setBigUint64(0,e,!0),new _PbULong(BI.V.getInt32(0,!0),BI.V.getInt32(4,!0))}else switch(typeof e){case"string":if("0"==e)return this.ZERO;if(e=e.trim(),!RE_DECIMAL_STR.test(e))throw new Error("string is no integer");var[r,t,a]=int64fromString(e);if(r)throw new Error("signed value");return new _PbULong(t,a);case"number":if(0==e)return this.ZERO;if(!Number.isSafeInteger(e))throw new Error("number is no integer");if(e<0)throw new Error("signed value for ulong");return new _PbULong(e,e/TWO_PWR_32_DBL)}throw new Error("unknown value "+typeof e)}toString(){return BI?this.toBigInt().toString():int64toString(this.lo,this.hi)}toBigInt(){return assertBi(BI),BI.V.setInt32(0,this.lo,!0),BI.V.setInt32(4,this.hi,!0),BI.V.getBigUint64(0,!0)}};let PbULong=_PbULong;PbULong.ZERO=new _PbULong(0,0);const _PbLong=class extends SharedPbLong{static from(e){if(BI)switch(typeof e){case"string":if("0"==e)return this.ZERO;if(""==e)throw new Error("string is no integer");e=BI.C(e);case"number":if(0===e)return this.ZERO;e=BI.C(e);case"bigint":if(!e)return this.ZERO;if(eBI.MAX)throw new Error("ulong too large");return BI.V.setBigInt64(0,e,!0),new _PbLong(BI.V.getInt32(0,!0),BI.V.getInt32(4,!0))}else switch(typeof e){case"string":if("0"==e)return this.ZERO;var r,t,a;if(e=e.trim(),RE_DECIMAL_STR.test(e))return[r,a,t]=int64fromString(e),a=new _PbLong(a,t),r?a.negate():a;throw new Error("string is no integer");case"number":if(0==e)return this.ZERO;if(Number.isSafeInteger(e))return 0INT32_MAX||eUINT32_MAX||e<0)throw new Error("invalid uint 32: "+e)}function assertFloat32(e){if("number"!=typeof e)throw new Error("invalid float 32: "+typeof e);if(Number.isFinite(e)&&(e>FLOAT32_MAX||e[e.no,e])))}read(a,n,s,e){this.prepare();for(var r=void 0===e?a.len:a.pos+e;a.pose.no-r.no))}write(n,s,i){this.prepare();for(const u of this.fields){let e,r,t=u.repeat,a=u.localName;if(u.oneof){var o=n[u.oneof];if(o.oneofKind!==a)continue;e=o[a],r=!0}else e=n[a],r=!1;switch(u.kind){case"scalar":case"enum":var l="enum"==u.kind?ScalarType.INT32:u.T;if(t)if(assert(Array.isArray(e)),t==RepeatType.PACKED)this.packed(s,l,u.no,e);else for(const h of e)this.scalar(s,l,u.no,h,!0);else void 0===e?assert(u.opt):this.scalar(s,l,u.no,e,r||u.opt);break;case"message":if(t){assert(Array.isArray(e));for(const p of e)this.message(s,i,u.T(),u.no,p)}else this.message(s,i,u.T(),u.no,e);break;case"map":assert("object"==typeof e&&null!==e);for(var[c,f]of Object.entries(e))this.mapEntry(s,i,u,c,f)}}var e=i.writeUnknownFields;!1!==e&&(!0===e?UnknownFieldHandler.onWrite:e)(this.info.typeName,n,s)}mapEntry(e,r,t,a,n){e.tag(t.no,WireType.LengthDelimited),e.fork();let s=a;switch(t.K){case ScalarType.INT32:case ScalarType.FIXED32:case ScalarType.UINT32:case ScalarType.SFIXED32:case ScalarType.SINT32:s=Number.parseInt(a);break;case ScalarType.BOOL:assert("true"==a||"false"==a),s="true"==a}switch(this.scalar(e,t.K,1,s,!0),t.V.kind){case"scalar":this.scalar(e,t.V.T,2,n,!0);break;case"enum":this.scalar(e,ScalarType.INT32,2,n,!0);break;case"message":this.message(e,r,t.V.T(),2,n)}e.join()}message(e,r,t,a,n){void 0!==n&&(t.internalBinaryWrite(n,e.tag(a,WireType.LengthDelimited).fork(),r),e.join())}scalar(e,r,t,a,n){var[r,s,i]=this.scalarInfo(r,a);i&&!n||(e.tag(t,r),e[s](a))}packed(r,e,t,a){if(a.length){assert(e!==ScalarType.BYTES&&e!==ScalarType.STRING),r.tag(t,WireType.LengthDelimited),r.fork();var[,n]=this.scalarInfo(e);for(let e=0;enew BinaryWriter};function binaryWriteOptions(e){return e?{...defaultsWrite,...e}:defaultsWrite}class BinaryWriter{constructor(e){this.stack=[],this.textEncoder=e??new TextEncoder,this.chunks=[],this.buf=[]}finish(){this.chunks.push(new Uint8Array(this.buf));let r=0;for(let e=0;e>>0)}raw(e){return this.buf.length&&(this.chunks.push(new Uint8Array(this.buf)),this.buf=[]),this.chunks.push(e),this}uint32(e){for(assertUInt32(e);127>>=7;return this.buf.push(e),this}int32(e){return assertInt32(e),varint32write(e,this.buf),this}bool(e){return this.buf.push(e?1:0),this}bytes(e){return this.uint32(e.byteLength),this.raw(e)}string(e){e=this.textEncoder.encode(e);return this.uint32(e.byteLength),this.raw(e)}float(e){assertFloat32(e);var r=new Uint8Array(4);return new DataView(r.buffer).setFloat32(0,e,!0),this.raw(r)}double(e){var r=new Uint8Array(8);return new DataView(r.buffer).setFloat64(0,e,!0),this.raw(r)}fixed32(e){assertUInt32(e);var r=new Uint8Array(4);return new DataView(r.buffer).setUint32(0,e,!0),this.raw(r)}sfixed32(e){assertInt32(e);var r=new Uint8Array(4);return new DataView(r.buffer).setInt32(0,e,!0),this.raw(r)}sint32(e){return assertInt32(e),varint32write(e=(e<<1^e>>31)>>>0,this.buf),this}sfixed64(e){var r=new Uint8Array(8),t=new DataView(r.buffer),e=PbLong.from(e);return t.setInt32(0,e.lo,!0),t.setInt32(4,e.hi,!0),this.raw(r)}fixed64(e){var r=new Uint8Array(8),t=new DataView(r.buffer),e=PbULong.from(e);return t.setInt32(0,e.lo,!0),t.setInt32(4,e.hi,!0),this.raw(r)}int64(e){e=PbLong.from(e);return varint64write(e.lo,e.hi,this.buf),this}sint64(e){var e=PbLong.from(e),r=e.hi>>31;return varint64write(e.lo<<1^r,(e.hi<<1|e.lo>>>31)^r,this.buf),this}uint64(e){e=PbULong.from(e);return varint64write(e.lo,e.hi,this.buf),this}}const defaultsRead={readUnknownField:!0,readerFactory:e=>new BinaryReader(e)};function binaryReadOptions(e){return e?{...defaultsRead,...e}:defaultsRead}class BinaryReader{constructor(e,r){this.varint64=varint64read,this.uint32=varint32read,this.buf=e,this.len=e.length,this.pos=0,this.view=new DataView(e.buffer,e.byteOffset,e.byteLength),this.textDecoder=r??new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0})}tag(){var e=this.uint32(),r=e>>>3,e=7&e;if(r<=0||e<0||5this.len)throw new RangeError("premature EOF")}int32(){return 0|this.uint32()}sint32(){var e=this.uint32();return e>>>1^-(1&e)}int64(){return new PbLong(...this.varint64())}uint64(){return new PbULong(...this.varint64())}sint64(){var[e,r]=this.varint64(),t=-(1&e),e=(e>>>1|(1&r)<<31)^t,r=r>>>1^t;return new PbLong(e,r)}bool(){var[e,r]=this.varint64();return 0!==e||0!==r}fixed32(){return this.view.getUint32((this.pos+=4)-4,!0)}sfixed32(){return this.view.getInt32((this.pos+=4)-4,!0)}fixed64(){return new PbULong(this.sfixed32(),this.sfixed32())}sfixed64(){return new PbLong(this.sfixed32(),this.sfixed32())}float(){return this.view.getFloat32((this.pos+=4)-4,!0)}double(){return this.view.getFloat64((this.pos+=8)-8,!0)}bytes(){var e=this.uint32(),r=this.pos;return this.pos+=e,this.assertBounds(),this.buf.subarray(r,r+e)}string(){return this.textDecoder.decode(this.bytes())}}class MessageType{constructor(e,r,t){this.defaultCheckDepth=16,this.typeName=e,this.fields=r.map(normalizeFieldInfo),this.options=t??{},this.refTypeCheck=new ReflectionTypeCheck(this),this.refJsonReader=new ReflectionJsonReader(this),this.refJsonWriter=new ReflectionJsonWriter(this),this.refBinReader=new ReflectionBinaryReader(this),this.refBinWriter=new ReflectionBinaryWriter(this)}create(e){var r=reflectionCreate(this);return void 0!==e&&reflectionMergePartial(this,r,e),r}clone(e){var r=this.create();return reflectionMergePartial(this,r,e),r}equals(e,r){return reflectionEquals(this,e,r)}is(e,r=this.defaultCheckDepth){return this.refTypeCheck.is(e,r,!1)}isAssignable(e,r=this.defaultCheckDepth){return this.refTypeCheck.is(e,r,!0)}mergePartial(e,r){reflectionMergePartial(this,e,r)}fromBinary(e,r){r=binaryReadOptions(r);return this.internalBinaryRead(r.readerFactory(e),e.byteLength,r)}fromJson(e,r){return this.internalJsonRead(e,jsonReadOptions(r))}fromJsonString(e,r){e=JSON.parse(e);return this.fromJson(e,r)}toJson(e,r){return this.internalJsonWrite(e,jsonWriteOptions(r))}toJsonString(e,r){e=this.toJson(e,r);return JSON.stringify(e,null,(null==r?void 0:r.prettySpaces)??0)}toBinary(e,r){r=binaryWriteOptions(r);return this.internalBinaryWrite(e,r.writerFactory(),r).finish()}internalJsonRead(e,r,t){if(null===e||"object"!=typeof e||Array.isArray(e))throw new Error(`Unable to parse message ${this.typeName} from JSON ${typeofJsonValue(e)}.`);return t=t??this.create(),this.refJsonReader.read(e,t,r),t}internalJsonWrite(e,r){return this.refJsonWriter.write(e,r)}internalBinaryWrite(e,r,t){return this.refBinWriter.write(e,r,t),r}internalBinaryRead(e,r,t,a){a=a??this.create();return this.refBinReader.read(e,a,t,r),a}} 126 | /****************** initialization finish *******************/ 127 | switch (FORMAT) { 128 | case "application/protobuf": 129 | case "application/x-protobuf": 130 | case "application/vnd.google.protobuf": 131 | switch (PLATFORM) { 132 | case "YouTube": 133 | break; 134 | case "Spotify": 135 | switch (HOST) { 136 | case "spclient.wg.spotify.com": { 137 | if (PATH.startsWith("color-lyrics/v2/track/")) { 138 | } else if (PATH.startsWith("transcript-read-along/v2/episode/")) { 139 | /****************** initialization start *******************/ 140 | class Transcript$Type extends MessageType { 141 | constructor() { 142 | super("Transcript", [ 143 | { no: 1, name: "version", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 144 | { no: 3, name: "uri", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 145 | { no: 4, name: "timeStamp", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 146 | { no: 6, name: "sections", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => Section } 147 | ]); 148 | } 149 | } 150 | const Transcript = new Transcript$Type(); 151 | class Section$Type extends MessageType { 152 | constructor() { 153 | super("Section", [ 154 | { no: 1, name: "startMs", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, 155 | { no: 3, name: "content", kind: "message", T: () => Content }, 156 | { no: 4, name: "header", kind: "message", T: () => Header } 157 | ]); 158 | } 159 | } 160 | const Section = new Section$Type(); 161 | class Content$Type extends MessageType { 162 | constructor() { 163 | super("Content", [ 164 | { no: 1, name: "line", kind: "message", T: () => Line } 165 | ]); 166 | } 167 | } 168 | const Content = new Content$Type(); 169 | class Line$Type extends MessageType { 170 | constructor() { 171 | super("Line", [ 172 | { no: 1, name: "startMs", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, 173 | { no: 2, name: "text", kind: "scalar", T: 9 /*ScalarType.STRING*/ } 174 | ]); 175 | } 176 | } 177 | const Line = new Line$Type(); 178 | class Header$Type extends MessageType { 179 | constructor() { 180 | super("Header", [ 181 | { no: 1, name: "texts", kind: "scalar", T: 9 /*ScalarType.STRING*/ } 182 | ]); 183 | } 184 | } 185 | const Header = new Header$Type(); 186 | /****************** initialization finish *******************/ 187 | body = Transcript.fromBinary(rawBody); 188 | $.log(`🚧 ${$.name}`, `body: ${JSON.stringify(body)}`, ""); 189 | Languages[0] = "AUTO"; 190 | let fullText = body.sections.map(section => section?.content?.line?.text ?? "\u200b"); 191 | const translation = await Translate(fullText, Settings?.Method, Settings?.Vendor, Languages[0], Languages[1], Settings?.[Settings?.Vendor], Configs?.Languages, Settings?.Times, Settings?.Interval, Settings?.Exponential); 192 | const translationMap = new Map(); 193 | fullText.forEach((text, i) => translationMap.set(text, translation?.[i])); 194 | body.sections = body.sections.map(section => { 195 | let text = section?.content?.line?.text; 196 | if (text) section.content.line.text = combineText(text, translationMap.get(text), Settings?.ShowOnly, Settings?.Position); 197 | return section; 198 | }); 199 | $.log(`🚧 ${$.name}`, `body: ${JSON.stringify(body)}`, ""); 200 | rawBody = Transcript.toBinary(body); 201 | }; 202 | break; 203 | }; 204 | case "episode-transcripts.spotifycdn.com": { 205 | /****************** initialization start *******************/ 206 | class Transcript$Type extends MessageType { 207 | constructor() { 208 | super("Transcript", [ 209 | { no: 1, name: "version", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 210 | { no: 2, name: "uri", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 211 | { no: 3, name: "timeStamp", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, 212 | { no: 4, name: "sections", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => Section } 213 | ]); 214 | } 215 | } 216 | const Transcript = new Transcript$Type(); 217 | class Section$Type extends MessageType { 218 | constructor() { 219 | super("Section", [ 220 | { no: 2, name: "startMs", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, 221 | { no: 3, name: "content", kind: "message", T: () => Content } 222 | ]); 223 | } 224 | } 225 | const Section = new Section$Type(); 226 | class Content$Type extends MessageType { 227 | constructor() { 228 | super("Content", [ 229 | { no: 1, name: "texts", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ } 230 | ]); 231 | } 232 | } 233 | const Content = new Content$Type(); 234 | /****************** initialization finish *******************/ 235 | body = Transcript.fromBinary(rawBody); 236 | Languages[0] = "AUTO"; 237 | let fullText = body.sections.map(section => section?.content?.texts?.map(text => text ?? "\u200b")).flat(Infinity); 238 | const translation = await Translate(fullText, Settings?.Method, Settings?.Vendor, Languages[0], Languages[1], Settings?.[Settings?.Vendor], Configs?.Languages, Settings?.Times, Settings?.Interval, Settings?.Exponential); 239 | const translationMap = new Map(); 240 | fullText.forEach((text, i) => translationMap.set(text, translation?.[i])); 241 | body.sections = body.sections.map(section => { 242 | //section.content.texts = section?.content?.texts?.map(text => combineText(text, translationMap.get(text), Settings?.ShowOnly, Settings?.Position)); 243 | section.content.texts = section?.content?.texts?.map(text => [text, translationMap.get(text)]).flat(Infinity); 244 | return section; 245 | }); 246 | rawBody = Transcript.toBinary(body); 247 | break; 248 | }; 249 | }; 250 | break; 251 | }; 252 | break; 253 | case "application/grpc": 254 | case "application/grpc+proto": 255 | break; 256 | }; 257 | // 写入二进制数据 258 | if ($.isQuanX()) $response.bodyBytes = rawBody 259 | else $response.body = rawBody; 260 | break; 261 | }; 262 | break; 263 | case false: 264 | break; 265 | }; 266 | })() 267 | .catch((e) => $.logErr(e)) 268 | .finally(() => { 269 | switch ($response) { 270 | default: { // 有回复数据,返回回复数据 271 | $.log(`🎉 ${$.name}, finally`, `$response`, `FORMAT: ${FORMAT}`, ""); 272 | //$.log(`🚧 ${$.name}, finally`, `$response: ${JSON.stringify($response)}`, ""); 273 | if ($response?.headers?.["Content-Encoding"]) $response.headers["Content-Encoding"] = "identity"; 274 | if ($response?.headers?.["content-encoding"]) $response.headers["content-encoding"] = "identity"; 275 | if ($.isQuanX()) { 276 | switch (FORMAT) { 277 | case undefined: // 视为无body 278 | // 返回普通数据 279 | $.done({ status: $response.status, headers: $response.headers }); 280 | break; 281 | default: 282 | // 返回普通数据 283 | $.done({ status: $response.status, headers: $response.headers, body: $response.body }); 284 | break; 285 | case "application/protobuf": 286 | case "application/x-protobuf": 287 | case "application/vnd.google.protobuf": 288 | case "application/grpc": 289 | case "application/grpc+proto": 290 | case "applecation/octet-stream": 291 | // 返回二进制数据 292 | //$.log(`${$response.bodyBytes.byteLength}---${$response.bodyBytes.buffer.byteLength}`); 293 | $.done({ status: $response.status, headers: $response.headers, bodyBytes: $response.bodyBytes.buffer.slice($response.bodyBytes.byteOffset, $response.bodyBytes.byteLength + $response.bodyBytes.byteOffset) }); 294 | break; 295 | }; 296 | } else $.done($response); 297 | break; 298 | }; 299 | case undefined: { // 无回复数据 300 | break; 301 | }; 302 | }; 303 | }) 304 | 305 | /***************** Function *****************/ 306 | function detectPlatform(url) { 307 | $.log(`☑️ ${$.name}, Detect Platform`, ""); 308 | /***************** Platform *****************/ 309 | let Platform = /\.(netflix\.com|nflxvideo\.net)/i.test(url) ? "Netflix" 310 | : /(\.youtube|youtubei\.googleapis)\.com/i.test(url) ? "YouTube" 311 | : /\.spotify(cdn)?\.com/i.test(url) ? "Spotify" 312 | : /\.apple\.com/i.test(url) ? "Apple" 313 | : /\.(dssott|starott)\.com/i.test(url) ? "Disney+" 314 | : /(\.(pv-cdn|aiv-cdn|akamaihd|cloudfront)\.net)|s3\.amazonaws\.com\/aiv-prod-timedtext\//i.test(url) ? "PrimeVideo" 315 | : /prd\.media\.h264\.io/i.test(url) ? "Max" 316 | : /\.(api\.hbo|hbomaxcdn)\.com/i.test(url) ? "HBOMax" 317 | : /\.(hulustream|huluim)\.com/i.test(url) ? "Hulu" 318 | : /\.(cbsaavideo|cbsivideo|cbs)\.com/i.test(url) ? "Paramount+" 319 | : /\.uplynk\.com/i.test(url) ? "Discovery+" 320 | : /dplus-ph-/i.test(url) ? "Discovery+Ph" 321 | : /\.peacocktv\.com/i.test(url) ? "PeacockTV" 322 | : /\.fubo\.tv/i.test(url) ? "FuboTV" 323 | : /\.viki\.io/i.test(url) ? "Viki" 324 | : /(epixhls\.akamaized\.net|epix\.services\.io)/i.test(url) ? "MGM+" 325 | : /\.nebula\.app|/i.test(url) ? "Nebula" 326 | : "Universal"; 327 | $.log(`✅ ${$.name}, Detect Platform, Platform: ${Platform}`, ""); 328 | return Platform; 329 | }; 330 | 331 | /** 332 | * Set Environment Variables 333 | * @author VirgilClyne 334 | * @param {String} name - Persistent Store Key 335 | * @param {Array} platforms - Platform Names 336 | * @param {Object} database - Default DataBase 337 | * @return {Object} { Settings, Caches, Configs } 338 | */ 339 | function setENV(name, platforms, database) { 340 | $.log(`☑️ ${$.name}, Set Environment Variables`, ""); 341 | let { Settings, Caches, Configs } = getENV(name, platforms, database); 342 | /***************** Settings *****************/ 343 | if (!Array.isArray(Settings?.Types)) Settings.Types = (Settings.Types) ? [Settings.Types] : []; // 只有一个选项时,无逗号分隔 344 | if ($.isLoon() && platforms.includes("YouTube")) { 345 | Settings.AutoCC = $persistentStore.read("自动显示翻译字幕") ?? Settings.AutoCC; 346 | switch (Settings.AutoCC) { 347 | case "是": 348 | Settings.AutoCC = true; 349 | break; 350 | case "否": 351 | Settings.AutoCC = false; 352 | break; 353 | default: 354 | break; 355 | }; 356 | Settings.ShowOnly = $persistentStore.read("仅输出译文") ?? Settings.ShowOnly; 357 | switch (Settings.ShowOnly) { 358 | case "是": 359 | Settings.ShowOnly = true; 360 | break; 361 | case "否": 362 | Settings.ShowOnly = false; 363 | break; 364 | default: 365 | break; 366 | }; 367 | Settings.Position = $persistentStore.read("字幕译文位置") ?? Settings.Position; 368 | switch (Settings.Position) { 369 | case "译文位于外文之上": 370 | Settings.Position = "Forward"; 371 | break; 372 | case "译文位于外文之下": 373 | Settings.Position = "Reverse"; 374 | break; 375 | default: 376 | break; 377 | }; 378 | }; 379 | $.log(`✅ ${$.name}, Set Environment Variables`, `Settings: ${typeof Settings}`, `Settings内容: ${JSON.stringify(Settings)}`, ""); 380 | /***************** Caches *****************/ 381 | //$.log(`✅ ${$.name}, Set Environment Variables`, `Caches: ${typeof Caches}`, `Caches内容: ${JSON.stringify(Caches)}`, ""); 382 | if (typeof Caches?.Playlists !== "object" || Array.isArray(Caches?.Playlists)) Caches.Playlists = {}; // 创建Playlists缓存 383 | Caches.Playlists.Master = new Map(JSON.parse(Caches?.Playlists?.Master || "[]")); // Strings转Array转Map 384 | Caches.Playlists.Subtitle = new Map(JSON.parse(Caches?.Playlists?.Subtitle || "[]")); // Strings转Array转Map 385 | if (typeof Caches?.Subtitles !== "object") Caches.Subtitles = new Map(JSON.parse(Caches?.Subtitles || "[]")); // Strings转Array转Map 386 | if (typeof Caches?.Metadatas !== "object" || Array.isArray(Caches?.Metadatas)) Caches.Metadatas = {}; // 创建Playlists缓存 387 | if (typeof Caches?.Metadatas?.Tracks !== "object") Caches.Metadatas.Tracks = new Map(JSON.parse(Caches?.Metadatas?.Tracks || "[]")); // Strings转Array转Map 388 | /***************** Configs *****************/ 389 | return { Settings, Caches, Configs }; 390 | }; 391 | 392 | /** 393 | * detect Format 394 | * @author VirgilClyne 395 | * @param {Object} url - Parsed URL 396 | * @param {String} body - response body 397 | * @return {String} format - format 398 | */ 399 | function detectFormat(url, body) { 400 | let format = undefined; 401 | $.log(`☑️ ${$.name}`, `detectFormat, format: ${url.format ?? url.query?.fmt ?? url.query?.format}`, ""); 402 | switch (url.format ?? url.query?.fmt ?? url.query?.format) { 403 | case "txt": 404 | format = "text/plain"; 405 | break; 406 | case "xml": 407 | case "srv3": 408 | case "ttml": 409 | case "ttml2": 410 | case "imsc": 411 | format = "text/xml"; 412 | break; 413 | case "vtt": 414 | case "webvtt": 415 | format = "text/vtt"; 416 | break; 417 | case "json": 418 | case "json3": 419 | format = "application/json"; 420 | break; 421 | case "m3u": 422 | case "m3u8": 423 | format = "application/x-mpegurl"; 424 | break; 425 | case "plist": 426 | format = "application/plist"; 427 | break; 428 | case undefined: 429 | const HEADER = body?.substring?.(0, 6).trim?.(); 430 | $.log(`🚧 ${$.name}`, `detectFormat, HEADER: ${HEADER}`, ""); 431 | $.log(`🚧 ${$.name}`, `detectFormat, HEADER?.substring?.(0, 1): ${HEADER?.substring?.(0, 1)}`, ""); 432 | switch (HEADER) { 433 | case "} 500 | */ 501 | async function Translate(text = [], method = "Part", vendor = "Google", source = "EN", target = "ZH", API = {}, database = {}, times = 3, interval = 100, exponential = true) { 502 | $.log(`☑️ ${$.name}, Translate, method: ${method}, vendor: ${vendor}, source: ${source}, target: ${target}`, ""); 503 | // 翻译长度设置 504 | let length = 127; 505 | switch (vendor) { 506 | case "Google": 507 | case "GoogleCloud": 508 | default: 509 | length = 120; 510 | break; 511 | case "Microsoft": 512 | case "Azure": 513 | length = 99; 514 | break; 515 | case "DeepL": 516 | length = 49; 517 | break; 518 | case "DeepLX": 519 | length = 20; 520 | break; 521 | }; 522 | let Translation = []; 523 | switch (method) { 524 | default: 525 | case "Part": // Part 逐段翻译 526 | let parts = chunk(text, length); 527 | Translation = await Promise.all(parts.map(async part => await retry(() => Translator(vendor, source, target, part, API, database), times, interval, exponential))).then(part => part.flat(Infinity)); 528 | break; 529 | case "Row": // Row 逐行翻译 530 | Translation = await Promise.all(text.map(async row => await retry(() => Translator(vendor, source, target, row, API, database), times, interval, exponential))); 531 | break; 532 | }; 533 | //$.log(`✅ ${$.name}, Translate, Translation: ${JSON.stringify(Translation)}`, ""); 534 | $.log(`✅ ${$.name}, Translate`, ""); 535 | return Translation; 536 | }; 537 | 538 | /** 539 | * Translator 540 | * @author VirgilClyne 541 | * @param {String} vendor - vendor 542 | * @param {String} source - source 543 | * @param {String} target - target 544 | * @param {String} text - text 545 | * @param {Object} api - API 546 | * @param {Object} database - Languages Database 547 | * @return {Promise<*>} 548 | */ 549 | async function Translator(vendor = "Google", source = "", target = "", text = "", api = {}, database) { 550 | $.log(`☑️ ${$.name}, Translator`, `orig: ${text}`, ""); 551 | // 构造请求 552 | let request = await GetRequest(vendor, source, target, text); 553 | // 发送请求 554 | let trans = await GetData(vendor, request); 555 | $.log(`🚧 ${$.name}, Translator`, `trans: ${trans}`, ""); 556 | return trans 557 | /***************** Fuctions *****************/ 558 | // Get Translate Request 559 | async function GetRequest(vendor = "", source = "", target = "", text = "") { 560 | $.log(`☑️ ${$.name}, Get Translate Request`, ""); 561 | const UAPool = [ 562 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36", // 13.5% 563 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", // 6.6% 564 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0", // 6.4% 565 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0", // 6.2% 566 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36", // 5.2% 567 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36", // 4.8% 568 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36", 569 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134", 570 | "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", 571 | "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1", 572 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", 573 | "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0", 574 | ]; 575 | let request = {}; 576 | let BaseURL = ""; 577 | let texts = ""; 578 | switch (vendor) { 579 | default: 580 | case "Google": 581 | const BaseRequest = [ 582 | { // Google API 583 | "url": "https://translate.googleapis.com/translate_a/single?client=gtx&dt=t", 584 | "headers": { 585 | "Accept": "*/*", 586 | "User-Agent": UAPool[Math.floor(Math.random() * UAPool.length)], // 随机UA 587 | "Referer": "https://translate.google.com" 588 | } 589 | }, 590 | { // Google Dictionary Chrome extension https://chrome.google.com/webstore/detail/google-dictionary-by-goog/mgijmajocgfcbeboacabfgobmjgjcoja 591 | "url": "https://clients5.google.com/translate_a/t?client=dict-chrome-ex", 592 | "headers": { 593 | "Accept": "*/*", 594 | "User-Agent": UAPool[Math.floor(Math.random() * UAPool.length)] // 随机UA 595 | } 596 | }, 597 | { // Google Translate App 598 | "url": "https://translate.google.com/translate_a/single?client=it&dt=qca&dt=t&dt=rmt&dt=bd&dt=rms&dt=sos&dt=md&dt=gt&dt=ld&dt=ss&dt=ex&otf=2&dj=1&hl=en&ie=UTF-8&oe=UTF-8", 599 | "headers": { 600 | "Accept": "*/*", 601 | "User-Agent": "GoogleTranslate/6.29.59279 (iPhone; iOS 15.4; en; iPhone14,2)", 602 | } 603 | }, 604 | { // Google Translate App 605 | "url": "https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&source=bubble&dt=t&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&dt=at", 606 | "headers": { 607 | "Accept": "*/*", 608 | "User-Agent": "GoogleTranslate/6.29.59279 (iPhone; iOS 15.4; en; iPhone14,2)", 609 | } 610 | } 611 | ] 612 | request = BaseRequest[Math.floor(Math.random() * (BaseRequest.length - 2))] // 随机Request, 排除最后两项 613 | text = (Array.isArray(text)) ? text.join("\r") : text; 614 | request.url = request.url + `&sl=${source}&tl=${target}&q=${encodeURIComponent(text)}`; 615 | break; 616 | case "GoogleCloud": 617 | BaseURL = "https://translation.googleapis.com"; 618 | switch (api?.Version) { 619 | case "v2": 620 | default: 621 | request.url = `${BaseURL}/language/translate/v2`; 622 | request.headers = { 623 | //"Authorization": `Bearer ${api?.Token ?? api?.Auth}`, 624 | "User-Agent": "DualSubs", 625 | "Content-Type": "application/json; charset=utf-8" 626 | }; 627 | request.body = JSON.stringify({ 628 | "q": text, 629 | "source": source, 630 | "target": target, 631 | "format": "html", 632 | //"key": api?.Key 633 | }); 634 | switch (api?.Mode) { 635 | case "Token": 636 | request.headers.Authorization = `Bearer ${api?.Token ?? api?.Auth}`; 637 | break; 638 | case "Key": 639 | default: 640 | request.url += `?key=${api?.Key ?? api?.Auth}`; 641 | break; 642 | }; 643 | break; 644 | case "v3": 645 | request.url = `${BaseURL}/v3/projects/${api?.ID}`; 646 | request.headers = { 647 | "Authorization": `Bearer ${api?.Token ?? api?.Auth}`, 648 | "x-goog-user-project": api?.ID, 649 | "User-Agent": "DualSubs", 650 | "Content-Type": "application/json; charset=utf-8" 651 | }; 652 | request.body = JSON.stringify({ 653 | "sourceLanguageCode": source, 654 | "targetLanguageCode": target, 655 | "contents": (Array.isArray(text)) ? text : [text], 656 | "mimeType": "text/html" 657 | }); 658 | break; 659 | } 660 | break; 661 | case "Bing": 662 | // https://github.com/Animenosekai/translate/blob/main/translatepy/translators/bing.py 663 | switch (api?.Version) { 664 | case "Bing": 665 | default: 666 | BaseURL = "https://www.bing.com/ttranslatev3?IG=839D27F8277F4AA3B0EDB83C255D0D70&IID=translator.5033.3"; 667 | break; 668 | case "BingCN": 669 | BaseURL = "https://cn.bing.com/ttranslatev3?IG=25FEE7A7C7C14533BBFD66AC5125C49E&IID=translator.5025.1"; 670 | break; 671 | }; 672 | request.url = `${BaseURL}`; 673 | request.headers = { 674 | "Accept": "*/*", 675 | "User-Agent": UAPool[Math.floor(Math.random() * UAPool.length)], // 随机UA 676 | "Content-type": "application/x-www-form-urlencoded", 677 | "Refer": "https://www.bing.com/", 678 | }; 679 | request.body = JSON.stringify({ 680 | "fromLang": "auto-detect", 681 | //"text": '%s' % trans, 682 | "text": text, 683 | //"from": source, 684 | "to": target 685 | }); 686 | break; 687 | case "Microsoft": 688 | case "Azure": 689 | // https://docs.microsoft.com/zh-cn/azure/cognitive-services/translator/ 690 | // https://docs.azure.cn/zh-cn/cognitive-services/translator/ 691 | switch (api?.Version) { 692 | case "Azure": 693 | default: 694 | BaseURL = "https://api.cognitive.microsofttranslator.com"; 695 | break; 696 | case "AzureCN": 697 | BaseURL = "https://api.translator.azure.cn"; 698 | break; 699 | case "AzureUS": 700 | BaseURL = "https://api.cognitive.microsofttranslator.us"; 701 | break; 702 | }; 703 | request.url = `${BaseURL}/translate?api-version=3.0&textType=html&${(source) ? `from=${source}` : ""}&to=${target}`; 704 | request.headers = { 705 | "Content-Type": "application/json; charset=UTF-8", 706 | "Accept": "application/json, text/javascript, */*; q=0.01", 707 | "Accept-Language": "zh-hans" 708 | //"Authorization": `Bearer ${api?.Auth}`, 709 | //"Ocp-Apim-Subscription-Key": api?.Auth, 710 | //"Ocp-Apim-Subscription-Region": api?.Region, // chinanorth, chinaeast2 711 | //"X-ClientTraceId": uuidv4().toString() 712 | }; 713 | switch (api?.Mode) { 714 | case "Token": 715 | default: 716 | request.headers.Authorization = `Bearer ${api?.Token ?? api?.Auth}`; 717 | break; 718 | case "Key": 719 | request.headers["Ocp-Apim-Subscription-Key"] = api?.Key ?? api?.Auth; 720 | request.headers["Ocp-Apim-Subscription-Region"] = api?.Region; 721 | break; 722 | }; 723 | text = (Array.isArray(text)) ? text : [text]; 724 | texts = await Promise.all(text?.map(async item => { return { "text": item } })) 725 | request.body = JSON.stringify(texts); 726 | /* 727 | request.body = JSON.stringify([{ 728 | "text": text 729 | }]); 730 | */ 731 | break; 732 | case "DeepL": { 733 | switch (api?.Version) { 734 | case "Free": 735 | default: 736 | BaseURL = "https://api-free.deepl.com"; 737 | break; 738 | case "Pro": 739 | BaseURL = "https://api.deepl.com"; 740 | break; 741 | }; 742 | request.url = `${BaseURL}/v2/translate`; 743 | request.headers = { 744 | "Accept": "*/*", 745 | "User-Agent": "DualSubs", 746 | "Content-Type": "application/x-www-form-urlencoded" 747 | }; 748 | const BaseBody = `auth_key=${api?.Key ?? api?.Auth}&source_lang=${source}&target_lang=${target}&tag_handling=html`; 749 | text = (Array.isArray(text)) ? text : [text]; 750 | texts = await Promise.all(text?.map(async item => `&text=${encodeURIComponent(item)}`)) 751 | request.body = BaseBody + texts.join(""); 752 | break; 753 | } 754 | case "DeepLX": { 755 | BaseURL = api?.Endpoint; 756 | request.url = BaseURL; 757 | request.headers = { 758 | "Accept": "*/*", 759 | "User-Agent": "DualSubs", 760 | "Content-Type": "application/json" 761 | }; 762 | if (api?.Token) request.headers.Authorization = `Bearer ${api?.Token ?? api?.Auth}`; 763 | request.body = JSON.stringify({ 764 | "text": (Array.isArray(text)) ? text.join("||") : text, 765 | "source_lang": source, 766 | "target_lang": target, 767 | }); 768 | break; 769 | } 770 | case "BaiduFanyi": 771 | // https://fanyi-api.baidu.com/doc/24 772 | BaseURL = "https://fanyi-api.baidu.com"; 773 | request.url = `${BaseURL}/api/trans/vip/language`; 774 | request.headers = { 775 | "User-Agent": "DualSubs", 776 | "Content-Type": "application/x-www-form-urlencoded" 777 | }; 778 | request.body = { 779 | "q": text, 780 | "from": source, 781 | "to": target, 782 | "appid": api?.Key, 783 | "salt": uuidv4().toString(), 784 | "sign": "", 785 | }; 786 | break; 787 | case "YoudaoAI": 788 | // https://ai.youdao.com/DOCSIRMA/html/自然语言翻译/API文档/文本翻译服务/文本翻译服务-API文档.html 789 | BaseURL = "https://openapi.youdao.com"; 790 | request.url = `${BaseURL}/api`; 791 | request.headers = { 792 | "User-Agent": "DualSubs", 793 | "Content-Type": "application/json; charset=utf-8" 794 | }; 795 | request.body = { 796 | "q": text, 797 | "from": source, 798 | "to": target, 799 | "appKey": api?.Key, 800 | "salt": uuidv4().toString(), 801 | "signType": "v3", 802 | "sign": "", 803 | "curtime": Math.floor(+new Date() / 1000) 804 | }; 805 | break; 806 | } 807 | //$.log(`✅ ${$.name}, Get Translate Request`, `request: ${JSON.stringify(request)}`, ""); 808 | return request 809 | }; 810 | // Get Translate Data 811 | async function GetData(vendor, request) { 812 | $.log(`☑️ ${$.name}, Get Translate Data`, ""); 813 | let texts = []; 814 | await Fetch(request) 815 | .then(response => JSON.parse(response.body)) 816 | .then(_data => { 817 | switch (vendor) { 818 | case "Google": 819 | default: 820 | if (Array.isArray(_data)) { 821 | if (_data.length === 1) { 822 | _data[0].pop(); 823 | texts = _data[0]; 824 | } else if (Array.isArray(_data?.[0])) texts = _data?.[0]?.map(item => item?.[0] ?? `翻译失败, vendor: ${vendor}`); 825 | else texts = _data; 826 | } else if (_data?.sentences) texts = _data?.sentences?.map(item => item?.trans ?? `翻译失败, vendor: ${vendor}`); 827 | texts = texts?.join("")?.split(/\r/); 828 | break; 829 | case "GoogleCloud": 830 | texts = _data?.data?.translations?.map(item => item?.translatedText ?? `翻译失败, vendor: ${vendor}`); 831 | break; 832 | case "Bing": 833 | case "Microsoft": 834 | case "Azure": 835 | texts = _data?.map(item => item?.translations?.[0]?.text ?? `翻译失败, vendor: ${vendor}`); 836 | break; 837 | case "DeepL": 838 | texts = _data?.translations?.map(item => item?.text ?? `翻译失败, vendor: ${vendor}`); 839 | break; 840 | case "DeepLX": 841 | texts = _data?.data?.split("||") ?? _data?.data; 842 | break; 843 | case "BaiduFanyi": 844 | break; 845 | case "YoudaoAI": 846 | break; 847 | }; 848 | }) 849 | .catch(error => Promise.reject(error)); 850 | //$.log(`✅ ${$.name}, Get Translate Data, texts: ${JSON.stringify(texts)}`, ""); 851 | $.log(`✅ ${$.name}, Get Translate Data`, ""); 852 | return texts 853 | }; 854 | }; 855 | 856 | /** 857 | * Fetch Ruled Reqeust 858 | * @author VirgilClyne 859 | * @link https://github.com/BiliUniverse/Global/blob/main/js/BiliBili.Global.request.js 860 | * @param {Object} request - Original Request Content 861 | * @return {Promise<*>} 862 | */ 863 | async function Fetch(request = {}) { 864 | $.log(`☑️ ${$.name}, Fetch Ruled Reqeust`, ""); 865 | //const FORMAT = (request?.headers?.["Content-Type"] ?? request?.headers?.["content-type"])?.split(";")?.[0]; 866 | $.log(`⚠ ${$.name}, Fetch Ruled Reqeust`, `FORMAT: ${FORMAT}`, ""); 867 | if ($.isQuanX()) { 868 | switch (FORMAT) { 869 | case undefined: // 视为无body 870 | // 返回普通数据 871 | break; 872 | default: 873 | // 返回普通数据 874 | delete request.bodyBytes; 875 | break; 876 | case "application/protobuf": 877 | case "application/x-protobuf": 878 | case "application/vnd.google.protobuf": 879 | case "application/grpc": 880 | case "application/grpc+proto": 881 | //case "applecation/octet-stream": 882 | // 返回二进制数据 883 | delete request.body; 884 | if (ArrayBuffer.isView(request.bodyBytes)) request.bodyBytes = request.bodyBytes.buffer.slice(request.bodyBytes.byteOffset, request.bodyBytes.byteLength + request.bodyBytes.byteOffset); 885 | break; 886 | }; 887 | }; 888 | let response = (request?.body ?? request?.bodyBytes) 889 | ? await $.http.post(request) 890 | : await $.http.get(request); 891 | $.log(`✅ ${$.name}, Fetch Ruled Reqeust`, ""); 892 | return response; 893 | }; 894 | 895 | /** 896 | * combine two text 897 | * @author VirgilClyne 898 | * @param {String} originText - original text 899 | * @param {String} transText - translate text 900 | * @param {Boolean} ShowOnly - only show translate text 901 | * @param {String} position - position 902 | * @param {String} lineBreak - line break 903 | * @return {String} combined text 904 | */ 905 | function combineText(originText, transText, ShowOnly = false, position = "Forward", lineBreak = "\n") { 906 | let text = ""; 907 | switch (ShowOnly) { 908 | case true: 909 | text = transText; 910 | break; 911 | case false: 912 | default: 913 | switch (position) { 914 | case "Forward": 915 | default: 916 | text = `${originText}${lineBreak}${transText}`; 917 | break; 918 | case "Reverse": 919 | text = `${transText}${lineBreak}${originText}`; 920 | break; 921 | } 922 | } 923 | return text; 924 | }; 925 | 926 | /** 927 | * Chunk Array 928 | * @author VirgilClyne 929 | * @param {Array} source - source 930 | * @param {Number} length - number 931 | * @return {Array<*>} target 932 | */ 933 | function chunk(source, length) { 934 | $.log(`⚠ ${$.name}, Chunk Array`, ""); 935 | var index = 0, target = []; 936 | while(index < source.length) target.push(source.slice(index, index += length)); 937 | //$.log(`🎉 ${$.name}, Chunk Array`, `target: ${JSON.stringify(target)}`, ""); 938 | return target; 939 | }; 940 | 941 | /** 942 | * Retries the given function until it succeeds given a number of retries and an interval between them. They are set 943 | * by default to retry 5 times with 1sec in between. There's also a flag to make the cooldown time exponential 944 | * @link https://gitlab.com/-/snippets/1775781 945 | * @author Daniel Iñigo 946 | * @param {Function} fn - Returns a promise 947 | * @param {Number} retriesLeft - Number of retries. If -1 will keep retrying 948 | * @param {Number} interval - Millis between retries. If exponential set to true will be doubled each retry 949 | * @param {Boolean} exponential - Flag for exponential back-off mode 950 | * @return {Promise<*>} 951 | */ 952 | async function retry(fn, retriesLeft = 5, interval = 1000, exponential = false) { 953 | $.log(`☑️ ${$.name}, retry, 剩余重试次数:${retriesLeft}`, `时间间隔:${interval}ms`); 954 | try { 955 | const val = await fn(); 956 | return val; 957 | } catch (error) { 958 | if (retriesLeft) { 959 | await new Promise(r => setTimeout(r, interval)); 960 | return retry(fn, retriesLeft - 1, exponential ? interval * 2 : interval, exponential); 961 | } else throw new Error(`❌ ${$.name}, retry, 最大重试次数`); 962 | } 963 | }; 964 | 965 | /***************** Env *****************/ 966 | // prettier-ignore 967 | // https://github.com/chavyleung/scripts/blob/master/Env.min.js 968 | function Env(t,e){class s{constructor(t){this.env=t}send(t,e="GET"){t="string"==typeof t?{url:t}:t;let s=this.get;return"POST"===e&&(s=this.post),new Promise((e,a)=>{s.call(this,t,(t,s,r)=>{t?a(t):e(s)})})}get(t){return this.send.call(this.env,t)}post(t){return this.send.call(this.env,t,"POST")}}return new class{constructor(t,e){this.name=t,this.http=new s(this),this.data=null,this.dataFile="box.dat",this.logs=[],this.isMute=!1,this.isNeedRewrite=!1,this.logSeparator="\n",this.encoding="utf-8",this.startTime=(new Date).getTime(),Object.assign(this,e),this.log("",`\ud83d\udd14${this.name}, \u5f00\u59cb!`)}getEnv(){return"undefined"!=typeof $environment&&$environment["surge-version"]?"Surge":"undefined"!=typeof $environment&&$environment["stash-version"]?"Stash":"undefined"!=typeof module&&module.exports?"Node.js":"undefined"!=typeof $task?"Quantumult X":"undefined"!=typeof $loon?"Loon":"undefined"!=typeof $rocket?"Shadowrocket":void 0}isNode(){return"Node.js"===this.getEnv()}isQuanX(){return"Quantumult X"===this.getEnv()}isSurge(){return"Surge"===this.getEnv()}isLoon(){return"Loon"===this.getEnv()}isShadowrocket(){return"Shadowrocket"===this.getEnv()}isStash(){return"Stash"===this.getEnv()}toObj(t,e=null){try{return JSON.parse(t)}catch{return e}}toStr(t,e=null){try{return JSON.stringify(t)}catch{return e}}getjson(t,e){let s=e;const a=this.getdata(t);if(a)try{s=JSON.parse(this.getdata(t))}catch{}return s}setjson(t,e){try{return this.setdata(JSON.stringify(t),e)}catch{return!1}}getScript(t){return new Promise(e=>{this.get({url:t},(t,s,a)=>e(a))})}runScript(t,e){return new Promise(s=>{let a=this.getdata("@chavy_boxjs_userCfgs.httpapi");a=a?a.replace(/\n/g,"").trim():a;let r=this.getdata("@chavy_boxjs_userCfgs.httpapi_timeout");r=r?1*r:20,r=e&&e.timeout?e.timeout:r;const[i,o]=a.split("@"),n={url:`http://${o}/v1/scripting/evaluate`,body:{script_text:t,mock_type:"cron",timeout:r},headers:{"X-Key":i,Accept:"*/*"},timeout:r};this.post(n,(t,e,a)=>s(a))}).catch(t=>this.logErr(t))}loaddata(){if(!this.isNode())return{};{this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),a=!s&&this.fs.existsSync(e);if(!s&&!a)return{};{const a=s?t:e;try{return JSON.parse(this.fs.readFileSync(a))}catch(t){return{}}}}}writedata(){if(this.isNode()){this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),a=!s&&this.fs.existsSync(e),r=JSON.stringify(this.data);s?this.fs.writeFileSync(t,r):a?this.fs.writeFileSync(e,r):this.fs.writeFileSync(t,r)}}lodash_get(t,e,s){const a=e.replace(/\[(\d+)\]/g,".$1").split(".");let r=t;for(const t of a)if(r=Object(r)[t],void 0===r)return s;return r}lodash_set(t,e,s){return Object(t)!==t?t:(Array.isArray(e)||(e=e.toString().match(/[^.[\]]+/g)||[]),e.slice(0,-1).reduce((t,s,a)=>Object(t[s])===t[s]?t[s]:t[s]=Math.abs(e[a+1])>>0==+e[a+1]?[]:{},t)[e[e.length-1]]=s,t)}getdata(t){let e=this.getval(t);if(/^@/.test(t)){const[,s,a]=/^@(.*?)\.(.*?)$/.exec(t),r=s?this.getval(s):"";if(r)try{const t=JSON.parse(r);e=t?this.lodash_get(t,a,""):e}catch(t){e=""}}return e}setdata(t,e){let s=!1;if(/^@/.test(e)){const[,a,r]=/^@(.*?)\.(.*?)$/.exec(e),i=this.getval(a),o=a?"null"===i?null:i||"{}":"{}";try{const e=JSON.parse(o);this.lodash_set(e,r,t),s=this.setval(JSON.stringify(e),a)}catch(e){const i={};this.lodash_set(i,r,t),s=this.setval(JSON.stringify(i),a)}}else s=this.setval(t,e);return s}getval(t){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":return $persistentStore.read(t);case"Quantumult X":return $prefs.valueForKey(t);case"Node.js":return this.data=this.loaddata(),this.data[t];default:return this.data&&this.data[t]||null}}setval(t,e){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":return $persistentStore.write(t,e);case"Quantumult X":return $prefs.setValueForKey(t,e);case"Node.js":return this.data=this.loaddata(),this.data[e]=t,this.writedata(),!0;default:return this.data&&this.data[e]||null}}initGotEnv(t){this.got=this.got?this.got:require("got"),this.cktough=this.cktough?this.cktough:require("tough-cookie"),this.ckjar=this.ckjar?this.ckjar:new this.cktough.CookieJar,t&&(t.headers=t.headers?t.headers:{},void 0===t.headers.Cookie&&void 0===t.cookieJar&&(t.cookieJar=this.ckjar))}get(t,e=(()=>{})){switch(t.headers&&(delete t.headers["Content-Type"],delete t.headers["Content-Length"],delete t.headers["content-type"],delete t.headers["content-length"]),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient.get(t,(t,s,a)=>{!t&&s&&(s.body=a,s.statusCode=s.status?s.status:s.statusCode,s.status=s.statusCode),e(t,s,a)});break;case"Quantumult X":this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then(t=>{const{statusCode:s,statusCode:a,headers:r,body:i,bodyBytes:o}=t;e(null,{status:s,statusCode:a,headers:r,body:i,bodyBytes:o},i,o)},t=>e(t&&t.error||"UndefinedError"));break;case"Node.js":let s=require("iconv-lite");this.initGotEnv(t),this.got(t).on("redirect",(t,e)=>{try{if(t.headers["set-cookie"]){const s=t.headers["set-cookie"].map(this.cktough.Cookie.parse).toString();s&&this.ckjar.setCookieSync(s,null),e.cookieJar=this.ckjar}}catch(t){this.logErr(t)}}).then(t=>{const{statusCode:a,statusCode:r,headers:i,rawBody:o}=t,n=s.decode(o,this.encoding);e(null,{status:a,statusCode:r,headers:i,rawBody:o,body:n},n)},t=>{const{message:a,response:r}=t;e(a,r,r&&s.decode(r.rawBody,this.encoding))})}}post(t,e=(()=>{})){const s=t.method?t.method.toLocaleLowerCase():"post";switch(t.body&&t.headers&&!t.headers["Content-Type"]&&!t.headers["content-type"]&&(t.headers["content-type"]="application/x-www-form-urlencoded"),t.headers&&(delete t.headers["Content-Length"],delete t.headers["content-length"]),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient[s](t,(t,s,a)=>{!t&&s&&(s.body=a,s.statusCode=s.status?s.status:s.statusCode,s.status=s.statusCode),e(t,s,a)});break;case"Quantumult X":t.method=s,this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then(t=>{const{statusCode:s,statusCode:a,headers:r,body:i,bodyBytes:o}=t;e(null,{status:s,statusCode:a,headers:r,body:i,bodyBytes:o},i,o)},t=>e(t&&t.error||"UndefinedError"));break;case"Node.js":let a=require("iconv-lite");this.initGotEnv(t);const{url:r,...i}=t;this.got[s](r,i).then(t=>{const{statusCode:s,statusCode:r,headers:i,rawBody:o}=t,n=a.decode(o,this.encoding);e(null,{status:s,statusCode:r,headers:i,rawBody:o,body:n},n)},t=>{const{message:s,response:r}=t;e(s,r,r&&a.decode(r.rawBody,this.encoding))})}}time(t,e=null){const s=e?new Date(e):new Date;let a={"M+":s.getMonth()+1,"d+":s.getDate(),"H+":s.getHours(),"m+":s.getMinutes(),"s+":s.getSeconds(),"q+":Math.floor((s.getMonth()+3)/3),S:s.getMilliseconds()};/(y+)/.test(t)&&(t=t.replace(RegExp.$1,(s.getFullYear()+"").substr(4-RegExp.$1.length)));for(let e in a)new RegExp("("+e+")").test(t)&&(t=t.replace(RegExp.$1,1==RegExp.$1.length?a[e]:("00"+a[e]).substr((""+a[e]).length)));return t}queryStr(t){let e="";for(const s in t){let a=t[s];null!=a&&""!==a&&("object"==typeof a&&(a=JSON.stringify(a)),e+=`${s}=${a}&`)}return e=e.substring(0,e.length-1),e}msg(e=t,s="",a="",r){const i=t=>{switch(typeof t){case void 0:return t;case"string":switch(this.getEnv()){case"Surge":case"Stash":default:return{url:t};case"Loon":case"Shadowrocket":return t;case"Quantumult X":return{"open-url":t};case"Node.js":return}case"object":switch(this.getEnv()){case"Surge":case"Stash":case"Shadowrocket":default:{let e=t.url||t.openUrl||t["open-url"];return{url:e}}case"Loon":{let e=t.openUrl||t.url||t["open-url"],s=t.mediaUrl||t["media-url"];return{openUrl:e,mediaUrl:s}}case"Quantumult X":{let e=t["open-url"]||t.url||t.openUrl,s=t["media-url"]||t.mediaUrl,a=t["update-pasteboard"]||t.updatePasteboard;return{"open-url":e,"media-url":s,"update-pasteboard":a}}case"Node.js":return}default:return}};if(!this.isMute)switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:$notification.post(e,s,a,i(r));break;case"Quantumult X":$notify(e,s,a,i(r));break;case"Node.js":}if(!this.isMuteLog){let t=["","==============\ud83d\udce3\u7cfb\u7edf\u901a\u77e5\ud83d\udce3=============="];t.push(e),s&&t.push(s),a&&t.push(a),console.log(t.join("\n")),this.logs=this.logs.concat(t)}}log(...t){t.length>0&&(this.logs=[...this.logs,...t]),console.log(t.join(this.logSeparator))}logErr(t,e){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":case"Quantumult X":default:this.log("",`\u2757\ufe0f${this.name}, \u9519\u8bef!`,t);break;case"Node.js":this.log("",`\u2757\ufe0f${this.name}, \u9519\u8bef!`,t.stack)}}wait(t){return new Promise(e=>setTimeout(e,t))}done(t={}){const e=(new Date).getTime(),s=(e-this.startTime)/1e3;switch(this.log("",`\ud83d\udd14${this.name}, \u7ed3\u675f! \ud83d\udd5b ${s} \u79d2`),this.log(),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":case"Quantumult X":default:$done(t);break;case"Node.js":process.exit(1)}}}(t,e)} 969 | 970 | /** 971 | * Get Environment Variables 972 | * @link https://github.com/VirgilClyne/GetSomeFries/blob/main/function/getENV/getENV.min.js 973 | * @author VirgilClyne 974 | * @param {String} key - Persistent Store Key 975 | * @param {Array} names - Platform Names 976 | * @param {Object} database - Default Database 977 | * @return {Object} { Settings, Caches, Configs } 978 | */ 979 | function getENV(key,names,database){let BoxJs=$.getjson(key,database),Argument={};if("undefined"!=typeof $argument&&Boolean($argument)){let arg=Object.fromEntries($argument.split("&").map((item=>item.split("=").map((i=>i.replace(/\"/g,""))))));for(let item in arg)setPath(Argument,item,arg[item])}const Store={Settings:database?.Default?.Settings||{},Configs:database?.Default?.Configs||{},Caches:{}};Array.isArray(names)||(names=[names]);for(let name of names)Store.Settings={...Store.Settings,...database?.[name]?.Settings,...Argument,...BoxJs?.[name]?.Settings},Store.Configs={...Store.Configs,...database?.[name]?.Configs},BoxJs?.[name]?.Caches&&"string"==typeof BoxJs?.[name]?.Caches&&(BoxJs[name].Caches=JSON.parse(BoxJs?.[name]?.Caches)),Store.Caches={...Store.Caches,...BoxJs?.[name]?.Caches};return function traverseObject(o,c){for(var t in o){var n=o[t];o[t]="object"==typeof n&&null!==n?traverseObject(n,c):c(t,n)}return o}(Store.Settings,((key,value)=>("true"===value||"false"===value?value=JSON.parse(value):"string"==typeof value&&(value=value.includes(",")?value.split(",").map((item=>string2number(item))):string2number(value)),value))),Store;function setPath(object,path,value){path.split(".").reduce(((o,p,i)=>o[p]=path.split(".").length===++i?value:o[p]||{}),object)}function string2number(string){return string&&!isNaN(string)&&(string=parseInt(string,10)),string}} 980 | 981 | // https://github.com/VirgilClyne/GetSomeFries/blob/main/function/URI/URIs.embedded.min.js 982 | function URIs(t){return new class{constructor(t=[]){this.name="URI v1.2.6",this.opts=t,this.json={scheme:"",host:"",path:"",query:{}}}parse(t){let s=t.match(/(?:(?.+):\/\/(?[^/]+))?\/?(?[^?]+)?\??(?[^?]+)?/)?.groups??null;if(s?.path?s.paths=s.path.split("/"):s.path="",s?.paths){const t=s.paths[s.paths.length-1];if(t?.includes(".")){const e=t.split(".");s.format=e[e.length-1]}}return s?.query&&(s.query=Object.fromEntries(s.query.split("&").map((t=>t.split("="))))),s}stringify(t=this.json){let s="";return t?.scheme&&t?.host&&(s+=t.scheme+"://"+t.host),t?.path&&(s+=t?.host?"/"+t.path:t.path),t?.query&&(s+="?"+Object.entries(t.query).map((t=>t.join("="))).join("&")),s}}(t)} 983 | -------------------------------------------------------------------------------- /modules/DualSubs.Spotify.Transcripts.beta.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=🍿️ DualSubs: 🎵 Spotify Transcripts β 2 | #!desc=(BETA) Spotify播客转译文本双语模块 3 | #!openUrl=http://boxjs.com/#/app/DualSubs.Translate.beta 4 | #!author=VirgilClyne 5 | #!homepage=https://github.com/DualSubs 6 | #!manual=https://github.com/DualSubs/Spotify/wiki/🍿-DualSubs:-🎵-Spotify 7 | #!icon=https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png 8 | #!category=🍿️ DualSubs 9 | 10 | [Script] 11 | DualSubs.Spotify.Transcript-read-along.Translate.response.proto = type=http-response, pattern=^https?:\/\/spclient\.wg\.spotify\.com\/transcript-read-along\/v2\/episode\/\w+, requires-body=1, binary-body-mode=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Spotify/beta/js/DualSubs.Transcripts.Translate.response.beta.js 12 | DualSubs.Spotify.Episode-transcripts.Translate.response.proto = type=http-response, pattern=^https?:\/\/episode-transcripts\.spotifycdn\.com\/1\.0\/spotify:transcript:\w+, requires-body=1, binary-body-mode=1, debug=1, script-path=https://raw.githubusercontent.com/DualSubs/Spotify/beta/js/DualSubs.Transcripts.Translate.response.beta.js 13 | 14 | [MITM] 15 | hostname = %APPEND% spclient.wg.spotify.com, episode-transcripts.spotifycdn.com 16 | -------------------------------------------------------------------------------- /modules/DualSubs.Spotify.Transcripts.plugin: -------------------------------------------------------------------------------- 1 | #!name=🍿️ DualSubs: 🎵 Spotify Transcripts 2 | #!desc=Spotify播客转译文本双语模块 3 | #!openUrl=http://boxjs.com/#/app/DualSubs.Translate 4 | #!author=VirgilClyne 5 | #!homepage=https://github.com/DualSubs 6 | #!manual=https://github.com/DualSubs/Spotify/wiki/🍿-DualSubs:-🎵-Spotify 7 | #!icon=https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png 8 | #!category=🍿️ DualSubs 9 | 10 | [Script] 11 | http-response ^https?:\/\/spclient\.wg\.spotify\.com\/transcript-read-along\/v2\/episode\/\w+ requires-body=1, binary-body-mode=1, script-path=https://raw.githubusercontent.com/DualSubs/Spotify/main/js/DualSubs.Transcripts.Translate.response.js, tag = DualSubs.Spotify.Transcript-read-along.Translate.response.proto 12 | http-response ^https?:\/\/episode-transcripts\.spotifycdn\.com\/1\.0\/spotify:transcript:\w+ requires-body=1, binary-body-mode=1, script-path=https://raw.githubusercontent.com/DualSubs/Spotify/main/js/DualSubs.Transcripts.Translate.response.js, tag = DualSubs.Spotify.Episode-transcripts.Translate.response.proto 13 | 14 | [MITM] 15 | hostname = spclient.wg.spotify.com, episode-transcripts.spotifycdn.com 16 | -------------------------------------------------------------------------------- /modules/DualSubs.Spotify.Transcripts.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=🍿️ DualSubs: 🎵 Spotify Transcripts 2 | #!desc=Spotify播客转译文本双语模块 3 | #!openUrl=http://boxjs.com/#/app/DualSubs.Translate 4 | #!author=VirgilClyne 5 | #!homepage=https://github.com/DualSubs 6 | #!manual=https://github.com/DualSubs/Spotify/wiki/🍿-DualSubs:-🎵-Spotify 7 | #!icon=https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png 8 | #!category=🍿️ DualSubs 9 | 10 | [Script] 11 | DualSubs.Spotify.Transcript-read-along.Translate.response.proto = type=http-response, pattern=^https?:\/\/spclient\.wg\.spotify\.com\/transcript-read-along\/v2\/episode\/\w+, requires-body=1, binary-body-mode=1, script-path=https://raw.githubusercontent.com/DualSubs/Spotify/main/js/DualSubs.Transcripts.Translate.response.js 12 | DualSubs.Spotify.Episode-transcripts.Translate.response.proto = type=http-response, pattern=^https?:\/\/episode-transcripts\.spotifycdn\.com\/1\.0\/spotify:transcript:\w+, requires-body=1, binary-body-mode=1, script-path=https://raw.githubusercontent.com/DualSubs/Spotify/main/js/DualSubs.Transcripts.Translate.response.js 13 | 14 | [MITM] 15 | hostname = %APPEND% spclient.wg.spotify.com, episode-transcripts.spotifycdn.com 16 | -------------------------------------------------------------------------------- /modules/DualSubs.Spotify.Transcripts.snippet: -------------------------------------------------------------------------------- 1 | #!name=🍿️ DualSubs: 🎵 Spotify Transcripts 2 | #!desc=Spotify播客转译文本双语模块 3 | #!openUrl=http://boxjs.com/#/app/DualSubs.Translate 4 | #!author=VirgilClyne 5 | #!homepage=https://github.com/DualSubs 6 | #!manual=https://github.com/DualSubs/Spotify/wiki/🍿-DualSubs:-🎵-Spotify 7 | #!icon=https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png 8 | #!category=🍿️ DualSubs 9 | 10 | #[rewrite_local] 11 | # DualSubs.Spotify.Transcript-read-along.Translate.response.proto 12 | ^https?:\/\/spclient\.wg\.spotify\.com\/transcript-read-along\/v2\/episode\/\w+ url script-response-body https://raw.githubusercontent.com/DualSubs/Spotify/main/js/DualSubs.Transcripts.Translate.response.js 13 | # DualSubs.Spotify.Episode-transcripts.Translate.response.proto 14 | ^https?:\/\/episode-transcripts\.spotifycdn\.com\/1\.0\/spotify:transcript:\w+ url script-response-body https://raw.githubusercontent.com/DualSubs/Spotify/main/js/DualSubs.Transcripts.Translate.response.js 15 | 16 | #[mitm] 17 | hostname = spclient.wg.spotify.com, episode-transcripts.spotifycdn.com 18 | -------------------------------------------------------------------------------- /modules/DualSubs.Spotify.Transcripts.srmodule: -------------------------------------------------------------------------------- 1 | #!name=🍿️ DualSubs: 🎵 Spotify Transcripts 2 | #!desc=Spotify播客转译文本双语模块 3 | #!openUrl=http://boxjs.com/#/app/DualSubs.Translate 4 | #!author=VirgilClyne 5 | #!homepage=https://github.com/DualSubs 6 | #!manual=https://github.com/DualSubs/Spotify/wiki/🍿-DualSubs:-🎵-Spotify 7 | #!icon=https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png 8 | #!category=🍿️ DualSubs 9 | 10 | [Script] 11 | DualSubs.Spotify.Transcript-read-along.Translate.response.proto = type=http-response, pattern=^https?:\/\/spclient\.wg\.spotify\.com\/transcript-read-along\/v2\/episode\/\w+, requires-body=1, binary-body-mode=1, script-path=https://raw.githubusercontent.com/DualSubs/Spotify/main/js/DualSubs.Transcripts.Translate.response.js 12 | DualSubs.Spotify.Episode-transcripts.Translate.response.proto = type=http-response, pattern=^https?:\/\/episode-transcripts\.spotifycdn\.com\/1\.0\/spotify:transcript:\w+, requires-body=1, binary-body-mode=1, script-path=https://raw.githubusercontent.com/DualSubs/Spotify/main/js/DualSubs.Transcripts.Translate.response.js 13 | 14 | [MITM] 15 | hostname = %APPEND% spclient.wg.spotify.com, episode-transcripts.spotifycdn.com 16 | -------------------------------------------------------------------------------- /modules/DualSubs.Spotify.Transcripts.stoverride: -------------------------------------------------------------------------------- 1 | name: "🍿️ DualSubs: 🎵 Spotify Transcripts" 2 | desc: "Spotify播客转译文本双语模块" 3 | openUrl: "http://boxjs.com/#/app/DualSubs.Translate" 4 | author: "VirgilClyne" 5 | homepage: "https://github.com/DualSubs" 6 | manual: "https://github.com/DualSubs/Spotify/wiki/🍿-DualSubs:-🎵-Spotify" 7 | icon: "https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png" 8 | category: "🍿️ DualSubs" 9 | 10 | http: 11 | mitm: 12 | - "spclient.wg.spotify.com" 13 | - "episode-transcripts.spotifycdn.com" 14 | script: 15 | - match: ^https?:\/\/spclient\.wg\.spotify\.com\/transcript-read-along\/v2\/episode\/\w+ 16 | name: DualSubs.Transcripts.Translate.response 17 | type: response 18 | require-body: true 19 | - match: ^https?:\/\/episode-transcripts\.spotifycdn\.com\/1\.0\/spotify:transcript:\w+ 20 | name: DualSubs.Transcripts.Translate.response 21 | type: response 22 | require-body: true 23 | binary-mode: true 24 | 25 | script-providers: 26 | DualSubs.Transcripts.Translate.response: 27 | url: https://raw.githubusercontent.com/DualSubs/Spotify/main/js/DualSubs.Transcripts.Translate.response.js 28 | interval: 86400 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dualsubs/spotify", 3 | "organizationName": "🍿️ DualSubs", 4 | "displayName": "🍿️ DualSubs: 🎵 Spotify", 5 | "description": "Spotify 增强及双语歌词", 6 | "homepage": "https://DualSubs.github.io/guide/spotify", 7 | "openUrl": "http://boxjs.com/#/app/DualSubs.Spotify", 8 | "icon": "https://github.com/DualSubs/Spotify/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 | ], 18 | "systemVersion": 15, 19 | "license": "Apache-2.0", 20 | "type": "module", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/DualSubs/Spotify.git" 24 | }, 25 | "directories": { 26 | "example": "example" 27 | }, 28 | "scripts": { 29 | "serve": "webpack serve", 30 | "build": "rspack build", 31 | "build:dev": "rspack build -c rspack.dev.config.js --mode=development", 32 | "build:args": "arguments-builder build", 33 | "dts": "arguments-builder dts" 34 | }, 35 | "browserslist": [ 36 | "iOS >= 15" 37 | ], 38 | "devDependencies": { 39 | "@iringo/arguments-builder": "^1.8.5", 40 | "@protobuf-ts/plugin": "^2.9.3", 41 | "@rspack/cli": "^1.0.13", 42 | "@rspack/core": "^1.0.13", 43 | "node-polyfill-webpack-plugin": "^4.0.0" 44 | }, 45 | "dependencies": { 46 | "@nsnanocat/util": "^1.7.6", 47 | "@protobuf-ts/runtime": "^2.9.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /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 | response: "./src/response.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 | -------------------------------------------------------------------------------- /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/DualSubs/Spotify/184b96c5ca3c7bf917ff5544742541f1c5c6d1ca/src/assets/icon.png -------------------------------------------------------------------------------- /src/assets/icon_rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DualSubs/Spotify/184b96c5ca3c7bf917ff5544742541f1c5c6d1ca/src/assets/icon_rounded.png -------------------------------------------------------------------------------- /src/function/database.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | Spotify: { 3 | Settings: { 4 | CountryCode: "US", 5 | Types: ["Translate", "External"], 6 | Languages: ["AUTO", "ZH"], 7 | }, 8 | }, 9 | Default: { 10 | Settings: { 11 | Type: "Translate", 12 | Types: ["Official", "Translate"], 13 | Languages: ["EN", "ZH"], 14 | CacheSize: 50, 15 | LogLevel: "WARN", 16 | }, 17 | Configs: { 18 | breakLine: { 19 | "text/xml": " ", 20 | "application/xml": " ", 21 | "text/vtt": "\n", 22 | "application/vtt": "\n", 23 | "text/json": "\n", 24 | "application/json": "\n", 25 | }, 26 | }, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/function/modifiedAccountAttributes.mjs: -------------------------------------------------------------------------------- 1 | export default function modifiedAccountAttributes(accountAttributes = {}) { 2 | accountAttributes["audiobook-onboarding-completed"] = { 3 | value: { oneofKind: "boolValue", boolValue: true }, 4 | }; // false 5 | accountAttributes["has-audiobooks-subscription"] = { 6 | value: { oneofKind: "boolValue", boolValue: true }, 7 | }; // false 8 | accountAttributes["player-license"] = { 9 | value: { oneofKind: "stringValue", stringValue: "premium" }, 10 | }; // "mft" 11 | /* 12 | accountAttributes["country_code"] = { 13 | value: { oneofKind: "stringValue", stringValue: Settings.Country }, 14 | }; 15 | */ 16 | accountAttributes["mobile"] = { 17 | value: { oneofKind: "boolValue", boolValue: true }, 18 | }; // false 19 | accountAttributes["financial-product"] = { 20 | value: { oneofKind: "stringValue", stringValue: "pr:premium,tc:0" }, 21 | }; // "pr:free,tc:0" 22 | accountAttributes["premium-mini"] = { 23 | value: { oneofKind: "boolValue", boolValue: true }, 24 | }; // false 25 | accountAttributes["streaming-rules"] = { 26 | value: { oneofKind: "stringValue", stringValue: "" }, 27 | }; // "shuffle-mode" 28 | accountAttributes["license-acceptance-grace-days"] = { 29 | value: { oneofKind: "longValue", longValue: 30 }, 30 | }; // "0" 31 | accountAttributes["name"] = { 32 | value: { oneofKind: "stringValue", stringValue: "Spotify Premium" }, 33 | }; // "Spotify Free" 34 | accountAttributes["mobile-login"] = { 35 | value: { oneofKind: "boolValue", boolValue: true }, 36 | }; // false 37 | accountAttributes["hifi-optin-intent"] = { 38 | value: { oneofKind: "boolValue", boolValue: true }, 39 | }; // false 40 | accountAttributes["pick-and-shuffle"] = { 41 | value: { oneofKind: "boolValue", boolValue: false }, 42 | }; // true 43 | accountAttributes["on-demand"] = { 44 | value: { oneofKind: "boolValue", boolValue: true }, 45 | }; // false 46 | accountAttributes["ads"] = { 47 | value: { oneofKind: "boolValue", boolValue: false }, 48 | }; // true 49 | accountAttributes["catalogue"] = { 50 | value: { oneofKind: "stringValue", stringValue: "premium" }, 51 | }; // "free" 52 | accountAttributes["high-bitrate"] = { 53 | value: { oneofKind: "boolValue", boolValue: true }, 54 | }; // false 55 | accountAttributes["libspotify"] = { 56 | value: { oneofKind: "boolValue", boolValue: true }, 57 | }; // false 58 | accountAttributes["nft-disabled"] = { 59 | value: { oneofKind: "stringValue", stringValue: "1" }, 60 | }; // "0" 61 | accountAttributes["shuffle"] = { 62 | value: { oneofKind: "boolValue", boolValue: false }, 63 | }; // true 64 | accountAttributes["audio-quality"] = { 65 | value: { oneofKind: "stringValue", stringValue: "1" }, 66 | }; // "0" 67 | accountAttributes["offline"] = { 68 | value: { oneofKind: "boolValue", boolValue: true }, 69 | }; // false 70 | accountAttributes["hifi-eligible"] = { 71 | value: { oneofKind: "boolValue", boolValue: true }, 72 | }; // false 73 | accountAttributes["pause-after"] = { 74 | value: { oneofKind: "longValue", longValue: 0 }, 75 | }; // "18000" 76 | accountAttributes["addon-hifi"] = { 77 | value: { oneofKind: "boolValue", boolValue: true }, 78 | }; // false 79 | accountAttributes["can_use_superbird"] = { 80 | value: { oneofKind: "boolValue", boolValue: true }, 81 | }; // false 82 | accountAttributes["type"] = { 83 | value: { oneofKind: "stringValue", stringValue: "premium" }, 84 | }; // "free" 85 | accountAttributes["india-experience"] = { 86 | value: { oneofKind: "stringValue", stringValue: "1" }, 87 | }; // "0" 88 | accountAttributes["loudness-levels"] = { 89 | value: { oneofKind: "stringValue", stringValue: "1:-9.0,0.0,3.0:-2.0" }, 90 | }; 91 | accountAttributes["payments-initial-campaign"] = { 92 | value: { oneofKind: "stringValue", stringValue: "web" }, 93 | }; 94 | accountAttributes["shuffle-eligible"] = { 95 | value: { oneofKind: "boolValue", boolValue: true }, 96 | }; 97 | accountAttributes["unrestricted"] = { 98 | value: { oneofKind: "boolValue", boolValue: true }, 99 | }; 100 | accountAttributes["com.spotify.madprops.use.ucs.product.state"] = { 101 | value: { oneofKind: "boolValue", boolValue: false }, 102 | }; 103 | return accountAttributes; 104 | } 105 | -------------------------------------------------------------------------------- /src/function/modifiedAssignedValues.mjs: -------------------------------------------------------------------------------- 1 | export default function modifiedAssignedValues(assignedValues = []) { 2 | assignedValues = assignedValues 3 | .map((assignedValue) => { 4 | switch (assignedValue?.propertyId?.scope) { 5 | case "ios-feature-reinventfree": 6 | switch (assignedValue?.propertyId?.name) { 7 | case "is_india_quicksilver_campaign_enabled": 8 | assignedValue.structuredValue = { 9 | oneofKind: "boolValue", 10 | boolValue: { value: false }, 11 | }; 12 | break; 13 | } 14 | break; 15 | case "ios-liveaudio-livestreampage": 16 | switch (assignedValue?.propertyId?.name) { 17 | case "context_menu_enabled": 18 | assignedValue.structuredValue = { 19 | oneofKind: "boolValue", 20 | boolValue: { value: true }, 21 | }; 22 | break; 23 | } 24 | break; 25 | /* 26 | case "ios-feature-lyrics": 27 | switch (assignedValue?.propertyId?.name) { 28 | case "enable_swift_concurrency": 29 | assignedValue.structuredValue = { "oneofKind": "boolValue", "boolValue": { "value": false } }; 30 | break; 31 | case "enable_hcux_improvements": 32 | assignedValue.structuredValue = { "oneofKind": "boolValue", "boolValue": { "value": false } }; 33 | break; 34 | }; 35 | break; 36 | */ 37 | } 38 | return assignedValue; 39 | }) 40 | .filter(Boolean); 41 | } 42 | -------------------------------------------------------------------------------- /src/function/setCache.mjs: -------------------------------------------------------------------------------- 1 | import { Console } from "@nsnanocat/util"; 2 | 3 | /** 4 | * Set Cache 5 | * @author VirgilClyne 6 | * @param {Map} cache - Playlists Cache / Subtitles Cache 7 | * @param {Number} cacheSize - Cache Size 8 | * @return {Boolean} isSaved 9 | */ 10 | export default function setCache(cache, cacheSize = 100) { 11 | Console.log("☑️ Set Cache", `cacheSize: ${cacheSize}`); 12 | cache = Array.from(cache || []); // Map转Array 13 | cache = cache.slice(-cacheSize); // 限制缓存大小 14 | Console.log("✅ Set Cache"); 15 | return cache; 16 | }; 17 | -------------------------------------------------------------------------------- /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 | if (!Array.isArray(Settings?.Types)) Settings.Types = Settings.Types ? [Settings.Types] : []; // 只有一个选项时,无逗号分隔 16 | Console.info(`typeof Settings: ${typeof Settings}`, `Settings: ${JSON.stringify(Settings)}`); 17 | /***************** Caches *****************/ 18 | if (typeof Caches?.Playlists !== "object" || Array.isArray(Caches?.Playlists)) Caches.Playlists = {}; // 创建Playlists缓存 19 | Caches.Playlists.Master = new Map(JSON.parse(Caches?.Playlists?.Master || "[]")); // Strings转Array转Map 20 | Caches.Playlists.Subtitle = new Map(JSON.parse(Caches?.Playlists?.Subtitle || "[]")); // Strings转Array转Map 21 | if (typeof Caches?.Subtitles !== "object") Caches.Subtitles = new Map(JSON.parse(Caches?.Subtitles || "[]")); // Strings转Array转Map 22 | if (typeof Caches?.Metadatas !== "object" || Array.isArray(Caches?.Metadatas)) Caches.Metadatas = {}; // 创建Playlists缓存 23 | if (typeof Caches?.Metadatas?.Tracks !== "object") Caches.Metadatas.Tracks = new Map(JSON.parse(Caches?.Metadatas?.Tracks || "[]")); // Strings转Array转Map 24 | //Console.debug(`typeof Caches: ${typeof Caches}`, `Caches: ${JSON.stringify(Caches)}`); 25 | /***************** Configs *****************/ 26 | Console.log("✅ Set Environment Variables"); 27 | return { Settings, Caches, Configs }; 28 | } 29 | -------------------------------------------------------------------------------- /src/request.dev.js: -------------------------------------------------------------------------------- 1 | import { $app, Console, done, fetch, Lodash as _, Storage } from "@nsnanocat/util"; 2 | import database from "./function/database.mjs"; 3 | import setENV from "./function/setENV.mjs"; 4 | import setCache from "./function/setCache.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("DualSubs", "Spotify", database); 24 | Console.logLevel = Settings.LogLevel; 25 | // 获取字幕类型与语言 26 | const Type = url.searchParams.get("subtype") ?? Settings.Type; 27 | const Languages = [url.searchParams.get("lang")?.toUpperCase?.() ?? Settings.Languages[0], (url.searchParams.get("tlang") ?? Caches?.tlang)?.toUpperCase?.() ?? Settings.Languages[1]]; 28 | Console.info(`Type: ${Type}`, `Languages: ${Languages}`); 29 | // 创建空数据 30 | let body = {}; 31 | // 方法判断 32 | switch ($request.method) { 33 | case "POST": 34 | case "PUT": 35 | case "PATCH": 36 | // biome-ignore lint/suspicious/noFallthroughSwitchClause: 37 | case "DELETE": 38 | // 格式判断 39 | switch (FORMAT) { 40 | case undefined: // 视为无body 41 | break; 42 | case "application/x-www-form-urlencoded": 43 | case "text/plain": 44 | default: 45 | break; 46 | case "application/x-mpegURL": 47 | case "application/x-mpegurl": 48 | case "application/vnd.apple.mpegurl": 49 | case "audio/mpegurl": 50 | //body = M3U8.parse($request.body); 51 | //Console.debug(`body: ${JSON.stringify(body)}`); 52 | //$request.body = M3U8.stringify(body); 53 | break; 54 | case "text/xml": 55 | case "text/html": 56 | case "text/plist": 57 | case "application/xml": 58 | case "application/plist": 59 | case "application/x-plist": 60 | //body = XML.parse($request.body); 61 | //Console.debug(`body: ${JSON.stringify(body)}`); 62 | //$request.body = XML.stringify(body); 63 | break; 64 | case "text/vtt": 65 | case "application/vtt": 66 | //body = VTT.parse($request.body); 67 | //Console.debug(`body: ${JSON.stringify(body)}`); 68 | //$request.body = VTT.stringify(body); 69 | break; 70 | case "text/json": 71 | case "application/json": 72 | //body = JSON.parse($request.body ?? "{}"); 73 | //Console.debug(`body: ${JSON.stringify(body)}`); 74 | //$request.body = JSON.stringify(body); 75 | break; 76 | case "application/protobuf": 77 | case "application/x-protobuf": 78 | case "application/vnd.google.protobuf": 79 | case "application/grpc": 80 | case "application/grpc+proto": 81 | case "application/octet-stream": { 82 | //Console.debug(`$request: ${JSON.stringify($request, null, 2)}`); 83 | let rawBody = $app === "Quantumult X" ? new Uint8Array($request.bodyBytes ?? []) : ($request.body ?? new Uint8Array()); 84 | //Console.debug(`isBuffer? ${ArrayBuffer.isView(rawBody)}: ${JSON.stringify(rawBody)}`); 85 | switch (FORMAT) { 86 | case "application/protobuf": 87 | case "application/x-protobuf": 88 | case "application/vnd.google.protobuf": 89 | switch (url.pathname) { 90 | case "/bootstrap/v1/bootstrap": 91 | case "/user-customization-service/v1/customize": 92 | delete $request.headers?.["If-None-Match"]; 93 | delete $request.headers?.["if-none-match"]; 94 | break; 95 | case "/extended-metadata/v0/extended-metadata": 96 | break; 97 | } 98 | break; 99 | case "application/grpc": 100 | case "application/grpc+proto": 101 | break; 102 | } 103 | // 写入二进制数据 104 | $request.body = rawBody; 105 | break; 106 | } 107 | } 108 | // break; // 不中断,继续处理URL 109 | // biome-ignore lint/suspicious/noFallthroughSwitchClause: 110 | case "GET": 111 | if (url.pathname.startsWith("/color-lyrics/v2/track/")) { 112 | const trackId = PATHs?.[3]; 113 | Console.debug(`trackId: ${trackId}`); 114 | const _request = JSON.parse(JSON.stringify($request)); 115 | _request.url = `https://api.spotify.com/v1/tracks?ids=${trackId}`; 116 | if (_request?.headers?.Accept) _request.headers.Accept = "application/json"; 117 | if (_request?.headers?.accept) _request.headers.accept = "application/json"; 118 | //Console.debug(`_request: ${JSON.stringify(_request)}`); 119 | const detectStutus = fetch($request); 120 | const detectTrack = fetch(_request); 121 | await Promise.allSettled([detectStutus, detectTrack]).then(results => { 122 | /* 123 | results.forEach((result, i) => { 124 | Console.debug(`result[${i}]: ${JSON.stringify(result)}`); 125 | }); 126 | */ 127 | switch (results[0].status) { 128 | case "fulfilled": { 129 | const response = results[0].value; 130 | switch (response?.statusCode ?? response?.status) { 131 | case 200: 132 | if (Settings.Types.includes("Translate")) url.searchParams.set("subtype", "Translate"); 133 | else if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); 134 | break; 135 | case 401: 136 | default: 137 | break; 138 | case 404: 139 | if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); 140 | break; 141 | } 142 | break; 143 | } 144 | case "rejected": 145 | Console.debug(`detectStutus.reason: ${JSON.stringify(results[0].reason)}`); 146 | if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); 147 | break; 148 | } 149 | switch (results[1].status) { 150 | case "fulfilled": { 151 | const response = results[1].value; 152 | body = JSON.parse(response.body); 153 | body?.tracks?.forEach?.(track => { 154 | //Console.debug(`track: ${JSON.stringify(track)}`); 155 | const trackId = track?.id; 156 | const trackInfo = { 157 | id: track?.id, 158 | track: track?.name, 159 | album: track?.album?.name, 160 | artist: track?.artists?.[0]?.name, 161 | }; 162 | // 写入数据 163 | Caches.Metadatas.Tracks.set(trackId, trackInfo); 164 | }); 165 | // 格式化缓存 166 | Console.debug(`Caches.Metadatas.Tracks: ${JSON.stringify([...Caches.Metadatas.Tracks.entries()])}`); 167 | Caches.Metadatas.Tracks = setCache(Caches.Metadatas.Tracks, Settings.CacheSize); 168 | // 写入持久化储存 169 | Storage.setItem(`@DualSubs.${"Spotify"}.Caches.Metadatas.Tracks`, Caches.Metadatas.Tracks); 170 | break; 171 | } 172 | case "rejected": 173 | Console.debug(`detectTrack.reason: ${JSON.stringify(results[1].reason)}`); 174 | break; 175 | } 176 | }); 177 | } 178 | case "HEAD": 179 | case "OPTIONS": 180 | break; 181 | case "CONNECT": 182 | case "TRACE": 183 | break; 184 | } 185 | $request.url = url.toString(); 186 | Console.debug(`$request.url: ${$request.url}`); 187 | })() 188 | .catch(e => Console.error(e)) 189 | .finally(() => { 190 | switch (typeof $response) { 191 | case "object": // 有构造回复数据,返回构造的回复数据 192 | //Console.debug("finally", `echo $response: ${JSON.stringify($response, null, 2)}`); 193 | if ($response.headers?.["Content-Encoding"]) $response.headers["Content-Encoding"] = "identity"; 194 | if ($response.headers?.["content-encoding"]) $response.headers["content-encoding"] = "identity"; 195 | switch ($app) { 196 | default: 197 | done({ response: $response }); 198 | break; 199 | case "Quantumult X": 200 | if (!$response.status) $response.status = "HTTP/1.1 200 OK"; 201 | delete $response.headers?.["Content-Length"]; 202 | delete $response.headers?.["content-length"]; 203 | delete $response.headers?.["Transfer-Encoding"]; 204 | done($response); 205 | break; 206 | } 207 | break; 208 | case "undefined": // 无构造回复数据,发送修改的请求数据 209 | //Console.debug("finally", `$request: ${JSON.stringify($request, null, 2)}`); 210 | done($request); 211 | break; 212 | default: 213 | Console.error(`不合法的 $response 类型: ${typeof $response}`); 214 | break; 215 | } 216 | }); 217 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | import { $app, Console, done, fetch, Lodash as _, Storage } from "@nsnanocat/util"; 2 | import database from "./function/database.mjs"; 3 | import setENV from "./function/setENV.mjs"; 4 | import setCache from "./function/setCache.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("DualSubs", "Spotify", database); 24 | Console.logLevel = Settings.LogLevel; 25 | // 获取字幕类型与语言 26 | const Type = url.searchParams.get("subtype") ?? Settings.Type; 27 | const Languages = [url.searchParams.get("lang")?.toUpperCase?.() ?? Settings.Languages[0], (url.searchParams.get("tlang") ?? Caches?.tlang)?.toUpperCase?.() ?? Settings.Languages[1]]; 28 | Console.info(`Type: ${Type}`, `Languages: ${Languages}`); 29 | // 创建空数据 30 | let body = {}; 31 | // 方法判断 32 | switch ($request.method) { 33 | case "POST": 34 | case "PUT": 35 | case "PATCH": 36 | // biome-ignore lint/suspicious/noFallthroughSwitchClause: 37 | case "DELETE": 38 | // 格式判断 39 | switch (FORMAT) { 40 | case undefined: // 视为无body 41 | break; 42 | case "application/x-www-form-urlencoded": 43 | case "text/plain": 44 | default: 45 | break; 46 | case "application/x-mpegURL": 47 | case "application/x-mpegurl": 48 | case "application/vnd.apple.mpegurl": 49 | case "audio/mpegurl": 50 | break; 51 | case "text/xml": 52 | case "text/html": 53 | case "text/plist": 54 | case "application/xml": 55 | case "application/plist": 56 | case "application/x-plist": 57 | break; 58 | case "text/vtt": 59 | case "application/vtt": 60 | break; 61 | case "text/json": 62 | case "application/json": 63 | break; 64 | case "application/protobuf": 65 | case "application/x-protobuf": 66 | case "application/vnd.google.protobuf": 67 | case "application/grpc": 68 | case "application/grpc+proto": 69 | case "application/octet-stream": { 70 | let rawBody = $app === "Quantumult X" ? new Uint8Array($request.bodyBytes ?? []) : ($request.body ?? new Uint8Array()); 71 | switch (FORMAT) { 72 | case "application/protobuf": 73 | case "application/x-protobuf": 74 | case "application/vnd.google.protobuf": 75 | switch (url.pathname) { 76 | case "/bootstrap/v1/bootstrap": 77 | case "/user-customization-service/v1/customize": 78 | delete $request.headers?.["If-None-Match"]; 79 | delete $request.headers?.["if-none-match"]; 80 | break; 81 | case "/extended-metadata/v0/extended-metadata": 82 | break; 83 | } 84 | break; 85 | case "application/grpc": 86 | case "application/grpc+proto": 87 | break; 88 | } 89 | // 写入二进制数据 90 | $request.body = rawBody; 91 | break; 92 | } 93 | } 94 | // break; // 不中断,继续处理URL 95 | // biome-ignore lint/suspicious/noFallthroughSwitchClause: 96 | case "GET": 97 | if (url.pathname.startsWith("/color-lyrics/v2/track/")) { 98 | const trackId = PATHs?.[3]; 99 | Console.debug(`trackId: ${trackId}`); 100 | const _request = JSON.parse(JSON.stringify($request)); 101 | _request.url = `https://api.spotify.com/v1/tracks?ids=${trackId}`; 102 | if (_request?.headers?.Accept) _request.headers.Accept = "application/json"; 103 | if (_request?.headers?.accept) _request.headers.accept = "application/json"; 104 | //Console.debug(`_request: ${JSON.stringify(_request)}`); 105 | const detectStutus = fetch($request); 106 | const detectTrack = fetch(_request); 107 | await Promise.allSettled([detectStutus, detectTrack]).then(results => { 108 | switch (results[0].status) { 109 | case "fulfilled": { 110 | const response = results[0].value; 111 | switch (response?.statusCode ?? response?.status) { 112 | case 200: 113 | if (Settings.Types.includes("Translate")) url.searchParams.set("subtype", "Translate"); 114 | else if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); 115 | break; 116 | case 401: 117 | default: 118 | break; 119 | case 404: 120 | if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); 121 | break; 122 | } 123 | break; 124 | } 125 | case "rejected": 126 | if (Settings.Types.includes("External")) url.searchParams.set("subtype", "External"); 127 | break; 128 | } 129 | switch (results[1].status) { 130 | case "fulfilled": { 131 | const response = results[1].value; 132 | body = JSON.parse(response.body); 133 | body?.tracks?.forEach?.(track => { 134 | const trackId = track?.id; 135 | const trackInfo = { 136 | id: track?.id, 137 | track: track?.name, 138 | album: track?.album?.name, 139 | artist: track?.artists?.[0]?.name, 140 | }; 141 | // 写入数据 142 | Caches.Metadatas.Tracks.set(trackId, trackInfo); 143 | }); 144 | // 格式化缓存 145 | Caches.Metadatas.Tracks = setCache(Caches.Metadatas.Tracks, Settings.CacheSize); 146 | // 写入持久化储存 147 | Storage.setItem(`@DualSubs.${"Spotify"}.Caches.Metadatas.Tracks`, Caches.Metadatas.Tracks); 148 | break; 149 | } 150 | case "rejected": 151 | Console.debug(`detectTrack.reason: ${JSON.stringify(results[1].reason)}`); 152 | break; 153 | } 154 | }); 155 | } 156 | case "HEAD": 157 | case "OPTIONS": 158 | break; 159 | case "CONNECT": 160 | case "TRACE": 161 | break; 162 | } 163 | $request.url = url.toString(); 164 | Console.debug(`$request.url: ${$request.url}`); 165 | })() 166 | .catch(e => Console.error(e)) 167 | .finally(() => { 168 | switch (typeof $response) { 169 | case "object": // 有构造回复数据,返回构造的回复数据 170 | //Console.debug("finally", `echo $response: ${JSON.stringify($response, null, 2)}`); 171 | if ($response.headers?.["Content-Encoding"]) $response.headers["Content-Encoding"] = "identity"; 172 | if ($response.headers?.["content-encoding"]) $response.headers["content-encoding"] = "identity"; 173 | switch ($app) { 174 | default: 175 | done({ response: $response }); 176 | break; 177 | case "Quantumult X": 178 | if (!$response.status) $response.status = "HTTP/1.1 200 OK"; 179 | delete $response.headers?.["Content-Length"]; 180 | delete $response.headers?.["content-length"]; 181 | delete $response.headers?.["Transfer-Encoding"]; 182 | done($response); 183 | break; 184 | } 185 | break; 186 | case "undefined": // 无构造回复数据,发送修改的请求数据 187 | //Console.debug("finally", `$request: ${JSON.stringify($request, null, 2)}`); 188 | done($request); 189 | break; 190 | default: 191 | Console.error(`不合法的 $response 类型: ${typeof $response}`); 192 | break; 193 | } 194 | }); 195 | -------------------------------------------------------------------------------- /src/response.dev.js: -------------------------------------------------------------------------------- 1 | import { $app, Console, done, Lodash as _, Storage } from "@nsnanocat/util"; 2 | import database from "./function/database.mjs"; 3 | import setENV from "./function/setENV.mjs"; 4 | import setCache from "./function/setCache.mjs"; 5 | import modifiedAssignedValues from "./function/modifiedAssignedValues.mjs"; 6 | import modifiedAccountAttributes from "./function/modifiedAccountAttributes.mjs"; 7 | import { BootstrapResponse } from "./protobuf/spotify/remoteConfig/Bootstrap.js"; 8 | import { UcsResponseWrapper } from "./protobuf/spotify/remoteConfig/Ucs.js"; 9 | import { BatchedExtensionResponse } from "./protobuf/spotify/ExtendedMetadata.js"; 10 | /***************** Processing *****************/ 11 | // 解构URL 12 | const url = new URL($request.url); 13 | Console.info(`url: ${url.toJSON()}`); 14 | // 获取连接参数 15 | const PATH = url.pathname; 16 | Console.info(`PATH: ${PATH}`); 17 | // 解析格式 18 | const FORMAT = ($response.headers?.["Content-Type"] ?? $response.headers?.["content-type"])?.split(";")?.[0]; 19 | Console.info(`FORMAT: ${FORMAT}`); 20 | (async () => { 21 | /** 22 | * 设置 23 | * @type {{Settings: import('./types').Settings}} 24 | */ 25 | const { Settings, Caches, Configs } = setENV("DualSubs", "Spotify", database); 26 | Console.logLevel = Settings.LogLevel; 27 | // 获取字幕类型与语言 28 | const Type = url.searchParams.get("subtype") ?? Settings.Type; 29 | const Languages = [url.searchParams.get("lang")?.toUpperCase?.() ?? Settings.Languages[0], (url.searchParams.get("tlang") ?? Caches?.tlang)?.toUpperCase?.() ?? Settings.Languages[1]]; 30 | Console.info(`Type: ${Type}`, `Languages: ${Languages}`); 31 | // 创建空数据 32 | let body = {}; 33 | // 格式判断 34 | switch (FORMAT) { 35 | case undefined: // 视为无body 36 | break; 37 | case "application/x-www-form-urlencoded": 38 | case "text/plain": 39 | default: 40 | break; 41 | case "application/x-mpegURL": 42 | case "application/x-mpegurl": 43 | case "application/vnd.apple.mpegurl": 44 | case "audio/mpegurl": 45 | //body = M3U8.parse($response.body); 46 | //Console.debug(`body: ${JSON.stringify(body)}`); 47 | //$response.body = M3U8.stringify(body); 48 | break; 49 | case "text/xml": 50 | case "text/html": 51 | case "text/plist": 52 | case "application/xml": 53 | case "application/plist": 54 | case "application/x-plist": 55 | //body = XML.parse($response.body); 56 | //Console.debug(`body: ${JSON.stringify(body)}`); 57 | //$response.body = XML.stringify(body); 58 | break; 59 | case "text/vtt": 60 | case "application/vtt": 61 | //body = VTT.parse($response.body); 62 | //Console.debug(`body: ${JSON.stringify(body)}`); 63 | //$response.body = VTT.stringify(body); 64 | break; 65 | case "text/json": 66 | case "application/json": 67 | body = JSON.parse($response.body ?? "{}"); 68 | Console.debug(`body: ${JSON.stringify(body)}`); 69 | switch (PATH) { 70 | case "/melody/v1/product_state": 71 | //body.product = "premium"; 72 | body.country = Settings.Country; 73 | //body.ads = "0"; 74 | //body["on-demand"] = "1"; 75 | body["selected-language"] = Settings.Languages[1].toLowerCase(); 76 | //body["multiuserplan-current-size"] 77 | //body["preferred-locale"] 78 | //body["multiuserplan-member-type"] 79 | //body["is-standalone-audiobooks"] 80 | //body.catalogue = "premium"; 81 | break; 82 | case "/v1/tracks": 83 | body?.tracks?.forEach?.(track => { 84 | Console.debug(`track: ${JSON.stringify(track)}`); 85 | const trackId = track?.id; 86 | const trackInfo = { 87 | track: track?.name, 88 | album: track?.album?.name, 89 | artist: track?.artists?.[0]?.name, 90 | }; 91 | // 写入数据 92 | Caches.Metadatas.Tracks.set(trackId, trackInfo); 93 | }); 94 | // 格式化缓存 95 | Console.debug(`Caches.Metadatas.Tracks: ${JSON.stringify([...Caches.Metadatas.Tracks.entries()])}`); 96 | Caches.Metadatas.Tracks = setCache(Caches.Metadatas.Tracks, Settings.CacheSize); 97 | // 写入持久化储存 98 | Storage.setItem(`@DualSubs.${"Spotify"}.Caches.Metadatas.Tracks`, Caches.Metadatas.Tracks); 99 | break; 100 | } 101 | $response.body = JSON.stringify(body); 102 | break; 103 | case "application/protobuf": 104 | case "application/x-protobuf": 105 | case "application/vnd.google.protobuf": 106 | case "application/grpc": 107 | case "application/grpc+proto": 108 | case "application/octet-stream": { 109 | //Console.debug(`$response: ${JSON.stringify($response, null, 2)}`); 110 | let rawBody = $app === "Quantumult X" ? new Uint8Array($response.bodyBytes ?? []) : ($response.body ?? new Uint8Array()); 111 | //Console.debug(`isBuffer? ${ArrayBuffer.isView(rawBody)}: ${JSON.stringify(rawBody)}`); 112 | switch (FORMAT) { 113 | case "application/protobuf": 114 | case "application/x-protobuf": 115 | case "application/vnd.google.protobuf": 116 | switch (PATH) { 117 | case "/bootstrap/v1/bootstrap": 118 | case "/user-customization-service/v1/customize": 119 | switch (PATH) { 120 | case "/bootstrap/v1/bootstrap": { 121 | body = BootstrapResponse.fromBinary(rawBody); 122 | Console.debug(`body: ${JSON.stringify(body)}`); 123 | let assignedValues = body?.ucsResponseV0?.result?.success?.customization?.result?.success?.resolveResult?.resolveSuccess?.configuration?.assignedValues; 124 | if (assignedValues) { 125 | assignedValues = modifiedAssignedValues(assignedValues); 126 | } 127 | let accountAttributes = body?.ucsResponseV0?.result?.success?.customization?.result?.success?.accountAttributesResult?.accountAttributesSuccess?.accountAttributes; 128 | if (accountAttributes) { 129 | accountAttributes.country_code = { 130 | value: { 131 | oneofKind: "stringValue", 132 | stringValue: Settings.CountryCode, 133 | }, 134 | }; 135 | accountAttributes = modifiedAccountAttributes(accountAttributes); 136 | } 137 | //Console.debug(`body: ${JSON.stringify(body)}`); 138 | rawBody = BootstrapResponse.toBinary(body); 139 | break; 140 | } 141 | case "/user-customization-service/v1/customize": { 142 | body = UcsResponseWrapper.fromBinary(rawBody); 143 | Console.debug(`body: ${JSON.stringify(body)}`); 144 | let assignedValues = body?.result?.success?.resolveResult?.resolveSuccess?.configuration?.assignedValues; 145 | if (assignedValues) { 146 | assignedValues = modifiedAssignedValues(assignedValues); 147 | } 148 | let accountAttributes = body?.result?.success?.accountAttributesResult?.accountAttributesSuccess?.accountAttributes; 149 | if (accountAttributes) { 150 | accountAttributes.country_code = { 151 | value: { 152 | oneofKind: "stringValue", 153 | stringValue: Settings.CountryCode, 154 | }, 155 | }; 156 | accountAttributes = modifiedAccountAttributes(accountAttributes); 157 | } 158 | Console.debug(`body: ${JSON.stringify(body)}`); 159 | rawBody = UcsResponseWrapper.toBinary(body); 160 | break; 161 | } 162 | } 163 | break; 164 | case "/extended-metadata/v0/extended-metadata": { 165 | body = BatchedExtensionResponse.fromBinary(rawBody); 166 | Console.debug(`body: ${JSON.stringify(body)}`); 167 | rawBody = BatchedExtensionResponse.toBinary(body); 168 | break; 169 | } 170 | } 171 | break; 172 | case "application/grpc": 173 | case "application/grpc+proto": 174 | break; 175 | } 176 | // 写入二进制数据 177 | $response.body = rawBody; 178 | break; 179 | } 180 | } 181 | })() 182 | .catch(e => Console.error(e)) 183 | .finally(() => done($response)); 184 | -------------------------------------------------------------------------------- /src/response.js: -------------------------------------------------------------------------------- 1 | import { $app, Console, done, Lodash as _, Storage } from "@nsnanocat/util"; 2 | import database from "./function/database.mjs"; 3 | import setENV from "./function/setENV.mjs"; 4 | import setCache from "./function/setCache.mjs"; 5 | import modifiedAssignedValues from "./function/modifiedAssignedValues.mjs"; 6 | import modifiedAccountAttributes from "./function/modifiedAccountAttributes.mjs"; 7 | import { BootstrapResponse } from "./protobuf/spotify/remoteConfig/Bootstrap.js"; 8 | import { UcsResponseWrapper } from "./protobuf/spotify/remoteConfig/Ucs.js"; 9 | import { BatchedExtensionResponse } from "./protobuf/spotify/ExtendedMetadata.js"; 10 | /***************** Processing *****************/ 11 | // 解构URL 12 | const url = new URL($request.url); 13 | Console.info(`url: ${url.toJSON()}`); 14 | // 获取连接参数 15 | const PATH = url.pathname; 16 | Console.info(`PATH: ${PATH}`); 17 | // 解析格式 18 | const FORMAT = ($response.headers?.["Content-Type"] ?? $response.headers?.["content-type"])?.split(";")?.[0]; 19 | Console.info(`FORMAT: ${FORMAT}`); 20 | (async () => { 21 | /** 22 | * 设置 23 | * @type {{Settings: import('./types').Settings}} 24 | */ 25 | const { Settings, Caches, Configs } = setENV("DualSubs", "Spotify", database); 26 | Console.logLevel = Settings.LogLevel; 27 | // 获取字幕类型与语言 28 | const Type = url.searchParams.get("subtype") ?? Settings.Type; 29 | const Languages = [url.searchParams.get("lang")?.toUpperCase?.() ?? Settings.Languages[0], (url.searchParams.get("tlang") ?? Caches?.tlang)?.toUpperCase?.() ?? Settings.Languages[1]]; 30 | Console.info(`Type: ${Type}`, `Languages: ${Languages}`); 31 | // 创建空数据 32 | let body = {}; 33 | // 格式判断 34 | switch (FORMAT) { 35 | case undefined: // 视为无body 36 | break; 37 | case "application/x-www-form-urlencoded": 38 | case "text/plain": 39 | default: 40 | break; 41 | case "application/x-mpegURL": 42 | case "application/x-mpegurl": 43 | case "application/vnd.apple.mpegurl": 44 | case "audio/mpegurl": 45 | break; 46 | case "text/xml": 47 | case "text/html": 48 | case "text/plist": 49 | case "application/xml": 50 | case "application/plist": 51 | case "application/x-plist": 52 | break; 53 | case "text/vtt": 54 | case "application/vtt": 55 | break; 56 | case "text/json": 57 | case "application/json": 58 | body = JSON.parse($response.body ?? "{}"); 59 | Console.debug(`body: ${JSON.stringify(body)}`); 60 | switch (PATH) { 61 | case "/melody/v1/product_state": 62 | body.country = Settings.Country; 63 | body["selected-language"] = Settings.Languages[1].toLowerCase(); 64 | break; 65 | case "/v1/tracks": 66 | body?.tracks?.forEach?.(track => { 67 | Console.debug(`track: ${JSON.stringify(track)}`); 68 | const trackId = track?.id; 69 | const trackInfo = { 70 | track: track?.name, 71 | album: track?.album?.name, 72 | artist: track?.artists?.[0]?.name, 73 | }; 74 | // 写入数据 75 | Caches.Metadatas.Tracks.set(trackId, trackInfo); 76 | }); 77 | // 格式化缓存 78 | Caches.Metadatas.Tracks = setCache(Caches.Metadatas.Tracks, Settings.CacheSize); 79 | // 写入持久化储存 80 | Storage.setItem(`@DualSubs.${"Spotify"}.Caches.Metadatas.Tracks`, Caches.Metadatas.Tracks); 81 | break; 82 | } 83 | $response.body = JSON.stringify(body); 84 | break; 85 | case "application/protobuf": 86 | case "application/x-protobuf": 87 | case "application/vnd.google.protobuf": 88 | case "application/grpc": 89 | case "application/grpc+proto": 90 | case "application/octet-stream": { 91 | let rawBody = $app === "Quantumult X" ? new Uint8Array($response.bodyBytes ?? []) : ($response.body ?? new Uint8Array()); 92 | switch (FORMAT) { 93 | case "application/protobuf": 94 | case "application/x-protobuf": 95 | case "application/vnd.google.protobuf": 96 | switch (PATH) { 97 | case "/bootstrap/v1/bootstrap": 98 | case "/user-customization-service/v1/customize": 99 | switch (PATH) { 100 | case "/bootstrap/v1/bootstrap": { 101 | body = BootstrapResponse.fromBinary(rawBody); 102 | let assignedValues = body?.ucsResponseV0?.result?.success?.customization?.result?.success?.resolveResult?.resolveSuccess?.configuration?.assignedValues; 103 | if (assignedValues) { 104 | assignedValues = modifiedAssignedValues(assignedValues); 105 | } 106 | let accountAttributes = body?.ucsResponseV0?.result?.success?.customization?.result?.success?.accountAttributesResult?.accountAttributesSuccess?.accountAttributes; 107 | if (accountAttributes) { 108 | accountAttributes.country_code = { 109 | value: { 110 | oneofKind: "stringValue", 111 | stringValue: Settings.CountryCode, 112 | }, 113 | }; 114 | accountAttributes = modifiedAccountAttributes(accountAttributes); 115 | } 116 | rawBody = BootstrapResponse.toBinary(body); 117 | break; 118 | } 119 | case "/user-customization-service/v1/customize": { 120 | body = UcsResponseWrapper.fromBinary(rawBody); 121 | let assignedValues = body?.result?.success?.resolveResult?.resolveSuccess?.configuration?.assignedValues; 122 | if (assignedValues) { 123 | assignedValues = modifiedAssignedValues(assignedValues); 124 | } 125 | let accountAttributes = body?.result?.success?.accountAttributesResult?.accountAttributesSuccess?.accountAttributes; 126 | if (accountAttributes) { 127 | accountAttributes.country_code = { 128 | value: { 129 | oneofKind: "stringValue", 130 | stringValue: Settings.CountryCode, 131 | }, 132 | }; 133 | accountAttributes = modifiedAccountAttributes(accountAttributes); 134 | } 135 | rawBody = UcsResponseWrapper.toBinary(body); 136 | break; 137 | } 138 | } 139 | break; 140 | case "/extended-metadata/v0/extended-metadata": { 141 | body = BatchedExtensionResponse.fromBinary(rawBody); 142 | rawBody = BatchedExtensionResponse.toBinary(body); 143 | break; 144 | } 145 | } 146 | break; 147 | case "application/grpc": 148 | case "application/grpc+proto": 149 | break; 150 | } 151 | // 写入二进制数据 152 | $response.body = rawBody; 153 | break; 154 | } 155 | } 156 | })() 157 | .catch(e => Console.error(e)) 158 | .finally(() => done($response)); 159 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Settings { 2 | /** 3 | * 总功能开关 4 | * 5 | * 是否启用此APP修改 6 | * 7 | * @defaultValue true 8 | */ 9 | Switch?: boolean; 10 | /** 11 | * [歌词]启用类型(多选) 12 | * 13 | * 请选择要添加的歌词选项,如果为多选,则会自动决定提供的歌词类型。 14 | * 15 | * @remarks 16 | * 17 | * Possible values: 18 | * - `'Translate'` - 翻译歌词(翻译器) 19 | * - `'External'` - 外部歌词(外部源) 20 | * 21 | * @defaultValue ["Translate","External"] 22 | */ 23 | Types?: ('Translate' | 'External')[]; 24 | /** 25 | * [翻译器]服务商API 26 | * 27 | * 请选择翻译器所使用的服务商API,更多翻译选项请使用BoxJs。 28 | * 29 | * @remarks 30 | * 31 | * Possible values: 32 | * - `'Google'` - Google Translate 33 | * - `'Microsoft'` - Microsoft Translator(需填写API) 34 | * 35 | * @defaultValue "Google" 36 | */ 37 | Vendor?: 'Google' | 'Microsoft'; 38 | /** 39 | * [歌词]服务商API 40 | * 41 | * 请选择外部源所使用的服务商API。 42 | * 43 | * @remarks 44 | * 45 | * Possible values: 46 | * - `'NeteaseMusic'` - 网易云音乐(官方) 47 | * - `'QQMusic'` - QQ音乐(官方) 48 | * - `'NeteaseMusicNodeJS'` - 网易云音乐 NodeJS API 49 | * 50 | * @defaultValue "NeteaseMusic" 51 | */ 52 | LrcVendor?: 'NeteaseMusic' | 'QQMusic' | 'NeteaseMusicNodeJS'; 53 | } 54 | -------------------------------------------------------------------------------- /template/boxjs.settings.json: -------------------------------------------------------------------------------- 1 | [{"id":"@DualSubs.Spotify.Settings.Types","name":"[歌词] 启用类型(多选)","type":"checkboxes","val":["Translate","External"],"items":[{"key":"Translate","label":"翻译歌词(翻译器)"},{"key":"External","label":"外部歌词(外部源)"}],"desc":"请选择要添加的歌词选项,如果为多选,则会自动决定提供的歌词类型。"},{"id":"@DualSubs.Spotify.Settings.Languages[0]","name":"[翻译器] 主语言(源语言)","type":"selects","val":"AUTO","items":[{"key":"AUTO","label":"自动 - Automatic"},{"key":"ZH","label":"中文(自动)"},{"key":"ZH-HANS","label":"中文(简体)"},{"key":"ZH-HK","label":"中文(香港)"},{"key":"ZH-HANT","label":"中文(繁体)"},{"key":"EN","label":"English - 英语(自动)"},{"key":"ES","label":"Español - 西班牙语(自动)"},{"key":"JA","label":"日本語 - 日语"},{"key":"KO","label":"한국어 - 韩语"},{"key":"DE","label":"Deutsch - 德语"},{"key":"FR","label":"Français - 法语"},{"key":"TR","label":"Türkçe - 土耳其语"},{"key":"KM","label":"ភាសាខ្មែរ - 高棉语"}],"desc":"仅当源语言识别不准确时更改此选项。"},{"id":"@DualSubs.Spotify.Settings.Languages[1]","name":"[翻译器] 副语言(目标语言)","type":"selects","val":"ZH","items":[{"key":"ZH","label":"中文(自动)"},{"key":"ZH-HANS","label":"中文(简体)"},{"key":"ZH-HK","label":"中文(香港)"},{"key":"ZH-HANT","label":"中文(繁体)"},{"key":"EN","label":"English - 英语(自动)"},{"key":"ES","label":"Español - 西班牙语(自动)"},{"key":"JA","label":"日本語 - 日语"},{"key":"KO","label":"한국어 - 韩语"},{"key":"DE","label":"Deutsch - 德语"},{"key":"FR","label":"Français - 法语"},{"key":"TR","label":"Türkçe - 土耳其语"},{"key":"KM","label":"ភាសាខ្មែរ - 高棉语"}],"desc":"请指定翻译歌词的目标语言。"},{"id":"@DualSubs.Spotify.Settings.Vendor","name":"[翻译器] 服务商API","type":"selects","val":"Google","items":[{"key":"Google","label":"Google Translate"},{"key":"Microsoft","label":"Microsoft Translator(需填写API)"}],"desc":"请选择翻译器所使用的服务商API,更多翻译选项请使用BoxJs。"},{"id":"@DualSubs.Spotify.Settings.LrcVendor","name":"[歌词] 服务商API","type":"selects","val":"NeteaseMusic","items":[{"key":"NeteaseMusic","label":"网易云音乐(官方)"},{"key":"QQMusic","label":"QQ音乐(官方)"},{"key":"NeteaseMusicNodeJS","label":"网易云音乐 NodeJS API"}],"desc":"请选择外部源所使用的服务商API。"},{"id":"@DualSubs.Spotify.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 | [Script] 17 | http-response ^https?:\/\/api\.spotify\.com\/v1\/tracks\? requires-body=1, script-path=https://github.com/DualSubs/Spotify/releases/download/v{{@package 'version'}}/response.bundle.js, tag=🍿️ DualSubs.Spotify.Tracks.response.json, argument={{{scriptParams}}} 18 | 19 | http-request ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/(.+)\?(.*)format=json requires-body=1, script-path=https://github.com/DualSubs/Spotify/releases/download/v{{@package 'version'}}/request.bundle.js, tag=🍿️ DualSubs.Spotify.Lyrics.request.json, argument={{{scriptParams}}} 20 | http-request ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/\w+\?(.*) requires-body=1, binary-body-mode=1, script-path=https://github.com/DualSubs/Spotify/releases/download/v{{@package 'version'}}/request.bundle.js, tag=🍿️ DualSubs.Spotify.Lyrics.request.proto, argument={{{scriptParams}}} 21 | 22 | http-response ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/(.+)\?(.*)format=json(.*)subtype=Translate requires-body=1, script-path=https://github.com/DualSubs/Universal/releases/latest/download/Translate.response.bundle.js, tag=🍿️ DualSubs.Spotify.Translate.Lyrics.response.json, argument={{{scriptParams}}} 23 | http-response ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/\w+\?(.*)subtype=Translate requires-body=1, binary-body-mode=1, script-path=https://github.com/DualSubs/Universal/releases/latest/download/Translate.response.bundle.js, tag=🍿️ DualSubs.Spotify.Translate.Lyrics.response.proto, argument={{{scriptParams}}} 24 | 25 | http-response ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/(.+)\?(.*)format=json(.*)subtype=External requires-body=1, script-path=https://github.com/DualSubs/Universal/releases/latest/download/External.Lyrics.response.bundle.js, tag=🍿️ DualSubs.Spotify.External.Lyrics.response.json, argument={{{scriptParams}}} 26 | http-response ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/\w+\?(.*)subtype=External requires-body=1, binary-body-mode=1, script-path=https://github.com/DualSubs/Universal/releases/latest/download/External.Lyrics.response.bundle.js, tag=🍿️ DualSubs.Spotify.External.Lyrics.response.proto, argument={{{scriptParams}}} 27 | 28 | [MITM] 29 | hostname = api.spotify.com, spclient.wg.spotify.com 30 | -------------------------------------------------------------------------------- /template/quantumultx.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 | 11 | #[rewrite_local] 12 | # 🍿️ DualSubs.Spotify.Tracks.response 13 | ^https?:\/\/api\.spotify\.com\/v1\/tracks\? url script-response-body https://github.com/DualSubs/Spotify/releases/download/v{{@package 'version'}}/response.bundle.js 14 | 15 | # 🍿️ DualSubs.Spotify.Lyrics.request 16 | ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/(.+)\?(.*) url script-request-body https://github.com/DualSubs/Spotify/releases/download/v{{@package 'version'}}/request.bundle.js 17 | 18 | # 🍿️ DualSubs.Spotify.Translate.Lyrics.response 19 | ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/(.+)\?(.*)subtype=Translate url script-response-body https://github.com/DualSubs/Universal/releases/latest/download/Translate.response.bundle.js 20 | 21 | # 🍿️ DualSubs.Spotify.External.Lyrics.response 22 | ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/(.+)\?(.*)subtype=External url script-response-body https://github.com/DualSubs/Universal/releases/latest/download/External.Lyrics.response.bundle.js 23 | 24 | #[mitm] 25 | hostname = api.spotify.com, spclient.wg.spotify.com 26 | -------------------------------------------------------------------------------- /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 | mitm: 19 | - "api.spotify.com" 20 | - "spclient.wg.spotify.com" 21 | script: 22 | - match: ^https?:\/\/api\.spotify\.com\/v1\/tracks\? 23 | name: 🍿️ DualSubs.Spotify.response 24 | type: response 25 | require-body: true 26 | 27 | - match: ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/(.+)\?(.*)format=json 28 | name: 🍿️ DualSubs.Spotify.request 29 | type: request 30 | require-body: true 31 | - match: ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/\w+\?(.*) 32 | name: 🍿️ DualSubs.Spotify.request 33 | type: request 34 | require-body: true 35 | binary-mode: true 36 | 37 | - match: ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/(.+)\?(.*)format=json(.*)subtype=Translate 38 | name: 🍿️ DualSubs.Translate.Lyrics.response 39 | type: response 40 | require-body: true 41 | - match: ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/\w+\?(.*)subtype=Translate 42 | name: 🍿️ DualSubs.Translate.Lyrics.response 43 | type: response 44 | require-body: true 45 | binary-mode: true 46 | 47 | - match: ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/(.+)\?(.*)format=json(.*)subtype=External 48 | name: 🍿️ DualSubs.External.Lyrics.response 49 | type: response 50 | require-body: true 51 | - match: ^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/\w+\?(.*)subtype=External 52 | name: 🍿️ DualSubs.External.Lyrics.response 53 | type: response 54 | require-body: true 55 | binary-mode: true 56 | 57 | script-providers: 58 | 🍿️ DualSubs.Spotify.request: 59 | url: https://github.com/DualSubs/Spotify/releases/download/v{{@package 'version'}}/request.bundle.js 60 | interval: 86400 61 | 🍿️ DualSubs.Spotify.response: 62 | url: https://github.com/DualSubs/Spotify/releases/download/v{{@package 'version'}}/response.bundle.js 63 | interval: 86400 64 | 🍿️ DualSubs.Translate.Lyrics.response: 65 | url: https://github.com/DualSubs/Universal/releases/latest/download/Translate.response.bundle.js 66 | interval: 86400 67 | 🍿️ DualSubs.External.Lyrics.response: 68 | url: https://github.com/DualSubs/Universal/releases/latest/download/External.Lyrics.response.bundle.js 69 | interval: 86400 70 | -------------------------------------------------------------------------------- /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 | [Script] 14 | 🍿️ DualSubs.Spotify.Tracks.response.json = type=http-response, pattern=^https?:\/\/api\.spotify\.com\/v1\/tracks\?, requires-body=1, engine=webview, script-path=https://github.com/DualSubs/Spotify/releases/download/v{{@package 'version'}}/response.bundle.js, argument={{{scriptParams}}} 15 | 16 | 🍿️ DualSubs.Spotify.Lyrics.request.json = type=http-request, pattern=^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/(.+)\?(.*)format=json, requires-body=1, script-path=https://github.com/DualSubs/Spotify/releases/download/v{{@package 'version'}}/request.bundle.js, argument={{{scriptParams}}} 17 | 🍿️ DualSubs.Spotify.Lyrics.request.proto = type=http-request, pattern=^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/\w+\?(.*), requires-body=1, binary-body-mode=1, script-path=https://github.com/DualSubs/Spotify/releases/download/v{{@package 'version'}}/request.bundle.js, argument={{{scriptParams}}} 18 | 19 | 🍿️ DualSubs.Spotify.Translate.Lyrics.response.json = type=http-response, pattern=^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/(.+)\?(.*)format=json(.*)subtype=Translate, requires-body=1, script-path=https://github.com/DualSubs/Universal/releases/latest/download/Translate.response.bundle.js, argument={{{scriptParams}}} 20 | 🍿️ DualSubs.Spotify.Translate.Lyrics.response.proto = type=http-response, pattern=^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/\w+\?(.*)subtype=Translate, requires-body=1, binary-body-mode=1, script-path=https://github.com/DualSubs/Universal/releases/latest/download/Translate.response.bundle.js, argument={{{scriptParams}}} 21 | 22 | 🍿️ DualSubs.Spotify.External.Lyrics.response.json = type=http-response, pattern=^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/(.+)\?(.*)format=json(.*)subtype=External, requires-body=1, script-path=https://github.com/DualSubs/Universal/releases/latest/download/External.Lyrics.response.bundle.js, argument={{{scriptParams}}} 23 | 🍿️ DualSubs.Spotify.External.Lyrics.response.proto = type=http-response, pattern=^https?:\/\/spclient\.wg\.spotify\.com\/color-lyrics\/v2\/track\/\w+\?(.*)subtype=External, requires-body=1, binary-body-mode=1, script-path=https://github.com/DualSubs/Universal/releases/latest/download/External.Lyrics.response.bundle.js, argument={{{scriptParams}}} 24 | 25 | [MITM] 26 | hostname = %APPEND% api.spotify.com, spclient.wg.spotify.com 27 | --------------------------------------------------------------------------------