2026-08-02 21:48:01 +08:00

262 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="docs/netease.png" />
<link rel="stylesheet" href="/static/shell.css" />
<title>听歌打卡 - 网易云音乐 API Enhanced</title>
<style>
body { padding: 1.25rem; }
.shell-header { text-align: center; }
.btn-shell { display: block; width: 100%; }
</style>
</head>
<body>
<header class="shell-topbar">
<a class="brand" href="/"><img src="docs/netease.png" alt="logo">听歌打卡</a>
<nav>
<a class="shell-btn shell-btn--ghost shell-btn--sm" href="/qrlogin-nocookie.html">登录</a>
<a class="shell-btn shell-btn--ghost shell-btn--sm" href="/">返回首页</a>
</nav>
</header>
<main class="shell-main">
<div class="shell-container shell-narrow">
<header class="shell-header">
<h1>听歌打卡</h1>
<p class="sub">同步听歌记录至网易云音乐,增加听歌排行计数</p>
</header>
<section class="shell-card">
<!-- Cookie (共享) -->
<div class="form-group">
<label for="cookie">Cookie <span class="tag o">可选</span></label>
<textarea id="cookie" placeholder="留空则默认读取本地存储的登录态" rows="2"></textarea>
<div class="quick-fill">
<span onclick="loadLocalCookie()">读取本地 Cookie</span>
</div>
</div>
<!-- Tabs -->
<div class="tabs">
<button class="tab-btn active" data-tab="v0" onclick="switchTab('v0')">原版</button>
<button class="tab-btn" data-tab="v1" onclick="switchTab('v1')">V2 (NCBL 加密版)</button>
</div>
<!-- ============ Tab: v0 (原版 /scrobble) ============ -->
<div class="tab-content active" id="tab-v0">
<div class="form-group">
<label>歌曲 ID <span class="tag r">必填</span></label>
<input type="text" id="v0-songId" placeholder="例如: 2756058128" value="2756058128" />
<div class="quick-fill">
示例:
<span onclick="fillV0('2756058128', '288651229')">妖精小姐的魔法邀约</span>
<span onclick="fillV0('2637402867', '251025018')">Echoes of Memoria</span>
<span onclick="fillV0('36307815', '3394198')">Lose Control</span>
</div>
</div>
<div class="form-group">
<label>来源 ID (歌单或专辑 ID) <span class="tag r">必填</span></label>
<input type="text" id="v0-sourceId" placeholder="例如: 2756058128" value="2756058128" />
</div>
<div class="form-group">
<label>播放时间 (秒) <span class="tag o">可选</span></label>
<input type="number" id="v0-time" placeholder="300" value="300" />
</div>
<button class="btn shell-btn shell-btn--default shell-btn--block" onclick="submitV0()">立即打卡 (原版)</button>
</div>
<!-- ============ Tab: v1 (NCBL 加密版 /scrobble/v1) ============ -->
<div class="tab-content" id="tab-v1">
<div class="form-group">
<label>歌曲 ID <span class="tag r">必填</span></label>
<input type="text" id="v1-songId" placeholder="例如: 518066366" value="518066366" />
<div class="quick-fill">
示例:
<span onclick="fillV1('518066366', '36780169', '291')">默认示例</span>
<span onclick="fillV1('2756058128', '288651229', '300')">妖精小姐的魔法邀约</span>
<span onclick="fillV1('2637402867', '251025018', '300')">Echoes of Memoria</span>
</div>
</div>
<div class="form-group">
<label>播放时间 (秒) <span class="tag r">必填</span></label>
<input type="number" id="v1-time" placeholder="例如: 291" value="291" />
</div>
<div class="form-group">
<label>来源 ID <span class="tag o">可选</span></label>
<input type="text" id="v1-sourceId" placeholder="歌单或专辑 ID" value="36780169" />
</div>
<div class="row">
<div class="form-group">
<label>歌曲名 <span class="tag o">可选</span></label>
<input type="text" id="v1-name" placeholder="歌曲名称" />
</div>
<div class="form-group">
<label>艺术家 <span class="tag o">可选</span></label>
<input type="text" id="v1-artist" placeholder="歌手名" />
</div>
</div>
<div class="row">
<div class="form-group">
<label>码率 <span class="tag o">可选</span></label>
<input type="number" id="v1-bitrate" placeholder="默认 320" value="320" />
</div>
<div class="form-group">
<label>音质等级 <span class="tag o">可选</span></label>
<input type="text" id="v1-level" placeholder="默认 exhigh" value="exhigh" />
</div>
</div>
<div class="form-group">
<label>歌曲总时长 (秒) <span class="tag o">可选</span></label>
<input type="number" id="v1-total" placeholder="留空则与播放时间相同" />
</div>
<button class="btn shell-btn shell-btn--default shell-btn--block" onclick="submitV1()">立即打卡 (NCBL 加密版)</button>
</div>
<!-- Result -->
<div id="result" class="result" style="display: none;"></div>
<div class="shell-flex shell-flex--center shell-mt-4">
<a class="shell-btn shell-btn--ghost shell-btn--sm" href="/">返回首页</a>
</div>
</section>
</div>
</main>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script>
/* ---- DOM refs ---- */
const cookieInput = document.getElementById('cookie')
const resultDiv = document.getElementById('result')
/* ---- init ---- */
if (localStorage.getItem('cookie')) {
cookieInput.value = localStorage.getItem('cookie')
}
/* ---- Tab switching ---- */
function switchTab(name) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === name))
document.querySelectorAll('.tab-content').forEach(c => c.classList.toggle('active', c.id === 'tab-' + name))
hideResult()
}
/* ---- Cookie ---- */
function loadLocalCookie() {
const local = localStorage.getItem('cookie')
if (local) {
cookieInput.value = local
showResult('已成功读取本地 Cookie', 'success')
} else {
showResult('未在本地发现登录 Cookie请先登录或手动粘贴', 'error')
}
}
/* ---- Quick fill ---- */
function fillV0(id, sourceId) {
document.getElementById('v0-songId').value = id
document.getElementById('v0-sourceId').value = sourceId
}
function fillV1(id, sourceId, time) {
document.getElementById('v1-songId').value = id
document.getElementById('v1-sourceId').value = sourceId
document.getElementById('v1-time').value = time
}
/* ---- Result ---- */
function hideResult() {
resultDiv.style.display = 'none'
}
function showResult(message, type) {
resultDiv.textContent = message
resultDiv.className = 'result ' + type
resultDiv.style.display = 'block'
}
function disableAll(disabled) {
document.querySelectorAll('.btn').forEach(b => b.disabled = disabled)
}
/* ---- Submit: 原版 /scrobble ---- */
async function submitV0() {
const id = document.getElementById('v0-songId').value.trim()
const sourceid = document.getElementById('v0-sourceId').value.trim()
const time = document.getElementById('v0-time').value.trim() || '300'
if (!id) { showResult('请输入歌曲 ID ', 'error'); return }
if (!sourceid) { showResult('请输入来源 ID ', 'error'); return }
disableAll(true)
showResult('打卡中,请稍候...', 'info')
try {
const params = { id, sourceid: sourceid || id, time, timestamp: Date.now() }
const cookie = cookieInput.value.trim()
if (cookie) params.cookie = cookie
const res = await axios({ method: 'get', url: '/scrobble', params })
if (res.data.code === 200) {
showResult('✅ 打卡成功! \n\n' + JSON.stringify(res.data, null, 2), 'success')
} else {
showResult('请求已发送,返回:\n\n' + JSON.stringify(res.data, null, 2), 'info')
}
} catch (error) {
const errData = error.response ? error.response.data : { error: error.message }
showResult('❌ 打卡失败\n\n' + JSON.stringify(errData, null, 2), 'error')
} finally {
disableAll(false)
}
}
/* ---- Submit: NCBL 加密版 /scrobble/v1 ---- */
async function submitV1() {
const id = document.getElementById('v1-songId').value.trim()
const time = document.getElementById('v1-time').value.trim()
if (!id) { showResult('请输入歌曲 ID ', 'error'); return }
if (!time || isNaN(+time) || +time <= 0) { showResult('请输入有效的播放时长(秒)!', 'error'); return }
disableAll(true)
showResult('打卡中NCBL 加密),请稍候...', 'info')
try {
const params = {
id,
time,
sourceid: document.getElementById('v1-sourceId').value.trim() || undefined,
name: document.getElementById('v1-name').value.trim() || undefined,
artist: document.getElementById('v1-artist').value.trim() || undefined,
bitrate: document.getElementById('v1-bitrate').value.trim() || undefined,
level: document.getElementById('v1-level').value.trim() || undefined,
total: document.getElementById('v1-total').value.trim() || undefined,
timestamp: Date.now(),
}
const cookie = cookieInput.value.trim()
if (cookie) params.cookie = cookie
// 清理 undefined 参数
Object.keys(params).forEach(k => params[k] === undefined && delete params[k])
const res = await axios({ method: 'get', url: '/scrobble/v1', params })
if (res.data.code === 200) {
showResult('✅ NCBL 加密打卡成功! \n\n' + JSON.stringify(res.data, null, 2), 'success')
} else {
showResult('请求已发送,返回:\n\n' + JSON.stringify(res.data, null, 2), 'info')
}
} catch (error) {
const errData = error.response ? error.response.data : { error: error.message }
showResult('❌ 打卡失败\n\n' + JSON.stringify(errData, null, 2), 'error')
} finally {
disableAll(false)
}
}
</script>
</body>
</html>