├── .babelrc ├── .editorconfig ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── ----.yml │ └── --bug.yml └── workflows │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── FAQ.md ├── LICENSE ├── README.md ├── jsconfig.json ├── package-lock.json ├── package.json ├── publish ├── changeLog.md ├── index.js ├── parseChangelog.js ├── updateChangeLog.js └── utils.js ├── src ├── config.js ├── index.js ├── sources │ ├── index.js │ ├── kg.js │ ├── kw.js │ ├── mg.js │ ├── tx.js │ └── wy.js └── utils.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "corejs": "3", 7 | "useBuiltIns": "usage" 8 | } 9 | ] 10 | ], 11 | "plugins": [ 12 | "@babel/plugin-syntax-dynamic-import", 13 | "@babel/plugin-transform-modules-umd", 14 | "@babel/plugin-transform-runtime", 15 | "@babel/plugin-proposal-class-properties" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailling_whitespace = true -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "parser": "@babel/eslint-parser", 4 | "rules": { 5 | "no-new": "off", 6 | "camelcase": "off", 7 | "no-return-assign": "off", 8 | "space-before-function-paren": ["error", "never"], 9 | "no-var": "error", 10 | "no-fallthrough": "off", 11 | "prefer-promise-reject-errors": "off", 12 | "eqeqeq": "off", 13 | "no-multiple-empty-lines": [1, {"max": 2}], 14 | "comma-dangle": [2, "always-multiline"], 15 | "standard/no-callback-literal": "off", 16 | "prefer-const": "off", 17 | "no-labels": "off", 18 | "node/no-callback-literal": "off" 19 | }, 20 | "ignorePatterns": ["dist/*.js", "vendors/*.js"], 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/----.yml: -------------------------------------------------------------------------------- 1 | name: ✨功能请求 2 | description: 为这个项目提出一个想法,请先查看常见问题及搜索issue列表中有无你要提的问题 3 | title: "[Feature]: " 4 | body: 5 | - type: checkboxes 6 | id: check-answer 7 | attributes: 8 | label: 解决方案检查 9 | description: 请确保你已完成以下所有操作 10 | options: 11 | - label: 我已阅读常见问题(),但没有找到解决方案 12 | required: true 13 | - label: 我已搜索issue列表(),但没有发现类似的问题 14 | required: true 15 | - type: textarea 16 | id: problem-description 17 | attributes: 18 | label: 问题描述 19 | description: 请添加清晰简洁的描述,说明你希望通过此功能请求解决的问题 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: proposed-solution 24 | attributes: 25 | label: 描述你想要的解决方案 26 | description: 简洁明了地描述你要发生的事情 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: alternatives-considered 31 | attributes: 32 | label: 描述你考虑过的替代方案 33 | description: 对你考虑过的所有替代解决方案或功能的简洁明了的描述 34 | validations: 35 | required: false 36 | - type: textarea 37 | id: additional-information 38 | attributes: 39 | label: 附加信息 40 | description: 如果你的问题需要进一步解释,或者想要表达其他内容,请在此处添加更多信息。(直接把图片、视频拖到编辑框即可添加图片或视频) 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--bug.yml: -------------------------------------------------------------------------------- 1 | name: 🐞报告Bug 2 | description: 报告bug,请先查看常见问题及搜索issue列表中有无你要提的问题 3 | title: "[Bug]: " 4 | body: 5 | - type: checkboxes 6 | id: check-answer 7 | attributes: 8 | label: 解决方案检查 9 | description: 请确保你已完成以下所有操作 10 | options: 11 | - label: 我已阅读常见问题(),并没有找到解决方案 12 | required: true 13 | - label: 我已搜索issue列表(),并没有发现类似的问题 14 | required: true 15 | - type: textarea 16 | id: expected-behavior 17 | attributes: 18 | label: 预期行为 19 | description: 对期望发生的事情的清晰简明描述 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: actual-behavior 24 | attributes: 25 | label: 实际行为 26 | description: 对实际发生的事情的清晰简明描述 27 | validations: 28 | required: true 29 | - type: input 30 | id: operating-system-version 31 | attributes: 32 | label: 操作系统及版本 33 | description: 您使用的是什么操作系统版本?在 Android 上,单击 设置 > 关于 34 | placeholder: "例如 Android 10" 35 | validations: 36 | required: true 37 | - type: input 38 | id: browser-version 39 | attributes: 40 | label: 浏览器及版本 41 | description: 您使用的是什么浏览器版本?在浏览器的选项-关于可以找到版本号 42 | placeholder: "例如 Chrome 90" 43 | validations: 44 | required: true 45 | - type: textarea 46 | id: additional-information 47 | attributes: 48 | label: 附加信息 49 | description: 如果你的问题需要进一步解释,或者你所遇到的问题不容易重现,请在此处添加更多信息。(直接把图片、视频拖到编辑框即可添加图片或视频) 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | Tampermonkey: 10 | name: Tampermonkey 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out git repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Install Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: '18' 20 | 21 | - name: Cache Node Dependencies 22 | uses: actions/cache@v4 23 | with: 24 | path: node_modules 25 | key: ${{ runner.os }}-node-caches-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-node-caches- 28 | 29 | - name: Install dependencies 30 | run: npm install 31 | 32 | - name: Build Code 33 | run: npm run build 34 | 35 | # Push tag to GitHub if package.json version's tag is not tagged 36 | - name: Get package version 37 | run: node -p -e '`PACKAGE_VERSION=${require("./package.json").version}`' >> $GITHUB_ENV 38 | 39 | - name: Create git tag 40 | uses: pkgdeps/git-tag-action@v3 41 | with: 42 | github_token: ${{ secrets.GITHUB_TOKEN }} 43 | github_repo: ${{ github.repository }} 44 | version: ${{ env.PACKAGE_VERSION }} 45 | git_commit_sha: ${{ github.sha }} 46 | git_tag_prefix: "v" 47 | 48 | - name: Release 49 | uses: softprops/action-gh-release@v1 50 | with: 51 | body_path: ./publish/changeLog.md 52 | prerelease: false 53 | draft: false 54 | tag_name: v${{ env.PACKAGE_VERSION }} 55 | files: | 56 | dist/lx-music-script.js 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | dist 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # lx-music-script change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | Project versioning adheres to [Semantic Versioning](http://semver.org/). 6 | Commit convention is based on [Conventional Commits](http://conventionalcommits.org). 7 | Change log format is based on [Keep a Changelog](http://keepachangelog.com/). 8 | 9 | ## [0.2.16](https://github.com/lyswhut/lx-music-script/compare/v0.2.15...v0.2.16) - 2025-06-05 10 | 11 | ### 修复 12 | 13 | - 修复 tx 注入失效的问题 14 | - 修复 mg 注入失效的问题 15 | 16 | ## [0.2.15](https://github.com/lyswhut/lx-music-script/compare/v0.2.14...v0.2.15) - 2024-02-28 17 | 18 | ### 修复 19 | 20 | - 修复kw歌曲详情页按钮注入失效的问题 21 | 22 | ## [0.2.14](https://github.com/lyswhut/lx-music-script/compare/v0.2.13...v0.2.14) - 2023-08-07 23 | 24 | ### 修复 25 | 26 | - 修复tx源某些情况下未注入按钮的问题 27 | 28 | ## [0.2.13](https://github.com/lyswhut/lx-music-script/compare/v0.2.12...v0.2.13) - 2023-01-18 29 | 30 | ### 修复 31 | 32 | - 兼容v2.0.0版本将歌曲音质支持类似从 flac32bit 改为 flac24bit 的问题 33 | 34 | ### 其他 35 | 36 | - 由于greasyfork不允许使用压缩后的代码,现将forge切换到未压缩的版本 37 | 38 | ## [0.2.12](https://github.com/lyswhut/lx-music-script/compare/v0.2.11...v0.2.12) - 2022-08-21 39 | 40 | ### 优化 41 | 42 | - 添加kw、tx源flac hires音质记录 43 | 44 | ## [0.2.11](https://github.com/lyswhut/lx-music-script/compare/v0.2.10...v0.2.11) - 2022-08-21 45 | 46 | ### 修复 47 | 48 | - 修复wy源无版权歌曲详情页播放按钮没有注入的问题 49 | 50 | ## [0.2.10](https://github.com/lyswhut/lx-music-script/compare/v0.2.9...v0.2.10) - 2022-03-28 51 | 52 | ### 修复 53 | 54 | - 修复wy源vip歌曲详情按钮在未登录是没有注入的问题 55 | 56 | ## [0.2.9](https://github.com/lyswhut/lx-music-script/compare/v0.2.8...v0.2.9) - 2022-03-28 57 | 58 | ### 修复 59 | 60 | - 修复wy源vip歌曲详情按钮没有注入的问题 61 | 62 | ## [0.2.8](https://github.com/lyswhut/lx-music-script/compare/v0.2.7...v0.2.8) - 2022-03-03 63 | 64 | ### 修复 65 | 66 | - 修复tx源的歌手详情页样式覆盖问题 67 | 68 | ## [0.2.7](https://github.com/lyswhut/lx-music-script/compare/v0.2.6...v0.2.7) - 2022-03-03 69 | 70 | ### 修复 71 | 72 | - 修复tx源的歌手详情页的操作按钮遮挡问题 73 | 74 | ## [0.2.6](https://github.com/lyswhut/lx-music-script/compare/v0.2.5...v0.2.6) - 2022-01-28 75 | 76 | ### 修复 77 | 78 | - 修复tx源的歌曲详情页的操作按钮显示问题 79 | 80 | ## [0.2.5](https://github.com/lyswhut/lx-music-script/compare/v0.2.4...v0.2.5) - 2022-01-24 81 | 82 | ### 优化 83 | 84 | - 注入按钮后,按钮栏在宽度不足的时候让按钮自动换行 85 | 86 | ## [0.2.4](https://github.com/lyswhut/lx-music-script/compare/v0.2.3...v0.2.4) - 2022-01-16 87 | 88 | ### 修复 89 | 90 | - 修复按钮被重复注入的问题 91 | 92 | ## [0.2.3](https://github.com/lyswhut/lx-music-script/compare/v0.2.2...v0.2.3) - 2022-01-12 93 | 94 | ### 修复 95 | 96 | - 修复tx源首次进入歌曲详情按钮没注入的问题 97 | 98 | ## [0.2.2](https://github.com/lyswhut/lx-music-script/compare/v0.2.1...v0.2.2) - 2022-01-08 99 | 100 | ### 修复 101 | 102 | - 修复kw歌单详情页按钮注入问题 103 | 104 | ## [0.2.1](https://github.com/lyswhut/lx-music-script/compare/v0.2.0...v0.2.1) - 2022-01-08 105 | 106 | ### 修复 107 | 108 | - 修复kw从歌单列表页进歌单时按钮没有注入的问题 109 | 110 | ## [0.2.0](https://github.com/lyswhut/lx-music-script/compare/v0.1.0...v0.2.0) - 2022-01-08 111 | 112 | ### 新增 113 | 114 | - 新增kw源的支持 115 | - 新增wy源的支持 116 | - 新增mg源的支持 117 | 118 | ## 0.1.0 - 2021-12-30 119 | 120 | v0.1.0发布~ 121 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyswhut/lx-music-script/f2d8c3b7a3585075481bd97ee687b95eaa999322/FAQ.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 lyswhut 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lx-music-script 2 | 3 | LX Music 辅助脚本,提供在官方平台歌单、歌曲详情页直接调用 LX Music 的能力。 4 | 5 | 注意:需要 v1.17.0 及以上版本的 LX Music 才支持使用本脚本调用。 6 | 7 | ## 安装 8 | 9 | 这是一个油猴脚本,需要配合 [Tampermonkey](https://www.tampermonkey.net/) 使用。 10 | 11 | 在安装 Tampermonkey 扩展后,你可以前往 [Greasy Fork](https://greasyfork.org/zh-CN/scripts/438148) 安装此脚本。 12 | 13 | ## 使用 14 | 15 | 进入歌单详情页、歌曲详情页时会自动显示相关操作按钮。 16 | 17 | ## LICENSE 18 | 19 | MIT 20 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"], 6 | } 7 | }, 8 | "exclude": ["node_modules", "build", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lx-music-script", 3 | "version": "0.2.16", 4 | "description": "LX Music 辅助脚本,提供在官方平台歌单、歌曲详情页直接调用 LX Music 的能力。", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "publish": "node publish", 11 | "build": "cross-env NODE_ENV=production webpack --config webpack.config.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/lyswhut/lx-music-script.git" 16 | }, 17 | "keywords": [ 18 | "lx-music", 19 | "javascript" 20 | ], 21 | "author": "lyswhut", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/lyswhut/lx-music-script/issues" 25 | }, 26 | "homepage": "https://github.com/lyswhut/lx-music-script#readme", 27 | "devDependencies": { 28 | "@babel/core": "^7.27.4", 29 | "@babel/eslint-parser": "^7.27.5", 30 | "@babel/eslint-plugin": "^7.27.1", 31 | "@babel/plugin-proposal-class-properties": "^7.18.6", 32 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 33 | "@babel/plugin-transform-modules-umd": "^7.27.1", 34 | "@babel/plugin-transform-runtime": "^7.27.4", 35 | "@babel/preset-env": "^7.27.2", 36 | "@babel/runtime": "^7.27.4", 37 | "babel-loader": "^9.2.1", 38 | "babel-preset-minify": "^0.5.2", 39 | "chalk": "^4.1.2", 40 | "core-js": "^3.42.0", 41 | "cross-env": "^7.0.3", 42 | "eslint": "^8.57.1", 43 | "eslint-config-standard": "^17.1.0", 44 | "eslint-plugin-import": "^2.31.0", 45 | "eslint-plugin-node": "^11.1.0", 46 | "eslint-plugin-promise": "^6.6.0", 47 | "webpack": "^5.99.9", 48 | "webpack-cli": "^5.1.4" 49 | }, 50 | "dependencies": { 51 | "node-forge": "^1.3.1" 52 | }, 53 | "browserslist": [ 54 | "Chrome >= 80", 55 | "Firefox >= 71" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /publish/changeLog.md: -------------------------------------------------------------------------------- 1 | ### 修复 2 | 3 | - 修复 tx 注入失效的问题 4 | - 修复 mg 注入失效的问题 5 | -------------------------------------------------------------------------------- /publish/index.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const updateVersionFile = require('./updateChangeLog') 3 | 4 | const run = async() => { 5 | // const params = parseArgv(process.argv.slice(2)) 6 | // const bak = await updateVersionFile(params.ver) 7 | await updateVersionFile(process.argv.slice(2)[0]) 8 | console.log(chalk.green('日志更新完成~')) 9 | } 10 | 11 | 12 | run() 13 | -------------------------------------------------------------------------------- /publish/parseChangelog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {string} text 4 | * @returns 5 | */ 6 | exports.parseChangelog = (text) => { 7 | const versions = [] 8 | const lines = text.split(/\r\n|\r|\n/) 9 | let currentVersion = null 10 | let currentDate = null 11 | let currentDesc = '' 12 | 13 | for (const line of lines) { 14 | const versionMatch = line.match(/^\s*##\s+\[?(\d+\.\d+\.\d+)\]?.*?-\s+(\d{4}-\d{2}-\d{2})$/) 15 | if (versionMatch) { 16 | if (currentVersion) { 17 | versions.push({ 18 | version: currentVersion, 19 | date: currentDate, 20 | desc: currentDesc.trim(), 21 | }) 22 | } 23 | currentVersion = versionMatch[1] 24 | currentDate = versionMatch[3] 25 | currentDesc = '' 26 | } else { 27 | currentDesc += `${line}\n` 28 | } 29 | } 30 | 31 | if (currentVersion) { 32 | versions.push({ 33 | version: currentVersion, 34 | date: currentDate, 35 | desc: currentDesc.trim(), 36 | }) 37 | } 38 | 39 | return versions 40 | } 41 | -------------------------------------------------------------------------------- /publish/updateChangeLog.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { jp, formatTime } = require('./utils') 3 | const pkgDir = '../package.json' 4 | const pkg = require(pkgDir) 5 | const chalk = require('chalk') 6 | const { parseChangelog } = require('./parseChangelog') 7 | const changelogPath = jp('../CHANGELOG.md') 8 | 9 | const getPrevVer = (changeLog) => { 10 | const versions = parseChangelog(changeLog) 11 | return versions[0].version 12 | } 13 | 14 | const updateChangeLog = async(newVerNum, newChangeLog) => { 15 | let changeLog = fs.readFileSync(changelogPath, 'utf-8') 16 | const prevVer = await getPrevVer(changeLog) 17 | const log = `## [${newVerNum}](${pkg.repository.url.replace(/^git\+(http.+)\.git$/, '$1')}/compare/v${prevVer}...v${newVerNum}) - ${formatTime()}\n\n${newChangeLog}` 18 | fs.writeFileSync(changelogPath, changeLog.replace(/(## [?0.1.1]?)/, log + '\n$1'), 'utf-8') 19 | } 20 | 21 | 22 | module.exports = async newVerNum => { 23 | if (!newVerNum) { 24 | let verArr = pkg.version.split('.') 25 | verArr[verArr.length - 1] = parseInt(verArr[verArr.length - 1]) + 1 26 | newVerNum = verArr.join('.') 27 | } 28 | const newMDChangeLog = fs.readFileSync(jp('./changeLog.md'), 'utf-8') 29 | pkg.version = newVerNum 30 | 31 | console.log(chalk.blue('new version: ') + chalk.green(newVerNum)) 32 | 33 | await updateChangeLog(newVerNum, newMDChangeLog) 34 | 35 | fs.writeFileSync(jp(pkgDir), JSON.stringify(pkg, null, 2) + '\n', 'utf-8') 36 | } 37 | 38 | -------------------------------------------------------------------------------- /publish/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | exports.jp = (...p) => p.length ? path.join(__dirname, ...p) : __dirname 5 | 6 | exports.copyFile = (source, target) => new Promise((resolve, reject) => { 7 | const rd = fs.createReadStream(source) 8 | rd.on('error', err => { 9 | reject(err) 10 | }) 11 | const wr = fs.createWriteStream(target) 12 | wr.on('error', err => { 13 | reject(err) 14 | }) 15 | wr.on('close', () => resolve()) 16 | rd.pipe(wr) 17 | }) 18 | 19 | /** 20 | * 时间格式化 21 | * @param {Date} d 格式化的时间 22 | * @param {boolean} b 是否精确到秒 23 | */ 24 | exports.formatTime = (d, b) => { 25 | const _date = d == null ? new Date() : typeof d == 'string' ? new Date(d) : d 26 | const year = _date.getFullYear() 27 | const month = fm(_date.getMonth() + 1) 28 | const day = fm(_date.getDate()) 29 | if (!b) return year + '-' + month + '-' + day 30 | return year + '-' + month + '-' + day + ' ' + fm(_date.getHours()) + ':' + fm(_date.getMinutes()) + ':' + fm(_date.getSeconds()) 31 | } 32 | 33 | function fm(value) { 34 | if (value < 10) return '0' + value 35 | return value 36 | } 37 | 38 | exports.sizeFormate = size => { 39 | // https://gist.github.com/thomseddon/3511330 40 | if (!size) return '0 b' 41 | let units = ['b', 'kB', 'MB', 'GB', 'TB'] 42 | let number = Math.floor(Math.log(size) / Math.log(1024)) 43 | return `${(size / Math.pow(1024, Math.floor(number))).toFixed(2)} ${units[number]}` 44 | } 45 | 46 | exports.parseArgv = argv => { 47 | const params = {} 48 | argv.forEach(item => { 49 | const argv = item.split('=') 50 | switch (argv[0]) { 51 | case 'ver': 52 | params.ver = argv[1] 53 | break 54 | case 'draft': 55 | params.isDraft = argv[1] === 'true' || argv[1] === undefined 56 | break 57 | case 'prerelease': 58 | params.isPrerelease = argv[1] === 'true' || argv[1] === undefined 59 | break 60 | case 'target_commitish': 61 | params.target_commitish = argv[1] 62 | break 63 | } 64 | }) 65 | return params 66 | } 67 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | // 脚本配置 2 | 3 | // 匹配的URL 4 | // https://www.tampermonkey.net/documentation.php#_match 5 | exports.matchs = [ 6 | // kw 7 | '*://*.kuwo.cn/*', 8 | 9 | // tx 10 | '*://y.qq.com/*', 11 | 12 | // wy 13 | '*://music.163.com/*', 14 | 15 | // mg 16 | '*://music.migu.cn/*', 17 | 18 | ] 19 | 20 | // 包含的URL 21 | // https://www.tampermonkey.net/documentation.php#_include 22 | exports.includes = [ 23 | 24 | ] 25 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import sources from './sources' 2 | 3 | switch (window.location.hostname) { 4 | case 'www.kuwo.cn': 5 | case 'kuwo.cn': 6 | sources.kw() 7 | break 8 | case 'y.qq.com': 9 | sources.tx() 10 | break 11 | 12 | case 'music.163.com': 13 | sources.wy() 14 | break 15 | 16 | case 'music.migu.cn': 17 | sources.mg() 18 | break 19 | 20 | default: 21 | break 22 | } 23 | -------------------------------------------------------------------------------- /src/sources/index.js: -------------------------------------------------------------------------------- 1 | import kw from './kw' 2 | // import kg from './kg' 3 | import tx from './tx' 4 | import wy from './wy' 5 | import mg from './mg' 6 | 7 | 8 | export default { 9 | kw, 10 | // kg, 11 | tx, 12 | wy, 13 | mg, 14 | } 15 | -------------------------------------------------------------------------------- /src/sources/kg.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyswhut/lx-music-script/f2d8c3b7a3585075481bd97ee687b95eaa999322/src/sources/kg.js -------------------------------------------------------------------------------- /src/sources/kw.js: -------------------------------------------------------------------------------- 1 | import { decodeName, formatPlayTime, requestHook, openApp, request } from '@/utils' 2 | 3 | let data = null 4 | 5 | 6 | export const formatSinger = rawData => rawData.replace(/&/g, '、') 7 | 8 | const filterListDetail = (rawList) => { 9 | // console.log(rawList) 10 | // console.log(rawList.length, rawList2.length) 11 | return rawList.map((item, inedx) => { 12 | let formats = item.formats.split('|') 13 | let types = [] 14 | let _types = {} 15 | if (formats.includes('MP3128')) { 16 | types.push({ type: '128k', size: null }) 17 | _types['128k'] = { 18 | size: null, 19 | } 20 | } 21 | // if (formats.includes('MP3192')) { 22 | // types.push({ type: '192k', size: null }) 23 | // _types['192k'] = { 24 | // size: null, 25 | // } 26 | // } 27 | if (formats.includes('MP3H')) { 28 | types.push({ type: '320k', size: null }) 29 | _types['320k'] = { 30 | size: null, 31 | } 32 | } 33 | // if (formats.includes('AL')) { 34 | // types.push({ type: 'ape', size: null }) 35 | // _types.ape = { 36 | // size: null, 37 | // } 38 | // } 39 | if (formats.includes('ALFLAC')) { 40 | types.push({ type: 'flac', size: null }) 41 | _types.flac = { 42 | size: null, 43 | } 44 | } 45 | if (formats.includes('HIRFLAC')) { 46 | types.push({ type: 'flac24bit', size: null }) 47 | _types.flac24bit = { 48 | size: null, 49 | } 50 | 51 | // 兼容2.0.0版本之前的 hires 音质使用 flac32bit 名字的问题 52 | types.push({ type: 'flac32bit', size: null }) 53 | _types.flac32bit = { 54 | size: null, 55 | } 56 | } 57 | // types.reverse() 58 | return { 59 | singer: formatSinger(decodeName(item.artist)), 60 | name: decodeName(item.songName), 61 | albumName: decodeName(item.album), 62 | albumId: item.albumId, 63 | songmid: item.id, 64 | source: 'kw', 65 | interval: formatPlayTime(parseInt(item.duration)), 66 | img: item.pic, 67 | lrc: null, 68 | otherSource: null, 69 | types, 70 | _types, 71 | typeUrl: {}, 72 | } 73 | }) 74 | } 75 | 76 | const injectStyle = () => { 77 | const style = document.createElement('style') 78 | style.innerHTML = '.btns {white-space: nowrap; flex-wrap: wrap;} .btns button{margin-bottom: 10px;} .btns .play { width: auto !important;} ' 79 | document.head.appendChild(style) 80 | } 81 | 82 | let dom_main 83 | const injectBtn = async(callback) => { 84 | const dom_btn = document.querySelector('.btns button') 85 | if (!dom_btn) { 86 | let mains = document.querySelectorAll('#__layout > .page > .container > *') 87 | if (!mains.length) return 88 | mains = Array.from(mains) 89 | let current_dom_main 90 | for (const dom of mains) { 91 | if (dom.nodeType == 1) { 92 | current_dom_main = dom 93 | break 94 | } 95 | } 96 | if (!current_dom_main) return 97 | dom_main = current_dom_main 98 | current_dom_main.addEventListener('DOMNodeRemoved', () => { 99 | if (dom_main !== current_dom_main) return 100 | dom_main = null 101 | setTimeout(() => { 102 | const dom_btn = document.querySelector('.btns button') 103 | if (!dom_btn) return 104 | if (dom_btn.parentNode.querySelector('.lx-btn')) return 105 | callback(dom_btn) 106 | }) 107 | }) 108 | return 109 | } 110 | if (dom_btn.parentNode.querySelector('.lx-btn')) return 111 | callback(dom_btn) 112 | } 113 | 114 | const createBtn = (label, onClick, dataKeys, className = 'play bg_primary') => { 115 | className += ' lx-btn' 116 | const dom_a = document.createElement('button') 117 | dom_a.className = className 118 | for (const key of dataKeys) dom_a.dataset[key] = '' 119 | dom_a.innerHTML = ` `data-${k}`).join(' ')}>${label}` 120 | dom_a.addEventListener('click', onClick) 121 | return dom_a 122 | } 123 | 124 | const injectPlaylistPage = ({ id }) => { 125 | injectBtn(dom_btn => { 126 | const dataKeys = Object.keys(dom_btn.dataset) 127 | dom_btn.insertAdjacentElement('afterend', createBtn('在 LX Music 中打开', () => { 128 | openApp('songlist', 'open', { 129 | source: 'kw', 130 | id, 131 | }) 132 | }, dataKeys, 'mod_btn')) 133 | dom_btn.insertAdjacentElement('afterend', createBtn('在 LX Music 中播放', () => { 134 | openApp('songlist', 'play', { 135 | source: 'kw', 136 | id, 137 | }) 138 | }, dataKeys)) 139 | }) 140 | } 141 | 142 | const injectSongDetailPage = (musicInfo) => { 143 | console.log(musicInfo) 144 | injectBtn((dom_btn) => { 145 | const dataKeys = Object.keys(dom_btn.dataset) 146 | dom_btn.insertAdjacentElement('afterend', createBtn('在 LX Music 中播放', () => { 147 | openApp('music', 'play', musicInfo) 148 | }, dataKeys)) 149 | }) 150 | } 151 | 152 | const hadnleInject = () => { 153 | if (!data) return 154 | if (window.location.pathname.includes('/playlist_detail/')) { 155 | injectPlaylistPage(data) 156 | } else if (window.location.pathname.includes('/play_detail/')) { 157 | injectSongDetailPage(data) 158 | } 159 | } 160 | 161 | let prevId = '' 162 | const getMusicInfo = async() => { 163 | const id = window.location.pathname.substring(window.location.pathname.lastIndexOf('/') + 1) 164 | if (!id || prevId == id) return 165 | prevId = id 166 | const resp = await request('get', `//kuwo.cn/newh5/singles/songinfoandlrc?musicId=${id}`).catch(_ => null) 167 | if (!resp || resp.status != 200) { 168 | prevId = '' 169 | data = null 170 | return 171 | } 172 | let detail = resp.data.songinfo 173 | data = filterListDetail([detail])[0] 174 | console.log(data) 175 | setTimeout(() => { 176 | hadnleInject() 177 | }) 178 | } 179 | 180 | export default () => { 181 | window.addEventListener('DOMContentLoaded', () => { 182 | injectStyle() 183 | }) 184 | window.addEventListener('load', () => { 185 | if (window.location.pathname.includes('/playlist_detail/')) { 186 | // eslint-disable-next-line no-undef 187 | const detail = __NUXT__.data[0].playListInfo 188 | data = { 189 | play_count: detail.listencnt, 190 | id: detail.id, 191 | author: detail.userName, 192 | name: detail.name, 193 | img: detail.img, 194 | desc: detail.info, 195 | source: 'kw', 196 | } 197 | console.log(data) 198 | hadnleInject() 199 | } else if (window.location.pathname.includes('/play_detail/')) { 200 | getMusicInfo() 201 | } 202 | }) 203 | // window.history.pushState = ((f) => 204 | // function pushState() { 205 | // const ret = f.apply(this, arguments) 206 | // window.dispatchEvent(new window.Event('pushstate')) 207 | // window.dispatchEvent(new window.Event('locationchange')) 208 | // return ret 209 | // })(window.history.pushState) 210 | 211 | // window.history.replaceState = ((f) => 212 | // function replaceState() { 213 | // const ret = f.apply(this, arguments) 214 | // window.dispatchEvent(new window.Event('replacestate')) 215 | // window.dispatchEvent(new window.Event('locationchange')) 216 | // return ret 217 | // })(window.history.replaceState) 218 | 219 | // window.addEventListener('popstate', () => { 220 | // window.dispatchEvent(new window.Event('locationchange')) 221 | // }) 222 | // window.addEventListener('locationchange', function() { 223 | 224 | // }) 225 | requestHook((url, requestBody, response) => { 226 | // if (!requestBody) return 227 | // console.log(url) 228 | if (url.includes('playlist/playListInfo?')) { 229 | if (response.code != 200) { 230 | data = null 231 | return 232 | } 233 | let detail = response.data 234 | data = { 235 | play_count: detail.listencnt, 236 | id: detail.id, 237 | author: detail.userName, 238 | name: detail.name, 239 | img: detail.img, 240 | desc: detail.info, 241 | source: 'kw', 242 | } 243 | console.log(data) 244 | setTimeout(() => { 245 | hadnleInject() 246 | }) 247 | } else if (url.includes('music/musicInfo')) { 248 | getMusicInfo() 249 | // if (response.status != 200) { 250 | // data = null 251 | // return 252 | // } 253 | // let detail = response.data.songinfo 254 | // data = filterListDetail([detail])[0] 255 | // console.log(data) 256 | // setTimeout(() => { 257 | // hadnleInject() 258 | // }) 259 | } 260 | }) 261 | } 262 | -------------------------------------------------------------------------------- /src/sources/mg.js: -------------------------------------------------------------------------------- 1 | import { request, sizeFormate, openApp, requestHook } from '@/utils' 2 | 3 | let data = null 4 | 5 | const getSinger = (singers) => { 6 | let arr = [] 7 | singers.forEach(singer => { 8 | arr.push(singer.name) 9 | }) 10 | return arr.join('、') 11 | } 12 | 13 | const filterListDetail = (rawList) => { 14 | // console.log(rawList) 15 | let ids = new Set() 16 | const list = [] 17 | rawList.forEach((item) => { 18 | if (ids.has(item.copyrightId)) return 19 | ids.add(item.copyrightId) 20 | 21 | const types = [] 22 | const _types = {} 23 | item.newRateFormats && item.newRateFormats.forEach(type => { 24 | let size 25 | switch (type.formatType) { 26 | case 'PQ': 27 | size = sizeFormate(type.size) 28 | types.push({ type: '128k', size }) 29 | _types['128k'] = { 30 | size, 31 | } 32 | break 33 | case 'HQ': 34 | size = sizeFormate(type.size) 35 | types.push({ type: '320k', size }) 36 | _types['320k'] = { 37 | size, 38 | } 39 | break 40 | case 'SQ': 41 | size = sizeFormate(type.size) 42 | types.push({ type: 'flac', size }) 43 | _types.flac = { 44 | size, 45 | } 46 | break 47 | case 'ZQ': 48 | types.push({ type: 'flac24bit', size }) 49 | _types.flac24bit = { 50 | size, 51 | } 52 | 53 | // 兼容2.0.0版本之前的 hires 音质使用 flac32bit 名字的问题 54 | size = sizeFormate(type.size) 55 | types.push({ type: 'flac32bit', size }) 56 | _types.flac32bit = { 57 | size, 58 | } 59 | break 60 | } 61 | }) 62 | 63 | const intervalTest = /(\d\d:\d\d)$/.test(item.length) 64 | 65 | list.push({ 66 | singer: getSinger(item.artists), 67 | name: item.songName, 68 | albumName: item.album, 69 | albumId: item.albumId, 70 | songmid: item.copyrightId, 71 | songId: item.songId, 72 | copyrightId: item.copyrightId, 73 | source: 'mg', 74 | interval: intervalTest ? RegExp.$1 : null, 75 | img: item.albumImgs && item.albumImgs.length ? item.albumImgs[0].img : null, 76 | lrc: null, 77 | lrcUrl: item.lrcUrl, 78 | otherSource: null, 79 | types, 80 | _types, 81 | typeUrl: {}, 82 | }) 83 | }) 84 | return list 85 | } 86 | 87 | 88 | // const injectStyle = () => { 89 | // const style = document.createElement('style') 90 | // style.innerHTML = `.content .actions> * {margin-bottom: .8em;} 91 | // .info_operate {white-space: nowrap;} 92 | // .info_operate .operate_btn.primary {border: 1px solid #e91e63 !important; background-color: #e91e63 !important; cursor: pointer;} 93 | // .info_operate .operate_btn.primary:hover {background-color: #d81558 !important;} 94 | // .info_operate .operate_btn.primary a {color: #fff !important;}` 95 | // document.head.appendChild(style) 96 | // } 97 | 98 | 99 | const createPlaylistBtn = (v, label, onClick, playAll) => { 100 | const dom_div = document.createElement('div') 101 | dom_div.className = 'options-btn ' 102 | dom_div.dataset[v] = '' 103 | if (playAll) { 104 | const img = document.createElement('img') 105 | img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQVSURBVHgB7ZlNaBNbFIDvvTNp2iRNJiF971nzoHmNvF8f8tLne7WbdqkbcdFuVFAo3RQU3IkgVRD/QaEr60IRXLRrEUWhoKioXVi11KgYa2IDxfzUNvOTzL2eaVPwpya5aSapmA9uZjI5Mzn3nnPuOfcOQjVq1KjxzcEYw3D4uJHcseKIiBND+Z6eHkNhi3G/2+0WRFHUrVarHggEMqOjo7ohhlYpOBgMWiTA3fzPCc/af+941m584/FtfCqt2bAXfndDqx8YGKiYRYr+Exh51NXVJcIIO0DpKwjjTctIRXR5bpfAYg/i8Xga7tExxqZag3DI4nA4LLqbgweWV35BxCc0NN5ADf4hqWndX6B8Q2dnp5iLGVPgeTDxeDwOZA/cRQz/UVicRaiunUJy+GIymUzDhSwzelJmi/BYAIFbCMUpb4B9RLCeIY51t13e1vVwwdbW1mZMGmW1RtEd6O7uXpoyef/iT6He+1BaEzwUCoWccMEKzxJQmTrC5UJOp1MSnb+/QyUDbkUzJ4ny+gJYU4bZSodG0QqocAdyMDqiq4lj9rr0ZEdHhzo8PExLjY3qdGABsEZWP8fkF4OpVMoI8kwpQV50DICpUXlnEAhyUTwsNP76yOX9Zb3X67VDnhF4p9wqWuAzqHaeZucHHXXpUHt7u1asW3F1wAUIjb/FkWksBjmG3JFIJBZyBypQV3HlATMz6iLgVqTuLLYFhiTJ3wxZvKBLccVAxcCkm9ibLo+Pj9vAjfJ2YPXEwDJQKm9Nvh2/CqeZr8lwuZDZleUXMMvf8CnkE+GqRlGFIQThlpaWvDI8KzJmfhB/CtXVW+FoOJtPpoqJrACMDidjTx7CWd5aiSuIoUke33/mBzFjd+j87PZkcjKKCuQCriCGPGayBYz6SN0dj97f0tpqjxrVKiqQyHgsIMA06jKvlMgO6fNzR1OpZzFYL2gjIyN6MbetgjzA7sF8f9ySid2YmZlR4ALXtkz18gBms5RlD9K50GavTb7a399fVO3zxWM4ZMtWzDFEr6lqYr/HqkwalSe4C0UlboZVuAPGTkV6D1Hf3vT7/fLY2Bj3iH8OTyJbURKjNHtaSb8b/ElCsfB0XIM18YrWwkvwxEBpI8XYXV1JtVkyUwP/b2iJwOaYhgokJx64gpgQQkGhSFHCGC0EKUs/3+z7UXgMtb0M25KGy5RNeQOBRxjWrVhjtiaMyaa8gpSen5+b3umyzF7PKa5PTEyUVfEluFwoEolkWPrlETh98hUZWBKq24j2ap+aejPV19en5BKSaRmcywIAUxSFyrPRS1bHD9NgiZ9BNQ3cZYpRelF//2xHk2SZ6O3tVYxRh7Yq3xNgY2sQ6vR6cKlG+O7KHa1ocUCq8qamFPAyr5pq1KhR4zvjA87Zy1ltUhN/AAAAAElFTkSuQmCC' 106 | img.dataset[v] = '' 107 | dom_div.appendChild(img) 108 | } 109 | const span = document.createElement('span') 110 | span.className = 'active' 111 | span.textContent = label 112 | span.dataset[v] = '' 113 | dom_div.appendChild(span) 114 | dom_div.addEventListener('click', onClick, { capture: true }) 115 | return dom_div 116 | } 117 | function observeRemoval(targetElement, callback) { 118 | const parent = targetElement.parentNode 119 | 120 | if (!parent) { 121 | console.warn('元素已经不在 DOM 中') 122 | return 123 | } 124 | 125 | const observer = new window.MutationObserver((mutationsList) => { 126 | for (const mutation of mutationsList) { 127 | for (const removedNode of mutation.removedNodes) { 128 | if (removedNode === targetElement) { 129 | callback && callback(targetElement) 130 | observer.disconnect() // 停止监听 131 | return 132 | } 133 | } 134 | } 135 | }) 136 | 137 | observer.observe(parent, { childList: true }) 138 | } 139 | const injectPlaylistPage = ({ id }) => { 140 | const dom_btn = document.querySelector('.song-handle .options')?.childNodes?.[0] 141 | if (!dom_btn) return 142 | const handleRemove = () => { 143 | document.querySelector('.song-handle .options')?.removeEventListener('DOMNodeRemoved', handleRemove) 144 | setTimeout(() => { 145 | injectPlaylistPage({ id }) 146 | }, 100) 147 | } 148 | document.querySelector('.song-handle .options').addEventListener('DOMNodeRemoved', handleRemove) 149 | 150 | const v = Object.keys(dom_btn.dataset).find(key => key.startsWith('v-')) 151 | dom_btn.insertAdjacentElement('afterend', createPlaylistBtn(v, '在 LX Music 中打开', (evt) => { 152 | evt.stopImmediatePropagation() 153 | evt.stopPropagation() 154 | openApp('songlist', 'open', { 155 | source: 'mg', 156 | id, 157 | }) 158 | })) 159 | dom_btn.insertAdjacentElement('afterend', createPlaylistBtn(v, '在 LX Music 中播放', (evt) => { 160 | evt.stopImmediatePropagation() 161 | evt.stopPropagation() 162 | openApp('songlist', 'play', { 163 | source: 'mg', 164 | id, 165 | }) 166 | }, true)) 167 | } 168 | 169 | const createSongDetailBtn = (label, onClick, className = 'primary') => { 170 | const dom_a = document.createElement('div') 171 | dom_a.className = 'operate_btn ' + className 172 | dom_a.innerHTML = `${label}` 173 | dom_a.addEventListener('click', onClick) 174 | return dom_a 175 | } 176 | const injectSongDetailPage = (musicInfo) => { 177 | console.log(musicInfo) 178 | const dom_btn = document.querySelector('.info_operate .operate_btn') 179 | if (!dom_btn) return 180 | dom_btn.insertAdjacentElement('afterend', createSongDetailBtn('在 LX Music 中播放', () => { 181 | openApp('music', 'play', musicInfo) 182 | })) 183 | } 184 | 185 | const hadnleInject = () => { 186 | if (!data) return 187 | if (window.location.hash.includes('/playlist')) { 188 | injectPlaylistPage(data) 189 | } else if (window.location.hash.includes('/music/song/')) { 190 | // injectSongDetailPage(data) 191 | } 192 | } 193 | 194 | 195 | export default () => { 196 | // window.addEventListener('DOMContentLoaded', () => { 197 | // injectStyle() 198 | 199 | // if (window.location.hash.includes('/playlist')) { 200 | // const dom_songcid = document.getElementById('J_ResId') 201 | // if (!dom_songcid || !dom_songcid.value) return 202 | // // eslint-disable-next-line no-undef 203 | // // const detail = __INITIAL_DATA__.detail 204 | // data = { 205 | // id: dom_songcid.value, 206 | // source: 'mg', 207 | // } 208 | // hadnleInject() 209 | // } else if (window.location.pathname.includes('/music/song/')) { 210 | // // eslint-disable-next-line no-undef 211 | // const dom_songcid = document.getElementById('songcid') 212 | // if (!dom_songcid || !dom_songcid.value) return 213 | // request('get', `https://c.musicapp.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?copyrightId=${dom_songcid.value}&resourceType=2`).then(response => { 214 | // if (response.code !== '000000') return 215 | // console.log(response) 216 | // const detail = response.resource[0] 217 | 218 | // data = filterListDetail([detail])[0] 219 | 220 | // console.log(data) 221 | // hadnleInject() 222 | // }) 223 | // } 224 | // }) 225 | let preData = null 226 | requestHook((url, requestBody, response) => { 227 | if (url.includes('/resource/playlist/v2.0')) { 228 | if (response.code !== '000000') return 229 | data = { 230 | id: response.data.musicListId, 231 | source: 'mg', 232 | } 233 | if (preData == data.id) return 234 | preData = data.id 235 | hadnleInject() 236 | } 237 | }) 238 | } 239 | -------------------------------------------------------------------------------- /src/sources/tx.js: -------------------------------------------------------------------------------- 1 | import { sizeFormate, formatPlayTime, openApp } from '@/utils' 2 | 3 | let data = null 4 | 5 | const getSinger = (singers) => { 6 | let arr = [] 7 | singers.forEach((singer) => { 8 | arr.push(singer.name) 9 | }) 10 | return arr.join('、') 11 | } 12 | 13 | const filterListDetail = (rawList) => { 14 | // console.log(rawList) 15 | return rawList.map((item) => { 16 | let types = [] 17 | let _types = {} 18 | if (item.file.size_128mp3 !== 0) { 19 | let size = sizeFormate(item.file.size_128mp3) 20 | types.push({ type: '128k', size }) 21 | _types['128k'] = { 22 | size, 23 | } 24 | } 25 | if (item.file.size_320mp3 !== 0) { 26 | let size = sizeFormate(item.file.size_320mp3) 27 | types.push({ type: '320k', size }) 28 | _types['320k'] = { 29 | size, 30 | } 31 | } 32 | if (item.file.size_flac !== 0) { 33 | let size = sizeFormate(item.file.size_flac) 34 | types.push({ type: 'flac', size }) 35 | _types.flac = { 36 | size, 37 | } 38 | } 39 | if (item.file.size_hires !== 0) { 40 | let size = sizeFormate(item.file.size_hires) 41 | types.push({ type: 'flac24bit', size }) 42 | _types.flac24bit = { 43 | size, 44 | } 45 | 46 | // 兼容2.0.0版本之前的 hires 音质使用 flac32bit 名字的问题 47 | types.push({ type: 'flac32bit', size }) 48 | _types.flac32bit = { 49 | size, 50 | } 51 | } 52 | // types.reverse() 53 | return { 54 | singer: getSinger(item.singer), 55 | name: item.title, 56 | albumName: item.album.title, 57 | albumId: item.album.id, 58 | source: 'tx', 59 | interval: formatPlayTime(item.interval), 60 | songId: item.id, 61 | albumMid: item.album.mid, 62 | strMediaMid: item.file.media_mid, 63 | songmid: item.mid, 64 | img: 65 | item.album.name === '' || item.album.name === '空' 66 | ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` 67 | : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${item.album.mid}.jpg`, 68 | lrc: null, 69 | otherSource: null, 70 | types, 71 | _types, 72 | typeUrl: {}, 73 | } 74 | }) 75 | } 76 | 77 | const injectStyle = () => { 78 | const style = document.createElement('style') 79 | style.innerHTML = ` .data__cont{position: relative;} 80 | .data__info { overflow: hidden; } 81 | .singer_exclusive .mod_data_statistic { height: auto !important; } /** 修复歌手页排版问题 **/ 82 | .data__actions {position: relative; bottom: initial !important; white-space: nowrap; display: flex; flex-wrap: wrap; margin-top: 6px;} 83 | .mod_btn, .mod_btn_green{ margin-bottom: 6px; } ` 84 | document.head.appendChild(style) 85 | } 86 | 87 | const injectBtn = async(callback) => { 88 | const dom_btn = document.querySelector('.data__actions a') 89 | if (!dom_btn) { 90 | let dom_loading = document.querySelector('.mod_loading') 91 | console.log(dom_loading) 92 | if (!dom_loading) return 93 | let observer_app 94 | let observer_wrap 95 | const handleChange = (list) => { 96 | // console.log(list) 97 | observer_app?.disconnect() 98 | observer_wrap?.disconnect() 99 | 100 | // if (dom_loading !== current_dom_loading) return 101 | // dom_loading = null 102 | setTimeout(() => { 103 | const dom_btn = document.querySelector('.data__actions a') 104 | if (!dom_btn) return 105 | if (dom_btn.parentNode.querySelector('.lx-btn')) return 106 | callback(dom_btn) 107 | }) 108 | } 109 | let dom_app = document.querySelector('#app') 110 | // console.log(dom_app) 111 | if (dom_app) { 112 | observer_app = new window.MutationObserver(handleChange) 113 | observer_app.observe(dom_app, { 114 | attributes: false, 115 | childList: true, 116 | subtree: false, 117 | }) 118 | } 119 | let dom_wrap = document.querySelector('#app>.wrap') 120 | // console.log(dom_wrap) 121 | if (dom_wrap) { 122 | observer_wrap = new window.MutationObserver(handleChange) 123 | observer_wrap.observe(dom_wrap, { 124 | attributes: false, 125 | childList: true, 126 | subtree: false, 127 | }) 128 | } 129 | // current_dom_loading.addEventListener('DOMNodeRemoved', () => { 130 | // console.log(dom_loading !== current_dom_loading) 131 | // if (dom_loading !== current_dom_loading) return 132 | // dom_loading = null 133 | // setTimeout(() => { 134 | // const dom_btn = document.querySelector('.data__actions a') 135 | // if (!dom_btn) return 136 | // callback(dom_btn) 137 | // }) 138 | // }) 139 | return 140 | } 141 | if (dom_btn.parentNode.querySelector('.lx-btn')) return 142 | callback(dom_btn) 143 | } 144 | 145 | const createBtn = (label, onClick, className = 'mod_btn_green') => { 146 | className += ' lx-btn' 147 | const dom_a = document.createElement('a') 148 | dom_a.className = className 149 | dom_a.innerHTML = `${label}` 150 | dom_a.addEventListener('click', onClick) 151 | return dom_a 152 | } 153 | 154 | const injectPlaylistPage = ({ id }) => { 155 | injectBtn(dom_btn => { 156 | dom_btn.insertAdjacentElement('afterend', createBtn('在 LX Music 中打开', () => { 157 | openApp('songlist', 'open', { 158 | source: 'tx', 159 | id, 160 | }) 161 | }, 'mod_btn')) 162 | dom_btn.insertAdjacentElement('afterend', createBtn('在 LX Music 中播放', () => { 163 | openApp('songlist', 'play', { 164 | source: 'tx', 165 | id, 166 | }) 167 | })) 168 | }) 169 | } 170 | 171 | const injectSongDetailPage = (musicInfo) => { 172 | console.log(musicInfo) 173 | injectBtn((dom_btn) => { 174 | dom_btn.insertAdjacentElement('afterend', createBtn('在 LX Music 中播放', () => { 175 | openApp('music', 'play', musicInfo) 176 | })) 177 | }) 178 | } 179 | 180 | const hadnleInject = () => { 181 | if (!data) return 182 | if (window.location.pathname.includes('/playlist/')) { 183 | injectPlaylistPage(data) 184 | } else if (window.location.pathname.includes('/songDetail/')) { 185 | injectSongDetailPage(data) 186 | } 187 | } 188 | 189 | export default () => { 190 | injectStyle() 191 | const requestHook = (requestBody, response) => { 192 | if (!requestBody) return 193 | if ( 194 | requestBody.includes('"module":"music.srfDissInfo.aiDissInfo"') && 195 | requestBody.includes('"method":"uniform_get_Dissinfo"') 196 | ) { 197 | if (response.code != 0) { 198 | data = null 199 | return 200 | } 201 | let detail = response.data.dirinfo 202 | data = { 203 | play_count: detail.listennum, 204 | id: detail.id, 205 | author: detail.host_nick, 206 | name: detail.title, 207 | img: detail.picurl, 208 | desc: detail.desc, 209 | source: 'tx', 210 | } 211 | setTimeout(() => { 212 | hadnleInject() 213 | }) 214 | } else if ( 215 | requestBody.includes('"module":"music.pf_song_detail_svr"') && 216 | requestBody.includes('"method":"get_song_detail_yqq"') 217 | ) { 218 | if (response.code != 0) { 219 | data = null 220 | return 221 | } 222 | data = filterListDetail([response.data.track_info])[0] 223 | setTimeout(() => { 224 | hadnleInject() 225 | }) 226 | } 227 | } 228 | let hooked = false 229 | 230 | const hookRequestMod = async() => { 231 | const getRequestModId = () => { 232 | if (typeof window.webpackJsonp === 'undefined') { 233 | throw new Error('window.webpackJsonp 为空,请到 github 提交 issue 反馈!') 234 | } 235 | const jsonp = window.webpackJsonp 236 | if (!Array.isArray(jsonp)) { 237 | throw new Error('window.webpackJsonp 不是有效的模块数组,请到 github 提交 issue 反馈!') 238 | } 239 | 240 | for (const item of jsonp) { 241 | if (!Array.isArray(item) || item.length < 2) continue 242 | 243 | const modules = item[1] // 模块定义对象 244 | 245 | if (typeof modules === 'object') { 246 | for (const [id, fn] of Object.entries(modules)) { 247 | if (typeof fn === 'function') { 248 | if (fn.toString().includes('this.sendRequest')) { 249 | return id 250 | } 251 | } 252 | } 253 | } 254 | } 255 | } 256 | const get__webpack_require__ = async() => { 257 | return new Promise((resolve) => { 258 | window.webpackJsonp.push([ 259 | [9999999], 260 | { 261 | fake_mod: function(module, exports, __req__) { 262 | resolve(__req__) 263 | }, 264 | }, 265 | [['fake_mod']], 266 | ]) 267 | }) 268 | } 269 | const hookReq = (mod) => { 270 | let oldReq = mod.request.bind(mod) 271 | mod.request = (opts) => { 272 | // console.log('opts', opts) 273 | return oldReq(opts).then((response) => { 274 | // console.log('response', response) 275 | if (!Array.isArray(opts)) { 276 | requestHook(JSON.stringify(opts), response) 277 | return response 278 | } 279 | opts.forEach((req, idx) => { 280 | requestHook(JSON.stringify(req), response[idx]) 281 | }) 282 | return response 283 | }) 284 | } 285 | } 286 | 287 | const modId = getRequestModId() 288 | if (!modId) return 289 | hooked = true 290 | const __webpack_require__ = await get__webpack_require__() 291 | const mod = __webpack_require__(modId) 292 | // Object(mod.j) 293 | for (const [k, fn] of Object.entries(mod)) { 294 | if (typeof fn == 'function' && fn.toString().startsWith('function(){return ')) { 295 | return hookReq(Object(mod[k])()) 296 | } 297 | } 298 | } 299 | const hookWebpackJsonp = () => { 300 | let val = window.webpackJsonp 301 | Object.defineProperty(window, 'webpackJsonp', { 302 | get() { 303 | return val 304 | }, 305 | set(value) { 306 | val = value 307 | if (hooked) return 308 | hookRequestMod() 309 | }, 310 | }) 311 | } 312 | hookWebpackJsonp() 313 | } 314 | -------------------------------------------------------------------------------- /src/sources/wy.js: -------------------------------------------------------------------------------- 1 | import forge from 'node-forge' 2 | import { sizeFormate, formatPlayTime, request, openApp } from '@/utils' 3 | 4 | let data = null 5 | 6 | const idRxp = /id=(\d+)/ 7 | 8 | let dom_iframe 9 | 10 | const getSinger = (singers) => { 11 | let arr = [] 12 | singers.forEach(singer => { 13 | arr.push(singer.name) 14 | }) 15 | return arr.join('、') 16 | } 17 | 18 | const filterList = ({ songs, privileges }) => { 19 | // console.log(songs, privileges) 20 | const list = [] 21 | songs.forEach((item, index) => { 22 | const types = [] 23 | const _types = {} 24 | let size 25 | let privilege = privileges[index] 26 | if (privilege.id !== item.id) privilege = privileges.find(p => p.id === item.id) 27 | if (!privilege) return 28 | 29 | switch (privilege.maxbr) { 30 | case 999000: 31 | size = null 32 | types.push({ type: 'flac', size }) 33 | _types.flac = { 34 | size, 35 | } 36 | case 320000: 37 | if (item.h) { 38 | size = sizeFormate(item.h.size) 39 | types.push({ type: '320k', size }) 40 | _types['320k'] = { 41 | size, 42 | } 43 | } 44 | case 192000: 45 | case 128000: 46 | if (item.l) { 47 | size = sizeFormate(item.l.size) 48 | types.push({ type: '128k', size }) 49 | _types['128k'] = { 50 | size, 51 | } 52 | } 53 | } 54 | 55 | types.reverse() 56 | 57 | list.push({ 58 | singer: getSinger(item.ar), 59 | name: item.name, 60 | albumName: item.al.name, 61 | albumId: item.al.id, 62 | source: 'wy', 63 | interval: formatPlayTime(item.dt / 1000), 64 | songmid: item.id, 65 | img: item.al.picUrl, 66 | lrc: null, 67 | otherSource: null, 68 | types, 69 | _types, 70 | typeUrl: {}, 71 | }) 72 | }) 73 | return list 74 | } 75 | 76 | const injectStyle = () => { 77 | const style = dom_iframe.contentWindow.document.createElement('style') 78 | style.innerHTML = '.btns{display: flex; flex-flow: row wrap;} .btns > a {margin-bottom: 6px;} .margin-right { margin-right: 5px; }' 79 | dom_iframe.contentWindow.document.head.appendChild(style) 80 | } 81 | 82 | const injectBtn = () => { 83 | let dom_btn = dom_iframe.contentWindow.document.querySelector('.btns .u-btni-add') 84 | if (!dom_btn) dom_btn = dom_iframe.contentWindow.document.querySelector('.btns .u-vip-btn-group') 85 | if (!dom_btn) dom_btn = dom_iframe.contentWindow.document.querySelector('.btns .u-btni-openvipply') 86 | if (!dom_btn) dom_btn = dom_iframe.contentWindow.document.querySelector('.btns .u-btni-play') 87 | return dom_btn 88 | } 89 | 90 | const createBtn = (label, onClick, className = 'u-btn2 u-btn2-2 u-btni-addply f-fl margin-right') => { 91 | const dom_a = dom_iframe.contentWindow.document.createElement('a') 92 | dom_a.className = className 93 | dom_a.innerHTML = `${className.includes('u-btn2-2') ? '' : ''}${label}` 94 | dom_a.addEventListener('click', onClick) 95 | return dom_a 96 | } 97 | 98 | const injectPlaylistPage = ({ id }) => { 99 | const dom_btn = injectBtn() 100 | if (!dom_btn) return 101 | 102 | dom_btn.insertAdjacentElement('afterend', createBtn('在 LX Music 中打开', () => { 103 | openApp('songlist', 'open', { 104 | source: 'wy', 105 | id, 106 | }) 107 | }, 'u-btni u-btni-share')) 108 | dom_btn.insertAdjacentElement('afterend', createBtn('在 LX Music 中播放', () => { 109 | openApp('songlist', 'play', { 110 | source: 'wy', 111 | id, 112 | }) 113 | })) 114 | } 115 | 116 | const injectSongDetailPage = (musicInfo) => { 117 | console.log(musicInfo) 118 | const dom_btn = injectBtn() 119 | if (!dom_btn) return 120 | dom_btn.insertAdjacentElement('afterend', createBtn('在 LX Music 中播放', () => { 121 | openApp('music', 'play', musicInfo) 122 | })) 123 | } 124 | 125 | const hadnleInject = () => { 126 | if (!data) return 127 | if (dom_iframe.contentWindow.location.href.includes('/playlist?')) { 128 | injectPlaylistPage(data) 129 | } else if (dom_iframe.contentWindow.location.href.includes('/song?')) { 130 | injectSongDetailPage(data) 131 | } 132 | } 133 | 134 | // https://github.com/listen1/listen1_chrome_extension/blob/master/js/provider/netease.js 135 | const create_secret_key = (size) => { 136 | const result = [] 137 | const choice = '012345679abcdef'.split('') 138 | for (let i = 0; i < size; i += 1) { 139 | const index = Math.floor(Math.random() * choice.length) 140 | result.push(choice[index]) 141 | } 142 | return result.join('') 143 | } 144 | const aes_encrypt = (text, sec_key, algo) => { 145 | const cipher = forge.cipher.createCipher(algo, sec_key) 146 | cipher.start({ iv: '0102030405060708' }) 147 | cipher.update(forge.util.createBuffer(text)) 148 | cipher.finish() 149 | return cipher.output 150 | } 151 | const rsa_encrypt = (text, pubKey, modulus) => { 152 | text = text.split('').reverse().join('') // eslint-disable-line no-param-reassign 153 | const n = new forge.jsbn.BigInteger(modulus, 16) 154 | const e = new forge.jsbn.BigInteger(pubKey, 16) 155 | const b = new forge.jsbn.BigInteger(forge.util.bytesToHex(text), 16) 156 | const enc = b.modPow(e, n).toString(16).padStart(256, '0') 157 | return enc 158 | } 159 | const weapi = (text) => { 160 | const modulus = 161 | '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b72' + 162 | '5152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbd' + 163 | 'a92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe48' + 164 | '75d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' 165 | const nonce = '0CoJUm6Qyw8W8jud' 166 | const pubKey = '010001' 167 | text = JSON.stringify(text) // eslint-disable-line no-param-reassign 168 | const sec_key = create_secret_key(16) 169 | const enc_text = btoa( 170 | aes_encrypt( 171 | btoa(aes_encrypt(text, nonce, 'AES-CBC').data), 172 | sec_key, 173 | 'AES-CBC', 174 | ).data, 175 | ) 176 | const enc_sec_key = rsa_encrypt(sec_key, pubKey, modulus) 177 | const data = { 178 | params: enc_text, 179 | encSecKey: enc_sec_key, 180 | } 181 | 182 | return data 183 | } 184 | 185 | 186 | const wyWeapiRequest = (method, url, data) => { 187 | const json = weapi(data) 188 | return request(method, url, `params=${encodeURIComponent(json.params)}&encSecKey=${encodeURIComponent(json.encSecKey)}`, { 189 | 'Content-Type': 'application/x-www-form-urlencoded', 190 | }) 191 | } 192 | 193 | export default () => { 194 | window.addEventListener('DOMContentLoaded', () => { 195 | dom_iframe = document.getElementById('g_iframe') 196 | if (!dom_iframe) return 197 | dom_iframe.addEventListener('load', () => { 198 | injectStyle() 199 | 200 | if (dom_iframe.contentWindow.location.href.includes('/playlist?')) { 201 | if (!idRxp.test(dom_iframe.contentWindow.location.href)) return 202 | const id = RegExp.$1 203 | data = { 204 | id, 205 | source: 'wy', 206 | } 207 | hadnleInject() 208 | } else if (dom_iframe.contentWindow.location.href.includes('/song?')) { 209 | if (!idRxp.test(dom_iframe.contentWindow.location.href)) return 210 | const id = RegExp.$1 211 | wyWeapiRequest('POST', 'https://music.163.com/weapi/v3/song/detail', { 212 | c: '[{"id":' + id + '}]', 213 | }).then((res) => { 214 | if (res.code != 200) return 215 | data = filterList(res)[0] 216 | hadnleInject() 217 | }) 218 | } 219 | }) 220 | }) 221 | } 222 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const requestHook = (callback) => { 2 | let oldXHROpen = window.XMLHttpRequest.prototype.open 3 | window.XMLHttpRequest.prototype.open = function(method, url) { 4 | // do something with the method, url and etc. 5 | this._url = url 6 | // this.addEventListener('load', function () { 7 | // // do something with the response text 8 | // console.log('load: ' + url) 9 | // console.log(JSON.parse(this.responseText)) 10 | // try { 11 | // callback(url, JSON.parse(this.responseText)) 12 | // } catch (_) {} 13 | // }) 14 | return oldXHROpen.apply(this, arguments) 15 | } 16 | let oldXHRSend = window.XMLHttpRequest.prototype.send 17 | window.XMLHttpRequest.prototype.send = function(data) { 18 | this.addEventListener('load', function() { 19 | // do something with the response text 20 | // console.log('load: ' + data) 21 | // console.log(JSON.parse(this.responseText)) 22 | try { 23 | callback(this._url, data, JSON.parse(this.responseText)) 24 | } catch (_) {} 25 | }) 26 | 27 | oldXHRSend.call(this, data) 28 | } 29 | } 30 | 31 | export const encodeData = (data) => encodeURIComponent(JSON.stringify(data)) 32 | 33 | export const sizeFormate = (size) => { 34 | // https://gist.github.com/thomseddon/3511330 35 | if (!size) return '0 B' 36 | let units = ['B', 'KB', 'MB', 'GB', 'TB'] 37 | let number = Math.floor(Math.log(size) / Math.log(1024)) 38 | return `${(size / Math.pow(1024, Math.floor(number))).toFixed(2)} ${ 39 | units[number] 40 | }` 41 | } 42 | 43 | export const formatPlayTime = (time) => { 44 | let m = parseInt(time / 60) 45 | let s = parseInt(time % 60) 46 | return m === 0 && s === 0 47 | ? '--/--' 48 | : (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s) 49 | } 50 | 51 | const encodeNames = { 52 | '&': '&', 53 | '<': '<', 54 | '>': '>', 55 | '"': '"', 56 | ''': "'", 57 | ''': "'", 58 | } 59 | export const decodeName = (str = '') => str?.replace(/(?:&|<|>|"|'|')/gm, s => encodeNames[s]) || '' 60 | 61 | export const openApp = (type, action, data) => { 62 | const dom_a = document.createElement('a') 63 | dom_a.href = `lxmusic://${type}/${action}?data=${encodeData(data)}` 64 | dom_a.click() 65 | } 66 | 67 | export const request = (method, url, data, headers) => { 68 | const xhr = new window.XMLHttpRequest() 69 | xhr.open(method, url) 70 | if (headers) { 71 | for (const [key, value] of Object.entries(headers)) { 72 | xhr.setRequestHeader(key, value) 73 | } 74 | } 75 | xhr.addEventListener('load', function() { 76 | let response 77 | try { 78 | response = JSON.parse(this.responseText) 79 | } catch (err) { 80 | _resolve(this.responseText) 81 | } 82 | _resolve(response) 83 | }) 84 | xhr.addEventListener('error', function(err) { 85 | _reject(err) 86 | }) 87 | let _resolve 88 | let _reject 89 | if (method && method.toUpperCase() === 'POST') { 90 | xhr.send(data) 91 | } else { 92 | xhr.send() 93 | } 94 | return new Promise((resolve, reject) => { 95 | _resolve = resolve 96 | _reject = reject 97 | }) 98 | } 99 | 100 | export const wait = time => new Promise(resolve => setTimeout(resolve, time)) 101 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const packageJson = require('./package.json') 4 | const config = require('./src/config') 5 | 6 | const generateMaths = () => { 7 | const matchUrlComment = config.matchs.map(url => `// @match ${url}`).join('\n') 8 | const includeUrlComment = config.includes.map(url => `// @include ${url}`).join('\n') 9 | let comment = '' 10 | if (matchUrlComment) comment += ('\n' + matchUrlComment) 11 | if (includeUrlComment) comment += ('\n' + includeUrlComment) 12 | return comment 13 | } 14 | 15 | module.exports = { 16 | mode: 'production', 17 | target: 'web', 18 | entry: path.join(__dirname, './src/index.js'), 19 | output: { 20 | filename: 'lx-music-script.js', 21 | path: path.join(__dirname, './dist'), 22 | }, 23 | resolve: { 24 | alias: { 25 | '@': path.join(__dirname, './src'), 26 | }, 27 | extensions: ['*', '.js', '.json', '.node'], 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.js$/, 33 | loader: 'babel-loader', 34 | exclude: /node_modules/, 35 | }, 36 | ], 37 | }, 38 | plugins: [ 39 | new webpack.BannerPlugin({ 40 | banner: `// ==UserScript== 41 | // @name LX Music 辅助脚本 42 | // @namespace ${packageJson.name} 43 | // @version ${packageJson.version} 44 | // @author ${packageJson.author} 45 | // @description ${packageJson.description} 46 | // @homepage ${packageJson.homepage} 47 | // @supportURL ${packageJson.bugs.url}${generateMaths()} 48 | // @run-at document-start 49 | // @noframes 50 | // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NDcuOTQyIiBoZWlnaHQ9IjQ0Ny45NDMiPjxwYXRoIGZpbGw9IiM1ZWQ2OTgiIGQ9Ik0yMDMuODA2LjQ4MmMtMTkuNjY4LTMuMzQ2LTM1Ljc2IDExLjEzOS0zNS43NiAzMS4wODZ2MjA2LjE2NmMtMTEuNjQyLTQuMjcxLTI0LjE2NS02LjcyNS0zNy4yODEtNi43MjUtNTkuOTA1IDAtMTA4LjQ2OSA0OC41NjYtMTA4LjQ2OSAxMDguNDczIDAgNTkuOTAzIDQ4LjU2NCAxMDguNDYxIDEwOC40NjkgMTA4LjQ2MSAzNC4xNDEgMCA2NC41NC0xNS44MiA4NC40MDYtNDAuNDgybC00OS42NTgtNDkuNjY0Yy0xNS4xMTYtMTUuMTEyLTExLjcwOC0yOC45MDEtOS41NDItMzQuMTQgMi4xNjYtNS4yMzMgOS41MTQtMTcuNCAzMC44ODMtMTcuNGgxOC4wODJ2LTU2Ljg4NWMwLTIxLjEzMiAxNC42MTctMzguODYyIDM0LjI2Ni00My43NDUuMDMyLTQ0LjM3My4wMzItODEuODA4LjAzMi04MS44MDggMTQwLjE0NyAwIDEzMS43MjQgODMuOTc0IDExNS4zMjUgMTMyLjE5Ni02LjQyIDE4Ljg4NC0yLjYwMSAyMi4wNSAxMC44OTMgNy4zNTRDNTM2LjQ3MyA3Ny4xMDYgMjk4LjM4IDE2LjU2NiAyMDMuODA2LjQ4MnoiLz48cGF0aCBmaWxsPSIjNGRhZjdjIiBkPSJNMzAxLjA2MSAyMjMuODc2aC01MC45OTRjLTMuOTExIDAtNy41NzQuOTUtMTAuODg5IDIuNTIzLTguNjE2IDQuMDktMTQuNjE1IDEyLjc5OC0xNC42MTUgMjIuOTczdjc2LjUxaC0zNy43MDhjLTE0LjA4MiAwLTE3LjQyOCA4LjA3MS03LjQ2NiAxOC4wMjlsNDYuODkzIDQ2Ljg5OCAzMS4yNSAzMS4yNDZhMjUuNDI0IDI1LjQyNCAwIDAwMTguMDMzIDcuNDc0YzYuNTIzIDAgMTMuMDUyLTIuNDg0IDE4LjAyOS03LjQ3NGw3OC4xNTItNzguMTQ1YzkuOTUxLTkuOTU4IDYuNjA4LTE4LjAyOS03LjQ3LTE4LjAyOWgtMzcuNzF2LTc2LjUxYy4wMDEtMTQuMDc4LTExLjQyLTI1LjQ5NS0yNS41MDUtMjUuNDk1eiIvPjwvc3ZnPg== 51 | // @grant none 52 | // ==/UserScript==`, 53 | // @grant GM_cookie 54 | raw: true, 55 | }), 56 | new webpack.DefinePlugin({ 57 | mode: JSON.stringify(process.env.NODE_ENV), 58 | }), 59 | ], 60 | optimization: { 61 | minimize: false, 62 | }, 63 | } 64 | --------------------------------------------------------------------------------