mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-10-20 22:51:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63d89aa906 | ||
|
|
8c212be86b | ||
|
|
89de56d2a2 | ||
|
|
d298025d7c | ||
|
|
a3287a0180 | ||
|
|
9adfd50c58 | ||
|
|
fda7c9e5b3 | ||
|
|
a8266c6d7b | ||
|
|
64a6b9677a | ||
|
|
20d3a1a7a5 | ||
|
|
4826c65042 | ||
|
|
7822ab08d3 | ||
|
|
41bd6d82ce | ||
|
|
6f8dd1d971 | ||
|
|
ddd0af5474 | ||
|
|
fbd2e06419 | ||
|
|
4442fb4341 | ||
|
|
35d1c61cb4 | ||
|
|
a183e153ef | ||
|
|
2fa78c3826 | ||
|
|
5ae1eab14c | ||
|
|
321c25bd7d | ||
|
|
f527c1ef73 | ||
|
|
0a5a1242f2 | ||
|
|
6946dc8e14 | ||
|
|
22b78779ca | ||
|
|
35607f5050 | ||
|
|
60af95ac06 | ||
|
|
46073e5f50 | ||
|
|
bf12abe2f1 | ||
|
|
1491b91829 | ||
|
|
63f669d7f8 | ||
|
|
d525204dda | ||
|
|
6eb31984fd | ||
|
|
9faeb0aa25 | ||
|
|
f6db3e76e9 | ||
|
|
9163d48f0a |
@@ -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
|
||||
```
|
||||
|
||||
|
||||
Vendored
+25
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// 获取免费听时长状态
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
entrance: 'FREE_LISTEN_RN',
|
||||
}
|
||||
return request(
|
||||
`/api/ad/homepage/free/tab/extend/v2`,
|
||||
data,
|
||||
createOption(query, 'xeapi'),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
)
|
||||
}
|
||||
@@ -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'),
|
||||
)
|
||||
}
|
||||
@@ -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'),
|
||||
)
|
||||
}
|
||||
@@ -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'),
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -25,6 +25,6 @@ module.exports = (query, request) => {
|
||||
return request(
|
||||
`/api/resource/comments/${query.t}`,
|
||||
data,
|
||||
createOption(query, 'weapi'),
|
||||
createOption(query, 'eapi', 'v2'),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
)
|
||||
}
|
||||
@@ -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'),
|
||||
)
|
||||
}
|
||||
@@ -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'),
|
||||
)
|
||||
}
|
||||
@@ -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'),
|
||||
)
|
||||
}
|
||||
@@ -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'),
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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`,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 云小编每日抽奖
|
||||
//
|
||||
// activityId:
|
||||
// 默认 6501202
|
||||
//
|
||||
// drawCount:
|
||||
// 默认 1
|
||||
//
|
||||
// checkToken:
|
||||
// 易盾反作弊 Token
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
activityId: query.activityId || '6501202',
|
||||
drawCount: query.drawCount || '1',
|
||||
}
|
||||
return request(
|
||||
`/api/middle/play/do/lottery`,
|
||||
data,
|
||||
createOption(query, 'eapi', 'v3'),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 云小编抽奖剩余次数查询
|
||||
//
|
||||
// activityId:
|
||||
// 默认 6501202
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
activityId: query.activityId || '6501202',
|
||||
}
|
||||
return request(
|
||||
`/api/middle/play/lottery/remain/chance`,
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
@@ -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'))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// 易盾反作弊 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 },
|
||||
}
|
||||
}
|
||||
|
||||
// 模块加载时自动获取一次 Token,确保 request.js 使用时不为空
|
||||
fetch()
|
||||
.then((token) => {
|
||||
_token = token
|
||||
})
|
||||
.catch(() => {
|
||||
// 静默失败,后续请求时会重试
|
||||
})
|
||||
|
||||
// 给 request.js 读取用
|
||||
module.exports.getToken = () => {
|
||||
if (!_token) {
|
||||
// 如果 Token 为空,异步触发获取
|
||||
fetch()
|
||||
.then((token) => {
|
||||
_token = token
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
return _token
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// 云小编活动信息
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
return request(`/api/rep/ugc/activity/get`, {}, createOption(query, 'eapi'))
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 云小编考试状态
|
||||
//
|
||||
// examType:
|
||||
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||
// 2. 歌曲语种审核: languageEnter
|
||||
// 3. 歌曲原唱审核: oriSingerEnter
|
||||
// 4. 情绪标签审核: emotionEnter
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
if (!query.examType)
|
||||
return Promise.reject({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
message: '参数不足',
|
||||
},
|
||||
})
|
||||
const data = {
|
||||
examType: query.examType,
|
||||
}
|
||||
return request(
|
||||
'/api/rep/ugc/exam/info/get',
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 云小编考试取题
|
||||
//
|
||||
// examType:
|
||||
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||
// 2. 歌曲语种审核: languageEnter
|
||||
// 3. 歌曲原唱审核: oriSingerEnter
|
||||
// 4. 情绪标签审核: emotionEnter
|
||||
//
|
||||
// taskId:
|
||||
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
if (!query.examType || !query.taskId)
|
||||
return Promise.reject({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
message: '参数不足',
|
||||
},
|
||||
})
|
||||
const data = {
|
||||
examType: query.examType,
|
||||
taskId: query.taskId,
|
||||
}
|
||||
return request(
|
||||
'/api/rep/ugc/exam/question/single/get',
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 云小编考试结果
|
||||
//
|
||||
// examType:
|
||||
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||
// 2. 歌曲语种审核: languageEnter
|
||||
// 3. 歌曲原唱审核: oriSingerEnter
|
||||
// 4. 情绪标签审核: emotionEnter
|
||||
//
|
||||
// taskId:
|
||||
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
if (!query.examType || !query.taskId)
|
||||
return Promise.reject({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
message: '参数不足',
|
||||
},
|
||||
})
|
||||
const data = {
|
||||
examType: query.examType,
|
||||
taskId: query.taskId,
|
||||
}
|
||||
return request(
|
||||
'/api/rep/ugc/exam/result/get',
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 云小编考试开始
|
||||
//
|
||||
// examType:
|
||||
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||
// 2. 歌曲语种审核: languageEnter
|
||||
// 3. 歌曲原唱审核: oriSingerEnter
|
||||
// 4. 情绪标签审核: emotionEnter
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
if (!query.examType)
|
||||
return Promise.reject({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
message: '参数不足',
|
||||
},
|
||||
})
|
||||
const data = {
|
||||
examType: query.examType,
|
||||
}
|
||||
return request('/api/rep/ugc/exam/start', data, createOption(query, 'eapi'))
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// 云小编考试提交
|
||||
//
|
||||
// examType:
|
||||
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||
// 2. 歌曲语种审核: languageEnter
|
||||
// 3. 歌曲原唱审核: oriSingerEnter
|
||||
// 4. 情绪标签审核: emotionEnter
|
||||
//
|
||||
// taskId:
|
||||
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
|
||||
//
|
||||
// questionId:
|
||||
// 调用 rep/ugc/exam/question/single/get 获取
|
||||
//
|
||||
// answer:
|
||||
// A 对, B 错
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
if (!query.examType || !query.taskId || !query.questionId || !query.answer)
|
||||
return Promise.reject({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
message: '参数不足',
|
||||
},
|
||||
})
|
||||
const data = {
|
||||
examType: query.examType,
|
||||
taskId: query.taskId,
|
||||
questionId: query.questionId,
|
||||
answer: query.answer,
|
||||
}
|
||||
return request('/api/rep/ugc/exam/submit', data, createOption(query, 'eapi'))
|
||||
}
|
||||
@@ -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'),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// 云小编获取用户详情
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
return request(`/api/rep/ugc/user/get`, {}, createOption(query, 'eapi'))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// 云小编每日签到
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
return request(`/api/rep/ugc/user/sign`, {}, createOption(query, 'eapi'))
|
||||
}
|
||||
@@ -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'))
|
||||
}
|
||||
@@ -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`,
|
||||
|
||||
@@ -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'),
|
||||
)
|
||||
}
|
||||
@@ -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'),
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@neteasecloudmusicapienhanced/api",
|
||||
"version": "4.36.1",
|
||||
"version": "4.39.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.3",
|
||||
"axios": "^1.18.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.61.1",
|
||||
"@typescript-eslint/parser": "^8.61.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.4",
|
||||
"prettier": "^3.9.6",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
Generated
+210
-201
File diff suppressed because it is too large
Load Diff
+494
-44
@@ -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,19 +322,41 @@
|
||||
<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>
|
||||
<textarea id="data" name="data">
|
||||
<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,
|
||||
"id": "2058263032",
|
||||
@@ -160,64 +368,306 @@
|
||||
"ytv": 0,
|
||||
"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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ===== Tab 切换 =====
|
||||
function syncJsonToKv() {
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/api?timestamp=${Date.now()}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
uri: uri,
|
||||
data: JSON.parse(data),
|
||||
crypto: crypto,
|
||||
},
|
||||
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]));
|
||||
});
|
||||
|
||||
document.getElementById('result').value = JSON.stringify(res.data, null, 2);
|
||||
} catch (error) {
|
||||
document.getElementById('result').value = 'Request failed: ' + error.message;
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
(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 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';
|
||||
}
|
||||
if (crypto) {
|
||||
const cryptoSelect = document.getElementById('crypto');
|
||||
if ([...cryptoSelect.options].some((opt) => opt.value === crypto)) {
|
||||
cryptoSelect.value = crypto;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (dataParam) {
|
||||
const decoded = dataParam;
|
||||
try {
|
||||
document.getElementById('data').value = JSON.stringify(
|
||||
JSON.parse(decoded),
|
||||
null,
|
||||
2,
|
||||
|
||||
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;
|
||||
}
|
||||
} catch (error) {
|
||||
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 */
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
+338
-5
@@ -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:取消收藏
|
||||
@@ -3612,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
|
||||
@@ -3742,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, 可获取歌手粉丝
|
||||
@@ -4810,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 权限
|
||||
@@ -5422,7 +5488,274 @@ 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`
|
||||
|
||||
### 获取免费听时长状态
|
||||
|
||||
说明 : 登录后调用此接口, 获取免费听剩余时长
|
||||
|
||||
**接口地址 :** `/ad/listening/rights`
|
||||
|
||||
**调用例子 :** `/ad/listening/rights`
|
||||
|
||||
### 云小编 - 获取用户详情
|
||||
|
||||
说明: 登录后调用此接口, 获取云小编用户详情
|
||||
|
||||
**接口地址:** `/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`
|
||||
|
||||
### 云小编 - 剩余抽奖次数
|
||||
|
||||
说明: 登录后调用此接口, 获取今日云小编抽奖剩余次数
|
||||
|
||||
**可选参数:**
|
||||
|
||||
`activityId`: 活动 ID, 默认 `6501202`
|
||||
|
||||
**接口地址:** `/middle/play/lottery/remain/chance`
|
||||
|
||||
**调用例子:** `/middle/play/lottery/remain/chance?activityId=6501202`
|
||||
|
||||
### 云小编 - 每日抽奖
|
||||
|
||||
说明: 登录后调用此接口, 消耗 200 积分进行抽奖, 每日最多抽 3 次
|
||||
|
||||
> 注意: 抽奖失败也消耗每日次数, 请先调用 `/rep/ugc/user/get` 查询可用积分
|
||||
|
||||
**可选参数:**
|
||||
|
||||
`activityId`: 活动 ID, 默认 `6501202`
|
||||
|
||||
`drawCount`: 未知, 默认 `1`
|
||||
|
||||
`checkToken`: 易盾反作弊 Token, 默认自动获取
|
||||
|
||||
**接口地址:** `/middle/play/do/lottery`
|
||||
|
||||
**调用例子:** `/middle/play/do/lottery?activityId=6501202&drawCount=1`
|
||||
|
||||
### 发送/删除评论
|
||||
|
||||
说明 : 调用此接口,可发送评论或者删除评论
|
||||
|
||||
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`
|
||||
|
||||
## 离线访问此文档
|
||||
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
<!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); }
|
||||
|
||||
/* ---- 剩余时长计时器 ---- */
|
||||
.timer-wrap { text-align: center; padding: 12px 0 4px; }
|
||||
.timer-row { display: flex; align-items: baseline; justify-content: center; gap: 2px; font-variant-numeric: tabular-nums; }
|
||||
.timer-num { font-size: 36px; font-weight: 700; letter-spacing: 1px; line-height: 1.2; font-family: 'SF Mono', 'Courier New', monospace; min-width: 2ch; text-align: center; }
|
||||
.timer-label { font-size: 13px; color: var(--muted); margin-right: 12px; }
|
||||
.timer-label:last-child { margin-right: 0; }
|
||||
.timer-sep { font-size: 28px; font-weight: 300; color: #bbb; margin: 0 2px; }
|
||||
.timer-unit { display: flex; flex-direction: column; align-items: center; }
|
||||
</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>获取广告 & 提取 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>
|
||||
|
||||
<!-- 剩余时长计时器 -->
|
||||
<div class="block" id="rightsBlock">
|
||||
<div class="flex sb">
|
||||
<h3 id="rightsTitleDisplay">🕐 免费听时长剩余</h3>
|
||||
<span id="rightsTag" class="tag no">未查询</span>
|
||||
</div>
|
||||
<p class="desc" id="rightsSubDesc">查询你的免费听权益剩余时长</p>
|
||||
<div class="timer-wrap">
|
||||
<div class="timer-row" id="timerRow">
|
||||
<div class="timer-unit">
|
||||
<span class="timer-num" id="timerDays">--</span>
|
||||
<span class="timer-label">天</span>
|
||||
</div>
|
||||
<span class="timer-sep">:</span>
|
||||
<div class="timer-unit">
|
||||
<span class="timer-num" id="timerHours">--</span>
|
||||
<span class="timer-label">时</span>
|
||||
</div>
|
||||
<span class="timer-sep">:</span>
|
||||
<div class="timer-unit">
|
||||
<span class="timer-num" id="timerMinutes">--</span>
|
||||
<span class="timer-label">分</span>
|
||||
</div>
|
||||
<span class="timer-sep">:</span>
|
||||
<div class="timer-unit">
|
||||
<span class="timer-num" id="timerSeconds">--</span>
|
||||
<span class="timer-label">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="justify-content:center">
|
||||
<button id="queryRightsBtn" class="primary">⟳ 查询剩余时长</button>
|
||||
<button id="claim30Btn">领取30分钟</button>
|
||||
</div>
|
||||
<div class="info-grid" id="rightsInfo" style="display:none">
|
||||
<div class="lbl">状态</div><div id="rightsStatus">-</div>
|
||||
<div class="lbl">结束时间</div><div id="rightsEndTimeDisplay">-</div>
|
||||
<div class="lbl">今日已覆盖</div><div id="rightsCoverToday">-</div>
|
||||
</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)
|
||||
// 领取成功后自动刷新剩余时长
|
||||
queryRights()
|
||||
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
|
||||
|
||||
// 重置计时器
|
||||
stopCountdown()
|
||||
$('timerDays').textContent = '--'
|
||||
$('timerHours').textContent = '--'
|
||||
$('timerMinutes').textContent = '--'
|
||||
$('timerSeconds').textContent = '--'
|
||||
setTag('rightsTag', 'no', '未查询')
|
||||
$('rightsInfo').style.display = 'none'
|
||||
$('rightsTitleDisplay').textContent = '🕐 免费听时长剩余'
|
||||
$('rightsSubDesc').textContent = '查询你的免费听权益剩余时长'
|
||||
$('runAll').disabled = true
|
||||
}
|
||||
|
||||
// ===== 剩余时长计时器 =====
|
||||
let countdownTimer = null
|
||||
let remainingMs = 0
|
||||
|
||||
/** 毫秒 → { d, h, m, s } */
|
||||
function parseMs(ms) {
|
||||
if (ms <= 0) return { d: 0, h: 0, m: 0, s: 0 }
|
||||
const totalSec = Math.floor(ms / 1000)
|
||||
return {
|
||||
d: Math.floor(totalSec / 86400),
|
||||
h: Math.floor((totalSec % 86400) / 3600),
|
||||
m: Math.floor((totalSec % 3600) / 60),
|
||||
s: totalSec % 60
|
||||
}
|
||||
}
|
||||
|
||||
function renderTimer(ms) {
|
||||
const t = parseMs(ms)
|
||||
$('timerDays').textContent = String(t.d).padStart(2, '0')
|
||||
$('timerHours').textContent = String(t.h).padStart(2, '0')
|
||||
$('timerMinutes').textContent = String(t.m).padStart(2, '0')
|
||||
$('timerSeconds').textContent = String(t.s).padStart(2, '0')
|
||||
}
|
||||
|
||||
function stopCountdown() {
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer)
|
||||
countdownTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function startCountdown(ms) {
|
||||
stopCountdown()
|
||||
remainingMs = Math.max(0, ms)
|
||||
renderTimer(remainingMs)
|
||||
if (remainingMs <= 0) {
|
||||
setTag('rightsTag', 'no', '已用完')
|
||||
return
|
||||
}
|
||||
countdownTimer = setInterval(() => {
|
||||
remainingMs = Math.max(0, remainingMs - 1000)
|
||||
renderTimer(remainingMs)
|
||||
if (remainingMs <= 0) {
|
||||
stopCountdown()
|
||||
setTag('rightsTag', 'no', '已过期')
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function queryRights() {
|
||||
const btn = $('queryRightsBtn')
|
||||
btn.disabled = true
|
||||
setTag('rightsTag', 'busy', '查询中…')
|
||||
try {
|
||||
const res = await fetch(nocache(base + '/ad/listening/rights'))
|
||||
const data = await res.json()
|
||||
if (data.code === 200 && data.data) {
|
||||
const d = data.data
|
||||
$('rightsTitleDisplay').textContent = '🕐 ' + (d.title || '免费听时长剩余')
|
||||
$('rightsSubDesc').textContent = d.vipInfoContent || '查询你的免费听权益剩余时长'
|
||||
$('rightsInfo').style.display = 'grid'
|
||||
|
||||
// 状态
|
||||
const status = d.status || ''
|
||||
$('rightsStatus').textContent = status
|
||||
|
||||
// 结束时间
|
||||
if (d.rightsEndTime) {
|
||||
const endDate = new Date(d.rightsEndTime)
|
||||
$('rightsEndTimeDisplay').textContent = endDate.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
|
||||
}
|
||||
|
||||
// 今日已覆盖
|
||||
$('rightsCoverToday').textContent = d.rightsCoverToday ? '是 ✓' : '否'
|
||||
|
||||
// 倒计时
|
||||
if (d.rightsRemainingTime > 0) {
|
||||
startCountdown(d.rightsRemainingTime)
|
||||
setTag('rightsTag', 'ok', '已解锁 ✓')
|
||||
} else {
|
||||
renderTimer(0)
|
||||
setTag('rightsTag', 'no', '已用完')
|
||||
}
|
||||
|
||||
// 领取按钮(仅当 step3 可用时启用)
|
||||
if (d.cardContent && d.cardContent.actionTitle && $('step3').disabled === false) {
|
||||
$('claim30Btn').disabled = false
|
||||
$('claim30Btn').textContent = d.cardContent.actionTitle
|
||||
} else {
|
||||
$('claim30Btn').disabled = true
|
||||
}
|
||||
|
||||
if (d.rightsUpperLimit) {
|
||||
$('rightsStatus').textContent += ' (已达上限)'
|
||||
}
|
||||
|
||||
return data.data
|
||||
}
|
||||
setTag('rightsTag', 'no', '查询失败')
|
||||
return null
|
||||
} catch (e) {
|
||||
setTag('rightsTag', 'no', e.message)
|
||||
return null
|
||||
} finally {
|
||||
btn.disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
$('queryRightsBtn').onclick = queryRights
|
||||
|
||||
$('claim30Btn').onclick = async function () {
|
||||
const ok = await step3()
|
||||
if (ok) await queryRights()
|
||||
}
|
||||
|
||||
// 页面加载后自动查询一次
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(queryRights, 300)
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+5
-1
@@ -96,7 +96,11 @@ curl -s {origin}/search?keywords=网易云</code></pre>
|
||||
<a href="/listen_together_host.html">一起听示例</a> ·
|
||||
<a href="/playlist_cover_update.html">更新歌单封面示例</a> ·
|
||||
<a href="/avatar_update.html">头像更新示例</a> ·
|
||||
<a href="/scrobble.html">听歌打卡示例</a>
|
||||
<a href="/scrobble.html">听歌打卡示例</a> ·
|
||||
<a href="/yidun.html">获取 CheckToken</a> ·
|
||||
<a href="/free_listen.html">免费听权益</a> ·
|
||||
<a href="/ugc.html">云小编任务中心</a> ·
|
||||
<a href="/ugc_lottery.html">云小编每日抽奖</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
+882
@@ -0,0 +1,882 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<link rel="icon" href="docs/netease.png" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<link rel="icon" href="docs/netease.png" />
|
||||
<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;
|
||||
}
|
||||
|
||||
#userPoint {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* ---- 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: 160px;
|
||||
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>
|
||||
<span onclick="saveCookie()">保存 Cookie</span>
|
||||
<span onclick="loadAll()">一键载入</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 src="docs/netease.png" alt="头像" class="avatar" id="userAvatar" />
|
||||
<span class="info-text" id="userName">未登录</span>
|
||||
<span id="userPoint">0</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>] 审核歌曲赚贡献分 (+<span id="taskPoint">0</span>)</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>
|
||||
(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');
|
||||
};
|
||||
|
||||
window.saveCookie = function () {
|
||||
var ck = getCookie()
|
||||
if (ck) localStorage.setItem(k_cookie, ck)
|
||||
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 userPoint = document.getElementById('userPoint');
|
||||
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;
|
||||
userPoint.textContent = d.availablePoints;
|
||||
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');
|
||||
if (d) userGet.click();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/// 会员领取 ///
|
||||
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 taskPoint = document.getElementById('taskPoint');
|
||||
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;
|
||||
taskPoint.textContent = d.points;
|
||||
// 注:超过 100 可能有 bug,导致 thinktank/audit/resource/detail 获取的任务 ID 一直是 5002,目标数量也不对,暂时限制前两个任务
|
||||
taskStartBtn.disabled = d.activityId !== 5001 && d.activityId !== 5002; // 5001: 1-50, 5002: 51-100
|
||||
showResult('任务详情加载成功', 'success');
|
||||
// 任务完成,领取积分
|
||||
if (d.count >= d.goalCount) {
|
||||
request('rep/ugc/activity/collect', { activityId: d.activityId }, function (res) {
|
||||
var d = res.data;
|
||||
showResult('积分领取' + (d ? '成功,可以兑换会员了!' : '失败'), d ? 'success' : 'error');
|
||||
// 刷新任务状态
|
||||
if (d) loadAll();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
taskStartBtn.onclick = function () {
|
||||
if (!taskStartBtn.disabled) {
|
||||
songAudio.pause();
|
||||
examInit();
|
||||
}
|
||||
};
|
||||
|
||||
/// 歌曲审核区域 ///
|
||||
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');
|
||||
|
||||
var examType = 'emotionEnter';
|
||||
// 考试初始化、检查状态
|
||||
function examInit() {
|
||||
request('rep/ugc/exam/info/get', { examType: examType }, function (res) {
|
||||
var d = res.data;
|
||||
switch (d.process) {
|
||||
case 'whole_exam_end':
|
||||
// 通过考试
|
||||
if (d.hasPassExamination) {
|
||||
taskStart();
|
||||
return;
|
||||
}
|
||||
request('rep/ugc/exam/result/get', { examType: examType, taskId: d.taskId }, function (res) {
|
||||
var d = res.data;
|
||||
showResult('考试未通过:正确数:' + d.wrightNum + ',正确率:' + d.wrightRate, 'error');
|
||||
});
|
||||
case 'no':
|
||||
// 未通过或未开始
|
||||
request('rep/ugc/exam/start', { examType: examType }, function (res) {
|
||||
examStart(res.data.taskId);
|
||||
});
|
||||
break;
|
||||
case 'process':
|
||||
// 正在考试
|
||||
examStart(d.taskId);
|
||||
break;
|
||||
default:
|
||||
showResult('未知考试进度:' + d.process, 'error');
|
||||
return;
|
||||
}
|
||||
// 在上个信息不为异常时显示考试中提示
|
||||
if (d.process !== 'whole_exam_end' && !resultDiv.className.endsWith('error')) showResult('正在考试,请认真审题!', 'info');
|
||||
});
|
||||
}
|
||||
// 考试取题开始
|
||||
function examStart(taskId) {
|
||||
request('rep/ugc/exam/question/single/get', { examType: examType, taskId: taskId }, function (res) {
|
||||
var d = res.data;
|
||||
|
||||
songCover.src = d.coverUrl + '?param=100y100';
|
||||
songTitle.textContent = '[' + d.resId + '] ' + d.resName + ' - ' + d.artists;
|
||||
songTag.textContent = '[' + (d.auditCount + 1) + '/' + d.auditTaskCount + '] 当前结果: ' + d.questionContent + '?';
|
||||
songLyric.textContent = d.lyric ? d.transLyric ? lrctran(d.lyric, d.transLyric) : d.lyric : '[00:00.00] 暂无歌词';
|
||||
songLyric.scrollTop = 0;
|
||||
// 有时候就是没有播放链接,只能根据歌词硬审
|
||||
if (d.songUrl) {
|
||||
songAudio.src = d.songUrl;
|
||||
songAudio.play().catch(function () { /* 自动播放被浏览器阻止 */ });
|
||||
} else {
|
||||
songAudio.src = ''; // 清理播放器
|
||||
}
|
||||
songApprove.onclick = function () { if (!songApprove.disabled) examSubmit(taskId, d.questionId, 'A') };
|
||||
songReject.onclick = function () { if (!songReject.disabled) examSubmit(taskId, d.questionId, 'B') };
|
||||
songSkip.onclick = null;
|
||||
|
||||
songApprove.disabled = songReject.disabled = songSkip.disabled = true;
|
||||
song.style.display = 'block';
|
||||
|
||||
setTimeout(function () {
|
||||
songApprove.disabled = songReject.disabled = false;
|
||||
}, 3e3);
|
||||
});
|
||||
}
|
||||
// 考试提交
|
||||
function examSubmit(taskId, questionId, answer) {
|
||||
songAudio.pause();
|
||||
songApprove.disabled = songReject.disabled = songSkip.disabled = true;
|
||||
request(
|
||||
'rep/ugc/exam/submit',
|
||||
{ examType: examType, taskId: taskId, questionId: questionId, answer: answer },
|
||||
function (res) {
|
||||
var d = res.data;
|
||||
if (d.result) {
|
||||
showResult('回答正确,加载下一首...', 'success');
|
||||
} else {
|
||||
showResult('回答错误:' + d.analysis, 'error');
|
||||
}
|
||||
examInit();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务
|
||||
*/
|
||||
function taskStart() {
|
||||
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) {
|
||||
var d = res.data;
|
||||
showResult('积分领取' + (d ? '成功,可以兑换会员了!' : '失败'), d ? 'success' : 'error');
|
||||
// 刷新任务状态
|
||||
if (d) {
|
||||
vipGet.click();
|
||||
taskGetBtn.click();
|
||||
}
|
||||
});
|
||||
songAudio.pause()
|
||||
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 ? lrctran(d.lyric, d.transLyric) : d.lyric : '[00:00.00] 暂无歌词';
|
||||
songLyric.scrollTop = 0;
|
||||
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) {
|
||||
songAudio.pause();
|
||||
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((d.test ? '抽查' : '审核') + '提交成功,加载下一首...', 'success');
|
||||
taskStart();
|
||||
} else if (d.test) {
|
||||
showResult('抽查失败: [' + d.dayErrorNum + '/' + d.dayErrorNumStandard + '] ' + d.errorAnalysis, 'error');
|
||||
taskStart();
|
||||
} else {
|
||||
showResult('未知状态: ' + JSON.stringify(d), 'error');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**解析歌词时间戳和文本 */
|
||||
function lrctrim(lyrics) {
|
||||
var lines = lyrics.split('\n');
|
||||
var data = [];
|
||||
lines.forEach(function (line, index) {
|
||||
var matches = line.match(/\[(\d{2}):(\d{2}[\.:]?\d*)\]/);
|
||||
if (matches) {
|
||||
var minutes = parseInt(matches[1], 10);
|
||||
var seconds = parseFloat(matches[2].replace('.', ':')) || 0;
|
||||
var timestamp = minutes * 6e4 + seconds * 1e3;
|
||||
var text = line.replace(/\[\d{2}:\d{2}[\.:]?\d*\]/g, '').trim();
|
||||
text = text.replace(/\s\s+/g, ' '); // Replace multiple spaces with a single space
|
||||
data.push([timestamp, index, text]);
|
||||
}
|
||||
});
|
||||
data.sort(function (a, b) {
|
||||
return a[0] - b[0];
|
||||
});
|
||||
return data;
|
||||
}
|
||||
/**合并原文歌词和翻译歌词 */
|
||||
function lrctran(lyric, tlyric) {
|
||||
lyric = lrctrim(lyric);
|
||||
tlyric = lrctrim(tlyric);
|
||||
var len1 = lyric.length;
|
||||
var len2 = tlyric.length;
|
||||
var result = '';
|
||||
for (var i = 0, j = 0; i < len1 && j < len2; i++) {
|
||||
while (lyric[i][0] > tlyric[j][0] && j + 1 < len2) {
|
||||
j++;
|
||||
}
|
||||
if (lyric[i][0] === tlyric[j][0]) {
|
||||
tlyric[j][2] = tlyric[j][2].replace('/', '');
|
||||
if (tlyric[j][2]) {
|
||||
lyric[i][2] += ' (' + tlyric[j][2] + ')';
|
||||
}
|
||||
j++;
|
||||
}
|
||||
}
|
||||
for (var k = 0; k < len1; k++) {
|
||||
var t = lyric[k][0];
|
||||
result += '['.concat(
|
||||
String(Math.floor(t / 6e4)).padStart(2, '0'),
|
||||
':',
|
||||
String(Math.floor((t % 6e4) / 1e3)).padStart(2, '0'),
|
||||
'.',
|
||||
String(t % 1e3).padStart(3, '0'),
|
||||
']',
|
||||
lyric[k][2],
|
||||
'\n',
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 一键载入 ///
|
||||
window.loadAll = function () {
|
||||
userGet.click();
|
||||
vipGet.click();
|
||||
taskGetBtn.click();
|
||||
showResult('正在载入所有数据...', 'info');
|
||||
};
|
||||
|
||||
window.loadLocalCookie();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,670 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<link rel="icon" href="docs/netease.png" />
|
||||
<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;
|
||||
}
|
||||
|
||||
.subtitle b {
|
||||
color: #d63031;
|
||||
}
|
||||
|
||||
/* ---- 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 Elements ---- */
|
||||
.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.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;
|
||||
}
|
||||
|
||||
.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: 16px;
|
||||
}
|
||||
|
||||
.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.gray {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
.btn-sm.gray:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.btn-sm.gray:disabled {
|
||||
background: #d1d5db;
|
||||
}
|
||||
|
||||
.btn-sm.green {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.btn-sm.green:hover {
|
||||
background: #047857;
|
||||
}
|
||||
|
||||
.btn-sm.green:disabled {
|
||||
background: #a7f3d0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* ---- Lottery Display ---- */
|
||||
.lottery-display {
|
||||
text-align: center;
|
||||
padding: 24px 16px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #eee;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.rolling-display {
|
||||
font-size: 2.4rem;
|
||||
font-weight: 800;
|
||||
color: #333;
|
||||
min-height: 4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
letter-spacing: 1px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.rolling-display.idle {
|
||||
color: #999;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ---- Winner Result ---- */
|
||||
.winner-display {
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #86efac;
|
||||
border-radius: 8px;
|
||||
padding: 20px 16px;
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.winner-label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #065f46;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.winner-name {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
color: #dc2626;
|
||||
letter-spacing: 1px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.confetti-row {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.confetti-emoji {
|
||||
font-size: 1.6rem;
|
||||
animation: bounce 0.6s ease infinite alternate;
|
||||
}
|
||||
|
||||
.confetti-emoji:nth-child(2) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
|
||||
.confetti-emoji:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Status ---- */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: #f9fafb;
|
||||
border-radius: 6px;
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #059669;
|
||||
flex-shrink: 0;
|
||||
animation: pulse 1.5s ease infinite;
|
||||
}
|
||||
|
||||
.status-dot.error {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.status-dot.idle {
|
||||
background: #9ca3af;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ---- Footer ---- */
|
||||
.footer-link {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #eee;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.footer-link a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ---- Responsive ---- */
|
||||
@media (max-width: 500px) {
|
||||
.container {
|
||||
padding: 20px 16px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.rolling-display {
|
||||
font-size: 1.8rem;
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
.winner-name {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>云小编每日抽奖</h1>
|
||||
<p class="subtitle">每次抽奖消耗 <b>200 个曲库编辑部积分</b>,每日最多可抽奖 3 次</p>
|
||||
|
||||
<!-- Cookie 管理 -->
|
||||
<div class="form-group">
|
||||
<label for="cookieInput">令牌 <span class="tag o">可选</span></label>
|
||||
<input type="text" id="cookieInput" placeholder="留空则默认读取本地存储的令牌" autocomplete="off" />
|
||||
<div class="quick-fill">
|
||||
<span onclick="readLocalCookie()">读取本地令牌</span>
|
||||
<span onclick="clearCookie()">清除令牌</span>
|
||||
<span onclick="saveCookie()">保存令牌</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 抽奖区域 -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">每日抽奖</span>
|
||||
</div>
|
||||
|
||||
<div class="lottery-display">
|
||||
<div class="rolling-display idle" id="rollingDisplay">等待抽奖</div>
|
||||
<div id="resultContainer"></div>
|
||||
</div>
|
||||
|
||||
<button class="btn" id="lotteryBtn">
|
||||
🎲 开始抽奖
|
||||
</button>
|
||||
|
||||
<div class="status-bar" id="statusBar">
|
||||
<span class="status-dot idle" id="statusDot"></span>
|
||||
<span class="status-text" id="statusText">就绪,点击按钮开始抽奖</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<div class="footer-link">
|
||||
<a href="/">← 返回首页</a>
|
||||
<span style="margin: 0 8px; color: #ddd;">·</span>
|
||||
<a href="/ugc.html">云小编任务中心</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// ============ DOM 元素 ============
|
||||
const cookieInput = document.getElementById('cookieInput');
|
||||
const rollingDisplay = document.getElementById('rollingDisplay');
|
||||
const resultContainer = document.getElementById('resultContainer');
|
||||
const lotteryBtn = document.getElementById('lotteryBtn');
|
||||
const statusDot = document.getElementById('statusDot');
|
||||
const statusText = document.getElementById('statusText');
|
||||
|
||||
// ============ 状态 ============
|
||||
let isDrawing = false;
|
||||
let rollingTimer = null;
|
||||
|
||||
// ============ 状态栏更新 ============
|
||||
function setStatus(type, message) {
|
||||
statusText.textContent = message;
|
||||
statusDot.className = 'status-dot';
|
||||
if (type === 'error') {
|
||||
statusDot.classList.add('error');
|
||||
} else if (type === 'info') {
|
||||
statusDot.style.background = '#059669';
|
||||
} else {
|
||||
statusDot.classList.add('idle');
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 重置结果 ============
|
||||
function resetResult() {
|
||||
resultContainer.innerHTML = '';
|
||||
rollingDisplay.textContent = '等待抽奖';
|
||||
rollingDisplay.className = 'rolling-display idle';
|
||||
setStatus('idle', '就绪,点击按钮开始抽奖');
|
||||
}
|
||||
|
||||
// ============ 本地令牌管理 ============
|
||||
const cookieKey = 'ncm_cookie';
|
||||
let cookie = localStorage.getItem(cookieKey) || '';
|
||||
if (cookie) {
|
||||
cookieInput.value = cookie;
|
||||
}
|
||||
|
||||
window.readLocalCookie = function () {
|
||||
const local = localStorage.getItem(cookieKey);
|
||||
if (local) {
|
||||
cookie = local;
|
||||
cookieInput.value = local;
|
||||
setStatus('info', '已读取本地令牌');
|
||||
} else {
|
||||
setStatus('error', '未在本地发现令牌,请手动输入');
|
||||
}
|
||||
};
|
||||
|
||||
window.clearCookie = function () {
|
||||
cookie = '';
|
||||
cookieInput.value = '';
|
||||
localStorage.removeItem(cookieKey);
|
||||
setStatus('info', '已清除本地令牌');
|
||||
};
|
||||
|
||||
window.saveCookie = function () {
|
||||
const ck = cookieInput.value.trim();
|
||||
if (ck) {
|
||||
cookie = ck;
|
||||
localStorage.setItem(cookieKey, ck);
|
||||
setStatus('info', '已保存本地令牌');
|
||||
} else {
|
||||
setStatus('error', '令牌内容为空,未保存');
|
||||
}
|
||||
};
|
||||
|
||||
function getCookie() {
|
||||
const ck = cookieInput.value.trim();
|
||||
if (ck) cookie = ck;
|
||||
return ck || cookie || '';
|
||||
}
|
||||
|
||||
// ============ 调用后端接口 ============
|
||||
async function request(url, params) {
|
||||
try {
|
||||
url += '?timestamp=' + Date.now() + params;
|
||||
const ck = getCookie();
|
||||
if (ck) url += '&cookie=' + encodeURIComponent(ck);
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await response.json();
|
||||
} catch (error) {
|
||||
console.error('数据解析错误:', error);
|
||||
throw new Error('接口请求错误: ' + response.status + ': ' + response.statusText);
|
||||
}
|
||||
|
||||
if (res.code !== 200) {
|
||||
throw new Error('接口返回错误: ' + res.code + ': ' + (res.message || '未知错误'));
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('接口请求失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchLotteryResult() {
|
||||
const user = await request('rep/ugc/user/get', '');
|
||||
|
||||
if ((user.data?.availablePoints ?? 0) < 200) {
|
||||
throw new Error('用户剩余积分不足,请先完成任务获取积分');
|
||||
}
|
||||
|
||||
const chance = await request('middle/play/lottery/remain/chance', '&activityId=6501202');
|
||||
|
||||
if (chance.data === 0) {
|
||||
throw new Error('今日机会已用完,明天再来吧');
|
||||
}
|
||||
|
||||
const res = await request('middle/play/do/lottery', '&activityId=6501202&drawCount=1');
|
||||
|
||||
const winner = res.data?.prizeDetailInfoMap?.['8186002']?.prizeName;
|
||||
if (!winner) {
|
||||
throw new Error('无法解析响应数据');
|
||||
}
|
||||
|
||||
return [winner, res.data.restChance];
|
||||
}
|
||||
|
||||
// ============ 滚动动画 ============
|
||||
function startRollingAnimation(finalWinner) {
|
||||
const placeholderNames = ['🎰', '🎲', '🎯', '✨', '💫', '🌟', '🎪', '🎭'];
|
||||
const totalDuration = 2800;
|
||||
const startInterval = 50;
|
||||
const endInterval = 280;
|
||||
const startTime = Date.now();
|
||||
|
||||
rollingDisplay.className = 'rolling-display';
|
||||
resultContainer.innerHTML = '';
|
||||
|
||||
function roll() {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / totalDuration, 1.0);
|
||||
const easeOutProgress = 1 - Math.pow(1 - progress, 3);
|
||||
const currentInterval = startInterval + (endInterval - startInterval) * easeOutProgress;
|
||||
|
||||
const randomPlaceholder = placeholderNames[Math.floor(Math.random() * placeholderNames.length)];
|
||||
rollingDisplay.textContent = randomPlaceholder;
|
||||
|
||||
if (progress >= 1.0) {
|
||||
clearTimeout(rollingTimer);
|
||||
rollingDisplay.textContent = finalWinner;
|
||||
showWinnerResult(finalWinner);
|
||||
finishDrawing();
|
||||
return;
|
||||
}
|
||||
|
||||
rollingTimer = setTimeout(roll, currentInterval);
|
||||
}
|
||||
|
||||
roll();
|
||||
}
|
||||
|
||||
function showWinnerResult(winner) {
|
||||
resultContainer.innerHTML =
|
||||
'<div class="winner-display">' +
|
||||
'<div class="winner-label">🏆 恭喜中奖</div>' +
|
||||
'<div class="winner-name">' + escapeHTML(winner) + '</div>' +
|
||||
'<div class="confetti-row">' +
|
||||
'<span class="confetti-emoji">🎉</span>' +
|
||||
'<span class="confetti-emoji">🎊</span>' +
|
||||
'<span class="confetti-emoji">✨</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function escapeHTML(str) {
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(str));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function finishDrawing() {
|
||||
isDrawing = false;
|
||||
lotteryBtn.disabled = false;
|
||||
}
|
||||
|
||||
// ============ 主抽奖流程 ============
|
||||
async function startLottery() {
|
||||
if (isDrawing) return;
|
||||
|
||||
isDrawing = true;
|
||||
lotteryBtn.disabled = true;
|
||||
resultContainer.innerHTML = '';
|
||||
rollingDisplay.className = 'rolling-display';
|
||||
rollingDisplay.textContent = '🎰';
|
||||
setStatus('info', '正在获取抽奖结果...');
|
||||
|
||||
try {
|
||||
const res = await fetchLotteryResult();
|
||||
setStatus('info', '抽奖成功,剩余次数 ' + res[1]);
|
||||
startRollingAnimation(res[0]);
|
||||
} catch (error) {
|
||||
rollingDisplay.textContent = '抽奖失败';
|
||||
rollingDisplay.className = 'rolling-display idle';
|
||||
setStatus('error', '抽奖失败: ' + error.message);
|
||||
isDrawing = false;
|
||||
lotteryBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 事件绑定 ============
|
||||
lotteryBtn.addEventListener('click', startLottery);
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === ' ' && document.activeElement !== cookieInput) {
|
||||
e.preventDefault();
|
||||
if (!isDrawing && !lotteryBtn.disabled) {
|
||||
startLottery();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 回车键在输入框中触发保存
|
||||
cookieInput.addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
saveCookie();
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 初始化 ============
|
||||
resetResult();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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>
|
||||
@@ -11,10 +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",
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
+33
-5
@@ -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 } : {}
|
||||
@@ -226,6 +243,9 @@ const createRequest = (uri, data, options) => {
|
||||
headers['Referer'] = options.domain || DOMAIN
|
||||
headers['User-Agent'] = options.ua || chooseUserAgent('weapi')
|
||||
data.csrf_token = csrfToken
|
||||
if (options.checkToken) {
|
||||
headers['X-antiCheatToken'] = token
|
||||
}
|
||||
encryptData = encrypt.weapi(data)
|
||||
url = (options.domain || DOMAIN) + '/weapi/' + uri.substr(5)
|
||||
break
|
||||
@@ -264,6 +284,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 +325,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 +346,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 +367,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'
|
||||
|
||||
Reference in New Issue
Block a user