mineral-server 0.5.10

Mineral 后台 server:收纳 audio engine + task scheduler + channels,对外发 ClientHandle
//! 队列编排与播放控制:替换 / 插入 / 追加队列、播放模式切换、上 / 下一首。

use mineral_channel_core::ChannelCaps;
use mineral_model::{Song, SourceKind};
use mineral_protocol::{PlayCursor, PlayMode, PlayQueueError, QueueEditOutcome, QueueOp};
use rand::seq::SliceRandom;

use super::PlayerCore;
use crate::queue::{advance_next, advance_prev, apply_play_mode};

impl PlayerCore {
    /// 原子校验并替换 queue,按 request-local index 选择 exact target occurrence。
    ///
    /// # Params:
    ///   - `new_queue`: 本次请求提交的新队列
    ///   - `target`: `new_queue` 内的 0-based queue index
    ///   - `context`: 队列语境(埋点 provenance:随该队列每个起播继承)
    ///
    /// # Return:
    ///   成功返回应立即起播的 target Song;invalid request 返回 structured error,状态不变。
    pub fn replace_queue(
        &self,
        mut new_queue: Vec<Song>,
        target: usize,
        context: mineral_stats::QueueContext,
    ) -> Result<Song, PlayQueueError> {
        let len = new_queue.len();
        if len == 0 {
            return Err(PlayQueueError::Empty);
        }
        if len > crate::queue::QUEUE_CAP {
            return Err(PlayQueueError::CapacityExceeded {
                len,
                cap: crate::queue::QUEUE_CAP,
            });
        }
        let Some(target_song) = new_queue.get(target).cloned() else {
            return Err(PlayQueueError::TargetOutOfBounds { target, len });
        };
        {
            let mut st = self.inner.state.lock();
            mineral_log::info!(
                target: "player",
                len = new_queue.len(),
                target,
                target_id = target_song.id.as_str(),
                mode = ?st.play_mode,
                "replace queue"
            );
            st.queue_context = context;
            // 换队列 = 旧队列的 per-song 语境覆盖全部作废:未播插队曲的条目不清会永久
            // 残留(泄漏),且同 id 歌在新队列起播时会误继承陈旧语境、污染归属统计。
            st.context_overrides.clear();
            if matches!(st.play_mode, PlayMode::Shuffle) {
                let original_queue = new_queue.clone();
                let selected = new_queue.remove(target);
                new_queue.shuffle(&mut rand::rng());
                new_queue.insert(0, selected);
                st.original_queue = Some(original_queue);
                st.queue = new_queue;
                st.cursor = PlayCursor::InQueue(0);
            } else {
                st.queue = new_queue;
                st.cursor = PlayCursor::InQueue(target);
                st.original_queue = None;
            }
            // 换队列后已预排的下一曲可能不再是新队列的 next:作废,让 check_prefetch 按新队列重排。
            st.invalidate_prefetch();
            st.bump_queue();
        }
        // 取消引擎里尚未 append 的待建预排(已 append 的无法摘除,会自然播完后由边界兜底)。
        self.inner.audio.clear_next();
        self.spawn_save_session();
        Ok(target_song)
    }

    /// 将整组歌曲保序插到当前曲之后,保留重复项;队列级 context、游标与当前曲不变。
    /// 悬空时插在接续点前,空队列从头插入。
    ///
    /// # Params:
    ///   - `songs`: 本次插播的整组歌曲
    ///   - `context`: 整组的来源语境,按歌曲 ID 写入起播时消费的语境覆盖
    ///
    /// # Return:
    ///   实际入队返回 `true`;空批次或容量不足返回 `false`,不改状态、预取或会话。
    pub fn queue_insert_next(
        &self,
        songs: Vec<Song>,
        context: mineral_stats::QueueContext,
    ) -> bool {
        let snapshot = {
            let mut st = self.inner.state.lock();
            if !crate::queue::insert_next(&mut st, &songs) {
                return false;
            }
            let contexts = std::iter::repeat_n(context, songs.len());
            for (song, context) in songs.into_iter().zip(contexts) {
                st.context_overrides.insert(song.id.qualified(), context);
            }
            // 整组入队后统一作废 gapless 预排,让 check_prefetch 按新队列重排。
            st.invalidate_prefetch();
            self.snapshot_session_with_state(&st)
        };
        self.inner.audio.clear_next();
        self.spawn_save_session_snapshot(snapshot);
        true
    }

    /// 将整组歌曲保序追加到队尾,保留重复项;队列级 context、游标与当前曲不变。
    /// 当前曲在尾部时下一首会改变,因此入队后统一作废预排。
    ///
    /// # Params:
    ///   - `songs`: 本次追加的整组歌曲
    ///   - `context`: 整组的来源语境,按歌曲 ID 写入起播时消费的语境覆盖
    ///
    /// # Return:
    ///   实际入队返回 `true`;空批次或容量不足返回 `false`,不改状态、预取或会话。
    pub fn queue_append(&self, songs: Vec<Song>, context: mineral_stats::QueueContext) -> bool {
        let snapshot = {
            let mut st = self.inner.state.lock();
            if !crate::queue::append(&mut st, &songs) {
                return false;
            }
            let contexts = std::iter::repeat_n(context, songs.len());
            for (song, context) in songs.into_iter().zip(contexts) {
                st.context_overrides.insert(song.id.qualified(), context);
            }
            st.invalidate_prefetch();
            self.snapshot_session_with_state(&st)
        };
        self.inner.audio.clear_next();
        self.spawn_save_session_snapshot(snapshot);
        true
    }

    /// 队列结构编辑:删除 / 重排 / 批量清理 / 撤销。
    ///
    /// [`QueueOp::ApplyTransform`] 不走这里——它要跨线程跑脚本,由调用方拿到新序后走
    /// [`Self::queue_reorder`]。
    ///
    /// # Params:
    ///   - `op`: 待执行的操作
    ///
    /// # Return:
    ///   本次编辑的结果。
    pub fn queue_edit(&self, op: &QueueOp) -> QueueEditOutcome {
        self.finish_edit(|st| crate::queue::apply(st, op))
    }

    /// 按给定 id 序列整表重排队列(脚本变换 / `mineral.queue.set` 的落地口)。
    ///
    /// # Params:
    ///   - `ids`: 新的队列顺序;每个 id 必须在原队列里出现过,次数不限
    ///
    /// # Return:
    ///   本次编辑的结果;含未知 id 时整体拒绝。
    pub fn queue_reorder(&self, ids: &[mineral_model::SongId]) -> QueueEditOutcome {
        self.finish_edit(|st| crate::queue::apply_order(st, ids))
    }

    /// 编辑落地后的共同收尾:精确作废预排、取消引擎待建预排、存会话。
    fn finish_edit(
        &self,
        edit: impl FnOnce(&mut crate::state::State) -> QueueEditOutcome,
    ) -> QueueEditOutcome {
        let (outcome, prefetch_dropped) = {
            let mut st = self.inner.state.lock();
            let outcome = edit(&mut st);
            let dropped =
                matches!(outcome, QueueEditOutcome::Applied) && st.revalidate_prefetch_after_edit();
            (outcome, dropped)
        };
        if prefetch_dropped {
            self.inner.audio.clear_next();
        }
        if matches!(outcome, QueueEditOutcome::Applied) {
            self.spawn_save_session();
        }
        outcome
    }

    /// 全部已注册 channel 的能力声明(按注册顺序)。
    pub fn channel_caps(&self) -> Vec<(SourceKind, ChannelCaps)> {
        self.inner
            .channels
            .iter()
            .map(|ch| (ch.source(), ch.caps()))
            .collect()
    }

    /// `m` 键:PlayMode cycle + 进/退 Shuffle 边界处洗牌或还原(+ 记 mode_changes)。
    ///
    /// # Params:
    ///   - `actor`: 发起方
    pub fn cycle_play_mode(&self, actor: mineral_stats::Actor) {
        let (from, to) = {
            let mut st = self.inner.state.lock();
            let from = st.play_mode;
            let new = st.play_mode.cycle();
            apply_play_mode(&mut st, new);
            (from, st.play_mode)
        };
        self.record_mode_change(actor, from, to);
        self.spawn_save_session();
    }

    /// 直接设目标 PlayMode(系统媒体控件按维度写 Shuffle/LoopStatus 后塌缩成的档,
    /// 脚本 set_mode 同入口;+ 记 mode_changes,设成同值不记)。
    ///
    /// # Params:
    ///   - `mode`: 目标模式
    ///   - `actor`: 发起方
    pub fn set_play_mode(&self, mode: PlayMode, actor: mineral_stats::Actor) {
        let (from, to) = {
            let mut st = self.inner.state.lock();
            let from = st.play_mode;
            apply_play_mode(&mut st, mode);
            (from, st.play_mode)
        };
        self.record_mode_change(actor, from, to);
        self.spawn_save_session();
    }

    /// 模式切换的埋点出口(cycle / 直设共用;无变化不记)。
    fn record_mode_change(&self, actor: mineral_stats::Actor, from: PlayMode, to: PlayMode) {
        if from == to {
            return;
        }
        self.record_behavior(
            actor,
            mineral_stats::BehaviorEvent::ModeChange {
                from_mode: crate::stats_play_mode(from),
                to_mode: crate::stats_play_mode(to),
            },
        );
    }

    /// 启动时恢复上次会话的播放模式:只写模式标志,不走 [`Self::set_play_mode`] 的
    /// 洗牌/还原边界(此刻队列为空,无可洗),也不回写会话(快照其余字段原样)。
    ///
    /// # Params:
    ///   - `mode`: 上次会话解析出的播放模式
    pub fn restore_play_mode(&self, mode: PlayMode) {
        let mut st = self.inner.state.lock();
        st.play_mode = mode;
    }

    /// `p` 键:进度 > 阈值 → seek(0);否则跳上一首。
    ///
    /// # Params:
    ///   - `actor`: 发起方(用户按键 / 脚本 / 媒体键;origin 恒 AutoAdvance,两维独立)
    pub fn prev_or_restart(&self, actor: mineral_stats::Actor) {
        let pos = self.inner.audio.snapshot().position_ms;
        let (old, prev) = {
            let mut st = self.inner.state.lock();
            if st.current_song.is_none() {
                return;
            }
            if pos > self.inner.prev_restart_threshold_ms {
                drop(st);
                // 回开头不算切歌/跳过,不打点。
                self.inner.audio.seek(0);
                return;
            }
            // advance_prev 把 queue_sel 钉到上一首下标,play_song 据守卫保留它(见其内注释)。
            (st.current_song.clone(), advance_prev(&mut st))
        };
        if let Some(s) = prev {
            if let Some(old) = old {
                self.spawn_on_played(old.id.clone(), mineral_stats::FinishReason::Skip, pos);
                self.inner
                    .notify
                    .track_finished(&old, mineral_protocol::FinishReason::Skip);
            }
            self.play_song(&s, mineral_stats::PlayOrigin::AutoAdvance, actor);
        }
    }

    /// `n` 键:按 PlayMode 切下一首。
    ///
    /// # Params:
    ///   - `actor`: 发起方(用户按键 / 脚本 / 媒体键;origin 恒 AutoAdvance,两维独立)
    pub fn next_song(&self, actor: mineral_stats::Actor) {
        let position_ms = self.inner.audio.snapshot().position_ms;
        let (old, next) = {
            let mut st = self.inner.state.lock();
            // advance_next 把 queue_sel 钉到下一首下标,play_song 据守卫保留它(见其内注释)。
            (st.current_song.clone(), advance_next(&mut st))
        };
        if let Some(s) = next {
            if let Some(old) = old {
                self.spawn_on_played(
                    old.id.clone(),
                    mineral_stats::FinishReason::Skip,
                    position_ms,
                );
                self.inner
                    .notify
                    .track_finished(&old, mineral_protocol::FinishReason::Skip);
            }
            self.play_song(&s, mineral_stats::PlayOrigin::AutoAdvance, actor);
        }
    }
}