Compare commits

..

No commits in common. "51a958936c3636993df76e1c0e0dd38a4c1ac6d0" and "d760b2960ab4d98fc949c379df2639d9a1d6789e" have entirely different histories.

17 changed files with 759 additions and 2819 deletions

View File

@ -7,16 +7,6 @@
- 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,148 +5,82 @@
<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: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: Arial, sans-serif;
min-height: 100vh; margin: 20px;
background: #f5f5f5; display: flex;
padding: 20px; flex-direction: column;
min-height: 100vh;
} }
.container { .container {
max-width: 1200px; display: flex;
margin: 0 auto; flex-direction: column;
background: white; flex-grow: 1;
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: column; flex-direction: row;
gap: 16px; align-items: center;
margin-bottom: 24px; gap: 10px;
margin-bottom: 10px;
} }
input, button {
.form-row { padding: 10px;
display: flex; box-sizing: border-box;
gap: 12px; flex: 1;
align-items: center;
} }
label {
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;
outline: none;
}
input:focus, select:focus {
border-color: #333;
}
button { button {
background: #333; background-color: #4CAF50;
color: white; color: white;
padding: 10px 24px; border: none;
border: none; cursor: pointer;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease;
} }
button:hover {
background: #555;
}
.data-result { .data-result {
display: flex; display: flex;
gap: 16px; flex-direction: row;
min-height: 400px; flex-grow: 1;
} }
.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: 8px; margin-bottom: 10px;
padding: 0;
} }
#data, #result {
textarea { height: 100%;
flex: 1; box-sizing: border-box;
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 {
textarea:focus { border-right: 1px solid #ccc;
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();">
<div class="form-row"> <label for="uri">uri</label>
<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> <select id="crypto" name="crypto">
<div class="form-row"> <option value="weapi">weapi</option>
<label for="crypto">加密方式</label> <option value="eapi">eapi</option>
<select id="crypto" name="crypto"> <option value="api">api</option>
<option value="weapi">weapi</option> <option value="linuxapi">linuxapi</option>
<option value="eapi">eapi</option> <option value="" selected>(默认)</option>
<option value="api">api</option> </select>
<option value="linuxapi">linuxapi</option> <button type="submit">发送</button>
<option value="" selected>(默认)</option>
</select>
</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">响应结果</label> <label for="result">result</label>
<textarea id="result" name="result" readonly></textarea> <textarea id="result" name="result"></textarea>
</div> </div>
<div> <div>
<label for="data">请求数据</label> <label for="data">data</label>
<textarea id="data" name="data"> <textarea id="data" name="data">
{ {
"cp": false, "cp": false,

View File

@ -1,5 +1,6 @@
'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) {
@ -1611,9 +1612,9 @@ function instantiateRuntime(){
function GenerateFP(floatArray) { function GenerateFP(floatArray) {
let PCMBuffer = Float32Array.from(floatArray) let PCMBuffer = Float32Array.from(floatArray)
console.info('[afp] input samples n=', PCMBuffer.length) logger.info('[afp] input samples n=', PCMBuffer.length)
return instantiateRuntime().then((fpRuntime) => { return instantiateRuntime().then((fpRuntime) => {
console.info('[afp] begin fingerprinting') logger.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,142 +1,27 @@
<!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>
* { * {
margin: 0; font-family: sans-serif;
padding: 0;
box-sizing: border-box;
} }
body { pre {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: monospace;
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 {
color: #333; font-family: sans-serif;
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 {
@ -144,80 +29,39 @@
} }
pre { pre {
font-family: 'Courier New', monospace; overflow: scroll;
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>
<div class="container"> <h1>听歌识曲 Demo (Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a>)</h1>
<h1>听歌识曲 Demo</h1> <hr>
<p class="subtitle">Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a></p> <p><b>DISCLAIMER: </b></p>
<p>This site uses the offical NetEase audio matcher APIs (reverse engineered from <a
href="https://fn.music.163.com/g/chrome-extension-home-page-beta/">https://fn.music.163.com/g/chrome-extension-home-page-beta/</a>)
</p>
<p>And DOES NOT condone copyright infringment nor intellectual property theft.</p>
<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>
<hr> <audio id="audio" controls autoplay></audio>
<canvas id="canvas"></canvas>
<div class="warning"> <button id="invoke">Clip</button>
<strong>免责声明:</strong>本站点使用网易云音乐官方音频识别API逆向自 <a href="https://fn.music.163.com/g/chrome-extension-home-page-beta/" target="_blank">Chrome 扩展页面</a>),不鼓励版权侵犯或知识产权盗窃。 <input type="file" name="picker" accept="*" id="file">
</div> <hr>
<label for="use-mic">Mix in Microphone input</label>
<div class="section"> <input type="checkbox" name="use-mic" id="usemic">
<h3>使用说明</h3> <hr>
<p>在使用本站点之前,您可能需要先访问以下链接:</p> <pre id="logs"></pre>
<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>
<audio id="audio" controls autoplay></audio>
<canvas id="canvas"></canvas>
<div class="control-group">
<button id="invoke">识别</button>
<input type="file" name="picker" accept="*" id="file">
</div>
<div class="control-group">
<div class="checkbox-group">
<input type="checkbox" name="use-mic" id="usemic">
<label for="usemic">混合麦克风输入</label>
</div>
</div>
<hr>
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 12px;">日志</h3>
<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">
@ -232,17 +76,13 @@
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.write = line => logs.innerHTML += line + '\n'
// 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] 生成指纹 ${FP}`) logs.write(`[index] Generated FP ${FP}`)
logs.write('[index] 正在查询,请稍候...') logs.write('[index] Now querying, please wait...')
fetch( fetch(
'/audio/match?' + '/audio/match?' +
new URLSearchParams({ new URLSearchParams({
@ -251,9 +91,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] 查询失败,无结果') return logs.write('[index] Query failed with no results.')
} }
logs.write(`[index] 查询完成。结果数量=${resp.data.result.length}`) logs.write(`[index] Query complete. Results=${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>`
@ -264,19 +104,20 @@
} }
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) audioNode.connect(recorderNode) // recorderNode doesn't output anything
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.innerHTML = 'Clip'
clip.disabled = false clip.disabled = false
canvas.classList.remove('canvas-active') canvas.classList.remove('canvas-active')
break break
@ -289,6 +130,7 @@
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,
@ -300,7 +142,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] 麦克风已连接') logs.write('[rec.js] Microphone attached.')
}); });
}); });
return true return true
@ -319,20 +161,10 @@
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 => {
const safeName = escapeHtml(file.files[0].name) logs.write(`[index] File ${file.files[0].name} loaded.`)
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
}) })
@ -356,13 +188,12 @@
UpdateCanvas() UpdateCanvas()
let requestCtx = setInterval(() => { let requestCtx = setInterval(() => {
try { try {
if (InitAudioCtx()) { if (InitAudioCtx()) { // Put this here so we don't have to deal with the 'user did not interact' thing
clearInterval(requestCtx) clearInterval(requestCtx)
logs.write('[rec.js] 音频上下文已启动') logs.write('[rec.js] Audio Context started.')
} }
} catch { } catch {
// Fail silently // Fail silently
} }
}, 100) }, 100)
</script> </script>
</html>

View File

@ -5,320 +5,67 @@
<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 class="container"> <div>
<h1>更新头像</h1> <a href="/qrlogin-nocookie.html">
<p class="subtitle">选择一张图片作为您的新头像</p> 如果没登录,请先登录
<div class="avatar-wrapper" id="avatarWrapper">
<img id="avatar" class="avatar" src="" alt="头像" />
<div class="loading-overlay">
<div class="spinner"></div>
</div>
</div>
<label class="upload-btn">
选择图片
<input id="file" type="file" accept="image/*" />
</label>
<a href="/qrlogin-nocookie.html" class="login-link">
还没有登录?点击登录
</a> </a>
<p class="hint">支持 JPG、PNG 格式,建议尺寸 200x200</p>
</div> </div>
<input id="file" type="file" />
<div id="toast" class="toast"></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
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script> "></script>
<script> <script>
main() main()
async function main() { async function main() {
const fileInput = document.querySelector('input[type="file"]'); document.querySelector('input[type="file"]').addEventListener(
const avatarWrapper = document.getElementById('avatarWrapper'); 'change',
function (e) {
fileInput.addEventListener('change', function(e) { var file = this.files[0]
const file = this.files[0]; upload(file)
if (file) { },
upload(file); false,
} )
}, false); const res = await axios({
url: `/user/detail?uid=32953014&timestamp=${Date.now()}`,
try { withCredentials: true, //跨域的话必须设置
showToast('正在加载头像...', 'info'); })
avatarWrapper.classList.add('loading'); document.querySelector('#avatar').src = res.data.profile.avatarUrl
const res = await axios({
url: `/user/detail?uid=32953014&timestamp=${Date.now()}`,
withCredentials: true
});
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) {
const avatarWrapper = document.getElementById('avatarWrapper'); var formData = new FormData()
formData.append('imgFile', file)
if (!file.type.startsWith('image/')) { const imgSize = await getImgSize(file)
showToast('请选择图片文件', 'error'); const res = await axios({
return; method: 'post',
} url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width
}&imgX=0&imgY=0&timestamp=${Date.now()}`,
try { headers: {
showToast('正在上传头像...', 'info'); 'Content-Type': 'multipart/form-data',
avatarWrapper.classList.add('loading'); },
data: formData,
var formData = new FormData(); })
formData.append('imgFile', file); document.querySelector('#avatar').src = res.data.data.url
const imgSize = await getImgSize(file);
const res = await axios({
method: 'post',
url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0&timestamp=${Date.now()}`,
headers: {
'Content-Type': 'multipart/form-data',
},
data: formData,
});
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,135 +4,26 @@
<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 class="container"> <div>
<h1>云盘上传</h1> <a href="/qrlogin-nocookie.html"> 如果没登录,请先登录 </a>
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a> </div>
<input id="file" type="file" multiple />
<div class="upload-section"> <div id="app">
<label class="upload-btn"> <ul>
选择文件(支持多选) <li v-for="(item,index) in songs" :key="index">{{item.songName}}</li>
<input id="file" type="file" multiple accept="audio/*" /> </ul>
</label>
</div>
<div id="app">
<div v-if="loading" class="loading">加载中...</div>
<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>
<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@3"></script> <script src="https://fastly.jsdelivr.net/npm/vue"></script>
<script> <script>
const app = Vue.createApp({ const app = Vue.createApp({
data() { data() {
return { return {
songs: [], songs: [],
loading: false,
} }
}, },
created() { created() {
@ -140,23 +31,19 @@
}, },
methods: { methods: {
getData() { getData() {
this.loading = true console.info('getdata')
const _this = this
axios({ axios({
url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`, url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem(
'cookie',
)}`,
}).then((res) => {
console.info(res.data)
_this.songs = res.data.data
}) })
.then((res) => {
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
@ -164,46 +51,51 @@
document document
.querySelector('input[type="file"]') .querySelector('input[type="file"]')
.addEventListener('change', function (e) { .addEventListener('change', function (e) {
const files = this.files console.info(this.files)
if (files.length === 0) return let currentIndx = 0
fileLength = this.files.length
fileLength = files.length for (const item of this.files) {
for (let i = 0; i < files.length; i++) { currentIndx += 1
upload(files[i], i + 1) upload(item, currentIndx)
} }
}) })
} }
main() main()
function upload(file, currentIndex) { function upload(file, currentIndx) {
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('cookie')}`, url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem(
'cookie',
)}`,
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
data: formData, data: formData,
}) })
.then((res) => { .then((res) => {
console.log(`${file.name} 上传成功`) console.info(`${file.name} 上传成功`)
if (currentIndex >= fileLength) { if (currentIndx >= fileLength) {
console.log('所有文件上传完毕') console.info('上传完毕')
} }
app.getData() app.getData()
}) })
.catch((err) => { .catch(async (err) => {
console.error(`${file.name} 上传失败:`, err) console.info(err)
fileUpdateTime[file.name] = (fileUpdateTime[file.name] || 0) + 1 console.info(fileUpdateTime)
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=G-BPRGR23JEG"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=UA-139996012-1"></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', 'G-BPRGR23JEG'); gtag('config', 'UA-139996012-1');
</script> </script>
</html> </html>

View File

@ -1,181 +1,50 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="en">
<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>
<style> <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
* {
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 {
white-space: pre-wrap;
word-break: break-all;
background: #f9f9f9;
padding: 16px;
border-radius: 6px;
border: 1px solid #eee;
min-height: 200px;
max-height: 400px;
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>
</head> </head>
<style>
.decode-result {
white-space: pre-wrap;
word-break: break-all;
background-color: #f0f0f0;
padding: 10px;
border-radius: 5px;
margin-top: 10px;
height: 300px;
overflow: auto;
}
</style>
<body> <body>
<div id="app" class="container"> <div id="app" class="p-5 flex flex-col">
<h1>eapi 参数和返回内容解析</h1> <h1 class="text-2xl font-bold mb-5">eapi 参数和返回内容解析</h1>
<textarea class="border border-gray-300 p-3 mb-5" v-model="hexString" rows="10"></textarea>
<div class="form-group"> <button @click="decrypt" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
<label for="hexString">十六进制字符串</label> 解密
<textarea id="hexString" v-model="hexString" rows="10"></textarea> </button>
<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>
<button @click="decrypt">解密</button> <div>
<p>使用例子:</p>
<div class="result-section"> <img src="/static/eapi_params.png" />
<label>解密结果:</label> <img src="/static/eapi_response.png" />
<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>
@ -195,13 +64,6 @@
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({
@ -215,7 +77,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: #333; --fg: #111827; /* gray-900 */
--muted: #666; --muted: #6b7280; /* gray-500 */
--border: #ddd; --border: #e5e7eb; /* gray-200 */
--bg: #f5f5f5; --bg: #ffffff;
--panel: #ffffff; --panel: #f9fafb; /* gray-50 */
--accent: #333; --accent: #2563eb; /* blue-600 */
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
html, body { height: 100%; } html, body { height: 100%; }
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; } 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; }
.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: 600; margin: 0; } header.site-header h1 { font-size: 28px; font-weight: 700; margin: 0; }
.badge { display: inline-block; margin-left: 8px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 12px; font-size: 12px; color: var(--muted); } .badge { display: inline-block; margin-left: 8px; padding: 2px 8px; border: 1px solid var(--border); border-radius: 14px; font-size: 12px; color: var(--muted); }
.sub { margin-top: 8px; color: var(--muted); font-size: 14px; } .sub { margin-top: 6px; color: var(--muted); }
.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 { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 16px; }
.block h2 { margin: 0 0 12px; font-size: 18px; font-weight: 600; } .block h2 { margin: 0 0 10px; font-size: 18px; }
.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: 8px 0; } ul.links li { margin: 6px 0; }
ul.links a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted var(--border); transition: all 0.2s ease; } ul.links a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted var(--border); }
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: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow: auto; } pre { margin: 0; background: #fff; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow: auto; }
code { font-family: 'Courier New', monospace; font-size: 13px; } code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-size: 13px; }
footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); text-align: center; } footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); }
footer.site-footer a { color: var(--fg); text-decoration: none; transition: color 0.2s ease; } footer.site-footer a { color: var(--fg); text-decoration: none; }
footer.site-footer a:hover { color: var(--accent); } footer.site-footer a:hover { color: var(--accent); }
</style> </style>
</head> </head>

View File

@ -1,321 +1,92 @@
<!-- 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>
<style> <link
* { rel="stylesheet"
margin: 0; href="https://unpkg.com/mdui@1.0.2/dist/css/mdui.min.css"
padding: 0; />
box-sizing: border-box; <script src="https://unpkg.com/mdui@1.0.2/dist/js/mdui.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: 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="container"> <body class="mdui-container">
<a href="/qrlogin.html" class="login-link">还没登录?点击登录</a> <div>
<a href="/qrlogin.html"> 如果没登录,请先登录 </a>
</div>
<h1>一起听 - 主机模式</h1> <h1>一起听 - 主机模式</h1>
<div>消息: {{message}}</div>
<div class="message">消息: {{message}}</div> <audio id="player" autoplay controls></audio>
<br />
<audio id="player" class="audio-player" autoplay controls></audio> <br />
<button v-if="!account.login" @click="login">获取登录状态</button>
<div v-if="!account.login"> <div>您的当前登录账号为: {{account.nickname}}</div>
<button class="btn" @click="login">获取登录状态</button> <br />
</div> <div v-if="account.login">
<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 class="input-group"> <div><span>房间ID: </span><input v-model="roomInfo.roomId" /></div>
<label>房间ID:</label> <div>
<input v-model="roomInfo.roomId" type="text" /> <span>邀请者 ID: </span><input v-model="roomInfo.inviterId" />
</div> </div>
<div class="input-group"> <button @click="joinRoom">点击加入</button>
<label>邀请者ID:</label>
<input v-model="roomInfo.inviterId" type="text" />
</div>
<button class="btn" @click="joinRoom">加入房间</button>
</details> </details>
<div v-if="roomInfo.roomId" style="margin-top: 16px;"> <div v-if="roomInfo.roomId">
<h4>分享链接</h4> <div>
<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>
<button class="btn" @click="refreshRoom">刷新房间状态</button> <br />
<button class="btn" @click="closeRoom">关闭房间</button> <button @click="refreshRoom">刷新房间状态</button>
<div>在线用户:</div>
<h4 style="margin-top: 16px;">在线用户</h4> <ul class="mdui-list">
<ul class="user-list"> <li
<li v-for="user in roomInfo.roomUsers" :key="user.userId" class="user-item"> v-for="user in roomInfo.roomUsers"
<img :src="user.avatarUrl" class="user-avatar" alt="avatar" /> class="mdui-list-item mdui-ripple"
<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>
<div class="section"> <button @click="pauseTrack">暂停</button>
<h3>播放控制</h3> <button @click="seekTrack">同步进度</button>
<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>
<div class="section"> <br />
<div class="input-group"> <div>
<label>歌单ID:</label> <span>歌单ID: </span><input v-model="playlistInfo.playlistId" />
<input v-model="playlistInfo.playlistId" type="text" />
</div>
<button class="btn" @click="loadPlaylist">加载歌单</button>
<div style="margin-top: 12px; font-size: 14px; color: #555;">
歌单名称: {{playlistInfo.playlistName}}
</div>
<h4 style="margin-top: 16px;">歌单内容</h4>
<ul class="track-list">
<li
@click="gotoTrack(track.id)"
v-for="track in playlistInfo.playlistTracks"
:key="track.id"
class="track-item"
>
<img :src="track.al.picUrl" class="track-cover" alt="cover" />
<span class="track-name">{{track.name}}</span>
</li>
</ul>
</div> </div>
<button @click="loadPlaylist">加载歌单到播放列表</button>
<span>{{playlistInfo.playlistName}}</span>
<br />
<br />
<div>歌单内容:</div>
<ul class="mdui-list">
<li
@click="gotoTrack(track.id)"
v-for="track in playlistInfo.playlistTracks"
class="mdui-list-item mdui-ripple"
>
<div class="mdui-list-item-avatar">
<img :src="track.al.picUrl" />
</div>
<div class="mdui-list-item-content">{{track.name}}</div>
</li>
</ul>
</details> </details>
</body> </body>
<script> <script>
PetiteVue.createApp({ PetiteVue.createApp({
message: '请点击获取登录状态', message: '请点击获取登录状态',
@ -355,7 +126,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 () {
@ -441,7 +212,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,143 +5,21 @@
<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(phone, password) { async function login() {
const res = await axios({ const res = await axios({
url: `/login/cellphone`, url: `/login/cellphone`,
method: 'post', method: 'post',
@ -152,67 +30,17 @@
}) })
return res.data.cookie return res.data.cookie
} }
async function main() {
async function handleLogin() { const cookieToken = await login()
const phoneInput = document.getElementById('phone') const res = await axios({
const passwordInput = document.getElementById('password') url: `/login/status`,
const loginBtn = document.getElementById('loginBtn') method: 'post',
const resultDiv = document.getElementById('result') data: {
cookie: cookieToken,
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({
url: `/login/status`,
method: 'post',
data: {
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,297 +5,56 @@
<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 class="container"> <div>
<h1>歌单封面上传</h1> <a href="/qrlogin-nocookie.html">
<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 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> </div>
<input id="file" type="file" name="filename" />
<img id="playlist_cover" style="height: 200px; width: 200px; border-radius: 50%" />
<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 loadingOverlay = document.getElementById('loading') const playlist_id = ''
const playlistIdInput = document.getElementById('playlistId') if (!playlist_id) {
const resultDiv = document.getElementById('result') const msg = '请设置你的歌单id'
alert(msg)
function showLoading() { throw new Error(msg)
loadingOverlay.classList.add('active')
} }
function hideLoading() { main()
loadingOverlay.classList.remove('active') async function main() {
} document.querySelector('input[type="file"]').addEventListener(
'change',
function showResult(message, type) { function (e) {
resultDiv.textContent = message var file = this.files[0]
resultDiv.className = 'result ' + type upload(file)
resultDiv.style.display = 'block' },
} false,
)
function hideResult() { const res = await axios({
resultDiv.style.display = 'none' url: `/playlist/detail?id=${playlist_id}&timestamp=${Date.now()}`,
}
async function loadPlaylistCover() {
const playlistId = playlistIdInput.value.trim()
if (!playlistId) {
return
}
showLoading()
hideResult()
try {
const res = await axios({
url: `/playlist/detail?id=${playlistId}&timestamp=${Date.now()}`,
})
document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl
hideResult()
} catch (error) {
console.error('加载封面失败:', error)
showResult('加载封面失败请检查歌单ID', 'error')
} finally {
hideLoading()
}
}
// 监听歌单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()
}
}) })
document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl
}
async function upload(file, playlistId) { async function upload(file) {
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=${playlistId}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0&timestamp=${Date.now()}`, url: `/playlist/cover/update?id=${playlist_id}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width
}&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()
@ -309,16 +68,10 @@
height: this.height, height: this.height,
}) })
} }
image.onerror = function() {
reject(new Error('图片加载失败'))
}
}
reader.onerror = function() {
reject(new Error('文件读取失败'))
} }
}) })
} }
</script> </script>
</body> </body>
</html> </html>

View File

@ -2,416 +2,261 @@
<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>
<style> <!-- 引入Bootstrap CSS -->
* { <link href="https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
margin: 0; <!-- 引入Bootstrap JS -->
padding: 0; <script src="https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
box-sizing: border-box; <!-- 引入axios用于发送异步请求 -->
} <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"> <div class="container mt-5">
<h1>歌单导入工具</h1> <h1 class="mb-4">歌单导入工具</h1>
<p class="subtitle">请选择一种导入方式并填写相关信息</p> <p>请选择一种导入方式并填写相关信息:</p>
<ul class="tabs" id="importTabs" role="tablist"> <!-- 表单开始 -->
<li role="presentation"> <form id="importForm" novalidate>
<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> <!-- 选项卡导航 -->
</li> <ul class="nav nav-tabs mb-3" id="importTabs" role="tablist">
<li role="presentation"> <li class="nav-item" role="presentation">
<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> <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 role="presentation"> <li class="nav-item" role="presentation">
<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> <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>
</li> </li>
</ul> <li class="nav-item" 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>
</li>
</ul>
<div class="tab-content active" id="importTabContent"> <!-- 选项卡面板 -->
<!-- 元数据导入 --> <div class="tab-content" id="importTabContent">
<div class="tab-content active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab"> <!-- 元数据导入 -->
<table> <div class="tab-pane fade show active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab">
<thead> <table class="table table-bordered mb-3">
<tr> <thead>
<th style="width: 33%">歌曲名称</th> <tr>
<th style="width: 33%">艺术家</th> <th scope="col">歌曲名称</th>
<th style="width: 33%">专辑</th> <th scope="col">艺术家</th>
</tr> <th scope="col">专辑</th>
</thead> </tr>
<tbody id="metadataTableBody"> </thead>
<tr> <tbody id="metadataTableBody">
<td><input type="text" name="name[]" placeholder="歌曲名称"></td> <!-- 默认添加一行 -->
<td><input type="text" name="artist[]" placeholder="艺术家"></td> <tr>
<td><input type="text" name="album[]" placeholder="专辑"></td> <td><input type="text" class="form-control" name="name[]" placeholder="歌曲名称"></td>
</tr> <td><input type="text" class="form-control" name="artist[]" placeholder="艺术家"></td>
</tbody> <td><input type="text" class="form-control" name="album[]" placeholder="专辑"></td>
</table> </tr>
<button type="button" class="btn btn-secondary" id="addMetadataRow">增加歌曲</button> </tbody>
</div> </table>
<!-- 文字导入 --> <button type="button" class="btn btn-secondary mb-3" id="addMetadataRow">增加歌曲</button>
<div class="tab-content" id="text" role="tabpanel" aria-labelledby="text-tab">
<div class="form-group">
<label for="textInput">文字内容</label>
<textarea id="textInput" name="text" rows="5" placeholder="请输入歌曲信息,每行一首歌曲"></textarea>
</div> </div>
<div class="form-group"> <!-- 文字导入 -->
<label for="playlistNameInput">歌单名称</label> <div class="tab-pane fade" id="text" role="tabpanel" aria-labelledby="text-tab">
<input type="text" id="playlistNameInput" name="playlistName" placeholder="请输入歌单名"> <div class="mb-3">
<label for="textInput" class="form-label">文字:</label>
<textarea class="form-control" id="textInput" name="text" rows="5"></textarea>
</div>
<div class="mb-3">
<label for="playlistNameInput" class="form-label">歌单名:</label>
<input type="text" class="form-control" 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>
<button type="button" class="btn btn-secondary mb-3" id="addLinkButton">增加链接</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> </div>
<button type="button" class="btn btn-secondary" id="addLinkButton" style="margin-top: 8px;">增加链接</button>
</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 class="checkbox-group"> <!-- 是否导入我喜欢的音乐 -->
<input type="checkbox" value="" id="importStarCheckbox"> <div class="form-check">
<label for="importStarCheckbox"> <input class="form-check-input" type="checkbox" value="" id="importStarCheckbox">
导入"我喜欢的音乐" <label class="form-check-label" for="importStarCheckbox">
</label> 导入“我喜欢的音乐”
</div> </label>
</div>
<button type="submit" class="btn" id="submitBtn">导入歌曲</button> <!-- 提交按钮 -->
</div> <button type="submit" class="btn btn-primary mt-3">导入歌曲</button>
</form>
<!-- 表单结束 -->
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script>
<script> // 动态增加链接输入框
// 选项卡切换 document.getElementById('addLinkButton').addEventListener('click', function() {
const tabBtns = document.querySelectorAll('.tab-btn'); var container = document.getElementById('linkInputsContainer');
const tabContents = document.querySelectorAll('.tab-content[id]'); var newIndex = container.childElementCount - 1; // 减去非输入框元素的数量
var newInput = document.createElement('input');
newInput.type = 'text';
newInput.className = 'form-control';
newInput.id = `linkInput${newIndex}`;
newInput.name = `linkInput${newIndex}`;
newInput.placeholder = '请输入链接';
tabBtns.forEach(btn => { var removeButton = document.createElement('button');
btn.addEventListener('click', () => { removeButton.type = 'button';
tabBtns.forEach(b => b.classList.remove('active')); removeButton.className = 'btn btn-secondary removeLinkButton';
tabContents.forEach(c => c.classList.remove('active')); removeButton.textContent = '×';
removeButton.dataset.index = newIndex.toString();
btn.classList.add('active'); removeButton.addEventListener('click', function() {
const targetId = btn.getAttribute('data-bs-target'); var group = this.closest('.input-group');
document.getElementById(targetId).classList.add('active'); container.removeChild(group);
});
});
// 动态增加链接输入框
document.getElementById('addLinkButton').addEventListener('click', function() {
var container = document.getElementById('linkInputsContainer');
var newIndex = container.children.length;
var newInput = document.createElement('input');
newInput.type = 'text';
newInput.className = '';
newInput.id = `linkInput${newIndex}`;
newInput.name = `linkInput${newIndex}`;
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');
removeButton.type = 'button';
removeButton.className = 'btn btn-secondary';
removeButton.textContent = '×';
removeButton.dataset.index = newIndex.toString();
removeButton.addEventListener('click', function() {
var group = this.closest('.input-group');
container.removeChild(group);
});
var inputGroup = document.createElement('div');
inputGroup.className = 'input-group';
inputGroup.appendChild(newInput);
inputGroup.appendChild(removeButton);
container.appendChild(inputGroup);
});
// 动态增加元数据行
document.getElementById('addMetadataRow').addEventListener('click', function() {
var container = document.getElementById('metadataTableBody');
var newRow = document.createElement('tr');
newRow.innerHTML = `
<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><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><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);
});
document.getElementById('submitBtn').addEventListener('click', async function() {
// 获取表单值
let text = document.getElementById('textInput').value;
let links = [];
let local = [];
let playlistName = '';
// 获取所有链接输入框的值
let linkInputs = document.querySelectorAll('#linkInputsContainer .input-group input[type="text"]');
linkInputs.forEach(function(input) {
if (input.value.trim() !== '') {
links.push(input.value);
}
});
// 获取元数据
let metadataRows = document.querySelectorAll('#metadataTableBody tr');
metadataRows.forEach(function(row) {
let name = row.querySelector('input[name="name[]"]').value;
let artist = row.querySelector('input[name="artist[]"]').value;
let album = row.querySelector('input[name="album[]"]').value;
if (name && artist && album) {
local.push({ name, artist, album });
}
});
// 检查是否有且只有一个输入字段被填写
let filledCount = (text ? 1 : 0) + (links.length > 0 ? 1 : 0) + (local.length > 0 ? 1 : 0);
if (filledCount !== 1) {
alert("请确保仅填写了一个输入字段!");
return;
}
// 获取歌单名
if (document.getElementById('importStarCheckbox').checked) {
playlistName = '我喜欢的音乐';
} else {
playlistName = document.getElementById('playlistNameInput').value ||
document.getElementById('playlistNameLinkInput').value ||
'导入音乐 ' + new Date().toLocaleString();
}
// 创建请求参数
let data = {};
if (text) {
data.text = text;
data.playlistName = playlistName;
} else if (links.length > 0) {
data.link = JSON.stringify(links);
data.playlistName = playlistName;
} else if (local.length > 0) {
data.local = JSON.stringify(local);
}
// 添加额外参数
if (document.getElementById('importStarCheckbox').checked) {
data.importStarPlaylist = true;
}
try {
const res = await axios({
url: `/playlist/import/name/task/create?timestamp=${Date.now()}`,
method: 'post',
data: data,
}); });
let taskId = res.data?.data?.taskId var inputGroup = document.createElement('div');
if (taskId) { inputGroup.className = 'input-group mb-3';
alert(`任务创建成功正在导入请稍等任务id${taskId}`) inputGroup.appendChild(newInput);
} inputGroup.appendChild(removeButton);
} catch (error) {
console.error('Error:', error);
alert('导入失败,请检查您的输入或稍后再试。');
}
});
// 监听复选框状态变化 container.appendChild(inputGroup);
document.getElementById('importStarCheckbox').addEventListener('change', function() {
let isChecked = this.checked;
let playlistNameInputs = document.querySelectorAll('[name="playlistName"]');
playlistNameInputs.forEach(function(input) {
input.disabled = isChecked;
}); });
});
// 初始化时设置歌单名输入框的状态 // 动态增加元数据行
document.getElementById('importStarCheckbox').dispatchEvent(new Event('change')); document.getElementById('addMetadataRow').addEventListener('click', function() {
</script> var container = document.getElementById('metadataTableBody');
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 = `
<td>${nameInput.outerHTML}</td>
<td>${artistInput.outerHTML}</td>
<td>${albumInput.outerHTML}</td>
`;
container.appendChild(newRow);
});
document.getElementById('importForm').addEventListener('submit', async function(event) {
// 阻止默认行为
event.preventDefault();
// 获取表单值
let text = document.getElementById('textInput').value;
let links = [];
let local = [];
let playlistName = '';
// 获取所有链接输入框的值
let linkInputs = document.querySelectorAll('#linkInputsContainer .input-group .form-control');
linkInputs.forEach(function(input) {
if (input.value.trim() !== '') {
links.push(input.value);
}
});
// 获取元数据
let metadataRows = document.querySelectorAll('#metadataTableBody tr');
metadataRows.forEach(function(row) {
let name = row.querySelector('input[name="name[]"]').value;
let artist = row.querySelector('input[name="artist[]"]').value;
let album = row.querySelector('input[name="album[]"]').value;
if (name && artist && album) {
local.push({ name, artist, album });
}
});
// 检查是否有且只有一个输入字段被填写
let filledCount = (text ? 1 : 0) + (links.length > 0 ? 1 : 0) + (local.length > 0 ? 1 : 0);
if (filledCount !== 1) {
alert("请确保仅填写了一个输入字段!");
return;
}
// 获取歌单名
if (document.getElementById('importStarCheckbox').checked) {
playlistName = '我喜欢的音乐';
} else {
playlistName = document.getElementById('playlistNameInput').value ||
document.getElementById('playlistNameLinkInput').value ||
'导入音乐 ' + new Date().toLocaleString();
}
// 创建请求参数
let data = {};
if (text) {
data.text = text;
data.playlistName = playlistName;
} else if (links.length > 0) {
data.link = JSON.stringify(links);
data.playlistName = playlistName;
} else if (local.length > 0) {
data.local = JSON.stringify(local);
}
// 添加额外参数
if (document.getElementById('importStarCheckbox').checked) {
data.importStarPlaylist = true;
}
try {
const res = await axios({
url: `/playlist/import/name/task/create?timestamp=${Date.now()}`,
method: 'post',
data: data,
});
let taskId = res.data?.data?.taskId
if (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) {
console.error('Error:', error);
alert('导入失败,请检查您的输入或稍后再试。');
}
});
// 监听复选框状态变化
document.getElementById('importStarCheckbox').addEventListener('change', function() {
let isChecked = this.checked;
let playlistNameInputs = document.querySelectorAll('[name="playlistName"]');
playlistNameInputs.forEach(function(input) {
input.disabled = isChecked;
});
});
// 初始化时设置歌单名输入框的状态
document.getElementById('importStarCheckbox').dispatchEvent(new Event('change'));
</script>
</div>
</body> </body>
</html> </html>

View File

@ -5,164 +5,45 @@
<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>
<div class="container"> <img id="qrImg" />
<h1>二维码登录</h1> <div id="info" class="info"></div>
<p class="subtitle">使用网易云音乐App扫描二维码登录</p> <script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js
"></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> <script>
async function login() { async function login() {
let timer let timer
const statusDiv = document.getElementById('status') let timestamp = Date.now()
const cookie = localStorage.getItem('cookie') const cookie = localStorage.getItem('cookie')
updateStatus('加载二维码...', 'waiting')
getLoginStatus(cookie) getLoginStatus(cookie)
const res = await axios({
url: `/login/qr/key?timestamp=${Date.now()}`,
})
const key = res.data.data.unikey
const res2 = await axios({
url: `/login/qr/create?key=${key}&platform=web&qrimg=true&timestamp=${Date.now()}`,
})
document.querySelector('#qrImg').src = res2.data.data.qrimg
try { timer = setInterval(async () => {
const res = await axios({ const statusRes = await checkStatus(key)
url: `/login/qr/key?timestamp=${Date.now()}`, if (statusRes.code === 800) {
}) alert('二维码已过期,请重新获取')
const key = res.data.data.unikey clearInterval(timer)
}
const res2 = await axios({ if (statusRes.code === 803) {
url: `/login/qr/create?key=${key}&platform=web&qrimg=true&timestamp=${Date.now()}`, // 这一步会返回cookie
}) clearInterval(timer)
document.querySelector('#qrImg').src = res2.data.data.qrimg alert('授权登录成功')
updateStatus('请扫描二维码', 'waiting') await getLoginStatus(statusRes.cookie)
localStorage.setItem('cookie', statusRes.cookie)
timer = setInterval(async () => { }
const statusRes = await checkStatus(key) }, 3000)
if (statusRes.code === 800) {
updateStatus('二维码已过期,请刷新页面', 'error')
clearInterval(timer)
} else if (statusRes.code === 801) {
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
} else if (statusRes.code === 802) {
updateStatus('登录成功,正在保存信息...', 'waiting')
} else if (statusRes.code === 803) {
clearInterval(timer)
updateStatus('授权登录成功!', 'success')
await getLoginStatus(statusRes.cookie)
localStorage.setItem('cookie', statusRes.cookie)
}
}, 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({
@ -170,30 +51,22 @@
}) })
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', data: {
data: { 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,164 +5,44 @@
<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>
<div class="container"> <img id="qrImg" />
<h1>二维码登录</h1> <div id="info" class="info"></div>
<p class="subtitle">使用网易云音乐App扫描二维码登录</p> <script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js
"></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> <script>
async function login() { async function login() {
let timer let timer
const statusDiv = document.getElementById('status') let timestamp = Date.now()
const cookie = localStorage.getItem('cookie') const cookie = localStorage.getItem('cookie')
updateStatus('加载二维码...', 'waiting')
getLoginStatus(cookie) getLoginStatus(cookie)
const res = await axios({
url: `/login/qr/key?timestamp=${Date.now()}`,
})
const key = res.data.data.unikey
const res2 = await axios({
url: `/login/qr/create?key=${key}&platform=web&qrimg=true&timestamp=${Date.now()}&ua=pc`,
})
document.querySelector('#qrImg').src = res2.data.data.qrimg
try { timer = setInterval(async () => {
const res = await axios({ const statusRes = await checkStatus(key)
url: `/login/qr/key?timestamp=${Date.now()}`, if (statusRes.code === 800) {
}) alert('二维码已过期,请重新获取')
const key = res.data.data.unikey clearInterval(timer)
}
const res2 = await axios({ if (statusRes.code === 803) {
url: `/login/qr/create?key=${key}&platform=web&qrimg=true&timestamp=${Date.now()}&ua=pc`, // 这一步会返回cookie
}) clearInterval(timer)
document.querySelector('#qrImg').src = res2.data.data.qrimg alert('授权登录成功')
updateStatus('请扫描二维码', 'waiting') await getLoginStatus(statusRes.cookie)
localStorage.setItem('cookie', statusRes.cookie)
timer = setInterval(async () => { }
const statusRes = await checkStatus(key) }, 3000)
if (statusRes.code === 800) {
updateStatus('二维码已过期,请刷新页面', 'error')
clearInterval(timer)
} else if (statusRes.code === 801) {
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
} else if (statusRes.code === 802) {
updateStatus('登录成功,正在保存信息...', 'waiting')
} else if (statusRes.code === 803) {
clearInterval(timer)
updateStatus('授权登录成功!', 'success')
await getLoginStatus(statusRes.cookie)
localStorage.setItem('cookie', statusRes.cookie)
}
}, 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({
@ -170,30 +50,22 @@
}) })
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', data: {
data: { 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,136 +5,79 @@
<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: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: Arial, sans-serif;
min-height: 100vh;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 20px auto;
background: white; padding: 0 20px;
border-radius: 12px;
padding: 32px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
.container {
h1 { background-color: #f5f5f5;
font-size: 24px; padding: 20px;
font-weight: 600; border-radius: 8px;
color: #333;
margin-bottom: 24px;
} }
.form-group { .form-group {
margin-bottom: 20px; margin-bottom: 15px;
} }
.source-options {
label { display: flex;
display: block; flex-wrap: wrap;
font-size: 14px; gap: 10px;
font-weight: 500; margin-bottom: 15px;
color: #555;
margin-bottom: 8px;
} }
.source-option {
input { display: flex;
width: 100%; align-items: center;
padding: 12px 14px; gap: 5px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 15px;
outline: none;
} }
input:focus {
border-color: #333;
}
button { button {
background: #333; background-color: #4CAF50;
color: white; color: white;
padding: 14px 28px; padding: 10px 20px;
border: none; border: none;
border-radius: 6px; border-radius: 4px;
font-size: 15px;
font-weight: 500;
cursor: pointer; cursor: pointer;
transition: background 0.2s ease;
} }
button:hover { button:hover {
background: #555; background-color: #45a049;
} }
button:disabled {
background: #999;
cursor: not-allowed;
}
#result { #result {
margin-top: 24px; margin-top: 20px;
padding: 16px; padding: 10px;
background: #f9f9f9; border: 1px solid #ddd;
border-radius: 6px; border-radius: 4px;
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" /> <input type="number" id="songId" placeholder="请输入音乐ID" required>
<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;
} }
testBtn.disabled = true; const sources = document.getElementById('sources').value;
testBtn.textContent = '测试中...';
const resultDiv = document.getElementById('result');
resultDiv.textContent = '正在请求...'; resultDiv.textContent = '正在请求...';
try { try {
@ -143,9 +86,6 @@
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,230 +4,36 @@
<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 class="container"> <div>
<h1>播客上传声音</h1> <a href="/qrlogin-nocookie.html"> 如果没登录,请先登录 </a>
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a> </div>
<div id="app">
<div class="content"> <ul>
<div class="voice-list"> <li
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 16px;">选择播客列表</h3> v-for="(item,index) in voicelist"
<div v-if="loading" class="loading">加载中...</div> @click="currentVoiceIndex=index"
<div v-else-if="voicelist.length > 0"> :class="{active:currentVoiceIndex===index}"
<div >
v-for="(item, index) in voicelist" <img :src="item.coverUrl" style="width: 50px; width: 50px" />
:key="index" <ul>
@click="currentVoiceIndex = index" <li v-for="(item2,index) in item.voiceListData">
:class="{ active: currentVoiceIndex === index }" {{item2.voiceName}}
class="voice-item" </li>
> </ul>
<div class="voice-header"> {{item.voiceListName}}
<img :src="item.coverUrl" class="voice-cover" alt="cover" /> </li>
<span class="voice-name">{{ item.voiceListName }}</span> </ul>
</div> <input v-model="songName" placeholder="请输入声音名称" />
<div class="voice-tracks" v-if="item.voiceListData"> <input v-model="description" placeholder="请输入介绍" />
<div <input type="file" name="songFile" />
v-for="(item2, index2) in item.voiceListData" <button @click="submit">上传</button>
:key="index2"
class="voice-track"
>
{{ item2.voiceName }}
</div>
</div>
</div>
</div>
<div v-else class="empty-state">暂无播客列表</div>
</div>
<div class="upload-section">
<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@3"></script> <script src="https://fastly.jsdelivr.net/npm/vue"></script>
<script> <script>
Vue.createApp({ Vue.createApp({
data() { data() {
@ -237,7 +43,6 @@
voicelist: [], voicelist: [],
cookieToken: '', cookieToken: '',
currentVoiceIndex: 0, currentVoiceIndex: 0,
loading: false,
} }
}, },
created() { created() {
@ -245,6 +50,12 @@
}, },
computed: { computed: {
currentVoice() { currentVoice() {
// {
// voiceListId: '',
// coverImgId: '',
// categoryId: '',
// secondCategoryId: '',
// }
return this.voicelist[this.currentVoiceIndex] return this.voicelist[this.currentVoiceIndex]
}, },
}, },
@ -252,49 +63,27 @@
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 const res = await axios({
try { url: `/voicelist/search?cookie=${localStorage.getItem('cookie')}`,
const res = await axios({ })
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.forEach(async (i) => { this.voicelist.map(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 || []
console.info(res2)
} catch (err) {
console.error('获取播客详情失败:', err)
}
}) })
} catch (err) { i.voiceListData = res2.data.data.list
console.error('获取播客列表失败:', err) console.info(res2)
} 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(
@ -313,14 +102,26 @@
}) })
.then((res) => { .then((res) => {
alert(`${file.name} 上传成功`) alert(`${file.name} 上传成功`)
if (currentIndx >= fileLength) {
console.info('上传完毕')
}
}) })
.catch((err) => { .catch(async (err) => {
console.error('上传失败:', err) console.info(err)
alert('上传失败,请重试')
}) })
}, },
}, },
}).mount('body') }).mount('#app')
</script> </script>
<style>
ul li {
cursor: pointer;
}
ul li.active {
color: red;
}
</style>
</body> </body>
</html> </html>