feat(login): 新增网易云扫码/手机号登录与酷狗 token 绑定
This commit is contained in:
1 parent
fd5e2122d3
commit
45f3a4b989
15 files changed
+716
-3
No files matched your search
@@ -11,6 +11,7 @@ lx-music-source-v5.js
|
|||||||
*.log
|
*.log
|
||||||
logs/
|
logs/
|
||||||
tmp/
|
tmp/
|
||||||
|
data/
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ pnpm test # 运行冒烟测试(自动启动/关闭本地服务器)
|
|||||||
| `GET /singer/:source/:id/songs?page=1&limit=30` | 歌手歌曲列表(kg/tx/wy) |
|
| `GET /singer/:source/:id/songs?page=1&limit=30` | 歌手歌曲列表(kg/tx/wy) |
|
||||||
| `GET /singer/:source/:id/albums?page=1&limit=10` | 歌手专辑列表(kg/tx/wy) |
|
| `GET /singer/:source/:id/albums?page=1&limit=10` | 歌手专辑列表(kg/tx/wy) |
|
||||||
| `GET /album/:source/:id?page=1` | 专辑详情与曲目(kg/kw/mg/tx) |
|
| `GET /album/:source/:id?page=1` | 专辑详情与曲目(kg/kw/mg/tx) |
|
||||||
|
| `POST /login/wy/qr` → `POST /login/wy/qr/check` | 网易云扫码登录(扫码后轮询完成登录) |
|
||||||
|
| `POST /login/wy/cellphone` | 网易云手机号登录(password 或 captcha) |
|
||||||
|
| `POST /login/kg` + `userid` | 绑定酷狗 token(App 抓包获取,服务端验证) |
|
||||||
|
| `GET /login/status` | 登录状态(登录后取流/请求自动携带凭据) |
|
||||||
| `GET /sources` | 平台列表(音质与能力) |
|
| `GET /sources` | 平台列表(音质与能力) |
|
||||||
| `GET /health` | 健康检查 |
|
| `GET /health` | 健康检查 |
|
||||||
| `GET /docs` `/redoc` `/openapi.json` | 接口文档(Swagger UI / ReDoc / OpenAPI 3.1) |
|
| `GET /docs` `/redoc` `/openapi.json` | 接口文档(Swagger UI / ReDoc / OpenAPI 3.1) |
|
||||||
|
|||||||
+15
@@ -75,6 +75,21 @@
|
|||||||
|
|
||||||
歌手/专辑 id 来源:搜索结果条目包含 `albumId`;歌手 id 需从平台方获取(不在搜索结果中)。
|
歌手/专辑 id 来源:搜索结果条目包含 `albumId`;歌手 id 需从平台方获取(不在搜索结果中)。
|
||||||
|
|
||||||
|
### 登录
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| POST | `/login/wy/qr` | 获取网易云扫码登录二维码(返回 `key`/`qrurl`/`qrimg`,`qrimg` 为 data URL 可直接展示) |
|
||||||
|
| POST | `/login/wy/qr/check` | 轮询扫码状态,body 传 `{ "key": "..." }`。返回 `code`:800 过期 / 801 等待 / 802 已扫码 / 803 登录成功(自动保存凭据) |
|
||||||
|
| POST | `/login/wy/cellphone` | 网易云手机号登录;body 传 `phone` + `password`(明文,服务端 MD5 后提交)或 `captcha`,可带 `countrycode`(默认 86) |
|
||||||
|
| DELETE | `/login/wy` | 退出网易云登录 |
|
||||||
|
| POST | `/login/kg` | 绑定酷狗 token;body 传 `{ "token": "...", "userid": ... }`(App 抓包获取),服务端向用户中心验证后存储 |
|
||||||
|
| DELETE | `/login/kg` | 退出酷狗登录 |
|
||||||
|
| GET | `/login/status` | 登录状态(wy 实时校验 / kg 已存凭据) |
|
||||||
|
|
||||||
|
- 登录态持久化于 `data/auth.json`(不入库),登录后取流与平台请求自动携带凭据。
|
||||||
|
- VIP 歌曲完整播放地址暂不在支持范围;登录态目前用于提升请求稳定性与为后续能力预留。
|
||||||
|
|
||||||
## 响应示例
|
## 响应示例
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { httpFetch } from '../../request.ts'
|
import { httpFetch } from '../../request.ts'
|
||||||
import { eapi } from './crypto.ts'
|
import { eapi } from './crypto.ts'
|
||||||
|
import { getWyCookie } from '../../src/auth/store.js'
|
||||||
import type { HttpRequestObject } from '../../types.ts'
|
import type { HttpRequestObject } from '../../types.ts'
|
||||||
|
|
||||||
export const eapiRequest = (url: string, data: object): HttpRequestObject => {
|
export const eapiRequest = (url: string, data: object): HttpRequestObject => {
|
||||||
|
// 登录后携带 MUSIC_U(见 /login/wy),未登录时不附加
|
||||||
|
const cookie = getWyCookie()
|
||||||
return httpFetch('http://interface.music.163.com/eapi/batch', {
|
return httpFetch('http://interface.music.163.com/eapi/batch', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
|
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
|
||||||
origin: 'https://music.163.com',
|
origin: 'https://music.163.com',
|
||||||
// cookie: 'os=pc; deviceId=A9C064BB4584D038B1565B58CB05F95290998EE8B025AA2D07AE; osver=Microsoft-Windows-10-Home-China-build-19043-64bit; appver=2.5.2.197409; channel=netease; MUSIC_A=37a11f2eb9de9930cad479b2ad495b0e4c982367fb6f909d9a3f18f876c6b49faddb3081250c4980dd7e19d4bd9bf384e004602712cf2b2b8efaafaab164268a00b47359f85f22705cc95cb6180f3aee40f5be1ebf3148d888aa2d90636647d0c3061cd18d77b7a0; __csrf=05b50d54082694f945d7de75c210ef94; mode=Z7M-KP5(7)GZ; NMTID=00OZLp2VVgq9QdwokUgq3XNfOddQyIAAAF_6i8eJg; ntes_kaola_ad=1',
|
...(cookie ? { cookie } : {}),
|
||||||
},
|
},
|
||||||
form: eapi(url, data),
|
form: eapi(url, data),
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-1
@@ -18,7 +18,8 @@
|
|||||||
"fastify": "^5.12.1",
|
"fastify": "^5.12.1",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"isolated-vm": "^7.0.1",
|
"isolated-vm": "^7.0.1",
|
||||||
"pako": "^2.1.0"
|
"pako": "^2.1.0",
|
||||||
|
"qrcode": "^1.5.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^26.4.0",
|
"@types/node": "^26.4.0",
|
||||||
|
|||||||
Generated
+216
@@ -190,6 +190,9 @@ importers:
|
|||||||
pako:
|
pako:
|
||||||
specifier: ^2.1.0
|
specifier: ^2.1.0
|
||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
|
qrcode:
|
||||||
|
specifier: ^1.5.4
|
||||||
|
version: 1.5.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^26.4.0
|
specifier: ^26.4.0
|
||||||
@@ -383,6 +386,14 @@ packages:
|
|||||||
ajv@8.20.0:
|
ajv@8.20.0:
|
||||||
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
|
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
|
||||||
|
|
||||||
|
ansi-regex@5.0.1:
|
||||||
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ansi-styles@4.3.0:
|
||||||
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
atomic-sleep@1.0.0:
|
atomic-sleep@1.0.0:
|
||||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
@@ -398,6 +409,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
|
resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
camelcase@5.3.1:
|
||||||
|
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
cliui@6.0.0:
|
||||||
|
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
|
engines: {node: '>=7.0.0'}
|
||||||
|
|
||||||
|
color-name@1.1.4:
|
||||||
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
content-disposition@2.0.1:
|
content-disposition@2.0.1:
|
||||||
resolution: {integrity: sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==}
|
resolution: {integrity: sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -419,6 +444,10 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decamelize@1.2.0:
|
||||||
|
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
depd@2.0.0:
|
depd@2.0.0:
|
||||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -427,6 +456,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
dijkstrajs@1.0.3:
|
||||||
|
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||||
|
|
||||||
|
emoji-regex@8.0.0:
|
||||||
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
escape-html@1.0.3:
|
escape-html@1.0.3:
|
||||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||||
|
|
||||||
@@ -461,6 +496,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==}
|
resolution: {integrity: sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
find-up@4.1.0:
|
||||||
|
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
get-caller-file@2.0.5:
|
||||||
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
|
|
||||||
glob@13.0.6:
|
glob@13.0.6:
|
||||||
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
|
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
@@ -480,6 +523,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==}
|
resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0:
|
||||||
|
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
isolated-vm@7.0.1:
|
isolated-vm@7.0.1:
|
||||||
resolution: {integrity: sha512-nBCQ1hXEaYKYJ79wwfhaF44ITOJC30Hw0wPEsFeNsdD3hdeKK7FW1SGMQYBUA+qasvkqdOazI+iBuq0lcbN/aw==}
|
resolution: {integrity: sha512-nBCQ1hXEaYKYJ79wwfhaF44ITOJC30Hw0wPEsFeNsdD3hdeKK7FW1SGMQYBUA+qasvkqdOazI+iBuq0lcbN/aw==}
|
||||||
engines: {node: '>=24.0.0'}
|
engines: {node: '>=24.0.0'}
|
||||||
@@ -497,6 +544,10 @@ packages:
|
|||||||
light-my-request@6.6.0:
|
light-my-request@6.6.0:
|
||||||
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
|
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
|
||||||
|
|
||||||
|
locate-path@5.0.0:
|
||||||
|
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
lru-cache@11.5.2:
|
lru-cache@11.5.2:
|
||||||
resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==}
|
resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -528,9 +579,25 @@ packages:
|
|||||||
openapi-types@12.1.3:
|
openapi-types@12.1.3:
|
||||||
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
||||||
|
|
||||||
|
p-limit@2.3.0:
|
||||||
|
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
p-locate@4.1.0:
|
||||||
|
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
p-try@2.2.0:
|
||||||
|
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
pako@2.2.0:
|
pako@2.2.0:
|
||||||
resolution: {integrity: sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==}
|
resolution: {integrity: sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==}
|
||||||
|
|
||||||
|
path-exists@4.0.0:
|
||||||
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
path-scurry@2.0.2:
|
path-scurry@2.0.2:
|
||||||
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
|
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
@@ -555,12 +622,21 @@ packages:
|
|||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
pngjs@5.0.0:
|
||||||
|
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
process-warning@4.0.1:
|
process-warning@4.0.1:
|
||||||
resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==}
|
resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==}
|
||||||
|
|
||||||
process-warning@5.1.0:
|
process-warning@5.1.0:
|
||||||
resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==}
|
resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==}
|
||||||
|
|
||||||
|
qrcode@1.5.4:
|
||||||
|
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4:
|
quick-format-unescaped@4.0.4:
|
||||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||||
|
|
||||||
@@ -571,10 +647,17 @@ packages:
|
|||||||
real-require@1.0.0:
|
real-require@1.0.0:
|
||||||
resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==}
|
resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==}
|
||||||
|
|
||||||
|
require-directory@2.1.1:
|
||||||
|
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
require-from-string@2.0.2:
|
require-from-string@2.0.2:
|
||||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
require-main-filename@2.0.0:
|
||||||
|
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||||
|
|
||||||
ret@0.5.0:
|
ret@0.5.0:
|
||||||
resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==}
|
resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -605,6 +688,9 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
set-blocking@2.0.0:
|
||||||
|
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||||
|
|
||||||
set-cookie-parser@2.7.2:
|
set-cookie-parser@2.7.2:
|
||||||
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
|
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
|
||||||
|
|
||||||
@@ -622,6 +708,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
thread-stream@4.2.0:
|
thread-stream@4.2.0:
|
||||||
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
|
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -642,11 +736,29 @@ packages:
|
|||||||
undici-types@8.9.0:
|
undici-types@8.9.0:
|
||||||
resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
|
resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
|
||||||
|
|
||||||
|
which-module@2.0.1:
|
||||||
|
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||||
|
|
||||||
|
wrap-ansi@6.2.0:
|
||||||
|
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
y18n@4.0.3:
|
||||||
|
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||||
|
|
||||||
yaml@2.9.1:
|
yaml@2.9.1:
|
||||||
resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==}
|
resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==}
|
||||||
engines: {node: '>= 14.6'}
|
engines: {node: '>= 14.6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
yargs-parser@18.1.3:
|
||||||
|
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
yargs@15.4.1:
|
||||||
|
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@fastify/accept-negotiator@2.1.0': {}
|
'@fastify/accept-negotiator@2.1.0': {}
|
||||||
@@ -796,6 +908,12 @@ snapshots:
|
|||||||
json-schema-traverse: 1.0.0
|
json-schema-traverse: 1.0.0
|
||||||
require-from-string: 2.0.2
|
require-from-string: 2.0.2
|
||||||
|
|
||||||
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
|
ansi-styles@4.3.0:
|
||||||
|
dependencies:
|
||||||
|
color-convert: 2.0.1
|
||||||
|
|
||||||
atomic-sleep@1.0.0: {}
|
atomic-sleep@1.0.0: {}
|
||||||
|
|
||||||
avvio@9.3.0:
|
avvio@9.3.0:
|
||||||
@@ -809,6 +927,20 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 4.0.4
|
balanced-match: 4.0.4
|
||||||
|
|
||||||
|
camelcase@5.3.1: {}
|
||||||
|
|
||||||
|
cliui@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
wrap-ansi: 6.2.0
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.4
|
||||||
|
|
||||||
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
content-disposition@2.0.1: {}
|
content-disposition@2.0.1: {}
|
||||||
|
|
||||||
cookie@1.1.1: {}
|
cookie@1.1.1: {}
|
||||||
@@ -819,10 +951,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
decamelize@1.2.0: {}
|
||||||
|
|
||||||
depd@2.0.0: {}
|
depd@2.0.0: {}
|
||||||
|
|
||||||
dequal@2.0.3: {}
|
dequal@2.0.3: {}
|
||||||
|
|
||||||
|
dijkstrajs@1.0.3: {}
|
||||||
|
|
||||||
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
escape-html@1.0.3: {}
|
escape-html@1.0.3: {}
|
||||||
|
|
||||||
fast-decode-uri-component@1.0.1: {}
|
fast-decode-uri-component@1.0.1: {}
|
||||||
@@ -876,6 +1014,13 @@ snapshots:
|
|||||||
fast-querystring: 1.1.2
|
fast-querystring: 1.1.2
|
||||||
safe-regex2: 5.1.1
|
safe-regex2: 5.1.1
|
||||||
|
|
||||||
|
find-up@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
locate-path: 5.0.0
|
||||||
|
path-exists: 4.0.0
|
||||||
|
|
||||||
|
get-caller-file@2.0.5: {}
|
||||||
|
|
||||||
glob@13.0.6:
|
glob@13.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimatch: 10.2.6
|
minimatch: 10.2.6
|
||||||
@@ -898,6 +1043,8 @@ snapshots:
|
|||||||
|
|
||||||
ipaddr.js@2.5.0: {}
|
ipaddr.js@2.5.0: {}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0: {}
|
||||||
|
|
||||||
isolated-vm@7.0.1:
|
isolated-vm@7.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
node-gyp-build: 4.8.4
|
node-gyp-build: 4.8.4
|
||||||
@@ -922,6 +1069,10 @@ snapshots:
|
|||||||
process-warning: 4.0.1
|
process-warning: 4.0.1
|
||||||
set-cookie-parser: 2.7.2
|
set-cookie-parser: 2.7.2
|
||||||
|
|
||||||
|
locate-path@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
p-locate: 4.1.0
|
||||||
|
|
||||||
lru-cache@11.5.2: {}
|
lru-cache@11.5.2: {}
|
||||||
|
|
||||||
mime@3.0.0: {}
|
mime@3.0.0: {}
|
||||||
@@ -940,8 +1091,20 @@ snapshots:
|
|||||||
|
|
||||||
openapi-types@12.1.3: {}
|
openapi-types@12.1.3: {}
|
||||||
|
|
||||||
|
p-limit@2.3.0:
|
||||||
|
dependencies:
|
||||||
|
p-try: 2.2.0
|
||||||
|
|
||||||
|
p-locate@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
p-limit: 2.3.0
|
||||||
|
|
||||||
|
p-try@2.2.0: {}
|
||||||
|
|
||||||
pako@2.2.0: {}
|
pako@2.2.0: {}
|
||||||
|
|
||||||
|
path-exists@4.0.0: {}
|
||||||
|
|
||||||
path-scurry@2.0.2:
|
path-scurry@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
lru-cache: 11.5.2
|
lru-cache: 11.5.2
|
||||||
@@ -973,18 +1136,30 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
playwright-core: 1.63.0
|
playwright-core: 1.63.0
|
||||||
|
|
||||||
|
pngjs@5.0.0: {}
|
||||||
|
|
||||||
process-warning@4.0.1: {}
|
process-warning@4.0.1: {}
|
||||||
|
|
||||||
process-warning@5.1.0: {}
|
process-warning@5.1.0: {}
|
||||||
|
|
||||||
|
qrcode@1.5.4:
|
||||||
|
dependencies:
|
||||||
|
dijkstrajs: 1.0.3
|
||||||
|
pngjs: 5.0.0
|
||||||
|
yargs: 15.4.1
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4: {}
|
quick-format-unescaped@4.0.4: {}
|
||||||
|
|
||||||
real-require@0.2.0: {}
|
real-require@0.2.0: {}
|
||||||
|
|
||||||
real-require@1.0.0: {}
|
real-require@1.0.0: {}
|
||||||
|
|
||||||
|
require-directory@2.1.1: {}
|
||||||
|
|
||||||
require-from-string@2.0.2: {}
|
require-from-string@2.0.2: {}
|
||||||
|
|
||||||
|
require-main-filename@2.0.0: {}
|
||||||
|
|
||||||
ret@0.5.0: {}
|
ret@0.5.0: {}
|
||||||
|
|
||||||
reusify@1.1.0: {}
|
reusify@1.1.0: {}
|
||||||
@@ -1003,6 +1178,8 @@ snapshots:
|
|||||||
|
|
||||||
semver@7.8.5: {}
|
semver@7.8.5: {}
|
||||||
|
|
||||||
|
set-blocking@2.0.0: {}
|
||||||
|
|
||||||
set-cookie-parser@2.7.2: {}
|
set-cookie-parser@2.7.2: {}
|
||||||
|
|
||||||
setprototypeof@1.2.0: {}
|
setprototypeof@1.2.0: {}
|
||||||
@@ -1015,6 +1192,16 @@ snapshots:
|
|||||||
|
|
||||||
statuses@2.0.2: {}
|
statuses@2.0.2: {}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
dependencies:
|
||||||
|
emoji-regex: 8.0.0
|
||||||
|
is-fullwidth-code-point: 3.0.0
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 5.0.1
|
||||||
|
|
||||||
thread-stream@4.2.0:
|
thread-stream@4.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
real-require: 1.0.0
|
real-require: 1.0.0
|
||||||
@@ -1048,4 +1235,33 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@8.9.0: {}
|
undici-types@8.9.0: {}
|
||||||
|
|
||||||
|
which-module@2.0.1: {}
|
||||||
|
|
||||||
|
wrap-ansi@6.2.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
y18n@4.0.3: {}
|
||||||
|
|
||||||
yaml@2.9.1: {}
|
yaml@2.9.1: {}
|
||||||
|
|
||||||
|
yargs-parser@18.1.3:
|
||||||
|
dependencies:
|
||||||
|
camelcase: 5.3.1
|
||||||
|
decamelize: 1.2.0
|
||||||
|
|
||||||
|
yargs@15.4.1:
|
||||||
|
dependencies:
|
||||||
|
cliui: 6.0.0
|
||||||
|
decamelize: 1.2.0
|
||||||
|
find-up: 4.1.0
|
||||||
|
get-caller-file: 2.0.5
|
||||||
|
require-directory: 2.1.1
|
||||||
|
require-main-filename: 2.0.0
|
||||||
|
set-blocking: 2.0.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
which-module: 2.0.1
|
||||||
|
y18n: 4.0.3
|
||||||
|
yargs-parser: 18.1.3
|
||||||
+3
-1
@@ -4,6 +4,7 @@
|
|||||||
// promise resolve → { type, url }
|
// promise resolve → { type, url }
|
||||||
|
|
||||||
import { httpFetch } from '../common/request.js'
|
import { httpFetch } from '../common/request.js'
|
||||||
|
import { getKgAuth } from '../auth/store.js'
|
||||||
|
|
||||||
let mid = ''
|
let mid = ''
|
||||||
|
|
||||||
@@ -16,7 +17,8 @@ const getMid = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getPlayUrl = (hash, albumId) => getMid().then(m => {
|
const getPlayUrl = (hash, albumId) => getMid().then(m => {
|
||||||
const url = `https://wwwapi.kugou.com/yy/index.php?r=play/getdata&hash=${hash}${albumId ? `&album_id=${albumId}` : ''}&mid=${m}`
|
const auth = getKgAuth()
|
||||||
|
const url = `https://wwwapi.kugou.com/yy/index.php?r=play/getdata&hash=${hash}${albumId ? `&album_id=${albumId}` : ''}&mid=${m}${auth ? `&token=${encodeURIComponent(auth.token)}&userid=${auth.userid}` : ''}`
|
||||||
return httpFetch(url, {
|
return httpFetch(url, {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { httpFetch } from '../common/request.js'
|
import { httpFetch } from '../common/request.js'
|
||||||
import { eapi } from '../../musicSdk/wy/utils/crypto.ts'
|
import { eapi } from '../../musicSdk/wy/utils/crypto.ts'
|
||||||
|
import { getWyCookie } from '../auth/store.js'
|
||||||
|
|
||||||
const levelMap = {
|
const levelMap = {
|
||||||
'128k': 'standard',
|
'128k': 'standard',
|
||||||
@@ -21,11 +22,13 @@ export const getMusicUrl = (songInfo, type) => {
|
|||||||
level: levelMap[type] || 'standard',
|
level: levelMap[type] || 'standard',
|
||||||
encodeType: 'flac',
|
encodeType: 'flac',
|
||||||
}
|
}
|
||||||
|
const cookie = getWyCookie()
|
||||||
const request = httpFetch('https://interface3.music.163.com/eapi/song/enhance/player/url/v1', {
|
const request = httpFetch('https://interface3.music.163.com/eapi/song/enhance/player/url/v1', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
origin: 'https://music.163.com',
|
origin: 'https://music.163.com',
|
||||||
|
...(cookie ? { cookie } : {}),
|
||||||
},
|
},
|
||||||
form: eapi('/api/song/enhance/player/url/v1', data),
|
form: eapi('/api/song/enhance/player/url/v1', data),
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
|
|||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
// 酷狗登录 —— token 绑定与验证(网关 android 签名,与 KuGouMusicApi 对齐)
|
||||||
|
// 完整的手机号/扫码登录需移植其 RSA/AES 全套协议,后续按需扩展;
|
||||||
|
// token 可从酷狗 App 抓包获取(login.token)
|
||||||
|
|
||||||
|
import crypto from 'node:crypto'
|
||||||
|
import { httpFetch } from '../common/request.js'
|
||||||
|
import { getCredentials, setCredentials } from './store.js'
|
||||||
|
|
||||||
|
const md5 = s => crypto.createHash('md5').update(s).digest('hex')
|
||||||
|
const SALT_KEY = '57ae12eb6890223e355ccfcb74edf70d'
|
||||||
|
const SALT_SIGN = 'OIlwieks28dk2k092lksi2UIkp'
|
||||||
|
const APPID = 1005
|
||||||
|
const CLIENTVER = 11430
|
||||||
|
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
||||||
|
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDIAG7QOELSYoIJvTFJhMpe1s/gbjDJX51HBNnEl5HXqTW6lQ7LC8jr9fWZTwusknp+sVGzwd40MwP6U5yDE27M/X1+UR4tvOGOqp94TJtQ1EPnWGWXngpeIW5GxoQGao1rmYWAu6oi1z9XkChrsUdC6DJE5E221wf/4WLFxwAtRQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----`
|
||||||
|
|
||||||
|
const calculateMid = (guid) => BigInt(`0x${md5(guid)}`).toString()
|
||||||
|
const randomString = (n) => {
|
||||||
|
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
|
return Array.from({ length: n }, () => chars[Math.floor(Math.random() * chars.length)]).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设备标识持久化在凭据里,保证同一登录态的请求指纹稳定 */
|
||||||
|
const device = () => {
|
||||||
|
const dev = getCredentials('_kg_device') ?? { guid: crypto.randomUUID(), mid: '', dfid: randomString(24) }
|
||||||
|
if (!dev.mid) dev.mid = calculateMid(dev.guid)
|
||||||
|
setCredentials('_kg_device', dev)
|
||||||
|
return dev
|
||||||
|
}
|
||||||
|
|
||||||
|
const androidSign = (params, data = '') => {
|
||||||
|
const s = Object.keys(params).sort()
|
||||||
|
.map(k => `${k}=${typeof params[k] === 'object' ? JSON.stringify(params[k]) : params[k]}`)
|
||||||
|
.join('')
|
||||||
|
return md5(SALT_SIGN + s + data + SALT_SIGN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 网关移动端签名请求(gateway.kugou.com + x-router) */
|
||||||
|
const gatewayRequest = async(url, { params: extraParams, data, router }) => {
|
||||||
|
const dev = device()
|
||||||
|
const mid = dev.mid || (dev.mid = calculateMid(dev.guid))
|
||||||
|
setCredentials('_kg_device', dev)
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
dfid: dev.dfid, mid, uuid: '-', appid: APPID, clientver: CLIENTVER,
|
||||||
|
clienttime: Math.floor(Date.now() / 1000),
|
||||||
|
...extraParams,
|
||||||
|
}
|
||||||
|
const signature = androidSign(params, data ? JSON.stringify(data) : '')
|
||||||
|
const qs = Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k])}`).join('&')
|
||||||
|
const r = await httpFetch(`https://gateway.kugou.com${url}?${qs}&signature=${signature}`, {
|
||||||
|
method: 'post',
|
||||||
|
headers: {
|
||||||
|
dfid: dev.dfid, mid, clienttime: String(params.clienttime),
|
||||||
|
'kg-rc': '1', 'kg-thash': '5d816a0', 'kg-rec': '1', 'kg-rf': 'B9EDA08A64250DEFFBCADDEE00F8F25F',
|
||||||
|
'x-router': router,
|
||||||
|
'User-Agent': 'Android15-1070-11083-46-0-DiscoveryDRADProtocol-wifi',
|
||||||
|
},
|
||||||
|
body: data ?? undefined,
|
||||||
|
timeout: 15000,
|
||||||
|
}).promise
|
||||||
|
return r.body ?? {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 token 并返回账号资料(usercenter /v3/get_my_info)
|
||||||
|
* p = RSA 原始加密(零填充){ token, clienttime }
|
||||||
|
*/
|
||||||
|
export const verifyToken = async({ token, userid }) => {
|
||||||
|
const unixSec = Math.floor(Date.now() / 1000)
|
||||||
|
const payload = Buffer.from(JSON.stringify({ token, clienttime: unixSec }))
|
||||||
|
// 原始 RSA(无填充):左侧零填充到密钥长度(128 字节)
|
||||||
|
const padded = Buffer.concat([Buffer.alloc(128 - payload.length), payload])
|
||||||
|
const p = crypto.publicEncrypt({ key: PUBLIC_KEY, padding: crypto.constants.RSA_NO_PADDING }, padded).toString('hex').toUpperCase()
|
||||||
|
|
||||||
|
const body = await gatewayRequest('/v3/get_my_info', {
|
||||||
|
router: 'usercenter.kugou.com',
|
||||||
|
params: { plat: 1 },
|
||||||
|
data: { visit_time: unixSec, usertype: 1, p, userid: Number(userid) || 0 },
|
||||||
|
})
|
||||||
|
if (body.status !== 1 || !body.data) {
|
||||||
|
const err = new Error(body.error ?? 'token 无效或已过期')
|
||||||
|
err.status = 401
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
userid: body.data.userid ?? Number(userid),
|
||||||
|
nickname: body.data.nickname ?? body.data.username ?? null,
|
||||||
|
vip: body.data.vip ? Boolean(body.data.vip) : null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绑定并验证 kg token */
|
||||||
|
export const loginByToken = async({ token, userid }) => {
|
||||||
|
const profile = await verifyToken({ token, userid })
|
||||||
|
setCredentials('kg', { token, userid: profile.userid, nickname: profile.nickname })
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前 kg 登录资料(未登录返回 null) */
|
||||||
|
export const status = () => {
|
||||||
|
const cred = getCredentials('kg')
|
||||||
|
return cred?.token ? { userid: cred.userid, nickname: cred.nickname ?? null } : null
|
||||||
|
}
|
||||||
Vendored
+29
@@ -0,0 +1,29 @@
|
|||||||
|
// src/auth/store.js 的类型声明(实现为同目录 store.js)
|
||||||
|
export interface WyCredentials {
|
||||||
|
cookie: string
|
||||||
|
musicU: string
|
||||||
|
userid?: number
|
||||||
|
nickname?: string | null
|
||||||
|
avatar?: string | null
|
||||||
|
vipType?: number | null
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KgCredentials {
|
||||||
|
token: string
|
||||||
|
userid: number | string
|
||||||
|
nickname?: string | null
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KgDevice {
|
||||||
|
guid: string
|
||||||
|
mid: string
|
||||||
|
dfid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function getCredentials(platform: 'wy' | 'kg' | '_kg_device'): WyCredentials | KgCredentials | KgDevice | null
|
||||||
|
export declare function setCredentials(platform: 'wy' | 'kg' | '_kg_device', credentials: object): void
|
||||||
|
export declare function clearCredentials(platform: 'wy' | 'kg' | '_kg_device'): void
|
||||||
|
export declare function getWyCookie(): string
|
||||||
|
export declare function getKgAuth(): { token: string; userid: number | string } | null
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// 登录态存储 —— 各平台凭据持久化(data/auth.json,不入库)
|
||||||
|
// 结构:{ wy: { cookie, musicU, userid?, nickname?, updatedAt }, kg: { token, userid, nickname?, updatedAt } }
|
||||||
|
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||||
|
import { dirname, join } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const DATA_FILE = join(dirname(fileURLToPath(import.meta.url)), '../../data/auth.json')
|
||||||
|
|
||||||
|
let cache = null
|
||||||
|
|
||||||
|
const load = () => {
|
||||||
|
if (!cache) {
|
||||||
|
try {
|
||||||
|
cache = existsSync(DATA_FILE) ? JSON.parse(readFileSync(DATA_FILE, 'utf8')) : {}
|
||||||
|
} catch {
|
||||||
|
cache = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = (data) => {
|
||||||
|
cache = data
|
||||||
|
mkdirSync(dirname(DATA_FILE), { recursive: true })
|
||||||
|
writeFileSync(DATA_FILE, JSON.stringify(data, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCredentials = (platform) => load()[platform] ?? null
|
||||||
|
|
||||||
|
export const setCredentials = (platform, credentials) => {
|
||||||
|
const data = load()
|
||||||
|
data[platform] = { ...data[platform], ...credentials, updatedAt: new Date().toISOString() }
|
||||||
|
save(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clearCredentials = (platform) => {
|
||||||
|
const data = load()
|
||||||
|
delete data[platform]
|
||||||
|
save(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** wy 登录 cookie 字符串(未登录返回空串) */
|
||||||
|
export const getWyCookie = () => getCredentials('wy')?.cookie || ''
|
||||||
|
|
||||||
|
/** kg 登录态(未登录返回 null) */
|
||||||
|
export const getKgAuth = () => {
|
||||||
|
const cred = getCredentials('kg')
|
||||||
|
return cred?.token ? { token: cred.token, userid: cred.userid || 0 } : null
|
||||||
|
}
|
||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
// 网易云登录 —— 扫码 / 手机号,weapi 加密请求
|
||||||
|
// 参考实现:NeteaseCloudMusicApi(login_qr_key / login_qr_check / login_cellphone / login_status)
|
||||||
|
|
||||||
|
import crypto from 'node:crypto'
|
||||||
|
import { httpFetch } from '../common/request.js'
|
||||||
|
import { weapi } from '../../musicSdk/wy/utils/crypto.ts'
|
||||||
|
import { getWyCookie, setCredentials } from './store.js'
|
||||||
|
|
||||||
|
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
|
||||||
|
|
||||||
|
const requestWeapi = async(path, data, cookie = getWyCookie()) => {
|
||||||
|
const r = await httpFetch(`https://music.163.com/weapi${path}`, {
|
||||||
|
method: 'post',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': UA,
|
||||||
|
origin: 'https://music.163.com',
|
||||||
|
Referer: 'https://music.163.com/',
|
||||||
|
...(cookie ? { cookie } : {}),
|
||||||
|
},
|
||||||
|
form: weapi(data),
|
||||||
|
timeout: 15000,
|
||||||
|
}).promise
|
||||||
|
if (r.statusCode !== 200) throw new Error('网易请求失败')
|
||||||
|
return { body: r.body, setCookie: r.headers['set-cookie'] ?? [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 明文 api 请求(二维码接口只提供该形态,weapi 形态已 404) */
|
||||||
|
const requestApi = async(path, data) => {
|
||||||
|
const r = await httpFetch(`https://music.163.com${path}`, {
|
||||||
|
method: 'post',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': UA,
|
||||||
|
origin: 'https://music.163.com',
|
||||||
|
Referer: 'https://music.163.com/',
|
||||||
|
},
|
||||||
|
form: data,
|
||||||
|
timeout: 15000,
|
||||||
|
}).promise
|
||||||
|
if (r.statusCode !== 200) throw new Error('网易请求失败')
|
||||||
|
return { body: r.body, setCookie: r.headers['set-cookie'] ?? [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 Set-Cookie 数组提取登录 cookie 串与 MUSIC_U */
|
||||||
|
const extractLoginCookie = (setCookie) => {
|
||||||
|
const joined = setCookie.map(c => c.split(';')[0]).filter(Boolean).join('; ')
|
||||||
|
const musicU = /MUSIC_U=([^;]+)/.exec(joined)?.[1]
|
||||||
|
return { cookie: joined, musicU }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录成功后拉取账号资料 */
|
||||||
|
const fetchProfile = async() => {
|
||||||
|
const { body } = await requestWeapi('/api/w/nuser/account/get', {})
|
||||||
|
return {
|
||||||
|
userid: body?.account?.id ?? null,
|
||||||
|
nickname: body?.profile?.nickname ?? null,
|
||||||
|
avatar: body?.profile?.avatarUrl ?? null,
|
||||||
|
vipType: body?.profile?.vipType ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 应用登录 cookie(成功后调用) */
|
||||||
|
const applyLogin = async(setCookie) => {
|
||||||
|
const { cookie, musicU } = extractLoginCookie(setCookie)
|
||||||
|
if (!musicU) throw new Error('登录响应中缺少 MUSIC_U')
|
||||||
|
const profile = await fetchProfile()
|
||||||
|
setCredentials('wy', { cookie, musicU, ...profile })
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建扫码登录 key 与二维码内容 */
|
||||||
|
export const qrKey = async() => {
|
||||||
|
const { body } = await requestApi('/api/login/qrcode/unikey', { type: 3 })
|
||||||
|
if (body?.code !== 200 || !body.unikey) throw new Error('获取二维码 key 失败')
|
||||||
|
const key = body.unikey
|
||||||
|
const qrurl = `https://music.163.com/login?codekey=${key}`
|
||||||
|
const qrimg = await import('qrcode').then(m => m.default.toDataURL(qrurl)).catch(() => '')
|
||||||
|
return { key, qrurl, qrimg }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询扫码状态
|
||||||
|
* @returns {{ code: 800|801|802|803, message?: string, profile? }} 800 过期 / 801 等待 / 802 已扫码 / 803 已授权(完成登录)
|
||||||
|
*/
|
||||||
|
export const qrCheck = async(key) => {
|
||||||
|
const { body, setCookie } = await requestApi('/api/login/qrcode/client/login', { key, type: 3 })
|
||||||
|
if (body.code === 803) {
|
||||||
|
const profile = await applyLogin(setCookie)
|
||||||
|
return { code: 803, message: '登录成功', profile }
|
||||||
|
}
|
||||||
|
const messages = { 800: '二维码已过期', 801: '等待扫码', 802: '已扫码,待确认' }
|
||||||
|
return { code: body.code, message: messages[body.code] ?? body.message ?? '未知状态' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号登录(密码为明文,传输前本地 MD5;或直接传验证码 captcha)
|
||||||
|
*/
|
||||||
|
export const cellphoneLogin = async({ phone, password, captcha, countrycode = '86' }) => {
|
||||||
|
const data = {
|
||||||
|
type: '1',
|
||||||
|
https: 'true',
|
||||||
|
phone,
|
||||||
|
countrycode,
|
||||||
|
remember: 'true',
|
||||||
|
[captcha ? 'captcha' : 'password']: captcha ?? crypto.createHash('md5').update(password ?? '').digest('hex'),
|
||||||
|
}
|
||||||
|
const { body, setCookie } = await requestWeapi('/api/w/login/cellphone', data)
|
||||||
|
if (body.code !== 200) {
|
||||||
|
const err = new Error(body.message ?? body.msg ?? '登录失败')
|
||||||
|
err.status = 401
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
const profile = await applyLogin(setCookie)
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前账号资料(未登录返回 null) */
|
||||||
|
export const status = async() => {
|
||||||
|
if (!getWyCookie()) return null
|
||||||
|
return fetchProfile()
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
// 登录路由 —— wy 扫码/手机号登录,kg token 绑定,凭据持久化于 data/auth.json
|
||||||
|
// 注意:成功响应返回裸数据对象,全局 preSerialization 钩子会统一包成 { code: 0, data }
|
||||||
|
|
||||||
|
import * as wyAuth from './auth/wy.js'
|
||||||
|
import * as kgAuth from './auth/kg.js'
|
||||||
|
import { clearCredentials, getCredentials } from './auth/store.js'
|
||||||
|
|
||||||
|
const fail = (reply, err) => {
|
||||||
|
const status = err.status ?? 502
|
||||||
|
return reply.code(status).send({ code: status, msg: err.message })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册登录相关路由
|
||||||
|
* @param {import('fastify').FastifyInstance} fastify
|
||||||
|
*/
|
||||||
|
export const loginRoutes = async(fastify) => {
|
||||||
|
// ---------- 网易云 ----------
|
||||||
|
|
||||||
|
fastify.post('/login/wy/qr', {
|
||||||
|
schema: {
|
||||||
|
summary: '获取网易云扫码登录二维码',
|
||||||
|
description: '用网易云 App 扫码后轮询 /login/wy/qr/check 完成登录。返回 qrimg(data URL)可直接展示。',
|
||||||
|
tags: ['登录'],
|
||||||
|
},
|
||||||
|
}, async() => wyAuth.qrKey())
|
||||||
|
|
||||||
|
fastify.post('/login/wy/qr/check', {
|
||||||
|
schema: {
|
||||||
|
summary: '轮询网易云扫码状态',
|
||||||
|
description: 'code:800 二维码过期 / 801 等待扫码 / 802 已扫码待确认 / 803 登录成功(自动保存凭据并返回账号资料)。',
|
||||||
|
tags: ['登录'],
|
||||||
|
body: { type: 'object', properties: { key: { type: 'string' } }, required: ['key'] },
|
||||||
|
},
|
||||||
|
}, async(request, reply) => {
|
||||||
|
try {
|
||||||
|
return await wyAuth.qrCheck(request.body.key)
|
||||||
|
} catch (err) {
|
||||||
|
return fail(reply, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fastify.post('/login/wy/cellphone', {
|
||||||
|
schema: {
|
||||||
|
summary: '网易云手机号登录',
|
||||||
|
description: 'password(明文,服务端 MD5 后提交)与 captcha(短信验证码)二选一。',
|
||||||
|
tags: ['登录'],
|
||||||
|
body: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
phone: { type: 'string' },
|
||||||
|
password: { type: 'string' },
|
||||||
|
captcha: { type: 'string' },
|
||||||
|
countrycode: { type: 'string', default: '86' },
|
||||||
|
},
|
||||||
|
required: ['phone'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, async(request, reply) => {
|
||||||
|
try {
|
||||||
|
return await wyAuth.cellphoneLogin(request.body)
|
||||||
|
} catch (err) {
|
||||||
|
return fail(reply, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fastify.delete('/login/wy', {
|
||||||
|
schema: { summary: '退出网易云登录', tags: ['登录'] },
|
||||||
|
}, async() => {
|
||||||
|
clearCredentials('wy')
|
||||||
|
return { logged: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------- 酷狗 ----------
|
||||||
|
|
||||||
|
fastify.post('/login/kg', {
|
||||||
|
schema: {
|
||||||
|
summary: '绑定酷狗 token',
|
||||||
|
description: 'token 从酷狗 App 抓包获取(login.token),服务端向用户中心验证后存储。手机号/扫码登录暂未实现。',
|
||||||
|
tags: ['登录'],
|
||||||
|
body: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
token: { type: 'string' },
|
||||||
|
userid: { type: ['integer', 'string'] },
|
||||||
|
},
|
||||||
|
required: ['token', 'userid'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, async(request, reply) => {
|
||||||
|
try {
|
||||||
|
return await kgAuth.loginByToken(request.body)
|
||||||
|
} catch (err) {
|
||||||
|
return fail(reply, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fastify.delete('/login/kg', {
|
||||||
|
schema: { summary: '退出酷狗登录', tags: ['登录'] },
|
||||||
|
}, async() => {
|
||||||
|
clearCredentials('kg')
|
||||||
|
return { logged: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------- 通用 ----------
|
||||||
|
|
||||||
|
fastify.get('/login/status', {
|
||||||
|
schema: {
|
||||||
|
summary: '查看登录状态',
|
||||||
|
description: 'wy 为实时校验(未登录或 cookie 失效返回 logged: false),kg 为已存凭据。',
|
||||||
|
tags: ['登录'],
|
||||||
|
},
|
||||||
|
}, async() => {
|
||||||
|
const [wy, kg] = await Promise.all([wyAuth.status().catch(() => null), Promise.resolve(kgAuth.status())])
|
||||||
|
return {
|
||||||
|
wy: wy ? { logged: true, ...wy } : { logged: false },
|
||||||
|
kg: kg ? { logged: true, ...kg } : { logged: false },
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import { dirname, join } from 'node:path'
|
|||||||
import musicSdk from '../musicSdk/index.js'
|
import musicSdk from '../musicSdk/index.js'
|
||||||
import { ApiError } from './http/api-error.js'
|
import { ApiError } from './http/api-error.js'
|
||||||
import { musicRoutes } from './routes.js'
|
import { musicRoutes } from './routes.js'
|
||||||
|
import { loginRoutes } from './routes-login.js'
|
||||||
import { COMPONENT_SCHEMAS } from './openapi/components.js'
|
import { COMPONENT_SCHEMAS } from './openapi/components.js'
|
||||||
import { redocHtml } from './openapi/ui.js'
|
import { redocHtml } from './openapi/ui.js'
|
||||||
|
|
||||||
@@ -144,6 +145,7 @@ fastify.register(swagger, {
|
|||||||
{ name: '榜单', description: '排行榜' },
|
{ name: '榜单', description: '排行榜' },
|
||||||
{ name: '歌单', description: '歌单与热搜' },
|
{ name: '歌单', description: '歌单与热搜' },
|
||||||
{ name: '歌手专辑', description: '歌手信息、歌曲与专辑详情' },
|
{ name: '歌手专辑', description: '歌手信息、歌曲与专辑详情' },
|
||||||
|
{ name: '登录', description: '账号登录(wy 扫码/手机号、kg token)与状态' },
|
||||||
{ name: '系统', description: '健康检查、平台信息、文档' },
|
{ name: '系统', description: '健康检查、平台信息、文档' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -180,6 +182,7 @@ fastify.get('/', {
|
|||||||
// ---- 业务路由 ----
|
// ---- 业务路由 ----
|
||||||
|
|
||||||
fastify.register(musicRoutes)
|
fastify.register(musicRoutes)
|
||||||
|
fastify.register(loginRoutes)
|
||||||
|
|
||||||
export const startServer = async(port = PORT) => {
|
export const startServer = async(port = PORT) => {
|
||||||
await fastify.listen({ port, host: '0.0.0.0' })
|
await fastify.listen({ port, host: '0.0.0.0' })
|
||||||
|
|||||||
@@ -42,6 +42,25 @@ const postRaw = (path) => new Promise((resolve, reject) => {
|
|||||||
req.end()
|
req.end()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// JSON POST(登录等写入接口)
|
||||||
|
const postJson = (path, body) => new Promise((resolve, reject) => {
|
||||||
|
const payload = JSON.stringify(body)
|
||||||
|
const req = http.request(`${BASE}${path}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
||||||
|
}, res => {
|
||||||
|
let data = ''
|
||||||
|
res.on('data', c => { data += c })
|
||||||
|
res.on('end', () => resolve({
|
||||||
|
status: res.statusCode,
|
||||||
|
body: (() => { try { return JSON.parse(data) } catch { return null } })(),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
req.on('error', reject)
|
||||||
|
req.write(payload)
|
||||||
|
req.end()
|
||||||
|
})
|
||||||
|
|
||||||
const test = async(name, fn) => {
|
const test = async(name, fn) => {
|
||||||
try {
|
try {
|
||||||
const result = await fn()
|
const result = await fn()
|
||||||
@@ -360,6 +379,28 @@ await test('Swagger UI 规范端点 /docs/json(不可包信封)', async() =>
|
|||||||
return spec.openapi
|
return spec.openapi
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---- 登录 ----
|
||||||
|
|
||||||
|
await test('登录状态 /login/status', async() => {
|
||||||
|
const r = await get('/login/status')
|
||||||
|
if (r.code != 0) throw new Error(r.msg)
|
||||||
|
if (typeof r.data.wy?.logged !== 'boolean' || typeof r.data.kg?.logged !== 'boolean') throw new Error('响应不完整')
|
||||||
|
return `wy:${r.data.wy.logged} kg:${r.data.kg.logged}`
|
||||||
|
})
|
||||||
|
|
||||||
|
await test('扫码二维码创建 POST /login/wy/qr', async() => {
|
||||||
|
const r = await postJson('/login/wy/qr', {})
|
||||||
|
if (r.status !== 200 || r.body?.code !== 0) throw new Error(r.body?.msg ?? `HTTP ${r.status}`)
|
||||||
|
if (!r.body.data?.key || !r.body.data?.qrurl || !r.body.data?.qrimg) throw new Error('响应不完整')
|
||||||
|
return r.body.data.key.slice(0, 18) + '…'
|
||||||
|
})
|
||||||
|
|
||||||
|
await test('kg 假 token 拒绝 POST /login/kg', async() => {
|
||||||
|
const r = await postJson('/login/kg', { token: 'smoke_fake_token', userid: 0 })
|
||||||
|
if (r.status !== 401) throw new Error(`期望 401,实际 ${r.status}`)
|
||||||
|
return r.body?.msg
|
||||||
|
})
|
||||||
|
|
||||||
// 汇总
|
// 汇总
|
||||||
const passed = results.filter(r => r.ok).length
|
const passed = results.filter(r => r.ok).length
|
||||||
const failed = results.filter(r => !r.ok)
|
const failed = results.filter(r => !r.ok)
|
||||||
|
|||||||
Reference in new issue
Block a user