mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-06 14:52:50 +08:00
feat: 新增 websocket 接口获取当前播放状态
This commit is contained in:
@@ -22,7 +22,8 @@ dependencies = [
|
||||
"python-multipart>=0.0.12",
|
||||
"requests>=2.32.3",
|
||||
"sentry-sdk[fastapi]==1.45.1",
|
||||
"python-socketio>=5.12.1",
|
||||
"python-socketio>=5.12.1",
|
||||
"pyjwt>=2.10.1",
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -6,13 +6,16 @@ import os
|
||||
import secrets
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.parse
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import asdict
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import jwt
|
||||
import socketio
|
||||
from fastapi import WebSocket, WebSocketDisconnect
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from xiaomusic.xiaomusic import XiaoMusic
|
||||
@@ -942,3 +945,85 @@ async def proxy(urlb64: str):
|
||||
except Exception as e:
|
||||
await close_session()
|
||||
raise HTTPException(status_code=500, detail=f"发生错误: {str(e)}") from e
|
||||
|
||||
|
||||
# 配置
|
||||
JWT_SECRET = secrets.token_urlsafe(32)
|
||||
JWT_ALGORITHM = "HS256"
|
||||
JWT_EXPIRE_SECONDS = 60 * 5 # 5 分钟有效期(足够前端连接和重连)
|
||||
|
||||
|
||||
@app.get("/generate_ws_token")
|
||||
def generate_ws_token(
|
||||
did: str,
|
||||
_: bool = Depends(verification), # 复用 HTTP Basic 验证
|
||||
):
|
||||
if not xiaomusic.did_exist(did):
|
||||
raise HTTPException(status_code=400, detail="Invalid did")
|
||||
|
||||
payload = {
|
||||
"did": did,
|
||||
"exp": time.time() + JWT_EXPIRE_SECONDS,
|
||||
"iat": time.time(),
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||
|
||||
return {
|
||||
"token": token,
|
||||
"expire_in": JWT_EXPIRE_SECONDS,
|
||||
}
|
||||
|
||||
|
||||
@app.websocket("/ws/playingmusic")
|
||||
async def ws_playingmusic(websocket: WebSocket):
|
||||
token = websocket.query_params.get("token")
|
||||
if not token:
|
||||
await websocket.close(code=1008, reason="Missing token")
|
||||
return
|
||||
|
||||
try:
|
||||
# 解码 JWT(自动校验签名 + 是否过期)
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
||||
did = payload.get("did")
|
||||
|
||||
if not did:
|
||||
await websocket.close(code=1008, reason="Invalid token")
|
||||
return
|
||||
|
||||
if not xiaomusic.did_exist(did):
|
||||
await websocket.close(code=1003, reason="Did not exist")
|
||||
return
|
||||
|
||||
await websocket.accept()
|
||||
|
||||
# 开始推送状态
|
||||
while True:
|
||||
is_playing = xiaomusic.isplaying(did)
|
||||
cur_music = xiaomusic.playingmusic(did)
|
||||
cur_playlist = xiaomusic.get_cur_play_list(did)
|
||||
offset, duration = xiaomusic.get_offset_duration(did)
|
||||
|
||||
await websocket.send_text(
|
||||
json.dumps(
|
||||
{
|
||||
"ret": "OK",
|
||||
"is_playing": is_playing,
|
||||
"cur_music": cur_music,
|
||||
"cur_playlist": cur_playlist,
|
||||
"offset": offset,
|
||||
"duration": duration,
|
||||
}
|
||||
)
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
except jwt.ExpiredSignatureError:
|
||||
await websocket.close(code=1008, reason="Token expired")
|
||||
except jwt.InvalidTokenError:
|
||||
await websocket.close(code=1008, reason="Invalid token")
|
||||
except WebSocketDisconnect:
|
||||
print(f"WebSocket disconnected: {did}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
await websocket.close()
|
||||
|
||||
119
xiaomusic/static/default/md.js
vendored
119
xiaomusic/static/default/md.js
vendored
@@ -332,11 +332,10 @@ function refresh_music_list() {
|
||||
searchInput.value = oriValue;
|
||||
searchInput.dispatchEvent(inputEvent);
|
||||
searchInput.placeholder = oriPlaceHolder;
|
||||
// 每3秒获取下正在播放的音乐
|
||||
get_playing_music();
|
||||
setInterval(() => {
|
||||
get_playing_music();
|
||||
}, 3000);
|
||||
// 获取下正在播放的音乐
|
||||
if (did != "web_device") {
|
||||
connectWebSocket(did);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -579,47 +578,6 @@ function handleSearch() {
|
||||
|
||||
handleSearch();
|
||||
|
||||
function get_playing_music() {
|
||||
$.get(`/playingmusic?did=${did}`, function (data, status) {
|
||||
console.log(data);
|
||||
if (data.ret == "OK") {
|
||||
if (data.is_playing) {
|
||||
$("#playering-music").text(`【播放中】 ${data.cur_music}`);
|
||||
isPlaying = true;
|
||||
} else {
|
||||
$("#playering-music").text(`【空闲中】 ${data.cur_music}`);
|
||||
isPlaying = false;
|
||||
}
|
||||
offset = data.offset;
|
||||
duration = data.duration;
|
||||
//检查歌曲是否在收藏中,如果是,设置收藏按钮为选中状态
|
||||
console.log(
|
||||
"%cmd.js:614 object",
|
||||
"color: #007acc;",
|
||||
favoritelist.includes(data.cur_music)
|
||||
);
|
||||
if (favoritelist.includes(data.cur_music)) {
|
||||
$(".favorite").addClass("favorite-active");
|
||||
} else {
|
||||
$(".favorite").removeClass("favorite-active");
|
||||
}
|
||||
localStorage.setItem("cur_music", data.cur_music);
|
||||
}
|
||||
});
|
||||
}
|
||||
setInterval(() => {
|
||||
if (duration > 0) {
|
||||
if (isPlaying) {
|
||||
offset++;
|
||||
$("#progress").val((offset / duration) * 100);
|
||||
$("#current-time").text(formatTime(offset));
|
||||
}
|
||||
$("#duration").text(formatTime(duration));
|
||||
} else {
|
||||
$("#current-time").text(formatTime(0));
|
||||
$("#duration").text(formatTime(0));
|
||||
}
|
||||
}, 1000);
|
||||
function formatTime(seconds) {
|
||||
var minutes = Math.floor(seconds / 60);
|
||||
var remainingSeconds = Math.floor(seconds % 60);
|
||||
@@ -735,3 +693,72 @@ function confirmSearch() {
|
||||
toggleSearch();
|
||||
}
|
||||
|
||||
|
||||
let ws = null;
|
||||
// 启动 WebSocket 连接
|
||||
function connectWebSocket(did) {
|
||||
fetch(`/generate_ws_token?did=${did}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const token = data.token;
|
||||
startWebSocket(did, token);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("获取 token 失败:", err);
|
||||
setTimeout(() => connectWebSocket(did), 5000);
|
||||
});
|
||||
}
|
||||
|
||||
function startWebSocket(did, token) {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const wsUrl = `${protocol}://${window.location.host}/ws/playingmusic?token=${token}`;
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.ret !== "OK") return;
|
||||
|
||||
isPlaying = data.is_playing;
|
||||
let cur_music = data.cur_music || "";
|
||||
|
||||
$("#playering-music").text(
|
||||
isPlaying ? `【播放中】 ${cur_music}` : `【空闲中】 ${cur_music}`
|
||||
);
|
||||
|
||||
offset = data.offset || 0;
|
||||
duration = data.duration || 0;
|
||||
|
||||
if (favoritelist.includes(cur_music)) {
|
||||
$(".favorite").addClass("favorite-active");
|
||||
} else {
|
||||
$(".favorite").removeClass("favorite-active");
|
||||
}
|
||||
|
||||
localStorage.setItem("cur_music", cur_music);
|
||||
updateProgressUI();
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log("WebSocket 已断开,正在重连...");
|
||||
setTimeout(() => startWebSocket(did, token), 3000);
|
||||
};
|
||||
|
||||
ws.onerror = (err) => console.error("WebSocket 错误:", err);
|
||||
}
|
||||
|
||||
// 每秒更新播放进度
|
||||
function updateProgressUI() {
|
||||
const progressPercent = duration > 0 ? (offset / duration) * 100 : 0;
|
||||
$("#progress").val(progressPercent);
|
||||
$("#current-time").text(formatTime(offset));
|
||||
$("#duration").text(formatTime(duration));
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
if (duration > 0 && isPlaying) {
|
||||
offset++;
|
||||
if (offset > duration) offset = duration;
|
||||
updateProgressUI();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user