1
0
mirror of https://github.com/hanxi/xiaomusic.git synced 2025-12-06 14:52:50 +08:00

feat: 新增 websocket 接口获取当前播放状态

This commit is contained in:
涵曦
2025-09-09 19:55:45 +08:00
parent fcfff7c090
commit a36709e838
3 changed files with 160 additions and 47 deletions

View File

@@ -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"

View File

@@ -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()

View File

@@ -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);