Compare commits

...

10 Commits

14 changed files with 1301 additions and 36 deletions

View File

@ -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'),
)
}

View File

@ -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'),
)
}

View File

@ -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'),
)
}

View File

@ -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'),
)
}

View File

@ -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'),
)
}

View File

@ -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'))
}

View File

@ -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'))
}

View File

@ -1,6 +1,6 @@
{
"name": "@neteasecloudmusicapienhanced/api",
"version": "4.38.0",
"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",

46
pnpm-lock.yaml generated
View File

@ -147,9 +147,9 @@ packages:
'@borewit/text-codec@0.2.2':
resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==}
'@eslint-community/eslint-utils@4.10.0':
resolution: {integrity: sha512-rd6n9Sefya1R1Vd594TRwzeHq2VnmICbvh3jebyt1Hs/6OoCSYNVpjGRRsKSTwAq3SKl09w0AfRCU57hoDLmxg==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint-community/eslint-utils@4.10.1':
resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
@ -523,9 +523,9 @@ packages:
brace-expansion@2.1.2:
resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==}
brace-expansion@5.0.7:
resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
engines: {node: 18 || 20 || >=22}
brace-expansion@5.0.8:
resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==}
engines: {node: 20 || >=22}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@ -973,6 +973,10 @@ packages:
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@3.4.3:
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint-visitor-keys@4.2.1:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -1149,8 +1153,8 @@ packages:
flatstr@1.0.12:
resolution: {integrity: sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==}
flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
flatted@3.4.3:
resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==}
follow-redirects@1.16.0:
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
@ -1636,8 +1640,8 @@ packages:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
media-typer@1.1.0:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
media-typer@1.1.1:
resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==}
engines: {node: '>= 0.8'}
media-typer@2.0.0:
@ -2683,10 +2687,10 @@ snapshots:
'@borewit/text-codec@0.2.2': {}
'@eslint-community/eslint-utils@4.10.0(eslint@9.39.5)':
'@eslint-community/eslint-utils@4.10.1(eslint@9.39.5)':
dependencies:
eslint: 9.39.5
eslint-visitor-keys: 5.0.1
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
@ -2940,7 +2944,7 @@ snapshots:
'@typescript-eslint/utils@8.65.0(eslint@9.39.5)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.10.0(eslint@9.39.5)
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
'@typescript-eslint/scope-manager': 8.65.0
'@typescript-eslint/types': 8.65.0
'@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3)
@ -3134,7 +3138,7 @@ snapshots:
dependencies:
balanced-match: 1.0.2
brace-expansion@5.0.7:
brace-expansion@5.0.8:
dependencies:
balanced-match: 4.0.4
@ -3666,13 +3670,15 @@ snapshots:
esrecurse: 4.3.0
estraverse: 5.3.0
eslint-visitor-keys@3.4.3: {}
eslint-visitor-keys@4.2.1: {}
eslint-visitor-keys@5.0.1: {}
eslint@9.39.5:
dependencies:
'@eslint-community/eslint-utils': 4.10.0(eslint@9.39.5)
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.2
'@eslint/config-helpers': 0.4.2
@ -3953,14 +3959,14 @@ snapshots:
flat-cache@4.0.1:
dependencies:
flatted: 3.4.2
flatted: 3.4.3
keyv: 4.5.4
flat@5.0.2: {}
flatstr@1.0.12: {}
flatted@3.4.2: {}
flatted@3.4.3: {}
follow-redirects@1.16.0: {}
@ -4453,7 +4459,7 @@ snapshots:
media-typer@0.3.0: {}
media-typer@1.1.0: {}
media-typer@1.1.1: {}
media-typer@2.0.0: {}
@ -4494,7 +4500,7 @@ snapshots:
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.7
brace-expansion: 5.0.8
minimatch@3.1.5:
dependencies:
@ -5440,7 +5446,7 @@ snapshots:
type-is@2.1.0:
dependencies:
content-type: 2.0.0
media-typer: 1.1.0
media-typer: 1.1.1
mime-types: 3.0.2
type-name@2.0.2: {}

View File

@ -5604,6 +5604,36 @@ let data = encodeURIComponent(
**调用例子:** `/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`
### 发送/删除评论
说明 : 调用此接口,可发送评论或者删除评论

View File

@ -56,6 +56,15 @@
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>
@ -141,6 +150,47 @@
</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>
@ -260,6 +310,8 @@
$('s3info').style.display = 'grid'
const body = data.data || {}
$('s3result').textContent = JSON.stringify(body).substring(0, 200)
// 领取成功后自动刷新剩余时长
queryRights()
return true
}
setTag('s3tag', 'no', '领取失败')
@ -306,7 +358,139 @@
$('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>

View File

@ -99,7 +99,8 @@ curl -s {origin}/search?keywords=网易云</code></pre>
<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.html">云小编任务中心</a> ·
<a href="/ugc_lottery.html">云小编每日抽奖</a>
</div>
</section>

View File

@ -4,6 +4,10 @@
<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>
* {
@ -325,6 +329,10 @@
flex-shrink: 0;
}
#userPoint {
font-size: 14px;
}
/* ---- Song Review ---- */
.song-review {
margin-top: 16px;
@ -377,7 +385,7 @@
border-radius: 6px;
border: 1px solid #e5e7eb;
white-space: pre-wrap;
max-height: 120px;
max-height: 160px;
overflow-y: auto;
font-family: monospace;
line-height: 1.6;
@ -404,6 +412,8 @@
<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>
@ -413,8 +423,9 @@
<button class="btn-sm" id="userGet">获取用户详情</button>
</div>
<div class="info-card" id="user">
<img alt="头像" class="avatar" id="userAvatar" />
<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>
@ -436,7 +447,7 @@
<button class="btn-sm" id="taskGet">获取任务详情</button>
</div>
<div class="info-card" id="task">
<span class="info-text" id="taskInfo">[<span id="taskCount">0</span>/<span id="taskGoal">0</span>] 审核歌曲赚贡献分 (+66)</span>
<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>
@ -462,7 +473,6 @@
</div>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script>
(function () {
var k_cookie = 'ncm_cookie';
@ -496,6 +506,12 @@
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';
@ -547,6 +563,7 @@
/// 用户信息 ///
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');
@ -555,6 +572,7 @@
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');
});
@ -567,6 +585,7 @@
var d = res.data;
userSign.disabled = d;
showResult('签到' + (d ? '成功 ✓' : '失败 ✗'), d ? 'success' : 'error');
if (d) userGet.click();
});
}
};
@ -599,21 +618,36 @@
/// 审核任务 ///
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) {
request('rep/ugc/activity/get', {}, function (res) {
var d = res.data;
taskCount.textContent = d.count;
taskGoal.textContent = d.goalCount;
taskStartBtn.disabled = d.activityId !== 5001;
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) taskStart();
if (!taskStartBtn.disabled) {
songAudio.pause();
examInit();
}
};
/// 歌曲审核区域 ///
@ -627,26 +661,114 @@
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() {
song.style.display = 'none';
request('/thinktank/audit/resource/detail', { type: '4' }, function (res) {
request('thinktank/audit/resource/detail', { type: '4' }, function (res) {
var d = res.data;
if (d.auditCount >= 50) {
request('rep/ugc/activity/collect', { activityId: '5001' }, function (res) {
showResult('积分领取' + (res.data ? '成功,可以兑换会员了!' : '失败'), res.data ? 'success' : 'error');
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 || '');
if (!songLyric.textContent.trim()) songLyric.textContent = '[00:00.00] 暂无歌词';
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 () { /* 自动播放被浏览器阻止 */ });
@ -669,6 +791,7 @@
* @param {'1'|'2'|'3'} judgement
*/
function taskSubmit(taskId, judgement) {
songAudio.pause();
songApprove.disabled = songReject.disabled = songSkip.disabled = true;
request(
'thinktank/audit/resource/update',
@ -676,7 +799,10 @@
function (res) {
var d = res.data;
if (d.result) {
showResult('审核提交成功,加载下一首...', 'success');
showResult((d.test ? '抽查' : '审核') + '提交成功,加载下一首...', 'success');
taskStart();
} else if (d.test) {
showResult('抽查失败: [' + d.dayErrorNum + '/' + d.dayErrorNumStandard + '] ' + d.errorAnalysis, 'error');
taskStart();
} else {
showResult('未知状态: ' + JSON.stringify(d), 'error');
@ -685,8 +811,63 @@
);
}
/**解析歌词时间戳和文本 */
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;
}
/// 一键载入 ///
document.getElementById('loadall').onclick = function () {
window.loadAll = function () {
userGet.click();
vipGet.click();
taskGetBtn.click();

670
public/ugc_lottery.html Normal file
View File

@ -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>