MoeFurina c2871322d0
refactor: QR login and voice upload pages with improved UI and error handling
- Enhanced styling for better user experience on qrlogin-nocookie.html and qrlogin.html
- Added loading indicators and improved status messages during QR code login process
- Updated error handling for login status retrieval
- Refactored unblock_test.html for better layout and user interaction
- Improved voice upload page with a more intuitive design and better feedback for file uploads
- Added loading state management for voice list retrieval
- Enhanced accessibility and usability across all modified pages
2026-02-06 19:56:14 +08:00

354 lines
9.0 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>听歌识曲 Demo</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: 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 {
color: #333;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.section {
margin-bottom: 24px;
}
.section h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.control-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 16px;
}
button {
padding: 10px 20px;
background: #333;
color: white;
font-size: 14px;
font-weight: 500;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
}
button:hover {
background: #555;
}
button:disabled {
background: #999;
cursor: not-allowed;
}
input[type="file"] {
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-group input[type="checkbox"] {
cursor: pointer;
}
.checkbox-group label {
margin: 0;
cursor: pointer;
font-size: 14px;
color: #555;
}
audio {
width: 100%;
margin-bottom: 16px;
}
canvas {
width: 100%;
height: 0;
transition: all linear 0.1s;
background: #f9f9f9;
border-radius: 6px;
}
.canvas-active {
height: 15vh;
}
pre {
font-family: 'Courier New', monospace;
font-size: 13px;
color: #666;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 400px;
overflow-y: auto;
padding: 16px;
background: #f9f9f9;
border-radius: 6px;
border: 1px solid #eee;
}
.warning {
padding: 12px 16px;
background: #fef3c7;
border-radius: 6px;
font-size: 14px;
color: #92400e;
margin-bottom: 16px;
}
</style>
</head>
<body>
<div class="container">
<h1>听歌识曲 Demo</h1>
<p class="subtitle">Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a></p>
<hr>
<div class="warning">
<strong>免责声明:</strong>本站点使用网易云音乐官方音频识别API逆向自 <a href="https://fn.music.163.com/g/chrome-extension-home-page-beta/" target="_blank">Chrome 扩展页面</a>),不鼓励版权侵犯或知识产权盗窃。
</div>
<div class="section">
<h3>使用说明</h3>
<p>在使用本站点之前,您可能需要先访问以下链接:</p>
<p><a href="https://cors-anywhere.herokuapp.com/corsdemo" target="_blank">https://cors-anywhere.herokuapp.com/corsdemo</a></p>
<p>由于网易云音乐API没有CORS头这是解决此限制的必要步骤。</p>
</div>
<div class="section">
<h3>使用方法</h3>
<ul style="padding-left: 20px; font-size: 14px; color: #555;">
<li>通过"选择文件"选择您的音频文件</li>
<li>点击"识别"按钮并等待结果</li>
</ul>
</div>
<hr>
<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>
<script src="./afp.wasm.js"></script>
<script src="./afp.js"></script>
<script type="module">
const duration = 3
let audioCtx, recorderNode, micSourceNode
let audioBuffer, bufferHealth
let audio = document.getElementById('audio')
let file = document.getElementById('file')
let clip = document.getElementById('invoke')
let usemic = document.getElementById('usemic')
let canvas = document.getElementById('canvas')
let canvasCtx = canvas.getContext('2d')
let logs = document.getElementById('logs')
logs.write = line => logs.innerHTML += line + '\n'
function RecorderCallback(channelL) {
let sampleBuffer = new Float32Array(channelL.subarray(0, duration * 8000))
GenerateFP(sampleBuffer).then(FP => {
logs.write(`[index] 生成指纹 ${FP}`)
logs.write('[index] 正在查询,请稍候...')
fetch(
'/audio/match?' +
new URLSearchParams({
duration: duration, audioFP: FP
}), {
method: 'POST'
}).then(resp => resp.json()).then(resp => {
if (!resp.data.result) {
return logs.write('[index] 查询失败,无结果')
}
logs.write(`[index] 查询完成。结果数量=${resp.data.result.length}`)
for (var song of resp.data.result) {
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>`
)
}
})
})
}
function InitAudioCtx() {
audioCtx = new AudioContext({ 'sampleRate': 8000 })
if (audioCtx.state == 'suspended')
return false
let audioNode = audioCtx.createMediaElementSource(audio)
audioCtx.audioWorklet.addModule('rec.js').then(() => {
recorderNode = new AudioWorkletNode(audioCtx, 'timed-recorder')
audioNode.connect(recorderNode)
audioNode.connect(audioCtx.destination)
recorderNode.port.onmessage = event => {
switch (event.data.message) {
case 'finished':
RecorderCallback(event.data.recording)
clip.innerHTML = '识别'
clip.disabled = false
canvas.classList.remove('canvas-active')
break
case 'bufferhealth':
clip.innerHTML = `${(duration * (1 - event.data.health)).toFixed(2)}s`
bufferHealth = event.data.health
audioBuffer = event.data.recording
break
default:
logs.write(event.data.message)
}
}
navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: false,
autoGainControl: false,
noiseSuppression: false,
latency: 0,
},
}).then(micStream => {
micSourceNode = audioCtx.createMediaStreamSource(micStream);
micSourceNode.connect(recorderNode)
usemic.checked = true
logs.write('[rec.js] 麦克风已连接')
});
});
return true
}
clip.addEventListener('click', event => {
recorderNode.port.postMessage({
message: 'start', duration: duration
})
clip.disabled = true
canvas.classList.add('canvas-active')
})
usemic.addEventListener('change', event => {
if (!usemic.checked)
micSourceNode.disconnect(recorderNode)
else
micSourceNode.connect(recorderNode)
})
file.addEventListener('change', event => {
file.files[0].arrayBuffer().then(
async buffer => {
logs.write(`[index] 文件 ${file.files[0].name} 已加载`)
audio.src = window.URL.createObjectURL(new Blob([buffer]))
clip.disabled = false
})
})
function UpdateCanvas() {
let w = canvas.clientWidth, h = canvas.clientHeight
canvas.width = w, canvas.height = h
canvasCtx.fillStyle = 'rgba(0,0,0,0)';
canvasCtx.fillRect(0, 0, w, h);
if (audioBuffer) {
canvasCtx.fillStyle = 'black';
for (var x = 0; x < w * bufferHealth; x++) {
var y = audioBuffer[Math.ceil((x / w) * audioBuffer.length)]
var z = Math.abs(y) * h / 2
canvasCtx.fillRect(x, h / 2 - (y > 0 ? z : 0), 1, z)
}
}
requestAnimationFrame(UpdateCanvas)
}
UpdateCanvas()
let requestCtx = setInterval(() => {
try {
if (InitAudioCtx()) {
clearInterval(requestCtx)
logs.write('[rec.js] 音频上下文已启动')
}
} catch {
// Fail silently
}
}, 100)
</script>
</html>