DECKYMUSIC
开发文档Docs › 播放队列语义Queue semantics

Steam Deck 音乐插件:播放队列(Queue)行为设计规范

本规范定义 decky-music 在不同播放入口下的队列语义。队列真相源由 Python bridge 维护,前端 UI 只触发动作和展示状态。

1. 核心队列模式

队列分为两种互斥模式:普通列表模式(Normal Queue) 与 电台流模式(Radio Stream)。

1.1 普通列表模式

  • 特征:有限长度静态列表,如歌单、专辑、搜索结果。
  • 控制:用户可查看完整队列,可手动上一首 / 下一首。
  • 循环策略:支持单曲循环、列表循环、随机播放。
  • 持久化:随 settings.json 落盘,重启 Steam Deck 后可恢复。

普通队列持久化结构为 "queue": {"items": [{id, media_mid, name, singer, cover, duration}], "index": 3}。存 id 类字段(歌曲 id + QQ 解析 URL 所需的 media_mid)+ 展示字段(恢复后队列浮层 / 状态徽章直接显示真实信息),键白名单写入,绝不存解析出的播放 URL(限时 vkey);播放时按 id 重新向 provider 解析。写入必须走临时文件 + os.replace() 原子替换。

1.2 电台流模式

  • 特征:动态、无限或分批拉取的歌曲流,如 NCM 私人 FM、QQ 猜你喜欢、QQ 雷达推荐。
  • 无上一首:电台只能往前走,不支持回退。
  • 无播放模式切换:单曲循环或随机在电台模式下无意义。
  • 动态补水:当当前批次播放到倒数第二首时,bridge 自动向 provider 请求下一批并追加到内存队列。
  • UI 表现:电台沉浸页隐藏上一首和播放模式;队列浮层不展示未来曲目,只显示“正在收听电台”与退出入口。

2. 播放触发场景与队列策略

触发场景 用户操作 队列行为策略 模式
歌单 / 专辑 / 排行榜 播放全部 全量替换:清空当前队列,写入完整列表,从第 1 首播放。 普通列表
歌单 / 专辑 / 排行榜 点击列表中某首单曲 上下文替换:写入该单曲所在列表,索引定位到该单曲。 普通列表
搜索结果页 点击某首单曲 全量替换:写入当前页搜索结果,从选中曲目播放。 普通列表
任意列表单曲 X -> 下一首播放 插队追加:插入到当前播放索引之后。 普通列表
任意列表单曲 X -> 添加到队列末尾 尾部追加:追加到队列最后。 普通列表
智能电台 / 私人 FM 点击电台入口 模式覆写:清空普通队列,切换电台流,拉第一批并开播。 电台流

2.1 模式切换互斥规则

  • 普通列表 -> 电台流:原普通列表被抛弃,进入电台流。
  • 电台流 -> 普通列表:电台流被丢弃,重新建立静态列表。
  • 跨 provider 切换:QQ 与 NCM Song ID 体系不通用,切 provider 时队列强制清空并停止播放。
  • 登录态不随切换清除:settings.json 中 accounts 按 provider 分 key 存储。切换只决定 spawn provider 时注入哪份 cookie。

3. Bridge 队列控制

bridge 必须处理 player 的 ended 事件以决定下一首。协议 v1 事件形态为 {"ev":"player","type":"ended","data":{}},即 protocol.ChildEvent(ev="player", type="ended")。

自动切歌逻辑:

  1. 单曲循环:seek(0) 重新播放当前索引。
  2. 普通列表:索引 +1。到末尾时:
    • 列表循环:索引重置为 0。
    • 随机播放:从随机洗牌后的内部索引取下一首。
  3. 电台流:索引 +1。若当前已是内存批次倒数第二首,异步调用 provider 对应接口补水。

3.1 队列为空

  • 触发:初次安装、跨 provider 切换、用户清空队列。
  • 表现:Tab 状态徽章显示空态;正在播放页显示“暂无播放”;进度和切歌控件禁用。
  • 拦截:此时 Start 盲操无效,Footer Legend 不展示“播放 / 暂停”为可执行动作。
  • 取消语义(#57):清空、空 play_queue、空电台及移除最后一曲,在首个 await 前作废旧播放代次并清除已加载/恢复状态。 旧 song_url、按需启动或写锁等待结束后都必须复核代次;过期 load/seek/meta/stop 不得继续写入。 已写出的旧 load 由后续 stop/new load 的 player 代次失效,不能在旧响应返回时无条件 stop 新歌。 player 用同一代次锁保护后台 load 的检查及音频入队,与 stop/new load 的代次更新互斥;不跨 HTTP await 持锁。
  • 迟到事件:空队列不能被旧 playing/paused/unloaded/ended 或流死亡事件复活;有效非空队列的流错误仍须进入恢复路径。

3.2 读停摆不是正常结束

流尚未读完、缓冲为空且持续等不到数据时,player 在原有 30 秒读等待上限内判定停摆。 锁内重新检查最终补货、已有错误和 EOF 后才记录失败,并失效旧 producer 的结果;正常 EOF 仍先排空缓冲。 音频层通过 failure probe 发 fetch_failed,不发 ended,因此不会自动跳到下一首。 bridge 保留当前曲与 _resume_at,resume() 重载后尝试 seek 回断点,seek 失败才按既有规则从头播放。

4. UI 队列浮层

普通大屏页面按 Y 打开队列浮层。QQ 智能电台、NCM 私人 FM 等专属沉浸页可按各自 UI 规格覆盖 Y 键,队列入口改由页面按钮或不展示未来队列。

浮层是右侧临时面板,不是常驻侧栏,也不是居中确认框。保留 Decky showModal/ModalRoot 的模态焦点与返回机制;近黑背景、小圆角、轻遮罩,顶部/底部避开系统状态区和 Footer Legend。 宽度随视口约束,窄屏可占满可用内容宽度,不按 Steam Deck 的物理分辨率写死尺寸。

普通模式下:

  • 所有歌曲均可导航;按可见区域窗口化渲染,不再固定截取“当前曲前后各 50 首”。
  • 打开时定位并聚焦当前曲;后续曲目变化不抢走用户正在浏览的焦点。
  • 当前曲用 Steam 蓝细标记与播放/暂停图标;手柄焦点独立使用白色细描边和灰色渐变。
  • A 播放聚焦歌曲且保持浮层打开,X 直接移除该曲;标题区提供低强调的“清空”操作。
  • 移除后保留该位置的焦点,末行移除回到前一行;清空后显示空态并聚焦返回入口。

电台模式下:

  • 不展示未来曲目。
  • 当前单曲卡片仅展示信息,不可聚焦,也不显示无效的 A 选择 / X 移除 提示。
  • 标题为“正在收听电台”,辅以“后续歌曲由电台自动推荐”说明。
  • “退出电台”放在次级操作区,执行后清空并关闭浮层;默认焦点在安全的返回按钮上。
  • bridge 对电台模式下的队列编辑 callable(播放队列索引 / 插队 / 追加 / 移除)一律 no-op,避免 UI 快照索引映射到被隐藏的历史 / 未来曲目;只有“清空队列”会退出电台。

读取失败或快照畸形时显示局部错误和重试入口,不把未知字段直接渲染成 React 节点。 Footer Legend 始终由系统渲染;B 关闭后恢复原焦点,原节点消失时由原生导航回退到可用入口。

5. 持久化总览

状态 是否持久化 说明
普通队列(ids + index) 是 随 settings.json 落盘;只存 id,URL 重新解析。
电台流内容 否 动态推荐流存了无意义;只记 queue_mode: "radio" 与电台类型。
播放模式 / 音量 是 bridge 统一持有。
登录态(双 provider cookie) 是 按 provider 分 key,切换 provider 不清除,文件 chmod 0600。
跨 provider 切换后的队列 清空 Song ID 体系互不通用,无保留意义。

在 GitHub 上查看 / 编辑此文档View or edit this page on GitHub ↗