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#[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 let hex = self.0.simple().to_string();
31 write!(f, "QId({})", &hex[hex.len() - 8..])
32 }
33}
34
35#[derive(Debug, Clone)]
40pub struct Lookahead {
41 pub after: QueueItemId,
42 pub next: Option<QueueItemId>,
43 pub chosen: Option<(QueueItemId, PathBuf)>,
44 pub wrapped: bool,
47 pub boundary: usize,
53 pub after_pass: u64,
57 pub pass: u64,
58}
59
60#[derive(
62 Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
63)]
64#[serde(rename_all = "lowercase")]
65pub enum Repeat {
66 #[default]
68 Off,
69 Queue,
71 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 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#[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#[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 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
186#[serde(tag = "kind", rename_all = "camelCase")]
187pub enum Sleep {
188 At {
190 unix_ms: u64,
191 },
192 EndOfTrack,
193 EndOfRecord,
194}
195
196#[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#[derive(Debug, Clone, Default)]
228pub struct PlayOrder {
229 upcoming: Vec<QueueItemId>,
230 next_pass: Option<Vec<QueueItemId>>,
234 history: Vec<QueueItemId>,
237}
238
239fn 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
265fn 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
292const WAITING: u8 = 0x80;
295
296#[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#[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
328pub const STREAM_THRESHOLD: u64 = 256 * 1024;
332
333pub const SEEK_SAFETY_MS: u64 = 2_000;
339
340#[derive(Debug, Clone, Default, PartialEq, Eq)]
351pub enum ItemState {
352 #[default]
354 Pending,
355 Ready,
357 Failed(String),
361}
362
363pub 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#[derive(Debug, Clone)]
380pub enum LoadState {
381 Pending,
382 Downloading {
383 path: PathBuf,
386 total: u64,
388 bytes_written: Arc<ByteFeed>,
391 },
392 Ready,
393 Failed(String),
394}
395
396impl LoadState {
397 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
420pub enum PlaybackSource {
422 Ready(PathBuf),
424 Streaming {
426 path: PathBuf,
427 bytes_written: Arc<crate::remote::downloads::ByteFeed>,
428 total: u64,
429 },
430}
431
432#[derive(Debug, Clone)]
434pub struct PlaylistItem {
435 pub id: QueueItemId,
436 pub db_id: Option<i64>,
439 pub uid: Option<String>,
444 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 pub state: ItemState,
464 pub played: bool,
470}
471
472#[derive(Debug, Clone, Default)]
474pub struct Playlist {
475 pub items: Vec<PlaylistItem>,
476 pub cursor: Option<QueueItemId>,
477 pub order: Option<PlayOrder>,
478 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
724pub enum QueueEntryStatus {
725 Queued,
726 Playing,
727 Played,
728 Downloading,
729 PriorityPending,
731 Failed,
732}
733
734impl QueueEntryStatus {
735 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#[derive(Debug, Clone)]
752pub struct QueueEntry {
753 pub id: QueueItemId,
754 pub db_id: Option<i64>,
756 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 pub error: Option<String>,
773}
774
775#[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#[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#[derive(Debug)]
800pub struct SharedPlayerState {
801 state: AtomicU8,
805 position_ms: AtomicU64,
808 timeline: std::sync::OnceLock<Arc<crate::audio::buffer::PlaybackTimeline>>,
809 track_info: parking_lot::RwLock<Option<TrackInfo>>,
810
811 playlist: parking_lot::RwLock<Playlist>,
813
814 playlist_version: AtomicU64,
816
817 content_version: AtomicU64,
821
822 pending_version: AtomicU64,
827
828 played_version: AtomicU64,
832
833 downloads: Arc<DownloadStore>,
835
836 quit_requested: AtomicBool,
838
839 metadata_refresh_pending: AtomicBool,
842
843 output_sample_rate: AtomicU64,
850
851 output_mixed: std::sync::atomic::AtomicU8,
855
856 start_failure: parking_lot::RwLock<Option<String>>,
860
861 dsp: parking_lot::RwLock<Option<crate::audio::dsp::DspStatus>>,
864 renderer_clock: parking_lot::Mutex<Option<RendererClock>>,
868
869 renderer: parking_lot::RwLock<Option<crate::upnp::Output>>,
871
872 play_mode: AtomicU8,
876
877 sleep: parking_lot::RwLock<Option<Sleep>>,
879 sleep_fading: AtomicBool,
881}
882
883#[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 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 pub fn is_waiting(&self) -> bool {
942 self.transport().1
943 }
944
945 pub fn wants_to_play(&self) -> bool {
948 matches!(
949 self.transport(),
950 (PlaybackState::Playing, _) | (PlaybackState::Stopped, true)
951 )
952 }
953
954 pub fn is_idle(&self) -> bool {
956 self.transport() == (PlaybackState::Stopped, false)
957 }
958
959 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 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 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 }
997
998 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 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 pub fn seekable_ms(&self) -> u64 {
1073 let Some(info) = self.track_info.read().clone() else {
1074 return 0;
1075 };
1076
1077 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 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 written.saturating_mul(8) / kbps as u64
1109 } else {
1110 return self.position_ms();
1113 };
1114
1115 reached.saturating_sub(SEEK_SAFETY_MS).min(info.duration_ms)
1116 }
1117
1118 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 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 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 pub fn current_download_fraction(&self) -> Option<f64> {
1157 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 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 pub fn signal_metadata_refresh(&self) {
1193 self.metadata_refresh_pending.store(true, Ordering::Release);
1194 self.changed();
1195 }
1196
1197 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 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 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 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 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 pub fn play_mode(&self) -> PlayMode {
1279 PlayMode::from_bits(self.play_mode.load(Ordering::Acquire))
1280 }
1281
1282 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 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 pub fn saved_version(&self) -> u64 {
1335 self.content_version()
1336 .wrapping_add(self.played_version.load(Ordering::Acquire))
1337 }
1338
1339 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 pub fn pending_version(&self) -> u64 {
1352 self.pending_version.load(Ordering::Acquire)
1353 }
1354
1355 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 pub fn changed(&self) {
1370 crate::signal::engine_changed().bump();
1371 }
1372
1373 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 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(), };
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 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 pub fn remove_item(&self, id: QueueItemId) {
1411 let mut pl = self.playlist.write();
1412 pl.items.retain(|item| item.id != id);
1413 if pl.cursor == Some(id) {
1415 pl.cursor = None;
1416 }
1417 drop(pl);
1418 self.bump_content();
1419 }
1420
1421 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 pub fn lookahead_after(&self, after_id: QueueItemId) -> Option<Lookahead> {
1658 self.lookahead(after_id, None)
1659 }
1660
1661 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 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 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 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 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 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 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 if retagged {
1825 self.bump_content();
1826 } else if retimed {
1827 self.bump_version();
1828 }
1829 }
1830
1831 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 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 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 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 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 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 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 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 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 pub fn snapshot_playlist(&self) -> (Vec<PlaylistItem>, Option<QueueItemId>) {
1990 let pl = self.playlist.read();
1991 (pl.items.clone(), pl.cursor)
1992 }
1993
1994 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 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 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 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 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 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 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 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 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 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(), }
2150 }
2151 None => 0,
2152 };
2153 pl.items.insert(insert_at, item);
2154 drop(pl);
2155 self.bump_content();
2156 }
2157
2158 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 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 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 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 fn each_visible(&self, mut f: impl FnMut(&PlaylistItem, Place, QueueReading)) {
2249 let playing_duration_ms = self.track_info.read().as_ref().map(|ti| ti.duration_ms);
2251 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 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 (Place::Unplayed, _) => QueueEntryStatus::Queued,
2288 };
2289
2290 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 (Place::Cursor, ItemState::Pending) => unreachable.clone(),
2313 _ => None,
2314 },
2315 },
2316 );
2317 }
2318 }
2319
2320 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
2341fn 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#[derive(Clone, Copy)]
2367enum Place {
2368 Played,
2369 Cursor,
2370 Unplayed,
2371}
2372
2373#[cfg(test)]
2374mod tests {
2375 use super::*;
2376
2377 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 let state = streaming_state_with_duration(400, 400, Some(128), 0);
2963 assert_eq!(state.seekable_ms(), 0);
2964
2965 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 #[test]
2985 fn advance_parks_on_a_still_downloading_track() {
2986 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 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 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 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 assert_eq!(
3290 state.surviving_item_before(ids[3], &ids[1..4]),
3291 Some(ids[0])
3292 );
3293 assert_eq!(state.surviving_item_before(ids[2], &ids), None);
3295 }
3296
3297 #[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 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 #[test]
3346 fn test_derive_visible_queue_statuses() {
3347 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 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 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 #[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 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 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 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 #[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 #[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 #[test]
3653 fn test_move_item_to_reorders_playlist() {
3654 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 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 #[test]
3694 fn test_move_items_batch_preserves_relative_order() {
3695 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 #[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 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 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}