Compare commits

...

3 Commits

Author SHA1 Message Date
51a958936c
Potential fix for code scanning alert no. 15: DOM text reinterpreted as HTML
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-02-06 20:07:23 +08:00
7772431cb7
fix: Potential fix for code scanning alert no. 6: DOM text reinterpreted as HTML
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-02-06 20:00:36 +08:00
c2871322d0
refactor: QR login and voice upload pages with improved UI and error handling
- Enhanced styling for better user experience on qrlogin-nocookie.html and qrlogin.html
- Added loading indicators and improved status messages during QR code login process
- Updated error handling for login status retrieval
- Refactored unblock_test.html for better layout and user interaction
- Improved voice upload page with a more intuitive design and better feedback for file uploads
- Added loading state management for voice list retrieval
- Enhanced accessibility and usability across all modified pages
2026-02-06 19:56:14 +08:00
17 changed files with 2839 additions and 779 deletions

View File

@ -7,6 +7,16 @@
- chore: 更新依赖项 (music-metadata: ^11.11.1 -> ^11.11.2, ansi-escapes: ^7.2.0 -> ^7.3.0, commander: ^14.0.2 -> ^14.0.3) - chore: 更新依赖项 (music-metadata: ^11.11.1 -> ^11.11.2, ansi-escapes: ^7.2.0 -> ^7.3.0, commander: ^14.0.2 -> ^14.0.3)
- chore: 更新GitHub Actions (checkout: v4 -> v6, setup-node: v4 -> v6, upload-artifact: v4 -> v6, download-artifact: v4 -> v7, github-script: v7 -> v8) - chore: 更新GitHub Actions (checkout: v4 -> v6, setup-node: v4 -> v6, upload-artifact: v4 -> v6, download-artifact: v4 -> v7, github-script: v7 -> v8)
- refactor: 注释掉IP地址日志输出以提升隐私保护 - refactor: 注释掉IP地址日志输出以提升隐私保护
- refactor: 重构前端测试页面, 主要改进:
- 统一使用简洁现代的设计风格
- 去除渐变背景和复杂动画
- 使用纯色背景(#f5f5f5和白色卡片
- 优化表单布局和交互体验
- 增强错误处理和加载状态
- 移除第三方框架依赖Tailwind、Bootstrap、MDUI
- 升级Vue 2到Vue 3
- 将硬编码的配置项移至前端表单
- 所有页面现在都保持一致的设计语言,简洁清爽,功能完整。
### 4.25.0 | 2024.11.16 ### 4.25.0 | 2024.11.16
- feat: 增加副歌时间、相关歌单推荐接口原有相关歌单接口已废弃fix: 将部分易盾白名单接口替换为eapi [#30](https://gitlab.com/Binaryify/neteasecloudmusicapi/-/merge_requests/30) - feat: 增加副歌时间、相关歌单推荐接口原有相关歌单接口已废弃fix: 将部分易盾白名单接口替换为eapi [#30](https://gitlab.com/Binaryify/neteasecloudmusicapi/-/merge_requests/30)

View File

@ -5,66 +5,128 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 调试界面</title> <title>API 调试界面</title>
<style> <style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
font-family: Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 20px;
display: flex;
flex-direction: column;
min-height: 100vh; min-height: 100vh;
background: #f5f5f5;
padding: 20px;
} }
.container { .container {
display: flex; max-width: 1200px;
flex-direction: column; margin: 0 auto;
flex-grow: 1; 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: 24px;
}
form { form {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
gap: 16px;
margin-bottom: 24px;
}
.form-row {
display: flex;
gap: 12px;
align-items: center; align-items: center;
gap: 10px;
margin-bottom: 10px;
} }
input, button {
padding: 10px; label {
box-sizing: border-box; font-size: 14px;
font-weight: 500;
color: #555;
min-width: 80px;
}
input, select {
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
flex: 1; flex: 1;
outline: none;
} }
input:focus, select:focus {
border-color: #333;
}
button { button {
background-color: #4CAF50; background: #333;
color: white; color: white;
padding: 10px 24px;
border: none; border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer; cursor: pointer;
transition: background 0.2s ease;
} }
button:hover {
background: #555;
}
.data-result { .data-result {
display: flex; display: flex;
flex-direction: row; gap: 16px;
flex-grow: 1; min-height: 400px;
} }
.data-result > div { .data-result > div {
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-grow: 1;
padding: 10px;
box-sizing: border-box;
} }
.data-result label { .data-result label {
margin-bottom: 10px; margin-bottom: 8px;
padding: 0;
} }
#data, #result {
height: 100%; textarea {
box-sizing: border-box; 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;
} }
#data {
border-right: 1px solid #ccc; textarea:focus {
border-color: #333;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>API 调试界面</h1>
<form onsubmit="event.preventDefault(); sendRequest();"> <form onsubmit="event.preventDefault(); sendRequest();">
<label for="uri">uri</label> <div class="form-row">
<label for="uri">URI</label>
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1"> <input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
<label for="crypto">crypto</label> </div>
<div class="form-row">
<label for="crypto">加密方式</label>
<select id="crypto" name="crypto"> <select id="crypto" name="crypto">
<option value="weapi">weapi</option> <option value="weapi">weapi</option>
<option value="eapi">eapi</option> <option value="eapi">eapi</option>
@ -72,15 +134,19 @@
<option value="linuxapi">linuxapi</option> <option value="linuxapi">linuxapi</option>
<option value="" selected>(默认)</option> <option value="" selected>(默认)</option>
</select> </select>
<button type="submit">发送</button> </div>
<div class="form-row">
<label></label>
<button type="submit">发送请求</button>
</div>
</form> </form>
<div class="data-result"> <div class="data-result">
<div> <div>
<label for="result">result</label> <label for="result">响应结果</label>
<textarea id="result" name="result"></textarea> <textarea id="result" name="result" readonly></textarea>
</div> </div>
<div> <div>
<label for="data">data</label> <label for="data">请求数据</label>
<textarea id="data" name="data"> <textarea id="data" name="data">
{ {
"cp": false, "cp": false,

View File

@ -1,6 +1,5 @@
'use strict' 'use strict'
const WASM_BINARY_PLACEHOLDER = 'WASM_BINARY_PLACEHOLDER'; const WASM_BINARY_PLACEHOLDER = 'WASM_BINARY_PLACEHOLDER';
const logger = require('../../util/logger.js')
// See https://github.com/Distributive-Network/PythonMonkey/issues/266 // See https://github.com/Distributive-Network/PythonMonkey/issues/266
if (typeof globalThis.setInterval != 'function'){ if (typeof globalThis.setInterval != 'function'){
globalThis.setInterval = function pm$$setInterval(fn, timeout) { globalThis.setInterval = function pm$$setInterval(fn, timeout) {
@ -1612,9 +1611,9 @@ function instantiateRuntime(){
function GenerateFP(floatArray) { function GenerateFP(floatArray) {
let PCMBuffer = Float32Array.from(floatArray) let PCMBuffer = Float32Array.from(floatArray)
logger.info('[afp] input samples n=', PCMBuffer.length) console.info('[afp] input samples n=', PCMBuffer.length)
return instantiateRuntime().then((fpRuntime) => { return instantiateRuntime().then((fpRuntime) => {
logger.info('[afp] begin fingerprinting') console.info('[afp] begin fingerprinting')
let fp_vector = fpRuntime.ExtractQueryFP(PCMBuffer.buffer) let fp_vector = fpRuntime.ExtractQueryFP(PCMBuffer.buffer)
let result_buf = new Uint8Array(fp_vector.size()); let result_buf = new Uint8Array(fp_vector.size());
for (let t = 0; t < fp_vector.size(); t++) for (let t = 0; t < fp_vector.size(); t++)

View File

@ -1,27 +1,142 @@
<!DOCTYPE html> <!DOCTYPE html>
<head> <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>听歌识曲 Demo</title>
<style> <style>
* { * {
font-family: sans-serif; margin: 0;
padding: 0;
box-sizing: border-box;
} }
pre { body {
font-family: monospace; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 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;
}
.subtitle {
font-size: 13px;
color: #666;
margin-bottom: 24px;
}
hr {
border: none;
border-top: 1px solid #eee;
margin: 20px 0;
}
p {
font-size: 14px;
color: #555;
line-height: 1.6;
margin-bottom: 12px;
} }
a { a {
font-family: sans-serif; color: #333;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.section {
margin-bottom: 24px;
}
.section h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.control-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 16px;
}
button {
padding: 10px 20px;
background: #333;
color: white;
font-size: 14px;
font-weight: 500;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
}
button:hover {
background: #555;
}
button:disabled {
background: #999;
cursor: not-allowed;
}
input[type="file"] {
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-group input[type="checkbox"] {
cursor: pointer;
}
.checkbox-group label {
margin: 0;
cursor: pointer;
font-size: 14px;
color: #555;
} }
audio { audio {
width: 100%; width: 100%;
margin-bottom: 16px;
} }
canvas { canvas {
width: 100%; width: 100%;
height: 0; height: 0;
transition: all linear 0.1s; transition: all linear 0.1s;
background: #f9f9f9;
border-radius: 6px;
} }
.canvas-active { .canvas-active {
@ -29,39 +144,80 @@
} }
pre { pre {
overflow: scroll; font-family: 'Courier New', monospace;
font-size: 13px;
color: #666;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 400px;
overflow-y: auto;
padding: 16px;
background: #f9f9f9;
border-radius: 6px;
border: 1px solid #eee;
}
.warning {
padding: 12px 16px;
background: #fef3c7;
border-radius: 6px;
font-size: 14px;
color: #92400e;
margin-bottom: 16px;
} }
</style> </style>
</head> </head>
<body> <body>
<h1>听歌识曲 Demo (Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a>)</h1> <div class="container">
<h1>听歌识曲 Demo</h1>
<p class="subtitle">Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a></p>
<hr> <hr>
<p><b>DISCLAIMER: </b></p>
<p>This site uses the offical NetEase audio matcher APIs (reverse engineered from <a <div class="warning">
href="https://fn.music.163.com/g/chrome-extension-home-page-beta/">https://fn.music.163.com/g/chrome-extension-home-page-beta/</a>) <strong>免责声明:</strong>本站点使用网易云音乐官方音频识别API逆向自 <a href="https://fn.music.163.com/g/chrome-extension-home-page-beta/" target="_blank">Chrome 扩展页面</a>),不鼓励版权侵犯或知识产权盗窃。
</p> </div>
<p>And DOES NOT condone copyright infringment nor intellectual property theft.</p>
<div class="section">
<h3>使用说明</h3>
<p>在使用本站点之前,您可能需要先访问以下链接:</p>
<p><a href="https://cors-anywhere.herokuapp.com/corsdemo" target="_blank">https://cors-anywhere.herokuapp.com/corsdemo</a></p>
<p>由于网易云音乐API没有CORS头这是解决此限制的必要步骤。</p>
</div>
<div class="section">
<h3>使用方法</h3>
<ul style="padding-left: 20px; font-size: 14px; color: #555;">
<li>通过"选择文件"选择您的音频文件</li>
<li>点击"识别"按钮并等待结果</li>
</ul>
</div>
<hr> <hr>
<p><b>NOTE:</b></p>
<p>Before start using the site, you may want to access this link first:</p>
<a href="https://cors-anywhere.herokuapp.com/corsdemo">https://cors-anywhere.herokuapp.com/corsdemo</a>
<p>Since Netease APIs do not have CORS headers, this is required to alleviate this restriction.</p>
<hr>
<p>Usage:</p>
<li>Select your audio file through "Choose File" picker</li>
<li>Hit the "Clip" button and wait for the results!</li>
<audio id="audio" controls autoplay></audio> <audio id="audio" controls autoplay></audio>
<canvas id="canvas"></canvas> <canvas id="canvas"></canvas>
<button id="invoke">Clip</button>
<div class="control-group">
<button id="invoke">识别</button>
<input type="file" name="picker" accept="*" id="file"> <input type="file" name="picker" accept="*" id="file">
<hr> </div>
<label for="use-mic">Mix in Microphone input</label>
<div class="control-group">
<div class="checkbox-group">
<input type="checkbox" name="use-mic" id="usemic"> <input type="checkbox" name="use-mic" id="usemic">
<label for="usemic">混合麦克风输入</label>
</div>
</div>
<hr> <hr>
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 12px;">日志</h3>
<pre id="logs"></pre> <pre id="logs"></pre>
</div>
</body> </body>
<script src="./afp.wasm.js"></script> <script src="./afp.wasm.js"></script>
<script src="./afp.js"></script> <script src="./afp.js"></script>
<script type="module"> <script type="module">
@ -76,13 +232,17 @@
let canvas = document.getElementById('canvas') let canvas = document.getElementById('canvas')
let canvasCtx = canvas.getContext('2d') let canvasCtx = canvas.getContext('2d')
let logs = document.getElementById('logs') let logs = document.getElementById('logs')
logs.write = line => logs.innerHTML += line + '\n' logs.write = line => {
// Append log lines as text to avoid interpreting content as HTML
logs.appendChild(document.createTextNode(line));
logs.appendChild(document.createElement('br'));
}
function RecorderCallback(channelL) { function RecorderCallback(channelL) {
let sampleBuffer = new Float32Array(channelL.subarray(0, duration * 8000)) let sampleBuffer = new Float32Array(channelL.subarray(0, duration * 8000))
GenerateFP(sampleBuffer).then(FP => { GenerateFP(sampleBuffer).then(FP => {
logs.write(`[index] Generated FP ${FP}`) logs.write(`[index] 生成指纹 ${FP}`)
logs.write('[index] Now querying, please wait...') logs.write('[index] 正在查询,请稍候...')
fetch( fetch(
'/audio/match?' + '/audio/match?' +
new URLSearchParams({ new URLSearchParams({
@ -91,9 +251,9 @@
method: 'POST' method: 'POST'
}).then(resp => resp.json()).then(resp => { }).then(resp => resp.json()).then(resp => {
if (!resp.data.result) { if (!resp.data.result) {
return logs.write('[index] Query failed with no results.') return logs.write('[index] 查询失败,无结果')
} }
logs.write(`[index] Query complete. Results=${resp.data.result.length}`) logs.write(`[index] 查询完成。结果数量=${resp.data.result.length}`)
for (var song of resp.data.result) { for (var song of resp.data.result) {
logs.write( logs.write(
`[result] <a target="_blank" href="https://music.163.com/song?id=${song.song.id}">${song.song.name} - ${song.song.album.name} (${song.startTime / 1000}s)</a>` `[result] <a target="_blank" href="https://music.163.com/song?id=${song.song.id}">${song.song.name} - ${song.song.album.name} (${song.startTime / 1000}s)</a>`
@ -104,20 +264,19 @@
} }
function InitAudioCtx() { function InitAudioCtx() {
// AFP.wasm can't do it with anything other than 8KHz
audioCtx = new AudioContext({ 'sampleRate': 8000 }) audioCtx = new AudioContext({ 'sampleRate': 8000 })
if (audioCtx.state == 'suspended') if (audioCtx.state == 'suspended')
return false return false
let audioNode = audioCtx.createMediaElementSource(audio) let audioNode = audioCtx.createMediaElementSource(audio)
audioCtx.audioWorklet.addModule('rec.js').then(() => { audioCtx.audioWorklet.addModule('rec.js').then(() => {
recorderNode = new AudioWorkletNode(audioCtx, 'timed-recorder') recorderNode = new AudioWorkletNode(audioCtx, 'timed-recorder')
audioNode.connect(recorderNode) // recorderNode doesn't output anything audioNode.connect(recorderNode)
audioNode.connect(audioCtx.destination) audioNode.connect(audioCtx.destination)
recorderNode.port.onmessage = event => { recorderNode.port.onmessage = event => {
switch (event.data.message) { switch (event.data.message) {
case 'finished': case 'finished':
RecorderCallback(event.data.recording) RecorderCallback(event.data.recording)
clip.innerHTML = 'Clip' clip.innerHTML = '识别'
clip.disabled = false clip.disabled = false
canvas.classList.remove('canvas-active') canvas.classList.remove('canvas-active')
break break
@ -130,7 +289,6 @@
logs.write(event.data.message) logs.write(event.data.message)
} }
} }
// Attempt to get user's microphone and connect it to the AudioContext.
navigator.mediaDevices.getUserMedia({ navigator.mediaDevices.getUserMedia({
audio: { audio: {
echoCancellation: false, echoCancellation: false,
@ -142,7 +300,7 @@
micSourceNode = audioCtx.createMediaStreamSource(micStream); micSourceNode = audioCtx.createMediaStreamSource(micStream);
micSourceNode.connect(recorderNode) micSourceNode.connect(recorderNode)
usemic.checked = true usemic.checked = true
logs.write('[rec.js] Microphone attached.') logs.write('[rec.js] 麦克风已连接')
}); });
}); });
return true return true
@ -161,10 +319,20 @@
else else
micSourceNode.connect(recorderNode) micSourceNode.connect(recorderNode)
}) })
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/\//g, '&#x2F;');
}
file.addEventListener('change', event => { file.addEventListener('change', event => {
file.files[0].arrayBuffer().then( file.files[0].arrayBuffer().then(
async buffer => { async buffer => {
logs.write(`[index] File ${file.files[0].name} loaded.`) const safeName = escapeHtml(file.files[0].name)
logs.write(`[index] 文件 ${safeName} 已加载`)
audio.src = window.URL.createObjectURL(new Blob([buffer])) audio.src = window.URL.createObjectURL(new Blob([buffer]))
clip.disabled = false clip.disabled = false
}) })
@ -188,12 +356,13 @@
UpdateCanvas() UpdateCanvas()
let requestCtx = setInterval(() => { let requestCtx = setInterval(() => {
try { try {
if (InitAudioCtx()) { // Put this here so we don't have to deal with the 'user did not interact' thing if (InitAudioCtx()) {
clearInterval(requestCtx) clearInterval(requestCtx)
logs.write('[rec.js] Audio Context started.') logs.write('[rec.js] 音频上下文已启动')
} }
} catch { } catch {
// Fail silently // Fail silently
} }
}, 100) }, 100)
</script> </script>
</html>

View File

@ -5,67 +5,320 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>更新头像</title> <title>更新头像</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<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;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 100%;
text-align: center;
}
h1 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 32px;
}
.avatar-wrapper {
position: relative;
width: 160px;
height: 160px;
margin: 0 auto 32px;
}
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
border: 3px solid #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.avatar-wrapper.loading .avatar {
opacity: 0.5;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.avatar-wrapper.loading .loading-overlay {
opacity: 1;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #e0e0e0;
border-top-color: #333;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.upload-btn {
display: inline-block;
position: relative;
padding: 12px 28px;
background: #333;
color: white;
font-size: 15px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
border: none;
}
.upload-btn:hover {
background: #555;
}
.upload-btn input[type="file"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.login-link {
display: block;
margin-top: 24px;
color: #666;
font-size: 14px;
text-decoration: none;
}
.login-link:hover {
color: #333;
text-decoration: underline;
}
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(100px);
padding: 12px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
opacity: 0;
transition: all 0.3s ease;
z-index: 1000;
max-width: 90%;
text-align: center;
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.toast.success {
background: #10b981;
}
.toast.error {
background: #ef4444;
}
.toast.info {
background: #3b82f6;
}
.hint {
margin-top: 20px;
font-size: 12px;
color: #999;
}
</style>
</head> </head>
<body> <body>
<div> <div class="container">
<a href="/qrlogin-nocookie.html"> <h1>更新头像</h1>
如果没登录,请先登录 <p class="subtitle">选择一张图片作为您的新头像</p>
</a>
<div class="avatar-wrapper" id="avatarWrapper">
<img id="avatar" class="avatar" src="" alt="头像" />
<div class="loading-overlay">
<div class="spinner"></div>
</div> </div>
<input id="file" type="file" /> </div>
<img id="avatar" style="height: 200px; width: 200px; border-radius: 50%" />
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js <label class="upload-btn">
"></script> 选择图片
<input id="file" type="file" accept="image/*" />
</label>
<a href="/qrlogin-nocookie.html" class="login-link">
还没有登录?点击登录
</a>
<p class="hint">支持 JPG、PNG 格式,建议尺寸 200x200</p>
</div>
<div id="toast" class="toast"></div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script> <script>
main() main()
async function main() { async function main() {
document.querySelector('input[type="file"]').addEventListener( const fileInput = document.querySelector('input[type="file"]');
'change', const avatarWrapper = document.getElementById('avatarWrapper');
function (e) {
var file = this.files[0] fileInput.addEventListener('change', function(e) {
upload(file) const file = this.files[0];
}, if (file) {
false, upload(file);
) }
}, false);
try {
showToast('正在加载头像...', 'info');
avatarWrapper.classList.add('loading');
const res = await axios({ const res = await axios({
url: `/user/detail?uid=32953014&timestamp=${Date.now()}`, url: `/user/detail?uid=32953014&timestamp=${Date.now()}`,
withCredentials: true, //跨域的话必须设置 withCredentials: true
}) });
document.querySelector('#avatar').src = res.data.profile.avatarUrl document.querySelector('#avatar').src = res.data.profile.avatarUrl;
hideToast();
} catch (error) {
hideToast();
showToast('加载头像失败,请刷新页面重试', 'error');
console.error('加载头像失败:', error);
} finally {
avatarWrapper.classList.remove('loading');
}
} }
async function upload(file) { async function upload(file) {
var formData = new FormData() const avatarWrapper = document.getElementById('avatarWrapper');
formData.append('imgFile', file)
const imgSize = await getImgSize(file) if (!file.type.startsWith('image/')) {
showToast('请选择图片文件', 'error');
return;
}
try {
showToast('正在上传头像...', 'info');
avatarWrapper.classList.add('loading');
var formData = new FormData();
formData.append('imgFile', file);
const imgSize = await getImgSize(file);
const res = await axios({ const res = await axios({
method: 'post', method: 'post',
url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0&timestamp=${Date.now()}`,
}&imgX=0&imgY=0&timestamp=${Date.now()}`,
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
data: formData, data: formData,
}) });
document.querySelector('#avatar').src = res.data.data.url
document.querySelector('#avatar').src = res.data.data.url;
showToast('头像更新成功!', 'success');
} catch (error) {
console.error('上传失败:', error);
const errorMsg = error.response?.data?.message || error.message || '上传失败,请重试';
showToast(errorMsg, 'error');
} finally {
avatarWrapper.classList.remove('loading');
} }
}
function getImgSize(file) { function getImgSize(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let reader = new FileReader() let reader = new FileReader();
reader.readAsDataURL(file) reader.readAsDataURL(file);
reader.onload = function(theFile) { reader.onload = function(theFile) {
let image = new Image() let image = new Image();
image.src = theFile.target.result image.src = theFile.target.result;
image.onload = function() { image.onload = function() {
resolve({ resolve({
width: this.width, width: this.width,
height: this.height, height: this.height,
}) });
};
image.onerror = function() {
reject(new Error('图片加载失败'));
};
};
reader.onerror = function() {
reject(new Error('文件读取失败'));
};
});
} }
function showToast(message, type = 'info') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast ${type}`;
requestAnimationFrame(() => {
toast.classList.add('show');
});
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
} }
})
function hideToast() {
const toast = document.getElementById('toast');
toast.classList.remove('show');
} }
</script> </script>
</body> </body>

View File

@ -4,26 +4,135 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>云盘上传</title> <title>云盘上传</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: 800px;
margin: 0 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: 24px;
}
.login-link {
display: block;
margin-bottom: 24px;
color: #666;
font-size: 14px;
text-decoration: none;
}
.login-link:hover {
color: #333;
text-decoration: underline;
}
.upload-section {
margin-bottom: 32px;
}
.upload-btn {
display: inline-block;
padding: 12px 28px;
background: #333;
color: white;
font-size: 15px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
border: none;
}
.upload-btn:hover {
background: #555;
}
.upload-btn input[type="file"] {
display: none;
}
.songs-list {
list-style: none;
}
.song-item {
padding: 12px 16px;
border-bottom: 1px solid #eee;
font-size: 14px;
color: #333;
}
.song-item:last-child {
border-bottom: none;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #999;
font-size: 14px;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
</style>
</head> </head>
<body> <body>
<div> <div class="container">
<a href="/qrlogin-nocookie.html"> 如果没登录,请先登录 </a> <h1>云盘上传</h1>
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
<div class="upload-section">
<label class="upload-btn">
选择文件(支持多选)
<input id="file" type="file" multiple accept="audio/*" />
</label>
</div> </div>
<input id="file" type="file" multiple />
<div id="app"> <div id="app">
<ul> <div v-if="loading" class="loading">加载中...</div>
<li v-for="(item,index) in songs" :key="index">{{item.songName}}</li> <ul v-else-if="songs.length > 0" class="songs-list">
<li v-for="(item, index) in songs" :key="index" class="song-item">
{{ item.songName }}
</li>
</ul> </ul>
<div v-else class="empty-state">暂无云盘歌曲</div>
</div>
</div> </div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script> <script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script src="https://fastly.jsdelivr.net/npm/vue"></script> <script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
<script> <script>
const app = Vue.createApp({ const app = Vue.createApp({
data() { data() {
return { return {
songs: [], songs: [],
loading: false,
} }
}, },
created() { created() {
@ -31,19 +140,23 @@
}, },
methods: { methods: {
getData() { getData() {
console.info('getdata') this.loading = true
const _this = this
axios({ axios({
url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem( url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
'cookie', })
)}`, .then((res) => {
}).then((res) => { this.songs = res.data.data || []
console.info(res.data) })
_this.songs = res.data.data .catch((err) => {
console.error('获取云盘数据失败:', err)
})
.finally(() => {
this.loading = false
}) })
}, },
}, },
}).mount('#app') }).mount('#app')
const fileUpdateTime = {} const fileUpdateTime = {}
let fileLength = 0 let fileLength = 0
@ -51,51 +164,46 @@
document document
.querySelector('input[type="file"]') .querySelector('input[type="file"]')
.addEventListener('change', function (e) { .addEventListener('change', function (e) {
console.info(this.files) const files = this.files
let currentIndx = 0 if (files.length === 0) return
fileLength = this.files.length
for (const item of this.files) { fileLength = files.length
currentIndx += 1 for (let i = 0; i < files.length; i++) {
upload(item, currentIndx) upload(files[i], i + 1)
} }
}) })
} }
main() main()
function upload(file, currentIndx) { function upload(file, currentIndex) {
var formData = new FormData() var formData = new FormData()
formData.append('songFile', file) formData.append('songFile', file)
axios({ axios({
method: 'post', method: 'post',
url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem( url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
'cookie',
)}`,
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
data: formData, data: formData,
}) })
.then((res) => { .then((res) => {
console.info(`${file.name} 上传成功`) console.log(`${file.name} 上传成功`)
if (currentIndx >= fileLength) { if (currentIndex >= fileLength) {
console.info('上传完毕') console.log('所有文件上传完毕')
} }
app.getData() app.getData()
}) })
.catch(async (err) => { .catch((err) => {
console.info(err) console.error(`${file.name} 上传失败:`, err)
console.info(fileUpdateTime) fileUpdateTime[file.name] = (fileUpdateTime[file.name] || 0) + 1
fileUpdateTime[file.name]
? (fileUpdateTime[file.name] += 1)
: (fileUpdateTime[file.name] = 1)
if (fileUpdateTime[file.name] >= 4) { if (fileUpdateTime[file.name] >= 4) {
console.error(`丢,这首歌怎么都传不上:${file.name}`) console.error(`文件 ${file.name} 上传失败次数过多,已停止重试`)
return return
} else { } else {
console.error(`${file.name} 失败 ${fileUpdateTime[file.name]} 次`) console.error(`${file.name} 上传失败 ${fileUpdateTime[file.name]} 次,正在重试...`)
upload(file, currentIndex)
} }
// await login()
upload(file, currentIndx)
}) })
} }
</script> </script>

View File

@ -36,12 +36,12 @@
} }
</script> </script>
<!-- Global site tag (gtag.js) - Google Analytics --> <!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-139996012-1"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=G-BPRGR23JEG"></script>
<script> <script>
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); } function gtag() { dataLayer.push(arguments); }
gtag('js', new Date()); gtag('js', new Date());
gtag('config', 'UA-139996012-1'); gtag('config', 'G-BPRGR23JEG');
</script> </script>
</html> </html>

View File

@ -1,50 +1,181 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>eapi 参数和返回内容解析</title> <title>eapi 参数和返回内容解析</title>
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
</head>
<style> <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: 900px;
margin: 0 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: 24px;
}
.form-group {
margin-bottom: 24px;
}
label {
display: block;
font-size: 14px;
font-weight: 500;
color: #555;
margin-bottom: 8px;
}
textarea {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 13px;
resize: vertical;
min-height: 200px;
outline: none;
}
textarea:focus {
border-color: #333;
}
.radio-group {
display: flex;
gap: 24px;
margin-bottom: 24px;
}
.radio-item {
display: flex;
align-items: center;
gap: 8px;
}
.radio-item input[type="radio"] {
cursor: pointer;
}
.radio-item label {
margin: 0;
cursor: pointer;
font-size: 14px;
}
button {
background: #333;
color: white;
padding: 12px 28px;
border: none;
border-radius: 6px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease;
}
button:hover {
background: #555;
}
.result-section {
margin-top: 24px;
}
.result-section label {
margin-bottom: 12px;
}
.decode-result { .decode-result {
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
background-color: #f0f0f0; background: #f9f9f9;
padding: 10px; padding: 16px;
border-radius: 5px; border-radius: 6px;
margin-top: 10px; border: 1px solid #eee;
height: 300px; min-height: 200px;
max-height: 400px;
overflow: auto; overflow: auto;
font-family: 'Courier New', monospace;
font-size: 13px;
}
.example-section {
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #eee;
}
.example-section h2 {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
}
.example-section img {
max-width: 100%;
height: auto;
border-radius: 6px;
margin-bottom: 16px;
border: 1px solid #eee;
} }
</style> </style>
</head>
<body> <body>
<div id="app" class="p-5 flex flex-col"> <div id="app" class="container">
<h1 class="text-2xl font-bold mb-5">eapi 参数和返回内容解析</h1> <h1>eapi 参数和返回内容解析</h1>
<textarea class="border border-gray-300 p-3 mb-5" v-model="hexString" rows="10"></textarea>
<button @click="decrypt" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"> <div class="form-group">
解密 <label for="hexString">十六进制字符串</label>
</button> <textarea id="hexString" v-model="hexString" rows="10"></textarea>
<div class="mt-3">
<input type="radio" id="format" name="format" v-model="isReq" value="true">
<label for="format" class="ml-2">请求数据request params(针对请求数据的 params)</label>
<input type="radio" id="noFormat" name="format" v-model="isReq" value="false" class="ml-5">
<label for="noFormat" class="ml-2">返回数据 response 二进制数据(针对返回内容解析)</label>
</div>
<div>
<p>解密结果:
<pre class="decode-result">{{ JSON.stringify(JSON.parse(result), null, 2) }}</pre>
</p>
</div> </div>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="req" name="format" v-model="isReq" value="true">
<label for="req">请求数据 request params</label>
</div>
<div class="radio-item">
<input type="radio" id="resp" name="format" v-model="isReq" value="false">
<label for="resp">返回数据 response 二进制数据</label>
</div>
</div>
<div> <button @click="decrypt">解密</button>
<p>使用例子:</p>
<img src="/static/eapi_params.png" /> <div class="result-section">
<img src="/static/eapi_response.png" /> <label>解密结果:</label>
<pre class="decode-result">{{ formatResult(result) }}</pre>
</div>
<div class="example-section">
<h2>使用示例</h2>
<img src="/static/eapi_params.png" alt="请求示例" />
<img src="/static/eapi_response.png" alt="响应示例" />
</div> </div>
</div> </div>
@ -64,6 +195,13 @@
this.decrypt() this.decrypt()
}, },
methods: { methods: {
formatResult(result) {
try {
return JSON.stringify(JSON.parse(result), null, 2)
} catch (e) {
return result
}
},
async decrypt() { async decrypt() {
try { try {
const res = await axios({ const res = await axios({
@ -77,7 +215,7 @@
console.log(res.data); console.log(res.data);
} catch (error) { } catch (error) {
console.error(error) console.error(error)
alert(error?.response?.data?.message || '解密失败,数据格式错误') alert(error?.response?.data?.message || '解密失败数据格式错误')
} }
} }
} }

View File

@ -7,33 +7,33 @@
<title>网易云音乐 API Enhanced</title> <title>网易云音乐 API Enhanced</title>
<style> <style>
:root { :root {
--fg: #111827; /* gray-900 */ --fg: #333;
--muted: #6b7280; /* gray-500 */ --muted: #666;
--border: #e5e7eb; /* gray-200 */ --border: #ddd;
--bg: #ffffff; --bg: #f5f5f5;
--panel: #f9fafb; /* gray-50 */ --panel: #ffffff;
--accent: #2563eb; /* blue-600 */ --accent: #333;
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
html, body { height: 100%; } html, body { height: 100%; }
body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, PingFang SC, Helvetica, Arial, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; } 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: 960px; margin: 40px auto; padding: 0 20px; } .container { max-width: 960px; margin: 40px auto; padding: 0 20px; }
header.site-header { margin-bottom: 24px; } header.site-header { margin-bottom: 24px; }
header.site-header h1 { font-size: 28px; font-weight: 700; margin: 0; } header.site-header h1 { font-size: 28px; font-weight: 600; margin: 0; }
.badge { display: inline-block; margin-left: 8px; padding: 2px 8px; border: 1px solid var(--border); border-radius: 14px; font-size: 12px; color: var(--muted); } .badge { display: inline-block; margin-left: 8px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 12px; font-size: 12px; color: var(--muted); }
.sub { margin-top: 6px; color: var(--muted); } .sub { margin-top: 8px; color: var(--muted); font-size: 14px; }
.block { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 16px; } .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 h2 { margin: 0 0 10px; font-size: 18px; } .block h2 { margin: 0 0 12px; font-size: 18px; font-weight: 600; }
.kvs { display: grid; grid-template-columns: 140px 1fr; gap: 8px 16px; align-items: center; } .kvs { display: grid; grid-template-columns: 140px 1fr; gap: 8px 16px; align-items: center; }
.kvs div:first-child { color: var(--muted); } .kvs div:first-child { color: var(--muted); }
ul.links { list-style: none; padding: 0; margin: 0; } ul.links { list-style: none; padding: 0; margin: 0; }
ul.links li { margin: 6px 0; } ul.links li { margin: 8px 0; }
ul.links a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted var(--border); } ul.links a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted var(--border); transition: all 0.2s ease; }
ul.links a:hover { color: var(--accent); border-bottom-color: var(--accent); } ul.links a:hover { color: var(--accent); border-bottom-color: var(--accent); }
pre { margin: 0; background: #fff; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow: auto; } pre { margin: 0; background: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow: auto; }
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-size: 13px; } code { font-family: 'Courier New', monospace; font-size: 13px; }
footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); } footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); text-align: center; }
footer.site-footer a { color: var(--fg); text-decoration: none; } footer.site-footer a { color: var(--fg); text-decoration: none; transition: color 0.2s ease; }
footer.site-footer a:hover { color: var(--accent); } footer.site-footer a:hover { color: var(--accent); }
</style> </style>
</head> </head>

View File

@ -1,92 +1,321 @@
<!-- eslint-disable prettier/prettier -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>一起听</title> <title>一起听 - 主机模式</title>
<script src="https://unpkg.com/petite-vue"></script> <script src="https://unpkg.com/petite-vue"></script>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script> <script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<link <style>
rel="stylesheet" * {
href="https://unpkg.com/mdui@1.0.2/dist/css/mdui.min.css" margin: 0;
/> padding: 0;
<script src="https://unpkg.com/mdui@1.0.2/dist/js/mdui.min.js"></script> 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: 900px;
margin: 0 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: 16px;
}
.login-link {
display: block;
margin-bottom: 24px;
color: #666;
font-size: 14px;
text-decoration: none;
}
.login-link:hover {
color: #333;
text-decoration: underline;
}
.message {
padding: 12px 16px;
background: #f9f9f9;
border-radius: 6px;
margin-bottom: 20px;
font-size: 14px;
color: #555;
}
.audio-player {
width: 100%;
margin-bottom: 20px;
}
.btn {
background: #333;
color: white;
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease;
margin-right: 8px;
margin-bottom: 8px;
}
.btn:hover {
background: #555;
}
.section {
margin-bottom: 24px;
padding: 20px;
background: #f9f9f9;
border-radius: 6px;
}
.section h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
input[type="text"], input[type="number"] {
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
outline: none;
width: 200px;
}
input:focus {
border-color: #333;
}
.input-group {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.input-group label {
font-size: 14px;
color: #555;
min-width: 80px;
}
.share-link {
padding: 12px;
background: #f0f0f0;
border-radius: 6px;
font-size: 13px;
color: #666;
word-break: break-all;
margin-bottom: 12px;
}
.user-list {
list-style: none;
max-height: 200px;
overflow-y: auto;
}
.user-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.user-item:last-child {
border-bottom: none;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.user-name {
font-size: 14px;
color: #333;
}
.track-list {
list-style: none;
max-height: 300px;
overflow-y: auto;
}
.track-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background 0.2s ease;
}
.track-item:hover {
background: #f5f5f5;
}
.track-item:last-child {
border-bottom: none;
}
.track-cover {
width: 40px;
height: 40px;
border-radius: 4px;
object-fit: cover;
}
.track-name {
font-size: 14px;
color: #333;
}
details {
cursor: pointer;
}
details summary {
font-size: 14px;
font-weight: 500;
color: #555;
padding: 8px 0;
}
details summary:hover {
color: #333;
}
details[open] summary {
margin-bottom: 12px;
}
.control-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
</style>
</head> </head>
<body class="mdui-container"> <body class="container">
<div> <a href="/qrlogin.html" class="login-link">还没登录?点击登录</a>
<a href="/qrlogin.html"> 如果没登录,请先登录 </a>
</div>
<h1>一起听 - 主机模式</h1> <h1>一起听 - 主机模式</h1>
<div>消息: {{message}}</div>
<audio id="player" autoplay controls></audio> <div class="message">消息: {{message}}</div>
<br />
<br /> <audio id="player" class="audio-player" autoplay controls></audio>
<button v-if="!account.login" @click="login">获取登录状态</button>
<div>您的当前登录账号为: {{account.nickname}}</div> <div v-if="!account.login">
<br /> <button class="btn" @click="login">获取登录状态</button>
<div v-if="account.login"> </div>
<button v-if="!roomInfo.roomId" @click="createRoom">创建房间</button>
<div class="section">
<h3>账号信息</h3>
<div>当前登录账号: {{account.nickname}}</div>
</div>
<div v-if="account.login" class="section">
<h3>房间管理</h3>
<div class="control-buttons">
<button v-if="!roomInfo.roomId" class="btn" @click="createRoom">创建房间</button>
</div>
<details> <details>
<summary>加入房间</summary> <summary>加入房间</summary>
<div><span>房间ID: </span><input v-model="roomInfo.roomId" /></div> <div class="input-group">
<div> <label>房间ID:</label>
<span>邀请者 ID: </span><input v-model="roomInfo.inviterId" /> <input v-model="roomInfo.roomId" type="text" />
</div> </div>
<button @click="joinRoom">点击加入</button> <div class="input-group">
<label>邀请者ID:</label>
<input v-model="roomInfo.inviterId" type="text" />
</div>
<button class="btn" @click="joinRoom">加入房间</button>
</details> </details>
<div v-if="roomInfo.roomId"> <div v-if="roomInfo.roomId" style="margin-top: 16px;">
<div> <h4>分享链接</h4>
分享链接为: <div class="share-link">
https://st.music.163.com/listen-together/share/?songId=1372188635&roomId={{roomInfo.roomId}}&inviterId={{roomInfo.inviterId}} https://st.music.163.com/listen-together/share/?songId=1372188635&roomId={{roomInfo.roomId}}&inviterId={{roomInfo.inviterId}}
</div> </div>
<br /> <button class="btn" @click="refreshRoom">刷新房间状态</button>
<button @click="refreshRoom">刷新房间状态</button> <button class="btn" @click="closeRoom">关闭房间</button>
<div>在线用户:</div>
<ul class="mdui-list"> <h4 style="margin-top: 16px;">在线用户</h4>
<li <ul class="user-list">
v-for="user in roomInfo.roomUsers" <li v-for="user in roomInfo.roomUsers" :key="user.userId" class="user-item">
class="mdui-list-item mdui-ripple" <img :src="user.avatarUrl" class="user-avatar" alt="avatar" />
> <span class="user-name">{{user.nickname}}</span>
<div class="mdui-list-item-avatar">
<img :src="user.avatarUrl" />
</div>
<div class="mdui-list-item-content">{{user.nickname}}</div>
</li> </li>
</ul> </ul>
<button v-if="roomInfo.roomId" @click="closeRoom">关闭房间</button>
</div> </div>
</div> </div>
<button @click="playTrack">播放</button>
<button @click="pauseTrack">暂停</button> <div class="section">
<button @click="seekTrack">同步进度</button> <h3>播放控制</h3>
<div class="control-buttons">
<button class="btn" @click="playTrack">播放</button>
<button class="btn" @click="pauseTrack">暂停</button>
<button class="btn" @click="seekTrack">同步进度</button>
</div>
</div>
<details> <details>
<summary>播放列表</summary> <summary>播放列表</summary>
<br /> <div class="section">
<div> <div class="input-group">
<span>歌单ID: </span><input v-model="playlistInfo.playlistId" /> <label>歌单ID:</label>
<input v-model="playlistInfo.playlistId" type="text" />
</div> </div>
<button @click="loadPlaylist">加载歌单到播放列表</button> <button class="btn" @click="loadPlaylist">加载歌单</button>
<span>{{playlistInfo.playlistName}}</span> <div style="margin-top: 12px; font-size: 14px; color: #555;">
<br /> 歌单名称: {{playlistInfo.playlistName}}
<br /> </div>
<div>歌单内容:</div>
<ul class="mdui-list"> <h4 style="margin-top: 16px;">歌单内容</h4>
<ul class="track-list">
<li <li
@click="gotoTrack(track.id)" @click="gotoTrack(track.id)"
v-for="track in playlistInfo.playlistTracks" v-for="track in playlistInfo.playlistTracks"
class="mdui-list-item mdui-ripple" :key="track.id"
class="track-item"
> >
<div class="mdui-list-item-avatar"> <img :src="track.al.picUrl" class="track-cover" alt="cover" />
<img :src="track.al.picUrl" /> <span class="track-name">{{track.name}}</span>
</div>
<div class="mdui-list-item-content">{{track.name}}</div>
</li> </li>
</ul> </ul>
</div>
</details> </details>
</body> </body>
<script> <script>
PetiteVue.createApp({ PetiteVue.createApp({
message: '请点击获取登录状态', message: '请点击获取登录状态',
@ -126,7 +355,7 @@
this.account.userId = res.data.data.profile.userId this.account.userId = res.data.data.profile.userId
this.account.nickname = res.data.data.profile.nickname this.account.nickname = res.data.data.profile.nickname
this.account.login = true this.account.login = true
this.message = '成功登录, 请创建房间' this.message = '成功登录请创建房间'
} }
}, },
joinRoom: async function () { joinRoom: async function () {
@ -212,7 +441,7 @@
}) })
console.info(res) console.info(res)
if (res.data.code != 200 || !res.data.data.inRoom) { if (res.data.code != 200 || !res.data.data.inRoom) {
this.message = '房间状态获取失败, 可能退出了房间' this.message = '房间状态获取失败可能退出了房间'
} else { } else {
this.roomInfo.roomUsers = res.data.data.roomInfo.roomUsers this.roomInfo.roomUsers = res.data.data.roomInfo.roomUsers
} }

View File

@ -5,21 +5,143 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录</title> <title>登录</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;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 100%;
text-align: center;
}
h1 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 32px;
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
label {
display: block;
font-size: 14px;
font-weight: 500;
color: #555;
margin-bottom: 8px;
}
input {
width: 100%;
padding: 12px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 15px;
outline: none;
}
input:focus {
border-color: #333;
}
.btn {
width: 100%;
padding: 14px;
background: #333;
color: white;
font-size: 15px;
font-weight: 500;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
}
.btn:hover {
background: #555;
}
.btn:disabled {
background: #999;
cursor: not-allowed;
}
.result {
margin-top: 24px;
padding: 16px;
background: #f9f9f9;
border-radius: 6px;
font-size: 13px;
color: #666;
text-align: left;
white-space: pre-wrap;
word-break: break-all;
}
.error {
color: #ef4444;
background: #fef2f2;
}
.success {
color: #10b981;
background: #f0fdf4;
}
</style>
</head> </head>
<body> <body>
<div class="container">
<h1>登录</h1>
<p class="subtitle">使用手机号和密码登录网易云音乐</p>
<div class="form-group">
<label for="phone">手机号</label>
<input type="tel" id="phone" placeholder="请输入手机号" />
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" placeholder="请输入密码" />
</div>
<button id="loginBtn" class="btn" onclick="handleLogin()">登录</button>
<div id="result" class="result" style="display: none;"></div>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script> <script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script> <script>
const phone = '' // 这里填手机号
const password = '' // 这里填密码
const fileUpdateTime = {} const fileUpdateTime = {}
if (!phone || !password) {
const msg = '请设置你的手机号码和密码'
alert(msg)
throw new Error(msg)
}
async function login() { async function login(phone, password) {
const res = await axios({ const res = await axios({
url: `/login/cellphone`, url: `/login/cellphone`,
method: 'post', method: 'post',
@ -30,8 +152,29 @@
}) })
return res.data.cookie return res.data.cookie
} }
async function main() {
const cookieToken = await login() async function handleLogin() {
const phoneInput = document.getElementById('phone')
const passwordInput = document.getElementById('password')
const loginBtn = document.getElementById('loginBtn')
const resultDiv = document.getElementById('result')
const phone = phoneInput.value.trim()
const password = passwordInput.value
if (!phone || !password) {
showResult('请输入手机号和密码', 'error')
return
}
loginBtn.disabled = true
loginBtn.textContent = '登录中...'
showResult('正在登录...', 'info')
try {
const cookieToken = await login(phone, password)
localStorage.setItem('cookie', cookieToken)
const res = await axios({ const res = await axios({
url: `/login/status`, url: `/login/status`,
method: 'post', method: 'post',
@ -39,8 +182,37 @@
cookie: cookieToken, cookie: cookieToken,
}, },
}) })
showResult(`登录成功!\n${JSON.stringify(res.data, null, 2)}`, 'success')
} catch (error) {
console.error('登录失败:', error)
const errorMsg = error.response?.data?.message || error.message || '登录失败,请重试'
showResult(`登录失败:${errorMsg}`, 'error')
} finally {
loginBtn.disabled = false
loginBtn.textContent = '登录'
} }
main() }
function showResult(message, type = 'info') {
const resultDiv = document.getElementById('result')
resultDiv.style.display = 'block'
resultDiv.textContent = message
resultDiv.className = 'result ' + type
}
// 支持回车登录
document.getElementById('password').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
handleLogin()
}
})
document.getElementById('phone').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
document.getElementById('password').focus()
}
})
</script> </script>
</body> </body>

View File

@ -5,56 +5,297 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>歌单封面上传</title> <title>歌单封面上传</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: 500px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 32px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}
h1 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 32px;
}
.login-link {
display: block;
margin-bottom: 24px;
color: #666;
font-size: 14px;
text-decoration: none;
}
.login-link:hover {
color: #333;
text-decoration: underline;
}
.cover-wrapper {
position: relative;
width: 180px;
height: 180px;
margin: 0 auto 24px;
}
.cover {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
border: 4px solid #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
label {
display: block;
font-size: 14px;
font-weight: 500;
color: #555;
margin-bottom: 8px;
}
input[type="text"] {
width: 100%;
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
outline: none;
}
input[type="text"]:focus {
border-color: #333;
}
.upload-btn {
display: inline-block;
padding: 12px 28px;
background: #333;
color: white;
font-size: 15px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
border: none;
}
.upload-btn:hover {
background: #555;
}
.upload-btn input[type="file"] {
display: none;
}
.loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.loading.active {
opacity: 1;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid #e0e0e0;
border-top-color: #333;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.result {
margin-top: 20px;
padding: 12px 16px;
border-radius: 6px;
font-size: 14px;
text-align: left;
}
.result.success {
background: #d1fae5;
color: #065f46;
}
.result.error {
background: #fee2e2;
color: #991b1b;
}
</style>
</head> </head>
<body> <body>
<div> <div class="container">
<a href="/qrlogin-nocookie.html"> <h1>歌单封面上传</h1>
如果没登录,请先登录 <p class="subtitle">上传自定义歌单封面图片</p>
</a>
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
<div class="form-group">
<label for="playlistId">歌单 ID</label>
<input type="text" id="playlistId" placeholder="请输入歌单ID" />
</div> </div>
<input id="file" type="file" name="filename" />
<img id="playlist_cover" style="height: 200px; width: 200px; border-radius: 50%" /> <div class="cover-wrapper">
<img id="playlist_cover" class="cover" src="" alt="歌单封面" />
<div class="loading" id="loading">
<div class="spinner"></div>
</div>
</div>
<label class="upload-btn">
选择封面图片
<input id="file" type="file" name="filename" accept="image/*" />
</label>
<div id="result" class="result" style="display: none;"></div>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script> <script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script> <script>
const playlist_id = '' const loadingOverlay = document.getElementById('loading')
if (!playlist_id) { const playlistIdInput = document.getElementById('playlistId')
const msg = '请设置你的歌单id' const resultDiv = document.getElementById('result')
alert(msg)
throw new Error(msg) function showLoading() {
loadingOverlay.classList.add('active')
} }
main() function hideLoading() {
async function main() { loadingOverlay.classList.remove('active')
document.querySelector('input[type="file"]').addEventListener( }
'change',
function (e) { function showResult(message, type) {
var file = this.files[0] resultDiv.textContent = message
upload(file) resultDiv.className = 'result ' + type
}, resultDiv.style.display = 'block'
false, }
)
function hideResult() {
resultDiv.style.display = 'none'
}
async function loadPlaylistCover() {
const playlistId = playlistIdInput.value.trim()
if (!playlistId) {
return
}
showLoading()
hideResult()
try {
const res = await axios({ const res = await axios({
url: `/playlist/detail?id=${playlist_id}&timestamp=${Date.now()}`, url: `/playlist/detail?id=${playlistId}&timestamp=${Date.now()}`,
}) })
document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl
hideResult()
} catch (error) {
console.error('加载封面失败:', error)
showResult('加载封面失败请检查歌单ID', 'error')
} finally {
hideLoading()
}
} }
async function upload(file) { // 监听歌单ID输入变化
playlistIdInput.addEventListener('input', function() {
loadPlaylistCover()
})
// 监听文件选择
document
.querySelector('input[type="file"]')
.addEventListener('change', async function (e) {
const file = this.files[0]
const playlistId = playlistIdInput.value.trim()
if (!playlistId) {
showResult('请先输入歌单ID', 'error')
return
}
if (!file) {
return
}
showLoading()
hideResult()
try {
await upload(file, playlistId)
} catch (error) {
console.error('上传失败:', error)
showResult('上传失败,请重试', 'error')
} finally {
hideLoading()
}
})
async function upload(file, playlistId) {
var formData = new FormData() var formData = new FormData()
formData.append('imgFile', file) formData.append('imgFile', file)
const imgSize = await getImgSize(file) const imgSize = await getImgSize(file)
const res = await axios({ const res = await axios({
method: 'post', method: 'post',
url: `/playlist/cover/update?id=${playlist_id}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width url: `/playlist/cover/update?id=${playlistId}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0&timestamp=${Date.now()}`,
}&imgX=0&imgY=0&timestamp=${Date.now()}`,
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
data: formData, data: formData,
}) })
document.querySelector('#playlist_cover').src = res.data.data.url document.querySelector('#playlist_cover').src = res.data.data.url
showResult('封面上传成功!', 'success')
} }
function getImgSize(file) { function getImgSize(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let reader = new FileReader() let reader = new FileReader()
@ -68,6 +309,12 @@
height: this.height, height: this.height,
}) })
} }
image.onerror = function() {
reject(new Error('图片加载失败'))
}
}
reader.onerror = function() {
reject(new Error('文件读取失败'))
} }
}) })
} }

View File

@ -2,115 +2,300 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>歌单导入工具</title> <title>歌单导入工具</title>
<!-- 引入Bootstrap CSS --> <style>
<link href="https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> * {
<!-- 引入Bootstrap JS --> margin: 0;
<script src="https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> padding: 0;
<!-- 引入axios用于发送异步请求 --> box-sizing: border-box;
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script> }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 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;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 24px;
}
.tabs {
display: flex;
gap: 4px;
margin-bottom: 24px;
border-bottom: 1px solid #eee;
}
.tab-btn {
padding: 12px 24px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
font-size: 14px;
font-weight: 500;
color: #666;
cursor: pointer;
transition: all 0.2s ease;
}
.tab-btn:hover {
color: #333;
}
.tab-btn.active {
color: #333;
border-bottom-color: #333;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
font-size: 14px;
font-weight: 500;
color: #555;
margin-bottom: 8px;
}
input[type="text"], textarea {
width: 100%;
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
outline: none;
font-family: inherit;
}
input[type="text"]:focus, textarea:focus {
border-color: #333;
}
textarea {
min-height: 120px;
resize: vertical;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
}
table th, table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
table th {
font-size: 14px;
font-weight: 600;
color: #555;
background: #f9f9f9;
}
table td input {
width: 100%;
}
.btn {
padding: 10px 20px;
background: #333;
color: white;
font-size: 14px;
font-weight: 500;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
}
.btn:hover {
background: #555;
}
.btn-secondary {
background: #666;
}
.btn-secondary:hover {
background: #888;
}
.input-group {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.input-group input {
flex: 1;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 24px;
}
.checkbox-group input[type="checkbox"] {
cursor: pointer;
}
.checkbox-group label {
margin: 0;
cursor: pointer;
}
input:disabled {
background: #f5f5f5;
cursor: not-allowed;
}
</style>
</head> </head>
<body> <body>
<div class="container mt-5"> <div class="container">
<h1 class="mb-4">歌单导入工具</h1> <h1>歌单导入工具</h1>
<p>请选择一种导入方式并填写相关信息:</p> <p class="subtitle">请选择一种导入方式并填写相关信息</p>
<!-- 表单开始 --> <ul class="tabs" id="importTabs" role="tablist">
<form id="importForm" novalidate> <li role="presentation">
<!-- 选项卡导航 --> <button class="tab-btn active" id="metadata-tab" data-bs-toggle="tab" data-bs-target="#metadata" type="button" role="tab" aria-controls="metadata" aria-selected="true">元数据导入</button>
<ul class="nav nav-tabs mb-3" id="importTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="metadata-tab" data-bs-toggle="tab" data-bs-target="#metadata" type="button" role="tab" aria-controls="metadata" aria-selected="true">元数据导入</button>
</li> </li>
<li class="nav-item" role="presentation"> <li role="presentation">
<button class="nav-link" id="text-tab" data-bs-toggle="tab" data-bs-target="#text" type="button" role="tab" aria-controls="text" aria-selected="false">文字导入</button> <button class="tab-btn" id="text-tab" data-bs-toggle="tab" data-bs-target="#text" type="button" role="tab" aria-controls="text" aria-selected="false">文字导入</button>
</li> </li>
<li class="nav-item" role="presentation"> <li role="presentation">
<button class="nav-link" id="link-tab" data-bs-toggle="tab" data-bs-target="#link" type="button" role="tab" aria-controls="link" aria-selected="false">链接导入</button> <button class="tab-btn" id="link-tab" data-bs-toggle="tab" data-bs-target="#link" type="button" role="tab" aria-controls="link" aria-selected="false">链接导入</button>
</li> </li>
</ul> </ul>
<!-- 选项卡面板 --> <div class="tab-content active" id="importTabContent">
<div class="tab-content" id="importTabContent">
<!-- 元数据导入 --> <!-- 元数据导入 -->
<div class="tab-pane fade show active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab"> <div class="tab-content active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab">
<table class="table table-bordered mb-3"> <table>
<thead> <thead>
<tr> <tr>
<th scope="col">歌曲名称</th> <th style="width: 33%">歌曲名称</th>
<th scope="col">艺术家</th> <th style="width: 33%">艺术家</th>
<th scope="col">专辑</th> <th style="width: 33%">专辑</th>
</tr> </tr>
</thead> </thead>
<tbody id="metadataTableBody"> <tbody id="metadataTableBody">
<!-- 默认添加一行 -->
<tr> <tr>
<td><input type="text" class="form-control" name="name[]" placeholder="歌曲名称"></td> <td><input type="text" name="name[]" placeholder="歌曲名称"></td>
<td><input type="text" class="form-control" name="artist[]" placeholder="艺术家"></td> <td><input type="text" name="artist[]" placeholder="艺术家"></td>
<td><input type="text" class="form-control" name="album[]" placeholder="专辑"></td> <td><input type="text" name="album[]" placeholder="专辑"></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<button type="button" class="btn btn-secondary mb-3" id="addMetadataRow">增加歌曲</button> <button type="button" class="btn btn-secondary" id="addMetadataRow">增加歌曲</button>
</div> </div>
<!-- 文字导入 --> <!-- 文字导入 -->
<div class="tab-pane fade" id="text" role="tabpanel" aria-labelledby="text-tab"> <div class="tab-content" id="text" role="tabpanel" aria-labelledby="text-tab">
<div class="mb-3"> <div class="form-group">
<label for="textInput" class="form-label">文字:</label> <label for="textInput">文字内容</label>
<textarea class="form-control" id="textInput" name="text" rows="5"></textarea> <textarea id="textInput" name="text" rows="5" placeholder="请输入歌曲信息,每行一首歌曲"></textarea>
</div> </div>
<div class="mb-3"> <div class="form-group">
<label for="playlistNameInput" class="form-label">歌单名:</label> <label for="playlistNameInput">歌单名称</label>
<input type="text" class="form-control" id="playlistNameInput" name="playlistName" placeholder="请输入歌单名"> <input type="text" id="playlistNameInput" name="playlistName" placeholder="请输入歌单名">
</div> </div>
</div> </div>
<!-- 链接导入 --> <!-- 链接导入 -->
<div class="tab-pane fade" id="link" role="tabpanel" aria-labelledby="link-tab"> <div class="tab-content" id="link" role="tabpanel" aria-labelledby="link-tab">
<div class="mb-3"> <div class="form-group">
<label for="linkInputs" class="form-label">链接:</label> <label>链接列表</label>
<div id="linkInputsContainer"> <div id="linkInputsContainer">
<div class="input-group mb-3"> <div class="input-group">
<input type="text" class="form-control" id="linkInput0" name="linkInput0" placeholder="请输入链接"> <input type="text" id="linkInput0" name="linkInput0" placeholder="请输入链接">
<button type="button" class="btn btn-secondary removeLinkButton" data-index="0">×</button> <button type="button" class="btn btn-secondary removeLinkButton" data-index="0">×</button>
</div> </div>
</div> </div>
<button type="button" class="btn btn-secondary mb-3" id="addLinkButton">增加链接</button> <button type="button" class="btn btn-secondary" id="addLinkButton" style="margin-top: 8px;">增加链接</button>
<div class="mb-3">
<label for="playlistNameLinkInput" class="form-label">歌单名:</label>
<input type="text" class="form-control" id="playlistNameLinkInput" name="playlistName" placeholder="请输入歌单名">
</div> </div>
<div class="form-group" style="margin-top: 20px;">
<label for="playlistNameLinkInput">歌单名称</label>
<input type="text" id="playlistNameLinkInput" name="playlistName" placeholder="请输入歌单名">
</div> </div>
</div> </div>
</div> </div>
<!-- 是否导入我喜欢的音乐 --> <div class="checkbox-group">
<div class="form-check"> <input type="checkbox" value="" id="importStarCheckbox">
<input class="form-check-input" type="checkbox" value="" id="importStarCheckbox"> <label for="importStarCheckbox">
<label class="form-check-label" for="importStarCheckbox"> 导入"我喜欢的音乐"
导入“我喜欢的音乐”
</label> </label>
</div> </div>
<!-- 提交按钮 --> <button type="submit" class="btn" id="submitBtn">导入歌曲</button>
<button type="submit" class="btn btn-primary mt-3">导入歌曲</button> </div>
</form>
<!-- 表单结束 -->
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script> <script>
// 选项卡切换
const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content[id]');
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
tabBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
btn.classList.add('active');
const targetId = btn.getAttribute('data-bs-target');
document.getElementById(targetId).classList.add('active');
});
});
// 动态增加链接输入框 // 动态增加链接输入框
document.getElementById('addLinkButton').addEventListener('click', function() { document.getElementById('addLinkButton').addEventListener('click', function() {
var container = document.getElementById('linkInputsContainer'); var container = document.getElementById('linkInputsContainer');
var newIndex = container.childElementCount - 1; // 减去非输入框元素的数量 var newIndex = container.children.length;
var newInput = document.createElement('input'); var newInput = document.createElement('input');
newInput.type = 'text'; newInput.type = 'text';
newInput.className = 'form-control'; newInput.className = '';
newInput.id = `linkInput${newIndex}`; newInput.id = `linkInput${newIndex}`;
newInput.name = `linkInput${newIndex}`; newInput.name = `linkInput${newIndex}`;
newInput.placeholder = '请输入链接'; newInput.placeholder = '请输入链接';
newInput.style.cssText = 'flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;';
var removeButton = document.createElement('button'); var removeButton = document.createElement('button');
removeButton.type = 'button'; removeButton.type = 'button';
removeButton.className = 'btn btn-secondary removeLinkButton'; removeButton.className = 'btn btn-secondary';
removeButton.textContent = '×'; removeButton.textContent = '×';
removeButton.dataset.index = newIndex.toString(); removeButton.dataset.index = newIndex.toString();
removeButton.addEventListener('click', function() { removeButton.addEventListener('click', function() {
@ -119,7 +304,7 @@
}); });
var inputGroup = document.createElement('div'); var inputGroup = document.createElement('div');
inputGroup.className = 'input-group mb-3'; inputGroup.className = 'input-group';
inputGroup.appendChild(newInput); inputGroup.appendChild(newInput);
inputGroup.appendChild(removeButton); inputGroup.appendChild(removeButton);
@ -131,37 +316,16 @@
var container = document.getElementById('metadataTableBody'); var container = document.getElementById('metadataTableBody');
var newRow = document.createElement('tr'); var newRow = document.createElement('tr');
var nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.className = 'form-control';
nameInput.name = 'name[]';
nameInput.placeholder = '歌曲名称';
var artistInput = document.createElement('input');
artistInput.type = 'text';
artistInput.className = 'form-control';
artistInput.name = 'artist[]';
artistInput.placeholder = '艺术家';
var albumInput = document.createElement('input');
albumInput.type = 'text';
albumInput.className = 'form-control';
albumInput.name = 'album[]';
albumInput.placeholder = '专辑';
newRow.innerHTML = ` newRow.innerHTML = `
<td>${nameInput.outerHTML}</td> <td><input type="text" name="name[]" placeholder="歌曲名称" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
<td>${artistInput.outerHTML}</td> <td><input type="text" name="artist[]" placeholder="艺术家" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
<td>${albumInput.outerHTML}</td> <td><input type="text" name="album[]" placeholder="专辑" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
`; `;
container.appendChild(newRow); container.appendChild(newRow);
}); });
document.getElementById('importForm').addEventListener('submit', async function(event) { document.getElementById('submitBtn').addEventListener('click', async function() {
// 阻止默认行为
event.preventDefault();
// 获取表单值 // 获取表单值
let text = document.getElementById('textInput').value; let text = document.getElementById('textInput').value;
let links = []; let links = [];
@ -169,7 +333,7 @@
let playlistName = ''; let playlistName = '';
// 获取所有链接输入框的值 // 获取所有链接输入框的值
let linkInputs = document.querySelectorAll('#linkInputsContainer .input-group .form-control'); let linkInputs = document.querySelectorAll('#linkInputsContainer .input-group input[type="text"]');
linkInputs.forEach(function(input) { linkInputs.forEach(function(input) {
if (input.value.trim() !== '') { if (input.value.trim() !== '') {
links.push(input.value); links.push(input.value);
@ -229,15 +393,7 @@
let taskId = res.data?.data?.taskId let taskId = res.data?.data?.taskId
if (taskId) { if (taskId) {
alert(`任务创建成功! 正在导入, 请稍等; 任务id:${taskId}`) alert(`任务创建成功正在导入请稍等任务id${taskId}`)
// const res2 = await axios({
// url: `/playlist/import/task/status?timestamp=${Date.now()}`,
// method: 'post',
// data: {
// id: taskId
// },
// });
// alert(JSON.stringify(res2.data, null, 2));
} }
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
@ -257,6 +413,5 @@
// 初始化时设置歌单名输入框的状态 // 初始化时设置歌单名输入框的状态
document.getElementById('importStarCheckbox').dispatchEvent(new Event('change')); document.getElementById('importStarCheckbox').dispatchEvent(new Event('change'));
</script> </script>
</div>
</body> </body>
</html> </html>

View File

@ -5,45 +5,164 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>二维码登录</title> <title>二维码登录</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;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
padding: 48px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
max-width: 450px;
width: 100%;
text-align: center;
}
h1 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 32px;
}
.qr-wrapper {
display: inline-block;
padding: 16px;
background: #f9f9f9;
border-radius: 8px;
margin-bottom: 24px;
}
#qrImg {
width: 200px;
height: 200px;
display: block;
}
.info {
text-align: left;
padding: 16px;
background: #f9f9f9;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 12px;
color: #666;
white-space: pre-wrap;
word-break: break-all;
max-height: 200px;
overflow-y: auto;
}
.status {
margin-top: 16px;
padding: 12px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
}
.status.waiting {
background: #fef3c7;
color: #92400e;
}
.status.success {
background: #d1fae5;
color: #065f46;
}
.status.error {
background: #fee2e2;
color: #991b1b;
}
.hint {
margin-top: 16px;
font-size: 13px;
color: #999;
}
</style>
</head> </head>
<body> <body>
<img id="qrImg" /> <div class="container">
<div id="info" class="info"></div> <h1>二维码登录</h1>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js <p class="subtitle">使用网易云音乐App扫描二维码登录</p>
"></script>
<script>
<div class="qr-wrapper">
<img id="qrImg" src="" alt="二维码加载中..." />
</div>
<div id="status" class="status waiting">等待扫描...</div>
<div id="info" class="info"></div>
<p class="hint">请打开网易云音乐App扫描上方二维码完成登录</p>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script>
async function login() { async function login() {
let timer let timer
let timestamp = Date.now() const statusDiv = document.getElementById('status')
const cookie = localStorage.getItem('cookie') const cookie = localStorage.getItem('cookie')
updateStatus('加载二维码...', 'waiting')
getLoginStatus(cookie) getLoginStatus(cookie)
try {
const res = await axios({ const res = await axios({
url: `/login/qr/key?timestamp=${Date.now()}`, url: `/login/qr/key?timestamp=${Date.now()}`,
}) })
const key = res.data.data.unikey const key = res.data.data.unikey
const res2 = await axios({ const res2 = await axios({
url: `/login/qr/create?key=${key}&platform=web&qrimg=true&timestamp=${Date.now()}`, url: `/login/qr/create?key=${key}&platform=web&qrimg=true&timestamp=${Date.now()}`,
}) })
document.querySelector('#qrImg').src = res2.data.data.qrimg document.querySelector('#qrImg').src = res2.data.data.qrimg
updateStatus('请扫描二维码', 'waiting')
timer = setInterval(async () => { timer = setInterval(async () => {
const statusRes = await checkStatus(key) const statusRes = await checkStatus(key)
if (statusRes.code === 800) { if (statusRes.code === 800) {
alert('二维码已过期,请重新获取') updateStatus('二维码已过期,请刷新页面', 'error')
clearInterval(timer) clearInterval(timer)
} } else if (statusRes.code === 801) {
if (statusRes.code === 803) { updateStatus('二维码已扫描,请在手机上确认', 'waiting')
// 这一步会返回cookie } else if (statusRes.code === 802) {
updateStatus('登录成功,正在保存信息...', 'waiting')
} else if (statusRes.code === 803) {
clearInterval(timer) clearInterval(timer)
alert('授权登录成功') updateStatus('授权登录成功!', 'success')
await getLoginStatus(statusRes.cookie) await getLoginStatus(statusRes.cookie)
localStorage.setItem('cookie', statusRes.cookie) localStorage.setItem('cookie', statusRes.cookie)
} }
}, 3000) }, 3000)
} catch (error) {
console.error('登录失败:', error)
updateStatus('二维码加载失败,请刷新页面重试', 'error')
}
} }
login()
async function checkStatus(key) { async function checkStatus(key) {
const res = await axios({ const res = await axios({
@ -51,7 +170,9 @@
}) })
return res.data return res.data
} }
async function getLoginStatus(cookie = '') { async function getLoginStatus(cookie = '') {
try {
const res = await axios({ const res = await axios({
url: `/login/status?timestamp=${Date.now()}`, url: `/login/status?timestamp=${Date.now()}`,
method: 'post', method: 'post',
@ -59,14 +180,20 @@
cookie, cookie,
}, },
}) })
document.querySelector('#info').innerText = JSON.stringify(res.data, null, 2) document.querySelector('#info').textContent = JSON.stringify(res.data, null, 2)
} catch (error) {
console.error('获取登录状态失败:', error)
} }
}
function updateStatus(message, type) {
const statusDiv = document.getElementById('status')
statusDiv.textContent = message
statusDiv.className = 'status ' + type
}
login()
</script> </script>
<style>
.info {
white-space: pre;
}
</style>
</body> </body>
</html> </html>

View File

@ -5,44 +5,164 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>二维码登录</title> <title>二维码登录</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;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
padding: 48px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
max-width: 450px;
width: 100%;
text-align: center;
}
h1 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 32px;
}
.qr-wrapper {
display: inline-block;
padding: 16px;
background: #f9f9f9;
border-radius: 8px;
margin-bottom: 24px;
}
#qrImg {
width: 200px;
height: 200px;
display: block;
}
.info {
text-align: left;
padding: 16px;
background: #f9f9f9;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 12px;
color: #666;
white-space: pre-wrap;
word-break: break-all;
max-height: 200px;
overflow-y: auto;
}
.status {
margin-top: 16px;
padding: 12px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
}
.status.waiting {
background: #fef3c7;
color: #92400e;
}
.status.success {
background: #d1fae5;
color: #065f46;
}
.status.error {
background: #fee2e2;
color: #991b1b;
}
.hint {
margin-top: 16px;
font-size: 13px;
color: #999;
}
</style>
</head> </head>
<body> <body>
<img id="qrImg" /> <div class="container">
<h1>二维码登录</h1>
<p class="subtitle">使用网易云音乐App扫描二维码登录</p>
<div class="qr-wrapper">
<img id="qrImg" src="" alt="二维码加载中..." />
</div>
<div id="status" class="status waiting">等待扫描...</div>
<div id="info" class="info"></div> <div id="info" class="info"></div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js
"></script> <p class="hint">请打开网易云音乐App扫描上方二维码完成登录</p>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script> <script>
async function login() { async function login() {
let timer let timer
let timestamp = Date.now() const statusDiv = document.getElementById('status')
const cookie = localStorage.getItem('cookie') const cookie = localStorage.getItem('cookie')
updateStatus('加载二维码...', 'waiting')
getLoginStatus(cookie) getLoginStatus(cookie)
try {
const res = await axios({ const res = await axios({
url: `/login/qr/key?timestamp=${Date.now()}`, url: `/login/qr/key?timestamp=${Date.now()}`,
}) })
const key = res.data.data.unikey const key = res.data.data.unikey
const res2 = await axios({ const res2 = await axios({
url: `/login/qr/create?key=${key}&platform=web&qrimg=true&timestamp=${Date.now()}&ua=pc`, url: `/login/qr/create?key=${key}&platform=web&qrimg=true&timestamp=${Date.now()}&ua=pc`,
}) })
document.querySelector('#qrImg').src = res2.data.data.qrimg document.querySelector('#qrImg').src = res2.data.data.qrimg
updateStatus('请扫描二维码', 'waiting')
timer = setInterval(async () => { timer = setInterval(async () => {
const statusRes = await checkStatus(key) const statusRes = await checkStatus(key)
if (statusRes.code === 800) { if (statusRes.code === 800) {
alert('二维码已过期,请重新获取') updateStatus('二维码已过期,请刷新页面', 'error')
clearInterval(timer) clearInterval(timer)
} } else if (statusRes.code === 801) {
if (statusRes.code === 803) { updateStatus('二维码已扫描,请在手机上确认', 'waiting')
// 这一步会返回cookie } else if (statusRes.code === 802) {
updateStatus('登录成功,正在保存信息...', 'waiting')
} else if (statusRes.code === 803) {
clearInterval(timer) clearInterval(timer)
alert('授权登录成功') updateStatus('授权登录成功!', 'success')
await getLoginStatus(statusRes.cookie) await getLoginStatus(statusRes.cookie)
localStorage.setItem('cookie', statusRes.cookie) localStorage.setItem('cookie', statusRes.cookie)
} }
}, 3000) }, 3000)
} catch (error) {
console.error('登录失败:', error)
updateStatus('二维码加载失败,请刷新页面重试', 'error')
}
} }
login()
async function checkStatus(key) { async function checkStatus(key) {
const res = await axios({ const res = await axios({
@ -50,7 +170,9 @@
}) })
return res.data return res.data
} }
async function getLoginStatus(cookie = '') { async function getLoginStatus(cookie = '') {
try {
const res = await axios({ const res = await axios({
url: `/login/status?timestamp=${Date.now()}&ua=pc`, url: `/login/status?timestamp=${Date.now()}&ua=pc`,
method: 'post', method: 'post',
@ -58,14 +180,20 @@
cookie, cookie,
}, },
}) })
document.querySelector('#info').innerText = JSON.stringify(res.data, null, 2) document.querySelector('#info').textContent = JSON.stringify(res.data, null, 2)
} catch (error) {
console.error('获取登录状态失败:', error)
} }
}
function updateStatus(message, type) {
const statusDiv = document.getElementById('status')
statusDiv.textContent = message
statusDiv.className = 'status ' + type
}
login()
</script> </script>
<style>
.info {
white-space: pre;
}
</style>
</body> </body>
</html> </html>

View File

@ -5,79 +5,136 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>音乐解灰测试</title> <title>音乐解灰测试</title>
<style> <style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
font-family: Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px; min-height: 100vh;
margin: 20px auto; background: #f5f5f5;
padding: 0 20px;
}
.container {
background-color: #f5f5f5;
padding: 20px; padding: 20px;
border-radius: 8px;
} }
.container {
max-width: 800px;
margin: 0 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: 24px;
}
.form-group { .form-group {
margin-bottom: 15px; margin-bottom: 20px;
} }
.source-options {
display: flex; label {
flex-wrap: wrap; display: block;
gap: 10px; font-size: 14px;
margin-bottom: 15px; font-weight: 500;
color: #555;
margin-bottom: 8px;
} }
.source-option {
display: flex; input {
align-items: center; width: 100%;
gap: 5px; padding: 12px 14px;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
#result {
margin-top: 20px;
padding: 10px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 6px;
font-size: 15px;
outline: none;
}
input:focus {
border-color: #333;
}
button {
background: #333;
color: white;
padding: 14px 28px;
border: none;
border-radius: 6px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease;
}
button:hover {
background: #555;
}
button:disabled {
background: #999;
cursor: not-allowed;
}
#result {
margin-top: 24px;
padding: 16px;
background: #f9f9f9;
border-radius: 6px;
border: 1px solid #eee;
font-family: 'Courier New', monospace;
font-size: 13px;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all;
min-height: 100px;
}
.hint {
font-size: 12px;
color: #999;
margin-top: 4px;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>音乐解灰测试</h1> <h1>音乐解灰测试</h1>
<div class="form-group"> <div class="form-group">
<label for="songId">音乐 ID</label> <label for="songId">音乐 ID</label>
<input type="number" id="songId" placeholder="请输入音乐ID" required> <input type="number" id="songId" placeholder="请输入音乐ID" />
<div class="hint">例如: 1372188635</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="sources">音源列表:</label> <label for="sources">音源列表(可选)</label>
<input type="text" id="sources" placeholder="请输入音源(非必填)"> <input type="text" id="sources" placeholder="请输入音源" />
<div class="hint">例如: kuwo, kugou, migu</div>
</div> </div>
<button onclick="testSong()">开始测试</button>
<button id="testBtn" onclick="testSong()">开始测试</button>
<div id="result"></div> <div id="result"></div>
</div> </div>
<script> <script>
async function testSong() { async function testSong() {
const songId = document.getElementById('songId').value; const songId = document.getElementById('songId').value;
const sources = document.getElementById('sources').value;
const testBtn = document.getElementById('testBtn');
const resultDiv = document.getElementById('result');
if (!songId) { if (!songId) {
alert('请输入音乐ID'); alert('请输入音乐ID');
return; return;
} }
const sources = document.getElementById('sources').value; testBtn.disabled = true;
testBtn.textContent = '测试中...';
const resultDiv = document.getElementById('result');
resultDiv.textContent = '正在请求...'; resultDiv.textContent = '正在请求...';
try { try {
@ -86,6 +143,9 @@
resultDiv.textContent = JSON.stringify(data, null, 2); resultDiv.textContent = JSON.stringify(data, null, 2);
} catch (error) { } catch (error) {
resultDiv.textContent = `请求失败: ${error.message}`; resultDiv.textContent = `请求失败: ${error.message}`;
} finally {
testBtn.disabled = false;
testBtn.textContent = '开始测试';
} }
} }
</script> </script>

View File

@ -4,36 +4,230 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>播客上传声音</title> <title>播客上传声音</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: 900px;
margin: 0 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: 24px;
}
.login-link {
display: block;
margin-bottom: 24px;
color: #666;
font-size: 14px;
text-decoration: none;
}
.login-link:hover {
color: #333;
text-decoration: underline;
}
.content {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.voice-list {
flex: 1;
min-width: 300px;
}
.voice-item {
padding: 12px 16px;
border: 1px solid #eee;
border-radius: 6px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.voice-item:hover {
border-color: #333;
background: #f9f9f9;
}
.voice-item.active {
border-color: #333;
background: #f0f0f0;
}
.voice-header {
display: flex;
align-items: center;
gap: 12px;
}
.voice-cover {
width: 50px;
height: 50px;
border-radius: 4px;
object-fit: cover;
}
.voice-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
.voice-tracks {
margin-top: 8px;
padding-left: 62px;
}
.voice-track {
font-size: 13px;
color: #666;
padding: 4px 0;
}
.upload-section {
flex: 1;
min-width: 300px;
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
font-size: 14px;
font-weight: 500;
color: #555;
margin-bottom: 8px;
}
input[type="text"], input[type="file"] {
width: 100%;
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
outline: none;
}
input[type="text"]:focus {
border-color: #333;
}
.btn {
width: 100%;
padding: 12px;
background: #333;
color: white;
font-size: 15px;
font-weight: 500;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
}
.btn:hover {
background: #555;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #999;
font-size: 14px;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
</style>
</head> </head>
<body> <body>
<div> <div class="container">
<a href="/qrlogin-nocookie.html"> 如果没登录,请先登录 </a> <h1>播客上传声音</h1>
</div> <a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
<div id="app">
<ul> <div class="content">
<li <div class="voice-list">
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 16px;">选择播客列表</h3>
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="voicelist.length > 0">
<div
v-for="(item, index) in voicelist" v-for="(item, index) in voicelist"
:key="index"
@click="currentVoiceIndex = index" @click="currentVoiceIndex = index"
:class="{ active: currentVoiceIndex === index }" :class="{ active: currentVoiceIndex === index }"
class="voice-item"
>
<div class="voice-header">
<img :src="item.coverUrl" class="voice-cover" alt="cover" />
<span class="voice-name">{{ item.voiceListName }}</span>
</div>
<div class="voice-tracks" v-if="item.voiceListData">
<div
v-for="(item2, index2) in item.voiceListData"
:key="index2"
class="voice-track"
> >
<img :src="item.coverUrl" style="width: 50px; width: 50px" />
<ul>
<li v-for="(item2,index) in item.voiceListData">
{{ item2.voiceName }} {{ item2.voiceName }}
</li> </div>
</ul> </div>
{{item.voiceListName}} </div>
</li> </div>
</ul> <div v-else class="empty-state">暂无播客列表</div>
<input v-model="songName" placeholder="请输入声音名称" /> </div>
<input v-model="description" placeholder="请输入介绍" />
<input type="file" name="songFile" /> <div class="upload-section">
<button @click="submit">上传</button> <h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 16px;">上传声音</h3>
<div class="form-group">
<label for="songName">声音名称</label>
<input id="songName" v-model="songName" placeholder="请输入声音名称" />
</div>
<div class="form-group">
<label for="description">介绍</label>
<input id="description" v-model="description" placeholder="请输入介绍" />
</div>
<div class="form-group">
<label>选择文件</label>
<input type="file" name="songFile" accept="audio/*" />
</div>
<button class="btn" @click="submit">上传</button>
</div>
</div>
</div> </div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script> <script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script src="https://fastly.jsdelivr.net/npm/vue"></script> <script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
<script> <script>
Vue.createApp({ Vue.createApp({
data() { data() {
@ -43,6 +237,7 @@
voicelist: [], voicelist: [],
cookieToken: '', cookieToken: '',
currentVoiceIndex: 0, currentVoiceIndex: 0,
loading: false,
} }
}, },
created() { created() {
@ -50,12 +245,6 @@
}, },
computed: { computed: {
currentVoice() { currentVoice() {
// {
// voiceListId: '',
// coverImgId: '',
// categoryId: '',
// secondCategoryId: '',
// }
return this.voicelist[this.currentVoiceIndex] return this.voicelist[this.currentVoiceIndex]
}, },
}, },
@ -63,27 +252,49 @@
submit() { submit() {
console.info('submit') console.info('submit')
const file = document.querySelector('input[type=file]').files[0] const file = document.querySelector('input[type=file]').files[0]
if (!file) {
alert('请选择文件')
return
}
this.upload(file) this.upload(file)
}, },
async getData() { async getData() {
this.loading = true
try {
const res = await axios({ const res = await axios({
url: `/voicelist/search?cookie=${localStorage.getItem('cookie')}`, url: `/voicelist/search?cookie=${localStorage.getItem('cookie')}`,
}) })
console.info(res.data.data) console.info(res.data.data)
this.voicelist = res.data.data.list this.voicelist = res.data.data.list || []
this.voicelist.map(async (i) => { this.voicelist.forEach(async (i) => {
try {
const res2 = await axios({ const res2 = await axios({
url: `/voicelist/list?voiceListId=${i.voiceListId}&limit=5`, url: `/voicelist/list?voiceListId=${i.voiceListId}&limit=5`,
}) })
i.voiceListData = res2.data.data.list i.voiceListData = res2.data.data.list || []
console.info(res2) console.info(res2)
} catch (err) {
console.error('获取播客详情失败:', err)
}
}) })
} catch (err) {
console.error('获取播客列表失败:', err)
} finally {
this.loading = false
}
}, },
upload(file) { upload(file) {
if (!this.currentVoice) {
alert('请先选择播客列表')
return
}
var formData = new FormData() var formData = new FormData()
formData.append('songFile', file) formData.append('songFile', file)
axios({ axios({
method: 'post', method: 'post',
url: `/voice/upload?time=${Date.now()}&cookie=${localStorage.getItem( url: `/voice/upload?time=${Date.now()}&cookie=${localStorage.getItem(
@ -102,26 +313,14 @@
}) })
.then((res) => { .then((res) => {
alert(`${file.name} 上传成功`) alert(`${file.name} 上传成功`)
if (currentIndx >= fileLength) {
console.info('上传完毕')
}
}) })
.catch(async (err) => { .catch((err) => {
console.info(err) console.error('上传失败:', err)
alert('上传失败,请重试')
}) })
}, },
}, },
}).mount('#app') }).mount('body')
</script> </script>
<style>
ul li {
cursor: pointer;
}
ul li.active {
color: red;
}
</style>
</body> </body>
</html> </html>