mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
6 files changed
+493
-319
No files matched your search
Generated
+59
-286
@@ -12,7 +12,7 @@
|
|||||||
"@discordjs/opus": "^0.10.0",
|
"@discordjs/opus": "^0.10.0",
|
||||||
"@honeybbq/teamspeak-client": "^0.2.1",
|
"@honeybbq/teamspeak-client": "^0.2.1",
|
||||||
"@koa/router": "^15.4.0",
|
"@koa/router": "^15.4.0",
|
||||||
"@sansenjian/qq-music-api": "^2.2.9",
|
"@sansenjian/qq-music-api": "file:../qq-music-api",
|
||||||
"axios": "^1.14.0",
|
"axios": "^1.14.0",
|
||||||
"better-sqlite3": "^12.8.0",
|
"better-sqlite3": "^12.8.0",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
@@ -38,6 +38,62 @@
|
|||||||
"vitest": "^4.1.2"
|
"vitest": "^4.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../qq-music-api": {
|
||||||
|
"name": "@sansenjian/qq-music-api",
|
||||||
|
"version": "2.2.11",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@koa/router": "^15.3.1",
|
||||||
|
"axios": "^1.13.6",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"is-generator-function": "1.0.10",
|
||||||
|
"koa": "^2.16.1",
|
||||||
|
"koa-bodyparser": "^4.4.1",
|
||||||
|
"koa-static": "^5.0.0",
|
||||||
|
"reflect-metadata": "^0.2.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"qq-music-api": "dist/app.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^18.0.0",
|
||||||
|
"@commitlint/config-conventional": "^18.0.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/koa": "^3.0.1",
|
||||||
|
"@types/koa__router": "^12.0.5",
|
||||||
|
"@types/koa-bodyparser": "^4.3.13",
|
||||||
|
"@types/koa-static": "^4.0.4",
|
||||||
|
"@types/node": "^25.3.3",
|
||||||
|
"@types/supertest": "^7.2.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
|
"chalk": "^4.1.0",
|
||||||
|
"conventional-changelog-cli": "^4.0.0",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-standard": "^17.0.0",
|
||||||
|
"eslint-config-standard-with-typescript": "^43.0.1",
|
||||||
|
"eslint-plugin-import": "^2.29.0",
|
||||||
|
"eslint-plugin-n": "^16.0.0",
|
||||||
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
|
"husky": "^9.0.0",
|
||||||
|
"jest": "^30.2.0",
|
||||||
|
"lint-staged": "^15.0.0",
|
||||||
|
"nodemon": "^3.1.14",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"rimraf": "^6.1.3",
|
||||||
|
"sinon": "^21.0.1",
|
||||||
|
"supertest": "^7.2.2",
|
||||||
|
"ts-jest": "^29.4.6",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "5.7.3",
|
||||||
|
"vitepress": "^1.6.4",
|
||||||
|
"vue": "^3.5.29"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "22.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@derhuerst/http-basic": {
|
"node_modules/@derhuerst/http-basic": {
|
||||||
"version": "8.2.4",
|
"version": "8.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz",
|
||||||
@@ -939,189 +995,8 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@sansenjian/qq-music-api": {
|
"node_modules/@sansenjian/qq-music-api": {
|
||||||
"version": "2.2.9",
|
"resolved": "../qq-music-api",
|
||||||
"resolved": "https://registry.npmjs.org/@sansenjian/qq-music-api/-/qq-music-api-2.2.9.tgz",
|
"link": true
|
||||||
"integrity": "sha512-nlXfuShYWuRjcMB6tXMlVyBW6dRuYwNJNYH8AtSkP8qDf0gxViHDXNkao22IVVOoSQl7risYXbOHPsPqrn4WQQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@koa/router": "^15.3.1",
|
|
||||||
"axios": "^1.13.6",
|
|
||||||
"date-fns": "^4.1.0",
|
|
||||||
"is-generator-function": "1.0.10",
|
|
||||||
"koa": "^2.16.1",
|
|
||||||
"koa-bodyparser": "^4.4.1",
|
|
||||||
"koa-static": "^5.0.0",
|
|
||||||
"reflect-metadata": "^0.2.2"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"qq-music-api": "dist/app.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sansenjian/qq-music-api/node_modules/accepts": {
|
|
||||||
"version": "1.3.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
|
||||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-types": "~2.1.34",
|
|
||||||
"negotiator": "0.6.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sansenjian/qq-music-api/node_modules/content-disposition": {
|
|
||||||
"version": "0.5.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
|
||||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": "5.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sansenjian/qq-music-api/node_modules/encodeurl": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sansenjian/qq-music-api/node_modules/fresh": {
|
|
||||||
"version": "0.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
|
||||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sansenjian/qq-music-api/node_modules/http-errors": {
|
|
||||||
"version": "1.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
|
|
||||||
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"depd": "~1.1.2",
|
|
||||||
"inherits": "2.0.4",
|
|
||||||
"setprototypeof": "1.2.0",
|
|
||||||
"statuses": ">= 1.5.0 < 2",
|
|
||||||
"toidentifier": "1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sansenjian/qq-music-api/node_modules/http-errors/node_modules/depd": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sansenjian/qq-music-api/node_modules/koa": {
|
|
||||||
"version": "2.16.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/koa/-/koa-2.16.4.tgz",
|
|
||||||
"integrity": "sha512-3An0GCLDSR34tsCO4H8Tef8Pp2ngtaZDAZnsWJYelqXUK5wyiHvGItgK/xcSkmHLSTn1Jcho1mRQs2ehRzvKKw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"accepts": "^1.3.5",
|
|
||||||
"cache-content-type": "^1.0.0",
|
|
||||||
"content-disposition": "~0.5.2",
|
|
||||||
"content-type": "^1.0.4",
|
|
||||||
"cookies": "~0.9.0",
|
|
||||||
"debug": "^4.3.2",
|
|
||||||
"delegates": "^1.0.0",
|
|
||||||
"depd": "^2.0.0",
|
|
||||||
"destroy": "^1.0.4",
|
|
||||||
"encodeurl": "^1.0.2",
|
|
||||||
"escape-html": "^1.0.3",
|
|
||||||
"fresh": "~0.5.2",
|
|
||||||
"http-assert": "^1.3.0",
|
|
||||||
"http-errors": "^1.6.3",
|
|
||||||
"is-generator-function": "^1.0.7",
|
|
||||||
"koa-compose": "^4.1.0",
|
|
||||||
"koa-convert": "^2.0.0",
|
|
||||||
"on-finished": "^2.3.0",
|
|
||||||
"only": "~0.0.2",
|
|
||||||
"parseurl": "^1.3.2",
|
|
||||||
"statuses": "^1.5.0",
|
|
||||||
"type-is": "^1.6.16",
|
|
||||||
"vary": "^1.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sansenjian/qq-music-api/node_modules/media-typer": {
|
|
||||||
"version": "0.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
|
||||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sansenjian/qq-music-api/node_modules/mime-db": {
|
|
||||||
"version": "1.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sansenjian/qq-music-api/node_modules/mime-types": {
|
|
||||||
"version": "2.1.35",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "1.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sansenjian/qq-music-api/node_modules/negotiator": {
|
|
||||||
"version": "0.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
|
||||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sansenjian/qq-music-api/node_modules/statuses": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
|
||||||
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sansenjian/qq-music-api/node_modules/type-is": {
|
|
||||||
"version": "1.6.18",
|
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
|
||||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"media-typer": "0.3.0",
|
|
||||||
"mime-types": "~2.1.24"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@@ -1745,40 +1620,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cache-content-type": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-types": "^2.1.18",
|
|
||||||
"ylru": "^1.2.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cache-content-type/node_modules/mime-db": {
|
|
||||||
"version": "1.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cache-content-type/node_modules/mime-types": {
|
|
||||||
"version": "2.1.35",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "1.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/call-bind-apply-helpers": {
|
"node_modules/call-bind-apply-helpers": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
@@ -1874,16 +1715,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/co": {
|
|
||||||
"version": "4.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
|
||||||
"integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"iojs": ">= 1.0.0",
|
|
||||||
"node": ">= 0.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/co-body": {
|
"node_modules/co-body": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/co-body/-/co-body-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/co-body/-/co-body-6.2.0.tgz",
|
||||||
@@ -2140,16 +1971,6 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/date-fns": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/kossnocorp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -3216,21 +3037,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-generator-function": {
|
|
||||||
"version": "1.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
|
|
||||||
"integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"has-tostringtag": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-promise": {
|
"node_modules/is-promise": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||||
@@ -3341,19 +3147,6 @@
|
|||||||
"integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==",
|
"integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/koa-convert": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"co": "^4.6.0",
|
|
||||||
"koa-compose": "^4.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/koa-send": {
|
"node_modules/koa-send": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz",
|
||||||
@@ -4491,11 +4284,6 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/only": {
|
|
||||||
"version": "0.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz",
|
|
||||||
"integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ=="
|
|
||||||
},
|
|
||||||
"node_modules/p-limit": {
|
"node_modules/p-limit": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
@@ -5058,12 +4846,6 @@
|
|||||||
"node": ">= 12.13.0"
|
"node": ">= 12.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/reflect-metadata": {
|
|
||||||
"version": "0.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
|
||||||
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
@@ -6261,15 +6043,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ylru": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yt-dlp-wrap": {
|
"node_modules/yt-dlp-wrap": {
|
||||||
"version": "2.3.12",
|
"version": "2.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/yt-dlp-wrap/-/yt-dlp-wrap-2.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/yt-dlp-wrap/-/yt-dlp-wrap-2.3.12.tgz",
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
"@discordjs/opus": "^0.10.0",
|
"@discordjs/opus": "^0.10.0",
|
||||||
"@honeybbq/teamspeak-client": "^0.2.1",
|
"@honeybbq/teamspeak-client": "^0.2.1",
|
||||||
"@koa/router": "^15.4.0",
|
"@koa/router": "^15.4.0",
|
||||||
"@sansenjian/qq-music-api": "^2.2.9",
|
"@sansenjian/qq-music-api": "file:../qq-music-api",
|
||||||
"axios": "^1.14.0",
|
"axios": "^1.14.0",
|
||||||
"better-sqlite3": "^12.8.0",
|
"better-sqlite3": "^12.8.0",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
"""Open a real Chromium browser at y.qq.com, let the user log in via any
|
||||||
|
method (password / QR / QQ connect), then extract the resulting cookie
|
||||||
|
set and save it to data/cookies/qq.json. Also tests whether the cookie
|
||||||
|
actually unlocks a known VIP track (Jay Chou 稻香) against the local
|
||||||
|
QQ Music API before declaring success.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
"C:/Users/saopig1/miniforge3/python.exe" scripts/qq_browser_login.py
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1. A visible Chromium window opens at y.qq.com/n/ryqq/player
|
||||||
|
2. Click the login button in the top right and log in with your
|
||||||
|
real QQ Music account (the one that has VIP)
|
||||||
|
3. The script POLLS cookies in the background and auto-detects
|
||||||
|
successful login by watching for the `uin` cookie to appear
|
||||||
|
4. Once detected, cookies are captured, tested against
|
||||||
|
/getMusicPlay for 稻香, and saved on success
|
||||||
|
5. If VIP still fails, cookies are NOT saved — your existing bot
|
||||||
|
cookie stays untouched
|
||||||
|
|
||||||
|
No terminal input required — the script exits on its own when login
|
||||||
|
is detected (or after the configured timeout).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
BOT_ROOT = Path(r"C:\Users\saopig1\Music\teamspeak music bot")
|
||||||
|
COOKIE_FILE = BOT_ROOT / "data" / "cookies" / "qq.json"
|
||||||
|
QQ_API = "http://localhost:3200"
|
||||||
|
VIP_TEST_SONGMID = "003aAYrm3GE0Ac" # 稻香 周杰伦
|
||||||
|
|
||||||
|
# How long to wait for the user to finish logging in
|
||||||
|
LOGIN_TIMEOUT_S = 300 # 5 minutes
|
||||||
|
POLL_INTERVAL_S = 1.0
|
||||||
|
# After detecting login, wait a bit for extra cookies (e.g. qqmusic_key)
|
||||||
|
SETTLE_DELAY_S = 4.0
|
||||||
|
|
||||||
|
|
||||||
|
def qq_cookies(ctx) -> list[dict]:
|
||||||
|
wanted_suffixes = (".qq.com", "y.qq.com", ".music.qq.com")
|
||||||
|
return [
|
||||||
|
c for c in ctx.cookies()
|
||||||
|
if any(c.get("domain", "").endswith(s) or c.get("domain", "") == s.lstrip(".")
|
||||||
|
for s in wanted_suffixes)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def cookies_to_header(cookies: list[dict]) -> str:
|
||||||
|
return "; ".join(f"{c['name']}={c['value']}" for c in cookies)
|
||||||
|
|
||||||
|
|
||||||
|
def cookie_has_uin(cookies: list[dict]) -> str | None:
|
||||||
|
for c in cookies:
|
||||||
|
if c["name"] == "uin" and c["value"]:
|
||||||
|
return c["value"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def test_vip_unlock(cookie_header: str) -> tuple[bool, dict]:
|
||||||
|
try:
|
||||||
|
r = requests.get(
|
||||||
|
f"{QQ_API}/getMusicPlay",
|
||||||
|
params={"songmid": VIP_TEST_SONGMID, "cookie": cookie_header},
|
||||||
|
timeout=10,
|
||||||
|
proxies={"http": None, "https": None},
|
||||||
|
)
|
||||||
|
body = r.json()
|
||||||
|
play = body.get("data", {}).get("playUrl", {}).get(VIP_TEST_SONGMID, {})
|
||||||
|
url = play.get("url", "")
|
||||||
|
return (
|
||||||
|
bool(url),
|
||||||
|
{
|
||||||
|
"url_length": len(url),
|
||||||
|
"url_prefix": url[:120] if url else "",
|
||||||
|
"error": play.get("error", ""),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return False, {"error": f"request failed: {e}"}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
print("[setup] launching visible Chromium — look for the window on your desktop")
|
||||||
|
print("[setup] goto https://y.qq.com/n/ryqq/player")
|
||||||
|
print()
|
||||||
|
print("action required:")
|
||||||
|
print(" 1. Click the 登录 button (top-right) in the browser window")
|
||||||
|
print(" 2. Log in with your VIP QQ Music account (QR / password / WeChat)")
|
||||||
|
print(" 3. Do NOTHING in this terminal — the script detects login itself")
|
||||||
|
print()
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=False)
|
||||||
|
ctx = browser.new_context(
|
||||||
|
viewport={"width": 1280, "height": 820},
|
||||||
|
user_agent=(
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||||
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/132.0.0.0 Safari/537.36"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
page = ctx.new_page()
|
||||||
|
try:
|
||||||
|
page.goto("https://y.qq.com/n/ryqq/player", wait_until="domcontentloaded", timeout=30_000)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[warn] initial navigation slow: {e}")
|
||||||
|
|
||||||
|
print(f"[wait] polling every {POLL_INTERVAL_S}s for login (timeout {LOGIN_TIMEOUT_S}s)")
|
||||||
|
deadline = time.time() + LOGIN_TIMEOUT_S
|
||||||
|
uin_detected: str | None = None
|
||||||
|
last_report = 0.0
|
||||||
|
while time.time() < deadline:
|
||||||
|
cks = qq_cookies(ctx)
|
||||||
|
uin = cookie_has_uin(cks)
|
||||||
|
if uin:
|
||||||
|
uin_detected = uin
|
||||||
|
print(f"[detect] uin cookie appeared: {uin}")
|
||||||
|
break
|
||||||
|
now = time.time()
|
||||||
|
if now - last_report >= 15:
|
||||||
|
remaining = int(deadline - now)
|
||||||
|
n = len(cks)
|
||||||
|
print(f"[wait] still waiting... {n} qq.com cookies so far, {remaining}s left")
|
||||||
|
last_report = now
|
||||||
|
time.sleep(POLL_INTERVAL_S)
|
||||||
|
|
||||||
|
if not uin_detected:
|
||||||
|
print("[abort] login not detected within timeout")
|
||||||
|
browser.close()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"[settle] waiting {SETTLE_DELAY_S}s for session cookies to populate")
|
||||||
|
time.sleep(SETTLE_DELAY_S)
|
||||||
|
|
||||||
|
cks = qq_cookies(ctx)
|
||||||
|
cookie_header = cookies_to_header(cks)
|
||||||
|
print(f"[capture] {len(cks)} cookies, {len(cookie_header)} char header")
|
||||||
|
|
||||||
|
qm_key = next((c["value"] for c in cks if c["name"] == "qqmusic_key"), "")
|
||||||
|
qm_keyst = next((c["value"] for c in cks if c["name"] == "qm_keyst"), "")
|
||||||
|
p_skey = next((c["value"] for c in cks if c["name"] == "p_skey"), "")
|
||||||
|
print(f"[capture] qqmusic_key: {'present (' + qm_key[:20] + '...)' if qm_key else '(absent)'}")
|
||||||
|
print(f"[capture] qm_keyst : {'present (' + qm_keyst[:20] + '...)' if qm_keyst else '(absent)'}")
|
||||||
|
print(f"[capture] p_skey : {'present' if p_skey else '(absent)'}")
|
||||||
|
|
||||||
|
print("\n[test] calling /getMusicPlay for 稻香 with captured cookie...")
|
||||||
|
unlocked, details = test_vip_unlock(cookie_header)
|
||||||
|
print(f"[test] unlocked: {unlocked}")
|
||||||
|
print(f"[test] details: {details}")
|
||||||
|
|
||||||
|
if not unlocked:
|
||||||
|
print(
|
||||||
|
"\n[result] VIP did NOT unlock even with browser-extracted cookies.\n"
|
||||||
|
" Existing cookie file is UNTOUCHED.\n"
|
||||||
|
" Diagnosis: the login flow is not the bottleneck — the\n"
|
||||||
|
" account likely lacks entitlement for this specific track,\n"
|
||||||
|
" OR QQ requires additional session setup (gateway handshake)\n"
|
||||||
|
" beyond what's in the cookie itself.\n"
|
||||||
|
)
|
||||||
|
# Dump the full cookie set for inspection
|
||||||
|
dump_path = BOT_ROOT / "data" / "cookies" / "qq.browser-capture.json"
|
||||||
|
dump_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{"cookie": cookie_header, "cookieList": cks, "capturedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ")},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
print(f"[dump] full browser cookies written to {dump_path}")
|
||||||
|
print(" (for side-by-side comparison with OAuth-derived cookies)")
|
||||||
|
browser.close()
|
||||||
|
return 2
|
||||||
|
|
||||||
|
print("\n[save] VIP unlocked. Writing cookie to bot...")
|
||||||
|
COOKIE_FILE.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{"cookie": cookie_header, "updatedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ")},
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
print(f"[save] wrote {COOKIE_FILE}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
"http://localhost:3000/api/auth/cookie",
|
||||||
|
json={"platform": "qq", "cookie": cookie_header},
|
||||||
|
timeout=5,
|
||||||
|
proxies={"http": None, "https": None},
|
||||||
|
)
|
||||||
|
print(f"[notify] /api/auth/cookie POST: {r.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[notify] failed to push cookie to bot: {e}")
|
||||||
|
print(" Restart the bot to pick up the new cookie from disk.")
|
||||||
|
|
||||||
|
print("\n[done] VIP should now work through the bot. Try playing 稻香!")
|
||||||
|
browser.close()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
"""Open a visible browser at y.qq.com so the user can manually verify
|
||||||
|
whether their VIP account can play 稻香 (Jay Chou) in the real QQ Music
|
||||||
|
web player.
|
||||||
|
|
||||||
|
If the browser plays the song → entitlement exists and our 104003 is a
|
||||||
|
request-signing issue.
|
||||||
|
If the browser refuses / shows a VIP modal / silently fails → the
|
||||||
|
account doesn't have entitlement OR QQ's web player hits the same wall.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
# Force line-buffered stdout so logs actually reach the output file
|
||||||
|
sys.stdout.reconfigure(line_buffering=True)
|
||||||
|
|
||||||
|
START_URL = "https://y.qq.com/n/ryqq/player"
|
||||||
|
SONG_URL = "https://y.qq.com/n/ryqq/songDetail/003aAYrm3GE0Ac"
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str) -> None:
|
||||||
|
print(msg, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
log("[setup] launching visible Chromium")
|
||||||
|
log(f"[setup] start URL: {START_URL}")
|
||||||
|
log(f"[setup] song URL: {SONG_URL}")
|
||||||
|
log("")
|
||||||
|
log("action required:")
|
||||||
|
log(" 1. The browser opens at the player page")
|
||||||
|
log(" 2. Make sure your VIP account is logged in (top-right avatar)")
|
||||||
|
log(" — if not, log in now, the script will wait")
|
||||||
|
log(" 3. Once logged in, the browser will auto-navigate to 稻香")
|
||||||
|
log(" 4. Click the PLAY button and report what happens:")
|
||||||
|
log(" (a) song plays → account has entitlement, issue is request signing")
|
||||||
|
log(" (b) VIP modal → account needs higher tier / digital album purchase")
|
||||||
|
log(" (c) silent failure → QQ web player has same 104003 wall")
|
||||||
|
log("")
|
||||||
|
log("[wait] browser stays open for 5 minutes")
|
||||||
|
log("")
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
try:
|
||||||
|
browser = p.chromium.launch(headless=False)
|
||||||
|
except Exception as e:
|
||||||
|
log(f"[fatal] failed to launch Chromium: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
ctx = browser.new_context(
|
||||||
|
viewport={"width": 1400, "height": 900},
|
||||||
|
user_agent=(
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||||
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/132.0.0.0 Safari/537.36"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
page = ctx.new_page()
|
||||||
|
|
||||||
|
# Log every navigation so we can see if pages fail
|
||||||
|
page.on("framenavigated", lambda f: log(f"[nav] {f.url[:120]}") if f == page.main_frame else None)
|
||||||
|
page.on("pageerror", lambda e: log(f"[js-error] {str(e)[:200]}"))
|
||||||
|
|
||||||
|
# Step 1: open the player page (known working)
|
||||||
|
log(f"[goto] {START_URL}")
|
||||||
|
try:
|
||||||
|
page.goto(START_URL, wait_until="domcontentloaded", timeout=30_000)
|
||||||
|
log(f"[ok] loaded: {page.url}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"[warn] initial goto failed: {e}")
|
||||||
|
log("[warn] browser stays open, try manual navigation")
|
||||||
|
|
||||||
|
# Wait briefly for auth state to settle
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Check if the user is logged in via the uin cookie
|
||||||
|
cookies = ctx.cookies()
|
||||||
|
uin = next((c["value"] for c in cookies if c["name"] == "uin" and c["value"]), None)
|
||||||
|
if uin:
|
||||||
|
log(f"[auth] logged in as uin={uin}")
|
||||||
|
else:
|
||||||
|
log("[auth] not logged in yet — please log in via the top-right avatar")
|
||||||
|
log("[auth] waiting up to 2 minutes for login...")
|
||||||
|
end = time.time() + 120
|
||||||
|
while time.time() < end:
|
||||||
|
time.sleep(1)
|
||||||
|
cookies = ctx.cookies()
|
||||||
|
uin = next((c["value"] for c in cookies if c["name"] == "uin" and c["value"]), None)
|
||||||
|
if uin:
|
||||||
|
log(f"[auth] detected login: uin={uin}")
|
||||||
|
break
|
||||||
|
if not uin:
|
||||||
|
log("[abort] no login detected within 2 minutes")
|
||||||
|
time.sleep(30) # keep browser visible
|
||||||
|
browser.close()
|
||||||
|
return 2
|
||||||
|
|
||||||
|
# Step 2: navigate to the song page
|
||||||
|
log(f"[goto] {SONG_URL}")
|
||||||
|
try:
|
||||||
|
page.goto(SONG_URL, wait_until="domcontentloaded", timeout=30_000)
|
||||||
|
log(f"[ok] loaded: {page.url}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"[warn] song navigation failed: {e}")
|
||||||
|
log(f"[info] current page: {page.url}")
|
||||||
|
|
||||||
|
log("")
|
||||||
|
log("===========================================================")
|
||||||
|
log("browser is open on the song page — click PLAY and observe.")
|
||||||
|
log("keeping browser open for 5 more minutes")
|
||||||
|
log("===========================================================")
|
||||||
|
|
||||||
|
time.sleep(300)
|
||||||
|
browser.close()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+25
-6
@@ -32,6 +32,7 @@ export function createApiServerManager(
|
|||||||
logger: Logger
|
logger: Logger
|
||||||
): ApiServerManager {
|
): ApiServerManager {
|
||||||
let neteaseServer: Server | null = null;
|
let neteaseServer: Server | null = null;
|
||||||
|
let qqMusicServer: Server | null = null;
|
||||||
|
|
||||||
const neteaseBaseUrl = `http://127.0.0.1:${options.neteasePort}`;
|
const neteaseBaseUrl = `http://127.0.0.1:${options.neteasePort}`;
|
||||||
const qqMusicBaseUrl = `http://127.0.0.1:${options.qqMusicPort}`;
|
const qqMusicBaseUrl = `http://127.0.0.1:${options.qqMusicPort}`;
|
||||||
@@ -62,7 +63,10 @@ export function createApiServerManager(
|
|||||||
logger.error({ err }, "Failed to start NetEase Cloud Music API");
|
logger.error({ err }, "Failed to start NetEase Cloud Music API");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start QQ Music API (auto-starts on import)
|
// Start QQ Music API. Older versions auto-started on import; the
|
||||||
|
// current fork (2.2.11+) only listens when run as `require.main`,
|
||||||
|
// so we explicitly call .listen() on the imported Koa app and keep
|
||||||
|
// the server handle for clean shutdown.
|
||||||
try {
|
try {
|
||||||
const portFree = await isPortFree(options.qqMusicPort);
|
const portFree = await isPortFree(options.qqMusicPort);
|
||||||
if (!portFree) {
|
if (!portFree) {
|
||||||
@@ -71,11 +75,22 @@ export function createApiServerManager(
|
|||||||
"QQ Music API port already in use — reusing existing instance"
|
"QQ Music API port already in use — reusing existing instance"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await import("@sansenjian/qq-music-api");
|
const qqModule = (await import("@sansenjian/qq-music-api")) as any;
|
||||||
logger.info(
|
const koaApp = qqModule.default ?? qqModule;
|
||||||
{ port: options.qqMusicPort },
|
if (koaApp && typeof koaApp.listen === "function") {
|
||||||
"QQ Music API started"
|
qqMusicServer = await new Promise<Server>((resolve, reject) => {
|
||||||
);
|
const srv = koaApp.listen(options.qqMusicPort, "127.0.0.1", () =>
|
||||||
|
resolve(srv)
|
||||||
|
);
|
||||||
|
srv.on("error", reject);
|
||||||
|
});
|
||||||
|
logger.info(
|
||||||
|
{ port: options.qqMusicPort },
|
||||||
|
"QQ Music API started"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn("QQ Music API module does not expose a Koa app");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -91,6 +106,10 @@ export function createApiServerManager(
|
|||||||
(neteaseServer as any).close();
|
(neteaseServer as any).close();
|
||||||
}
|
}
|
||||||
neteaseServer = null;
|
neteaseServer = null;
|
||||||
|
if (qqMusicServer && typeof (qqMusicServer as any).close === "function") {
|
||||||
|
(qqMusicServer as any).close();
|
||||||
|
}
|
||||||
|
qqMusicServer = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
getNeteaseBaseUrl(): string {
|
getNeteaseBaseUrl(): string {
|
||||||
|
|||||||
+77
-26
@@ -66,7 +66,12 @@ export class QQMusicProvider implements MusicProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSongDetail(songId: string): Promise<Song | null> {
|
async getSongDetail(songId: string): Promise<Song | null> {
|
||||||
// getSongInfo requires cookie; use search as fallback
|
// Try /getSongInfo for full metadata, but fall through to a minimal
|
||||||
|
// stub if the library endpoint fails (current @sansenjian/qq-music-api
|
||||||
|
// returns upstream code 500001 for this route — the param format it
|
||||||
|
// sends doesn't match QQ's current API). The bot's resolveAndPlay path
|
||||||
|
// only needs `id` and `platform` to fetch a play URL, and the fallback
|
||||||
|
// stub is sufficient to let /play-by-id and /add-by-id flows succeed.
|
||||||
try {
|
try {
|
||||||
const res = await this.api.get("/getSongInfo", {
|
const res = await this.api.get("/getSongInfo", {
|
||||||
params: { songmid: songId, ...this.cookieParams },
|
params: { songmid: songId, ...this.cookieParams },
|
||||||
@@ -87,9 +92,20 @@ export class QQMusicProvider implements MusicProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// fallback: search by songmid (less reliable)
|
// fall through to stub
|
||||||
}
|
}
|
||||||
return null;
|
// Minimal stub — resolveAndPlay only needs id + platform to fetch a
|
||||||
|
// play URL. Name/artist/album will be empty in play history, but the
|
||||||
|
// song will actually play, which is the important part.
|
||||||
|
return {
|
||||||
|
id: songId,
|
||||||
|
name: "",
|
||||||
|
artist: "",
|
||||||
|
album: "",
|
||||||
|
duration: 0,
|
||||||
|
coverUrl: "",
|
||||||
|
platform: "qq",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPlaylistSongs(playlistId: string): Promise<Song[]> {
|
async getPlaylistSongs(playlistId: string): Promise<Song[]> {
|
||||||
@@ -152,30 +168,53 @@ export class QQMusicProvider implements MusicProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getQrCode(): Promise<QrCodeResult> {
|
async getQrCode(): Promise<QrCodeResult> {
|
||||||
|
// @sansenjian/qq-music-api 2.x returns { img, qrsig, ptqrtoken } via
|
||||||
|
// customResponse (no { response: ... } wrapping). /checkQQLoginQr
|
||||||
|
// requires BOTH qrsig AND ptqrtoken — passing only one gives a 400
|
||||||
|
// "参数错误". Pack both into the opaque `key` field so the polling
|
||||||
|
// endpoint can split them back out. Separator "|" is safe: QQ tokens
|
||||||
|
// are alphanumeric.
|
||||||
const res = await this.api.get("/getQQLoginQr");
|
const res = await this.api.get("/getQQLoginQr");
|
||||||
|
const qrsig: string = res.data?.qrsig ?? "";
|
||||||
|
const ptqrtoken: string = String(res.data?.ptqrtoken ?? "");
|
||||||
return {
|
return {
|
||||||
qrUrl: "",
|
qrUrl: "",
|
||||||
qrImg: res.data?.img ?? "",
|
qrImg: res.data?.img ?? "",
|
||||||
key: res.data?.qrsig ?? res.data?.ptqrtoken ?? "",
|
key: `${qrsig}|${ptqrtoken}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkQrCodeStatus(
|
async checkQrCodeStatus(
|
||||||
key: string
|
key: string
|
||||||
): Promise<"waiting" | "scanned" | "confirmed" | "expired"> {
|
): Promise<"waiting" | "scanned" | "confirmed" | "expired"> {
|
||||||
const res = await this.api.get("/checkQQLoginQr", {
|
const [qrsig, ptqrtoken] = key.split("|");
|
||||||
params: { qrsig: key },
|
if (!qrsig || !ptqrtoken) return "expired";
|
||||||
});
|
|
||||||
const code = res.data?.code ?? res.data?.response?.code;
|
// NOTE: /checkQQLoginQr is registered as POST only in
|
||||||
if (code === 0) {
|
// @sansenjian/qq-music-api 2.x. GET returns 405 Method Not Allowed.
|
||||||
if (res.data?.cookie) {
|
let res;
|
||||||
this.cookie = res.data.cookie;
|
try {
|
||||||
}
|
res = await this.api.post("/checkQQLoginQr", null, {
|
||||||
|
params: { qrsig, ptqrtoken },
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return "expired";
|
||||||
|
}
|
||||||
|
|
||||||
|
// customResponse shape:
|
||||||
|
// success: { isOk: true, message: '登录成功', session: { cookie, ... } }
|
||||||
|
// scanning: { isOk: false, refresh: false, message: '未扫描二维码' }
|
||||||
|
// expired: { isOk: false, refresh: true, message: '二维码已失效' }
|
||||||
|
const body = res.data;
|
||||||
|
if (body?.isOk === true) {
|
||||||
|
const cookie: string = body.session?.cookie ?? "";
|
||||||
|
if (cookie) this.cookie = cookie;
|
||||||
return "confirmed";
|
return "confirmed";
|
||||||
}
|
}
|
||||||
if (code === 1) return "scanned";
|
if (body?.refresh === true) return "expired";
|
||||||
if (code === 2) return "waiting";
|
if (typeof body?.message === "string" && body.message.includes("未扫描"))
|
||||||
return "expired";
|
return "waiting";
|
||||||
|
return "waiting";
|
||||||
}
|
}
|
||||||
|
|
||||||
setCookie(cookie: string): void {
|
setCookie(cookie: string): void {
|
||||||
@@ -188,20 +227,32 @@ export class QQMusicProvider implements MusicProvider {
|
|||||||
|
|
||||||
async getAuthStatus(): Promise<AuthStatus> {
|
async getAuthStatus(): Promise<AuthStatus> {
|
||||||
if (!this.cookie) return { loggedIn: false };
|
if (!this.cookie) return { loggedIn: false };
|
||||||
|
// /getUserAvatar in @sansenjian/qq-music-api 2.x is NOT registered on
|
||||||
|
// the main router; the real endpoint is /user/getUserAvatar, and even
|
||||||
|
// that just builds a static URL from a uin without validating the
|
||||||
|
// cookie against QQ. Round-trip through /user/getUserPlaylists which
|
||||||
|
// actually hits QQ Music with the cookie; if the upstream returns
|
||||||
|
// code=0, the cookie is valid.
|
||||||
|
//
|
||||||
|
// IMPORTANT: /user/getUserPlaylists requires `uin` as a query param —
|
||||||
|
// the library 400s with "缺少 uin 参数" otherwise. Parse it out of the
|
||||||
|
// cookie (uin=<qq>; comes after the various *uin prefixed names, which
|
||||||
|
// is why the regex anchors on a word boundary).
|
||||||
|
const uinMatch = /(?:^|; )uin=o?0?(\d+)/.exec(this.cookie);
|
||||||
|
const uin = uinMatch ? uinMatch[1] : "";
|
||||||
|
if (!uin) return { loggedIn: false };
|
||||||
try {
|
try {
|
||||||
const res = await this.api.get("/getUserAvatar", {
|
const res = await this.api.get("/user/getUserPlaylists", {
|
||||||
params: { ...this.cookieParams },
|
params: { uin, ...this.cookieParams },
|
||||||
});
|
});
|
||||||
if (res.data?.response?.data) {
|
if (res.data?.response?.code !== 0) return { loggedIn: false };
|
||||||
return {
|
return {
|
||||||
loggedIn: true,
|
loggedIn: true,
|
||||||
nickname: res.data.response.data.nickname,
|
nickname: `QQ ${uin}`,
|
||||||
avatarUrl: res.data.response.data.headpic,
|
avatarUrl: `https://q.qlogo.cn/headimg_dl?dst_uin=${uin}&spec=100`,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
return { loggedIn: false };
|
||||||
}
|
}
|
||||||
return { loggedIn: false };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user