Skip to main content

koan_core/player/
state.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::path::PathBuf;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering};
6
7use uuid::Uuid;
8
9use crate::remote::downloads::{ByteFeed, DownloadStore};
10
11/// Stable identity for a queue entry. UUIDv7 — time-ordered, unique across duplicates.
12#[derive(Clone, Copy, PartialEq, Eq, Hash)]
13pub struct QueueItemId(pub Uuid);
14
15impl QueueItemId {
16    pub fn new() -> Self {
17        Self(Uuid::now_v7())
18    }
19}
20
21impl Default for QueueItemId {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl fmt::Debug for QueueItemId {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        // The tail: a v7's leading hex is its timestamp, shared by a whole batch.
30        let hex = self.0.simple().to_string();
31        write!(f, "QId({})", &hex[hex.len() - 8..])
32    }
33}
34
35/// A step the decoder took through the queue from `after`, to `next`: the
36/// item the play mode says follows it, or the end of the queue. The decoder
37/// queues `next` as `chosen` when it is Ready, and stops at it when it is
38/// still arriving.
39#[derive(Debug, Clone)]
40pub struct Lookahead {
41    pub after: QueueItemId,
42    pub next: Option<QueueItemId>,
43    pub chosen: Option<(QueueItemId, PathBuf)>,
44    /// `next` was found by going back to the top of the queue, which only
45    /// repeating the queue does.
46    pub wrapped: bool,
47    /// The timeline boundary `chosen` opens as, if it opens: the session's
48    /// boundary count when the step was taken. A step whose file fails to
49    /// open leaves no boundary, and the decoder steps again for the same one,
50    /// so this rather than a step's index is what places it against the
51    /// playhead. Set by the decoder's cursor; 0 elsewhere.
52    pub boundary: usize,
53    /// The pass of the queue `after` is in, and the one `next` is in: one
54    /// more once a step has gone round. A row can be in this pass and the
55    /// next at once, so its id alone cannot say which a step is from.
56    pub after_pass: u64,
57    pub pass: u64,
58}
59
60/// What follows a track once it ends, beyond the queue's own order.
61#[derive(
62    Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
63)]
64#[serde(rename_all = "lowercase")]
65pub enum Repeat {
66    /// The queue ends at its last item.
67    #[default]
68    Off,
69    /// The last item runs on into the first.
70    Queue,
71    /// The item plays again. An explicit next or previous still moves on.
72    One,
73}
74
75impl Repeat {
76    pub fn is_off(&self) -> bool {
77        *self == Repeat::Off
78    }
79
80    pub fn as_str(self) -> &'static str {
81        match self {
82            Repeat::Off => "off",
83            Repeat::Queue => "queue",
84            Repeat::One => "one",
85        }
86    }
87
88    pub fn parse(s: &str) -> Option<Self> {
89        match s {
90            "off" => Some(Repeat::Off),
91            "queue" => Some(Repeat::Queue),
92            "one" => Some(Repeat::One),
93            _ => None,
94        }
95    }
96
97    /// The next mode a single repeat button steps to: off, the queue, one.
98    pub fn cycled(self) -> Self {
99        match self {
100            Repeat::Off => Repeat::Queue,
101            Repeat::Queue => Repeat::One,
102            Repeat::One => Repeat::Off,
103        }
104    }
105}
106
107/// The transport's play mode. Shuffle never moves the queue: it plays the
108/// queue in an order kept beside it (`PlayOrder`), and repeat says what
109/// follows the last track of that order or of the queue.
110#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
111pub struct PlayMode {
112    pub shuffle: bool,
113    pub repeat: Repeat,
114}
115
116impl PlayMode {
117    fn to_bits(self) -> u8 {
118        let repeat = match self.repeat {
119            Repeat::Off => 0,
120            Repeat::Queue => 1,
121            Repeat::One => 2,
122        };
123        repeat << 1 | self.shuffle as u8
124    }
125
126    fn from_bits(bits: u8) -> Self {
127        Self {
128            shuffle: bits & 1 != 0,
129            repeat: match bits >> 1 {
130                1 => Repeat::Queue,
131                2 => Repeat::One,
132                _ => Repeat::Off,
133            },
134        }
135    }
136}
137
138/// What replacing the queue does to the play mode. Something played from
139/// its play button — a record, an artist, a playlist, a selection — starts
140/// as asked, in order or shuffled, with repeat off, whatever the modes were.
141/// A queue restored, synced or handed over keeps them.
142#[derive(
143    Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
144)]
145#[serde(rename_all = "camelCase")]
146pub enum QueueMode {
147    #[default]
148    Keep,
149    InOrder,
150    Shuffled,
151}
152
153impl QueueMode {
154    pub fn is_keep(&self) -> bool {
155        *self == QueueMode::Keep
156    }
157
158    /// The mode a queue started this way plays in, or `None` to keep it.
159    pub fn play_mode(self) -> Option<PlayMode> {
160        let shuffle = match self {
161            QueueMode::Keep => return None,
162            QueueMode::InOrder => false,
163            QueueMode::Shuffled => true,
164        };
165        Some(PlayMode {
166            shuffle,
167            repeat: Repeat::Off,
168        })
169    }
170}
171
172/// A sleep timer as asked for: stop after a while, or at the end of the
173/// track or record playing. It pauses, fading out, and leaves the queue as it
174/// was, so playing again carries on from there.
175#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
176#[serde(tag = "kind", rename_all = "camelCase")]
177pub enum SleepTimer {
178    After { minutes: u32 },
179    EndOfTrack,
180    EndOfRecord,
181}
182
183/// A sleep timer that is set, as clients show it. A time rather than what
184/// is left of one, so it says the same thing for as long as it stands.
185#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
186#[serde(tag = "kind", rename_all = "camelCase")]
187pub enum Sleep {
188    /// Milliseconds since the Unix epoch.
189    At {
190        unix_ms: u64,
191    },
192    EndOfTrack,
193    EndOfRecord,
194}
195
196/// What identifies a track for shuffle. Rows sharing one are one track, which
197/// a pass plays once: a queue holding a track twice does not give it two
198/// chances.
199#[derive(Clone, Copy, PartialEq, Eq, Hash)]
200enum TrackKey<'a> {
201    Library(i64),
202    File(&'a std::path::Path),
203}
204
205impl PlaylistItem {
206    fn key(&self) -> TrackKey<'_> {
207        match self.db_id {
208            Some(id) => TrackKey::Library(id),
209            None => TrackKey::File(&self.path),
210        }
211    }
212
213    fn playable(&self) -> bool {
214        !matches!(self.state, ItemState::Failed(_))
215    }
216}
217
218/// The order shuffle plays the queue in, held beside it and never shown as
219/// its order. Present while shuffle is on.
220///
221/// A pass plays every track in the queue once. `upcoming` is what is left of
222/// this one; it never holds a played row, the cursor's track, or two rows of
223/// one track. Rows queued while shuffle is on take random places in it, and
224/// rows removed leave it — `Playlist::reconcile` keeps it so after every
225/// edit. What plays next is `upcoming`'s first playable row, so the
226/// decoder's lookahead and the advance read one order and make one pick.
227#[derive(Debug, Clone, Default)]
228pub struct PlayOrder {
229    upcoming: Vec<QueueItemId>,
230    /// The order of the pass after this one, made when this one has nothing
231    /// left and the queue repeats. Kept rather than drawn at each look, so a
232    /// lookahead taken across the end of a pass is the pass that plays.
233    next_pass: Option<Vec<QueueItemId>>,
234    /// The rows the cursor has been on, oldest first: what Previous goes
235    /// back along.
236    history: Vec<QueueItemId>,
237}
238
239/// Put `fresh` into `order` at random places, leaving the order of what is
240/// there alone.
241fn scatter(order: &mut Vec<QueueItemId>, mut fresh: Vec<QueueItemId>) {
242    if fresh.is_empty() {
243        return;
244    }
245    crate::helpers::shuffle(&mut fresh);
246    let Some(mut rng) = crate::helpers::Rng::seeded() else {
247        order.extend(fresh);
248        return;
249    };
250    let old = std::mem::take(order);
251    let (mut left, mut right) = (old.len(), fresh.len());
252    let (mut old, mut fresh) = (old.into_iter(), fresh.into_iter());
253    order.reserve(left + right);
254    while left + right > 0 {
255        if rng.below(left + right) < left {
256            order.extend(old.next());
257            left -= 1;
258        } else {
259            order.extend(fresh.next());
260            right -= 1;
261        }
262    }
263}
264
265/// What follows `after` under `repeat`: `Some(None)` at the end of the queue,
266/// `None` when `after` is not in it — a removed item has nothing following it,
267/// whatever the mode, since wrapping from it would replay the queue from a
268/// place nobody is at. The flag says the step went back to the top.
269///
270/// Failed items are passed over. Repeating one item plays it again unless it
271/// has failed, in which case the queue carries on as it would when repeating
272/// the queue.
273fn follows(
274    items: &[PlaylistItem],
275    after: QueueItemId,
276    repeat: Repeat,
277) -> Option<(Option<&PlaylistItem>, bool)> {
278    let at = items.iter().position(|item| item.id == after)?;
279    let playable = |item: &&PlaylistItem| !matches!(item.state, ItemState::Failed(_));
280    if repeat == Repeat::One && playable(&&items[at]) {
281        return Some((Some(&items[at]), false));
282    }
283    if let Some(next) = items[at + 1..].iter().find(playable) {
284        return Some((Some(next), false));
285    }
286    if repeat == Repeat::Off {
287        return Some((None, false));
288    }
289    Some((items[..=at].iter().find(playable), true))
290}
291
292/// Set in `SharedPlayerState::state` beside the playback state while the
293/// player waits for a track.
294const WAITING: u8 = 0x80;
295
296/// Playback state.
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298#[repr(u8)]
299pub enum PlaybackState {
300    Stopped = 0,
301    Playing = 1,
302    Paused = 2,
303}
304
305impl PlaybackState {
306    pub fn from_u8(v: u8) -> Self {
307        match v {
308            1 => Self::Playing,
309            2 => Self::Paused,
310            _ => Self::Stopped,
311        }
312    }
313}
314
315/// Audio format info for the currently playing track.
316#[derive(Debug, Clone, PartialEq)]
317pub struct TrackInfo {
318    pub id: QueueItemId,
319    pub path: PathBuf,
320    pub codec: String,
321    pub sample_rate: u32,
322    pub bit_depth: Option<u16>,
323    pub bitrate_kbps: Option<u32>,
324    pub channels: u16,
325    pub duration_ms: u64,
326}
327
328// --- Playlist data model ---
329
330/// Minimum bytes written before streaming playback can begin.
331pub const STREAM_THRESHOLD: u64 = 256 * 1024;
332
333/// Held back from the seekable extent of a downloading track.
334///
335/// Bytes are converted to time at the average bitrate, so on VBR the estimate
336/// wanders either side of the truth; landing short of the write head costs a
337/// couple of seconds of reach and landing past it costs a stall.
338pub const SEEK_SAFETY_MS: u64 = 2_000;
339
340/// What a playlist item can say about itself.
341///
342/// Only what is true of the item regardless of any transfer: whether the bytes
343/// at its path can be played. Whether one is *arriving* is the download store's
344/// business, and asking the item would mean two accounts of one fact that have
345/// to be kept in step. Read [`LoadState`] for the two
346/// together.
347///
348/// Once a transfer ends, this is written before anything is woken: a reader
349/// waiting on the transfer learns how it ended from here.
350#[derive(Debug, Clone, Default, PartialEq, Eq)]
351pub enum ItemState {
352    /// Nothing has resolved this yet.
353    #[default]
354    Pending,
355    /// The file at `path` is there and playable.
356    Ready,
357    /// It cannot be made playable, and this is why. Not only download
358    /// failures: a track with no local file and no remote copy fails here
359    /// without a transfer ever being attempted.
360    Failed(String),
361}
362
363/// What an item relinked to a row, or to none, can now say about itself.
364pub fn relinked_state(state: &ItemState, linked: bool, path: &std::path::Path) -> ItemState {
365    match (linked, state) {
366        (false, ItemState::Pending) if !path.exists() => {
367            ItemState::Failed("no longer in the library".into())
368        }
369        (true, ItemState::Failed(_)) if !path.exists() => ItemState::Pending,
370        (_, state) => state.clone(),
371    }
372}
373
374/// An item's state, and any transfer against it, as one answer.
375///
376/// Derived rather than stored. `Downloading` carries the store's own figures —
377/// the very same counter the downloader writes — so there is nothing to copy
378/// and nothing that can drift.
379#[derive(Debug, Clone)]
380pub enum LoadState {
381    Pending,
382    Downloading {
383        /// Where the bytes are going: the in-progress `.part` file, not the
384        /// destination it is renamed to at the end.
385        path: PathBuf,
386        /// Total bytes expected, or 0 when the server sent no Content-Length.
387        total: u64,
388        /// How many bytes have landed. The download thread writes it per chunk
389        /// without taking any lock the player holds.
390        bytes_written: Arc<ByteFeed>,
391    },
392    Ready,
393    Failed(String),
394}
395
396impl LoadState {
397    /// An item's state, with whatever the download store says about it.
398    ///
399    /// The item's own state stands once it is anything but `Pending`: a
400    /// transfer's end is written to every entry waiting on it before the
401    /// transfer itself is marked settled. While it is pending, a listed
402    /// transfer for its track is what is happening to it — whichever entry
403    /// that transfer was started for.
404    pub fn of(item: &PlaylistItem, downloads: &DownloadStore) -> Self {
405        match &item.state {
406            ItemState::Ready => Self::Ready,
407            ItemState::Failed(reason) => Self::Failed(reason.clone()),
408            ItemState::Pending => match item.db_id.and_then(|id| downloads.live(id)) {
409                Some(live) => Self::Downloading {
410                    path: live.source,
411                    total: live.total,
412                    bytes_written: live.written,
413                },
414                None => Self::Pending,
415            },
416        }
417    }
418}
419
420/// Resolved playback source for a playlist item.
421pub enum PlaybackSource {
422    /// File fully downloaded — play from path.
423    Ready(PathBuf),
424    /// File being downloaded — enough data buffered to start streaming.
425    Streaming {
426        path: PathBuf,
427        bytes_written: Arc<crate::remote::downloads::ByteFeed>,
428        total: u64,
429    },
430}
431
432/// A single item in the playlist. Created once when tracks are added to the playlist.
433#[derive(Debug, Clone)]
434pub struct PlaylistItem {
435    pub id: QueueItemId,
436    /// Database track ID — set for tracks loaded from DB, used for downloads.
437    /// A row id names a track only while the row lasts: see `uid`.
438    pub db_id: Option<i64>,
439    /// The track's uid, which outlives its row: a server's tracks forgotten
440    /// and synced again come back under the same uids and, SQLite reusing
441    /// row ids, perhaps other ids. When the library loses rows the queue
442    /// finds its tracks again by this (`helpers::queue_relinks`).
443    pub uid: Option<String>,
444    /// The playlist entry this came from, when it came from a playlist.
445    ///
446    /// A playlist may hold the same track twice, and two copies are two queue
447    /// items. Without this a playlist row can only ask "is my *track* playing?"
448    /// and both copies answer yes. The entry id is the one thing that tells
449    /// them apart, so the queue carries it.
450    pub playlist_entry_id: Option<i64>,
451    pub path: PathBuf,
452    pub title: String,
453    pub artist: String,
454    pub album_artist: String,
455    pub album: String,
456    pub year: Option<String>,
457    pub codec: Option<String>,
458    pub track_number: Option<i64>,
459    pub disc: Option<i64>,
460    pub duration_ms: Option<u64>,
461    /// What the item can say about itself. Ask [`SharedPlayerState::load_state`]
462    /// for this together with any transfer against it.
463    pub state: ItemState,
464    /// The item has played in this pass of the queue: set when the
465    /// playhead reaches it, cleared when a repeating queue starts over. A
466    /// fact about the row, not its place relative to the cursor, so a queue
467    /// played out of order — shuffled, or jumped about in — shows what was
468    /// heard.
469    pub played: bool,
470}
471
472/// The playlist — one flat array, one cursor, and shuffle's order beside it.
473#[derive(Debug, Clone, Default)]
474pub struct Playlist {
475    pub items: Vec<PlaylistItem>,
476    pub cursor: Option<QueueItemId>,
477    pub order: Option<PlayOrder>,
478    /// Counts the times the queue has started over.
479    pub pass: u64,
480}
481
482impl Playlist {
483    fn find(&self, id: QueueItemId) -> Option<&PlaylistItem> {
484        self.items.iter().find(|item| item.id == id)
485    }
486
487    /// Bring shuffle's order up to date with the queue: rows gone or played
488    /// leave it, and rows that could play but are not in it — queued since,
489    /// or a duplicate whose twin was removed — take random places in it.
490    fn reconcile(&mut self) {
491        let Playlist {
492            items,
493            cursor,
494            order,
495            ..
496        } = self;
497        let Some(order) = order.as_mut() else {
498            return;
499        };
500        let rows: HashMap<QueueItemId, &PlaylistItem> =
501            items.iter().map(|item| (item.id, item)).collect();
502
503        let mut seen: std::collections::HashSet<TrackKey> = items
504            .iter()
505            .filter(|item| item.played)
506            .map(PlaylistItem::key)
507            .chain(cursor.and_then(|c| rows.get(&c)).map(|item| item.key()))
508            .collect();
509        // A row stays in, or joins, an order when it can play, has not
510        // played (this pass's order only), and its track is not in it yet.
511        fn keep<'a>(
512            seen: &mut std::collections::HashSet<TrackKey<'a>>,
513            item: &'a PlaylistItem,
514            unplayed: bool,
515        ) -> bool {
516            (!unplayed || !item.played) && item.playable() && seen.insert(item.key())
517        }
518        order
519            .upcoming
520            .retain(|id| rows.get(id).is_some_and(|item| keep(&mut seen, item, true)));
521        let fresh = items
522            .iter()
523            .filter(|item| keep(&mut seen, item, true))
524            .map(|item| item.id)
525            .collect();
526        scatter(&mut order.upcoming, fresh);
527
528        if let Some(next) = order.next_pass.as_mut() {
529            let mut seen = std::collections::HashSet::new();
530            next.retain(|id| {
531                rows.get(id)
532                    .is_some_and(|item| keep(&mut seen, item, false))
533            });
534            let fresh = items
535                .iter()
536                .filter(|item| keep(&mut seen, item, false))
537                .map(|item| item.id)
538                .collect();
539            scatter(next, fresh);
540        }
541
542        order.history.retain(|id| rows.contains_key(id));
543    }
544
545    /// Every track in the queue once, in a random order: a pass. Not opening
546    /// on `last`'s track when there is another, so the turn of a pass does
547    /// not play one track twice running.
548    fn new_pass(&self, last: Option<QueueItemId>) -> Vec<QueueItemId> {
549        let mut seen = std::collections::HashSet::new();
550        let mut pass: Vec<QueueItemId> = self
551            .items
552            .iter()
553            .filter(|item| item.playable() && seen.insert(item.key()))
554            .map(|item| item.id)
555            .collect();
556        crate::helpers::shuffle(&mut pass);
557        let last = last.and_then(|id| self.find(id)).map(PlaylistItem::key);
558        if pass.len() > 1 && last.is_some() && self.find(pass[0]).map(PlaylistItem::key) == last {
559            let other =
560                crate::helpers::Rng::seeded().map_or(1, |mut rng| 1 + rng.below(pass.len() - 1));
561            pass.swap(0, other);
562        }
563        pass
564    }
565
566    /// What plays after `after` — after nothing, with `None` — under
567    /// `repeat`, and whether getting there starts the queue over.
568    /// `Some(None)` at the end; `None` when `after` is no longer queued,
569    /// since starting over from a row nobody is at would replay the queue.
570    ///
571    /// In order, that is `follows`. Shuffled, it is the next playable row of
572    /// the play order, and once this pass is spent and the queue repeats,
573    /// the next pass's, which is drawn here the first time it is needed.
574    ///
575    /// `ahead` says `after` is a step the lookahead took into the next pass,
576    /// which is searched from it rather than from the top.
577    fn follows(
578        &mut self,
579        after: Option<QueueItemId>,
580        ahead: bool,
581        repeat: Repeat,
582    ) -> Option<(Option<QueueItemId>, bool)> {
583        let Some(order) = self.order.as_ref() else {
584            return match after {
585                Some(after) => follows(&self.items, after, repeat)
586                    .map(|(next, wrapped)| (next.map(|item| item.id), wrapped)),
587                None => Some((
588                    self.items
589                        .iter()
590                        .find(|item| item.playable())
591                        .map(|item| item.id),
592                    false,
593                )),
594            };
595        };
596        if let Some(after) = after {
597            let item = self.find(after)?;
598            if repeat == Repeat::One && item.playable() {
599                return Some((Some(after), false));
600            }
601        }
602        let playable = |id: &&QueueItemId| self.find(**id).is_some_and(PlaylistItem::playable);
603        let from = |order: &[QueueItemId]| {
604            after
605                .and_then(|after| order.iter().position(|id| *id == after))
606                .map_or(0, |at| at + 1)
607        };
608        if ahead {
609            let Some(next) = order.next_pass.as_ref() else {
610                return Some((None, false));
611            };
612            let at = from(next);
613            let found = next[at..].iter().chain(&next[..at]).find(playable).copied();
614            return Some((found, true));
615        }
616        let upcoming = &order.upcoming;
617        if let Some(next) = upcoming[from(upcoming)..].iter().find(playable) {
618            return Some((Some(*next), false));
619        }
620        if repeat == Repeat::Off {
621            return Some((None, false));
622        }
623        if order.next_pass.is_none() {
624            let pass = self.new_pass(after.or(self.cursor));
625            self.order.as_mut().expect("checked above").next_pass = Some(pass);
626        }
627        let next = self.order.as_ref()?.next_pass.as_ref()?;
628        let playable = |id: &&QueueItemId| self.find(**id).is_some_and(PlaylistItem::playable);
629        Some((next.iter().find(playable).copied(), true))
630    }
631
632    /// Put the cursor on `id`. Shuffled, its track leaves what is still to
633    /// play this pass, and the row goes on the history Previous walks back.
634    fn place_cursor(&mut self, id: Option<QueueItemId>) {
635        let Playlist {
636            items,
637            cursor,
638            order,
639            ..
640        } = self;
641        *cursor = id;
642        let Some(id) = id else { return };
643        let Some(key) = items
644            .iter()
645            .find(|item| item.id == id)
646            .map(PlaylistItem::key)
647        else {
648            return;
649        };
650        let Some(order) = order.as_mut() else { return };
651        let keys: HashMap<QueueItemId, TrackKey> =
652            items.iter().map(|item| (item.id, item.key())).collect();
653        order.upcoming.retain(|row| keys.get(row) != Some(&key));
654        if order.history.last() != Some(&id) {
655            order.history.push(id);
656        }
657    }
658
659    /// The cursor moves on to `id`, the row that follows it: at the end of a
660    /// track, or by Next. Getting there by starting the queue over begins a
661    /// new pass — nothing has played in it yet, and shuffled, the next
662    /// pass's order becomes what is left to play.
663    ///
664    /// `pass` is the pass the step that chose `id` put it in, when known.
665    /// Without it, a move one step on from the cursor is read: a row of the
666    /// next pass is chosen only once nothing in this one can play.
667    fn move_on(&mut self, id: QueueItemId, pass: Option<u64>) {
668        let wrapped = match (&self.order, pass) {
669            (_, Some(pass)) => pass > self.pass,
670            (Some(order), None) => {
671                self.cursor != Some(id)
672                    && !order.upcoming.contains(&id)
673                    && order
674                        .next_pass
675                        .as_ref()
676                        .is_some_and(|pass| pass.contains(&id))
677            }
678            (None, None) => {
679                let at = |id| self.items.iter().position(|item| item.id == id);
680                matches!((self.cursor.and_then(at), at(id)), (Some(from), Some(to)) if to < from)
681            }
682        };
683        if wrapped {
684            for item in &mut self.items {
685                item.played = false;
686            }
687            self.pass += 1;
688            if let Some(order) = self.order.as_mut() {
689                order.upcoming = order.next_pass.take().unwrap_or_default();
690            }
691        }
692        self.place_cursor(Some(id));
693    }
694
695    /// Start a new pass when every row has played: a queue played out, then
696    /// played again from a row someone picked, or shuffled once it has.
697    /// Without this, a shuffled queue with nothing left unplayed would play
698    /// the one row and stop. Says whether it did.
699    ///
700    /// Only for what the listener asks for. A restore, a hand-off or a
701    /// track repeating sets the cursor too, and must leave the pass as it is.
702    fn start_over_if_spent(&mut self) -> bool {
703        let mut playable = self.items.iter().filter(|item| item.playable()).peekable();
704        if playable.peek().is_none() || !playable.all(|item| item.played) {
705            return false;
706        }
707        for item in &mut self.items {
708            item.played = false;
709        }
710        self.pass += 1;
711        if let Some(order) = self.order.as_mut() {
712            order.upcoming.clear();
713            order.next_pass = None;
714        }
715        self.reconcile();
716        true
717    }
718}
719
720// --- UI view types ---
721
722/// Status of a track in the queue — for UI display.
723#[derive(Debug, Clone, Copy, PartialEq, Eq)]
724pub enum QueueEntryStatus {
725    Queued,
726    Playing,
727    Played,
728    Downloading,
729    /// User double-clicked — this track is priority, will play when ready.
730    PriorityPending,
731    Failed,
732}
733
734impl QueueEntryStatus {
735    /// The item under the cursor. Every front end maps it through here, so
736    /// the transport and the queue row agree on what it is doing.
737    ///
738    /// `PriorityPending` is waiting its turn with no bytes moving; once its
739    /// transfer starts it is `Downloading`, with progress to draw.
740    pub fn at_cursor(state: &ItemState, transferring: bool) -> Self {
741        match state {
742            ItemState::Ready => Self::Playing,
743            ItemState::Failed(_) => Self::Failed,
744            ItemState::Pending if transferring => Self::Downloading,
745            ItemState::Pending => Self::PriorityPending,
746        }
747    }
748}
749
750/// A single entry in the UI-visible queue snapshot.
751#[derive(Debug, Clone)]
752pub struct QueueEntry {
753    pub id: QueueItemId,
754    /// Database track ID — set for tracks loaded from DB, used for downloads.
755    pub db_id: Option<i64>,
756    /// The playlist row this came from — see `PlaylistItem::playlist_entry_id`.
757    pub playlist_entry_id: Option<i64>,
758    pub path: PathBuf,
759    pub title: String,
760    pub artist: String,
761    pub album_artist: String,
762    pub album: String,
763    pub year: Option<String>,
764    pub codec: Option<String>,
765    pub track_number: Option<i64>,
766    pub disc: Option<i64>,
767    pub duration_ms: Option<u64>,
768    pub status: QueueEntryStatus,
769    pub download_progress: Option<(u64, u64)>,
770    /// Why this entry cannot play, when `status` is `Failed`; or, under the
771    /// cursor and still to come, why it has not arrived yet.
772    pub error: Option<String>,
773}
774
775/// Pre-built visible queue — single atomic snapshot for the UI.
776#[derive(Debug, Clone, Default)]
777pub struct VisibleQueueSnapshot {
778    pub entries: Vec<QueueEntry>,
779    pub finished_count: usize,
780    pub has_playing: bool,
781    pub queue_count: usize,
782}
783
784/// The part of a visible queue row that moves while the queue stands still.
785/// See `SharedPlayerState::queue_readings`.
786#[derive(Debug, Clone, PartialEq)]
787pub struct QueueReading {
788    pub id: QueueItemId,
789    pub db_id: Option<i64>,
790    pub status: QueueEntryStatus,
791    pub duration_ms: Option<u64>,
792    pub download_progress: Option<(u64, u64)>,
793    pub error: Option<String>,
794}
795
796/// Shared player state — atomics for lock-free reads from UI thread.
797///
798/// The engine writes these, the UI reads them. No mutexes in the hot path.
799#[derive(Debug)]
800pub struct SharedPlayerState {
801    /// The playback state, with `WAITING` set while the player waits for a
802    /// track it was asked for. One atomic, so no reader sees one updated
803    /// without the other.
804    state: AtomicU8,
805    /// Where a session starts, and where a stopped one stands. While one is
806    /// running the playhead is read off the timeline instead.
807    position_ms: AtomicU64,
808    timeline: std::sync::OnceLock<Arc<crate::audio::buffer::PlaybackTimeline>>,
809    track_info: parking_lot::RwLock<Option<TrackInfo>>,
810
811    /// The playlist and its cursor, under one lock.
812    playlist: parking_lot::RwLock<Playlist>,
813
814    /// Bumped on every playlist mutation so UI can skip redundant redraws.
815    playlist_version: AtomicU64,
816
817    /// Bumped only when what a saved session holds changes: the items and
818    /// their metadata, not the cursor or load states. What decides whether
819    /// the saved queue has to be written again.
820    content_version: AtomicU64,
821
822    /// Bumped when the set of items still waiting for a file may have
823    /// changed: items added or removed, or one put back to `Pending`. What the
824    /// download queue follows; a download landing or the cursor moving does
825    /// not move it.
826    pending_version: AtomicU64,
827
828    /// Bumped when an item is marked played or a pass clears the marks: a
829    /// change to what a saved session holds that clients read as a status
830    /// change, not an edit. See `saved_version`.
831    played_version: AtomicU64,
832
833    /// Every transfer this player's items are fetched by.
834    downloads: Arc<DownloadStore>,
835
836    /// Set by external signals (e.g. souvlaki Quit event) to request clean shutdown.
837    quit_requested: AtomicBool,
838
839    /// Set when metadata has been refreshed (e.g. download completed while streaming).
840    /// The UI loop checks this to force a souvlaki/cover-art update without a track change.
841    metadata_refresh_pending: AtomicBool,
842
843    /// The rate the output device settled at for the current track, or 0 when
844    /// nothing has played yet. Compared against the source rate, it is the one
845    /// thing koan can say for certain about the path to the DAC: whether it
846    /// handed the device the samples as they are, or something had to resample
847    /// to reach it. Everything past that — other clients, the volume stage — is
848    /// the system's, and not ours to claim.
849    output_sample_rate: AtomicU64,
850
851    /// Whether the system mixes the output on its way to the device: 0 where
852    /// the platform does not say, 1 mixed, 2 passed through. See
853    /// `AudioBackend::output_mixed`.
854    output_mixed: std::sync::atomic::AtomicU8,
855
856    /// Why the last track asked for did not start, until one does. The
857    /// output failing to open, a file that does not decode: what the person
858    /// would otherwise meet as silence.
859    start_failure: parking_lot::RwLock<Option<String>>,
860
861    /// What DSP is doing to the current session's audio. `None` is the
862    /// bit-perfect path.
863    dsp: parking_lot::RwLock<Option<crate::audio::dsp::DspStatus>>,
864    /// The playhead of a renderer this koan is playing to, which keeps its
865    /// own clock: where it was last heard to be, and since when it has been
866    /// running from there. Read in place of the timeline while set.
867    renderer_clock: parking_lot::Mutex<Option<RendererClock>>,
868
869    /// The UPnP renderer playing in place of the local output, if one is.
870    renderer: parking_lot::RwLock<Option<crate::upnp::Output>>,
871
872    /// The play mode, as `PlayMode::to_bits`. Written by the player's
873    /// `publish` alone; the queue's own reads (the lookahead, advancing)
874    /// follow it.
875    play_mode: AtomicU8,
876
877    /// The sleep timer, while one is set. Written by the player's `publish`.
878    sleep: parking_lot::RwLock<Option<Sleep>>,
879    /// The sleep timer is fading playback out.
880    sleep_fading: AtomicBool,
881}
882
883/// A renderer's playhead: `position_ms`, plus the time since `running` if it
884/// is playing.
885#[derive(Debug, Clone, Copy, PartialEq)]
886pub struct RendererClock {
887    pub position_ms: u64,
888    pub running: Option<std::time::Instant>,
889}
890
891impl RendererClock {
892    pub fn now_ms(&self) -> u64 {
893        self.position_ms + self.running.map_or(0, |at| at.elapsed().as_millis() as u64)
894    }
895}
896
897impl SharedPlayerState {
898    pub fn new() -> Arc<Self> {
899        Arc::new(Self {
900            state: AtomicU8::new(PlaybackState::Stopped as u8),
901            position_ms: AtomicU64::new(0),
902            timeline: std::sync::OnceLock::new(),
903            track_info: parking_lot::RwLock::new(None),
904            playlist: parking_lot::RwLock::new(Playlist::default()),
905            playlist_version: AtomicU64::new(0),
906            content_version: AtomicU64::new(0),
907            pending_version: AtomicU64::new(0),
908            played_version: AtomicU64::new(0),
909            downloads: DownloadStore::new(),
910            quit_requested: AtomicBool::new(false),
911            metadata_refresh_pending: AtomicBool::new(false),
912            output_sample_rate: AtomicU64::new(0),
913            output_mixed: std::sync::atomic::AtomicU8::new(0),
914            start_failure: parking_lot::RwLock::new(None),
915            dsp: parking_lot::RwLock::new(None),
916            renderer_clock: parking_lot::Mutex::new(None),
917            renderer: parking_lot::RwLock::new(None),
918            play_mode: AtomicU8::new(0),
919            sleep: parking_lot::RwLock::new(None),
920            sleep_fading: AtomicBool::new(false),
921        })
922    }
923
924    // --- Playback state ---
925
926    fn transport(&self) -> (PlaybackState, bool) {
927        let bits = self.state.load(Ordering::Acquire);
928        (PlaybackState::from_u8(bits & !WAITING), bits & WAITING != 0)
929    }
930
931    pub fn playback_state(&self) -> PlaybackState {
932        self.transport().0
933    }
934
935    pub fn set_playback_state(&self, state: PlaybackState) {
936        self.set_transport(state, false);
937    }
938
939    /// Whether the player is waiting for a track it was asked for to arrive.
940    /// Stopped while waiting, the track opens playing; paused, it opens paused.
941    pub fn is_waiting(&self) -> bool {
942        self.transport().1
943    }
944
945    /// Playing, or waiting for a track that will open playing: what a
946    /// play/pause toggle pauses.
947    pub fn wants_to_play(&self) -> bool {
948        matches!(
949            self.transport(),
950            (PlaybackState::Playing, _) | (PlaybackState::Stopped, true)
951        )
952    }
953
954    /// Nothing loaded and nothing waited for: where adding tracks starts them.
955    pub fn is_idle(&self) -> bool {
956        self.transport() == (PlaybackState::Stopped, false)
957    }
958
959    /// Publish the playback state and the wait together.
960    pub fn set_transport(&self, state: PlaybackState, waiting: bool) {
961        let bits = state as u8 | if waiting { WAITING } else { 0 };
962        if self.state.swap(bits, Ordering::AcqRel) != bits {
963            self.changed();
964        }
965    }
966
967    /// Where the playhead is, read off the samples the output has played, so
968    /// it is right whenever it is asked and nothing has to keep it up to date.
969    pub fn position_ms(&self) -> u64 {
970        let clock = *self.renderer_clock.lock();
971        if let Some(clock) = clock {
972            let at = clock.now_ms();
973            let duration = self.duration_ms();
974            return if duration > 0 { at.min(duration) } else { at };
975        }
976        if self.playback_state() != PlaybackState::Stopped
977            && let Some(playhead) = self.timeline.get().and_then(|t| t.playhead())
978        {
979            return playhead.position_ms;
980        }
981        self.position_ms.load(Ordering::Acquire)
982    }
983
984    /// The timeline the playhead is read from. Set once, by the player.
985    pub(crate) fn attach_timeline(&self, timeline: Arc<crate::audio::buffer::PlaybackTimeline>) {
986        let _ = self.timeline.set(timeline);
987    }
988
989    pub fn set_position_ms(&self, pos: u64) {
990        self.position_ms.store(pos, Ordering::Release);
991        // Deliberately silent. The playhead advances on its own and is
992        // published as an anchor rather than as a reading — a wake per
993        // position would be the tick this whole arrangement removes. A seek, a
994        // pause and a track change all move something else here as well, and
995        // those are exactly the ones a client has to be told about.
996    }
997
998    /// Set by the player while a renderer is the output. A change to it is a
999    /// change clients are told about: the clock only moves when the renderer
1000    /// is heard from, or when a command moved it.
1001    /// Whether the playhead is advancing on its own. Playing, but held, while
1002    /// a renderer has been told to play and has not yet started: a client
1003    /// counting on from the command would run ahead of the sound.
1004    ///
1005    /// A renderer playing a file keeps its clock here; one playing a stream
1006    /// processed here keeps it on the timeline, in time into the stream.
1007    pub fn playhead_moving(&self) -> bool {
1008        let clock =
1009            (*self.renderer_clock.lock()).or_else(|| self.timeline.get().and_then(|t| t.clock()));
1010        match clock {
1011            Some(clock) => clock.running.is_some(),
1012            None => self.playback_state() == PlaybackState::Playing,
1013        }
1014    }
1015
1016    pub(crate) fn renderer_clock(&self) -> Option<RendererClock> {
1017        *self.renderer_clock.lock()
1018    }
1019
1020    pub(crate) fn set_renderer_clock(&self, clock: Option<RendererClock>) {
1021        let mut guard = self.renderer_clock.lock();
1022        if *guard != clock {
1023            *guard = clock;
1024            drop(guard);
1025            self.changed();
1026        }
1027    }
1028
1029    /// The renderer this koan is playing to, when it is not its own output.
1030    pub fn renderer(&self) -> Option<crate::upnp::Output> {
1031        self.renderer.read().clone()
1032    }
1033
1034    pub(crate) fn set_renderer(&self, output: Option<crate::upnp::Output>) {
1035        *self.renderer.write() = output;
1036        self.changed();
1037    }
1038
1039    pub(crate) fn update_renderer(&self, f: impl FnOnce(&mut crate::upnp::Output)) {
1040        let changed = match self.renderer.write().as_mut() {
1041            Some(out) => {
1042                let before = out.clone();
1043                f(out);
1044                *out != before
1045            }
1046            None => false,
1047        };
1048        if changed {
1049            self.changed();
1050        }
1051    }
1052
1053    pub fn track_info(&self) -> Option<TrackInfo> {
1054        self.track_info.read().clone()
1055    }
1056
1057    pub fn set_track_info(&self, info: Option<TrackInfo>) {
1058        *self.track_info.write() = info;
1059        self.changed();
1060    }
1061
1062    /// How far into the currently playing track a seek can land.
1063    ///
1064    /// A track on disk is seekable end to end. One still downloading is
1065    /// seekable only as far as its bytes reach: bytes map to time by the
1066    /// average bitrate, exact for lossless and CBR and drifting on VBR, which
1067    /// is what `SEEK_SAFETY_MS` covers. Zero when nothing is playing.
1068    ///
1069    /// The one value both the clamp in `Player::seek` and the extent front ends
1070    /// draw on the seek bar come from — a bar that shows a reachable position
1071    /// the player then refuses is worse than no bar.
1072    pub fn seekable_ms(&self) -> u64 {
1073        let Some(info) = self.track_info.read().clone() else {
1074            return 0;
1075        };
1076
1077        // Released before the playlist lock is taken: derive_visible_queue takes
1078        // these two in the opposite order, so holding both would close a cycle.
1079        let pl = self.playlist.read();
1080        let Some(item) = pl.items.iter().find(|item| item.id == info.id) else {
1081            return info.duration_ms;
1082        };
1083
1084        let LoadState::Downloading {
1085            total,
1086            bytes_written,
1087            ..
1088        } = self.load_state(item)
1089        else {
1090            return info.duration_ms;
1091        };
1092
1093        // A container that could not describe itself from the bytes downloaded
1094        // states no duration, and cannot be seeked at all until the rest of it
1095        // lands — there is no index to seek against and no end to seek within.
1096        // Ogg is the one that does this; it keeps its duration in its last page.
1097        if info.duration_ms == 0 {
1098            return 0;
1099        }
1100
1101        let written = bytes_written.load(Ordering::Acquire);
1102        let reached = if total > 0 && info.duration_ms > 0 {
1103            ((written as f64 / total as f64) * info.duration_ms as f64) as u64
1104        } else if let Some(kbps) = info.bitrate_kbps.filter(|k| *k > 0) {
1105            // No Content-Length. Bytes still say how much audio has arrived,
1106            // given what the probe measured the bitrate to be: 1 kbps is
1107            // 1 bit per ms, so bits divided by kbps is milliseconds.
1108            written.saturating_mul(8) / kbps as u64
1109        } else {
1110            // Nothing to derive a position from — forward seeking would be a
1111            // guess, so allow only what has already been played.
1112            return self.position_ms();
1113        };
1114
1115        reached.saturating_sub(SEEK_SAFETY_MS).min(info.duration_ms)
1116    }
1117
1118    /// The duration to show for what is playing.
1119    ///
1120    /// The container's own answer wherever it gave one. A partial file that
1121    /// could not be read far enough to state a duration has none, and the
1122    /// library's figure stands in — it came from the server, it is right, and
1123    /// a transport that reads 0:00 for nine hours of music is worse than one
1124    /// reading a figure the container has not caught up with yet.
1125    pub fn duration_ms(&self) -> u64 {
1126        let Some(info) = self.track_info.read().clone() else {
1127            return 0;
1128        };
1129        if info.duration_ms > 0 {
1130            return info.duration_ms;
1131        }
1132        // Released before the playlist lock, as everywhere else here.
1133        self.playlist
1134            .read()
1135            .items
1136            .iter()
1137            .find(|item| item.id == info.id)
1138            .and_then(|item| item.duration_ms)
1139            .unwrap_or(0)
1140    }
1141
1142    /// `seekable_ms`, but `None` when the whole track is reachable — which is
1143    /// every track that is not mid-download. What a front end draws a boundary
1144    /// from: no boundary is the normal case and should cost no mark.
1145    pub fn seek_ceiling_ms(&self) -> Option<u64> {
1146        let duration = self.duration_ms();
1147        if duration == 0 {
1148            return None;
1149        }
1150        let seekable = self.seekable_ms();
1151        (seekable < duration).then_some(seekable)
1152    }
1153
1154    /// Download fraction (0.0..1.0) for the currently playing track, if streaming.
1155    /// Returns `None` for fully-downloaded or non-playing tracks.
1156    pub fn current_download_fraction(&self) -> Option<f64> {
1157        // Released before the playlist lock is taken: derive_visible_queue takes
1158        // these two in the opposite order, so holding both would close a cycle.
1159        let id = self.track_info.read().as_ref()?.id;
1160        let pl = self.playlist.read();
1161        pl.items
1162            .iter()
1163            .find(|item| item.id == id)
1164            .and_then(|item| match self.load_state(item) {
1165                LoadState::Downloading {
1166                    bytes_written,
1167                    total,
1168                    ..
1169                } => {
1170                    let written = bytes_written.load(Ordering::Acquire);
1171                    (total > 0).then(|| (written as f64 / total as f64).min(1.0))
1172                }
1173                _ => None,
1174            })
1175    }
1176
1177    // --- Quit ---
1178
1179    pub fn request_quit(&self) {
1180        self.quit_requested.store(true, Ordering::Release);
1181    }
1182
1183    pub fn quit_requested(&self) -> bool {
1184        self.quit_requested.load(Ordering::Acquire)
1185    }
1186
1187    // --- Metadata refresh ---
1188
1189    /// Signal that metadata has been refreshed mid-stream (e.g. download completed).
1190    /// The UI loop calls `take_metadata_refresh()` to consume this flag and
1191    /// force a souvlaki/cover-art update without waiting for a track change.
1192    pub fn signal_metadata_refresh(&self) {
1193        self.metadata_refresh_pending.store(true, Ordering::Release);
1194        self.changed();
1195    }
1196
1197    /// Returns true and clears the flag if a metadata refresh is pending.
1198    pub fn take_metadata_refresh(&self) -> bool {
1199        self.metadata_refresh_pending
1200            .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire)
1201            .is_ok()
1202    }
1203
1204    // --- Output device rate ---
1205
1206    /// `None` until a track has started and the device rate is known.
1207    pub fn output_sample_rate(&self) -> Option<u32> {
1208        match self.output_sample_rate.load(Ordering::Acquire) {
1209            0 => None,
1210            rate => Some(rate as u32),
1211        }
1212    }
1213
1214    pub fn set_output_sample_rate(&self, rate: u32) {
1215        self.output_sample_rate
1216            .store(u64::from(rate), Ordering::Release);
1217        self.changed();
1218    }
1219
1220    /// Back to "not known yet", for the window where the device is between
1221    /// rates. A switch takes as long as the hardware needs to reclock — the
1222    /// better part of a second on USB — and the previous track's rate is not
1223    /// an answer for this one.
1224    pub fn clear_output_sample_rate(&self) {
1225        self.output_sample_rate.store(0, Ordering::Release);
1226    }
1227
1228    pub fn output_mixed(&self) -> Option<bool> {
1229        match self.output_mixed.load(Ordering::Acquire) {
1230            1 => Some(true),
1231            2 => Some(false),
1232            _ => None,
1233        }
1234    }
1235
1236    /// Why the last track asked for did not start, while none has since.
1237    pub fn start_failure(&self) -> Option<String> {
1238        self.start_failure.read().clone()
1239    }
1240
1241    pub fn set_start_failure(&self, failure: Option<String>) {
1242        let mut slot = self.start_failure.write();
1243        if *slot != failure {
1244            *slot = failure;
1245            drop(slot);
1246            self.changed();
1247        }
1248    }
1249
1250    pub fn set_output_mixed(&self, mixed: Option<bool>) {
1251        let v = match mixed {
1252            None => 0,
1253            Some(true) => 1,
1254            Some(false) => 2,
1255        };
1256        if self.output_mixed.swap(v, Ordering::AcqRel) != v {
1257            self.changed();
1258        }
1259    }
1260
1261    // --- DSP ---
1262
1263    pub fn dsp(&self) -> Option<crate::audio::dsp::DspStatus> {
1264        self.dsp.read().clone()
1265    }
1266
1267    pub fn set_dsp(&self, status: Option<crate::audio::dsp::DspStatus>) {
1268        let mut dsp = self.dsp.write();
1269        if *dsp != status {
1270            *dsp = status;
1271            drop(dsp);
1272            self.changed();
1273        }
1274    }
1275
1276    // --- Play mode ---
1277
1278    pub fn play_mode(&self) -> PlayMode {
1279        PlayMode::from_bits(self.play_mode.load(Ordering::Acquire))
1280    }
1281
1282    /// The player's `publish`, and nothing else — but for a front end that
1283    /// mirrors another process's player into a state of its own, as it
1284    /// mirrors the playback state. The mode is saved with the queue, so a
1285    /// change moves the content version.
1286    pub fn set_play_mode(&self, mode: PlayMode) {
1287        if self.play_mode.swap(mode.to_bits(), Ordering::AcqRel) != mode.to_bits() {
1288            self.content_version.fetch_add(1, Ordering::AcqRel);
1289            self.bump_version();
1290        }
1291    }
1292
1293    pub fn sleep(&self) -> Option<Sleep> {
1294        *self.sleep.read()
1295    }
1296
1297    pub fn set_sleep(&self, sleep: Option<Sleep>) {
1298        let mut held = self.sleep.write();
1299        if *held != sleep {
1300            *held = sleep;
1301            drop(held);
1302            self.changed();
1303        }
1304    }
1305
1306    pub fn sleep_fading(&self) -> bool {
1307        self.sleep_fading.load(Ordering::Acquire)
1308    }
1309
1310    pub fn set_sleep_fading(&self, fading: bool) {
1311        if self.sleep_fading.swap(fading, Ordering::AcqRel) != fading {
1312            self.changed();
1313        }
1314    }
1315
1316    // --- Playlist version ---
1317
1318    pub fn playlist_version(&self) -> u64 {
1319        self.playlist_version.load(Ordering::Acquire)
1320    }
1321
1322    fn bump_version(&self) {
1323        self.playlist_version.fetch_add(1, Ordering::AcqRel);
1324        self.changed();
1325    }
1326
1327    pub fn content_version(&self) -> u64 {
1328        self.content_version.load(Ordering::Acquire)
1329    }
1330
1331    /// Moves whenever anything a saved session holds changes: the content,
1332    /// and which items have played. What a saver compares to know whether to
1333    /// write the queue again.
1334    pub fn saved_version(&self) -> u64 {
1335        self.content_version()
1336            .wrapping_add(self.played_version.load(Ordering::Acquire))
1337    }
1338
1339    /// `bump_version`, for a change to what a saved session holds. Shuffle's
1340    /// order is brought up to date with it here, so no edit can leave it
1341    /// naming a row that is gone or missing one that was queued.
1342    fn bump_content(&self) {
1343        self.playlist.write().reconcile();
1344        self.content_version.fetch_add(1, Ordering::AcqRel);
1345        self.pending_version.fetch_add(1, Ordering::AcqRel);
1346        self.bump_version();
1347    }
1348
1349    /// See the field. Moves with every content change, and when an item goes
1350    /// back to `Pending`.
1351    pub fn pending_version(&self) -> u64 {
1352        self.pending_version.load(Ordering::Acquire)
1353    }
1354
1355    /// The transfers this player's items are fetched by.
1356    pub fn downloads(&self) -> &Arc<DownloadStore> {
1357        &self.downloads
1358    }
1359
1360    fn load_state(&self, item: &PlaylistItem) -> LoadState {
1361        LoadState::of(item, &self.downloads)
1362    }
1363
1364    /// Say that something here moved, without saying what.
1365    ///
1366    /// Every version and atomic in this struct stays exactly as it was — they
1367    /// are what a watcher consults to find out what changed. This is what
1368    /// spares it looking when nothing did. See `crate::signal`.
1369    pub fn changed(&self) {
1370        crate::signal::engine_changed().bump();
1371    }
1372
1373    // --- Playlist mutations (called from player thread via commands) ---
1374
1375    /// Append items to the playlist.
1376    pub fn add_items(&self, items: Vec<PlaylistItem>) {
1377        let mut pl = self.playlist.write();
1378        pl.items.extend(items);
1379        drop(pl);
1380        self.bump_content();
1381    }
1382
1383    /// Insert items after a specific queue item.
1384    pub fn insert_items_after(&self, items: Vec<PlaylistItem>, after: QueueItemId) {
1385        let mut pl = self.playlist.write();
1386        let insert_at = match pl.items.iter().position(|item| item.id == after) {
1387            Some(pos) => pos + 1,
1388            None => pl.items.len(), // fallback: append
1389        };
1390        for (i, item) in items.into_iter().enumerate() {
1391            pl.items.insert(insert_at + i, item);
1392        }
1393        drop(pl);
1394        self.bump_content();
1395    }
1396
1397    /// Update file paths for playlist items (after organize moves files).
1398    pub fn update_paths(&self, updates: &[(QueueItemId, PathBuf)]) {
1399        let mut pl = self.playlist.write();
1400        for (id, new_path) in updates {
1401            if let Some(item) = pl.items.iter_mut().find(|item| item.id == *id) {
1402                item.path = new_path.clone();
1403            }
1404        }
1405        drop(pl);
1406        self.bump_content();
1407    }
1408
1409    /// Remove an item by ID.
1410    pub fn remove_item(&self, id: QueueItemId) {
1411        let mut pl = self.playlist.write();
1412        pl.items.retain(|item| item.id != id);
1413        // If cursor was on removed item, clear it (caller handles next_track).
1414        if pl.cursor == Some(id) {
1415            pl.cursor = None;
1416        }
1417        drop(pl);
1418        self.bump_content();
1419    }
1420
1421    /// Move an item relative to another entry.
1422    pub fn move_item(&self, id: QueueItemId, target: QueueItemId, after: bool) {
1423        let mut pl = self.playlist.write();
1424        let Some(from) = pl.items.iter().position(|item| item.id == id) else {
1425            return;
1426        };
1427        let item = pl.items.remove(from);
1428        let Some(to) = pl.items.iter().position(|item| item.id == target) else {
1429            // Target gone — put it back.
1430            let pos = from.min(pl.items.len());
1431            pl.items.insert(pos, item);
1432            return;
1433        };
1434        let insert_at = if after { to + 1 } else { to };
1435        pl.items.insert(insert_at, item);
1436        drop(pl);
1437        self.bump_content();
1438    }
1439
1440    /// Batch move: extract items by ID, reinsert them at `target` position.
1441    /// Preserves the relative order of the moved items.
1442    pub fn move_items(&self, ids: &[QueueItemId], target: QueueItemId, after: bool) {
1443        use std::collections::HashSet;
1444        let id_set: HashSet<QueueItemId> = ids.iter().copied().collect();
1445
1446        let mut pl = self.playlist.write();
1447
1448        // Partition: extract moved items, keep the rest.
1449        let mut remaining = Vec::with_capacity(pl.items.len());
1450        let mut moved = Vec::with_capacity(ids.len());
1451        for item in pl.items.drain(..) {
1452            if id_set.contains(&item.id) {
1453                moved.push(item);
1454            } else {
1455                remaining.push(item);
1456            }
1457        }
1458
1459        // Find target in the remaining items.
1460        let insert_at = match remaining.iter().position(|item| item.id == target) {
1461            Some(pos) => {
1462                if after {
1463                    pos + 1
1464                } else {
1465                    pos
1466                }
1467            }
1468            None => remaining.len(),
1469        };
1470
1471        // Splice moved items in at the target position.
1472        for (i, item) in moved.into_iter().enumerate() {
1473            remaining.insert(insert_at + i, item);
1474        }
1475
1476        pl.items = remaining;
1477        drop(pl);
1478        self.bump_content();
1479    }
1480
1481    /// Set the cursor (what's playing / should play).
1482    pub fn set_cursor(&self, id: Option<QueueItemId>) {
1483        let mut pl = self.playlist.write();
1484        pl.place_cursor(id);
1485        drop(pl);
1486        self.bump_version();
1487    }
1488
1489    /// The listener picked `id` to play: the cursor goes there, and if every
1490    /// row has played, a new pass starts from it.
1491    pub fn pick(&self, id: QueueItemId) {
1492        let mut pl = self.playlist.write();
1493        pl.place_cursor(Some(id));
1494        let replayed = pl.start_over_if_spent();
1495        drop(pl);
1496        if replayed {
1497            self.played_version.fetch_add(1, Ordering::AcqRel);
1498        }
1499        self.bump_version();
1500    }
1501
1502    /// The playhead has moved on to `id`, the item that followed the cursor:
1503    /// gaplessly, or a renderer taking the track it was handed. As an advance
1504    /// does, a move that starts the queue over begins a new pass. `pass` is
1505    /// the pass the lookahead step that chose `id` put it in, if one did.
1506    pub fn move_on_to(&self, id: QueueItemId, pass: Option<u64>) {
1507        let mut pl = self.playlist.write();
1508        pl.move_on(id, pass);
1509        drop(pl);
1510        self.played_version.fetch_add(1, Ordering::AcqRel);
1511        self.bump_version();
1512    }
1513
1514    /// The item has started playing: mark it played for this pass.
1515    pub fn mark_played(&self, id: QueueItemId) {
1516        let mut pl = self.playlist.write();
1517        let Some(item) = pl.items.iter_mut().find(|item| item.id == id) else {
1518            return;
1519        };
1520        if std::mem::replace(&mut item.played, true) {
1521            return;
1522        }
1523        drop(pl);
1524        self.played_version.fetch_add(1, Ordering::AcqRel);
1525        self.bump_version();
1526    }
1527
1528    /// Turn shuffle's play order on or off. On, it is drawn from the items
1529    /// yet to play this pass; off, it is dropped, and the queue — never moved
1530    /// — plays on in its own order from the cursor.
1531    pub fn set_shuffled(&self, on: bool) {
1532        let mut pl = self.playlist.write();
1533        if pl.order.is_some() == on {
1534            return;
1535        }
1536        pl.order = on.then(PlayOrder::default);
1537        let cursor = pl.cursor;
1538        pl.reconcile();
1539        pl.place_cursor(cursor);
1540        if on && pl.start_over_if_spent() {
1541            self.played_version.fetch_add(1, Ordering::AcqRel);
1542        }
1543        drop(pl);
1544        // The downloads follow the play order.
1545        self.pending_version.fetch_add(1, Ordering::AcqRel);
1546        self.bump_version();
1547    }
1548
1549    pub fn is_shuffled(&self) -> bool {
1550        self.playlist.read().order.is_some()
1551    }
1552
1553    /// What is left of this pass of the play order, next first.
1554    #[cfg(test)]
1555    pub(crate) fn upcoming(&self) -> Vec<QueueItemId> {
1556        let pl = self.playlist.read();
1557        pl.order
1558            .as_ref()
1559            .map(|o| o.upcoming.clone())
1560            .unwrap_or_default()
1561    }
1562
1563    pub fn is_empty(&self) -> bool {
1564        self.playlist.read().items.is_empty()
1565    }
1566
1567    pub fn cursor(&self) -> Option<QueueItemId> {
1568        self.playlist.read().cursor
1569    }
1570
1571    /// The path of the item under the cursor, without copying the playlist.
1572    pub fn cursor_path(&self) -> Option<PathBuf> {
1573        let pl = self.playlist.read();
1574        let cursor = pl.cursor?;
1575        pl.items
1576            .iter()
1577            .find(|item| item.id == cursor)
1578            .map(|item| item.path.clone())
1579    }
1580
1581    /// Clear the entire playlist + cursor.
1582    /// Swap the whole playlist for `items`, with no cursor, as one change.
1583    /// Returns what it held, for undo.
1584    ///
1585    /// One write and one version bump, not a clear and an add. The download
1586    /// queue reads the playlist on its own thread whenever it changes, and an
1587    /// empty playlist between the two would read as nothing wanted: every
1588    /// transfer for a track in both the old queue and the new one let go,
1589    /// cancelled, and started again.
1590    pub fn replace_playlist(
1591        &self,
1592        items: Vec<PlaylistItem>,
1593    ) -> (Vec<PlaylistItem>, Option<QueueItemId>) {
1594        let mut pl = self.playlist.write();
1595        let old = std::mem::replace(&mut pl.items, items);
1596        let cursor = pl.cursor.take();
1597        drop(pl);
1598        self.bump_content();
1599        (old, cursor)
1600    }
1601
1602    pub fn clear_playlist(&self) {
1603        let mut pl = self.playlist.write();
1604        pl.items.clear();
1605        pl.cursor = None;
1606        drop(pl);
1607        self.bump_content();
1608    }
1609
1610    // --- Called from decode thread (gapless) ---
1611
1612    /// Move the cursor to the next item that can still play — the first item
1613    /// after the cursor that is not `Failed`, from the top again when the
1614    /// queue repeats — and return its ID. An advance is a move on, so
1615    /// repeating one item wraps the queue here as repeating the queue does;
1616    /// playing the item again at its end is the player's call.
1617    ///
1618    /// An item that is still downloading parks the cursor rather than being
1619    /// skipped, so playback resumes from it when its data lands. Skipping it
1620    /// would drop it from the queue for good.
1621    ///
1622    /// With no cursor set, starts from the top. A cursor pointing at an item
1623    /// that is no longer in the playlist yields `None` — restarting from the
1624    /// top would silently replay the queue.
1625    pub fn advance_cursor_loadable(&self) -> Option<QueueItemId> {
1626        let repeat = match self.play_mode().repeat {
1627            Repeat::Off => Repeat::Off,
1628            Repeat::Queue | Repeat::One => Repeat::Queue,
1629        };
1630        let mut pl = self.playlist.write();
1631        let cursor = pl.cursor;
1632        let (next, wrapped) = pl.follows(cursor, false, repeat)?;
1633        let pass = pl.pass + u64::from(wrapped);
1634        pl.move_on(next?, Some(pass));
1635        drop(pl);
1636        self.played_version.fetch_add(1, Ordering::AcqRel);
1637        self.bump_version();
1638        next
1639    }
1640
1641    /// One step of the decoder's gapless lookahead from `after_id`. Does not
1642    /// move the cursor; `update_playback_state` does that when the playhead
1643    /// gets there.
1644    ///
1645    /// Failed items are passed over, as `advance_cursor_loadable` passes over
1646    /// them. A track still arriving is not: the decoder stops at it, the
1647    /// session drains, and the advance that follows waits for it. Passing over
1648    /// it would play the track after it and move the cursor beyond it, so it
1649    /// would never be heard.
1650    ///
1651    /// The play mode decides what follows: the queue's next item, the first
1652    /// again at its end when the queue repeats, or the same item when one
1653    /// repeats — gapless all three.
1654    ///
1655    /// None when `after_id` has been removed: the lookahead then has nothing
1656    /// to follow, and starting from the top would gaplessly replay the queue.
1657    pub fn lookahead_after(&self, after_id: QueueItemId) -> Option<Lookahead> {
1658        self.lookahead(after_id, None)
1659    }
1660
1661    /// The step after `step`, from what it chose: the decoder's next step
1662    /// through the queue, in the pass `step` reached.
1663    pub fn lookahead_from(&self, step: &Lookahead) -> Option<Lookahead> {
1664        self.lookahead(step.chosen.as_ref()?.0, Some(step.pass))
1665    }
1666
1667    fn lookahead(&self, after_id: QueueItemId, after_pass: Option<u64>) -> Option<Lookahead> {
1668        let repeat = self.play_mode().repeat;
1669        let mut pl = self.playlist.write();
1670        let after_pass = after_pass.unwrap_or(pl.pass);
1671        let ahead = after_pass > pl.pass;
1672        let (next, wrapped) = pl.follows(Some(after_id), ahead, repeat)?;
1673        let pass = pl.pass + u64::from(ahead || wrapped);
1674        let next = next.and_then(|id| pl.find(id));
1675        Some(Lookahead {
1676            after: after_id,
1677            next: next.map(|item| item.id),
1678            chosen: next
1679                .filter(|item| matches!(item.state, ItemState::Ready))
1680                .map(|item| (item.id, item.path.clone())),
1681            wrapped,
1682            boundary: 0,
1683            after_pass,
1684            pass,
1685        })
1686    }
1687
1688    /// Whether the decoder would still take `step`: under the play mode now,
1689    /// `next` still follows `after`. A track it stopped at landing since is
1690    /// not a change, since the advance at the end of the session plays it in
1691    /// order; an edit that puts another track first is, and so is a change of
1692    /// mode that sends the queue elsewhere — a wrap once repeat is off, or a
1693    /// track appended after the one a wrap left from.
1694    pub fn still_follows(&self, step: &Lookahead) -> bool {
1695        let repeat = self.play_mode().repeat;
1696        let mut pl = self.playlist.write();
1697        let ahead = step.after_pass > pl.pass;
1698        pl.follows(Some(step.after), ahead, repeat)
1699            .is_some_and(|(next, _)| next == step.next)
1700    }
1701
1702    /// Retreat cursor to the previous item. Returns (id, path) if found.
1703    /// For prev_track — goes to the item before cursor regardless of load state,
1704    /// and from the first to the last while repeat is on.
1705    ///
1706    /// Shuffled, the previous item is the one played before this, from the
1707    /// play order's history; with none, there is nothing to go back to.
1708    pub fn retreat_cursor(&self) -> Option<(QueueItemId, PathBuf)> {
1709        let mut pl = self.playlist.write();
1710        if pl.order.is_some() {
1711            let cursor = pl.cursor;
1712            let history = &pl.order.as_ref()?.history;
1713            let end =
1714                history.len() - usize::from(cursor.is_some() && history.last() == cursor.as_ref());
1715            let at = history[..end]
1716                .iter()
1717                .rposition(|id| pl.find(*id).is_some())?;
1718            let id = history[at];
1719            let path = pl.find(id)?.path.clone();
1720            let order = pl.order.as_mut()?;
1721            order.history.truncate(at + 1);
1722            pl.cursor = Some(id);
1723            drop(pl);
1724            self.bump_version();
1725            return Some((id, path));
1726        }
1727        let cursor_pos = match pl.cursor {
1728            Some(cid) => pl.items.iter().position(|item| item.id == cid),
1729            None => None,
1730        };
1731
1732        // From the first item, round to the last while the queue repeats. A
1733        // queue of one has nothing to go back to: the caller restarts it.
1734        let wraps = self.play_mode().repeat != Repeat::Off && pl.items.len() > 1;
1735        let prev_pos = cursor_pos.and_then(|p| match p.checked_sub(1) {
1736            None if wraps => Some(pl.items.len() - 1),
1737            prev => prev,
1738        });
1739
1740        match prev_pos {
1741            Some(pos) => {
1742                let item = &pl.items[pos];
1743                let result = (item.id, item.path.clone());
1744                pl.cursor = Some(item.id);
1745                drop(pl);
1746                self.bump_version();
1747                Some(result)
1748            }
1749            None => None,
1750        }
1751    }
1752
1753    // --- Called from resolve thread ---
1754
1755    /// Update the load state of a playlist item.
1756    pub fn update_item_state(&self, id: QueueItemId, new_state: ItemState) {
1757        let pending = new_state == ItemState::Pending;
1758        let mut pl = self.playlist.write();
1759        if let Some(item) = pl.items.iter_mut().find(|item| item.id == id) {
1760            item.state = new_state;
1761        }
1762        drop(pl);
1763        if pending {
1764            self.pending_version.fetch_add(1, Ordering::AcqRel);
1765        }
1766        self.bump_version();
1767    }
1768
1769    /// An item's own state, without the rest of it.
1770    pub fn item_state(&self, id: QueueItemId) -> Option<ItemState> {
1771        let pl = self.playlist.read();
1772        pl.items
1773            .iter()
1774            .find(|item| item.id == id)
1775            .map(|item| item.state.clone())
1776    }
1777
1778    /// Take what a finished download's own tags can add.
1779    ///
1780    /// Streaming starts on partial Symphonia tags, so an item with nothing
1781    /// behind it takes the lot once the whole file is there. An item that came
1782    /// out of the library does not: the record is what the queue was built
1783    /// from and what every other track on it carries, and a file whose tags
1784    /// disagree — a server album titled one way, the file inside titled
1785    /// another — would split its album in two the moment it finished
1786    /// downloading. The duration is the file's to know either way.
1787    pub fn update_item_metadata(
1788        &self,
1789        id: QueueItemId,
1790        title: String,
1791        artist: String,
1792        album_artist: String,
1793        album: String,
1794        duration_ms: Option<u64>,
1795    ) {
1796        let mut pl = self.playlist.write();
1797        let mut retagged = false;
1798        let mut retimed = false;
1799        if let Some(item) = pl.items.iter_mut().find(|item| item.id == id) {
1800            if item.db_id.is_none() {
1801                retagged = item.title != title
1802                    || item.artist != artist
1803                    || item.album_artist != album_artist
1804                    || item.album != album;
1805                item.title = title;
1806                item.artist = artist;
1807                item.album_artist = album_artist;
1808                item.album = album;
1809            }
1810            if let Some(dur) = duration_ms
1811                && item.duration_ms != Some(dur)
1812            {
1813                item.duration_ms = Some(dur);
1814                retimed = true;
1815            }
1816        }
1817        drop(pl);
1818        // Every download landing comes through here. A content change rewrites
1819        // the saved queue and has every client read the whole queue again,
1820        // which on a long queue is not something to do per track: so only new
1821        // tags are one. A duration is corrected on nearly every streamed track
1822        // — a server gives whole seconds, the file milliseconds — and goes to
1823        // clients as a change to that row alone.
1824        if retagged {
1825            self.bump_content();
1826        } else if retimed {
1827            self.bump_version();
1828        }
1829    }
1830
1831    /// Get the playback source for an item if it's ready to play.
1832    /// Returns `None` if not enough data is available yet.
1833    pub fn item_playback_source(&self, id: QueueItemId) -> Option<PlaybackSource> {
1834        let pl = self.playlist.read();
1835        pl.items
1836            .iter()
1837            .find(|item| item.id == id)
1838            .and_then(|item| match self.load_state(item) {
1839                LoadState::Ready => Some(PlaybackSource::Ready(item.path.clone())),
1840                LoadState::Downloading {
1841                    path,
1842                    total,
1843                    bytes_written,
1844                } => {
1845                    let written = bytes_written.load(Ordering::Acquire);
1846                    (written >= STREAM_THRESHOLD).then_some(PlaybackSource::Streaming {
1847                        path,
1848                        bytes_written,
1849                        total,
1850                    })
1851                }
1852                _ => None,
1853            })
1854    }
1855
1856    /// Put back to `Pending` every queue item whose file has gone, and say
1857    /// which they were so they can be fetched again.
1858    ///
1859    /// The queue holds paths, and clearing downloads deletes the files under
1860    /// them. An item left claiming `Ready` opens nothing when it is played —
1861    /// it is not broken, it is a remote track that has to be fetched a second
1862    /// time. Only items with a database row behind them: one without has
1863    /// nowhere to be fetched from, and parking the cursor on it would be worse
1864    /// than letting it fail honestly.
1865    ///
1866    /// The files are looked at with no lock held: a slow or hung mount would
1867    /// otherwise stall the decode thread's lookahead behind the write lock.
1868    pub fn reset_items_with_missing_files(&self) -> Vec<(i64, QueueItemId)> {
1869        self.reset_items_where(|path| !path.exists())
1870    }
1871
1872    fn reset_items_where(
1873        &self,
1874        missing: impl Fn(&std::path::Path) -> bool,
1875    ) -> Vec<(i64, QueueItemId)> {
1876        let ready: Vec<(QueueItemId, i64, PathBuf)> = self
1877            .playlist
1878            .read()
1879            .items
1880            .iter()
1881            .filter(|item| matches!(item.state, ItemState::Ready))
1882            .filter_map(|item| Some((item.id, item.db_id?, item.path.clone())))
1883            .collect();
1884        let gone: Vec<_> = ready.into_iter().filter(|(_, _, p)| missing(p)).collect();
1885        if gone.is_empty() {
1886            return Vec::new();
1887        }
1888
1889        // Only what is still Ready at the path that was looked at: the item
1890        // may have been fetched again, or replaced, in the meantime.
1891        let mut pl = self.playlist.write();
1892        let mut reset = Vec::new();
1893        for item in pl.items.iter_mut() {
1894            if matches!(item.state, ItemState::Ready)
1895                && let Some((_, db_id, _)) = gone
1896                    .iter()
1897                    .find(|(id, _, path)| *id == item.id && *path == item.path)
1898            {
1899                item.state = ItemState::Pending;
1900                reset.push((*db_id, item.id));
1901            }
1902        }
1903        drop(pl);
1904        if !reset.is_empty() {
1905            self.pending_version.fetch_add(1, Ordering::AcqRel);
1906            self.bump_version();
1907        }
1908        reset
1909    }
1910
1911    /// The item's path if it is `Ready`. A caller that can stream wants `item_playback_source`.
1912    pub fn item_path_if_ready(&self, id: QueueItemId) -> Option<PathBuf> {
1913        let pl = self.playlist.read();
1914        pl.items.iter().find(|item| item.id == id).and_then(|item| {
1915            if matches!(item.state, ItemState::Ready) {
1916                Some(item.path.clone())
1917            } else {
1918                None
1919            }
1920        })
1921    }
1922
1923    pub fn is_cursor(&self, id: QueueItemId) -> bool {
1924        self.playlist.read().cursor == Some(id)
1925    }
1926
1927    /// Get QueueItemIds of all playlist items sharing the same album as the given item.
1928    /// Matches on both album name and album artist to avoid false positives
1929    /// (e.g. two different "Greatest Hits" by different artists).
1930    pub fn same_album_item_ids(&self, id: QueueItemId) -> Vec<QueueItemId> {
1931        let pl = self.playlist.read();
1932        let Some(cursor) = pl.items.iter().find(|item| item.id == id) else {
1933            return vec![];
1934        };
1935        let album = cursor.album.clone();
1936        let album_artist = cursor.album_artist.clone();
1937        pl.items
1938            .iter()
1939            .filter(|item| {
1940                item.id != id && item.album == album && item.album_artist == album_artist
1941            })
1942            .map(|item| item.id)
1943            .collect()
1944    }
1945
1946    /// Every playlist item still waiting for its file that has a track to
1947    /// fetch, as `(db_id, QueueItemId)`, in the order the player will reach
1948    /// it — see `reach_order`. What the download queue fetches, and in that
1949    /// order.
1950    pub fn pending_downloads(&self) -> Vec<(i64, QueueItemId)> {
1951        let pl = self.playlist.read();
1952        reach_order(&pl)
1953            .filter(|item| matches!(item.state, ItemState::Pending))
1954            .filter_map(|item| item.db_id.map(|db_id| (db_id, item.id)))
1955            .collect()
1956    }
1957
1958    /// Every entry with a library track, in the order `pending_downloads`
1959    /// gives, whatever its state: what the cache has to hold, downloaded or
1960    /// not, for the player to reach it.
1961    pub fn playback_order(&self) -> Vec<(i64, QueueItemId)> {
1962        let pl = self.playlist.read();
1963        reach_order(&pl)
1964            .filter_map(|item| item.db_id.map(|db_id| (db_id, item.id)))
1965            .collect()
1966    }
1967
1968    /// Get the db_id for a specific playlist item.
1969    pub fn item_db_id(&self, id: QueueItemId) -> Option<i64> {
1970        let pl = self.playlist.read();
1971        pl.items
1972            .iter()
1973            .find(|item| item.id == id)
1974            .and_then(|item| item.db_id)
1975    }
1976
1977    /// Get the load state of a specific playlist item.
1978    pub fn item_load_state(&self, id: QueueItemId) -> Option<LoadState> {
1979        let pl = self.playlist.read();
1980        pl.items
1981            .iter()
1982            .find(|item| item.id == id)
1983            .map(|item| self.load_state(item))
1984    }
1985
1986    // --- Snapshot helpers for undo ---
1987
1988    /// Get the full playlist snapshot (items + cursor) for undo of ClearPlaylist.
1989    pub fn snapshot_playlist(&self) -> (Vec<PlaylistItem>, Option<QueueItemId>) {
1990        let pl = self.playlist.read();
1991        (pl.items.clone(), pl.cursor)
1992    }
1993
1994    /// Get an item by ID (for undo of RemoveFromPlaylist).
1995    pub fn get_item(&self, id: QueueItemId) -> Option<PlaylistItem> {
1996        let pl = self.playlist.read();
1997        pl.items.iter().find(|item| item.id == id).cloned()
1998    }
1999
2000    /// Get the ID of the item immediately before the given ID (None if first).
2001    pub fn item_before(&self, id: QueueItemId) -> Option<QueueItemId> {
2002        let pl = self.playlist.read();
2003        let pos = pl.items.iter().position(|item| item.id == id)?;
2004        if pos == 0 {
2005            None
2006        } else {
2007            Some(pl.items[pos - 1].id)
2008        }
2009    }
2010
2011    /// Put the items in exactly this order.
2012    ///
2013    /// Items not named keep their relative order and follow at the end, so a
2014    /// stale order cannot lose anything. The items themselves are moved, not
2015    /// rebuilt: their ids, load states and download progress are what the rest
2016    /// of the player is holding on to.
2017    pub fn reorder_to(&self, order: &[QueueItemId]) {
2018        let mut pl = self.playlist.write();
2019        let mut taken: Vec<Option<PlaylistItem>> = pl.items.drain(..).map(Some).collect();
2020        let mut sorted = Vec::with_capacity(taken.len());
2021        for id in order {
2022            if let Some(slot) = taken
2023                .iter_mut()
2024                .find(|i| i.as_ref().is_some_and(|i| i.id == *id))
2025                && let Some(item) = slot.take()
2026            {
2027                sorted.push(item);
2028            }
2029        }
2030        sorted.extend(taken.into_iter().flatten());
2031        pl.items = sorted;
2032        drop(pl);
2033        self.bump_content();
2034    }
2035
2036    /// For each ID, the ID of the item before it (or None if first), returned in
2037    /// playlist order regardless of the order `ids` arrives in.
2038    ///
2039    /// Undo replays these left to right, so an item whose recorded predecessor is
2040    /// also in `ids` must come after it — otherwise the predecessor is missing at
2041    /// replay time and the item lands at the end of the playlist instead.
2042    pub fn items_before(&self, ids: &[QueueItemId]) -> Vec<(QueueItemId, Option<QueueItemId>)> {
2043        use std::collections::HashSet;
2044        let wanted: HashSet<QueueItemId> = ids.iter().copied().collect();
2045        let pl = self.playlist.read();
2046        pl.items
2047            .iter()
2048            .enumerate()
2049            .filter(|(_, item)| wanted.contains(&item.id))
2050            .map(|(pos, item)| {
2051                let before = if pos == 0 {
2052                    None
2053                } else {
2054                    Some(pl.items[pos - 1].id)
2055                };
2056                (item.id, before)
2057            })
2058            .collect()
2059    }
2060
2061    /// The nearest item before `id` that is not itself being removed — where
2062    /// playback resumes from after a batch delete that takes out the cursor.
2063    /// `None` means resume from the top of what survives.
2064    pub fn surviving_item_before(
2065        &self,
2066        id: QueueItemId,
2067        removed: &[QueueItemId],
2068    ) -> Option<QueueItemId> {
2069        use std::collections::HashSet;
2070        let removed: HashSet<QueueItemId> = removed.iter().copied().collect();
2071        let pl = self.playlist.read();
2072        let pos = pl.items.iter().position(|item| item.id == id)?;
2073        pl.items[..pos]
2074            .iter()
2075            .rev()
2076            .find(|item| !removed.contains(&item.id))
2077            .map(|item| item.id)
2078    }
2079
2080    /// Restore a full playlist from snapshot (for redo of ClearPlaylist undo).
2081    pub fn restore_playlist(&self, items: Vec<PlaylistItem>, cursor: Option<QueueItemId>) {
2082        let mut pl = self.playlist.write();
2083        pl.items = items;
2084        pl.cursor = cursor;
2085        drop(pl);
2086        self.bump_content();
2087    }
2088
2089    /// The queue's items with uids, by item, with their row and file: what
2090    /// `helpers::queue_relinks` finds each track again by.
2091    pub fn queue_uids(&self) -> Vec<(QueueItemId, Option<i64>, String, PathBuf)> {
2092        self.playlist
2093            .read()
2094            .items
2095            .iter()
2096            .filter_map(|i| Some((i.id, i.db_id, i.uid.clone()?, i.path.clone())))
2097            .collect()
2098    }
2099
2100    /// Point items at the rows their tracks now hold, or at none. The queue's
2101    /// content changes only if an item's row does.
2102    ///
2103    /// An item left with no row and no file has nothing to fetch it by: it
2104    /// fails, rather than waiting for a download nothing will start. One that
2105    /// failed for want of its file goes back to waiting when its track
2106    /// returns, so the download queue takes it up again.
2107    pub fn relink(&self, links: &[(QueueItemId, Option<i64>)]) {
2108        let mut changed = false;
2109        {
2110            let mut pl = self.playlist.write();
2111            for &(id, db_id) in links {
2112                if let Some(item) = pl.items.iter_mut().find(|i| i.id == id)
2113                    && item.db_id != db_id
2114                {
2115                    item.db_id = db_id;
2116                    item.state = relinked_state(&item.state, db_id.is_some(), &item.path);
2117                    changed = true;
2118                }
2119            }
2120        }
2121        if changed {
2122            self.bump_content();
2123        }
2124    }
2125
2126    /// Remove multiple items by IDs.
2127    pub fn remove_items(&self, ids: &[QueueItemId]) {
2128        use std::collections::HashSet;
2129        let id_set: HashSet<QueueItemId> = ids.iter().copied().collect();
2130        let mut pl = self.playlist.write();
2131        pl.items.retain(|item| !id_set.contains(&item.id));
2132        if let Some(cursor) = pl.cursor
2133            && id_set.contains(&cursor)
2134        {
2135            pl.cursor = None;
2136        }
2137        drop(pl);
2138        self.bump_content();
2139    }
2140
2141    /// Insert a single item after a given ID (or at front if None).
2142    pub fn insert_item_at(&self, item: PlaylistItem, after: Option<QueueItemId>) {
2143        let mut pl = self.playlist.write();
2144        let insert_at = match after {
2145            Some(after_id) => {
2146                match pl.items.iter().position(|i| i.id == after_id) {
2147                    Some(pos) => pos + 1,
2148                    None => pl.items.len(), // fallback
2149                }
2150            }
2151            None => 0,
2152        };
2153        pl.items.insert(insert_at, item);
2154        drop(pl);
2155        self.bump_content();
2156    }
2157
2158    /// Move a single item to after `after` (or to front if None).
2159    pub fn move_item_to(&self, id: QueueItemId, after: Option<QueueItemId>) {
2160        let mut pl = self.playlist.write();
2161        let Some(from) = pl.items.iter().position(|item| item.id == id) else {
2162            return;
2163        };
2164        let item = pl.items.remove(from);
2165        let insert_at = match after {
2166            Some(after_id) => match pl.items.iter().position(|i| i.id == after_id) {
2167                Some(pos) => pos + 1,
2168                None => pl.items.len(),
2169            },
2170            None => 0,
2171        };
2172        pl.items.insert(insert_at, item);
2173        drop(pl);
2174        self.bump_content();
2175    }
2176
2177    /// Batch move: reposition each item to after its given predecessor.
2178    /// Processes in order so earlier insertions don't corrupt later positions.
2179    pub fn move_items_to(&self, entries: &[(QueueItemId, Option<QueueItemId>)]) {
2180        for &(id, after) in entries {
2181            self.move_item_to(id, after);
2182        }
2183    }
2184
2185    // --- Called from UI thread (read lock) ---
2186
2187    /// Derive the visible queue from the playlist + cursor. O(n), and a copy
2188    /// of every row's text: for a front end that draws the whole queue. One
2189    /// that only needs to know what moved reads `queue_readings`.
2190    pub fn derive_visible_queue(&self) -> VisibleQueueSnapshot {
2191        let mut entries = Vec::with_capacity(self.playlist.read().items.len());
2192        let mut finished_count = 0;
2193        let mut has_playing = false;
2194        let mut queue_count = 0;
2195        self.each_visible(|item, place, reading| {
2196            match place {
2197                Place::Played => finished_count += 1,
2198                Place::Cursor => has_playing = true,
2199                Place::Unplayed => queue_count += 1,
2200            }
2201            entries.push(QueueEntry {
2202                id: item.id,
2203                db_id: item.db_id,
2204                playlist_entry_id: item.playlist_entry_id,
2205                path: item.path.clone(),
2206                title: item.title.clone(),
2207                artist: item.artist.clone(),
2208                album_artist: item.album_artist.clone(),
2209                album: item.album.clone(),
2210                year: item.year.clone(),
2211                codec: item.codec.clone(),
2212                track_number: item.track_number,
2213                disc: item.disc,
2214                duration_ms: reading.duration_ms,
2215                status: reading.status,
2216                download_progress: reading.download_progress,
2217                error: reading.error,
2218            });
2219        });
2220
2221        VisibleQueueSnapshot {
2222            entries,
2223            finished_count,
2224            has_playing,
2225            queue_count,
2226        }
2227    }
2228
2229    /// What each row of the visible queue says that can change without the
2230    /// queue being edited — its status, its duration, why it failed — in
2231    /// queue order, and none of its text.
2232    ///
2233    /// The cursor moving and a download landing change these and nothing
2234    /// else. A front end holding the rows already can find what moved from
2235    /// this at a small fraction of what deriving the whole queue costs.
2236    pub fn queue_readings(&self) -> Vec<QueueReading> {
2237        let mut readings = Vec::with_capacity(self.playlist.read().items.len());
2238        self.each_visible(|_, _, reading| readings.push(reading));
2239        readings
2240    }
2241
2242    /// Walk the playlist under its read lock, with each item's place — the
2243    /// cursor, played or not — and its reading. The one place a row's status
2244    /// is decided, so the derived queue and its readings cannot disagree.
2245    ///
2246    /// Played is the item's own mark, never its position: a row behind the
2247    /// cursor that was jumped over or not yet reached by shuffle is queued.
2248    fn each_visible(&self, mut f: impl FnMut(&PlaylistItem, Place, QueueReading)) {
2249        // Read before the playlist lock — see current_download_fraction.
2250        let playing_duration_ms = self.track_info.read().as_ref().map(|ti| ti.duration_ms);
2251        // One pass over the transfers rather than one lookup, and a path
2252        // cloned, per row.
2253        let transfers: HashMap<i64, (u64, u64)> = self
2254            .downloads
2255            .readings()
2256            .into_iter()
2257            .map(|r| (r.track_id, (r.written, r.total)))
2258            .collect();
2259        let unreachable = crate::helpers::server_unreachable();
2260        let pl = self.playlist.read();
2261
2262        for item in &pl.items {
2263            let place = if pl.cursor == Some(item.id) {
2264                Place::Cursor
2265            } else if item.played {
2266                Place::Played
2267            } else {
2268                Place::Unplayed
2269            };
2270
2271            // The byte count is the download thread's own counter, written per
2272            // chunk without the playlist lock, so a transfer never bumps the
2273            // playlist version.
2274            let download_progress = match item.state {
2275                ItemState::Pending => item.db_id.and_then(|id| transfers.get(&id).copied()),
2276                _ => None,
2277            };
2278            let transferring = download_progress.is_some();
2279
2280            let status = match (place, &item.state) {
2281                (Place::Cursor, state) => QueueEntryStatus::at_cursor(state, transferring),
2282                (_, ItemState::Failed(_)) => QueueEntryStatus::Failed,
2283                (_, ItemState::Pending) if transferring => QueueEntryStatus::Downloading,
2284                (Place::Played, _) => QueueEntryStatus::Played,
2285                // Waiting its turn, not arriving: a spinner on every one of
2286                // these read as the whole album downloading at once.
2287                (Place::Unplayed, _) => QueueEntryStatus::Queued,
2288            };
2289
2290            // The playing track's duration from its stream, when the item
2291            // had none of its own.
2292            let duration_ms = if status == QueueEntryStatus::Playing && item.duration_ms.is_none() {
2293                playing_duration_ms
2294            } else {
2295                item.duration_ms
2296            };
2297
2298            f(
2299                item,
2300                place,
2301                QueueReading {
2302                    id: item.id,
2303                    db_id: item.db_id,
2304                    status,
2305                    duration_ms,
2306                    download_progress,
2307                    error: match (place, &item.state) {
2308                        (_, ItemState::Failed(reason)) => Some(reason.clone()),
2309                        // Waiting for a server that is not answering: why it
2310                        // has not started, though it will once the server is
2311                        // back.
2312                        (Place::Cursor, ItemState::Pending) => unreachable.clone(),
2313                        _ => None,
2314                    },
2315                },
2316            );
2317        }
2318    }
2319
2320    /// At most `max` items from `before` ahead of the cursor, and the cursor.
2321    ///
2322    /// What a window of the queue needs, without copying the rest of it: on a
2323    /// queue of tens of thousands, `snapshot_playlist` is a copy of every row
2324    /// for the sake of a few hundred.
2325    pub fn playlist_window(
2326        &self,
2327        before: usize,
2328        max: usize,
2329    ) -> (Vec<PlaylistItem>, Option<QueueItemId>) {
2330        let pl = self.playlist.read();
2331        let at = pl
2332            .cursor
2333            .and_then(|c| pl.items.iter().position(|i| i.id == c))
2334            .unwrap_or(0);
2335        let start = at.saturating_sub(before);
2336        let end = pl.items.len().min(start + max);
2337        (pl.items[start..end].to_vec(), pl.cursor)
2338    }
2339}
2340
2341/// Every item once, in the order the player will reach it: from the cursor
2342/// to the end, then from the top — the tracks before the cursor are the ones
2343/// least likely to be played next. Shuffled, the cursor and then the play
2344/// order go first, and the rest follow in that order.
2345fn reach_order(pl: &Playlist) -> impl Iterator<Item = &PlaylistItem> {
2346    let from = pl
2347        .cursor
2348        .and_then(|c| pl.items.iter().position(|item| item.id == c))
2349        .unwrap_or(0);
2350    let (before, after) = pl.items.split_at(from);
2351    let queue = after.iter().chain(before);
2352    let mut first: Vec<&PlaylistItem> = Vec::new();
2353    if let Some(order) = &pl.order {
2354        let rows: HashMap<QueueItemId, &PlaylistItem> =
2355            pl.items.iter().map(|item| (item.id, item)).collect();
2356        first.extend(pl.cursor.and_then(|c| rows.get(&c).copied()));
2357        first.extend(order.upcoming.iter().filter_map(|id| rows.get(id).copied()));
2358    }
2359    let led: std::collections::HashSet<QueueItemId> = first.iter().map(|item| item.id).collect();
2360    first
2361        .into_iter()
2362        .chain(queue.filter(move |item| !led.contains(&item.id)))
2363}
2364
2365/// Whether a row is the cursor's, or has played or is yet to.
2366#[derive(Clone, Copy)]
2367enum Place {
2368    Played,
2369    Cursor,
2370    Unplayed,
2371}
2372
2373#[cfg(test)]
2374mod tests {
2375    use super::*;
2376
2377    // --- helpers ---
2378
2379    fn make_item(title: &str, state: ItemState) -> PlaylistItem {
2380        PlaylistItem {
2381            uid: None,
2382            playlist_entry_id: None,
2383            id: QueueItemId::new(),
2384            db_id: None,
2385            path: PathBuf::from(format!("/music/{title}.flac")),
2386            title: title.to_string(),
2387            artist: "Artist".to_string(),
2388            album_artist: "Artist".to_string(),
2389            album: "Album".to_string(),
2390            year: None,
2391            codec: Some("FLAC".to_string()),
2392            track_number: None,
2393            disc: None,
2394            duration_ms: Some(200_000),
2395            state,
2396            played: false,
2397        }
2398    }
2399
2400    // --- missing files ---
2401
2402    #[test]
2403    fn missing_files_are_checked_with_no_lock_held() {
2404        let state = SharedPlayerState::new();
2405        let gone = PlaylistItem {
2406            db_id: Some(1),
2407            ..ready_item("gone")
2408        };
2409        let kept = PlaylistItem {
2410            db_id: Some(2),
2411            ..ready_item("kept")
2412        };
2413        let loose = ready_item("no-db-row");
2414        let (gone_id, kept_id, loose_id) = (gone.id, kept.id, loose.id);
2415        state.add_items(vec![gone, kept, loose]);
2416
2417        let reset = state.reset_items_where(|path| {
2418            // A write lock held across the look would refuse this.
2419            assert!(
2420                state.playlist.try_write().is_some(),
2421                "the playlist is locked while its files are checked"
2422            );
2423            path.ends_with("gone.flac") || path.ends_with("no-db-row.flac")
2424        });
2425
2426        assert_eq!(reset, vec![(1, gone_id)]);
2427        let pl = state.playlist.read();
2428        let state_of = |id| pl.items.iter().find(|i| i.id == id).unwrap().state.clone();
2429        assert!(matches!(state_of(gone_id), ItemState::Pending));
2430        assert!(matches!(state_of(kept_id), ItemState::Ready));
2431        assert!(matches!(state_of(loose_id), ItemState::Ready));
2432    }
2433
2434    // --- shuffle ---
2435
2436    /// `n` ready items, each its own library track.
2437    fn tracks(n: usize) -> Vec<PlaylistItem> {
2438        (0..n)
2439            .map(|i| PlaylistItem {
2440                db_id: Some(i as i64 + 1),
2441                ..make_item(&format!("t{i}"), ItemState::Ready)
2442            })
2443            .collect()
2444    }
2445
2446    fn shuffled(items: Vec<PlaylistItem>) -> (Arc<SharedPlayerState>, Vec<QueueItemId>) {
2447        let state = SharedPlayerState::new();
2448        let ids = items.iter().map(|i| i.id).collect();
2449        state.add_items(items);
2450        state.set_shuffled(true);
2451        (state, ids)
2452    }
2453
2454    fn queue_ids(state: &SharedPlayerState) -> Vec<QueueItemId> {
2455        state.snapshot_playlist().0.iter().map(|i| i.id).collect()
2456    }
2457
2458    /// Play what is under the cursor, then move on as the end of a track
2459    /// does: what the advance lands on.
2460    fn play_on(state: &SharedPlayerState) -> Option<QueueItemId> {
2461        if let Some(id) = state.cursor() {
2462            state.mark_played(id);
2463        }
2464        state.advance_cursor_loadable()
2465    }
2466
2467    fn status_of(state: &SharedPlayerState, id: QueueItemId) -> QueueEntryStatus {
2468        let snap = state.derive_visible_queue();
2469        snap.entries.iter().find(|e| e.id == id).unwrap().status
2470    }
2471
2472    fn set_repeat(state: &SharedPlayerState, repeat: Repeat) {
2473        let shuffle = state.play_mode().shuffle;
2474        state.set_play_mode(PlayMode { shuffle, repeat });
2475    }
2476
2477    #[test]
2478    fn shuffle_on_and_off_never_moves_the_queue() {
2479        let state = SharedPlayerState::new();
2480        let items = tracks(20);
2481        let ids: Vec<_> = items.iter().map(|i| i.id).collect();
2482        state.add_items(items);
2483        state.set_cursor(Some(ids[3]));
2484
2485        state.set_shuffled(true);
2486        assert_eq!(queue_ids(&state), ids);
2487        for _ in 0..6 {
2488            play_on(&state).unwrap();
2489        }
2490        assert_eq!(queue_ids(&state), ids, "playing shuffled moves nothing");
2491        state.set_shuffled(false);
2492        assert_eq!(queue_ids(&state), ids);
2493    }
2494
2495    #[test]
2496    fn the_rows_that_played_shuffled_stay_marked_once_it_is_off() {
2497        let (state, ids) = shuffled(tracks(20));
2498        let mut heard = vec![state.advance_cursor_loadable().unwrap()];
2499        for _ in 0..7 {
2500            heard.push(play_on(&state).unwrap());
2501        }
2502        let playing = state.cursor().unwrap();
2503        state.set_shuffled(false);
2504
2505        for &id in &ids {
2506            let expected = if id == playing {
2507                QueueEntryStatus::Playing
2508            } else if heard.contains(&id) {
2509                QueueEntryStatus::Played
2510            } else {
2511                QueueEntryStatus::Queued
2512            };
2513            assert_eq!(
2514                status_of(&state, id),
2515                expected,
2516                "row {}",
2517                ids.iter().position(|i| *i == id).unwrap()
2518            );
2519        }
2520        let snap = state.derive_visible_queue();
2521        assert_eq!(snap.finished_count, 7);
2522        assert_eq!(snap.queue_count, 12);
2523
2524        // Off, the queue plays on in its own order from the cursor.
2525        let at = ids.iter().position(|id| *id == playing).unwrap();
2526        assert_eq!(play_on(&state), ids.get(at + 1).copied());
2527    }
2528
2529    #[test]
2530    fn a_row_is_played_because_it_played_not_for_being_behind_the_cursor() {
2531        let state = SharedPlayerState::new();
2532        let items = tracks(6);
2533        let ids: Vec<_> = items.iter().map(|i| i.id).collect();
2534        state.add_items(items);
2535        state.set_cursor(Some(ids[0]));
2536        play_on(&state);
2537        state.set_cursor(Some(ids[4]));
2538
2539        assert_eq!(status_of(&state, ids[0]), QueueEntryStatus::Played);
2540        for &id in &ids[1..4] {
2541            assert_eq!(
2542                status_of(&state, id),
2543                QueueEntryStatus::Queued,
2544                "jumped over"
2545            );
2546        }
2547    }
2548
2549    #[test]
2550    fn a_shuffled_pass_plays_every_track_once() {
2551        let (state, ids) = shuffled(tracks(30));
2552        let mut heard = vec![state.advance_cursor_loadable().unwrap()];
2553        while let Some(id) = play_on(&state) {
2554            heard.push(id);
2555        }
2556        assert_ne!(heard, ids, "a shuffled order");
2557        let mut sorted = heard.clone();
2558        sorted.sort_by_key(|id| ids.iter().position(|i| i == id));
2559        assert_eq!(sorted, ids, "each once");
2560    }
2561
2562    #[test]
2563    fn a_track_queued_twice_plays_once_a_pass() {
2564        let mut items = tracks(5);
2565        let twins: Vec<_> = [1, 3, 3]
2566            .iter()
2567            .map(|&i| PlaylistItem {
2568                id: QueueItemId::new(),
2569                ..items[i].clone()
2570            })
2571            .collect();
2572        items.extend(twins);
2573        let (state, _) = shuffled(items);
2574        let track = |id| state.item_db_id(id).unwrap();
2575
2576        let mut heard = vec![track(state.advance_cursor_loadable().unwrap())];
2577        while let Some(id) = play_on(&state) {
2578            heard.push(track(id));
2579        }
2580        heard.sort();
2581        assert_eq!(heard, vec![1, 2, 3, 4, 5]);
2582    }
2583
2584    #[test]
2585    fn rows_queued_while_shuffled_play_later_in_the_pass() {
2586        let (state, ids) = shuffled(tracks(10));
2587        let mut heard = vec![state.advance_cursor_loadable().unwrap()];
2588        for _ in 0..3 {
2589            heard.push(play_on(&state).unwrap());
2590        }
2591        let more: Vec<_> = (10..15)
2592            .map(|i| PlaylistItem {
2593                db_id: Some(i + 1),
2594                ..make_item(&format!("t{i}"), ItemState::Ready)
2595            })
2596            .collect();
2597        let added: Vec<_> = more.iter().map(|i| i.id).collect();
2598        state.add_items(more);
2599        assert_eq!(queue_ids(&state)[10..], added, "added where they were put");
2600
2601        while let Some(id) = play_on(&state) {
2602            heard.push(id);
2603        }
2604        assert_eq!(heard.len(), 15);
2605        assert!(added.iter().all(|id| heard[4..].contains(id)));
2606        assert!(ids.iter().all(|id| heard.contains(id)));
2607    }
2608
2609    #[test]
2610    fn a_row_removed_while_shuffled_does_not_play() {
2611        let (state, ids) = shuffled(tracks(10));
2612        let mut heard = vec![state.advance_cursor_loadable().unwrap()];
2613        let gone = *ids.iter().find(|id| !heard.contains(id)).unwrap();
2614        state.remove_items(&[gone]);
2615        while let Some(id) = play_on(&state) {
2616            heard.push(id);
2617        }
2618        assert_eq!(heard.len(), 9);
2619        assert!(!heard.contains(&gone));
2620    }
2621
2622    #[test]
2623    fn previous_goes_back_along_what_played_while_shuffled() {
2624        let (state, ids) = shuffled(tracks(12));
2625        let mut heard = vec![state.advance_cursor_loadable().unwrap()];
2626        for _ in 0..4 {
2627            heard.push(play_on(&state).unwrap());
2628        }
2629        assert_eq!(state.retreat_cursor().map(|(id, _)| id), Some(heard[3]));
2630        assert_eq!(state.retreat_cursor().map(|(id, _)| id), Some(heard[2]));
2631        assert_eq!(state.retreat_cursor().map(|(id, _)| id), Some(heard[1]));
2632        assert_eq!(state.retreat_cursor().map(|(id, _)| id), Some(heard[0]));
2633        assert_eq!(state.retreat_cursor(), None, "nothing before the first");
2634        assert_eq!(state.cursor(), Some(heard[0]));
2635
2636        // Back is not a new pass: what has played does not come round again.
2637        let next = play_on(&state).unwrap();
2638        assert!(!heard.contains(&next));
2639        assert_eq!(queue_ids(&state), ids);
2640    }
2641
2642    #[test]
2643    fn a_repeating_shuffled_queue_starts_a_new_pass_once_every_track_played() {
2644        let (state, ids) = shuffled(tracks(8));
2645        set_repeat(&state, Repeat::Queue);
2646        let mut heard = vec![state.advance_cursor_loadable().unwrap()];
2647        for _ in 0..(3 * ids.len() - 1) {
2648            heard.push(play_on(&state).unwrap());
2649        }
2650        for (n, pass) in heard.chunks(ids.len()).enumerate() {
2651            let mut sorted = pass.to_vec();
2652            sorted.sort_by_key(|id| ids.iter().position(|i| i == id));
2653            assert_eq!(sorted, ids, "pass {n} plays each track once");
2654        }
2655        for turn in heard.windows(2) {
2656            assert_ne!(turn[0], turn[1], "no track twice running at a turn");
2657        }
2658
2659        // The third pass's last track playing: the rest of it has played.
2660        // Moving on to the fourth clears the marks.
2661        let snap = state.derive_visible_queue();
2662        assert_eq!(snap.finished_count, ids.len() - 1);
2663        play_on(&state);
2664        let snap = state.derive_visible_queue();
2665        assert_eq!(snap.finished_count, 0, "a new pass clears the marks");
2666        assert!(snap.has_playing);
2667    }
2668
2669    #[test]
2670    fn repeating_in_order_starts_a_new_pass_at_the_top() {
2671        let state = SharedPlayerState::new();
2672        let items = tracks(4);
2673        let ids: Vec<_> = items.iter().map(|i| i.id).collect();
2674        state.add_items(items);
2675        set_repeat(&state, Repeat::Queue);
2676        state.set_cursor(Some(ids[0]));
2677        for _ in 0..4 {
2678            play_on(&state);
2679        }
2680        assert_eq!(state.cursor(), Some(ids[0]));
2681        for &id in &ids[1..] {
2682            assert_eq!(status_of(&state, id), QueueEntryStatus::Queued);
2683        }
2684    }
2685
2686    #[test]
2687    fn the_lookahead_picks_what_the_advance_plays() {
2688        let (state, ids) = shuffled(tracks(7));
2689        set_repeat(&state, Repeat::Queue);
2690        state.advance_cursor_loadable();
2691        // Across three passes, the turns between them included: each step the
2692        // decoder would queue — two ahead, as it looks ahead — is the step
2693        // the cursor then takes.
2694        for _ in 0..(3 * ids.len()) {
2695            let cursor = state.cursor().unwrap();
2696            let next = state.lookahead_after(cursor).unwrap();
2697            let after = state.lookahead_from(&next).unwrap();
2698            assert!(state.still_follows(&next));
2699            assert_eq!(play_on(&state), next.next);
2700            assert_eq!(play_on(&state), after.next);
2701            assert_eq!(next.chosen.map(|(id, _)| id), next.next);
2702        }
2703    }
2704
2705    #[test]
2706    fn a_lookahead_steps_deep_crosses_into_the_next_pass_as_the_advance_does() {
2707        let (state, ids) = shuffled(tracks(4));
2708        set_repeat(&state, Repeat::Queue);
2709        state.advance_cursor_loadable();
2710        // As the decoder chains steps when tracks are shorter than the ring:
2711        // the rest of this pass and all of the next, before the playhead
2712        // reaches any of them.
2713        let mut step = state.lookahead_after(state.cursor().unwrap()).unwrap();
2714        let mut chain = vec![step.next.unwrap()];
2715        for _ in 1..(ids.len() - 1 + ids.len()) {
2716            step = state.lookahead_from(&step).unwrap();
2717            chain.push(step.next.unwrap());
2718        }
2719        let played: Vec<_> = chain.iter().map(|_| play_on(&state).unwrap()).collect();
2720        assert_eq!(played, chain);
2721        let mut next_pass = chain[ids.len() - 1..].to_vec();
2722        next_pass.sort_by_key(|id| ids.iter().position(|i| i == id));
2723        assert_eq!(next_pass, ids, "the next pass whole, not this one again");
2724    }
2725
2726    #[test]
2727    fn a_played_out_shuffled_queue_plays_again_from_a_row_picked() {
2728        let (state, ids) = shuffled(tracks(5));
2729        state.advance_cursor_loadable();
2730        while play_on(&state).is_some() {}
2731
2732        state.pick(ids[2]);
2733        let mut heard = vec![ids[2]];
2734        while let Some(id) = play_on(&state) {
2735            heard.push(id);
2736        }
2737        heard.sort_by_key(|id| ids.iter().position(|i| i == id));
2738        assert_eq!(heard, ids, "a new pass from the row picked");
2739    }
2740
2741    #[test]
2742    fn shuffling_a_played_out_queue_plays_it_again() {
2743        let state = SharedPlayerState::new();
2744        let items = tracks(5);
2745        let ids: Vec<_> = items.iter().map(|i| i.id).collect();
2746        state.add_items(items);
2747        state.set_cursor(Some(ids[0]));
2748        while play_on(&state).is_some() {}
2749
2750        state.set_shuffled(true);
2751        let mut heard = vec![state.cursor().unwrap()];
2752        while let Some(id) = play_on(&state) {
2753            heard.push(id);
2754        }
2755        assert_eq!(heard.len(), ids.len());
2756    }
2757
2758    #[test]
2759    fn the_downloads_follow_the_play_order() {
2760        let state = SharedPlayerState::new();
2761        let items: Vec<_> = tracks(10)
2762            .into_iter()
2763            .map(|item| PlaylistItem {
2764                state: ItemState::Pending,
2765                ..item
2766            })
2767            .collect();
2768        state.add_items(items);
2769        state.set_shuffled(true);
2770        let first = state.advance_cursor_loadable().unwrap();
2771        let second = state.lookahead_after(first).unwrap().next.unwrap();
2772        let order = state.pending_downloads();
2773        assert_eq!(order[0].1, first);
2774        assert_eq!(order[1].1, second);
2775        assert_eq!(order.len(), 10);
2776    }
2777
2778    /// An item with a transfer running against it, told to the state's store
2779    /// the way the downloader tells it. Returns the transfer's byte feed.
2780    fn downloading_item(
2781        state: &SharedPlayerState,
2782        title: &str,
2783        total: u64,
2784    ) -> (PlaylistItem, Arc<ByteFeed>) {
2785        static NEXT_TRACK: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(1);
2786        let mut item = make_item(title, ItemState::Pending);
2787        item.db_id = Some(NEXT_TRACK.fetch_add(1, Ordering::Relaxed));
2788        let feed = start_transfer(state, &item, total);
2789        (item, feed)
2790    }
2791
2792    /// Claim, announce and start the transfer for `item`'s track.
2793    fn start_transfer(state: &SharedPlayerState, item: &PlaylistItem, total: u64) -> Arc<ByteFeed> {
2794        let track_id = item.db_id.expect("a transfer is for a library track");
2795        let store = state.downloads();
2796        store.claim(track_id, Some(item.id));
2797        let feed = store.announce(
2798            track_id,
2799            item.title.clone(),
2800            String::new(),
2801            PathBuf::from(format!("/cache/{}.flac.part", item.title)),
2802            PathBuf::from(format!("/cache/{}.flac", item.title)),
2803        );
2804        store.started(track_id, total);
2805        feed
2806    }
2807
2808    fn ready_item(title: &str) -> PlaylistItem {
2809        make_item(title, ItemState::Ready)
2810    }
2811
2812    fn pending_item(title: &str) -> PlaylistItem {
2813        make_item(title, ItemState::Pending)
2814    }
2815
2816    fn failed_item(title: &str) -> PlaylistItem {
2817        make_item(title, ItemState::Failed("nope".into()))
2818    }
2819
2820    const DURATION_MS: u64 = 32_523_787;
2821
2822    /// A nine-hour track under the cursor, `downloaded` bytes of `total` in.
2823    /// `total` of 0 stands for a server that sent no Content-Length.
2824    fn streaming_state(
2825        downloaded: u64,
2826        total: u64,
2827        bitrate_kbps: Option<u32>,
2828    ) -> Arc<SharedPlayerState> {
2829        streaming_state_with_duration(downloaded, total, bitrate_kbps, DURATION_MS)
2830    }
2831
2832    /// The same, but saying what the container managed to state about itself.
2833    /// A partial Ogg states nothing, which is zero here.
2834    fn streaming_state_with_duration(
2835        downloaded: u64,
2836        total: u64,
2837        bitrate_kbps: Option<u32>,
2838        container_duration_ms: u64,
2839    ) -> Arc<SharedPlayerState> {
2840        let mut item = make_item("train", ItemState::Pending);
2841        item.db_id = Some(1);
2842        let id = item.id;
2843        let path = item.path.clone();
2844
2845        let state = SharedPlayerState::new();
2846        start_transfer(&state, &item, total).set(downloaded);
2847        state.add_items(vec![item]);
2848        state.set_cursor(Some(id));
2849        state.set_track_info(Some(TrackInfo {
2850            id,
2851            path,
2852            codec: "Opus".into(),
2853            sample_rate: 48_000,
2854            bit_depth: None,
2855            bitrate_kbps,
2856            channels: 2,
2857            duration_ms: container_duration_ms,
2858        }));
2859        state
2860    }
2861
2862    // --- seekable_ms ---
2863
2864    #[test]
2865    fn a_track_on_disk_is_seekable_end_to_end() {
2866        let item = ready_item("done");
2867        let id = item.id;
2868        let path = item.path.clone();
2869        let state = SharedPlayerState::new();
2870        state.add_items(vec![item]);
2871        state.set_cursor(Some(id));
2872        state.set_track_info(Some(TrackInfo {
2873            id,
2874            path,
2875            codec: "FLAC".into(),
2876            sample_rate: 44_100,
2877            bit_depth: Some(16),
2878            bitrate_kbps: None,
2879            channels: 2,
2880            duration_ms: 200_000,
2881        }));
2882
2883        assert_eq!(state.seekable_ms(), 200_000);
2884        // Nothing to draw a boundary for, so front ends are told there isn't one.
2885        assert_eq!(state.seek_ceiling_ms(), None);
2886    }
2887
2888    #[test]
2889    fn a_downloading_track_is_seekable_as_far_as_its_bytes_reach() {
2890        // A quarter of a nine-hour file in: a quarter of the way through it,
2891        // less the margin the byte-to-time estimate is worth.
2892        let state = streaming_state(100, 400, None);
2893        assert_eq!(state.seekable_ms(), 32_523_787 / 4 - SEEK_SAFETY_MS);
2894        assert_eq!(
2895            state.seek_ceiling_ms(),
2896            Some(32_523_787 / 4 - SEEK_SAFETY_MS)
2897        );
2898    }
2899
2900    #[test]
2901    fn a_transfer_without_a_content_length_falls_back_to_bitrate() {
2902        // No total to take a fraction of. 128 kbps is 128 bits per ms, so a
2903        // megabyte is 8 388 608 bits and a little over 65 seconds.
2904        let state = streaming_state(1024 * 1024, 0, Some(128));
2905        assert_eq!(state.seekable_ms(), 1024 * 1024 * 8 / 128 - SEEK_SAFETY_MS);
2906    }
2907
2908    #[test]
2909    fn nothing_to_estimate_from_allows_no_forward_seek() {
2910        // Neither a length nor a bitrate: anywhere past the playhead is a
2911        // guess, and a guess that lands past the write head is a stall.
2912        let state = streaming_state(1024 * 1024, 0, None);
2913        state.set_position_ms(12_000);
2914        assert_eq!(state.seekable_ms(), 12_000);
2915    }
2916
2917    #[test]
2918    fn the_seekable_extent_never_exceeds_the_track() {
2919        // A download reporting more bytes than it advertised must not offer a
2920        // seek past the end of the music.
2921        let state = streaming_state(500, 400, None);
2922        assert_eq!(state.seekable_ms(), 32_523_787);
2923    }
2924
2925    #[test]
2926    fn nothing_playing_is_seekable_nowhere() {
2927        assert_eq!(SharedPlayerState::new().seekable_ms(), 0);
2928        assert_eq!(SharedPlayerState::new().seek_ceiling_ms(), None);
2929    }
2930
2931    #[test]
2932    fn a_container_that_cannot_state_its_duration_cannot_be_seeked() {
2933        // A partial Ogg keeps its duration in a last page that has not arrived,
2934        // so it opens and plays but has nothing to seek against. Half the bytes
2935        // being present does not change that.
2936        let state = streaming_state_with_duration(200, 400, Some(128), 0);
2937        assert_eq!(state.seekable_ms(), 0);
2938    }
2939
2940    #[test]
2941    fn the_library_duration_stands_in_for_a_silent_container() {
2942        // What is shown on the transport, so nine hours of music does not read
2943        // as 0:00 while it caches.
2944        let state = streaming_state_with_duration(200, 400, Some(128), 0);
2945        assert_eq!(state.duration_ms(), 200_000, "the item's own figure");
2946        // And it is a display figure only — it grants no seeking.
2947        assert_eq!(state.seekable_ms(), 0);
2948        assert_eq!(state.seek_ceiling_ms(), Some(0));
2949    }
2950
2951    #[test]
2952    fn the_container_duration_wins_where_there_is_one() {
2953        let state = streaming_state(200, 400, None);
2954        assert_eq!(state.duration_ms(), DURATION_MS);
2955    }
2956
2957    #[test]
2958    fn the_download_landing_restores_seeking() {
2959        // The sequence the whole design turns on: a track that opened without a
2960        // duration gets one when the finished file is re-read, and is seekable
2961        // end to end from that moment — no restart, no handover.
2962        let state = streaming_state_with_duration(400, 400, Some(128), 0);
2963        assert_eq!(state.seekable_ms(), 0);
2964
2965        // What the downloader does when the bytes land: settle the transfer,
2966        // then say the file is playable. In that order — while the store still
2967        // says a transfer is running, it is.
2968        let id = state.cursor().expect("cursor");
2969        let _ =
2970            crate::remote::downloads::settle(&state, 1, &Ok(PathBuf::from("/cache/train.flac")));
2971        assert_eq!(state.item_state(id), Some(ItemState::Ready));
2972        let info = state.track_info().expect("track info");
2973        state.set_track_info(Some(TrackInfo {
2974            duration_ms: DURATION_MS,
2975            ..info
2976        }));
2977
2978        assert_eq!(state.seekable_ms(), DURATION_MS);
2979        assert_eq!(state.seek_ceiling_ms(), None, "no boundary left to draw");
2980    }
2981
2982    // --- advance_cursor_loadable ---
2983
2984    #[test]
2985    fn advance_parks_on_a_still_downloading_track() {
2986        // A track that has not arrived yet must hold the cursor, not be skipped:
2987        // skipping it drops it from the queue for good, and it is the item whose
2988        // TrackReady has to resume playback.
2989        let state = SharedPlayerState::new();
2990        let item0 = ready_item("track-0");
2991        let item1 = pending_item("track-1");
2992        let item2 = ready_item("track-2");
2993        let (id0, id1) = (item0.id, item1.id);
2994
2995        state.add_items(vec![item0, item1, item2]);
2996
2997        assert_eq!(state.advance_cursor_loadable(), Some(id0));
2998        assert_eq!(state.advance_cursor_loadable(), Some(id1));
2999        assert_eq!(state.cursor(), Some(id1));
3000    }
3001
3002    #[test]
3003    fn advance_skips_failed_items() {
3004        let state = SharedPlayerState::new();
3005        let item0 = ready_item("track-0");
3006        let item1 = failed_item("track-1");
3007        let item2 = ready_item("track-2");
3008        let (id0, id2) = (item0.id, item2.id);
3009
3010        state.add_items(vec![item0, item1, item2]);
3011        state.set_cursor(Some(id0));
3012
3013        assert_eq!(state.advance_cursor_loadable(), Some(id2));
3014    }
3015
3016    #[test]
3017    fn advance_stops_at_end_of_playlist() {
3018        let state = SharedPlayerState::new();
3019        let item0 = ready_item("track-0");
3020        let item1 = ready_item("track-1");
3021        let id1 = item1.id;
3022
3023        state.add_items(vec![item0, item1]);
3024        state.set_cursor(Some(id1));
3025
3026        assert_eq!(state.advance_cursor_loadable(), None);
3027        assert_eq!(
3028            state.cursor(),
3029            Some(id1),
3030            "cursor unchanged on a failed advance"
3031        );
3032    }
3033
3034    #[test]
3035    fn advance_with_only_failed_items_returns_none() {
3036        let state = SharedPlayerState::new();
3037        state.add_items(vec![failed_item("bad-0"), failed_item("bad-1")]);
3038
3039        assert_eq!(state.advance_cursor_loadable(), None);
3040    }
3041
3042    #[test]
3043    fn advance_from_a_vanished_cursor_does_not_restart_the_queue() {
3044        let state = SharedPlayerState::new();
3045        let item0 = ready_item("track-0");
3046        let item1 = ready_item("track-1");
3047        let id0 = item0.id;
3048
3049        state.add_items(vec![item0, item1]);
3050        let ghost = QueueItemId::new();
3051        state.set_cursor(Some(ghost));
3052
3053        assert_eq!(state.advance_cursor_loadable(), None);
3054        assert_ne!(state.cursor(), Some(id0));
3055    }
3056
3057    // --- lookahead_after ---
3058
3059    fn chosen_after(state: &SharedPlayerState, id: QueueItemId) -> Option<QueueItemId> {
3060        state
3061            .lookahead_after(id)
3062            .and_then(|step| step.chosen)
3063            .map(|(id, _)| id)
3064    }
3065
3066    #[test]
3067    fn a_step_stops_at_a_track_still_arriving() {
3068        let state = SharedPlayerState::new();
3069        let a = ready_item("a");
3070        let b = PlaylistItem {
3071            state: ItemState::Pending,
3072            ..ready_item("b")
3073        };
3074        let c = ready_item("c");
3075        let (ida, idb) = (a.id, b.id);
3076        state.add_items(vec![a, b, c]);
3077
3078        let step = state.lookahead_after(ida).unwrap();
3079        assert_eq!(step.next, Some(idb));
3080        assert!(step.chosen.is_none(), "c is not queued over it");
3081
3082        state.update_item_state(idb, ItemState::Ready);
3083        assert!(
3084            state.still_follows(&step),
3085            "its landing is not an edit: the advance plays it"
3086        );
3087
3088        state.add_items(vec![ready_item("d")]);
3089        assert!(state.still_follows(&step), "nor is adding after it");
3090
3091        state.insert_items_after(vec![ready_item("next")], ida);
3092        assert!(!state.still_follows(&step), "a track put before it is");
3093    }
3094
3095    #[test]
3096    fn a_step_passes_over_failed_tracks() {
3097        let state = SharedPlayerState::new();
3098        let a = ready_item("a");
3099        let failed = PlaylistItem {
3100            state: ItemState::Failed("gone".into()),
3101            ..ready_item("failed")
3102        };
3103        let c = ready_item("c");
3104        let (ida, idc) = (a.id, c.id);
3105        state.add_items(vec![a, failed, c]);
3106
3107        let step = state.lookahead_after(ida).unwrap();
3108        assert_eq!(step.chosen.as_ref().map(|(id, _)| *id), Some(idc));
3109
3110        let another = PlaylistItem {
3111            state: ItemState::Failed("gone".into()),
3112            ..ready_item("another")
3113        };
3114        state.insert_items_after(vec![another], ida);
3115        assert!(
3116            state.still_follows(&step),
3117            "another failed track before it changes nothing"
3118        );
3119
3120        let arriving = PlaylistItem {
3121            state: ItemState::Pending,
3122            ..ready_item("arriving")
3123        };
3124        state.insert_items_after(vec![arriving], ida);
3125        assert!(!state.still_follows(&step), "a track still arriving does");
3126    }
3127
3128    #[test]
3129    fn a_step_breaks_when_what_it_chose_moves_ahead_of_it() {
3130        let state = SharedPlayerState::new();
3131        let (a, c) = (ready_item("a"), ready_item("c"));
3132        let (ida, idc) = (a.id, c.id);
3133        state.add_items(vec![a, c]);
3134        let step = state.lookahead_after(ida).unwrap();
3135        state.move_item(idc, ida, false);
3136        assert!(!state.still_follows(&step));
3137    }
3138
3139    #[test]
3140    fn a_step_to_the_end_of_the_queue_breaks_when_a_track_is_added() {
3141        let state = SharedPlayerState::new();
3142        let a = ready_item("a");
3143        let ida = a.id;
3144        state.add_items(vec![a]);
3145        let step = state.lookahead_after(ida).unwrap();
3146        assert!(step.chosen.is_none());
3147        assert!(state.still_follows(&step));
3148        state.add_items(vec![ready_item("b")]);
3149        assert!(!state.still_follows(&step));
3150    }
3151
3152    #[test]
3153    fn peek_after_a_removed_item_returns_none() {
3154        // The decode thread's lookahead runs seconds ahead of what is audible.
3155        // Removing the track it is pre-decoding must end the lookahead, not send
3156        // it back to the top of the queue.
3157        let state = SharedPlayerState::new();
3158        let item0 = ready_item("track-0");
3159        let item1 = ready_item("track-1");
3160        let item2 = ready_item("track-2");
3161        let (id0, id1, id2) = (item0.id, item1.id, item2.id);
3162
3163        state.add_items(vec![item0, item1, item2]);
3164        assert_eq!(chosen_after(&state, id1), Some(id2));
3165
3166        state.remove_item(id2);
3167        assert!(
3168            state.lookahead_after(id2).is_none(),
3169            "a vanished reference must not resolve to the head of the queue"
3170        );
3171        assert_ne!(chosen_after(&state, id2), Some(id0));
3172    }
3173
3174    #[test]
3175    fn the_lookahead_steps_over_a_track_that_failed() {
3176        let state = SharedPlayerState::new();
3177        let playing = ready_item("playing");
3178        let failed = failed_item("failed");
3179        let after = ready_item("after");
3180        let (playing_id, after_id) = (playing.id, after.id);
3181        state.add_items(vec![playing, failed, after]);
3182
3183        assert_eq!(chosen_after(&state, playing_id), Some(after_id));
3184    }
3185
3186    // --- surviving_item_before ---
3187
3188    fn repeating(state: &SharedPlayerState, repeat: Repeat) {
3189        state.set_play_mode(PlayMode {
3190            shuffle: false,
3191            repeat,
3192        });
3193    }
3194
3195    #[test]
3196    fn a_step_from_the_last_item_wraps_only_when_the_queue_repeats() {
3197        let state = SharedPlayerState::new();
3198        let items: Vec<_> = ["a", "b"].map(ready_item).into();
3199        let (a, b) = (items[0].id, items[1].id);
3200        state.add_items(items);
3201
3202        assert_eq!(state.lookahead_after(b).unwrap().next, None);
3203        repeating(&state, Repeat::Queue);
3204        let step = state.lookahead_after(b).unwrap();
3205        assert_eq!((step.next, step.wrapped), (Some(a), true));
3206        assert!(state.still_follows(&step));
3207
3208        let later = ready_item("later");
3209        state.add_items(vec![later.clone()]);
3210        assert!(!state.still_follows(&step), "something follows b now");
3211        state.remove_items(&[later.id]);
3212        repeating(&state, Repeat::Off);
3213        assert!(!state.still_follows(&step), "nor does repeat off wrap");
3214    }
3215
3216    #[test]
3217    fn repeating_one_steps_to_the_same_item_but_an_advance_moves_on() {
3218        let state = SharedPlayerState::new();
3219        let items: Vec<_> = ["a", "b"].map(ready_item).into();
3220        let (a, b) = (items[0].id, items[1].id);
3221        state.add_items(items);
3222        repeating(&state, Repeat::One);
3223
3224        let step = state.lookahead_after(a).unwrap();
3225        assert_eq!((step.next, step.wrapped), (Some(a), false));
3226        state.set_cursor(Some(a));
3227        assert_eq!(state.advance_cursor_loadable(), Some(b));
3228        assert_eq!(
3229            state.advance_cursor_loadable(),
3230            Some(a),
3231            "round, as repeating"
3232        );
3233    }
3234
3235    #[test]
3236    fn previous_from_the_first_item_wraps_only_while_repeating() {
3237        let state = SharedPlayerState::new();
3238        let items: Vec<_> = ["a", "b", "c"].map(ready_item).into();
3239        let (a, c) = (items[0].id, items[2].id);
3240        state.add_items(items);
3241
3242        state.set_cursor(Some(a));
3243        assert!(state.retreat_cursor().is_none());
3244        for repeat in [Repeat::Queue, Repeat::One] {
3245            repeating(&state, repeat);
3246            state.set_cursor(Some(a));
3247            assert_eq!(
3248                state.retreat_cursor().map(|(id, _)| id),
3249                Some(c),
3250                "{repeat:?}"
3251            );
3252        }
3253    }
3254
3255    #[test]
3256    fn a_removed_item_has_nothing_after_it_even_when_the_queue_repeats() {
3257        let state = SharedPlayerState::new();
3258        let items: Vec<_> = ["a", "b"].map(ready_item).into();
3259        let b = items[1].id;
3260        state.add_items(items);
3261        repeating(&state, Repeat::Queue);
3262
3263        state.set_cursor(Some(b));
3264        state.remove_item(b);
3265        assert!(state.lookahead_after(b).is_none());
3266        state.set_cursor(Some(b));
3267        assert_eq!(state.advance_cursor_loadable(), None);
3268    }
3269
3270    #[test]
3271    fn a_wrap_passes_over_failed_items() {
3272        let state = SharedPlayerState::new();
3273        let items = vec![failed_item("a"), ready_item("b"), ready_item("c")];
3274        let (b, c) = (items[1].id, items[2].id);
3275        state.add_items(items);
3276        repeating(&state, Repeat::Queue);
3277
3278        assert_eq!(state.lookahead_after(c).unwrap().next, Some(b));
3279    }
3280
3281    #[test]
3282    fn surviving_predecessor_skips_items_being_removed() {
3283        let state = SharedPlayerState::new();
3284        let items: Vec<_> = (0..4).map(|i| ready_item(&format!("track-{i}"))).collect();
3285        let ids: Vec<_> = items.iter().map(|i| i.id).collect();
3286        state.add_items(items);
3287
3288        // Deleting 1..=3 leaves 0 as the resume point for a cursor on 3.
3289        assert_eq!(
3290            state.surviving_item_before(ids[3], &ids[1..4]),
3291            Some(ids[0])
3292        );
3293        // Deleting everything from the top leaves nothing to resume after.
3294        assert_eq!(state.surviving_item_before(ids[2], &ids), None);
3295    }
3296
3297    // --- retreat_cursor ---
3298
3299    #[test]
3300    fn test_retreat_cursor_goes_to_previous_item() {
3301        let state = SharedPlayerState::new();
3302        let item0 = ready_item("track-0");
3303        let item1 = ready_item("track-1");
3304        let id0 = item0.id;
3305        let id1 = item1.id;
3306
3307        state.add_items(vec![item0, item1]);
3308        state.set_cursor(Some(id1));
3309
3310        let result = state.retreat_cursor();
3311        assert!(result.is_some(), "expected to retreat to previous item");
3312        assert_eq!(result.unwrap().0, id0, "should retreat to first item");
3313        assert_eq!(state.cursor(), Some(id0));
3314    }
3315
3316    #[test]
3317    fn test_retreat_cursor_returns_none_when_at_first_item() {
3318        let state = SharedPlayerState::new();
3319        let item0 = ready_item("only-track");
3320        let id0 = item0.id;
3321
3322        state.add_items(vec![item0]);
3323        state.set_cursor(Some(id0));
3324
3325        let result = state.retreat_cursor();
3326        assert!(result.is_none(), "cannot retreat before the first item");
3327        // Cursor stays on the first item.
3328        assert_eq!(state.cursor(), Some(id0));
3329    }
3330
3331    #[test]
3332    fn test_retreat_cursor_returns_none_when_cursor_is_unset() {
3333        let state = SharedPlayerState::new();
3334        state.add_items(vec![ready_item("track-0")]);
3335
3336        let result = state.retreat_cursor();
3337        assert!(
3338            result.is_none(),
3339            "retreat with no cursor should return None"
3340        );
3341    }
3342
3343    // --- derive_visible_queue ---
3344
3345    #[test]
3346    fn test_derive_visible_queue_statuses() {
3347        // playlist: [played, playing, queued]
3348        let state = SharedPlayerState::new();
3349        let item0 = ready_item("played-track");
3350        let item1 = ready_item("playing-track");
3351        let item2 = ready_item("queued-track");
3352        let (id0, id1) = (item0.id, item1.id);
3353
3354        state.add_items(vec![item0, item1, item2]);
3355        state.set_cursor(Some(id0));
3356        state.mark_played(id0);
3357        state.set_cursor(Some(id1));
3358
3359        let snap = state.derive_visible_queue();
3360
3361        assert_eq!(snap.entries.len(), 3);
3362        assert_eq!(snap.entries[0].status, QueueEntryStatus::Played);
3363        assert_eq!(snap.entries[1].status, QueueEntryStatus::Playing);
3364        assert_eq!(snap.entries[2].status, QueueEntryStatus::Queued);
3365        assert!(snap.has_playing);
3366        assert_eq!(snap.finished_count, 1);
3367        assert_eq!(snap.queue_count, 1);
3368    }
3369
3370    #[test]
3371    fn test_derive_visible_queue_downloading_statuses() {
3372        // Downloading reads the same at the cursor as after it: bytes are
3373        // moving, and the row has progress to draw.
3374        let state = SharedPlayerState::new();
3375        let (dl_cursor, _) = downloading_item(&state, "downloading-at-cursor", 1_000_000);
3376        let (dl_queued, _) = downloading_item(&state, "downloading-queued", 500_000);
3377        let id_cursor = dl_cursor.id;
3378
3379        state.add_items(vec![dl_cursor, dl_queued]);
3380        state.set_cursor(Some(id_cursor));
3381
3382        let snap = state.derive_visible_queue();
3383
3384        assert_eq!(snap.entries[0].status, QueueEntryStatus::Downloading);
3385        assert_eq!(snap.entries[1].status, QueueEntryStatus::Downloading);
3386    }
3387
3388    #[test]
3389    fn a_cursor_waiting_its_turn_is_priority_pending() {
3390        let state = SharedPlayerState::new();
3391        let item = pending_item("waiting");
3392        let id = item.id;
3393        state.add_items(vec![item]);
3394        state.set_cursor(Some(id));
3395
3396        let snap = state.derive_visible_queue();
3397        assert_eq!(snap.entries[0].status, QueueEntryStatus::PriorityPending);
3398    }
3399
3400    #[test]
3401    fn a_second_entry_for_a_track_reads_the_transfer_running_for_it() {
3402        // One transfer per track: the entry queued second has none of its own,
3403        // and streams and reports from the first one's.
3404        let state = SharedPlayerState::new();
3405        let (first, bytes) = downloading_item(&state, "twice", 1_000_000);
3406        bytes.set(STREAM_THRESHOLD);
3407        let mut again = pending_item("twice");
3408        again.db_id = first.db_id;
3409        let again_id = again.id;
3410        state.add_items(vec![first, again]);
3411
3412        assert!(matches!(
3413            state.item_load_state(again_id),
3414            Some(LoadState::Downloading { .. })
3415        ));
3416        assert!(matches!(
3417            state.item_playback_source(again_id),
3418            Some(PlaybackSource::Streaming { .. })
3419        ));
3420    }
3421
3422    /// The saved queue is rewritten when its contents move, and only then:
3423    /// a track change or a download landing is a position save.
3424    #[test]
3425    fn only_a_change_to_what_is_saved_moves_the_content_version() {
3426        let state = SharedPlayerState::new();
3427        let a = make_item("a", ItemState::Pending);
3428        let b = make_item("b", ItemState::Ready);
3429        let (a_id, b_id) = (a.id, b.id);
3430
3431        let start = state.content_version();
3432        state.add_items(vec![a, b]);
3433        let added = state.content_version();
3434        assert_ne!(added, start);
3435
3436        state.set_cursor(Some(a_id));
3437        state.update_item_state(a_id, ItemState::Ready);
3438        state.advance_cursor_loadable();
3439        state.retreat_cursor();
3440        assert_eq!(state.content_version(), added);
3441        assert_eq!(state.cursor_path(), Some(PathBuf::from("/music/a.flac")));
3442
3443        state.move_item_to(b_id, None);
3444        assert_ne!(state.content_version(), added);
3445    }
3446
3447    #[test]
3448    fn progress_follows_the_counter_without_touching_the_playlist() {
3449        // The download thread writes bytes and nothing else. A queue derived
3450        // afterwards must see them — the version has not moved, and the load
3451        // state it was given is the one it still holds.
3452        let state = SharedPlayerState::new();
3453        let (item, bytes) = downloading_item(&state, "downloading", 1_000);
3454        state.add_items(vec![item]);
3455
3456        let version = state.playlist_version();
3457        bytes.set(250);
3458
3459        let snap = state.derive_visible_queue();
3460        assert_eq!(snap.entries[0].download_progress, Some((250, 1_000)));
3461        assert_eq!(
3462            state.playlist_version(),
3463            version,
3464            "progress must not read as a queue mutation"
3465        );
3466        assert_eq!(state.downloads().readings()[0].written, 250);
3467    }
3468
3469    #[test]
3470    fn test_derive_visible_queue_no_cursor_all_queued() {
3471        let state = SharedPlayerState::new();
3472        state.add_items(vec![ready_item("a"), ready_item("b"), ready_item("c")]);
3473
3474        let snap = state.derive_visible_queue();
3475
3476        assert_eq!(snap.entries.len(), 3);
3477        for entry in &snap.entries {
3478            assert_eq!(entry.status, QueueEntryStatus::Queued);
3479        }
3480        assert!(!snap.has_playing);
3481        assert_eq!(snap.finished_count, 0);
3482        assert_eq!(snap.queue_count, 3);
3483    }
3484
3485    // --- same_album_item_ids ---
3486
3487    fn make_album_item(title: &str, album: &str, album_artist: &str) -> PlaylistItem {
3488        PlaylistItem {
3489            uid: None,
3490            playlist_entry_id: None,
3491            id: QueueItemId::new(),
3492            db_id: None,
3493            path: PathBuf::from(format!("/music/{title}.flac")),
3494            title: title.to_string(),
3495            artist: "Artist".to_string(),
3496            album_artist: album_artist.to_string(),
3497            album: album.to_string(),
3498            year: None,
3499            codec: Some("FLAC".to_string()),
3500            track_number: None,
3501            disc: None,
3502            duration_ms: Some(200_000),
3503            state: ItemState::Ready,
3504            played: false,
3505        }
3506    }
3507
3508    #[test]
3509    fn test_same_album_item_ids_returns_album_mates() {
3510        let state = SharedPlayerState::new();
3511        let a1 = make_album_item("A1", "Album A", "Artist A");
3512        let a2 = make_album_item("A2", "Album A", "Artist A");
3513        let b1 = make_album_item("B1", "Album B", "Artist B");
3514        let a3 = make_album_item("A3", "Album A", "Artist A");
3515
3516        let id_a1 = a1.id;
3517        let id_a2 = a2.id;
3518        let id_a3 = a3.id;
3519
3520        state.add_items(vec![a1, a2, b1, a3]);
3521
3522        let mates = state.same_album_item_ids(id_a1);
3523        assert_eq!(mates.len(), 2);
3524        assert!(mates.contains(&id_a2));
3525        assert!(mates.contains(&id_a3));
3526    }
3527
3528    #[test]
3529    fn test_same_album_item_ids_distinguishes_album_artists() {
3530        // Two albums named the same but by different artists — should NOT match.
3531        let state = SharedPlayerState::new();
3532        let a1 = make_album_item("A1", "Greatest Hits", "Artist A");
3533        let b1 = make_album_item("B1", "Greatest Hits", "Artist B");
3534
3535        let id_a1 = a1.id;
3536
3537        state.add_items(vec![a1, b1]);
3538
3539        let mates = state.same_album_item_ids(id_a1);
3540        assert!(mates.is_empty(), "different album_artist should not match");
3541    }
3542
3543    #[test]
3544    fn test_same_album_item_ids_unknown_id_returns_empty() {
3545        let state = SharedPlayerState::new();
3546        state.add_items(vec![ready_item("track-0")]);
3547
3548        let bogus = QueueItemId::new();
3549        let mates = state.same_album_item_ids(bogus);
3550        assert!(mates.is_empty());
3551    }
3552
3553    // --- update_item_metadata ---
3554
3555    #[test]
3556    fn test_update_item_metadata_leaves_library_tags_alone() {
3557        let state = SharedPlayerState::new();
3558        let mut item = make_album_item("A1", "Nite Versions (mixed)", "Soulwax");
3559        item.db_id = Some(29615);
3560        let id = item.id;
3561        state.add_items(vec![item]);
3562
3563        state.update_item_metadata(
3564            id,
3565            "[unknown]".into(),
3566            "Soulwax".into(),
3567            "Soulwax".into(),
3568            "Nite Versions".into(),
3569            Some(54_000),
3570        );
3571
3572        let pl = state.playlist.read();
3573        assert_eq!(pl.items[0].album, "Nite Versions (mixed)");
3574        assert_eq!(pl.items[0].title, "A1");
3575        assert_eq!(pl.items[0].duration_ms, Some(54_000));
3576    }
3577
3578    #[test]
3579    fn test_update_item_metadata_fills_in_an_item_with_nothing_behind_it() {
3580        let state = SharedPlayerState::new();
3581        let item = make_album_item("A1", "", "");
3582        let id = item.id;
3583        state.add_items(vec![item]);
3584
3585        state.update_item_metadata(
3586            id,
3587            "Teachers".into(),
3588            "Soulwax".into(),
3589            "Soulwax".into(),
3590            "Nite Versions".into(),
3591            Some(148_000),
3592        );
3593
3594        let pl = state.playlist.read();
3595        assert_eq!(pl.items[0].title, "Teachers");
3596        assert_eq!(pl.items[0].album, "Nite Versions");
3597        assert_eq!(pl.items[0].duration_ms, Some(148_000));
3598    }
3599
3600    /// A download landing with tags that say what the queue already held is
3601    /// not an edit: nothing is saved again and no client reads the queue again.
3602    /// Nor is one that only corrects the duration — a server's whole seconds
3603    /// against the file's milliseconds — which clients take as a change to that
3604    /// row's reading.
3605    #[test]
3606    fn test_update_item_metadata_is_an_edit_only_for_new_tags() {
3607        let state = SharedPlayerState::new();
3608        let mut item = make_album_item("A1", "Nite Versions", "Soulwax");
3609        item.db_id = Some(1);
3610        let id = item.id;
3611        state.add_items(vec![item]);
3612        let content = state.content_version();
3613        let version = state.playlist_version();
3614
3615        let land = |duration_ms| {
3616            state.update_item_metadata(
3617                id,
3618                "A1".into(),
3619                "Soulwax".into(),
3620                "Soulwax".into(),
3621                "Nite Versions".into(),
3622                Some(duration_ms),
3623            )
3624        };
3625        land(200_000);
3626        assert_eq!(state.content_version(), content);
3627        assert_eq!(state.playlist_version(), version);
3628
3629        land(200_417);
3630        assert_eq!(state.content_version(), content);
3631        assert_ne!(state.playlist_version(), version);
3632        assert_eq!(state.queue_readings()[0].duration_ms, Some(200_417));
3633
3634        let mut untagged = make_album_item("", "", "");
3635        let untagged_id = untagged.id;
3636        untagged.db_id = None;
3637        state.add_items(vec![untagged]);
3638        let content = state.content_version();
3639        state.update_item_metadata(
3640            untagged_id,
3641            "Teachers".into(),
3642            "Soulwax".into(),
3643            "Soulwax".into(),
3644            "Nite Versions".into(),
3645            None,
3646        );
3647        assert_ne!(state.content_version(), content);
3648    }
3649
3650    // --- move_item_to ---
3651
3652    #[test]
3653    fn test_move_item_to_reorders_playlist() {
3654        // Start: [A, B, C]. Move C to after A → [A, C, B].
3655        let state = SharedPlayerState::new();
3656        let item_a = ready_item("A");
3657        let item_b = ready_item("B");
3658        let item_c = ready_item("C");
3659        let id_a = item_a.id;
3660        let id_b = item_b.id;
3661        let id_c = item_c.id;
3662
3663        state.add_items(vec![item_a, item_b, item_c]);
3664        state.move_item_to(id_c, Some(id_a));
3665
3666        let (items, _) = state.snapshot_playlist();
3667        let titles: Vec<&str> = items.iter().map(|i| i.title.as_str()).collect();
3668        assert_eq!(titles, vec!["A", "C", "B"]);
3669        assert_eq!(items[0].id, id_a);
3670        assert_eq!(items[1].id, id_c);
3671        assert_eq!(items[2].id, id_b);
3672    }
3673
3674    #[test]
3675    fn test_move_item_to_front_when_after_is_none() {
3676        // Start: [A, B, C]. Move C to front (after=None) → [C, A, B].
3677        let state = SharedPlayerState::new();
3678        let item_a = ready_item("A");
3679        let item_b = ready_item("B");
3680        let item_c = ready_item("C");
3681        let id_c = item_c.id;
3682
3683        state.add_items(vec![item_a, item_b, item_c]);
3684        state.move_item_to(id_c, None);
3685
3686        let (items, _) = state.snapshot_playlist();
3687        let titles: Vec<&str> = items.iter().map(|i| i.title.as_str()).collect();
3688        assert_eq!(titles, vec!["C", "A", "B"]);
3689    }
3690
3691    // --- move_items (batch) ---
3692
3693    #[test]
3694    fn test_move_items_batch_preserves_relative_order() {
3695        // Start: [A, B, C, D]. Move [A, C] after D → [B, D, A, C].
3696        let state = SharedPlayerState::new();
3697        let item_a = ready_item("A");
3698        let item_b = ready_item("B");
3699        let item_c = ready_item("C");
3700        let item_d = ready_item("D");
3701        let id_a = item_a.id;
3702        let id_b = item_b.id;
3703        let id_c = item_c.id;
3704        let id_d = item_d.id;
3705
3706        state.add_items(vec![item_a, item_b, item_c, item_d]);
3707        state.move_items(&[id_a, id_c], id_d, true);
3708
3709        let (items, _) = state.snapshot_playlist();
3710        let titles: Vec<&str> = items.iter().map(|i| i.title.as_str()).collect();
3711        assert_eq!(titles, vec!["B", "D", "A", "C"]);
3712        assert_eq!(items[0].id, id_b);
3713        assert_eq!(items[1].id, id_d);
3714        assert_eq!(items[2].id, id_a);
3715        assert_eq!(items[3].id, id_c);
3716    }
3717
3718    // --- pending_downloads ---
3719
3720    #[test]
3721    fn test_pending_downloads_collects_pending_with_db_id() {
3722        let state = SharedPlayerState::new();
3723        let mut item_a = ready_item("local");
3724        item_a.db_id = None;
3725
3726        let mut item_b = pending_item("remote-1");
3727        item_b.db_id = Some(10);
3728        let id_b = item_b.id;
3729
3730        let mut item_c = ready_item("cached");
3731        item_c.db_id = Some(20);
3732
3733        let mut item_d = pending_item("remote-2");
3734        item_d.db_id = Some(30);
3735        let id_d = item_d.id;
3736
3737        // Pending without db_id — should NOT appear (no way to download).
3738        let item_e = pending_item("orphan");
3739
3740        state.add_items(vec![item_a, item_b, item_c, item_d, item_e]);
3741
3742        let pending = state.pending_downloads();
3743        assert_eq!(pending.len(), 2);
3744        assert_eq!(pending[0], (10, id_b));
3745        assert_eq!(pending[1], (30, id_d));
3746
3747        // From the cursor on, then from the top: playing the fourth, the
3748        // tracks before it come last.
3749        state.set_cursor(Some(id_d));
3750        assert_eq!(state.pending_downloads(), vec![(30, id_d), (10, id_b)]);
3751    }
3752
3753    #[test]
3754    fn test_item_db_id_and_load_state() {
3755        let state = SharedPlayerState::new();
3756        let mut item = pending_item("track");
3757        item.db_id = Some(42);
3758        let id = item.id;
3759        state.add_items(vec![item]);
3760
3761        assert_eq!(state.item_db_id(id), Some(42));
3762        assert!(matches!(
3763            state.item_load_state(id),
3764            Some(LoadState::Pending)
3765        ));
3766
3767        state.update_item_state(id, ItemState::Ready);
3768        assert!(matches!(state.item_load_state(id), Some(LoadState::Ready)));
3769    }
3770}