Initial commit

Created from https://vercel.com/new
This commit is contained in:
IamFurina
2025-02-22 11:16:58 +00:00
commit 8f9d38529c
435 changed files with 59341 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 调试界面</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.container {
display: flex;
flex-direction: column;
flex-grow: 1;
}
form {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
input, button {
padding: 10px;
box-sizing: border-box;
flex: 1;
}
button {
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
.data-result {
display: flex;
flex-direction: row;
flex-grow: 1;
}
.data-result > div {
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 10px;
box-sizing: border-box;
}
.data-result label {
margin-bottom: 10px;
}
#data, #result {
height: 100%;
box-sizing: border-box;
}
#data {
border-right: 1px solid #ccc;
}
</style>
</head>
<body>
<div class="container">
<form onsubmit="event.preventDefault(); sendRequest();">
<label for="uri">uri</label>
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
<label for="crypto">crypto</label>
<select id="crypto" name="crypto">
<option value="weapi">weapi</option>
<option value="eapi">eapi</option>
<option value="api">api</option>
<option value="linuxapi">linuxapi</option>
<option value="" selected>(默认)</option>
</select>
<button type="submit">发送</button>
</form>
<div class="data-result">
<div>
<label for="result">result</label>
<textarea id="result" name="result"></textarea>
</div>
<div>
<label for="data">data</label>
<textarea id="data" name="data">
{
"cp": false,
"id": "2058263032",
"kv": 0,
"lv": 0,
"rv": 0,
"tv": 0,
"yrv": 0,
"ytv": 0,
"yv": 0
}</textarea>
</div>
</div>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
async function sendRequest() {
document.getElementById('result').value = ""
const uri = document.getElementById('uri').value;
const crypto = document.getElementById('crypto').value;
const data = document.getElementById('data').value;
try {
const res = await axios({
url: `/api?timestamp=${Date.now()}`,
method: 'post',
data: {
uri: uri,
data: JSON.parse(data),
crypto: crypto,
},
});
document.getElementById('result').value = JSON.stringify(res.data, null, 2);
} catch (error) {
document.getElementById('result').value = 'Request failed: ' + error.message;
}
}
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+199
View File
@@ -0,0 +1,199 @@
<!DOCTYPE html>
<head>
<style>
* {
font-family: sans-serif;
}
pre {
font-family: monospace;
}
a {
font-family: sans-serif;
}
audio {
width: 100%;
}
canvas {
width: 100%;
height: 0;
transition: all linear 0.1s;
}
.canvas-active {
height: 15vh;
}
pre {
overflow: scroll;
}
</style>
</head>
<body>
<h1>听歌识曲 Demo (Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a>)</h1>
<hr>
<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>
<audio id="audio" controls autoplay></audio>
<canvas id="canvas"></canvas>
<button id="invoke">Clip</button>
<input type="file" name="picker" accept="*" id="file">
<hr>
<label for="use-mic">Mix in Microphone input</label>
<input type="checkbox" name="use-mic" id="usemic">
<hr>
<pre id="logs"></pre>
</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] Generated FP ${FP}`)
logs.write('[index] Now querying, please wait...')
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] Query failed with no results.')
}
logs.write(`[index] Query complete. Results=${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() {
// AFP.wasm can't do it with anything other than 8KHz
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) // recorderNode doesn't output anything
audioNode.connect(audioCtx.destination)
recorderNode.port.onmessage = event => {
switch (event.data.message) {
case 'finished':
RecorderCallback(event.data.recording)
clip.innerHTML = 'Clip'
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)
}
}
// Attempt to get user's microphone and connect it to the AudioContext.
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] Microphone attached.')
});
});
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 ${file.files[0].name} loaded.`)
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()) { // Put this here so we don't have to deal with the 'user did not interact' thing
clearInterval(requestCtx)
logs.write('[rec.js] Audio Context started.')
}
} catch {
// Fail silently
}
}, 100)
</script>
+49
View File
@@ -0,0 +1,49 @@
/* AudioWorkletProcesser must be initialized as modules (i.e. seperate files)
* Ref : https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor
*/
class TimedRecorder extends AudioWorkletProcessor {
constructor(options) {
super()
this.max_length = 0
this.recbuffer = new Float32Array()
this.recording = false
this.buf_index = 0
this.port.onmessage = (event) => {
switch (event.data.message) {
case 'start':
this.max_length = event.data.duration * 8000
this.recbuffer = new Float32Array(this.max_length)
this.buf_index = 0
this.recording = true
this.port.postMessage({ message: '[rec.js] Recording started' })
break
}
}
}
process(inputs) {
// Only take care of channel 0 (Left)
if (this.recording) {
let channelL = inputs[0][0]
this.port.postMessage({
message: 'bufferhealth',
health: this.buf_index / this.max_length,
recording: this.recbuffer,
})
if (this.buf_index + channelL.length > this.max_length) {
this.port.postMessage({ message: '[rec.js] Recording finished' })
this.recording = false
this.buf_index = 0
this.port.postMessage({
message: 'finished',
recording: this.recbuffer,
})
} else {
this.recbuffer.set(channelL, this.buf_index)
this.buf_index += channelL.length
}
}
return true
}
}
registerProcessor('timed-recorder', TimedRecorder)
+73
View File
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>更新头像</title>
</head>
<body>
<div>
<a href="/qrlogin-nocookie.html">
如果没登录,请先登录
</a>
</div>
<input id="file" type="file" />
<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>
<script>
main()
async function main() {
document.querySelector('input[type="file"]').addEventListener(
'change',
function (e) {
var file = this.files[0]
upload(file)
},
false,
)
const res = await axios({
url: `/user/detail?uid=32953014&timestamp=${Date.now()}`,
withCredentials: true, //跨域的话必须设置
})
document.querySelector('#avatar').src = res.data.profile.avatarUrl
}
async function upload(file) {
var formData = new FormData()
formData.append('imgFile', file)
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
}
function getImgSize(file) {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = function (theFile) {
let image = new Image()
image.src = theFile.target.result
image.onload = function () {
resolve({
width: this.width,
height: this.height,
})
}
}
})
}
</script>
</body>
</html>
+99
View File
@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>云盘上传</title>
</head>
<body>
<div>
<a href="/qrlogin-nocookie.html">
如果没登录,请先登录
</a>
</div>
<input id="file" type="file" multiple />
<div id="app">
<ul>
<li v-for="(item,index) in songs" :key="index">
{{item.songName}}
</li>
</ul>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script src="https://fastly.jsdelivr.net/npm/vue"></script>
<script>
const app = Vue.createApp({
data() {
return {
songs: []
}
},
created() {
this.getData()
},
methods: {
getData() {
console.log('getdata');
const _this = this
axios({
url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
}).then(res => {
console.log(res.data)
_this.songs = res.data.data
})
}
}
}).mount('#app')
const fileUpdateTime = {}
let fileLength = 0
function main() {
document
.querySelector('input[type="file"]')
.addEventListener('change', function (e) {
console.log(this.files)
let currentIndx = 0
fileLength = this.files.length
for (const item of this.files) {
currentIndx += 1
upload(item, currentIndx)
}
})
}
main()
function upload(file, currentIndx) {
var formData = new FormData()
formData.append('songFile', file)
axios({
method: 'post',
url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
headers: {
'Content-Type': 'multipart/form-data',
},
data: formData,
}).then(res => {
console.log(`${file.name} 上传成功`)
if (currentIndx >= fileLength) { console.log('上传完毕') }
app.getData()
}).catch(async err => {
console.log(err)
console.log(fileUpdateTime)
fileUpdateTime[file.name] ? fileUpdateTime[file.name] += 1 : fileUpdateTime[file.name] = 1
if (fileUpdateTime[file.name] >= 4) {
console.error(`丢,这首歌怎么都传不上:${file.name}`)
return
} else {
console.error(`${file.name} 失败 ${fileUpdateTime[file.name]} 次`)
}
// await login()
upload(file, currentIndx)
})
}
</script>
</body>
</html>
View File
+13
View File
@@ -0,0 +1,13 @@
# 网易云音乐 API
> 网易云音乐 NodeJS 版 API
- 全部接口已升级到最新
- 具备登录接口,多达200多个接口
- 更完善的文档
[GitLab](https://gitlab.com/Binaryify/NeteaseCloudMusicApi)
[Get Started](#neteasecloudmusicapi)
![color](#ffffff)
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+5099
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+47
View File
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="KEYWords" contect="网易云音乐,网易云音乐 api,网易云音乐 nodejs,网易云音乐 node.js">
<meta name="description" content="网易云音乐 NodeJS 版 API">
<title>网易云音乐 NodeJS 版 API</title>
<link rel="icon" href="favicon.ico">
<meta name="description" content="Description">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta name="referrer" content="never">
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
(adsbygoogle = window.adsbygoogle || []).push({
google_ad_client: "ca-pub-5159844745975514",
enable_page_level_ads: true
});
</script>
</head>
<body>
<div id="app"></div>
</body>
<script>
window.$docsify = {
name: '网易云音乐 API',
repo: 'https://gitlab.com/Binaryify/NeteaseCloudMusicApi',
coverpage: true,
homepage: 'home.md',
}
</script>
<script src="https://unpkg.com/docsify@4.11.3/lib/docsify.min.js"></script>
<script>
if (typeof navigator.serviceWorker !== 'undefined') {
navigator.serviceWorker.register('sw.js')
}
</script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-139996012-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-139996012-1');
</script>
</html>
+90
View File
@@ -0,0 +1,90 @@
/* ===========================================================
* docsify sw.js
* ===========================================================
* Copyright 2016 @huxpro
* Licensed under Apache 2.0
* Register service worker.
* ========================================================== */
const RUNTIME = 'docsify'
const HOSTNAME_WHITELIST = [
self.location.hostname,
'fonts.gstatic.com',
'fonts.googleapis.com',
'unpkg.com',
]
// The Util Function to hack URLs of intercepted requests
const getFixedUrl = (req) => {
var now = Date.now()
var url = new URL(req.url)
// 1. fixed http URL
// Just keep syncing with location.protocol
// fetch(httpURL) belongs to active mixed content.
// And fetch(httpRequest) is not supported yet.
url.protocol = self.location.protocol
// 2. add query for caching-busting.
// Github Pages served with Cache-Control: max-age=600
// max-age on mutable content is error-prone, with SW life of bugs can even extend.
// Until cache mode of Fetch API landed, we have to workaround cache-busting with query string.
// Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190
if (url.hostname === self.location.hostname) {
url.search += (url.search ? '&' : '?') + 'cache-bust=' + now
}
return url.href
}
/**
* @Lifecycle Activate
* New one activated when old isnt being used.
*
* waitUntil(): activating ====> activated
*/
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim())
})
/**
* @Functional Fetch
* All network requests are being intercepted here.
*
* void respondWith(Promise<Response> r)
*/
self.addEventListener('fetch', (event) => {
// Skip some of cross-origin requests, like those for Google Analytics.
if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) {
// Stale-while-revalidate
// similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale
// Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1
const cached = caches.match(event.request)
const fixedUrl = getFixedUrl(event.request)
const fetched = fetch(fixedUrl, { cache: 'no-store' })
const fetchedCopy = fetched.then((resp) => resp.clone())
// Call respondWith() with whatever we get first.
// If the fetch fails (e.g disconnected), wait for the cache.
// If there’s nothing in cache, wait for the fetch.
// If neither yields a response, return offline pages.
event.respondWith(
Promise.race([fetched.catch((_) => cached), cached])
.then((resp) => resp || fetched)
.catch((_) => {
/* eat any errors */
}),
)
// Update the cache with the version we fetched (only for ok status)
event.waitUntil(
Promise.all([fetchedCopy, caches.open(RUNTIME)])
.then(
([response, cache]) =>
response.ok && cache.put(event.request, response),
)
.catch((_) => {
/* eat any errors */
}),
)
}
})
+76
View File
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>eapi 参数和返回内容解析</title>
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
</head>
<body>
<div id="app" class="p-5 flex flex-col">
<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>
<button @click="decrypt" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
解密
</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>{{ JSON.stringify(JSON.parse(result), null, 2) }}</pre>
</p>
</div>
<div>
<p>使用例子:</p>
<img src="/static/eapi_params.png" />
<img src="/static/eapi_response.png" />
</div>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios"></script>
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
<script>
const app = Vue.createApp({
data() {
return {
hexString: 'AD96DDB984491E79B6F429DD650C6E2AE524627AC223AC9A123C66BB0997965950FED137544A93DFC718E16F57C8C121AF537086F395570A5602A3922366D11964DAFACD7830AACABF62E5650E67F457E79C1D2E13502391FC3487216CC5BF8681843FCB8E05559487EB18AAC1BE0EFEA4F7B6A050478366153A9426C238B8869600B275704555A9EB94C92E4F3FDABE9E0BCE07645410D0AA7B675698A4CAE6CD3620633ABF0B849A4244CC8DFC5DB2646D5EA9B3954E62BFEF19AFEAFDDC34E55C3E9A1DD3167CF53D443617108141',
result: '{}',
isReq: true
}
},
mounted() {
this.decrypt()
},
methods: {
async decrypt() {
try {
const res = await axios({
url: `/eapi/decrypt?isReq=${this.isReq}&timestamp=${Date.now()}`,
method: 'post',
data: {
hexString: this.hexString
}
})
this.result = JSON.stringify(res.data.data)
console.log(res.data);
} catch (error) {
console.error(error)
alert(error?.response?.data?.message || '解密失败,数据格式错误')
}
}
}
})
app.mount('#app')
</script>
</body>
</html>
+44
View File
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>home</title>
</head>
<body>
<div>
<a href="/qrlogin-nocookie.html">
如果没登录,请先登录
</a>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script>
async function main() {
const res = await axios({
url: `/homepage/block/page`,
data: {
cookie: localStorage.getItem('cookie'),
},
method: 'post',
})
let cursor = ''
console.log(res.data.data)
if (res.data.data.hasMore) {
cursor = res.data.data.cursor
const res2 = await axios({
url: `/homepage/block/page?time=${Date.now()}`,
data: {
cookie: localStorage.getItem('cookie'),
cursor: cursor,
},
method: 'post',
})
}
}
main()
</script>
</body>
</html>
+136
View File
@@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>网易云音乐 API</title>
<style>
html, body {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
background: #f9f9f9;
font-family: Arial, sans-serif;
color: #333;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
h1 {
font-size: 2.5em;
margin-bottom: 1em;
color: #42b983;
}
p {
font-size: 1.2em;
margin-bottom: 2em;
color: #666;
}
a {
color: #42b983;
text-decoration: none;
transition: color 0.3s ease;
}
a:hover {
color: #329d6e;
}
ul {
list-style: none;
padding: 0;
margin: 0;
text-align: left;
max-width: 600px;
width: 100%;
}
li {
margin: 0.5em 0;
line-height: 1.5;
display: flex;
align-items: center;
}
.number {
margin-right: 10px;
color: #999;
font-weight: bold;
}
.container {
background: #fff;
padding: 2em;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
max-height: 80vh; /* 设置最大高度 */
overflow-y: auto; /* 启用垂直滚动条 */
}
.version {
margin-top: 0.5em;
font-size: 1.0em;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<h1>网易云音乐 API <span id="api-version"></span></h1>
<p>当你看到这个页面时,这个服务已经成功跑起来了~</p>
<p class="current-url"><span id="current-url"></span></p>
<a href="/docs">查看文档</a>
<h2>例子:</h2>
<ul id="example-list">
<li><a href="./search?keywords=海阔天空">搜索</a></li>
<li><a href="./comment/music?id=186016&limit=1">歌曲评论</a></li>
<li><a href="./dj/program?rid=336355127">电台节目</a></li>
<li><a href="./qrlogin.html">二维码登录</a></li>
<li><a href="./audio_match_demo/index.html">听歌识曲</a></li>
<li><a href="./cloud.html">云盘上传</a></li>
<li><a href="./eapi_decrypt.html">eapi 参数/返回值解析</a></li>
<li><a href="./api.html">API 调试界面</a></li>
<li><a href="./playlist_import.html">歌单导入工具</a></li>
</ul>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const currentUrlElement = document.getElementById('current-url');
currentUrlElement.textContent = window.location.href;
const exampleList = document.getElementById('example-list');
const items = exampleList.getElementsByTagName('li');
for (let i = 0; i < items.length; i++) {
const number = document.createElement('span');
number.className = 'number';
number.textContent = (i + 1) + '. ';
items[i].prepend(number);
}
// 获取API版本号
axios({
url: '/inner/version',
method: 'post',
data: {},
}).then((res) => {
const body = res.data;
const version = body.data.version;
const apiVersionElement = document.getElementById('api-version');
apiVersionElement.textContent = ` v${version}`;
});
});
</script>
</body>
</html>
+318
View File
@@ -0,0 +1,318 @@
<!-- eslint-disable prettier/prettier -->
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>一起听</title>
<script src="https://unpkg.com/petite-vue"></script>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/mdui@1.0.2/dist/css/mdui.min.css" />
<script src="https://unpkg.com/mdui@1.0.2/dist/js/mdui.min.js"></script>
</head>
<body class="mdui-container">
<div>
<a href="/qrlogin.html">
如果没登录,请先登录
</a>
</div>
<h1>一起听 - 主机模式</h1>
<div>消息: {{message}}</div>
<audio id="player" autoplay controls>
</audio>
<br />
<br />
<button v-if="!account.login" @click="login">获取登录状态</button>
<div>您的当前登录账号为: {{account.nickname}}</div>
<br />
<div v-if="account.login">
<button v-if="!roomInfo.roomId" @click="createRoom">创建房间</button>
<details>
<summary>加入房间</summary>
<div><span>房间ID: </span><input v-model="roomInfo.roomId" /></div>
<div><span>邀请者 ID: </span><input v-model="roomInfo.inviterId" /></div>
<button @click="joinRoom">点击加入</button>
</details>
<div v-if="roomInfo.roomId">
<div>分享链接为:
https://st.music.163.com/listen-together/share/?songId=1372188635&roomId={{roomInfo.roomId}}&inviterId={{roomInfo.inviterId}}
</div>
<br />
<button @click="refreshRoom">刷新房间状态</button>
<div>在线用户: </div>
<ul class="mdui-list">
<li v-for="user in roomInfo.roomUsers" class="mdui-list-item mdui-ripple">
<div class="mdui-list-item-avatar">
<img :src="user.avatarUrl" />
</div>
<div class="mdui-list-item-content">{{user.nickname}}</div>
</li>
</ul>
<button v-if="roomInfo.roomId" @click="closeRoom">关闭房间</button>
</div>
</div>
<button @click="playTrack">播放</button>
<button @click="pauseTrack">暂停</button>
<button @click="seekTrack">同步进度</button>
<details>
<summary>播放列表</summary>
<br />
<div><span>歌单ID: </span><input v-model="playlistInfo.playlistId" /></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>
</body>
<script>
PetiteVue.createApp({
message: '请点击获取登录状态',
account: {
login: false,
userId: 0,
nickname: '未登录',
},
roomInfo: {
roomId: null,
inviterId: 0,
roomUsers: [],
},
playlistInfo: {
playlistId: 0,
playlistName: '未获取',
playlistTrackIds: [],
playlistTracks: [],
},
playingInfo: {
trackId: 0,
status: 'PLAY',
progress: 1,
},
clientSeq: 1,
login: async function () {
const res = await axios({
url: `/login/status?timestamp=${new Date().getTime()}`,
method: 'get',
data: {
cookie: localStorage.getItem('cookie'),
},
})
if (res.data.data.code != 200) {
alert('请先使用登录 API 登录到网易云音乐')
} else {
this.account.userId = res.data.data.profile.userId
this.account.nickname = res.data.data.profile.nickname
this.account.login = true
this.message = '成功登录, 请创建房间'
}
},
joinRoom: async function () {
const res = await axios({
url: 'listentogether/accept',
method: 'post',
data: {
roomId: this.roomInfo.roomId,
inviterId: this.roomInfo.inviterId,
cookie: localStorage.getItem('cookie'),
},
})
console.log(res)
if (res.data.code != 200) {
this.message = '加入房间出现问题: ' + res.data.message
} else {
this.message = '加入房间成功: ' + this.roomInfo.roomId
const res2 = await axios({
url: 'listentogether/room/check',
method: 'post',
data: {
roomId: this.roomInfo.roomId,
cookie: localStorage.getItem('cookie'),
},
})
console.log(res2)
const res3 = await axios({
url: 'listentogether/sync/playlist/get',
method: 'post',
data: {
roomId: this.roomInfo.roomId,
cookie: localStorage.getItem('cookie'),
},
})
this.playlistInfo.playlistName = "其他人的歌单"
this.playlistInfo.playlistTrackIds = res3.data.data.playlist.displayList.result
.join(',')
const resa = await axios({
url: '/song/detail',
method: 'post',
data: {
ids: this.playlistInfo.playlistTrackIds,
cookie: localStorage.getItem('cookie'),
},
})
console.log(resa)
this.playlistInfo.playlistTracks = resa.data.songs
}
},
createRoom: async function () {
const res = await axios({
url: 'listentogether/room/create',
method: 'get',
data: {
cookie: localStorage.getItem('cookie'),
},
})
console.log(res)
if (res.data.code != 200) {
this.message = '创建房间出现问题: ' + res.data.message
} else {
this.message = '创建房间成功: ' + res.data.data.roomInfo.roomId
this.roomInfo.inviterId = this.account.userId
this.roomInfo.roomId = res.data.data.roomInfo.roomId
const res2 = await axios({
url: 'listentogether/room/check',
method: 'post',
data: {
roomId: this.roomInfo.roomId,
cookie: localStorage.getItem('cookie'),
},
})
console.log(res2)
}
},
refreshRoom: async function () {
const res = await axios({
url: '/listentogether/status',
data: {
cookie: localStorage.getItem('cookie'),
},
})
console.log(res)
if (res.data.code != 200 || !res.data.data.inRoom) {
this.message = '房间状态获取失败, 可能退出了房间'
} else {
this.roomInfo.roomUsers = res.data.data.roomInfo.roomUsers
}
},
closeRoom: async function () {
const res = await axios({
url: '/listentogether/end',
method: 'post',
data: {
roomId: this.roomInfo.roomId,
cookie: localStorage.getItem('cookie'),
},
})
console.log(res)
if (res.data.code != 200 || !res.data.data.success) {
this.message = '房间关闭失败'
} else {
this.message = '房间关闭成功'
this.roomInfo.roomId = null
}
},
loadPlaylist: async function () {
const res = await axios({
url: '/playlist/detail',
method: 'post',
data: {
id: this.playlistInfo.playlistId,
cookie: localStorage.getItem('cookie'),
},
})
console.log(res)
this.playlistInfo.playlistName = res.data.playlist.name
this.playlistInfo.playlistTrackIds = res.data.playlist.trackIds
.map((track) => track.id)
.join(',')
const resa = await axios({
url: '/song/detail',
method: 'post',
data: {
ids: this.playlistInfo.playlistTrackIds,
cookie: localStorage.getItem('cookie'),
},
})
console.log(resa)
this.playlistInfo.playlistTracks = resa.data.songs
if (this.roomInfo.roomId) {
const resb = await axios({
url: 'listentogether/sync/list/command',
method: 'post',
data: {
roomId: this.roomInfo.roomId,
commandType: 'REPLACE',
userId: this.account.userId,
version: this.clientSeq++,
playMode: 'ORDER_LOOP',
displayList: this.playlistInfo.playlistTrackIds,
randomList: this.playlistInfo.playlistTrackIds,
cookie: localStorage.getItem('cookie'),
},
})
console.log(resb)
}
},
gotoTrack: async function (trackId) {
this.playingInfo.trackId = trackId
if (this.roomInfo.roomId) {
await this.playCommand('GOTO')
}
document.getElementById('player').src =
'https://music.163.com/song/media/outer/url?id=' + trackId + '.mp3'
},
playTrack: async function () {
this.playingInfo.status = 'PLAY'
await this.playCommand('PLAY')
document.getElementById('player').play()
},
pauseTrack: async function () {
this.playingInfo.status = 'PAUSE'
await this.playCommand('PAUSE')
document.getElementById('player').pause()
},
seekTrack: async function () {
this.playingInfo.status = 'PLAY'
await this.playCommand('seek')
document.getElementById('player').play()
},
playCommand: async function (action) {
const res = await axios({
url: 'listentogether/play/command',
method: 'post',
data: {
roomId: this.roomInfo.roomId,
progress: Math.floor(
document.getElementById('player').currentTime * 1000,
),
commandType: action,
formerSongId: '-1',
targetSongId: this.playingInfo.trackId,
clientSeq: this.clientSeq++,
playStatus: this.playingInfo.status,
cookie: localStorage.getItem('cookie'),
},
})
console.log(res)
},
}).mount()
</script>
</html>
+47
View File
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录</title>
</head>
<body>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script>
const phone = '' // 这里填手机号
const password = '' // 这里填密码
const fileUpdateTime = {}
if (!phone || !password) {
const msg = '请设置你的手机号码和密码'
alert(msg)
throw new Error(msg)
}
async function login() {
const res = await axios({
url: `/login/cellphone`,
method: 'post',
data: {
phone: phone,
password: password,
},
})
return res.data.cookie
}
async function main() {
const cookieToken = await login()
const res = await axios({
url: `/login/status`,
method: 'post',
data: {
cookie: cookieToken,
},
})
}
main()
</script>
</body>
</html>
+77
View File
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>歌单封面上传</title>
</head>
<body>
<div>
<a href="/qrlogin-nocookie.html">
如果没登录,请先登录
</a>
</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>
const playlist_id = ''
if (!playlist_id) {
const msg = '请设置你的歌单id'
alert(msg)
throw new Error(msg)
}
main()
async function main() {
document.querySelector('input[type="file"]').addEventListener(
'change',
function (e) {
var file = this.files[0]
upload(file)
},
false,
)
const res = await axios({
url: `/playlist/detail?id=${playlist_id}&timestamp=${Date.now()}`,
})
document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl
}
async function upload(file) {
var formData = new FormData()
formData.append('imgFile', file)
const imgSize = await getImgSize(file)
const res = await axios({
method: 'post',
url: `/playlist/cover/update?id=${playlist_id}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width
}&imgX=0&imgY=0&timestamp=${Date.now()}`,
headers: {
'Content-Type': 'multipart/form-data',
},
data: formData,
})
document.querySelector('#playlist_cover').src = res.data.data.url
}
function getImgSize(file) {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = function (theFile) {
let image = new Image()
image.src = theFile.target.result
image.onload = function () {
resolve({
width: this.width,
height: this.height,
})
}
}
})
}
</script>
</body>
</html>
+262
View File
@@ -0,0 +1,262 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>歌单导入工具</title>
<!-- 引入Bootstrap CSS -->
<link href="https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- 引入Bootstrap JS -->
<script src="https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- 引入axios用于发送异步请求 -->
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<div class="container mt-5">
<h1 class="mb-4">歌单导入工具</h1>
<p>请选择一种导入方式并填写相关信息:</p>
<!-- 表单开始 -->
<form id="importForm" novalidate>
<!-- 选项卡导航 -->
<ul class="nav nav-tabs mb-3" id="importTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="metadata-tab" data-bs-toggle="tab" data-bs-target="#metadata" type="button" role="tab" aria-controls="metadata" aria-selected="true">元数据导入</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="text-tab" data-bs-toggle="tab" data-bs-target="#text" type="button" role="tab" aria-controls="text" aria-selected="false">文字导入</button>
</li>
<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" id="importTabContent">
<!-- 元数据导入 -->
<div class="tab-pane fade show active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab">
<table class="table table-bordered mb-3">
<thead>
<tr>
<th scope="col">歌曲名称</th>
<th scope="col">艺术家</th>
<th scope="col">专辑</th>
</tr>
</thead>
<tbody id="metadataTableBody">
<!-- 默认添加一行 -->
<tr>
<td><input type="text" class="form-control" name="name[]" placeholder="歌曲名称"></td>
<td><input type="text" class="form-control" name="artist[]" placeholder="艺术家"></td>
<td><input type="text" class="form-control" name="album[]" placeholder="专辑"></td>
</tr>
</tbody>
</table>
<button type="button" class="btn btn-secondary mb-3" id="addMetadataRow">增加歌曲</button>
</div>
<!-- 文字导入 -->
<div class="tab-pane fade" id="text" role="tabpanel" aria-labelledby="text-tab">
<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 class="tab-pane fade" id="link" role="tabpanel" aria-labelledby="link-tab">
<div class="mb-3">
<label for="linkInputs" class="form-label">链接:</label>
<div id="linkInputsContainer">
<div class="input-group mb-3">
<input type="text" class="form-control" id="linkInput0" name="linkInput0" placeholder="请输入链接">
<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>
<!-- 是否导入我喜欢的音乐 -->
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="importStarCheckbox">
<label class="form-check-label" for="importStarCheckbox">
导入“我喜欢的音乐”
</label>
</div>
<!-- 提交按钮 -->
<button type="submit" class="btn btn-primary mt-3">导入歌曲</button>
</form>
<!-- 表单结束 -->
<script>
// 动态增加链接输入框
document.getElementById('addLinkButton').addEventListener('click', function() {
var container = document.getElementById('linkInputsContainer');
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 = '请输入链接';
var removeButton = document.createElement('button');
removeButton.type = 'button';
removeButton.className = 'btn btn-secondary removeLinkButton';
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 mb-3';
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');
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>
</html>
+72
View File
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>二维码登录</title>
</head>
<body>
<img id="qrImg" />
<div id="info" class="info"></div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js
"></script>
<script>
async function login() {
let timer
let timestamp = Date.now()
const cookie = localStorage.getItem('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}&qrimg=true&timestamp=${Date.now()}`,
})
document.querySelector('#qrImg').src = res2.data.data.qrimg
timer = setInterval(async () => {
const statusRes = await checkStatus(key)
if (statusRes.code === 800) {
alert('二维码已过期,请重新获取')
clearInterval(timer)
}
if (statusRes.code === 803) {
// 这一步会返回cookie
clearInterval(timer)
alert('授权登录成功')
await getLoginStatus(statusRes.cookie)
localStorage.setItem('cookie', statusRes.cookie)
}
}, 3000)
}
login()
async function checkStatus(key) {
const res = await axios({
url: `/login/qr/check?key=${key}&timestamp=${Date.now()}&noCookie=true`,
})
return res.data
}
async function getLoginStatus(cookie = '') {
const res = await axios({
url: `/login/status?timestamp=${Date.now()}`,
method: 'post',
data: {
cookie,
},
})
document.querySelector('#info').innerText = JSON.stringify(res.data, null, 2)
}
</script>
<style>
.info {
white-space: pre;
}
</style>
</body>
</html>
+71
View File
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>二维码登录</title>
</head>
<body>
<img id="qrImg" />
<div id="info" class="info"></div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js
"></script>
<script>
async function login() {
let timer
let timestamp = Date.now()
const cookie = localStorage.getItem('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}&qrimg=true&timestamp=${Date.now()}`,
})
document.querySelector('#qrImg').src = res2.data.data.qrimg
timer = setInterval(async () => {
const statusRes = await checkStatus(key)
if (statusRes.code === 800) {
alert('二维码已过期,请重新获取')
clearInterval(timer)
}
if (statusRes.code === 803) {
// 这一步会返回cookie
clearInterval(timer)
alert('授权登录成功')
await getLoginStatus(statusRes.cookie)
localStorage.setItem('cookie', statusRes.cookie)
}
}, 3000)
}
login()
async function checkStatus(key) {
const res = await axios({
url: `/login/qr/check?key=${key}&timestamp=${Date.now()}`,
})
return res.data
}
async function getLoginStatus(cookie = '') {
const res = await axios({
url: `/login/status?timestamp=${Date.now()}`,
method: 'post',
data: {
cookie,
},
})
document.querySelector('#info').innerText = JSON.stringify(res.data, null, 2)
}
</script>
<style>
.info {
white-space: pre;
}
</style>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

+125
View File
@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>播客上传声音</title>
</head>
<body>
<div>
<a href="/qrlogin-nocookie.html">
如果没登录,请先登录
</a>
</div>
<div id="app">
<ul>
<li v-for="(item,index) in voicelist" @click="currentVoiceIndex=index"
:class="{active:currentVoiceIndex===index}">
<img :src="item.coverUrl" style="width:50px;width:50px;" />
<ul>
<li v-for="(item2,index) in item.voiceListData">
{{item2.voiceName}}
</li>
</ul>
{{item.voiceListName}}
</li>
</ul>
<input v-model="songName" placeholder="请输入声音名称" />
<input v-model="description" placeholder="请输入介绍" />
<input type="file" name="songFile" />
<button @click="submit">上传</button>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script src="https://fastly.jsdelivr.net/npm/vue"></script>
<script>
Vue.createApp({
data() {
return {
songName: '',
description: '',
voicelist: [],
cookieToken: '',
currentVoiceIndex: 0,
}
},
created() {
this.getData()
},
computed: {
currentVoice() {
// {
// voiceListId: '',
// coverImgId: '',
// categoryId: '',
// secondCategoryId: '',
// }
return this.voicelist[this.currentVoiceIndex]
},
},
methods: {
submit() {
console.log('submit')
const file = document.querySelector('input[type=file]').files[0]
this.upload(file)
},
async getData() {
const res = await axios({
url: `/voicelist/search?cookie=${localStorage.getItem('cookie')}`,
})
console.log(res.data.data)
this.voicelist = res.data.data.list
this.voicelist.map(async i => {
const res2 = await axios({
url: `/voicelist/list?voiceListId=${i.voiceListId}&limit=5`,
})
i.voiceListData = res2.data.data.list
console.log(res2);
})
},
upload(file) {
var formData = new FormData()
formData.append('songFile', file)
axios({
method: 'post',
url: `/voice/upload?time=${Date.now()}&cookie=${localStorage.getItem('cookie')
}&songName=${this.songName}&voiceListId=${this.currentVoice.voiceListId
}&categoryId=${this.currentVoice.categoryId}&coverImgId=${this.currentVoice.coverImgId
}&secondCategoryId=${this.currentVoice.secondCategoryId}&description=${this.description
}&privacy=1`,
headers: {
'Content-Type': 'multipart/form-data',
},
data: formData,
})
.then((res) => {
alert(`${file.name} 上传成功`)
if (currentIndx >= fileLength) {
console.log('上传完毕')
}
})
.catch(async (err) => {
console.log(err)
})
},
},
}).mount('#app')
</script>
<style>
ul li {
cursor: pointer;
}
ul li.active {
color: red;
}
</style>
</body>
</html>