Skip to main content

phemios_core/playback/
removal.rs

1//! Single-occurrence edits retain slot identities while a different occurrence is audible.
2use super::*;
3pub(super) enum Outcome {
4    Keep,
5    Advance(usize),
6    Stop,
7}
8/// Apply only to the retained queue. Removed slots cannot be restored by ordinary reordering.
9pub(super) fn edit(state: &mut State, occurrence: usize, paused: bool) -> Result<Outcome, String> {
10    let at = state
11        .queue
12        .iter()
13        .position(|index| *index == occurrence)
14        .ok_or("La liste de lecture a changé.")?;
15    let current = state.index == occurrence;
16    let next = state.queue.get(at + 1).copied();
17    state.queue.remove(at);
18    state.tracks[occurrence] = String::new();
19    if state.queue.is_empty() {
20        state.clear_selection();
21        return Ok(Outcome::Stop);
22    }
23    if !current {
24        return Ok(Outcome::Keep);
25    }
26    state.position = 0;
27    state.duration = None;
28    state.confirmed = false;
29    if let Some(next) = next {
30        state.index = next;
31        state.reference = state.tracks[next].clone();
32        state.status = if paused { "paused" } else { "connecting" };
33        Ok(Outcome::Advance(next))
34    } else {
35        state.status = "idle";
36        state.reference.clear();
37        state.index = state.queue[0];
38        Ok(Outcome::Stop)
39    }
40}
41/// Reserve both voices before producing overlap PCM, atomically against occurrence edits.
42pub(super) fn reserve_mix(
43    state: &Mutex<State>,
44    current: usize,
45    next: usize,
46) -> Result<bool, String> {
47    let mut state = state.lock().map_err(|_| "État audio indisponible.")?;
48    if !state.queue.contains(&next) {
49        return Ok(false);
50    }
51    state.protected = vec![current, next];
52    Ok(true)
53}
54impl Player {
55    /// Remove one captured occurrence, never all copies of the same track reference.
56    pub fn remove(&self, id: u64, occurrence: usize) -> Result<(), String> {
57        let worker = self.worker.lock().map_err(|_| "Lecteur indisponible.")?;
58        let worker = worker
59            .as_ref()
60            .filter(|worker| !worker.handle.is_finished())
61            .ok_or("Aucune lecture active.")?;
62        let mut state = self.state.lock().map_err(|_| "État audio indisponible.")?;
63        if state.queue_id != id
64            || occurrence >= state.tracks.len()
65            || !state.queue.contains(&occurrence)
66        {
67            return Err("La liste de lecture a changé.".into());
68        }
69        if occurrence != state.index && state.protected.contains(&occurrence) {
70            return Err(
71                "Ce titre participe au fondu en cours. Réessayez à la fin du fondu.".into(),
72            );
73        }
74        if occurrence != state.index {
75            edit(&mut state, occurrence, false)?;
76            return Ok(());
77        }
78        let (done, wait) = mpsc::sync_channel(1);
79        worker
80            .commands
81            .try_send(Command::Remove(occurrence, done))
82            .map_err(|_| "Lecteur occupé, réessayez.")?;
83        self.signals.paused.store(true, Ordering::Relaxed);
84        drop(state);
85        wait.recv_timeout(std::time::Duration::from_secs(15))
86            .map_err(|_| "Retrait de piste interrompu.")?
87    }
88}
89#[cfg(test)]
90#[path = "removal_tests.rs"]
91mod tests;