diff --git a/backend/app/storage.py b/backend/app/storage.py index f6271cf..cf3dc6f 100644 --- a/backend/app/storage.py +++ b/backend/app/storage.py @@ -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): diff --git a/src/pages/MediaScreen.jsx b/src/pages/MediaScreen.jsx index 24d2741..a561a2e 100644 --- a/src/pages/MediaScreen.jsx +++ b/src/pages/MediaScreen.jsx @@ -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) => { diff --git a/src/utils/useMqttControl.js b/src/utils/useMqttControl.js index af007b6..de0dc43 100644 --- a/src/utils/useMqttControl.js +++ b/src/utils/useMqttControl.js @@ -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':