fix: 修复大屏播放控制链路

前端:
- useMqttControl 指令分发补上 play_target(此前被静默丢弃,立即播放无法触发大屏跳转)
- MediaScreen 重构 playTarget:pendingTargetRef 统一刷新播放列表路径,
  消除与 playlist_changed 重载的竞态(避免跳转被旧索引覆盖)

后端:
- storage.add_to_playlist 自动识别 URL 来源(source=url),
  修复 URL 媒体加入播放列表后被当本地文件过滤、永远无法播放的问题
- _load 时规范化旧数据(source=None 的 http 条目自动补 url)
This commit is contained in:
Pine
2026-08-17 21:59:13 +08:00
parent c55835fb7d
commit 7bb0e9024a
3 changed files with 26 additions and 17 deletions
+10 -2
View File
@@ -30,7 +30,13 @@ class Storage:
def _load(self):
if self.data_file.exists():
try:
return json.loads(self.data_file.read_text("utf-8"))
data = json.loads(self.data_file.read_text("utf-8"))
# 规范化:URL 来源自动标记(兼容旧数据 source=None 的 URL 条目)
for it in data.setdefault("playlist", []):
p = it.get("path", "")
if p.startswith(("http://", "https://")) and not it.get("source"):
it["source"] = "url"
return data
except Exception:
pass
return {"settings": dict(DEFAULT_SETTINGS), "playlist": [], "url_media": []}
@@ -67,7 +73,9 @@ class Storage:
with self._lock:
items = self._data.setdefault("playlist", [])
if not any(it.get("path") == path for it in items):
items.append({"path": path, "source": None, "name": None})
# URL 来源自动标记,避免被当作本地文件过滤
source = "url" if path.startswith(("http://", "https://")) else None
items.append({"path": path, "source": source, "name": None})
self._save()
def remove_from_playlist(self, path):
+15 -15
View File
@@ -228,6 +228,8 @@ export default function MediaScreen() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [skip]);
const pendingTargetRef = useRef(null);
const reloadPlaylist = useCallback(async () => {
try {
const data = await api.getPlaylist();
@@ -246,7 +248,12 @@ export default function MediaScreen() {
setLoaded(true);
let newIndex = -1;
if (currentItem) {
const target = pendingTargetRef.current;
if (target) {
// 指定播放优先:跳转到目标媒体(一次性)
pendingTargetRef.current = null;
newIndex = newList.findIndex(item => item.relative_path === target);
} else if (currentItem) {
newIndex = newList.findIndex(item => item.relative_path === currentItem.relative_path);
}
if (newIndex >= 0) {
@@ -259,6 +266,13 @@ export default function MediaScreen() {
} catch {/* ignore */}
}, []);
// 指定媒体立即播放:先标记目标,再刷新播放列表(统一路径,避免竞态)
const playTarget = useCallback(async (path) => {
pendingTargetRef.current = path;
await reloadPlaylist();
setPaused(false);
}, [reloadPlaylist]);
const applySettings = useCallback((msg) => {
if (msg.volume !== undefined) {
const vol = msg.volume / 100;
@@ -295,20 +309,6 @@ export default function MediaScreen() {
}
}, []);
// 指定媒体立即播放:刷新播放列表并跳转到目标项
const playTarget = useCallback(async (path) => {
try {
const data = await api.getPlaylist();
const newList = data.files || [];
if (newList.length === 0) return;
setList(newList);
setLoaded(true);
const idx = newList.findIndex((it) => it.relative_path === path);
if (idx >= 0) setIndex(idx);
setPaused(false);
} catch { /* ignore */ }
}, []);
// MQTT 控制通道(后端 opc/display/command → window 事件)
useEffect(() => {
const handler = (e) => {
+1
View File
@@ -81,6 +81,7 @@ export function useMqttControl() {
case 'next':
case 'prev':
case 'set_mode':
case 'play_target':
case 'minimize':
case 'settings_changed':
case 'playlist_changed':