Compare commits

...
28 Commits
Author SHA1 Message Date
MoeFurina 7822ab08d3 feat: 新增评论相关接口 2026-07-22 19:18:18 +08:00
Takanashi Hoshino 41bd6d82ce Merge pull request #216 from Suxiaoqinx/main
feat(song/url-v1): 新增immerseType参数支持并更新文档
2026-07-19 21:20:40 +08:00
苏晓晴 6f8dd1d971 删除 package-lock.json 2026-07-19 17:45:39 +08:00
Suxiaoqinx ddd0af5474 修复(song_url_v1): 删除非sky等级的immerseType处理逻辑
移除原代码中非sky等级下处理query.immerseType的代码分支,仅在level为sky时设置immerseType参数
2026-07-19 10:04:08 +08:00
Suxiaoqinx fbd2e06419 feat(song/url-v1): 新增immerseType参数支持并更新文档
补充sky音质下immerseType参数的处理逻辑,更新文档说明与调用示例,注明参数仅在level为sky时生效
2026-07-19 09:54:55 +08:00
MoeFurina 35d1c61cb4 revert: some changes 2026-07-17 20:41:49 +08:00
MoeFurina a183e153ef Merge branch 'pr/214' 2026-07-17 20:11:37 +08:00
MoeFurina 2fa78c3826 fix: 修复评论接口 2026-07-17 20:11:20 +08:00
Zxwy 5ae1eab14c feat: 新增云小编接口 2026-07-17 19:02:16 +08:00
MoeFurina 321c25bd7d style: 简化createOption 2026-07-15 21:06:50 +08:00
MoeFurina f527c1ef73 docs: 新增免费听接口文档 2026-07-15 16:23:25 +08:00
MoeFurina 0a5a1242f2 feat: 新增免费听接口 2026-07-15 16:14:19 +08:00
MoeFurina 6946dc8e14 docs: 修改运行要求 2026-07-12 10:42:50 +08:00
MoeFurina 22b78779ca Merge branch 'main' of https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced 2026-07-11 17:20:25 +08:00
MoeFurina 35607f5050 perf: 改进api调试页面 2026-07-11 17:20:14 +08:00
Takanashi Hoshino 60af95ac06 Merge pull request #210 from laoshuikaixue/feature/artist-new-song-playall
feat(artist): 添加关注歌手新歌播放全部&新作品接口
2026-07-11 17:04:55 +08:00
LaoShui 46073e5f50 docs(docs): 更新 sourceType 参数说明
- 明确 `1` 表示新歌,其他取值待确认
- 添加默认值 `1` 的标注
2026-07-10 23:07:31 +08:00
LaoShui bf12abe2f1 feat(artist): 添加关注歌手新作品(歌曲/MV)接口
- 新增 module/artist_new_song_mv_list_v2.js 模块实现接口逻辑
- 在文档中添加接口说明、参数详情和调用示例
- 在 TypeScript 定义文件中添加对应的接口类型声明
- 支持分页获取关注歌手的新歌曲和 MV 列表
- 实现时间游标和翻页功能支持
- 提供多种可选参数配置选项
2026-07-10 19:20:54 +08:00
LaoShui 1491b91829 feat(artist): 添加关注歌手新歌播放全部功能
- 新增 module/artist_new_song_playall.js 文件实现获取所有关注歌手最近50首新歌的功能
- 在文档中添加关注歌手最近新歌播放全部接口说明和调用示例
- 在类型定义文件中添加 artist_new_song_playall 函数声明
2026-07-10 18:56:50 +08:00
MoeFurina 63f669d7f8 fix: 黑胶乐签签到失败 (#197) 2026-06-30 14:34:58 +08:00
Furina's Little Ocean Act Theater d525204dda Merge pull request #199 from zhuixingzhe-baisheng/add-vip-growthpoint-getall
feat: add vip_growthpoint_getall - one-click claim all VIP growth rew…
2026-06-29 16:14:42 +08:00
Furina's Little Ocean Act Theater 6eb31984fd Merge pull request #200 from floatwoozy/feat/listen-data-song-play-rank
feat: 新增 listen_data_song_play_rank 歌曲播放排行接口
2026-06-29 16:14:11 +08:00
MoeFurina 9faeb0aa25 docs: 删减文档内容 2026-06-29 15:53:01 +08:00
floatwoozy f6db3e76e9 feat: 新增 listen_data_song_play_rank 歌曲播放排行接口
新增 /api/content/activity/listen/data/song/play/rank,返回某周/月
听歌排行(每首带 playCount),默认 Top20、不足则返回全部。与已有的
listen_data_today_song(今日)同族,补全「带次数的历史周/月榜」。

- module/listen_data_song_play_rank.js:type(week|month) + endTime 参数
- interface.d.ts:补类型声明
- public/docs/home.md:补接口文档
2026-06-25 03:25:20 +08:00
MonkeyCode Agent 9163d48f0a feat: add vip_growthpoint_getall - one-click claim all VIP growth rewards (xeapi)
- New module: module/vip_growthpoint_getall.js
- Uses xeapi encryption for direct  endpoint
- No parameters required, claims all completed task rewards at once
- Added TypeScript type declaration in interface.d.ts
- Added API documentation in public/docs/home.md
2026-06-21 05:24:35 +00:00
MoeFurina 1454ee216c fix: 请求异常 (83ee2cda34) 2026-06-20 23:28:08 +08:00
MoeFurina 83ee2cda34 feat: add NCBL encrypted scrobble interface and update related documentation
- Implemented new scrobble endpoint for NCBL encrypted logs in `scrobble_v1.js`.
- Updated `scrobble.js` to use domain from config.
- Enhanced `scrobble.html` with tabbed interface for original and NCBL versions, including new input fields.
- Added cookie handling and quick fill functionality for both versions.
- Updated `config.json` to include new domain configurations.
- Introduced `ncbl.js` for NCBL encryption utilities.
- Improved documentation in `home.md` to reflect changes in API usage.
2026-06-20 22:51:51 +08:00
MoeFurina 3c352b5752 feat: 新增听歌打卡示例页面 2026-06-20 21:16:14 +08:00
48 changed files with 3909 additions and 327 deletions
+4 -2
View File
@@ -45,14 +45,14 @@
### 环境要求
- Node.js 18 及以上
- Node.js 22 及以上
- 推荐使用 pnpm 进行依赖管理
### 安装
```bash
git clone https://github.com/neteasecloudmusicapienhanced/api-enhanced.git
cd api
cd api-enhanced
pnpm i
```
@@ -225,6 +225,8 @@ pnpm test
[chaunsin/netease-cloud-music](https://github.com/chaunsin/netease-cloud-music)
[folltoshe/netease-report-listen-song](https://github.com/folltoshe/netease-report-listen-song)
### SDK 生态
| 语言 | 作者 | 地址 | 类型 |
+25
View File
@@ -1474,6 +1474,20 @@ export function artist_new_song(
} & RequestBaseConfig,
): Promise<Response>
export function artist_new_song_playall(
params: RequestBaseConfig,
): Promise<Response>
export function artist_new_song_mv_list_v2(
params: {
startTimestamp?: number | string
before?: number | string
sourceType?: number | string
limit?: number | string
firstRequest?: boolean | string
} & RequestBaseConfig,
): Promise<Response>
export function artist_detail(
params: {
id: number | string
@@ -1594,6 +1608,10 @@ export function vip_growthpoint_get(
} & RequestBaseConfig,
): Promise<Response>
export function vip_growthpoint_getall(
params: RequestBaseConfig,
): Promise<Response>
export function artist_fans(
params: { id: number | string } & MultiPageConfig & RequestBaseConfig,
): Promise<Response>
@@ -2049,6 +2067,13 @@ export function listen_data_report(
} & RequestBaseConfig,
): Promise<Response>
export function listen_data_song_play_rank(
params: {
type?: string
endTime?: string | number
} & RequestBaseConfig,
): Promise<Response>
export function listen_data_today_song(
params: RequestBaseConfig,
): Promise<Response>
+36
View File
@@ -0,0 +1,36 @@
// 获取广告
const createOption = require('../util/option.js')
module.exports = async (query, request) => {
const data = {
type_ids: query.type_ids || '["400002_0"]',
}
const option = createOption(query, 'xeapi', 'v3')
const res = await request(`/api/ad/get`, data, option)
const raw = res.body
// 提取广告中的 req_id
let reqId = ''
try {
if (raw?.ads) {
const ad = Object.values(raw.ads)[0]
if (ad?.extJson) {
const ext = JSON.parse(ad.extJson)
reqId = ext?.contextInfo?.req_id || ''
}
}
} catch (_) {}
return {
status: 200,
body: {
code: 200,
ads: raw?.ads || null,
message: raw?.message || null,
extra: { reqId },
},
}
}
+88
View File
@@ -0,0 +1,88 @@
// 看广告免费听歌 - 领取免费听权益
// 请求流程(基于逆向网易云音乐 v9.5.45 RN Hermes 源码):
// 1. 从广告平台拉广告 → 用户看完/点击广告 → 获取 ad 对象的 extJson.contextInfo.req_id 作为 reqUid
// 2. 调用本接口传入 reqUid 及相关权益参数,领取免费听权益
// 3. 服务器返回 gainFlag 等标识,用于展示领取结果
const createOption = require('../util/option.js')
const adGet = require('./ad_get.js')
module.exports = async (query, request) => {
const time = Date.now()
// 如果未传入 reqUid,自动从 ad_get 获取最新广告的 req_id
let reqUid = query.reqUid || ''
if (!reqUid) {
try {
const adRes = await adGet(
{ ...query, type_ids: query.type_ids || '["400002_0"]' },
request,
)
reqUid = adRes?.body?.extra?.reqId || ''
console.log(`自动获取 reqUid: ${reqUid}`)
} catch (e) {
// 获取广告失败,后续请求会因缺少 reqUid 被拒绝
}
}
const rightsParam = {
// 必填: 广告请求 ID,自动从 ad_get 获取
reqUid,
// 广告创意类型 (默认 1)
creativeType: parseInt(query.creativeType || 2),
// 时间戳
exposureTime: query.exposureTime || time,
clickTime: query.clickTime || time,
// 权益领取方式
rightsGainMethod: parseInt(query.rightsGainMethod || 2),
// 权益时长相关
rightsGainDuration: query.rightsGainDuration
? parseInt(query.rightsGainDuration)
: undefined,
extraRightsGainMethod: query.extraRightsGainMethod
? parseInt(query.extraRightsGainMethod)
: undefined,
extraRightsGainDuration: query.extraRightsGainDuration
? parseInt(query.extraRightsGainDuration)
: undefined,
nextRightsGainDuration: query.nextRightsGainDuration
? parseInt(query.nextRightsGainDuration)
: undefined,
// 来源标识 | 权益扩展信息
source: query.source || undefined,
rightsExtJson: query.rightsExtJson || undefined,
// 应用信息(下载类广告)
appInfo: query.appInfo ? JSON.parse(query.appInfo) : undefined,
installed: query.installed ? parseInt(query.installed) : undefined,
}
// 清理 undefined 字段
Object.keys(rightsParam).forEach((key) => {
if (rightsParam[key] === undefined) delete rightsParam[key]
})
// 将参数序列化为 reqParam 字符串 (与 RN 源码完全一致)
const data = {
reqParam: JSON.stringify(rightsParam),
}
const res = await request(
`/api/ad/listening/rights/gain`,
data,
createOption(query, 'xeapi', 'v3'),
)
return {
status: 200,
body: {
code: 200,
data: res.body,
},
}
}
+16
View File
@@ -0,0 +1,16 @@
// 获取关注歌手的新歌曲和 MV
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
startTimestamp: query.startTimestamp || query.before || Date.now(),
sourceType: query.sourceType || 1,
limit: query.limit || 10,
firstRequest: query.firstRequest ?? true,
}
return request(
`/api/sub/artist/new/works/song-mv/list/v2`,
data,
createOption(query, 'eapi'),
)
}
+10
View File
@@ -0,0 +1,10 @@
// 获取所有关注歌手最近的 50 首新歌
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(
`/api/sub/artist/new/works/song/playall`,
{},
createOption(query, 'eapi'),
)
}
+13
View File
@@ -0,0 +1,13 @@
// 发送安全验证码
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
ctcode: query.ctcode || '86',
}
return request(
`/api/sms/captcha/safe/sent`,
data,
createOption(query, 'eapi'),
)
}
+16
View File
@@ -0,0 +1,16 @@
// 发送验证码
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
ctcode: query.ctcode || '86',
secrete: 'music_middleuser_pclogin',
cellphone: query.phone,
scene: '0',
}
return request(
`/api/middle/captcha/sent/v1`,
data,
createOption(query, 'eapi'),
)
}
+5 -1
View File
@@ -6,5 +6,9 @@ module.exports = (query, request) => {
cellphone: query.phone,
countrycode: query.countrycode,
}
return request(`/api/cellphone/existence/check`, data, createOption(query))
return request(
`/api/cellphone/existence/check`,
data,
createOption(query, 'eapi'),
)
}
+1 -1
View File
@@ -25,6 +25,6 @@ module.exports = (query, request) => {
return request(
`/api/resource/comments/${query.t}`,
data,
createOption(query, 'weapi'),
createOption(query, 'eapi', 'v2'),
)
}
+18
View File
@@ -0,0 +1,18 @@
const { resourceTypeMap } = require('../util/config.json')
// 发送评论
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
threadId: resourceTypeMap[query.type] + query.id,
content: query.content,
resourceType: '0',
expressionPicId: '-1',
bubbleId: '-1',
}
return request(
`/api/resource/comments/add`,
data,
createOption(query, 'xeapi', 'v3'),
)
}
+15
View File
@@ -0,0 +1,15 @@
const { resourceTypeMap } = require('../util/config.json')
// 删除评论
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
commentId: query.cid,
threadId: resourceTypeMap[query.type] + query.id,
}
return request(
`/api/resource/comments/delete`,
data,
createOption(query, 'xeapi'),
)
}
+17
View File
@@ -0,0 +1,17 @@
const { resourceTypeMap } = require('../util/config.json')
// 发送评论
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
threadId: resourceTypeMap[query.type] + query.id,
commentId: query.cid,
content: query.content,
resourceType: '0',
}
return request(
`/api/v1/resource/comments/reply`,
data,
createOption(query, 'xeapi', 'v3'),
)
}
+14
View File
@@ -0,0 +1,14 @@
// 强制下线设备
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
key: query.deviceKey,
captcha: query.captcha || '',
}
return request(
`/api/middle/user/security/device/kickoff`,
data,
createOption(query, 'eapi'),
)
}
+13
View File
@@ -0,0 +1,13 @@
// 登录设备列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
excStatus: '9',
}
return request(
`/api/middle/user/device/list`,
data,
createOption(query, 'eapi'),
)
}
+12
View File
@@ -0,0 +1,12 @@
// 听歌足迹 - 歌曲播放排行 (Top20)
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(
`/api/content/activity/listen/data/song/play/rank`,
{
type: query.type || 'month', //周 week 月 month
endTime: query.endTime, // 不填就是本周/月的
},
createOption(query),
)
}
+1
View File
@@ -14,6 +14,7 @@ module.exports = async (query, request) => {
? query.captcha
: query.md5_password || CryptoJS.MD5(query.password).toString(),
remember: 'true',
secureCaptcha: query.sca || '',
}
let result = await request(
`/api/w/login/cellphone`,
+1 -1
View File
@@ -9,6 +9,6 @@ module.exports = (query, request) => {
? { checkToken: query.checkToken || APP_CONF.checkToken }
: {}),
}
query.checkToken = true // 强制开启checkToken
query.checkToken = 'v2' // 强制开启checkToken
return request(`/api/playlist/${path}`, data, createOption(query, 'eapi'))
}
+38
View File
@@ -0,0 +1,38 @@
// 易盾反作弊 Token 注册端点
// 调用后获取实时 token 并存入共享存储,供后续带 checkToken 的请求使用
//
// GET /register/checktoken → 返回当前 token(尚无则自动获取)
// POST /register/checktoken → 强制刷新 token
// GET /register/checktoken?refresh=1 → 强制刷新
//
const { default: axios } = require('axios')
const { APP_CONF } = require('../util/config.json')
const URL = APP_CONF.dunDomainV2 + '/v2/config/js?pn=YD00000558929251'
let _token = ''
async function fetch() {
const res = await axios.get(URL, { timeout: 10000 })
const data = res.data
if (data && data.code === 200 && data.result && data.result.conf) {
return data.result.conf
}
throw new Error('易盾返回异常: ' + JSON.stringify(data).substring(0, 200))
}
// 端点处理
module.exports = async (query) => {
const refresh = query.refresh === '1' || query.refresh === 'true'
let token = refresh ? null : _token
if (!token) {
token = await fetch()
_token = token
}
return {
status: 200,
body: { code: 200, token, registered: !!token },
}
}
// 给 request.js 读取用
module.exports.getToken = () => _token
+37
View File
@@ -0,0 +1,37 @@
// 易盾反作弊 Token 注册端点
// 调用后获取实时 token 并存入共享存储,供后续带 checkToken 的请求使用
//
// GET /register/checktoken → 返回当前 token(尚无则自动获取)
// POST /register/checktoken → 强制刷新 token
// GET /register/checktoken?refresh=1 → 强制刷新
//
const { default: axios } = require('axios')
const { APP_CONF } = require('../util/config.json')
const URL = APP_CONF.dunDomainV3 + '/v3/b?pn=YD00000558929251'
let _token = ''
async function fetch() {
const res = await axios.get(URL, { timeout: 10000 })
const body = String(res.data)
const m = body.match(/null\(\[(\d+),\d+,\"([^\"]+)\"\]\)/)
if (m && m[1] === '200') return m[2]
throw new Error('易盾返回异常: ' + body.substring(0, 100))
}
// 端点处理
module.exports = async (query) => {
const refresh = query.refresh === '1' || query.refresh === 'true'
let token = refresh ? null : _token
if (!token) {
token = await fetch()
_token = token
}
return {
status: 200,
body: { code: 200, token, registered: !!token },
}
}
// 给 request.js 读取用
module.exports.getToken = () => _token
+17
View File
@@ -0,0 +1,17 @@
// 云小编领取任务积分
//
// activityId:
// 调用 rep/ugc/activity/get 获取
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
activityId: query.activityId || '5001',
}
return request(
`/api/rep/ugc/activity/collect`,
data,
createOption(query, 'eapi'),
)
}
+7
View File
@@ -0,0 +1,7 @@
// 云小编活动信息
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(`/api/rep/ugc/activity/get`, {}, createOption(query, 'eapi'))
}
+16
View File
@@ -0,0 +1,16 @@
// 云小编领取一日会员
//
// 注:前提条件见 rep/ugc/user/vip
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
activityId: query.activityId || '5001',
}
return request(
`/api/rep/ugc/user/collect-vip`,
data,
createOption(query, 'eapi'),
)
}
+7
View File
@@ -0,0 +1,7 @@
// 云小编获取用户详情
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(`/api/rep/ugc/user/get`, {}, createOption(query, 'eapi'))
}
+7
View File
@@ -0,0 +1,7 @@
// 云小编每日签到
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(`/api/rep/ugc/user/sign`, {}, createOption(query, 'eapi'))
}
+12
View File
@@ -0,0 +1,12 @@
// 云小编查询会员任务状态
//
// 状态 (data.status)
// 10: 用户积分达50,可免费领取1日黑胶会员
// 20: 用户积分已达50,可免费领取1日黑胶会员
// 30: 已领取1日黑胶会员,明天再来吧~
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(`/api/rep/ugc/user/vip`, {}, createOption(query, 'eapi'))
}
+3 -1
View File
@@ -1,6 +1,8 @@
// 听歌打卡
const createOption = require('../util/option.js')
const { APP_CONF } = require('../util/config.json')
const DOMAIN = APP_CONF.clDomian
module.exports = async (query, request) => {
// 注入 os=osx 的 cookie
let cookie = query.cookie || ''
@@ -56,7 +58,7 @@ module.exports = async (query, request) => {
}
const option = createOption(query, 'eapi')
option.domain = 'https://clientlog.music.163.com'
option.domain = DOMAIN
// 发送两次请求
const res1 = await request(`/api/feedback/weblog`, startplayData, option)
+128
View File
@@ -0,0 +1,128 @@
// 听歌打卡 - NCBL 加密版 (仿桌面客户端 PLV/PLD 上报)
// 复制自 https://github.com/folltoshe/netease-report-listen-song 的 PC 端日志上报方式
//
// PLV 和 PLD 分两次独立上传
const {
buildPlv,
buildPld,
buildRecords,
extractContext,
parseCookie,
buildCookieStr,
buildMetaJson,
doUpload,
} = require('../util/ncbl')
module.exports = async (query, request) => {
// --- 参数校验 ---
const songId = Number(query.id)
if (!songId || isNaN(songId)) {
return { status: 400, body: { code: 400, msg: '缺少有效的 id (歌曲ID)' } }
}
const playTime = Number(query.time)
if (isNaN(playTime) || playTime <= 0) {
return {
status: 400,
body: { code: 400, msg: '缺少有效的 time (播放时长)' },
}
}
const totalTime = Number(query.total) || playTime
const sourceId = String(query.sourceid || query.sourceId || '')
const sourceName = query.source || 'list'
// --- 解析认证上下文 ---
const rawCookie = query.cookie || ''
const cookieObj = parseCookie(rawCookie)
cookieObj.os = 'pc'
const ctx = extractContext(cookieObj)
// 兜底取 token
if (!ctx.auth.token && rawCookie) {
const parsed =
typeof rawCookie === 'string' ? parseCookie(rawCookie) : rawCookie
ctx.auth.token = parsed.MUSIC_U || ''
}
if (!ctx.auth.token) {
return { status: 401, body: { code: 401, msg: '缺少 MUSIC_U 鉴权令牌' } }
}
// --- 构建歌曲和来源 ---
const song = {
id: songId,
name: query.name || '',
artist: query.artist || '',
bitrate: Number(query.bitrate) || 320,
level: query.level || 'exhigh',
vip: query.vip === 'true' || query.vip === true,
time: totalTime,
}
const source = {
id: sourceId || String(songId),
type: 'track',
name: sourceName,
}
const metaJson = buildMetaJson(ctx)
const cookieStr = buildCookieStr(ctx)
const ts = Math.floor(Date.now() / 1000)
const played = Math.min(playTime, totalTime)
const plvBody = buildRecords([
{ time: ts, action: '_plv', data: buildPlv(ctx, song, source) },
])
const pldBody = buildRecords([
{ time: ts, action: '_pld', data: buildPld(ctx, song, source, played) },
])
try {
// 1) 上传 PLV
const plv = await doUpload(ctx, metaJson, plvBody, cookieStr, 'PLV')
if (!plv.success) {
const rateMsg =
plv.respBody?.data?.rate != null
? ` (rate=${plv.respBody.data.rate})`
: ''
return {
status: 200,
body: {
code: plv.respBody?.code || -1,
msg: `PLV 上报失败${rateMsg}`,
details: plv.respBody,
},
}
}
// 2) 上传 PLD
const pld = await doUpload(ctx, metaJson, pldBody, cookieStr, 'PLD')
if (!pld.success) {
return {
status: 200,
body: {
code: pld.respBody?.code || -1,
msg: 'PLV 成功但 PLD 失败',
details: { plv: plv.respBody, pld: pld.respBody },
},
}
}
return {
status: 200,
body: {
code: 200,
data: 'scrobble_v1 上报成功',
details: {
plv: { fileName: plv.fileName, payloadSize: plv.payload.length },
pld: { fileName: pld.fileName, payloadSize: pld.payload.length },
},
},
}
} catch (err) {
return {
status: 502,
body: { code: 502, msg: `请求异常: ${err.message || err}` },
}
}
}
+2 -1
View File
@@ -2,6 +2,7 @@
// 此版本不再采用 br 作为音质区分的标准
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
// 当unblock为true时, 会尝试使用unblockmusic-utils进行解锁, 同时音质设置不会生效, 但仍然为必须传入参数
// 当level为sky时, 可通过 immerseType 选择沉浸声类型, 支持 c51(c51类型)、ste(环绕立体声类型)、aac(aac类型), 默认为 c51
const logger = require('../util/logger.js')
const createOption = require('../util/option.js')
@@ -51,7 +52,7 @@ module.exports = async (query, request) => {
}
}
if (data.level == 'sky') {
data.immerseType = 'c51'
data.immerseType = query.immerseType || 'c51'
}
return request(
`/api/song/enhance/player/url/v1`,
+20
View File
@@ -0,0 +1,20 @@
// 云小编获取任务
//
// type:
// 1: 歌曲曲风审核 musicalStyleEnter
// 2: 歌曲语种审核 languageEnter
// 3: 歌曲原唱审核 oriSingerEnter
// 4: 情绪标签审核 emotionEnter
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
type: query.type || '4',
}
return request(
`/api/thinktank/audit/resource/detail`,
data,
createOption(query, 'eapi'),
)
}
+36
View File
@@ -0,0 +1,36 @@
// 云小编提交任务
//
// type:
// 1: 歌曲曲风审核 musicalStyleEnter
// 2: 歌曲语种审核 languageEnter
// 3: 歌曲原唱审核 oriSingerEnter
// 4: 情绪标签审核 emotionEnter
//
// taskId:
// 调用 thinktank/audit/resource/detail 获取
//
// judgement:
// 1: 同意, 2: 否决, 3: 跳过 (不算次数)
const createOption = require('../util/option.js')
module.exports = (query, request) => {
if (!query.taskId || !query.judgement)
return Promise.reject({
status: 400,
body: {
code: 400,
message: '参数不足',
},
})
const data = {
type: query.type || '4',
taskId: query.taskId,
judgement: query.judgement,
}
return request(
`/api/thinktank/audit/resource/update`,
data,
createOption(query, 'eapi'),
)
}
+11
View File
@@ -0,0 +1,11 @@
// 一键领取所有会员成长值
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {}
return request(
`/api/vipnewcenter/app/level/task/reward/getall`,
data,
createOption(query, 'xeapi'),
)
}
+34 -5
View File
@@ -1,11 +1,40 @@
// 黑胶乐签打卡
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {}
return request(
`/api/vip-center-bff/task/sign`,
data,
module.exports = async (query, request) => {
const results = {}
const taskSign = await request(
'/api/vip-center-bff/task/sign',
{},
createOption(query, 'weapi'),
)
results.taskSign = taskSign.body
const checkinDetail = await request(
'/api/vipnewcenter/app/level/user/checkin/history/detail',
{
signDayTime: Date.now(),
type: 1,
},
createOption(query, 'eapi'),
)
results.checkinDetail = checkinDetail.body
// 两个接口都返回 code=200 即视为打卡成功
const signed =
Number(results.taskSign?.code) === 200 &&
Number(results.checkinDetail?.code) === 200
return {
body: {
code: 200,
...results,
signed,
message: signed ? '黑胶乐签打卡成功' : '黑胶乐签打卡失败',
},
cookie: taskSign.cookie,
status: 200,
}
}
+4 -3
View File
@@ -1,13 +1,14 @@
// 黑胶乐签打卡历史
// 黑胶乐签打卡历史 / 状态查询
// 支持传入 type=0(用户信息栏)或 type=1(黑胶乐签)
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
type: '0',
type: query.type || '0',
}
return request(
`/api/vipnewcenter/app/minidesk/music/sign/pc`,
data,
createOption(query, 'xeapi'),
createOption(query, 'eapi'),
)
}
+11 -11
View File
@@ -1,6 +1,6 @@
{
"name": "@neteasecloudmusicapienhanced/api",
"version": "4.35.2",
"version": "4.38.0",
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护",
"scripts": {
"dev": "nodemon app.js",
@@ -75,14 +75,14 @@
"data"
],
"dependencies": {
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.3.2",
"axios": "^1.17.0",
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.3.4",
"axios": "^1.18.1",
"crypto-js": "^4.2.0",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"express-fileupload": "^1.5.2",
"gzip": "^0.1.0",
"music-metadata": "^11.13.0",
"music-metadata": "^11.14.0",
"node-forge": "^1.4.0",
"pac-proxy-agent": "^7.2.0",
"qrcode": "^1.5.4",
@@ -92,19 +92,19 @@
"yargs": "^18.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.5",
"@eslint/js": "^9.39.4",
"@eslint/eslintrc": "^3.3.6",
"@eslint/js": "^9.39.5",
"@types/express": "^5.0.6",
"@types/express-fileupload": "^1.5.1",
"@types/mocha": "^10.0.10",
"@types/node": "25.9.2",
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/parser": "^8.60.1",
"eslint": "^9.39.4",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.65.0",
"eslint": "^9.39.5",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-html": "^8.1.4",
"eslint-plugin-prettier": "^5.5.6",
"globals": "^17.6.0",
"globals": "^17.7.0",
"husky": "^9.1.7",
"intelli-espower-loader": "^1.1.0",
"lint-staged": "^16.4.0",
@@ -112,7 +112,7 @@
"nodemon": "^3.1.14",
"pkg": "^5.8.1",
"power-assert": "^1.6.1",
"prettier": "^3.8.3",
"prettier": "^3.9.6",
"typescript": "^5.9.3"
},
"publishConfig": {
+271 -244
View File
File diff suppressed because it is too large Load Diff
+490 -40
View File
@@ -115,6 +115,182 @@
textarea:focus {
border-color: #333;
}
/* ===== 自定义选项区域 ===== */
.section-container {
margin-top: 20px;
padding: 16px 20px;
background: #fafafa;
border: 1px solid #e8e8e8;
border-radius: 8px;
}
.section-container + .section-container {
margin-top: 12px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #555;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.section-title .count-badge {
font-size: 11px;
background: #ddd;
color: #666;
padding: 1px 8px;
border-radius: 10px;
font-weight: 400;
}
.dynamic-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
align-items: center;
}
.dynamic-row .key-input {
flex: 0 0 200px;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 13px;
outline: none;
font-family: 'Courier New', monospace;
}
.dynamic-row .value-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 13px;
outline: none;
}
.dynamic-row .key-input:focus,
.dynamic-row .value-input:focus {
border-color: #333;
}
.remove-btn {
width: 28px;
height: 28px;
padding: 0;
background: #fff;
color: #999;
border: 1px solid #ddd;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.remove-btn:hover {
background: #fee;
color: #e33;
border-color: #fcc;
}
.add-btn {
background: #fff;
color: #333;
border: 1px dashed #ccc;
padding: 6px 16px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
margin-top: 4px;
}
.add-btn:hover {
border-color: #999;
background: #f5f5f5;
}
/* ===== 参数编辑区 Tab ===== */
.param-container {
flex: 1;
display: flex;
flex-direction: column;
}
.param-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.param-header-title {
font-size: 14px;
font-weight: 500;
color: #555;
}
.param-tabs {
display: flex;
gap: 0;
background: #f0f0f0;
border-radius: 6px;
padding: 2px;
}
.tab-btn {
padding: 4px 14px;
border: none;
background: transparent;
color: #888;
font-size: 12px;
font-weight: 500;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
}
.tab-btn:hover {
color: #555;
}
.tab-btn.active {
background: #fff;
color: #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.param-panel {
flex: 1;
display: flex;
flex-direction: column;
}
.param-panel textarea {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 13px;
resize: vertical;
min-height: 350px;
outline: none;
}
.param-panel textarea:focus {
border-color: #333;
}
</style>
</head>
<body>
@@ -125,6 +301,16 @@
<label for="uri">URI</label>
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
</div>
<div class="form-row">
<label for="method">请求方法</label>
<select id="method" name="method">
<option value="POST" selected>POST</option>
<option value="GET">GET</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
</select>
</div>
<div class="form-row">
<label for="crypto">加密方式</label>
<select id="crypto" name="crypto">
@@ -136,18 +322,40 @@
<option value="" selected>(默认)</option>
</select>
</div>
<div class="form-row">
<label for="timeout">超时(ms)</label>
<input type="number" id="timeout" name="timeout" value="30000" min="0" step="1000" placeholder="0 表示无超时">
</div>
<div class="form-row">
<label></label>
<button type="submit">发送请求</button>
</div>
</form>
<!-- ===== 自定义请求头 ===== -->
<div class="section-container" id="headersSection">
<div class="section-title">
请求头 (Headers)
<span class="count-badge" id="headerCount">4</span>
</div>
<div id="headersList"></div>
<button class="add-btn" onclick="addHeaderRow('', '')">+ 添加请求头</button>
</div>
<div class="data-result">
<div>
<label for="result">响应结果</label>
<textarea id="result" name="result" readonly></textarea>
</div>
<div>
<label for="data">请求数据</label>
<div class="param-container">
<div class="param-header">
<span class="param-header-title">请求参数</span>
<div class="param-tabs">
<button class="tab-btn active" data-mode="json" onclick="switchParamTab('json')">JSON</button>
<button class="tab-btn" data-mode="kv" onclick="switchParamTab('kv')">键值对</button>
</div>
</div>
<div class="param-panel" id="jsonPanel">
<textarea id="data" name="data">
{
"cp": false,
@@ -161,60 +369,302 @@
"yv": 0
}</textarea>
</div>
<div class="param-panel" id="kvPanel" style="display:none">
<div id="kvList"></div>
<button class="add-btn" onclick="addKvRow('', '')">+ 添加参数</button>
</div>
</div>
</div>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
async function sendRequest() {
document.getElementById('result').value = ""
// ===== 动态行管理 =====
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
const uri = document.getElementById('uri').value;
const crypto = document.getElementById('crypto').value;
const data = document.getElementById('data').value;
function addHeaderRow(key, value) {
const list = document.getElementById('headersList');
const row = document.createElement('div');
row.className = 'dynamic-row';
row.innerHTML =
'<input type="text" class="key-input" placeholder="Header 名称" value="' +
escapeHtml(key) +
'">' +
'<input type="text" class="value-input" placeholder="值" value="' +
escapeHtml(value) +
'">' +
'<button class="remove-btn" onclick="removeRow(this)" title="删除">✕</button>';
list.appendChild(row);
updateCount();
}
try {
const res = await axios({
url: `/api?timestamp=${Date.now()}`,
method: 'post',
data: {
uri: uri,
data: JSON.parse(data),
crypto: crypto,
},
function addKvRow(key, value) {
const list = document.getElementById('kvList');
const row = document.createElement('div');
row.className = 'dynamic-row';
row.innerHTML =
'<input type="text" class="key-input" placeholder="参数名" value="' +
escapeHtml(key) +
'">' +
'<input type="text" class="value-input" placeholder="值" value="' +
escapeHtml(value) +
'">' +
'<button class="remove-btn" onclick="removeRow(this)" title="删除">✕</button>';
list.appendChild(row);
}
function removeRow(btn) {
btn.parentElement.remove();
}
function collectHeaders() {
const headers = {};
document.querySelectorAll('#headersList .dynamic-row').forEach(function (row) {
var key = row.querySelector('.key-input').value.trim();
var value = row.querySelector('.value-input').value;
if (key) headers[key] = value;
});
document.getElementById('result').value = JSON.stringify(res.data, null, 2);
} catch (error) {
document.getElementById('result').value = 'Request failed: ' + error.message;
}
return headers;
}
(function fillFromQuery() {
const params = new URLSearchParams(window.location.search);
if (!params.toString()) return;
const uri = params.get('uri');
const crypto = params.get('crypto');
const dataParam = params.get('data');
if (uri) {
document.getElementById('uri').value = uri;
function collectKvData() {
var obj = {};
document.querySelectorAll('#kvList .dynamic-row').forEach(function (row) {
var key = row.querySelector('.key-input').value.trim();
var val = row.querySelector('.value-input').value;
if (!key) return;
// 自动类型转换
if (val === 'true') obj[key] = true;
else if (val === 'false') obj[key] = false;
else if (val === 'null') obj[key] = null;
else if (/^-?\d+(\.\d+)?$/.test(val) && !isNaN(Number(val))) obj[key] = Number(val);
else obj[key] = val;
});
return obj;
}
if (crypto) {
const cryptoSelect = document.getElementById('crypto');
if ([...cryptoSelect.options].some((opt) => opt.value === crypto)) {
cryptoSelect.value = crypto;
}
}
if (dataParam) {
const decoded = dataParam;
// ===== Tab 切换 =====
function syncJsonToKv() {
try {
document.getElementById('data').value = JSON.stringify(
JSON.parse(decoded),
var data = JSON.parse(document.getElementById('data').value);
var list = document.getElementById('kvList');
list.innerHTML = '';
Object.keys(data).forEach(function (k) {
addKvRow(k, String(data[k]));
});
return true;
} catch (e) {
return false;
}
}
function syncKvToJson() {
var obj = collectKvData();
document.getElementById('data').value = JSON.stringify(obj, null, 2);
}
function switchParamTab(mode) {
var jsonPanel = document.getElementById('jsonPanel');
var kvPanel = document.getElementById('kvPanel');
if (mode === 'kv') {
if (!syncJsonToKv()) {
alert('JSON 格式有误,无法切换到键值对模式,请先修正 JSON');
return;
}
jsonPanel.style.display = 'none';
kvPanel.style.display = 'block';
} else {
syncKvToJson();
kvPanel.style.display = 'none';
jsonPanel.style.display = 'block';
}
document.querySelectorAll('.tab-btn').forEach(function (btn) {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
}
// ===== 默认请求头(直接套用后端预设值)=====
var defaultHeaders = [
{ key: 'Cookie', value: '' },
{
key: 'User-Agent',
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0',
},
{ key: 'Referer', value: 'https://music.163.com' },
{ key: 'Content-Type', value: 'application/json' },
];
// ===== 发送请求 =====
async function sendRequest() {
document.getElementById('result').value = '';
var uri = document.getElementById('uri').value;
var method = document.getElementById('method').value;
var crypto = document.getElementById('crypto').value;
var timeout = parseInt(document.getElementById('timeout').value, 10) || 0;
var headers = collectHeaders();
// 根据当前激活的 tab 读取数据
var parsedData;
var activeTab = document.querySelector('.tab-btn.active');
if (activeTab && activeTab.dataset.mode === 'kv') {
parsedData = collectKvData();
} else {
var rawData = document.getElementById('data').value;
try {
parsedData = rawData.trim() ? JSON.parse(rawData) : {};
} catch (e) {
document.getElementById('result').value =
'请求数据格式错误:' + e.message;
return;
}
}
var axiosConfig = {
url: '/api?timestamp=' + Date.now(),
method: method,
};
if (timeout > 0) {
axiosConfig.timeout = timeout;
}
if (method === 'GET') {
axiosConfig.params = {
uri: uri,
crypto: crypto,
data: JSON.stringify(parsedData),
headers: Object.keys(headers).length ? JSON.stringify(headers) : undefined,
};
} else {
axiosConfig.data = {
uri: uri,
crypto: crypto,
data: parsedData,
headers: Object.keys(headers).length ? headers : undefined,
};
}
try {
var res = await axios(axiosConfig);
document.getElementById('result').value = JSON.stringify(
res.data,
null,
2,
);
} catch (error) {
document.getElementById('data').value = decoded;
document.getElementById('result').value =
'Request failed: ' + error.message;
}
}
// ===== 从 URL 查询参数恢复 =====
(function fillFromQuery() {
var params = new URLSearchParams(window.location.search);
if (!params.toString()) {
// 没有查询参数,用默认 header
defaultHeaders.forEach(function (h) {
addHeaderRow(h.key, h.value);
});
return;
}
var uri = params.get('uri');
var crypto = params.get('crypto');
var dataParam = params.get('data');
var method = params.get('method');
var timeout = params.get('timeout');
var headersParam = params.get('headers');
var kvParams = params.get('kvParams');
var paramMode = params.get('paramMode');
if (uri) document.getElementById('uri').value = uri;
if (method) {
var methodSelect = document.getElementById('method');
if (
[].slice.call(methodSelect.options).some(function (opt) {
return opt.value === method;
})
) {
methodSelect.value = method;
}
}
if (timeout) document.getElementById('timeout').value = timeout;
if (crypto) {
var cryptoSelect = document.getElementById('crypto');
if (
[].slice.call(cryptoSelect.options).some(function (opt) {
return opt.value === crypto;
})
) {
cryptoSelect.value = crypto;
}
}
if (dataParam) {
try {
document.getElementById('data').value = JSON.stringify(
JSON.parse(dataParam),
null,
2,
);
} catch (_) {
document.getElementById('data').value = dataParam;
}
}
if (headersParam) {
try {
var parsed = JSON.parse(headersParam);
Object.keys(parsed).forEach(function (k) {
addHeaderRow(k, parsed[k]);
});
} catch (_) {
// fallback: 用默认 header
defaultHeaders.forEach(function (h) {
addHeaderRow(h.key, h.value);
});
}
} else {
defaultHeaders.forEach(function (h) {
addHeaderRow(h.key, h.value);
});
}
if (kvParams) {
try {
var parsed = JSON.parse(kvParams);
// 先填充 kv 列表,再切换到 kv 模式
Object.keys(parsed).forEach(function (k) {
addKvRow(k, String(parsed[k]));
});
if (paramMode === 'kv') {
switchParamTab('kv');
}
} catch (_) {
/* ignore */
}
} else if (paramMode === 'kv' && dataParam) {
// 从 data 参数同步到键值对模式
try {
var data = JSON.parse(dataParam);
Object.keys(data).forEach(function (k) {
addKvRow(k, String(data[k]));
});
switchParamTab('kv');
} catch (_) {
/* ignore */
}
}
})();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

+312 -5
View File
@@ -283,7 +283,9 @@ AI 生成的图,仅供娱乐()
`md5_password`: md5 加密后的密码,传入后 `password` 参数将失效
`captcha`: 验证码,使用 [`/captcha/sent`](#发送验证码)接口传入手机号获取验证码,调用此接口传入验证码,可使用验证码登录,传入后 `password` 参数将失效
`captcha`: 验证码,使用 `/captcha/sent` 或 `/captcha/sent/v1`接口传入手机号获取验证码,调用此接口传入验证码,可使用验证码登录,传入后 `password` 参数将失效
`sca`: 网易易盾滑块验证token, 获取方式未知
**接口地址 :** `/login/cellphone`
@@ -397,6 +399,19 @@ body {
**调用例子 :** `/captcha/sent?phone=13xxx`
### 新版发送验证码
说明 : 调用此接口 ,传入手机号码, 可发送验证码
**必选参数 :** `phone`: 手机号码
**可选参数 :**
`ctcode`: 国家区号,默认 86 即中国
**接口地址 :** `/captcha/sent/v1`
**调用例子 :** `/captcha/sent/v1?phone=13xxx`
### 验证验证码
说明 : 调用此接口 ,传入手机号码和验证码, 可校验验证码是否正确
@@ -1259,9 +1274,11 @@ tags: 歌单标签
`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清环绕声`, `sky` => `沉浸环绕声`, `dolby` => `杜比全景声`, `jymaster` => `超清母带`
`unblock`: 是否使用使用歌曲解锁, 分为`true`和`false`
**可选参数 :** `immerseType`: 沉浸声环绕声类型, 分为 `c51` => `c51类型`, `ste` => `环绕立体声类型`, `aac` => `aac类型`, 仅在 `level=sky` 时生效, 默认为 `c51`
**接口地址 :** `/song/url/v1`
**调用例子 :** `/song/url/v1?id=1969519579&level=exhigh` `/song/url/v1?id=1969519579,33894312&level=lossless`
**调用例子 :** `/song/url/v1?id=1969519579&level=exhigh` `/song/url/v1?id=1969519579,33894312&level=lossless` `/song/url/v1?id=1969519579&level=sky&immerseType=ste`
说明:`杜比全景声`音质需要设备支持,不同的设备可能会返回不同码率的 url。cookie 需要传入`os=pc`保证返回正常码率的 url。
@@ -1395,8 +1412,6 @@ tags: 歌单标签
说明 : 调用此接口 , 传入类型和歌单 id 可收藏歌单或者取消收藏歌单
!> 警告: 在`v4.29.7`版本后, 在网易云登陆后请求要带上`timestamp`字段, 否则会导致请求不合法
**必选参数 :**
`t` : 类型,1:收藏,2:取消收藏
@@ -2500,6 +2515,18 @@ privilege:权限相关信息
**调用例子 :** `/scrobble?id=518066366&sourceid=36780169&time=291`
#### 听歌打卡 V2 (NCBL 加密版)
说明 : 调用此接口,使用桌面客户端 NCBL 加密日志格式上报听歌记录
**必选参数 :** `id`: 歌曲 id, `time`: 播放时长(秒)
**可选参数 :** `sourceid`: 来源列表 id, `source`: 来源名称(默认 list), `name`: 歌曲名, `artist`: 艺术家, `bitrate`: 码率(默认 320), `level`: 音质等级(默认 exhigh), `total`: 歌曲总时长(秒)
**接口地址 :** `/scrobble/v1`
**调用例子 :** `/scrobble/v1?id=518066366&sourceid=36780169&time=291`
### 提交歌曲播放状态
说明 : 调用此接口可提交歌曲播放状态,支持会话追踪和播放模式记录,未传入 `sessionId` 时后端会自动生成
@@ -3600,6 +3627,32 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
**调用例子 :** `/artist/new/song?limit=1` `/artist/new/song?limit=1&before=1602777625000`
### 关注歌手最近新歌 - 播放全部
说明 : 登录后调用此接口可获取所有关注歌手最近的 50 首新歌
**接口地址 :** `/artist/new/song/playall`
**调用例子 :** `/artist/new/song/playall`
### 关注歌手新作品(歌曲/MV)
说明 : 登录后调用此接口可分页获取关注歌手的新歌曲和 MV
**可选参数 :** `limit`: 返回数量,默认为 10
`startTimestamp`: 时间游标,首次请求默认为当前时间,后续请求传入上一页返回的时间游标
`before`: `startTimestamp` 的别名
`sourceType`: 作品来源类型,已确认 `1` 表示新歌,其他取值待确认,默认为 `1`
`firstRequest`: 是否首次请求,默认为 `true`,翻页时传 `false`
**接口地址 :** `/artist/new/song/mv/list/v2`
**调用例子 :** `/artist/new/song/mv/list/v2` `/artist/new/song/mv/list/v2?startTimestamp=1783680576099&sourceType=1&limit=10&firstRequest=false`
### 关注歌手新 MV
说明 :登录后调用此接口可获取关注歌手新 MV
@@ -3730,6 +3783,14 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
**调用例子 :** `/vip/growthpoint/get?ids=7043206830_7` `/vip/growthpoint/get?ids=8613118351_1,8607552957_1`
### 一键领取所有 vip 成长值
说明 : 登录后调用此接口 , 一键领取所有已完成的会员任务成长值奖励
**接口地址 :** `/vip/growthpoint/getall`
**调用例子 :** `/vip/growthpoint/getall`
### 歌手粉丝
说明 : 调用此接口 , 传入歌手 id, 可获取歌手粉丝
@@ -4798,6 +4859,23 @@ bitrate = Math.floor(br / 1000)
**接口地址 :** `/listen/data/today/song`
### 听歌足迹 - 歌曲播放排行
说明 : 登录后调用此接口, 获取歌曲播放排行, 返回每首歌的播放次数 (playCount); 默认返回前 20 名 (Top20), 不足则返回全部
**必选参数 :**
`type`: 维度类型 周 week 月 month
**可选参数 :**
`endTime` : 周: 每周周六 0 点的时间戳 月: 每月最后一天 0 点的时间戳
不填就是本周/月的
**接口地址 :** `/listen/data/song/play/rank`
**调用例子 :** `/listen/data/song/play/rank?type=month`
### 听歌足迹 - 总收听时长
说明 : 登录后调用此接口, 获取总收听时长; 相关接口可能需要 vip 权限
@@ -5410,7 +5488,236 @@ let data = encodeURIComponent(
**接口地址 :** `/song/cloud/download`
**调用例子 :** `/song/cloud/download?id=123456789`
**调用例子 :** `/song/cloud/download?id=123456789`'
### 获取广告
说明 : 调用此接口, 可获取广告
**接口地址 :** `/ad/get`
**调用例子 :** `/ad/get`
### 获取30分钟免费听歌时长
说明 : 登录后调用此接口, 获取30分钟免费听歌时长
!> 警告: 通过调取接口出现的任何问题由调用者自行承担
**可选参数 :** `reqUid` 通过`/ad/get` 获取的广告ID
**接口地址 :** `/ad/listening/rights/gain`
**调用例子 :** `/ad/listening/rights/gain`
### 云小编 - 获取用户详情
说明: 登录后调用此接口, 获取云小编用户详情
**接口地址:** `/rep/ugc/user/get`
**调用例子:** `/rep/ugc/user/get`
### 云小编 - 每日签到
说明: 登录后调用此接口, 进行云小编签到, 领取 5 积分, 签到后 `/rep/ugc/user/get` 返回 `data.signed = 1`
**接口地址:** `/rep/ugc/user/sign`
**调用例子:** `/rep/ugc/user/sign`
### 云小编 - 查询会员任务状态
说明: 登录后调用此接口, 查询云小编会员任务状态, 当 `data.status = 20` 时, 可调用 `/rep/ugc/user/collect-vip` 领取会员
**接口地址:** `/rep/ugc/user/vip`
**调用例子:** `/rep/ugc/user/vip`
### 云小编 - 活动信息
说明: 登录后调用此接口, 查询云小编会员活动信息
**接口地址:** `/rep/ugc/activity/get`
**调用例子:** `/rep/ugc/activity/get`
### 云小编 - 获取任务
> 注意: 调用前请先使用官方客户端完成云小编“情绪标签审核”入站考试
**可选参数:**
`type`: 任务类型, 1: 歌曲曲风审核, 2: 歌曲语种审核, 3: 歌曲原唱审核, 4: 情绪标签审核, 默认 `4`
**接口地址:** `/thinktank/audit/resource/detail`
**调用例子:** `/thinktank/audit/resource/detail?type=4`
### 云小编 - 提交任务
> 注意: 投票结果会在后台审核, 一致 +3 积分, 不一致 -2 积分
**可选参数:**
`type`: 任务类型, 1: 歌曲曲风审核, 2: 歌曲语种审核, 3: 歌曲原唱审核, 4: 情绪标签审核, 默认 `4`
**必选参数:**
`taskId`: 任务 ID, 调用 `/thinktank/audit/resource/detail` 获取 `data.taskId`
`judgement`: 审核结果, 1: 同意, 2: 否决, 3: 跳过 (不算次数)
**接口地址:** `/thinktank/audit/resource/update`
**调用例子:** `/thinktank/audit/resource/update?type=4&taskId=123456&judgement=1`
### 云小编 - 领取任务积分
说明: 完成任务后调用此接口, 领取云小编任务积分
**可选参数:**
`activityId`: 活动 ID, 调用 `/rep/ugc/activity/get` 获取, 默认 `5001`
**接口地址:** `/rep/ugc/activity/collect`
**调用例子:** `/rep/ugc/activity/collect?activityId=5001`
### 云小编 - 领取一日会员
说明: 达成领取条件 (`/rep/ugc/user/vip`) 后调用此接口, 领取一日会员
**可选参数:**
`activityId`: 活动 ID, 调用 `/rep/ugc/activity/get` 获取, 默认 `5001`
**接口地址:** `/rep/ugc/user/collect-vip`
**调用例子:** `/rep/ugc/user/collect-vip?activityId=5001`
### 发送/删除评论
说明 : 调用此接口,可发送评论或者删除评论
1. 发送评论
**必选参数**
`type`: 数字,资源类型,对应歌曲,mv,专辑,歌单,电台,视频对应以下类型
```
0: 歌曲
1: mv
2: 歌单
3: 专辑
4: 电台
5: 视频
6: 动态
```
`id`: 对应资源 id
`content`: 要发送的内容
**调用例子** : `/comment/add?type=1&id=5436712&content=test` (往广岛之恋 mv 发送评论: test)
2. 回复评论
**必选参数**
`type`: 数字,资源类型,对应歌曲,mv,专辑,歌单,电台,视频对应以下类型
```
0: 歌曲
1: mv
2: 歌单
3: 专辑
4: 电台
5: 视频
6: 动态
```
`id`: 对应资源 id
`cid`: 评论 id
`content`: 要发送的内容
**调用例子** : `/comment/add?type=1&id=5436712&cid=1535550516319&content=test` (往广岛之恋 mv 回复test评论: test)
3. 删除评论
**必选参数**
`type`: 数字,资源类型,对应歌曲,mv,专辑,歌单,电台,视频对应以下类型
```
0: 歌曲
1: mv
2: 歌单
3: 专辑
4: 电台节目
5: 视频
6: 动态
7: 电台
```
`id`: 对应资源 id
`cid`: 评论 id
**调用例子** : `/comment?type=1&id=5436712&cid=1535550516319` (在广岛之恋 mv 删除评论)
### 获取在线设备列表
说明: 登录后调用此接口, 获取在线设备列表
**接口地址:** `/device/list`
**调用例子:** `/device/list`
### 发送安全验证码
说明: 登录后调用此接口, 传入手机号, 可发送安全验证码
**必选参数 :** `phone`: 手机号
**接口地址 :** `/captcha/safe/sent`
**调用例子 :** `/captcha/safe/sent?phone=13XXXXXXXXX`
### 强制下线设备
说明: 登录后调用此接口, 传入设备 id, 可强制下线设备的登录会话
**必选参数 :** `key`: 设备的 `deviceKey`, 可通过 `/device/list` 获取
`captcha`: 安全验证码, 可通过 `/captcha/safe/sent` 获取
**接口地址 :** `/device/kickoff`
**调用例子 :** `/device/kickoff?key=00ALDFGEXXXXXXXXXXXXXXXXX&captcha=1234`
## 离线访问此文档
+312
View File
@@ -0,0 +1,312 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>免费听 — 网易云音乐 API Enhanced</title>
<style>
:root {
--fg: #333;
--muted: #666;
--border: #ddd;
--bg: #f5f5f5;
--panel: #ffffff;
--accent: #333;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; }
.container { max-width: 800px; margin: 40px auto; padding: 0 20px; }
@media (max-width: 480px) {
.container { margin: 20px auto; padding: 0 16px; }
header.site-header h1 { font-size: 22px; }
.block { padding: 16px; }
}
header.site-header { margin-bottom: 24px; }
header.site-header h1 { font-size: 24px; font-weight: 600; margin: 0; }
.sub { margin-top: 4px; color: var(--muted); font-size: 14px; }
.block { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); }
.block h3 { margin: 0 0 4px; font-size: 15px; font-weight: 600; }
.block .desc { margin: 0 0 12px; color: var(--muted); font-size: 13px; }
pre { margin: 0; background: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; font-family: 'Courier New', monospace; font-size: 13px; max-height: 200px; overflow: auto; }
pre.data { max-height: 360px; }
.row { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
button { padding: 8px 18px; border-radius: 8px; border: 1px solid var(--border); font-size: 14px; font-weight: 500; cursor: pointer; background: var(--panel); color: var(--fg); transition: all .15s; }
button:hover { background: #eee; }
button.primary { background: var(--fg); color: var(--panel); border-color: var(--fg); }
button.primary:hover { opacity: .85; }
button:disabled { opacity: .4; cursor: not-allowed; }
button.danger { background: #a11; color: #fff; border-color: #a11; }
button.danger:hover { opacity: .85; }
.tag { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; border: 1px solid var(--border); }
.tag.ok { background: #e6f7e6; color: #1a7a1a; border-color: #b7e6b7; }
.tag.no { background: #ffe6e6; color: #a11; border-color: #f5c6c6; }
.tag.busy { background: #fff3cd; color: #856404; border-color: #ffeeba; }
.flex { display: flex; align-items: center; gap: 10px; }
.flex.sb { justify-content: space-between; }
.step { display: flex; gap: 12px; align-items: flex-start; }
.step .num { width: 26px; height: 26px; border-radius: 50%; background: var(--fg); color: var(--panel); display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; flex-shrink: 0; margin-top: 1px; }
.step .num.done { background: #1a7a1a; }
.step .body { flex: 1; min-width: 0; }
input[type=text] { width: 100%; padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border); font-size: 14px; font-family: inherit; margin-top: 6px; }
input[type=text]:focus { outline: 2px solid var(--fg); outline-offset: -1px; border-color: transparent; }
footer.site-footer { margin-top: 32px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); text-align: center; font-size: 13px; }
footer.site-footer a { color: var(--fg); text-decoration: none; }
footer.site-footer a:hover { border-bottom: 1px dotted var(--fg); }
.info-grid { display: grid; grid-template-columns: 80px 1fr; gap: 4px 12px; font-size: 13px; margin-top: 8px; }
.info-grid .lbl { color: var(--muted); }
</style>
</head>
<body>
<main class="container">
<header class="site-header">
<h1>🎧 免费听 · 看广告获取时长</h1>
<p class="sub">模拟 App 内看激励视频广告 → 领取免费听权益的完整流程</p>
</header>
<!-- 步骤 1: 注册 Token -->
<div class="block">
<div class="step">
<div class="num" id="s1num">1</div>
<div class="body">
<div class="flex sb">
<h3>注册反作弊 Token</h3>
<span id="s1tag" class="tag no">未开始</span>
</div>
<p class="desc">调用易盾接口获取实时 token,广告接口需要此 token 才能正常响应</p>
<pre id="s1pre">(尚未执行)</pre>
<div class="row">
<button id="step1" class="primary">① 注册 Token</button>
</div>
</div>
</div>
</div>
<!-- 步骤 2: 获取广告 -->
<div class="block">
<div class="step">
<div class="num" id="s2num">2</div>
<div class="body">
<div class="flex sb">
<h3>获取广告 &amp; 提取 reqId</h3>
<span id="s2tag" class="tag no">未开始</span>
</div>
<p class="desc">请求广告位,从返回的 extJson.contextInfo.req_id 提取用于领取权益的标识</p>
<pre id="s2pre" class="data">(尚未执行)</pre>
<div class="row">
<button id="step2" class="primary" disabled>② 获取广告</button>
<button id="step2b" disabled>换一个广告位</button>
</div>
<div class="info-grid" id="s2info" style="display:none">
<div class="lbl">广告 ID</div><div id="s2adId">-</div>
<div class="lbl">reqId</div><div id="s2reqId">-</div>
<div class="lbl">文案</div><div id="s2text">-</div>
</div>
</div>
</div>
</div>
<!-- 步骤 3: 领取权益 -->
<div class="block">
<div class="step">
<div class="num" id="s3num">3</div>
<div class="body">
<div class="flex sb">
<h3>领取免费听权益</h3>
<span id="s3tag" class="tag no">未开始</span>
</div>
<p class="desc">传入 reqId 及相关参数,领取免费听权益(模拟点击广告后的领取请求)</p>
<pre id="s3pre" class="data">(尚未执行)</pre>
<div class="row">
<button id="step3" class="primary" disabled>③ 领取权益</button>
</div>
<div class="info-grid" id="s3info" style="display:none">
<div class="lbl">结果</div><div id="s3result">-</div>
</div>
</div>
</div>
</div>
<!-- 一键全流程 -->
<div class="block">
<div class="flex sb">
<h3>一键运行</h3>
<span id="allTag" class="tag no">就绪</span>
</div>
<p class="desc" style="margin-top:4px">按顺序自动执行以上三个步骤</p>
<div class="row">
<button id="runAll" class="primary">▶ 一键获取免费听时长</button>
<button id="clearAll" class="danger">✕ 清空</button>
</div>
</div>
<footer class="site-footer">
<a href="/">← 返回首页</a>
</footer>
</main>
<script>
const $ = (id) => document.getElementById(id)
const base = location.origin
function setTag(id, type, text) {
const el = $(id)
el.className = 'tag ' + type
el.textContent = text
}
function setNum(id, done) {
$(id).className = 'num' + (done ? ' done' : '')
}
function nocache(url) {
const sep = url.indexOf('?') > -1 ? '&' : '?'
return url + sep + '_t=' + Date.now()
}
// ===== 步骤 1: 注册 Token =====
async function step1() {
const btn = $('step1')
btn.disabled = true
setTag('s1tag', 'busy', '注册中…')
setNum('s1num', false)
try {
const res = await fetch(nocache(base + '/register/checktoken/v3?refresh=1'))
const data = await res.json()
$('s1pre').textContent = JSON.stringify(data, null, 2)
if (data.token) {
setTag('s1tag', 'ok', '已注册')
setNum('s1num', true)
$('step2').disabled = false
$('step2b').disabled = false
return data.token
}
setTag('s1tag', 'no', '失败')
return null
} catch (e) {
$('s1pre').textContent = JSON.stringify({ error: e.message }, null, 2)
setTag('s1tag', 'no', e.message)
return null
} finally {
btn.disabled = false
}
}
// ===== 步骤 2: 获取广告 =====
async function step2(typeId) {
const btn = $('step2')
const btnB = $('step2b')
btn.disabled = btnB.disabled = true
setTag('s2tag', 'busy', '获取中…')
setNum('s2num', false)
$('s2info').style.display = 'none'
try {
let url = base + '/ad/get' + (typeId ? '?type_ids=' + encodeURIComponent(typeId) : '')
const res = await fetch(nocache(url))
const data = await res.json()
$('s2pre').textContent = JSON.stringify(data, null, 2)
if (data.code === 200 && data.extra && data.extra.reqId) {
setTag('s2tag', 'ok', '已获取')
setNum('s2num', true)
$('s2info').style.display = 'grid'
const adKeys = data.ads ? Object.keys(data.ads) : []
let adId = '-', text = '-'
if (adKeys.length > 0) {
const ad = data.ads[adKeys[0]]
adId = ad.ad_id || '-'
text = ad.text || '-'
}
$('s2adId').textContent = adId
$('s2reqId').textContent = data.extra.reqId
$('s2text').textContent = text
$('step3').disabled = false
return data.extra.reqId
}
setTag('s2tag', 'no', '未获取到 reqId')
return null
} catch (e) {
$('s2pre').textContent = JSON.stringify({ error: e.message }, null, 2)
setTag('s2tag', 'no', e.message)
return null
} finally {
btn.disabled = false
btnB.disabled = false
}
}
$('step2').onclick = () => step2()
$('step2b').onclick = () => step2('["400002_0"]')
// ===== 步骤 3: 领取权益 =====
async function step3() {
const btn = $('step3')
btn.disabled = true
setTag('s3tag', 'busy', '领取中…')
setNum('s3num', false)
$('s3info').style.display = 'none'
try {
const res = await fetch(nocache(base + '/ad/listening/rights/gain'))
const data = await res.json()
$('s3pre').textContent = JSON.stringify(data, null, 2)
if (data.code === 200) {
setTag('s3tag', 'ok', '已领取')
setNum('s3num', true)
$('s3info').style.display = 'grid'
const body = data.data || {}
$('s3result').textContent = JSON.stringify(body).substring(0, 200)
return true
}
setTag('s3tag', 'no', '领取失败')
return false
} catch (e) {
$('s3pre').textContent = JSON.stringify({ error: e.message }, null, 2)
setTag('s3tag', 'no', e.message)
return false
} finally {
btn.disabled = false
}
}
$('step3').onclick = step3
// ===== 一键运行 =====
$('runAll').onclick = async function () {
const btn = this
btn.disabled = true
setTag('allTag', 'busy', '运行中…')
await step1()
await step2()
await step3()
setTag('allTag', 'ok', '完成')
btn.disabled = false
}
// ===== 清空 =====
$('clearAll').onclick = function () {
const steps = ['s1', 's2', 's3']
const tags = { s1: '未开始', s2: '未开始', s3: '未开始' }
steps.forEach(s => {
setTag(s + 'tag', 'no', tags[s])
setNum(s + 'num', false)
$(s + 'pre').textContent = '(尚未执行)'
const info = $(s + 'info')
if (info) info.style.display = 'none'
})
$('step1').disabled = false
$('step2').disabled = true
$('step2b').disabled = true
$('step3').disabled = true
setTag('allTag', 'no', '就绪')
$('runAll').disabled = false
}
</script>
</body>
</html>
+5 -1
View File
@@ -95,7 +95,11 @@ curl -s {origin}/search?keywords=网易云</code></pre>
<a href="/api_decrypt.html">API 解密</a> ·
<a href="/listen_together_host.html">一起听示例</a> ·
<a href="/playlist_cover_update.html">更新歌单封面示例</a> ·
<a href="/avatar_update.html">头像更新示例</a>
<a href="/avatar_update.html">头像更新示例</a> ·
<a href="/scrobble.html">听歌打卡示例</a> ·
<a href="/yidun.html">获取 CheckToken</a> ·
<a href="/free_listen.html">免费听权益</a> ·
<a href="/ugc.html">云小编任务中心</a>
</div>
</section>
+480
View File
@@ -0,0 +1,480 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>听歌打卡 - 网易云音乐 API Enhanced</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 520px;
margin: 40px auto;
background: white;
border-radius: 12px;
padding: 32px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
text-align: center;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 24px;
text-align: center;
}
.login-link {
display: block;
margin-bottom: 20px;
color: #666;
font-size: 14px;
text-decoration: none;
text-align: center;
}
.login-link:hover {
color: #333;
text-decoration: underline;
}
/* ---- Tabs ---- */
.tabs {
display: flex;
gap: 0;
margin-bottom: 24px;
border-bottom: 2px solid #eee;
}
.tab-btn {
flex: 1;
padding: 10px 0;
background: none;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
font-size: 14px;
font-weight: 500;
color: #999;
cursor: pointer;
transition: color 0.2s, border-color 0.2s;
}
.tab-btn:hover {
color: #555;
}
.tab-btn.active {
color: #333;
border-bottom-color: #333;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* ---- Form ---- */
.form-group {
margin-bottom: 16px;
text-align: left;
}
label {
display: block;
font-size: 13px;
font-weight: 500;
color: #555;
margin-bottom: 6px;
}
label .tag {
display: inline-block;
font-size: 11px;
font-weight: 400;
padding: 1px 6px;
border-radius: 3px;
margin-left: 6px;
}
label .tag.r {
background: #fee2e2;
color: #991b1b;
}
label .tag.o {
background: #fef3c7;
color: #92400e;
}
input[type="text"],
input[type="number"],
textarea {
width: 100%;
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
outline: none;
font-family: inherit;
transition: border-color 0.2s;
}
input[type="text"]:focus,
input[type="number"]:focus,
textarea:focus {
border-color: #333;
}
textarea {
resize: vertical;
}
.row {
display: flex;
gap: 12px;
}
.row .form-group {
flex: 1;
}
.quick-fill {
margin-top: 5px;
font-size: 12px;
color: #666;
}
.quick-fill span {
color: #0066cc;
cursor: pointer;
margin-right: 8px;
}
.quick-fill span:hover {
text-decoration: underline;
}
.btn {
display: block;
width: 100%;
padding: 12px;
background: #333;
color: white;
font-size: 15px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
border: none;
text-align: center;
margin-top: 20px;
}
.btn:hover {
background: #555;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.result {
margin-top: 16px;
padding: 12px 16px;
border-radius: 6px;
font-size: 14px;
text-align: left;
white-space: pre-wrap;
word-break: break-all;
font-family: monospace;
}
.result.success {
background: #d1fae5;
color: #065f46;
}
.result.error {
background: #fee2e2;
color: #991b1b;
}
.result.info {
background: #e0f2fe;
color: #0369a1;
}
.footer-link {
text-align: center;
margin-top: 24px;
font-size: 13px;
}
.footer-link a {
color: #666;
text-decoration: none;
}
.footer-link a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>听歌打卡</h1>
<p class="subtitle">同步听歌记录至网易云音乐,增加听歌排行计数</p>
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
<!-- Cookie (共享) -->
<div class="form-group">
<label for="cookie">Cookie <span class="tag o">可选</span></label>
<textarea id="cookie" placeholder="留空则默认读取本地存储的登录态" rows="2"></textarea>
<div class="quick-fill">
<span onclick="loadLocalCookie()">读取本地 Cookie</span>
</div>
</div>
<!-- Tabs -->
<div class="tabs">
<button class="tab-btn active" data-tab="v0" onclick="switchTab('v0')">原版</button>
<button class="tab-btn" data-tab="v1" onclick="switchTab('v1')">V2 (NCBL 加密版)</button>
</div>
<!-- ============ Tab: v0 (原版 /scrobble) ============ -->
<div class="tab-content active" id="tab-v0">
<div class="form-group">
<label>歌曲 ID <span class="tag r">必填</span></label>
<input type="text" id="v0-songId" placeholder="例如: 2756058128" value="2756058128" />
<div class="quick-fill">
示例:
<span onclick="fillV0('2756058128', '288651229')">妖精小姐的魔法邀约</span>
<span onclick="fillV0('2637402867', '251025018')">Echoes of Memoria</span>
<span onclick="fillV0('36307815', '3394198')">Lose Control</span>
</div>
</div>
<div class="form-group">
<label>来源 ID (歌单或专辑 ID) <span class="tag r">必填</span></label>
<input type="text" id="v0-sourceId" placeholder="例如: 2756058128" value="2756058128" />
</div>
<div class="form-group">
<label>播放时间 (秒) <span class="tag o">可选</span></label>
<input type="number" id="v0-time" placeholder="300" value="300" />
</div>
<button class="btn" onclick="submitV0()">立即打卡 (原版)</button>
</div>
<!-- ============ Tab: v1 (NCBL 加密版 /scrobble/v1) ============ -->
<div class="tab-content" id="tab-v1">
<div class="form-group">
<label>歌曲 ID <span class="tag r">必填</span></label>
<input type="text" id="v1-songId" placeholder="例如: 518066366" value="518066366" />
<div class="quick-fill">
示例:
<span onclick="fillV1('518066366', '36780169', '291')">默认示例</span>
<span onclick="fillV1('2756058128', '288651229', '300')">妖精小姐的魔法邀约</span>
<span onclick="fillV1('2637402867', '251025018', '300')">Echoes of Memoria</span>
</div>
</div>
<div class="form-group">
<label>播放时间 (秒) <span class="tag r">必填</span></label>
<input type="number" id="v1-time" placeholder="例如: 291" value="291" />
</div>
<div class="form-group">
<label>来源 ID <span class="tag o">可选</span></label>
<input type="text" id="v1-sourceId" placeholder="歌单或专辑 ID" value="36780169" />
</div>
<div class="row">
<div class="form-group">
<label>歌曲名 <span class="tag o">可选</span></label>
<input type="text" id="v1-name" placeholder="歌曲名称" />
</div>
<div class="form-group">
<label>艺术家 <span class="tag o">可选</span></label>
<input type="text" id="v1-artist" placeholder="歌手名" />
</div>
</div>
<div class="row">
<div class="form-group">
<label>码率 <span class="tag o">可选</span></label>
<input type="number" id="v1-bitrate" placeholder="默认 320" value="320" />
</div>
<div class="form-group">
<label>音质等级 <span class="tag o">可选</span></label>
<input type="text" id="v1-level" placeholder="默认 exhigh" value="exhigh" />
</div>
</div>
<div class="form-group">
<label>歌曲总时长 (秒) <span class="tag o">可选</span></label>
<input type="number" id="v1-total" placeholder="留空则与播放时间相同" />
</div>
<button class="btn" onclick="submitV1()">立即打卡 (NCBL 加密版)</button>
</div>
<!-- Result -->
<div id="result" class="result" style="display: none;"></div>
<div class="footer-link">
<a href="/">返回首页</a>
</div>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script>
/* ---- DOM refs ---- */
const cookieInput = document.getElementById('cookie')
const resultDiv = document.getElementById('result')
/* ---- init ---- */
if (localStorage.getItem('cookie')) {
cookieInput.value = localStorage.getItem('cookie')
}
/* ---- Tab switching ---- */
function switchTab(name) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === name))
document.querySelectorAll('.tab-content').forEach(c => c.classList.toggle('active', c.id === 'tab-' + name))
hideResult()
}
/* ---- Cookie ---- */
function loadLocalCookie() {
const local = localStorage.getItem('cookie')
if (local) {
cookieInput.value = local
showResult('已成功读取本地 Cookie', 'success')
} else {
showResult('未在本地发现登录 Cookie,请先登录或手动粘贴', 'error')
}
}
/* ---- Quick fill ---- */
function fillV0(id, sourceId) {
document.getElementById('v0-songId').value = id
document.getElementById('v0-sourceId').value = sourceId
}
function fillV1(id, sourceId, time) {
document.getElementById('v1-songId').value = id
document.getElementById('v1-sourceId').value = sourceId
document.getElementById('v1-time').value = time
}
/* ---- Result ---- */
function hideResult() {
resultDiv.style.display = 'none'
}
function showResult(message, type) {
resultDiv.textContent = message
resultDiv.className = 'result ' + type
resultDiv.style.display = 'block'
}
function disableAll(disabled) {
document.querySelectorAll('.btn').forEach(b => b.disabled = disabled)
}
/* ---- Submit: 原版 /scrobble ---- */
async function submitV0() {
const id = document.getElementById('v0-songId').value.trim()
const sourceid = document.getElementById('v0-sourceId').value.trim()
const time = document.getElementById('v0-time').value.trim() || '300'
if (!id) { showResult('请输入歌曲 ID !', 'error'); return }
if (!sourceid) { showResult('请输入来源 ID !', 'error'); return }
disableAll(true)
showResult('打卡中,请稍候...', 'info')
try {
const params = { id, sourceid: sourceid || id, time, timestamp: Date.now() }
const cookie = cookieInput.value.trim()
if (cookie) params.cookie = cookie
const res = await axios({ method: 'get', url: '/scrobble', params })
if (res.data.code === 200) {
showResult('✅ 打卡成功! \n\n' + JSON.stringify(res.data, null, 2), 'success')
} else {
showResult('请求已发送,返回:\n\n' + JSON.stringify(res.data, null, 2), 'info')
}
} catch (error) {
const errData = error.response ? error.response.data : { error: error.message }
showResult('❌ 打卡失败\n\n' + JSON.stringify(errData, null, 2), 'error')
} finally {
disableAll(false)
}
}
/* ---- Submit: NCBL 加密版 /scrobble/v1 ---- */
async function submitV1() {
const id = document.getElementById('v1-songId').value.trim()
const time = document.getElementById('v1-time').value.trim()
if (!id) { showResult('请输入歌曲 ID !', 'error'); return }
if (!time || isNaN(+time) || +time <= 0) { showResult('请输入有效的播放时长(秒)!', 'error'); return }
disableAll(true)
showResult('打卡中(NCBL 加密),请稍候...', 'info')
try {
const params = {
id,
time,
sourceid: document.getElementById('v1-sourceId').value.trim() || undefined,
name: document.getElementById('v1-name').value.trim() || undefined,
artist: document.getElementById('v1-artist').value.trim() || undefined,
bitrate: document.getElementById('v1-bitrate').value.trim() || undefined,
level: document.getElementById('v1-level').value.trim() || undefined,
total: document.getElementById('v1-total').value.trim() || undefined,
timestamp: Date.now(),
}
const cookie = cookieInput.value.trim()
if (cookie) params.cookie = cookie
// 清理 undefined 参数
Object.keys(params).forEach(k => params[k] === undefined && delete params[k])
const res = await axios({ method: 'get', url: '/scrobble/v1', params })
if (res.data.code === 200) {
showResult('✅ NCBL 加密打卡成功! \n\n' + JSON.stringify(res.data, null, 2), 'success')
} else {
showResult('请求已发送,返回:\n\n' + JSON.stringify(res.data, null, 2), 'info')
}
} catch (error) {
const errData = error.response ? error.response.data : { error: error.message }
showResult('❌ 打卡失败\n\n' + JSON.stringify(errData, null, 2), 'error')
} finally {
disableAll(false)
}
}
</script>
</body>
</html>
+701
View File
@@ -0,0 +1,701 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>云小编任务中心 - 网易云音乐 API Enhanced</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 600px;
margin: 40px auto;
background: white;
border-radius: 12px;
padding: 32px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
text-align: center;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 24px;
text-align: center;
}
.login-link {
display: block;
margin-bottom: 20px;
color: #666;
font-size: 14px;
text-decoration: none;
text-align: center;
}
.login-link:hover {
color: #333;
text-decoration: underline;
}
/* ---- Section ---- */
.section {
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.section:last-of-type {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
/* ---- Form ---- */
.form-group {
margin-bottom: 16px;
text-align: left;
}
label {
display: block;
font-size: 13px;
font-weight: 500;
color: #555;
margin-bottom: 6px;
}
label .tag {
display: inline-block;
font-size: 11px;
font-weight: 400;
padding: 1px 6px;
border-radius: 3px;
margin-left: 6px;
}
label .tag.r {
background: #fee2e2;
color: #991b1b;
}
label .tag.o {
background: #fef3c7;
color: #92400e;
}
label .tag.g {
background: #d1fae5;
color: #065f46;
}
input[type="text"],
input[type="number"],
textarea {
width: 100%;
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
outline: none;
font-family: inherit;
transition: border-color 0.2s;
}
input[type="text"]:focus,
input[type="number"]:focus,
textarea:focus {
border-color: #333;
}
textarea {
resize: vertical;
}
.row {
display: flex;
gap: 12px;
}
.row .form-group {
flex: 1;
}
.quick-fill {
margin-top: 5px;
font-size: 12px;
color: #666;
}
.quick-fill span {
color: #0066cc;
cursor: pointer;
margin-right: 8px;
}
.quick-fill span:hover {
text-decoration: underline;
}
.btn {
display: block;
width: 100%;
padding: 12px;
background: #333;
color: white;
font-size: 15px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
border: none;
text-align: center;
margin-top: 20px;
}
.btn:hover {
background: #555;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn-sm {
display: inline-block;
width: auto;
padding: 6px 16px;
background: #333;
color: white;
font-size: 13px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
border: none;
text-align: center;
margin-top: 0;
}
.btn-sm:hover {
background: #555;
}
.btn-sm:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn-sm.green {
background: #059669;
}
.btn-sm.green:hover {
background: #047857;
}
.btn-sm.green:disabled {
background: #a7f3d0;
}
.btn-sm.red {
background: #dc2626;
}
.btn-sm.red:hover {
background: #b91c1c;
}
.btn-sm.red:disabled {
background: #fca5a5;
}
.btn-sm.gray {
background: #6b7280;
}
.btn-sm.gray:hover {
background: #4b5563;
}
.btn-sm.gray:disabled {
background: #d1d5db;
}
.result {
margin-top: 16px;
padding: 12px 16px;
border-radius: 6px;
font-size: 14px;
text-align: left;
white-space: pre-wrap;
word-break: break-all;
font-family: monospace;
}
.result.success {
background: #d1fae5;
color: #065f46;
}
.result.error {
background: #fee2e2;
color: #991b1b;
}
.result.info {
background: #e0f2fe;
color: #0369a1;
}
.footer-link {
text-align: center;
margin-top: 24px;
font-size: 13px;
}
.footer-link a {
color: #666;
text-decoration: none;
}
.footer-link a:hover {
text-decoration: underline;
}
/* ---- Info Card ---- */
.info-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: #f9fafb;
border-radius: 8px;
border: 1px solid #eee;
}
.info-card .avatar {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
background: #e5e7eb;
}
.info-card .info-text {
flex: 1;
font-size: 14px;
color: #333;
}
.info-card .btn-sm {
flex-shrink: 0;
}
/* ---- Song Review ---- */
.song-review {
margin-top: 16px;
padding: 16px;
background: #f9fafb;
border-radius: 8px;
border: 1px solid #eee;
}
.song-review .cover {
width: 100px;
height: 100px;
border-radius: 6px;
object-fit: cover;
background: #e5e7eb;
display: block;
margin-bottom: 12px;
}
.song-review .song-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.song-review audio {
width: 100%;
margin-bottom: 8px;
}
.song-review .song-tag {
font-size: 13px;
color: #666;
margin-bottom: 10px;
}
.song-review .btn-group {
display: flex;
gap: 8px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.song-review .lyric {
font-size: 13px;
color: #555;
background: #fff;
padding: 10px 12px;
border-radius: 6px;
border: 1px solid #e5e7eb;
white-space: pre-wrap;
max-height: 120px;
overflow-y: auto;
font-family: monospace;
line-height: 1.6;
}
.status-text {
font-size: 14px;
color: #888;
}
</style>
</head>
<body>
<div class="container">
<h1>云小编任务中心</h1>
<p class="subtitle">管理云音乐审核任务、签到与会员权益领取</p>
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
<!-- Cookie -->
<div class="form-group">
<label for="cookie">Cookie <span class="tag o">可选</span></label>
<textarea id="cookie" placeholder="留空则默认读取本地存储的登录态" rows="2"></textarea>
<div class="quick-fill">
<span onclick="loadLocalCookie()">读取本地 Cookie</span>
<span onclick="clearCookie()">清除 Cookie</span>
</div>
</div>
<div class="section">
<div class="section-header">
<span class="section-title">用户信息</span>
<button class="btn-sm" id="userGet">获取用户详情</button>
</div>
<div class="info-card" id="user">
<img alt="头像" class="avatar" id="userAvatar" />
<span class="info-text" id="userName">未登录</span>
<button class="btn-sm green" id="userSign" disabled>签到</button>
</div>
</div>
<div class="section">
<div class="section-header">
<span class="section-title">会员领取</span>
<button class="btn-sm" id="vipGet">获取会员详情</button>
</div>
<div class="info-card" id="vip">
<span class="info-text" id="vipStatus">未载入</span>
<button class="btn-sm" id="vipCollect" disabled>领取</button>
</div>
</div>
<div class="section">
<div class="section-header">
<span class="section-title">审核任务</span>
<button class="btn-sm" id="taskGet">获取任务详情</button>
</div>
<div class="info-card" id="task">
<span class="info-text" id="taskInfo">[<span id="taskCount">0</span>/<span id="taskGoal">0</span>] 审核歌曲赚贡献分 (+66)</span>
<button class="btn-sm" id="taskStart" disabled>去完成</button>
</div>
<!-- Song Review -->
<div class="song-review" id="song" style="display: none;">
<img alt="封面" class="cover" id="songCover" />
<div class="song-title" id="songTitle">未载入</div>
<audio controls id="songAudio"></audio>
<div class="song-tag" id="songTag"></div>
<div class="btn-group">
<button class="btn-sm green" id="songApprove" disabled>同意</button>
<button class="btn-sm red" id="songReject" disabled>否决</button>
<button class="btn-sm gray" id="songSkip" disabled>跳过 (不计次数)</button>
</div>
<div class="lyric" id="songLyric">[00:00.00] 暂无歌词</div>
</div>
</div>
<div id="result" class="result" style="display: none;"></div>
<div class="footer-link">
<a href="/">返回首页</a>
</div>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script>
(function () {
var k_cookie = 'ncm_cookie';
var cookie = localStorage.getItem(k_cookie) || '';
/* ---- DOM refs ---- */
var cookieInput = document.getElementById('cookie');
var resultDiv = document.getElementById('result');
/* ---- Init ---- */
if (cookie) {
cookieInput.value = cookie;
}
/* ---- Cookie ---- */
window.loadLocalCookie = function () {
var local = localStorage.getItem(k_cookie);
if (local) {
cookie = local;
cookieInput.value = local;
showResult('已成功读取本地 Cookie', 'success');
} else {
showResult('未在本地发现登录 Cookie,请先登录或手动粘贴', 'error');
}
};
window.clearCookie = function () {
cookie = '';
cookieInput.value = '';
localStorage.removeItem(k_cookie);
showResult('已清除本地 Cookie', 'info');
};
/* ---- Result ---- */
function hideResult() {
resultDiv.style.display = 'none';
}
function showResult(message, type) {
resultDiv.textContent = message;
resultDiv.className = 'result ' + type;
resultDiv.style.display = 'block';
}
function getCookie() {
var ck = cookieInput.value.trim();
if (ck) cookie = ck;
return ck || cookie || '';
}
/**
* 发起请求 (兼容旧版)
* @param {string} url
* @param {Record<string,string>} params
* @param {(resp:Record<string,any>)=>void} done
*/
function request(url, params, done) {
var xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.timeout = 1e4;
xhr.onload = function () {
var r = xhr.response;
if (r.code !== 200) {
showResult('错误 ' + r.code + ': ' + (r.message || '未知错误'), 'error');
} else {
done(r);
}
};
xhr.onerror = function () {
showResult('网络请求失败: ' + xhr.statusText, 'error');
};
var ck = getCookie();
if (ck) params.cookie = ck;
var s = '?_=' + Date.now();
for (var k in params) {
s += '&' + k + '=' + encodeURIComponent(params[k]);
}
xhr.open('GET', url + s);
xhr.send();
}
/// 用户信息 ///
var userAvatar = document.getElementById('userAvatar');
var userName = document.getElementById('userName');
var userSign = document.getElementById('userSign');
var userGet = document.getElementById('userGet');
userGet.onclick = function () {
request('rep/ugc/user/get', {}, function (res) {
var d = res.data;
if (d.imgUrl) userAvatar.src = d.imgUrl + '?param=50y50';
userName.textContent = d.name || '游客' + d.id;
userSign.disabled = d.signed;
showResult('用户信息加载成功', 'success');
});
};
userSign.onclick = function () {
if (!userSign.disabled) {
userSign.disabled = true;
request('rep/ugc/user/sign', {}, function (res) {
var d = res.data;
userSign.disabled = d;
showResult('签到' + (d ? '成功 ✓' : '失败 ✗'), d ? 'success' : 'error');
});
}
};
/// 会员领取 ///
var vipStatus = document.getElementById('vipStatus');
var vipCollect = document.getElementById('vipCollect');
var vipGet = document.getElementById('vipGet');
vipGet.onclick = function () {
request('rep/ugc/user/vip', {}, function (res) {
var d = res.data;
vipStatus.textContent = d.status + ': ' + d.title;
vipCollect.disabled = d.status !== 20;
showResult('会员详情加载成功', 'success');
});
};
vipCollect.onclick = function () {
if (!vipCollect.disabled) {
vipCollect.disabled = true;
request('rep/ugc/user/collect-vip', {}, function (res) {
var d = res.data;
vipCollect.disabled = d;
showResult('领取' + (d ? '成功 ✓' : '失败 ✗'), d ? 'success' : 'error');
});
}
};
/// 审核任务 ///
var taskCount = document.getElementById('taskCount');
var taskGoal = document.getElementById('taskGoal');
var taskStartBtn = document.getElementById('taskStart');
var taskGetBtn = document.getElementById('taskGet');
taskGetBtn.onclick = function () {
request('/rep/ugc/activity/get', {}, function (res) {
var d = res.data;
taskCount.textContent = d.count;
taskGoal.textContent = d.goalCount;
taskStartBtn.disabled = d.activityId !== 5001;
showResult('任务详情加载成功', 'success');
});
};
taskStartBtn.onclick = function () {
if (!taskStartBtn.disabled) taskStart();
};
/// 歌曲审核区域 ///
var song = document.getElementById('song');
var songCover = document.getElementById('songCover');
var songTitle = document.getElementById('songTitle');
var songAudio = document.getElementById('songAudio');
var songTag = document.getElementById('songTag');
var songApprove = document.getElementById('songApprove');
var songReject = document.getElementById('songReject');
var songSkip = document.getElementById('songSkip');
var songLyric = document.getElementById('songLyric');
/**
* 获取任务
*/
function taskStart() {
song.style.display = 'none';
request('/thinktank/audit/resource/detail', { type: '4' }, function (res) {
var d = res.data;
if (d.auditCount >= 50) {
request('rep/ugc/activity/collect', { activityId: '5001' }, function (res) {
showResult('积分领取' + (res.data ? '成功,可以兑换会员了!' : '失败'), res.data ? 'success' : 'error');
});
return;
}
songCover.src = d.coverUrl + '?param=100y100';
songTitle.textContent = '[' + d.resId + '] ' + d.resName + ' - ' + d.artists;
songTag.textContent = '[' + (d.auditCount + 1) + '/' + d.auditTaskCount + '] 当前结果: ' + d.initResult + '?';
songLyric.textContent = (d.lyric || '') + (d.transLyric || '');
if (!songLyric.textContent.trim()) songLyric.textContent = '[00:00.00] 暂无歌词';
songAudio.src = d.songUrl;
songAudio.play().catch(function () { /* 自动播放被浏览器阻止 */ });
songApprove.onclick = function () { if (!songApprove.disabled) taskSubmit(d.taskId, '1'); };
songReject.onclick = function () { if (!songReject.disabled) taskSubmit(d.taskId, '2'); };
songSkip.onclick = function () { if (!songSkip.disabled) taskSubmit(d.taskId, '3'); };
songApprove.disabled = songReject.disabled = songSkip.disabled = true;
song.style.display = 'block';
setTimeout(function () {
songApprove.disabled = songReject.disabled = songSkip.disabled = false;
}, 3e3);
});
}
/**
* 提交任务
* @param {string} taskId
* @param {'1'|'2'|'3'} judgement
*/
function taskSubmit(taskId, judgement) {
songApprove.disabled = songReject.disabled = songSkip.disabled = true;
request(
'thinktank/audit/resource/update',
{ type: '4', taskId: taskId, judgement: judgement },
function (res) {
var d = res.data;
if (d.result) {
showResult('审核提交成功,加载下一首...', 'success');
taskStart();
} else {
showResult('未知状态: ' + JSON.stringify(d), 'error');
}
},
);
}
/// 一键载入 ///
document.getElementById('loadall').onclick = function () {
userGet.click();
vipGet.click();
taskGetBtn.click();
showResult('正在载入所有数据...', 'info');
};
window.loadLocalCookie();
})();
</script>
</body>
</html>
+119
View File
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Token 注册器 — 网易云音乐 API Enhanced</title>
<style>
:root {
--fg: #333;
--muted: #666;
--border: #ddd;
--bg: #f5f5f5;
--panel: #ffffff;
--accent: #333;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; }
.container { max-width: 720px; margin: 40px auto; padding: 0 20px; }
@media (max-width: 480px) {
.container { margin: 20px auto; padding: 0 16px; }
header.site-header h1 { font-size: 22px; }
.block { padding: 16px; }
}
header.site-header { margin-bottom: 24px; }
header.site-header h1 { font-size: 24px; font-weight: 600; margin: 0; }
.sub { margin-top: 4px; color: var(--muted); font-size: 14px; }
.block { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); }
.block h3 { margin: 0 0 12px; font-size: 15px; font-weight: 600; }
pre { margin: 0; background: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; font-family: 'Courier New', monospace; font-size: 13px; max-height: 160px; overflow: auto; }
.row { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
button { padding: 8px 18px; border-radius: 8px; border: 1px solid var(--border); font-size: 14px; font-weight: 500; cursor: pointer; background: var(--panel); color: var(--fg); transition: all .15s; }
button:hover { background: #eee; }
button.primary { background: var(--fg); color: var(--panel); border-color: var(--fg); }
button.primary:hover { opacity: .85; }
button:disabled { opacity: .4; cursor: not-allowed; }
.tag { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; border: 1px solid var(--border); }
.tag.ok { background: #e6f7e6; color: #1a7a1a; border-color: #b7e6b7; }
.tag.no { background: #ffe6e6; color: #a11; border-color: #f5c6c6; }
.tag.busy { background: #fff3cd; color: #856404; border-color: #ffeeba; }
.flex { display: flex; align-items: center; gap: 10px; }
.flex.sb { justify-content: space-between; }
.mt-2 { margin-top: 14px; }
footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); text-align: center; font-size: 13px; }
</style>
</head>
<body>
<main class="container">
<header class="site-header">
<h1>🔑 Token 注册器</h1>
<p class="sub">从易盾获取反作弊 token 并注册到服务器,供广告等需要 checkToken 的接口使用</p>
</header>
<div class="block">
<div class="flex sb">
<h3>当前 Token</h3>
<span id="statusTag" class="tag no">未注册</span>
</div>
<pre id="tokenDisplay">(空)</pre>
<div class="row">
<button id="btnGet" class="primary">获取 Token</button>
<button id="btnRefresh">强制刷新</button>
</div>
</div>
<div class="block">
<h3>服务器响应</h3>
<pre id="respDisplay">{}</pre>
</div>
<footer class="site-footer">
<a href="/">← 返回首页</a>
</footer>
</main>
<script>
const $ = (id) => document.getElementById(id)
const base = location.origin
async function call(refresh) {
const btn = $('btnGet')
const btnR = $('btnRefresh')
btn.disabled = btnR.disabled = true
const tag = $('statusTag')
tag.className = 'tag busy'
tag.textContent = '获取中…'
try {
const url = base + '/register/checktoken/v2' + (refresh ? '?refresh=1' : '')
const res = await fetch(url)
const data = await res.json()
$('respDisplay').textContent = JSON.stringify(data, null, 2)
if (data.token) {
$('tokenDisplay').textContent = data.token
tag.className = 'tag ok'
tag.textContent = '已注册'
} else {
$('tokenDisplay').textContent = '(获取失败)'
tag.className = 'tag no'
tag.textContent = '失败'
}
} catch (e) {
$('respDisplay').textContent = JSON.stringify({ error: e.message }, null, 2)
tag.className = 'tag no'
tag.textContent = e.message
}
btn.disabled = btnR.disabled = false
}
$('btnGet').onclick = () => call(false)
$('btnRefresh').onclick = () => call(true)
call(false)
</script>
</body>
</html>
+5
View File
@@ -11,8 +11,13 @@
},
"APP_CONF": {
"apiDomain": "https://interface.music.163.com",
"eapiDomain": "https://interfacepc.music.163.com",
"xeapiDomain": "https://interface3.music.163.com",
"domain": "https://music.163.com",
"clDomian": "https://clientlog.music.163.com",
"clDomian3": "https://clientlog3.music.163.com",
"dunDomainV3": "https://ac.dun.163yun.com",
"dunDomainV2": "https://ac.dun.163.com",
"encrypt": true,
"encryptResponse": false,
"clientSign": "18:C0:4D:B9:8F:FE@@@453832335F384641365F424635335F303030315F303031425F343434415F343643365F333638332@@@@@@6ff673ef74955b38bce2fa8562d95c976ed4758b1227c4e9ee345987cee17bc9",
+481
View File
@@ -0,0 +1,481 @@
// NCBL 加密工具 —— 复制自 @netease-report-listen-song
// 提供 ChaCha20 / RSA-256 / NCBL 加密 / PLV+PLD 构建 / 设备上下文提取
const crypto = require('crypto')
const zlib = require('zlib')
const axios = require('axios')
const { APP_CONF } = require('./config.json')
const DOMAIN3 = APP_CONF.clDomian3
// ---- ChaCha20 纯 JS 实现 ----
const SIGMA = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]
const rotl = (x, n) => ((x << n) | (x >>> (32 - n))) >>> 0
const quarterRound = (s, a, b, c, d) => {
s[a] = (s[a] + s[b]) >>> 0
s[d] ^= s[a]
s[d] = rotl(s[d], 16)
s[c] = (s[c] + s[d]) >>> 0
s[b] ^= s[c]
s[b] = rotl(s[b], 12)
s[a] = (s[a] + s[b]) >>> 0
s[d] ^= s[a]
s[d] = rotl(s[d], 8)
s[c] = (s[c] + s[d]) >>> 0
s[b] ^= s[c]
s[b] = rotl(s[b], 7)
}
const chachaBlock = (key, counter, nonce) => {
const state = new Uint32Array(16)
state[0] = SIGMA[0]
state[1] = SIGMA[1]
state[2] = SIGMA[2]
state[3] = SIGMA[3]
for (let i = 0; i < 8; i++) state[4 + i] = key.readUInt32LE(i * 4)
state[12] = counter >>> 0
state[13] = nonce.readUInt32LE(0)
state[14] = nonce.readUInt32LE(4)
state[15] = nonce.readUInt32LE(8)
const work = state.slice()
for (let i = 0; i < 10; i++) {
quarterRound(work, 0, 4, 8, 12)
quarterRound(work, 1, 5, 9, 13)
quarterRound(work, 2, 6, 10, 14)
quarterRound(work, 3, 7, 11, 15)
quarterRound(work, 0, 5, 10, 15)
quarterRound(work, 1, 6, 11, 12)
quarterRound(work, 2, 7, 8, 13)
quarterRound(work, 3, 4, 9, 14)
}
const out = Buffer.allocUnsafe(64)
for (let i = 0; i < 16; i++)
out.writeUInt32LE((work[i] + state[i]) >>> 0, i * 4)
return out
}
const chacha20 = (key, counter, nonce, data) => {
const out = Buffer.allocUnsafe(data.length)
for (let off = 0; off < data.length; off += 64) {
const ks = chachaBlock(key, (counter + (off >>> 6)) >>> 0, nonce)
const end = Math.min(off + 64, data.length)
for (let i = off; i < end; i++) out[i] = data[i] ^ ks[i - off]
}
return out
}
// ---- RSA-256 Key Wrap (网易 NCBL 专用) ----
// 256-bit 模数 N 已被因式分解,故可直接计算私钥
const RSA_N =
0xfd90bd466ff9bc8a3fec2fbcf263b90d5c564879fa5d7aab89b31c1d5cb4139dn
const RSA_E = 65537n
const beToBig = (buf) => {
let n = 0n
for (const b of buf) n = (n << 8n) | BigInt(b)
return n
}
const bigToBe = (n, len) => {
const out = Buffer.alloc(len)
for (let i = len - 1; i >= 0; i--) {
out[i] = Number(n & 0xffn)
n >>= 8n
}
return out
}
const modPow = (base, exp, mod) => {
let result = 1n
base %= mod
while (exp > 0n) {
if (exp & 1n) result = (result * base) % mod
base = (base * base) % mod
exp >>= 1n
}
return result
}
const rsaWrap = (keyA) => {
return bigToBe(modPow(beToBig(keyA), RSA_E, RSA_N), 32)
}
// ---- NCBL 加密格式 ----
const MAGIC = Buffer.from('NCBL', 'ascii')
const NCBL_VERSION = 3
const HEADER_FIXED_LEN = 70
const META_BLOCK_TYPE = 0x4343
const DEFAULT_MAX_FRAME = 0x8000
const getCompress = () => {
if (typeof zlib.zstdCompressSync === 'function') {
return { compress: (buf) => zlib.zstdCompressSync(buf), name: 'zstd' }
}
return { compress: (buf) => zlib.gzipSync(buf), name: 'gzip' }
}
const encryptNCBL = (meta, body, opts = {}) => {
const metaBuf = Buffer.isBuffer(meta) ? meta : Buffer.from(meta, 'utf-8')
const bodyBuf = Buffer.isBuffer(body) ? body : Buffer.from(body, 'utf-8')
const maxFrame = opts.maxFrame || DEFAULT_MAX_FRAME
const keyA = opts.keyA || crypto.randomBytes(32)
if (keyA[0] >= 0xa3) keyA[0] = 0xa2
const keyB = rsaWrap(keyA)
const uuid = opts.uuid || crypto.randomBytes(16)
if (!opts.uuid) {
uuid[6] = (uuid[6] & 0x0f) | 0x40
uuid[8] = (uuid[8] & 0x3f) | 0x80
}
const nonce = uuid.subarray(0, 12)
const counter = uuid.readUInt32LE(12) >>> 2
const baseSeq = opts.baseSeq || crypto.randomBytes(2).readUInt16LE(0)
const metaCipher = chacha20(keyB, counter, nonce, metaBuf)
const metaBlock = Buffer.concat([
(() => {
const h = Buffer.allocUnsafe(4)
h.writeUInt16LE(META_BLOCK_TYPE, 0)
h.writeUInt16LE(metaCipher.length, 2)
return h
})(),
metaCipher,
])
const headerLen = HEADER_FIXED_LEN + metaBlock.length
const { compress } = getCompress()
const compressed = compress(bodyBuf)
const frames = []
let seq = baseSeq
for (let off = 0; off < compressed.length || off === 0; off += maxFrame) {
const slice = compressed.subarray(off, off + maxFrame)
const cipher = chacha20(keyA, counter, nonce, slice)
const head = Buffer.allocUnsafe(6)
head.writeUInt16LE(cipher.length, 0)
head.writeUInt32LE(seq >>> 0, 2)
frames.push(head, cipher)
seq++
if (compressed.length === 0) break
}
const trailing = Buffer.concat(frames)
const frameCount = seq - baseSeq
const header = Buffer.alloc(HEADER_FIXED_LEN)
MAGIC.copy(header, 0)
header.writeUInt32LE(NCBL_VERSION, 4)
header.writeUInt16LE(headerLen, 8)
uuid.copy(header, 10)
keyB.copy(header, 26)
header.writeUInt32LE(baseSeq >>> 0, 58)
header.writeUInt32LE((baseSeq + frameCount - 1) >>> 0, 62)
header.writeUInt32LE(trailing.length, 66)
return Buffer.concat([header, metaBlock, trailing])
}
// ---- 日志记录格式 ----
const FIELD_SEP = '\x01'
const buildRecord = ({ time, action, data }) => {
const json = typeof data === 'string' ? data : JSON.stringify(data)
return [time, action, json].join(FIELD_SEP)
}
const buildRecords = (records) => records.map(buildRecord).join('')
// ---- PLV / PLD 构建器 (桌面客户端格式) ----
const buildPlv = (ctx, song, source) => {
const now = Date.now()
const addRefer = `[F:63][${now}#933#${ctx.app.version}#${ctx.app.versionCode}#c9156c3][e][2][23][cell_pc_songlist_song:2|page_pc_songlist_songflow|page_mine_like_music][${song.id}:song:x:x|:::|${source.id}:list::]`
const multiRefers = [
'[F:26][s][18][_ai]',
'[F:26][s][12][_ai]',
`[F:63][${now}#933#${ctx.app.version}#${ctx.app.versionCode}#c9156c3][e][2][8][cell_pc_main_tab_entrance:6|page_pc_main_tab][我喜欢的音乐:spm::|:::]`,
'[F:26][s][5][_ai]',
'[F:26][s][0][_ai]',
]
return {
mode: 'circulation',
download: 0,
alg: '',
status: 'front',
id: String(song.id),
bitrate: song.bitrate,
type: 'song',
is_listentogether: 0,
source: source.name,
is_heart: 0,
resource_ratio: '',
resource_time: song.time,
musiceffect_id: '',
app_mode: 2,
bitrate_level: song.level,
_addrefer: addRefer,
_multirefers: multiRefers,
vipType: ctx.auth.vipType,
fee: 1,
file: 4,
rightSource: 0,
sourceId: source.id,
sourcetype: source.type,
libra_abt: '',
channel: ctx.app.channel,
curStartChannel: '',
}
}
const buildPld = (ctx, song, source, played) => {
const now = Date.now()
const addRefer = `[F:63][${now}#616#${ctx.app.version}#${ctx.app.versionCode}#c9156c3][e][2][92][btn_pc_cover_play|cell_pc_songlist_song:6|page_pc_songlist_songflow|page_mine_like_music][:::|${song.id}:song:x:x|:::|${source.id}:list::]`
const multiRefers = [
'[F:26][s][87][_ai]',
'[F:26][s][81][_ai]',
'[F:26][s][75][_ai]',
'[F:26][s][69][_ai]',
'[F:26][s][63][_ai]',
]
return {
mode: 'circulation',
download: 0,
alg: '',
status: 'front',
id: String(song.id),
time: played,
type: 'song',
is_listentogether: 0,
source: source.name,
is_heart: 0,
realtime: played,
resource_ratio: '',
resource_time: song.time,
musiceffect_id: '1001',
app_mode: 1,
lyriceffect: 'default',
displayMode: 'classic',
bitrate: song.bitrate,
bitrate_level: song.level,
_addrefer: addRefer,
_multirefers: multiRefers,
vipType: ctx.auth.vipType,
fee: 8,
file: 4,
rightSource: 0,
sourceId: source.id,
sourcetype: source.type,
end: 'interrupt',
libra_abt: '',
channel: ctx.app.channel,
curStartChannel: '',
}
}
// ---- Cookie → 设备/认证上下文 转换 ----
const extractContext = (cookieObj) => {
return {
app: {
id: cookieObj.appid || '',
urs: '',
pid: '',
nsm: cookieObj.WEVNSM || '1.0.0',
cid:
cookieObj.WNMCID ||
`${crypto.randomBytes(3).toString('hex')}.${Date.now()}.01.0`,
channel: cookieObj.channel || 'netease',
version: cookieObj.appver || '3.1.35',
versionCode: cookieObj.versioncode || '205293',
buildCode: cookieObj.buildver || '',
buildType: 'release',
packageId: '',
},
device: {
id: cookieObj.deviceId || cookieObj.sDeviceId || '',
ti: cookieObj.NMTID || '',
sign: cookieObj.clientSign || '',
model: cookieObj.mode || cookieObj.mobilename || '',
nnid: cookieObj._ntes_nnid || ',',
nuid: cookieObj._ntes_nuid || '',
csrf: cookieObj.__csrf || '',
systemType: cookieObj.os || 'pc',
systemVersion:
cookieObj.osver ||
'Microsoft-Windows-10-Professional-build-19045-64bit',
},
auth: {
id: cookieObj.uid || '',
token: cookieObj.MUSIC_U || '',
sessionId: cookieObj['JSESSIONID-WYYY'] || '',
vipType: cookieObj.vipType || '',
},
startTime: Date.now(),
processId: Math.floor(Math.random() * 90000) + 10000,
}
}
// 从 query.cookie 解析 cookie 对象
// 支持字符串 ("key=val; key2=val2") 或已解析的对象
// 注意:不做 URL decode!MUSIC_U 中的 '+' 会被 decodeURIComponent 错误地转为空格
const parseCookie = (cookie) => {
if (typeof cookie === 'object' && cookie !== null) return cookie
if (typeof cookie === 'string') {
const obj = {}
cookie.split(';').forEach((part) => {
const idx = part.indexOf('=')
if (idx > 0) {
const key = part.substring(0, idx).trim()
const val = part.substring(idx + 1).trim()
if (key) obj[key] = val
}
})
return obj
}
return {}
}
// ---- 辅助函数 ----
const randomUUID = () => {
if (typeof crypto.randomUUID === 'function') {
return crypto.randomUUID().replace(/-/g, '')
}
return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/x/g, () =>
Math.floor(Math.random() * 16).toString(16),
)
}
const randomHex = (len) => crypto.randomBytes(len / 2).toString('hex')
// ---- HTTP 上传工具 (NCBL 专用) ----
const buildMultipart = (payload) => {
const boundary = randomUUID()
const fileName = `op_${Math.floor(Math.random() * 90000) + 10000}_0_${Math.floor(Math.random() * 4294967295) + 1}`
const CRLF = '\r\n'
const headerLines = [
`--${boundary}`,
`Content-Disposition: form-data; name="file"; filename="${fileName}"`,
'Content-Type: multipart/form-data',
'',
'',
].join(CRLF)
const footer = `${CRLF}--${boundary}--${CRLF}`
return {
boundary,
fileName,
multipartBody: Buffer.concat([
Buffer.from(headerLines, 'utf-8'),
payload,
Buffer.from(footer, 'utf-8'),
]),
}
}
const buildCookieStr = (ctx) => {
const parts = [
`JSESSIONID-WYYY=${ctx.auth.sessionId}`,
`MUSIC_U=${ctx.auth.token}`,
`NMTID=${ctx.device.ti}`,
`WEVNSM=${ctx.app.nsm}`,
`WNMCID=${ctx.app.cid}`,
`__csrf=${ctx.device.csrf}`,
`__remember_me=true`,
`_iuqxldmzr_=33`,
`_ntes_nnid=${ctx.device.nnid}`,
`_ntes_nuid=${ctx.device.nuid}`,
`appver=${ctx.app.version}.${ctx.app.versionCode}`,
`channel=${ctx.app.channel}`,
`clientSign=${ctx.device.sign}`,
`deviceId=${ctx.device.id}`,
`mode=${ctx.device.model}`,
`ntes_kaola_ad=1`,
`os=${ctx.device.systemType}`,
`osver=${ctx.device.systemVersion}`,
]
return parts.join('; ')
}
const buildMetaJson = (ctx) =>
JSON.stringify({
'JSESSIONID-WYYY': ctx.auth.sessionId,
MUSIC_U: ctx.auth.token,
NMTID: ctx.device.ti,
WEVNSM: ctx.app.nsm,
WNMCID: ctx.app.cid,
__csrf: ctx.device.csrf,
_iuqxldmzr_: '33',
_ntes_nnid: ctx.device.nnid,
_ntes_nuid: ctx.device.nuid,
appver: `${ctx.app.version}.${ctx.app.versionCode}`,
channel: ctx.app.channel,
clientSign: ctx.device.sign,
deviceId: ctx.device.id,
mode: ctx.device.model,
ntes_kaola_ad: '1',
os: ctx.device.systemType,
osver: ctx.device.systemVersion,
})
const doUpload = async (ctx, metaJson, body, cookieStr, label) => {
const uploadUrl = DOMAIN3 + '/api/clientlog/encrypt/upload?multiupload=true'
const payload = encryptNCBL(metaJson, body)
const { boundary, fileName, multipartBody } = buildMultipart(payload)
const resp = await axios({
method: 'POST',
url: uploadUrl,
headers: {
'Content-Type': `multipart/form-data; boundary=${boundary}`,
Referer: 'https://music.163.com/di',
'User-Agent': `Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/${ctx.app.version}`,
'Accept-Encoding': 'gzip,deflate',
'Accept-Language': 'zh-CN,zh;q=0.8',
Cookie: cookieStr,
},
data: multipartBody,
maxBodyLength: 10 * 1024 * 1024,
timeout: 15000,
validateStatus: () => true,
})
const respBody = resp.data
const code = respBody?.code
const success =
code === 200 && respBody?.data?.successfiles?.includes?.(fileName)
return { success, fileName, payload, respBody }
}
// ---- 导出 ----
module.exports = {
chacha20,
rsaWrap,
encryptNCBL,
getCompress,
MAGIC,
NCBL_VERSION,
HEADER_FIXED_LEN,
META_BLOCK_TYPE,
DEFAULT_MAX_FRAME,
buildRecord,
buildRecords,
FIELD_SEP,
buildPlv,
buildPld,
extractContext,
parseCookie,
randomUUID,
randomHex,
buildMultipart,
buildCookieStr,
buildMetaJson,
doUpload,
}
+4 -2
View File
@@ -1,4 +1,4 @@
const createOption = (query, crypto = '') => {
const createOption = (query, crypto = '', checkToken = false) => {
return {
crypto: query.crypto || crypto || '',
cookie: query.cookie || process.env.NETEASE_COOKIE,
@@ -11,7 +11,9 @@ const createOption = (query, crypto = '') => {
: ['true', true].includes(query.randomCNIP),
e_r: query.e_r || undefined,
domain: query.domain || '',
checkToken: query.checkToken || false,
checkToken: query.checkToken || checkToken,
headers: query.headers || {},
timeout: query.timeout || 0,
}
}
module.exports = createOption
+30 -5
View File
@@ -18,6 +18,12 @@ const {
} = require('./index')
const { URLSearchParams, URL } = require('url')
const { APP_CONF } = require('../util/config.json')
const {
getToken: antiCheatTokenV2,
} = require('../module/register_checktoken_v2')
const {
getToken: antiCheatTokenV3,
} = require('../module/register_checktoken_v3')
// 预先读取匿名token并缓存
const anonymous_token = fs.readFileSync(
@@ -106,7 +112,7 @@ const userAgentMap = {
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
},
api: {
pc: 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/3.0.18.203152',
pc: 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/3.1.29.205117',
android:
'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)',
iphone: 'NeteaseMusic 9.0.90/5038 (iPhone; iOS 16.2; zh_CN)',
@@ -116,6 +122,7 @@ const userAgentMap = {
// 预先定义常量
const DOMAIN = APP_CONF.domain
const API_DOMAIN = APP_CONF.apiDomain
const EAPI_DOMAIN = APP_CONF.eapiDomain
const XEAPI_DOMAIN = APP_CONF.xeapiDomain
const ENCRYPT_RESPONSE = APP_CONF.encryptResponse
const SPECIAL_STATUS_CODES = new Set([201, 302, 400, 502, 800, 801, 802, 803])
@@ -181,6 +188,16 @@ const generateRequestId = () => {
}
const createRequest = (uri, data, options) => {
let token = ''
switch (options.checkToken) {
case 'v2':
token = antiCheatTokenV2()
break
case 'v3':
token = antiCheatTokenV3()
break
}
return new Promise((resolve, reject) => {
// 变量声明和初始化
const headers = options.headers ? { ...options.headers } : {}
@@ -264,6 +281,9 @@ const createRequest = (uri, data, options) => {
headers['x-sdeviceid'] = cookie.sDeviceId || cookie.deviceId
headers['x-buildver'] = xeapiBuildver
if (cookie.MUSIC_U) headers['x-music-u'] = cookie.MUSIC_U
if (options.checkToken) {
headers['X-antiCheatToken'] = token
}
const xeapiCookie = {
...cookie,
os: xeapiOs,
@@ -302,14 +322,14 @@ const createRequest = (uri, data, options) => {
__csrf: csrfToken,
channel: cookie.channel,
requestId: generateRequestId(),
...(options.checkToken
? { 'X-antiCheatToken': APP_CONF.checkToken }
: {}),
// clientSign: APP_CONF.clientSign,
}
if (cookie.MUSIC_U) header['MUSIC_U'] = cookie.MUSIC_U
if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A
if (options.checkToken) {
header['X-antiCheatToken'] = token
}
headers['Cookie'] = createHeaderCookie(header)
headers['User-Agent'] =
@@ -323,7 +343,7 @@ const createRequest = (uri, data, options) => {
data.header = header
encryptData = encrypt.eapi(uri, data)
url = (options.domain || API_DOMAIN) + '/eapi/' + uri.substr(5)
url = (options.domain || EAPI_DOMAIN) + '/eapi/' + uri.substr(5)
} else if (crypto === 'api') {
url = (options.domain || API_DOMAIN) + uri
encryptData = data
@@ -344,6 +364,11 @@ const createRequest = (uri, data, options) => {
httpsAgent: createHttpsAgent(),
}
// 自定义超时
if (options.timeout > 0) {
settings.timeout = options.timeout
}
// 使用返回值加密
const use_e_r = (crypto === 'eapi' || crypto === 'weapi') && data.e_r
const use_xeapi = crypto === 'xeapi'