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
363#[derive(Debug, Clone)]
369pub enum LoadState {
370 Pending,
371 Downloading {
372 path: PathBuf,
375 total: u64,
377 bytes_written: Arc<ByteFeed>,
380 },
381 Ready,
382 Failed(String),
383}
384
385impl LoadState {
386 pub fn of(item: &PlaylistItem, downloads: &DownloadStore) -> Self {
394 match &item.state {
395 ItemState::Ready => Self::Ready,
396 ItemState::Failed(reason) => Self::Failed(reason.clone()),
397 ItemState::Pending => match item.db_id.and_then(|id| downloads.live(id)) {
398 Some(live) => Self::Downloading {
399 path: live.source,
400 total: live.total,
401 bytes_written: live.written,
402 },
403 None => Self::Pending,
404 },
405 }
406 }
407}
408
409pub enum PlaybackSource {
411 Ready(PathBuf),
413 Streaming {
415 path: PathBuf,
416 bytes_written: Arc<crate::remote::downloads::ByteFeed>,
417 total: u64,
418 },
419}
420
421#[derive(Debug, Clone)]
423pub struct PlaylistItem {
424 pub id: QueueItemId,
425 pub db_id: Option<i64>,
427 pub playlist_entry_id: Option<i64>,
434 pub path: PathBuf,
435 pub title: String,
436 pub artist: String,
437 pub album_artist: String,
438 pub album: String,
439 pub year: Option<String>,
440 pub codec: Option<String>,
441 pub track_number: Option<i64>,
442 pub disc: Option<i64>,
443 pub duration_ms: Option<u64>,
444 pub state: ItemState,
447 pub played: bool,
453}
454
455#[derive(Debug, Clone, Default)]
457pub struct Playlist {
458 pub items: Vec<PlaylistItem>,
459 pub cursor: Option<QueueItemId>,
460 pub order: Option<PlayOrder>,
461 pub pass: u64,
463}
464
465impl Playlist {
466 fn find(&self, id: QueueItemId) -> Option<&PlaylistItem> {
467 self.items.iter().find(|item| item.id == id)
468 }
469
470 fn reconcile(&mut self) {
474 let Playlist {
475 items,
476 cursor,
477 order,
478 ..
479 } = self;
480 let Some(order) = order.as_mut() else {
481 return;
482 };
483 let rows: HashMap<QueueItemId, &PlaylistItem> =
484 items.iter().map(|item| (item.id, item)).collect();
485
486 let mut seen: std::collections::HashSet<TrackKey> = items
487 .iter()
488 .filter(|item| item.played)
489 .map(PlaylistItem::key)
490 .chain(cursor.and_then(|c| rows.get(&c)).map(|item| item.key()))
491 .collect();
492 fn keep<'a>(
495 seen: &mut std::collections::HashSet<TrackKey<'a>>,
496 item: &'a PlaylistItem,
497 unplayed: bool,
498 ) -> bool {
499 (!unplayed || !item.played) && item.playable() && seen.insert(item.key())
500 }
501 order
502 .upcoming
503 .retain(|id| rows.get(id).is_some_and(|item| keep(&mut seen, item, true)));
504 let fresh = items
505 .iter()
506 .filter(|item| keep(&mut seen, item, true))
507 .map(|item| item.id)
508 .collect();
509 scatter(&mut order.upcoming, fresh);
510
511 if let Some(next) = order.next_pass.as_mut() {
512 let mut seen = std::collections::HashSet::new();
513 next.retain(|id| {
514 rows.get(id)
515 .is_some_and(|item| keep(&mut seen, item, false))
516 });
517 let fresh = items
518 .iter()
519 .filter(|item| keep(&mut seen, item, false))
520 .map(|item| item.id)
521 .collect();
522 scatter(next, fresh);
523 }
524
525 order.history.retain(|id| rows.contains_key(id));
526 }
527
528 fn new_pass(&self, last: Option<QueueItemId>) -> Vec<QueueItemId> {
532 let mut seen = std::collections::HashSet::new();
533 let mut pass: Vec<QueueItemId> = self
534 .items
535 .iter()
536 .filter(|item| item.playable() && seen.insert(item.key()))
537 .map(|item| item.id)
538 .collect();
539 crate::helpers::shuffle(&mut pass);
540 let last = last.and_then(|id| self.find(id)).map(PlaylistItem::key);
541 if pass.len() > 1 && last.is_some() && self.find(pass[0]).map(PlaylistItem::key) == last {
542 let other =
543 crate::helpers::Rng::seeded().map_or(1, |mut rng| 1 + rng.below(pass.len() - 1));
544 pass.swap(0, other);
545 }
546 pass
547 }
548
549 fn follows(
561 &mut self,
562 after: Option<QueueItemId>,
563 ahead: bool,
564 repeat: Repeat,
565 ) -> Option<(Option<QueueItemId>, bool)> {
566 let Some(order) = self.order.as_ref() else {
567 return match after {
568 Some(after) => follows(&self.items, after, repeat)
569 .map(|(next, wrapped)| (next.map(|item| item.id), wrapped)),
570 None => Some((
571 self.items
572 .iter()
573 .find(|item| item.playable())
574 .map(|item| item.id),
575 false,
576 )),
577 };
578 };
579 if let Some(after) = after {
580 let item = self.find(after)?;
581 if repeat == Repeat::One && item.playable() {
582 return Some((Some(after), false));
583 }
584 }
585 let playable = |id: &&QueueItemId| self.find(**id).is_some_and(PlaylistItem::playable);
586 let from = |order: &[QueueItemId]| {
587 after
588 .and_then(|after| order.iter().position(|id| *id == after))
589 .map_or(0, |at| at + 1)
590 };
591 if ahead {
592 let Some(next) = order.next_pass.as_ref() else {
593 return Some((None, false));
594 };
595 let at = from(next);
596 let found = next[at..].iter().chain(&next[..at]).find(playable).copied();
597 return Some((found, true));
598 }
599 let upcoming = &order.upcoming;
600 if let Some(next) = upcoming[from(upcoming)..].iter().find(playable) {
601 return Some((Some(*next), false));
602 }
603 if repeat == Repeat::Off {
604 return Some((None, false));
605 }
606 if order.next_pass.is_none() {
607 let pass = self.new_pass(after.or(self.cursor));
608 self.order.as_mut().expect("checked above").next_pass = Some(pass);
609 }
610 let next = self.order.as_ref()?.next_pass.as_ref()?;
611 let playable = |id: &&QueueItemId| self.find(**id).is_some_and(PlaylistItem::playable);
612 Some((next.iter().find(playable).copied(), true))
613 }
614
615 fn place_cursor(&mut self, id: Option<QueueItemId>) {
618 let Playlist {
619 items,
620 cursor,
621 order,
622 ..
623 } = self;
624 *cursor = id;
625 let Some(id) = id else { return };
626 let Some(key) = items
627 .iter()
628 .find(|item| item.id == id)
629 .map(PlaylistItem::key)
630 else {
631 return;
632 };
633 let Some(order) = order.as_mut() else { return };
634 let keys: HashMap<QueueItemId, TrackKey> =
635 items.iter().map(|item| (item.id, item.key())).collect();
636 order.upcoming.retain(|row| keys.get(row) != Some(&key));
637 if order.history.last() != Some(&id) {
638 order.history.push(id);
639 }
640 }
641
642 fn move_on(&mut self, id: QueueItemId, pass: Option<u64>) {
651 let wrapped = match (&self.order, pass) {
652 (_, Some(pass)) => pass > self.pass,
653 (Some(order), None) => {
654 self.cursor != Some(id)
655 && !order.upcoming.contains(&id)
656 && order
657 .next_pass
658 .as_ref()
659 .is_some_and(|pass| pass.contains(&id))
660 }
661 (None, None) => {
662 let at = |id| self.items.iter().position(|item| item.id == id);
663 matches!((self.cursor.and_then(at), at(id)), (Some(from), Some(to)) if to < from)
664 }
665 };
666 if wrapped {
667 for item in &mut self.items {
668 item.played = false;
669 }
670 self.pass += 1;
671 if let Some(order) = self.order.as_mut() {
672 order.upcoming = order.next_pass.take().unwrap_or_default();
673 }
674 }
675 self.place_cursor(Some(id));
676 }
677
678 fn start_over_if_spent(&mut self) -> bool {
686 let mut playable = self.items.iter().filter(|item| item.playable()).peekable();
687 if playable.peek().is_none() || !playable.all(|item| item.played) {
688 return false;
689 }
690 for item in &mut self.items {
691 item.played = false;
692 }
693 self.pass += 1;
694 if let Some(order) = self.order.as_mut() {
695 order.upcoming.clear();
696 order.next_pass = None;
697 }
698 self.reconcile();
699 true
700 }
701}
702
703#[derive(Debug, Clone, Copy, PartialEq, Eq)]
707pub enum QueueEntryStatus {
708 Queued,
709 Playing,
710 Played,
711 Downloading,
712 PriorityPending,
714 Failed,
715}
716
717impl QueueEntryStatus {
718 pub fn at_cursor(state: &ItemState, transferring: bool) -> Self {
724 match state {
725 ItemState::Ready => Self::Playing,
726 ItemState::Failed(_) => Self::Failed,
727 ItemState::Pending if transferring => Self::Downloading,
728 ItemState::Pending => Self::PriorityPending,
729 }
730 }
731}
732
733#[derive(Debug, Clone)]
735pub struct QueueEntry {
736 pub id: QueueItemId,
737 pub db_id: Option<i64>,
739 pub playlist_entry_id: Option<i64>,
741 pub path: PathBuf,
742 pub title: String,
743 pub artist: String,
744 pub album_artist: String,
745 pub album: String,
746 pub year: Option<String>,
747 pub codec: Option<String>,
748 pub track_number: Option<i64>,
749 pub disc: Option<i64>,
750 pub duration_ms: Option<u64>,
751 pub status: QueueEntryStatus,
752 pub download_progress: Option<(u64, u64)>,
753 pub error: Option<String>,
755}
756
757#[derive(Debug, Clone, Default)]
759pub struct VisibleQueueSnapshot {
760 pub entries: Vec<QueueEntry>,
761 pub finished_count: usize,
762 pub has_playing: bool,
763 pub queue_count: usize,
764}
765
766#[derive(Debug, Clone, PartialEq)]
769pub struct QueueReading {
770 pub id: QueueItemId,
771 pub db_id: Option<i64>,
772 pub status: QueueEntryStatus,
773 pub duration_ms: Option<u64>,
774 pub download_progress: Option<(u64, u64)>,
775 pub error: Option<String>,
776}
777
778#[derive(Debug)]
782pub struct SharedPlayerState {
783 state: AtomicU8,
787 position_ms: AtomicU64,
790 timeline: std::sync::OnceLock<Arc<crate::audio::buffer::PlaybackTimeline>>,
791 track_info: parking_lot::RwLock<Option<TrackInfo>>,
792
793 playlist: parking_lot::RwLock<Playlist>,
795
796 playlist_version: AtomicU64,
798
799 content_version: AtomicU64,
803
804 pending_version: AtomicU64,
809
810 played_version: AtomicU64,
814
815 downloads: Arc<DownloadStore>,
817
818 quit_requested: AtomicBool,
820
821 metadata_refresh_pending: AtomicBool,
824
825 output_sample_rate: AtomicU64,
832
833 dsp: parking_lot::RwLock<Option<crate::audio::dsp::DspStatus>>,
836 renderer_clock: parking_lot::Mutex<Option<RendererClock>>,
840
841 renderer: parking_lot::RwLock<Option<crate::upnp::Output>>,
843
844 play_mode: AtomicU8,
848
849 sleep: parking_lot::RwLock<Option<Sleep>>,
851 sleep_fading: AtomicBool,
853}
854
855#[derive(Debug, Clone, Copy, PartialEq)]
858pub struct RendererClock {
859 pub position_ms: u64,
860 pub running: Option<std::time::Instant>,
861}
862
863impl RendererClock {
864 pub fn now_ms(&self) -> u64 {
865 self.position_ms + self.running.map_or(0, |at| at.elapsed().as_millis() as u64)
866 }
867}
868
869impl SharedPlayerState {
870 pub fn new() -> Arc<Self> {
871 Arc::new(Self {
872 state: AtomicU8::new(PlaybackState::Stopped as u8),
873 position_ms: AtomicU64::new(0),
874 timeline: std::sync::OnceLock::new(),
875 track_info: parking_lot::RwLock::new(None),
876 playlist: parking_lot::RwLock::new(Playlist::default()),
877 playlist_version: AtomicU64::new(0),
878 content_version: AtomicU64::new(0),
879 pending_version: AtomicU64::new(0),
880 played_version: AtomicU64::new(0),
881 downloads: DownloadStore::new(),
882 quit_requested: AtomicBool::new(false),
883 metadata_refresh_pending: AtomicBool::new(false),
884 output_sample_rate: AtomicU64::new(0),
885 dsp: parking_lot::RwLock::new(None),
886 renderer_clock: parking_lot::Mutex::new(None),
887 renderer: parking_lot::RwLock::new(None),
888 play_mode: AtomicU8::new(0),
889 sleep: parking_lot::RwLock::new(None),
890 sleep_fading: AtomicBool::new(false),
891 })
892 }
893
894 fn transport(&self) -> (PlaybackState, bool) {
897 let bits = self.state.load(Ordering::Acquire);
898 (PlaybackState::from_u8(bits & !WAITING), bits & WAITING != 0)
899 }
900
901 pub fn playback_state(&self) -> PlaybackState {
902 self.transport().0
903 }
904
905 pub fn set_playback_state(&self, state: PlaybackState) {
906 self.set_transport(state, false);
907 }
908
909 pub fn is_waiting(&self) -> bool {
912 self.transport().1
913 }
914
915 pub fn wants_to_play(&self) -> bool {
918 matches!(
919 self.transport(),
920 (PlaybackState::Playing, _) | (PlaybackState::Stopped, true)
921 )
922 }
923
924 pub fn is_idle(&self) -> bool {
926 self.transport() == (PlaybackState::Stopped, false)
927 }
928
929 pub fn set_transport(&self, state: PlaybackState, waiting: bool) {
931 let bits = state as u8 | if waiting { WAITING } else { 0 };
932 if self.state.swap(bits, Ordering::AcqRel) != bits {
933 self.changed();
934 }
935 }
936
937 pub fn position_ms(&self) -> u64 {
940 let clock = *self.renderer_clock.lock();
941 if let Some(clock) = clock {
942 let at = clock.now_ms();
943 let duration = self.duration_ms();
944 return if duration > 0 { at.min(duration) } else { at };
945 }
946 if self.playback_state() != PlaybackState::Stopped
947 && let Some(playhead) = self.timeline.get().and_then(|t| t.playhead())
948 {
949 return playhead.position_ms;
950 }
951 self.position_ms.load(Ordering::Acquire)
952 }
953
954 pub(crate) fn attach_timeline(&self, timeline: Arc<crate::audio::buffer::PlaybackTimeline>) {
956 let _ = self.timeline.set(timeline);
957 }
958
959 pub fn set_position_ms(&self, pos: u64) {
960 self.position_ms.store(pos, Ordering::Release);
961 }
967
968 pub fn playhead_moving(&self) -> bool {
978 let clock =
979 (*self.renderer_clock.lock()).or_else(|| self.timeline.get().and_then(|t| t.clock()));
980 match clock {
981 Some(clock) => clock.running.is_some(),
982 None => self.playback_state() == PlaybackState::Playing,
983 }
984 }
985
986 pub(crate) fn renderer_clock(&self) -> Option<RendererClock> {
987 *self.renderer_clock.lock()
988 }
989
990 pub(crate) fn set_renderer_clock(&self, clock: Option<RendererClock>) {
991 let mut guard = self.renderer_clock.lock();
992 if *guard != clock {
993 *guard = clock;
994 drop(guard);
995 self.changed();
996 }
997 }
998
999 pub fn renderer(&self) -> Option<crate::upnp::Output> {
1001 self.renderer.read().clone()
1002 }
1003
1004 pub(crate) fn set_renderer(&self, output: Option<crate::upnp::Output>) {
1005 *self.renderer.write() = output;
1006 self.changed();
1007 }
1008
1009 pub(crate) fn update_renderer(&self, f: impl FnOnce(&mut crate::upnp::Output)) {
1010 let changed = match self.renderer.write().as_mut() {
1011 Some(out) => {
1012 let before = out.clone();
1013 f(out);
1014 *out != before
1015 }
1016 None => false,
1017 };
1018 if changed {
1019 self.changed();
1020 }
1021 }
1022
1023 pub fn track_info(&self) -> Option<TrackInfo> {
1024 self.track_info.read().clone()
1025 }
1026
1027 pub fn set_track_info(&self, info: Option<TrackInfo>) {
1028 *self.track_info.write() = info;
1029 self.changed();
1030 }
1031
1032 pub fn seekable_ms(&self) -> u64 {
1043 let Some(info) = self.track_info.read().clone() else {
1044 return 0;
1045 };
1046
1047 let pl = self.playlist.read();
1050 let Some(item) = pl.items.iter().find(|item| item.id == info.id) else {
1051 return info.duration_ms;
1052 };
1053
1054 let LoadState::Downloading {
1055 total,
1056 bytes_written,
1057 ..
1058 } = self.load_state(item)
1059 else {
1060 return info.duration_ms;
1061 };
1062
1063 if info.duration_ms == 0 {
1068 return 0;
1069 }
1070
1071 let written = bytes_written.load(Ordering::Acquire);
1072 let reached = if total > 0 && info.duration_ms > 0 {
1073 ((written as f64 / total as f64) * info.duration_ms as f64) as u64
1074 } else if let Some(kbps) = info.bitrate_kbps.filter(|k| *k > 0) {
1075 written.saturating_mul(8) / kbps as u64
1079 } else {
1080 return self.position_ms();
1083 };
1084
1085 reached.saturating_sub(SEEK_SAFETY_MS).min(info.duration_ms)
1086 }
1087
1088 pub fn duration_ms(&self) -> u64 {
1096 let Some(info) = self.track_info.read().clone() else {
1097 return 0;
1098 };
1099 if info.duration_ms > 0 {
1100 return info.duration_ms;
1101 }
1102 self.playlist
1104 .read()
1105 .items
1106 .iter()
1107 .find(|item| item.id == info.id)
1108 .and_then(|item| item.duration_ms)
1109 .unwrap_or(0)
1110 }
1111
1112 pub fn seek_ceiling_ms(&self) -> Option<u64> {
1116 let duration = self.duration_ms();
1117 if duration == 0 {
1118 return None;
1119 }
1120 let seekable = self.seekable_ms();
1121 (seekable < duration).then_some(seekable)
1122 }
1123
1124 pub fn current_download_fraction(&self) -> Option<f64> {
1127 let id = self.track_info.read().as_ref()?.id;
1130 let pl = self.playlist.read();
1131 pl.items
1132 .iter()
1133 .find(|item| item.id == id)
1134 .and_then(|item| match self.load_state(item) {
1135 LoadState::Downloading {
1136 bytes_written,
1137 total,
1138 ..
1139 } => {
1140 let written = bytes_written.load(Ordering::Acquire);
1141 (total > 0).then(|| (written as f64 / total as f64).min(1.0))
1142 }
1143 _ => None,
1144 })
1145 }
1146
1147 pub fn request_quit(&self) {
1150 self.quit_requested.store(true, Ordering::Release);
1151 }
1152
1153 pub fn quit_requested(&self) -> bool {
1154 self.quit_requested.load(Ordering::Acquire)
1155 }
1156
1157 pub fn signal_metadata_refresh(&self) {
1163 self.metadata_refresh_pending.store(true, Ordering::Release);
1164 self.changed();
1165 }
1166
1167 pub fn take_metadata_refresh(&self) -> bool {
1169 self.metadata_refresh_pending
1170 .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire)
1171 .is_ok()
1172 }
1173
1174 pub fn output_sample_rate(&self) -> Option<u32> {
1178 match self.output_sample_rate.load(Ordering::Acquire) {
1179 0 => None,
1180 rate => Some(rate as u32),
1181 }
1182 }
1183
1184 pub fn set_output_sample_rate(&self, rate: u32) {
1185 self.output_sample_rate
1186 .store(u64::from(rate), Ordering::Release);
1187 self.changed();
1188 }
1189
1190 pub fn clear_output_sample_rate(&self) {
1195 self.output_sample_rate.store(0, Ordering::Release);
1196 }
1197
1198 pub fn dsp(&self) -> Option<crate::audio::dsp::DspStatus> {
1201 self.dsp.read().clone()
1202 }
1203
1204 pub fn set_dsp(&self, status: Option<crate::audio::dsp::DspStatus>) {
1205 let mut dsp = self.dsp.write();
1206 if *dsp != status {
1207 *dsp = status;
1208 drop(dsp);
1209 self.changed();
1210 }
1211 }
1212
1213 pub fn play_mode(&self) -> PlayMode {
1216 PlayMode::from_bits(self.play_mode.load(Ordering::Acquire))
1217 }
1218
1219 pub fn set_play_mode(&self, mode: PlayMode) {
1224 if self.play_mode.swap(mode.to_bits(), Ordering::AcqRel) != mode.to_bits() {
1225 self.content_version.fetch_add(1, Ordering::AcqRel);
1226 self.bump_version();
1227 }
1228 }
1229
1230 pub fn sleep(&self) -> Option<Sleep> {
1231 *self.sleep.read()
1232 }
1233
1234 pub fn set_sleep(&self, sleep: Option<Sleep>) {
1235 let mut held = self.sleep.write();
1236 if *held != sleep {
1237 *held = sleep;
1238 drop(held);
1239 self.changed();
1240 }
1241 }
1242
1243 pub fn sleep_fading(&self) -> bool {
1244 self.sleep_fading.load(Ordering::Acquire)
1245 }
1246
1247 pub fn set_sleep_fading(&self, fading: bool) {
1248 if self.sleep_fading.swap(fading, Ordering::AcqRel) != fading {
1249 self.changed();
1250 }
1251 }
1252
1253 pub fn playlist_version(&self) -> u64 {
1256 self.playlist_version.load(Ordering::Acquire)
1257 }
1258
1259 fn bump_version(&self) {
1260 self.playlist_version.fetch_add(1, Ordering::AcqRel);
1261 self.changed();
1262 }
1263
1264 pub fn content_version(&self) -> u64 {
1265 self.content_version.load(Ordering::Acquire)
1266 }
1267
1268 pub fn saved_version(&self) -> u64 {
1272 self.content_version()
1273 .wrapping_add(self.played_version.load(Ordering::Acquire))
1274 }
1275
1276 fn bump_content(&self) {
1280 self.playlist.write().reconcile();
1281 self.content_version.fetch_add(1, Ordering::AcqRel);
1282 self.pending_version.fetch_add(1, Ordering::AcqRel);
1283 self.bump_version();
1284 }
1285
1286 pub fn pending_version(&self) -> u64 {
1289 self.pending_version.load(Ordering::Acquire)
1290 }
1291
1292 pub fn downloads(&self) -> &Arc<DownloadStore> {
1294 &self.downloads
1295 }
1296
1297 fn load_state(&self, item: &PlaylistItem) -> LoadState {
1298 LoadState::of(item, &self.downloads)
1299 }
1300
1301 pub fn changed(&self) {
1307 crate::signal::engine_changed().bump();
1308 }
1309
1310 pub fn add_items(&self, items: Vec<PlaylistItem>) {
1314 let mut pl = self.playlist.write();
1315 pl.items.extend(items);
1316 drop(pl);
1317 self.bump_content();
1318 }
1319
1320 pub fn insert_items_after(&self, items: Vec<PlaylistItem>, after: QueueItemId) {
1322 let mut pl = self.playlist.write();
1323 let insert_at = match pl.items.iter().position(|item| item.id == after) {
1324 Some(pos) => pos + 1,
1325 None => pl.items.len(), };
1327 for (i, item) in items.into_iter().enumerate() {
1328 pl.items.insert(insert_at + i, item);
1329 }
1330 drop(pl);
1331 self.bump_content();
1332 }
1333
1334 pub fn update_paths(&self, updates: &[(QueueItemId, PathBuf)]) {
1336 let mut pl = self.playlist.write();
1337 for (id, new_path) in updates {
1338 if let Some(item) = pl.items.iter_mut().find(|item| item.id == *id) {
1339 item.path = new_path.clone();
1340 }
1341 }
1342 drop(pl);
1343 self.bump_content();
1344 }
1345
1346 pub fn remove_item(&self, id: QueueItemId) {
1348 let mut pl = self.playlist.write();
1349 pl.items.retain(|item| item.id != id);
1350 if pl.cursor == Some(id) {
1352 pl.cursor = None;
1353 }
1354 drop(pl);
1355 self.bump_content();
1356 }
1357
1358 pub fn move_item(&self, id: QueueItemId, target: QueueItemId, after: bool) {
1360 let mut pl = self.playlist.write();
1361 let Some(from) = pl.items.iter().position(|item| item.id == id) else {
1362 return;
1363 };
1364 let item = pl.items.remove(from);
1365 let Some(to) = pl.items.iter().position(|item| item.id == target) else {
1366 let pos = from.min(pl.items.len());
1368 pl.items.insert(pos, item);
1369 return;
1370 };
1371 let insert_at = if after { to + 1 } else { to };
1372 pl.items.insert(insert_at, item);
1373 drop(pl);
1374 self.bump_content();
1375 }
1376
1377 pub fn move_items(&self, ids: &[QueueItemId], target: QueueItemId, after: bool) {
1380 use std::collections::HashSet;
1381 let id_set: HashSet<QueueItemId> = ids.iter().copied().collect();
1382
1383 let mut pl = self.playlist.write();
1384
1385 let mut remaining = Vec::with_capacity(pl.items.len());
1387 let mut moved = Vec::with_capacity(ids.len());
1388 for item in pl.items.drain(..) {
1389 if id_set.contains(&item.id) {
1390 moved.push(item);
1391 } else {
1392 remaining.push(item);
1393 }
1394 }
1395
1396 let insert_at = match remaining.iter().position(|item| item.id == target) {
1398 Some(pos) => {
1399 if after {
1400 pos + 1
1401 } else {
1402 pos
1403 }
1404 }
1405 None => remaining.len(),
1406 };
1407
1408 for (i, item) in moved.into_iter().enumerate() {
1410 remaining.insert(insert_at + i, item);
1411 }
1412
1413 pl.items = remaining;
1414 drop(pl);
1415 self.bump_content();
1416 }
1417
1418 pub fn set_cursor(&self, id: Option<QueueItemId>) {
1420 let mut pl = self.playlist.write();
1421 pl.place_cursor(id);
1422 drop(pl);
1423 self.bump_version();
1424 }
1425
1426 pub fn pick(&self, id: QueueItemId) {
1429 let mut pl = self.playlist.write();
1430 pl.place_cursor(Some(id));
1431 let replayed = pl.start_over_if_spent();
1432 drop(pl);
1433 if replayed {
1434 self.played_version.fetch_add(1, Ordering::AcqRel);
1435 }
1436 self.bump_version();
1437 }
1438
1439 pub fn move_on_to(&self, id: QueueItemId, pass: Option<u64>) {
1444 let mut pl = self.playlist.write();
1445 pl.move_on(id, pass);
1446 drop(pl);
1447 self.played_version.fetch_add(1, Ordering::AcqRel);
1448 self.bump_version();
1449 }
1450
1451 pub fn mark_played(&self, id: QueueItemId) {
1453 let mut pl = self.playlist.write();
1454 let Some(item) = pl.items.iter_mut().find(|item| item.id == id) else {
1455 return;
1456 };
1457 if std::mem::replace(&mut item.played, true) {
1458 return;
1459 }
1460 drop(pl);
1461 self.played_version.fetch_add(1, Ordering::AcqRel);
1462 self.bump_version();
1463 }
1464
1465 pub fn set_shuffled(&self, on: bool) {
1469 let mut pl = self.playlist.write();
1470 if pl.order.is_some() == on {
1471 return;
1472 }
1473 pl.order = on.then(PlayOrder::default);
1474 let cursor = pl.cursor;
1475 pl.reconcile();
1476 pl.place_cursor(cursor);
1477 if on && pl.start_over_if_spent() {
1478 self.played_version.fetch_add(1, Ordering::AcqRel);
1479 }
1480 drop(pl);
1481 self.pending_version.fetch_add(1, Ordering::AcqRel);
1483 self.bump_version();
1484 }
1485
1486 pub fn is_shuffled(&self) -> bool {
1487 self.playlist.read().order.is_some()
1488 }
1489
1490 #[cfg(test)]
1492 pub(crate) fn upcoming(&self) -> Vec<QueueItemId> {
1493 let pl = self.playlist.read();
1494 pl.order
1495 .as_ref()
1496 .map(|o| o.upcoming.clone())
1497 .unwrap_or_default()
1498 }
1499
1500 pub fn is_empty(&self) -> bool {
1501 self.playlist.read().items.is_empty()
1502 }
1503
1504 pub fn cursor(&self) -> Option<QueueItemId> {
1505 self.playlist.read().cursor
1506 }
1507
1508 pub fn cursor_path(&self) -> Option<PathBuf> {
1510 let pl = self.playlist.read();
1511 let cursor = pl.cursor?;
1512 pl.items
1513 .iter()
1514 .find(|item| item.id == cursor)
1515 .map(|item| item.path.clone())
1516 }
1517
1518 pub fn replace_playlist(
1528 &self,
1529 items: Vec<PlaylistItem>,
1530 ) -> (Vec<PlaylistItem>, Option<QueueItemId>) {
1531 let mut pl = self.playlist.write();
1532 let old = std::mem::replace(&mut pl.items, items);
1533 let cursor = pl.cursor.take();
1534 drop(pl);
1535 self.bump_content();
1536 (old, cursor)
1537 }
1538
1539 pub fn clear_playlist(&self) {
1540 let mut pl = self.playlist.write();
1541 pl.items.clear();
1542 pl.cursor = None;
1543 drop(pl);
1544 self.bump_content();
1545 }
1546
1547 pub fn advance_cursor_loadable(&self) -> Option<QueueItemId> {
1563 let repeat = match self.play_mode().repeat {
1564 Repeat::Off => Repeat::Off,
1565 Repeat::Queue | Repeat::One => Repeat::Queue,
1566 };
1567 let mut pl = self.playlist.write();
1568 let cursor = pl.cursor;
1569 let (next, wrapped) = pl.follows(cursor, false, repeat)?;
1570 let pass = pl.pass + u64::from(wrapped);
1571 pl.move_on(next?, Some(pass));
1572 drop(pl);
1573 self.played_version.fetch_add(1, Ordering::AcqRel);
1574 self.bump_version();
1575 next
1576 }
1577
1578 pub fn lookahead_after(&self, after_id: QueueItemId) -> Option<Lookahead> {
1595 self.lookahead(after_id, None)
1596 }
1597
1598 pub fn lookahead_from(&self, step: &Lookahead) -> Option<Lookahead> {
1601 self.lookahead(step.chosen.as_ref()?.0, Some(step.pass))
1602 }
1603
1604 fn lookahead(&self, after_id: QueueItemId, after_pass: Option<u64>) -> Option<Lookahead> {
1605 let repeat = self.play_mode().repeat;
1606 let mut pl = self.playlist.write();
1607 let after_pass = after_pass.unwrap_or(pl.pass);
1608 let ahead = after_pass > pl.pass;
1609 let (next, wrapped) = pl.follows(Some(after_id), ahead, repeat)?;
1610 let pass = pl.pass + u64::from(ahead || wrapped);
1611 let next = next.and_then(|id| pl.find(id));
1612 Some(Lookahead {
1613 after: after_id,
1614 next: next.map(|item| item.id),
1615 chosen: next
1616 .filter(|item| matches!(item.state, ItemState::Ready))
1617 .map(|item| (item.id, item.path.clone())),
1618 wrapped,
1619 boundary: 0,
1620 after_pass,
1621 pass,
1622 })
1623 }
1624
1625 pub fn still_follows(&self, step: &Lookahead) -> bool {
1632 let repeat = self.play_mode().repeat;
1633 let mut pl = self.playlist.write();
1634 let ahead = step.after_pass > pl.pass;
1635 pl.follows(Some(step.after), ahead, repeat)
1636 .is_some_and(|(next, _)| next == step.next)
1637 }
1638
1639 pub fn retreat_cursor(&self) -> Option<(QueueItemId, PathBuf)> {
1646 let mut pl = self.playlist.write();
1647 if pl.order.is_some() {
1648 let cursor = pl.cursor;
1649 let history = &pl.order.as_ref()?.history;
1650 let end =
1651 history.len() - usize::from(cursor.is_some() && history.last() == cursor.as_ref());
1652 let at = history[..end]
1653 .iter()
1654 .rposition(|id| pl.find(*id).is_some())?;
1655 let id = history[at];
1656 let path = pl.find(id)?.path.clone();
1657 let order = pl.order.as_mut()?;
1658 order.history.truncate(at + 1);
1659 pl.cursor = Some(id);
1660 drop(pl);
1661 self.bump_version();
1662 return Some((id, path));
1663 }
1664 let cursor_pos = match pl.cursor {
1665 Some(cid) => pl.items.iter().position(|item| item.id == cid),
1666 None => None,
1667 };
1668
1669 let wraps = self.play_mode().repeat != Repeat::Off && pl.items.len() > 1;
1672 let prev_pos = cursor_pos.and_then(|p| match p.checked_sub(1) {
1673 None if wraps => Some(pl.items.len() - 1),
1674 prev => prev,
1675 });
1676
1677 match prev_pos {
1678 Some(pos) => {
1679 let item = &pl.items[pos];
1680 let result = (item.id, item.path.clone());
1681 pl.cursor = Some(item.id);
1682 drop(pl);
1683 self.bump_version();
1684 Some(result)
1685 }
1686 None => None,
1687 }
1688 }
1689
1690 pub fn update_item_state(&self, id: QueueItemId, new_state: ItemState) {
1694 let pending = new_state == ItemState::Pending;
1695 let mut pl = self.playlist.write();
1696 if let Some(item) = pl.items.iter_mut().find(|item| item.id == id) {
1697 item.state = new_state;
1698 }
1699 drop(pl);
1700 if pending {
1701 self.pending_version.fetch_add(1, Ordering::AcqRel);
1702 }
1703 self.bump_version();
1704 }
1705
1706 pub fn item_state(&self, id: QueueItemId) -> Option<ItemState> {
1708 let pl = self.playlist.read();
1709 pl.items
1710 .iter()
1711 .find(|item| item.id == id)
1712 .map(|item| item.state.clone())
1713 }
1714
1715 pub fn update_item_metadata(
1725 &self,
1726 id: QueueItemId,
1727 title: String,
1728 artist: String,
1729 album_artist: String,
1730 album: String,
1731 duration_ms: Option<u64>,
1732 ) {
1733 let mut pl = self.playlist.write();
1734 let mut retagged = false;
1735 let mut retimed = false;
1736 if let Some(item) = pl.items.iter_mut().find(|item| item.id == id) {
1737 if item.db_id.is_none() {
1738 retagged = item.title != title
1739 || item.artist != artist
1740 || item.album_artist != album_artist
1741 || item.album != album;
1742 item.title = title;
1743 item.artist = artist;
1744 item.album_artist = album_artist;
1745 item.album = album;
1746 }
1747 if let Some(dur) = duration_ms
1748 && item.duration_ms != Some(dur)
1749 {
1750 item.duration_ms = Some(dur);
1751 retimed = true;
1752 }
1753 }
1754 drop(pl);
1755 if retagged {
1762 self.bump_content();
1763 } else if retimed {
1764 self.bump_version();
1765 }
1766 }
1767
1768 pub fn item_playback_source(&self, id: QueueItemId) -> Option<PlaybackSource> {
1771 let pl = self.playlist.read();
1772 pl.items
1773 .iter()
1774 .find(|item| item.id == id)
1775 .and_then(|item| match self.load_state(item) {
1776 LoadState::Ready => Some(PlaybackSource::Ready(item.path.clone())),
1777 LoadState::Downloading {
1778 path,
1779 total,
1780 bytes_written,
1781 } => {
1782 let written = bytes_written.load(Ordering::Acquire);
1783 (written >= STREAM_THRESHOLD).then_some(PlaybackSource::Streaming {
1784 path,
1785 bytes_written,
1786 total,
1787 })
1788 }
1789 _ => None,
1790 })
1791 }
1792
1793 pub fn reset_items_with_missing_files(&self) -> Vec<(i64, QueueItemId)> {
1803 let mut pl = self.playlist.write();
1804 let mut reset = Vec::new();
1805 for item in pl.items.iter_mut() {
1806 let Some(db_id) = item.db_id else { continue };
1807 if !matches!(item.state, ItemState::Ready) {
1808 continue;
1809 }
1810 if item.path.exists() {
1811 continue;
1812 }
1813 item.state = ItemState::Pending;
1814 reset.push((db_id, item.id));
1815 }
1816 drop(pl);
1817 if !reset.is_empty() {
1818 self.pending_version.fetch_add(1, Ordering::AcqRel);
1819 self.bump_version();
1820 }
1821 reset
1822 }
1823
1824 pub fn item_path_if_ready(&self, id: QueueItemId) -> Option<PathBuf> {
1826 let pl = self.playlist.read();
1827 pl.items.iter().find(|item| item.id == id).and_then(|item| {
1828 if matches!(item.state, ItemState::Ready) {
1829 Some(item.path.clone())
1830 } else {
1831 None
1832 }
1833 })
1834 }
1835
1836 pub fn is_cursor(&self, id: QueueItemId) -> bool {
1837 self.playlist.read().cursor == Some(id)
1838 }
1839
1840 pub fn same_album_item_ids(&self, id: QueueItemId) -> Vec<QueueItemId> {
1844 let pl = self.playlist.read();
1845 let Some(cursor) = pl.items.iter().find(|item| item.id == id) else {
1846 return vec![];
1847 };
1848 let album = cursor.album.clone();
1849 let album_artist = cursor.album_artist.clone();
1850 pl.items
1851 .iter()
1852 .filter(|item| {
1853 item.id != id && item.album == album && item.album_artist == album_artist
1854 })
1855 .map(|item| item.id)
1856 .collect()
1857 }
1858
1859 pub fn pending_downloads(&self) -> Vec<(i64, QueueItemId)> {
1864 let pl = self.playlist.read();
1865 reach_order(&pl)
1866 .filter(|item| matches!(item.state, ItemState::Pending))
1867 .filter_map(|item| item.db_id.map(|db_id| (db_id, item.id)))
1868 .collect()
1869 }
1870
1871 pub fn playback_order(&self) -> Vec<(i64, QueueItemId)> {
1875 let pl = self.playlist.read();
1876 reach_order(&pl)
1877 .filter_map(|item| item.db_id.map(|db_id| (db_id, item.id)))
1878 .collect()
1879 }
1880
1881 pub fn item_db_id(&self, id: QueueItemId) -> Option<i64> {
1883 let pl = self.playlist.read();
1884 pl.items
1885 .iter()
1886 .find(|item| item.id == id)
1887 .and_then(|item| item.db_id)
1888 }
1889
1890 pub fn item_load_state(&self, id: QueueItemId) -> Option<LoadState> {
1892 let pl = self.playlist.read();
1893 pl.items
1894 .iter()
1895 .find(|item| item.id == id)
1896 .map(|item| self.load_state(item))
1897 }
1898
1899 pub fn snapshot_playlist(&self) -> (Vec<PlaylistItem>, Option<QueueItemId>) {
1903 let pl = self.playlist.read();
1904 (pl.items.clone(), pl.cursor)
1905 }
1906
1907 pub fn get_item(&self, id: QueueItemId) -> Option<PlaylistItem> {
1909 let pl = self.playlist.read();
1910 pl.items.iter().find(|item| item.id == id).cloned()
1911 }
1912
1913 pub fn item_before(&self, id: QueueItemId) -> Option<QueueItemId> {
1915 let pl = self.playlist.read();
1916 let pos = pl.items.iter().position(|item| item.id == id)?;
1917 if pos == 0 {
1918 None
1919 } else {
1920 Some(pl.items[pos - 1].id)
1921 }
1922 }
1923
1924 pub fn reorder_to(&self, order: &[QueueItemId]) {
1931 let mut pl = self.playlist.write();
1932 let mut taken: Vec<Option<PlaylistItem>> = pl.items.drain(..).map(Some).collect();
1933 let mut sorted = Vec::with_capacity(taken.len());
1934 for id in order {
1935 if let Some(slot) = taken
1936 .iter_mut()
1937 .find(|i| i.as_ref().is_some_and(|i| i.id == *id))
1938 && let Some(item) = slot.take()
1939 {
1940 sorted.push(item);
1941 }
1942 }
1943 sorted.extend(taken.into_iter().flatten());
1944 pl.items = sorted;
1945 drop(pl);
1946 self.bump_content();
1947 }
1948
1949 pub fn items_before(&self, ids: &[QueueItemId]) -> Vec<(QueueItemId, Option<QueueItemId>)> {
1956 use std::collections::HashSet;
1957 let wanted: HashSet<QueueItemId> = ids.iter().copied().collect();
1958 let pl = self.playlist.read();
1959 pl.items
1960 .iter()
1961 .enumerate()
1962 .filter(|(_, item)| wanted.contains(&item.id))
1963 .map(|(pos, item)| {
1964 let before = if pos == 0 {
1965 None
1966 } else {
1967 Some(pl.items[pos - 1].id)
1968 };
1969 (item.id, before)
1970 })
1971 .collect()
1972 }
1973
1974 pub fn surviving_item_before(
1978 &self,
1979 id: QueueItemId,
1980 removed: &[QueueItemId],
1981 ) -> Option<QueueItemId> {
1982 use std::collections::HashSet;
1983 let removed: HashSet<QueueItemId> = removed.iter().copied().collect();
1984 let pl = self.playlist.read();
1985 let pos = pl.items.iter().position(|item| item.id == id)?;
1986 pl.items[..pos]
1987 .iter()
1988 .rev()
1989 .find(|item| !removed.contains(&item.id))
1990 .map(|item| item.id)
1991 }
1992
1993 pub fn restore_playlist(&self, items: Vec<PlaylistItem>, cursor: Option<QueueItemId>) {
1995 let mut pl = self.playlist.write();
1996 pl.items = items;
1997 pl.cursor = cursor;
1998 drop(pl);
1999 self.bump_content();
2000 }
2001
2002 pub fn remove_items(&self, ids: &[QueueItemId]) {
2004 use std::collections::HashSet;
2005 let id_set: HashSet<QueueItemId> = ids.iter().copied().collect();
2006 let mut pl = self.playlist.write();
2007 pl.items.retain(|item| !id_set.contains(&item.id));
2008 if let Some(cursor) = pl.cursor
2009 && id_set.contains(&cursor)
2010 {
2011 pl.cursor = None;
2012 }
2013 drop(pl);
2014 self.bump_content();
2015 }
2016
2017 pub fn insert_item_at(&self, item: PlaylistItem, after: Option<QueueItemId>) {
2019 let mut pl = self.playlist.write();
2020 let insert_at = match after {
2021 Some(after_id) => {
2022 match pl.items.iter().position(|i| i.id == after_id) {
2023 Some(pos) => pos + 1,
2024 None => pl.items.len(), }
2026 }
2027 None => 0,
2028 };
2029 pl.items.insert(insert_at, item);
2030 drop(pl);
2031 self.bump_content();
2032 }
2033
2034 pub fn move_item_to(&self, id: QueueItemId, after: Option<QueueItemId>) {
2036 let mut pl = self.playlist.write();
2037 let Some(from) = pl.items.iter().position(|item| item.id == id) else {
2038 return;
2039 };
2040 let item = pl.items.remove(from);
2041 let insert_at = match after {
2042 Some(after_id) => match pl.items.iter().position(|i| i.id == after_id) {
2043 Some(pos) => pos + 1,
2044 None => pl.items.len(),
2045 },
2046 None => 0,
2047 };
2048 pl.items.insert(insert_at, item);
2049 drop(pl);
2050 self.bump_content();
2051 }
2052
2053 pub fn move_items_to(&self, entries: &[(QueueItemId, Option<QueueItemId>)]) {
2056 for &(id, after) in entries {
2057 self.move_item_to(id, after);
2058 }
2059 }
2060
2061 pub fn derive_visible_queue(&self) -> VisibleQueueSnapshot {
2067 let mut entries = Vec::with_capacity(self.playlist.read().items.len());
2068 let mut finished_count = 0;
2069 let mut has_playing = false;
2070 let mut queue_count = 0;
2071 self.each_visible(|item, place, reading| {
2072 match place {
2073 Place::Played => finished_count += 1,
2074 Place::Cursor => has_playing = true,
2075 Place::Unplayed => queue_count += 1,
2076 }
2077 entries.push(QueueEntry {
2078 id: item.id,
2079 db_id: item.db_id,
2080 playlist_entry_id: item.playlist_entry_id,
2081 path: item.path.clone(),
2082 title: item.title.clone(),
2083 artist: item.artist.clone(),
2084 album_artist: item.album_artist.clone(),
2085 album: item.album.clone(),
2086 year: item.year.clone(),
2087 codec: item.codec.clone(),
2088 track_number: item.track_number,
2089 disc: item.disc,
2090 duration_ms: reading.duration_ms,
2091 status: reading.status,
2092 download_progress: reading.download_progress,
2093 error: reading.error,
2094 });
2095 });
2096
2097 VisibleQueueSnapshot {
2098 entries,
2099 finished_count,
2100 has_playing,
2101 queue_count,
2102 }
2103 }
2104
2105 pub fn queue_readings(&self) -> Vec<QueueReading> {
2113 let mut readings = Vec::with_capacity(self.playlist.read().items.len());
2114 self.each_visible(|_, _, reading| readings.push(reading));
2115 readings
2116 }
2117
2118 fn each_visible(&self, mut f: impl FnMut(&PlaylistItem, Place, QueueReading)) {
2125 let playing_duration_ms = self.track_info.read().as_ref().map(|ti| ti.duration_ms);
2127 let transfers: HashMap<i64, (u64, u64)> = self
2130 .downloads
2131 .readings()
2132 .into_iter()
2133 .map(|r| (r.track_id, (r.written, r.total)))
2134 .collect();
2135 let pl = self.playlist.read();
2136
2137 for item in &pl.items {
2138 let place = if pl.cursor == Some(item.id) {
2139 Place::Cursor
2140 } else if item.played {
2141 Place::Played
2142 } else {
2143 Place::Unplayed
2144 };
2145
2146 let download_progress = match item.state {
2150 ItemState::Pending => item.db_id.and_then(|id| transfers.get(&id).copied()),
2151 _ => None,
2152 };
2153 let transferring = download_progress.is_some();
2154
2155 let status = match (place, &item.state) {
2156 (Place::Cursor, state) => QueueEntryStatus::at_cursor(state, transferring),
2157 (_, ItemState::Failed(_)) => QueueEntryStatus::Failed,
2158 (_, ItemState::Pending) if transferring => QueueEntryStatus::Downloading,
2159 (Place::Played, _) => QueueEntryStatus::Played,
2160 (Place::Unplayed, _) => QueueEntryStatus::Queued,
2163 };
2164
2165 let duration_ms = if status == QueueEntryStatus::Playing && item.duration_ms.is_none() {
2168 playing_duration_ms
2169 } else {
2170 item.duration_ms
2171 };
2172
2173 f(
2174 item,
2175 place,
2176 QueueReading {
2177 id: item.id,
2178 db_id: item.db_id,
2179 status,
2180 duration_ms,
2181 download_progress,
2182 error: match &item.state {
2183 ItemState::Failed(reason) => Some(reason.clone()),
2184 _ => None,
2185 },
2186 },
2187 );
2188 }
2189 }
2190
2191 pub fn playlist_window(
2197 &self,
2198 before: usize,
2199 max: usize,
2200 ) -> (Vec<PlaylistItem>, Option<QueueItemId>) {
2201 let pl = self.playlist.read();
2202 let at = pl
2203 .cursor
2204 .and_then(|c| pl.items.iter().position(|i| i.id == c))
2205 .unwrap_or(0);
2206 let start = at.saturating_sub(before);
2207 let end = pl.items.len().min(start + max);
2208 (pl.items[start..end].to_vec(), pl.cursor)
2209 }
2210}
2211
2212fn reach_order(pl: &Playlist) -> impl Iterator<Item = &PlaylistItem> {
2217 let from = pl
2218 .cursor
2219 .and_then(|c| pl.items.iter().position(|item| item.id == c))
2220 .unwrap_or(0);
2221 let (before, after) = pl.items.split_at(from);
2222 let queue = after.iter().chain(before);
2223 let mut first: Vec<&PlaylistItem> = Vec::new();
2224 if let Some(order) = &pl.order {
2225 let rows: HashMap<QueueItemId, &PlaylistItem> =
2226 pl.items.iter().map(|item| (item.id, item)).collect();
2227 first.extend(pl.cursor.and_then(|c| rows.get(&c).copied()));
2228 first.extend(order.upcoming.iter().filter_map(|id| rows.get(id).copied()));
2229 }
2230 let led: std::collections::HashSet<QueueItemId> = first.iter().map(|item| item.id).collect();
2231 first
2232 .into_iter()
2233 .chain(queue.filter(move |item| !led.contains(&item.id)))
2234}
2235
2236#[derive(Clone, Copy)]
2238enum Place {
2239 Played,
2240 Cursor,
2241 Unplayed,
2242}
2243
2244#[cfg(test)]
2245mod tests {
2246 use super::*;
2247
2248 fn make_item(title: &str, state: ItemState) -> PlaylistItem {
2251 PlaylistItem {
2252 playlist_entry_id: None,
2253 id: QueueItemId::new(),
2254 db_id: None,
2255 path: PathBuf::from(format!("/music/{title}.flac")),
2256 title: title.to_string(),
2257 artist: "Artist".to_string(),
2258 album_artist: "Artist".to_string(),
2259 album: "Album".to_string(),
2260 year: None,
2261 codec: Some("FLAC".to_string()),
2262 track_number: None,
2263 disc: None,
2264 duration_ms: Some(200_000),
2265 state,
2266 played: false,
2267 }
2268 }
2269
2270 fn tracks(n: usize) -> Vec<PlaylistItem> {
2274 (0..n)
2275 .map(|i| PlaylistItem {
2276 db_id: Some(i as i64 + 1),
2277 ..make_item(&format!("t{i}"), ItemState::Ready)
2278 })
2279 .collect()
2280 }
2281
2282 fn shuffled(items: Vec<PlaylistItem>) -> (Arc<SharedPlayerState>, Vec<QueueItemId>) {
2283 let state = SharedPlayerState::new();
2284 let ids = items.iter().map(|i| i.id).collect();
2285 state.add_items(items);
2286 state.set_shuffled(true);
2287 (state, ids)
2288 }
2289
2290 fn queue_ids(state: &SharedPlayerState) -> Vec<QueueItemId> {
2291 state.snapshot_playlist().0.iter().map(|i| i.id).collect()
2292 }
2293
2294 fn play_on(state: &SharedPlayerState) -> Option<QueueItemId> {
2297 if let Some(id) = state.cursor() {
2298 state.mark_played(id);
2299 }
2300 state.advance_cursor_loadable()
2301 }
2302
2303 fn status_of(state: &SharedPlayerState, id: QueueItemId) -> QueueEntryStatus {
2304 let snap = state.derive_visible_queue();
2305 snap.entries.iter().find(|e| e.id == id).unwrap().status
2306 }
2307
2308 fn set_repeat(state: &SharedPlayerState, repeat: Repeat) {
2309 let shuffle = state.play_mode().shuffle;
2310 state.set_play_mode(PlayMode { shuffle, repeat });
2311 }
2312
2313 #[test]
2314 fn shuffle_on_and_off_never_moves_the_queue() {
2315 let state = SharedPlayerState::new();
2316 let items = tracks(20);
2317 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
2318 state.add_items(items);
2319 state.set_cursor(Some(ids[3]));
2320
2321 state.set_shuffled(true);
2322 assert_eq!(queue_ids(&state), ids);
2323 for _ in 0..6 {
2324 play_on(&state).unwrap();
2325 }
2326 assert_eq!(queue_ids(&state), ids, "playing shuffled moves nothing");
2327 state.set_shuffled(false);
2328 assert_eq!(queue_ids(&state), ids);
2329 }
2330
2331 #[test]
2332 fn the_rows_that_played_shuffled_stay_marked_once_it_is_off() {
2333 let (state, ids) = shuffled(tracks(20));
2334 let mut heard = vec![state.advance_cursor_loadable().unwrap()];
2335 for _ in 0..7 {
2336 heard.push(play_on(&state).unwrap());
2337 }
2338 let playing = state.cursor().unwrap();
2339 state.set_shuffled(false);
2340
2341 for &id in &ids {
2342 let expected = if id == playing {
2343 QueueEntryStatus::Playing
2344 } else if heard.contains(&id) {
2345 QueueEntryStatus::Played
2346 } else {
2347 QueueEntryStatus::Queued
2348 };
2349 assert_eq!(
2350 status_of(&state, id),
2351 expected,
2352 "row {}",
2353 ids.iter().position(|i| *i == id).unwrap()
2354 );
2355 }
2356 let snap = state.derive_visible_queue();
2357 assert_eq!(snap.finished_count, 7);
2358 assert_eq!(snap.queue_count, 12);
2359
2360 let at = ids.iter().position(|id| *id == playing).unwrap();
2362 assert_eq!(play_on(&state), ids.get(at + 1).copied());
2363 }
2364
2365 #[test]
2366 fn a_row_is_played_because_it_played_not_for_being_behind_the_cursor() {
2367 let state = SharedPlayerState::new();
2368 let items = tracks(6);
2369 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
2370 state.add_items(items);
2371 state.set_cursor(Some(ids[0]));
2372 play_on(&state);
2373 state.set_cursor(Some(ids[4]));
2374
2375 assert_eq!(status_of(&state, ids[0]), QueueEntryStatus::Played);
2376 for &id in &ids[1..4] {
2377 assert_eq!(
2378 status_of(&state, id),
2379 QueueEntryStatus::Queued,
2380 "jumped over"
2381 );
2382 }
2383 }
2384
2385 #[test]
2386 fn a_shuffled_pass_plays_every_track_once() {
2387 let (state, ids) = shuffled(tracks(30));
2388 let mut heard = vec![state.advance_cursor_loadable().unwrap()];
2389 while let Some(id) = play_on(&state) {
2390 heard.push(id);
2391 }
2392 assert_ne!(heard, ids, "a shuffled order");
2393 let mut sorted = heard.clone();
2394 sorted.sort_by_key(|id| ids.iter().position(|i| i == id));
2395 assert_eq!(sorted, ids, "each once");
2396 }
2397
2398 #[test]
2399 fn a_track_queued_twice_plays_once_a_pass() {
2400 let mut items = tracks(5);
2401 let twins: Vec<_> = [1, 3, 3]
2402 .iter()
2403 .map(|&i| PlaylistItem {
2404 id: QueueItemId::new(),
2405 ..items[i].clone()
2406 })
2407 .collect();
2408 items.extend(twins);
2409 let (state, _) = shuffled(items);
2410 let track = |id| state.item_db_id(id).unwrap();
2411
2412 let mut heard = vec![track(state.advance_cursor_loadable().unwrap())];
2413 while let Some(id) = play_on(&state) {
2414 heard.push(track(id));
2415 }
2416 heard.sort();
2417 assert_eq!(heard, vec![1, 2, 3, 4, 5]);
2418 }
2419
2420 #[test]
2421 fn rows_queued_while_shuffled_play_later_in_the_pass() {
2422 let (state, ids) = shuffled(tracks(10));
2423 let mut heard = vec![state.advance_cursor_loadable().unwrap()];
2424 for _ in 0..3 {
2425 heard.push(play_on(&state).unwrap());
2426 }
2427 let more: Vec<_> = (10..15)
2428 .map(|i| PlaylistItem {
2429 db_id: Some(i + 1),
2430 ..make_item(&format!("t{i}"), ItemState::Ready)
2431 })
2432 .collect();
2433 let added: Vec<_> = more.iter().map(|i| i.id).collect();
2434 state.add_items(more);
2435 assert_eq!(queue_ids(&state)[10..], added, "added where they were put");
2436
2437 while let Some(id) = play_on(&state) {
2438 heard.push(id);
2439 }
2440 assert_eq!(heard.len(), 15);
2441 assert!(added.iter().all(|id| heard[4..].contains(id)));
2442 assert!(ids.iter().all(|id| heard.contains(id)));
2443 }
2444
2445 #[test]
2446 fn a_row_removed_while_shuffled_does_not_play() {
2447 let (state, ids) = shuffled(tracks(10));
2448 let mut heard = vec![state.advance_cursor_loadable().unwrap()];
2449 let gone = *ids.iter().find(|id| !heard.contains(id)).unwrap();
2450 state.remove_items(&[gone]);
2451 while let Some(id) = play_on(&state) {
2452 heard.push(id);
2453 }
2454 assert_eq!(heard.len(), 9);
2455 assert!(!heard.contains(&gone));
2456 }
2457
2458 #[test]
2459 fn previous_goes_back_along_what_played_while_shuffled() {
2460 let (state, ids) = shuffled(tracks(12));
2461 let mut heard = vec![state.advance_cursor_loadable().unwrap()];
2462 for _ in 0..4 {
2463 heard.push(play_on(&state).unwrap());
2464 }
2465 assert_eq!(state.retreat_cursor().map(|(id, _)| id), Some(heard[3]));
2466 assert_eq!(state.retreat_cursor().map(|(id, _)| id), Some(heard[2]));
2467 assert_eq!(state.retreat_cursor().map(|(id, _)| id), Some(heard[1]));
2468 assert_eq!(state.retreat_cursor().map(|(id, _)| id), Some(heard[0]));
2469 assert_eq!(state.retreat_cursor(), None, "nothing before the first");
2470 assert_eq!(state.cursor(), Some(heard[0]));
2471
2472 let next = play_on(&state).unwrap();
2474 assert!(!heard.contains(&next));
2475 assert_eq!(queue_ids(&state), ids);
2476 }
2477
2478 #[test]
2479 fn a_repeating_shuffled_queue_starts_a_new_pass_once_every_track_played() {
2480 let (state, ids) = shuffled(tracks(8));
2481 set_repeat(&state, Repeat::Queue);
2482 let mut heard = vec![state.advance_cursor_loadable().unwrap()];
2483 for _ in 0..(3 * ids.len() - 1) {
2484 heard.push(play_on(&state).unwrap());
2485 }
2486 for (n, pass) in heard.chunks(ids.len()).enumerate() {
2487 let mut sorted = pass.to_vec();
2488 sorted.sort_by_key(|id| ids.iter().position(|i| i == id));
2489 assert_eq!(sorted, ids, "pass {n} plays each track once");
2490 }
2491 for turn in heard.windows(2) {
2492 assert_ne!(turn[0], turn[1], "no track twice running at a turn");
2493 }
2494
2495 let snap = state.derive_visible_queue();
2498 assert_eq!(snap.finished_count, ids.len() - 1);
2499 play_on(&state);
2500 let snap = state.derive_visible_queue();
2501 assert_eq!(snap.finished_count, 0, "a new pass clears the marks");
2502 assert!(snap.has_playing);
2503 }
2504
2505 #[test]
2506 fn repeating_in_order_starts_a_new_pass_at_the_top() {
2507 let state = SharedPlayerState::new();
2508 let items = tracks(4);
2509 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
2510 state.add_items(items);
2511 set_repeat(&state, Repeat::Queue);
2512 state.set_cursor(Some(ids[0]));
2513 for _ in 0..4 {
2514 play_on(&state);
2515 }
2516 assert_eq!(state.cursor(), Some(ids[0]));
2517 for &id in &ids[1..] {
2518 assert_eq!(status_of(&state, id), QueueEntryStatus::Queued);
2519 }
2520 }
2521
2522 #[test]
2523 fn the_lookahead_picks_what_the_advance_plays() {
2524 let (state, ids) = shuffled(tracks(7));
2525 set_repeat(&state, Repeat::Queue);
2526 state.advance_cursor_loadable();
2527 for _ in 0..(3 * ids.len()) {
2531 let cursor = state.cursor().unwrap();
2532 let next = state.lookahead_after(cursor).unwrap();
2533 let after = state.lookahead_from(&next).unwrap();
2534 assert!(state.still_follows(&next));
2535 assert_eq!(play_on(&state), next.next);
2536 assert_eq!(play_on(&state), after.next);
2537 assert_eq!(next.chosen.map(|(id, _)| id), next.next);
2538 }
2539 }
2540
2541 #[test]
2542 fn a_lookahead_steps_deep_crosses_into_the_next_pass_as_the_advance_does() {
2543 let (state, ids) = shuffled(tracks(4));
2544 set_repeat(&state, Repeat::Queue);
2545 state.advance_cursor_loadable();
2546 let mut step = state.lookahead_after(state.cursor().unwrap()).unwrap();
2550 let mut chain = vec![step.next.unwrap()];
2551 for _ in 1..(ids.len() - 1 + ids.len()) {
2552 step = state.lookahead_from(&step).unwrap();
2553 chain.push(step.next.unwrap());
2554 }
2555 let played: Vec<_> = chain.iter().map(|_| play_on(&state).unwrap()).collect();
2556 assert_eq!(played, chain);
2557 let mut next_pass = chain[ids.len() - 1..].to_vec();
2558 next_pass.sort_by_key(|id| ids.iter().position(|i| i == id));
2559 assert_eq!(next_pass, ids, "the next pass whole, not this one again");
2560 }
2561
2562 #[test]
2563 fn a_played_out_shuffled_queue_plays_again_from_a_row_picked() {
2564 let (state, ids) = shuffled(tracks(5));
2565 state.advance_cursor_loadable();
2566 while play_on(&state).is_some() {}
2567
2568 state.pick(ids[2]);
2569 let mut heard = vec![ids[2]];
2570 while let Some(id) = play_on(&state) {
2571 heard.push(id);
2572 }
2573 heard.sort_by_key(|id| ids.iter().position(|i| i == id));
2574 assert_eq!(heard, ids, "a new pass from the row picked");
2575 }
2576
2577 #[test]
2578 fn shuffling_a_played_out_queue_plays_it_again() {
2579 let state = SharedPlayerState::new();
2580 let items = tracks(5);
2581 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
2582 state.add_items(items);
2583 state.set_cursor(Some(ids[0]));
2584 while play_on(&state).is_some() {}
2585
2586 state.set_shuffled(true);
2587 let mut heard = vec![state.cursor().unwrap()];
2588 while let Some(id) = play_on(&state) {
2589 heard.push(id);
2590 }
2591 assert_eq!(heard.len(), ids.len());
2592 }
2593
2594 #[test]
2595 fn the_downloads_follow_the_play_order() {
2596 let state = SharedPlayerState::new();
2597 let items: Vec<_> = tracks(10)
2598 .into_iter()
2599 .map(|item| PlaylistItem {
2600 state: ItemState::Pending,
2601 ..item
2602 })
2603 .collect();
2604 state.add_items(items);
2605 state.set_shuffled(true);
2606 let first = state.advance_cursor_loadable().unwrap();
2607 let second = state.lookahead_after(first).unwrap().next.unwrap();
2608 let order = state.pending_downloads();
2609 assert_eq!(order[0].1, first);
2610 assert_eq!(order[1].1, second);
2611 assert_eq!(order.len(), 10);
2612 }
2613
2614 fn downloading_item(
2617 state: &SharedPlayerState,
2618 title: &str,
2619 total: u64,
2620 ) -> (PlaylistItem, Arc<ByteFeed>) {
2621 static NEXT_TRACK: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(1);
2622 let mut item = make_item(title, ItemState::Pending);
2623 item.db_id = Some(NEXT_TRACK.fetch_add(1, Ordering::Relaxed));
2624 let feed = start_transfer(state, &item, total);
2625 (item, feed)
2626 }
2627
2628 fn start_transfer(state: &SharedPlayerState, item: &PlaylistItem, total: u64) -> Arc<ByteFeed> {
2630 let track_id = item.db_id.expect("a transfer is for a library track");
2631 let store = state.downloads();
2632 store.claim(track_id, Some(item.id));
2633 let feed = store.announce(
2634 track_id,
2635 item.title.clone(),
2636 String::new(),
2637 PathBuf::from(format!("/cache/{}.flac.part", item.title)),
2638 PathBuf::from(format!("/cache/{}.flac", item.title)),
2639 );
2640 store.started(track_id, total);
2641 feed
2642 }
2643
2644 fn ready_item(title: &str) -> PlaylistItem {
2645 make_item(title, ItemState::Ready)
2646 }
2647
2648 fn pending_item(title: &str) -> PlaylistItem {
2649 make_item(title, ItemState::Pending)
2650 }
2651
2652 fn failed_item(title: &str) -> PlaylistItem {
2653 make_item(title, ItemState::Failed("nope".into()))
2654 }
2655
2656 const DURATION_MS: u64 = 32_523_787;
2657
2658 fn streaming_state(
2661 downloaded: u64,
2662 total: u64,
2663 bitrate_kbps: Option<u32>,
2664 ) -> Arc<SharedPlayerState> {
2665 streaming_state_with_duration(downloaded, total, bitrate_kbps, DURATION_MS)
2666 }
2667
2668 fn streaming_state_with_duration(
2671 downloaded: u64,
2672 total: u64,
2673 bitrate_kbps: Option<u32>,
2674 container_duration_ms: u64,
2675 ) -> Arc<SharedPlayerState> {
2676 let mut item = make_item("train", ItemState::Pending);
2677 item.db_id = Some(1);
2678 let id = item.id;
2679 let path = item.path.clone();
2680
2681 let state = SharedPlayerState::new();
2682 start_transfer(&state, &item, total).set(downloaded);
2683 state.add_items(vec![item]);
2684 state.set_cursor(Some(id));
2685 state.set_track_info(Some(TrackInfo {
2686 id,
2687 path,
2688 codec: "Opus".into(),
2689 sample_rate: 48_000,
2690 bit_depth: None,
2691 bitrate_kbps,
2692 channels: 2,
2693 duration_ms: container_duration_ms,
2694 }));
2695 state
2696 }
2697
2698 #[test]
2701 fn a_track_on_disk_is_seekable_end_to_end() {
2702 let item = ready_item("done");
2703 let id = item.id;
2704 let path = item.path.clone();
2705 let state = SharedPlayerState::new();
2706 state.add_items(vec![item]);
2707 state.set_cursor(Some(id));
2708 state.set_track_info(Some(TrackInfo {
2709 id,
2710 path,
2711 codec: "FLAC".into(),
2712 sample_rate: 44_100,
2713 bit_depth: Some(16),
2714 bitrate_kbps: None,
2715 channels: 2,
2716 duration_ms: 200_000,
2717 }));
2718
2719 assert_eq!(state.seekable_ms(), 200_000);
2720 assert_eq!(state.seek_ceiling_ms(), None);
2722 }
2723
2724 #[test]
2725 fn a_downloading_track_is_seekable_as_far_as_its_bytes_reach() {
2726 let state = streaming_state(100, 400, None);
2729 assert_eq!(state.seekable_ms(), 32_523_787 / 4 - SEEK_SAFETY_MS);
2730 assert_eq!(
2731 state.seek_ceiling_ms(),
2732 Some(32_523_787 / 4 - SEEK_SAFETY_MS)
2733 );
2734 }
2735
2736 #[test]
2737 fn a_transfer_without_a_content_length_falls_back_to_bitrate() {
2738 let state = streaming_state(1024 * 1024, 0, Some(128));
2741 assert_eq!(state.seekable_ms(), 1024 * 1024 * 8 / 128 - SEEK_SAFETY_MS);
2742 }
2743
2744 #[test]
2745 fn nothing_to_estimate_from_allows_no_forward_seek() {
2746 let state = streaming_state(1024 * 1024, 0, None);
2749 state.set_position_ms(12_000);
2750 assert_eq!(state.seekable_ms(), 12_000);
2751 }
2752
2753 #[test]
2754 fn the_seekable_extent_never_exceeds_the_track() {
2755 let state = streaming_state(500, 400, None);
2758 assert_eq!(state.seekable_ms(), 32_523_787);
2759 }
2760
2761 #[test]
2762 fn nothing_playing_is_seekable_nowhere() {
2763 assert_eq!(SharedPlayerState::new().seekable_ms(), 0);
2764 assert_eq!(SharedPlayerState::new().seek_ceiling_ms(), None);
2765 }
2766
2767 #[test]
2768 fn a_container_that_cannot_state_its_duration_cannot_be_seeked() {
2769 let state = streaming_state_with_duration(200, 400, Some(128), 0);
2773 assert_eq!(state.seekable_ms(), 0);
2774 }
2775
2776 #[test]
2777 fn the_library_duration_stands_in_for_a_silent_container() {
2778 let state = streaming_state_with_duration(200, 400, Some(128), 0);
2781 assert_eq!(state.duration_ms(), 200_000, "the item's own figure");
2782 assert_eq!(state.seekable_ms(), 0);
2784 assert_eq!(state.seek_ceiling_ms(), Some(0));
2785 }
2786
2787 #[test]
2788 fn the_container_duration_wins_where_there_is_one() {
2789 let state = streaming_state(200, 400, None);
2790 assert_eq!(state.duration_ms(), DURATION_MS);
2791 }
2792
2793 #[test]
2794 fn the_download_landing_restores_seeking() {
2795 let state = streaming_state_with_duration(400, 400, Some(128), 0);
2799 assert_eq!(state.seekable_ms(), 0);
2800
2801 let id = state.cursor().expect("cursor");
2805 let _ =
2806 crate::remote::downloads::settle(&state, 1, &Ok(PathBuf::from("/cache/train.flac")));
2807 assert_eq!(state.item_state(id), Some(ItemState::Ready));
2808 let info = state.track_info().expect("track info");
2809 state.set_track_info(Some(TrackInfo {
2810 duration_ms: DURATION_MS,
2811 ..info
2812 }));
2813
2814 assert_eq!(state.seekable_ms(), DURATION_MS);
2815 assert_eq!(state.seek_ceiling_ms(), None, "no boundary left to draw");
2816 }
2817
2818 #[test]
2821 fn advance_parks_on_a_still_downloading_track() {
2822 let state = SharedPlayerState::new();
2826 let item0 = ready_item("track-0");
2827 let item1 = pending_item("track-1");
2828 let item2 = ready_item("track-2");
2829 let (id0, id1) = (item0.id, item1.id);
2830
2831 state.add_items(vec![item0, item1, item2]);
2832
2833 assert_eq!(state.advance_cursor_loadable(), Some(id0));
2834 assert_eq!(state.advance_cursor_loadable(), Some(id1));
2835 assert_eq!(state.cursor(), Some(id1));
2836 }
2837
2838 #[test]
2839 fn advance_skips_failed_items() {
2840 let state = SharedPlayerState::new();
2841 let item0 = ready_item("track-0");
2842 let item1 = failed_item("track-1");
2843 let item2 = ready_item("track-2");
2844 let (id0, id2) = (item0.id, item2.id);
2845
2846 state.add_items(vec![item0, item1, item2]);
2847 state.set_cursor(Some(id0));
2848
2849 assert_eq!(state.advance_cursor_loadable(), Some(id2));
2850 }
2851
2852 #[test]
2853 fn advance_stops_at_end_of_playlist() {
2854 let state = SharedPlayerState::new();
2855 let item0 = ready_item("track-0");
2856 let item1 = ready_item("track-1");
2857 let id1 = item1.id;
2858
2859 state.add_items(vec![item0, item1]);
2860 state.set_cursor(Some(id1));
2861
2862 assert_eq!(state.advance_cursor_loadable(), None);
2863 assert_eq!(
2864 state.cursor(),
2865 Some(id1),
2866 "cursor unchanged on a failed advance"
2867 );
2868 }
2869
2870 #[test]
2871 fn advance_with_only_failed_items_returns_none() {
2872 let state = SharedPlayerState::new();
2873 state.add_items(vec![failed_item("bad-0"), failed_item("bad-1")]);
2874
2875 assert_eq!(state.advance_cursor_loadable(), None);
2876 }
2877
2878 #[test]
2879 fn advance_from_a_vanished_cursor_does_not_restart_the_queue() {
2880 let state = SharedPlayerState::new();
2881 let item0 = ready_item("track-0");
2882 let item1 = ready_item("track-1");
2883 let id0 = item0.id;
2884
2885 state.add_items(vec![item0, item1]);
2886 let ghost = QueueItemId::new();
2887 state.set_cursor(Some(ghost));
2888
2889 assert_eq!(state.advance_cursor_loadable(), None);
2890 assert_ne!(state.cursor(), Some(id0));
2891 }
2892
2893 fn chosen_after(state: &SharedPlayerState, id: QueueItemId) -> Option<QueueItemId> {
2896 state
2897 .lookahead_after(id)
2898 .and_then(|step| step.chosen)
2899 .map(|(id, _)| id)
2900 }
2901
2902 #[test]
2903 fn a_step_stops_at_a_track_still_arriving() {
2904 let state = SharedPlayerState::new();
2905 let a = ready_item("a");
2906 let b = PlaylistItem {
2907 state: ItemState::Pending,
2908 ..ready_item("b")
2909 };
2910 let c = ready_item("c");
2911 let (ida, idb) = (a.id, b.id);
2912 state.add_items(vec![a, b, c]);
2913
2914 let step = state.lookahead_after(ida).unwrap();
2915 assert_eq!(step.next, Some(idb));
2916 assert!(step.chosen.is_none(), "c is not queued over it");
2917
2918 state.update_item_state(idb, ItemState::Ready);
2919 assert!(
2920 state.still_follows(&step),
2921 "its landing is not an edit: the advance plays it"
2922 );
2923
2924 state.add_items(vec![ready_item("d")]);
2925 assert!(state.still_follows(&step), "nor is adding after it");
2926
2927 state.insert_items_after(vec![ready_item("next")], ida);
2928 assert!(!state.still_follows(&step), "a track put before it is");
2929 }
2930
2931 #[test]
2932 fn a_step_passes_over_failed_tracks() {
2933 let state = SharedPlayerState::new();
2934 let a = ready_item("a");
2935 let failed = PlaylistItem {
2936 state: ItemState::Failed("gone".into()),
2937 ..ready_item("failed")
2938 };
2939 let c = ready_item("c");
2940 let (ida, idc) = (a.id, c.id);
2941 state.add_items(vec![a, failed, c]);
2942
2943 let step = state.lookahead_after(ida).unwrap();
2944 assert_eq!(step.chosen.as_ref().map(|(id, _)| *id), Some(idc));
2945
2946 let another = PlaylistItem {
2947 state: ItemState::Failed("gone".into()),
2948 ..ready_item("another")
2949 };
2950 state.insert_items_after(vec![another], ida);
2951 assert!(
2952 state.still_follows(&step),
2953 "another failed track before it changes nothing"
2954 );
2955
2956 let arriving = PlaylistItem {
2957 state: ItemState::Pending,
2958 ..ready_item("arriving")
2959 };
2960 state.insert_items_after(vec![arriving], ida);
2961 assert!(!state.still_follows(&step), "a track still arriving does");
2962 }
2963
2964 #[test]
2965 fn a_step_breaks_when_what_it_chose_moves_ahead_of_it() {
2966 let state = SharedPlayerState::new();
2967 let (a, c) = (ready_item("a"), ready_item("c"));
2968 let (ida, idc) = (a.id, c.id);
2969 state.add_items(vec![a, c]);
2970 let step = state.lookahead_after(ida).unwrap();
2971 state.move_item(idc, ida, false);
2972 assert!(!state.still_follows(&step));
2973 }
2974
2975 #[test]
2976 fn a_step_to_the_end_of_the_queue_breaks_when_a_track_is_added() {
2977 let state = SharedPlayerState::new();
2978 let a = ready_item("a");
2979 let ida = a.id;
2980 state.add_items(vec![a]);
2981 let step = state.lookahead_after(ida).unwrap();
2982 assert!(step.chosen.is_none());
2983 assert!(state.still_follows(&step));
2984 state.add_items(vec![ready_item("b")]);
2985 assert!(!state.still_follows(&step));
2986 }
2987
2988 #[test]
2989 fn peek_after_a_removed_item_returns_none() {
2990 let state = SharedPlayerState::new();
2994 let item0 = ready_item("track-0");
2995 let item1 = ready_item("track-1");
2996 let item2 = ready_item("track-2");
2997 let (id0, id1, id2) = (item0.id, item1.id, item2.id);
2998
2999 state.add_items(vec![item0, item1, item2]);
3000 assert_eq!(chosen_after(&state, id1), Some(id2));
3001
3002 state.remove_item(id2);
3003 assert!(
3004 state.lookahead_after(id2).is_none(),
3005 "a vanished reference must not resolve to the head of the queue"
3006 );
3007 assert_ne!(chosen_after(&state, id2), Some(id0));
3008 }
3009
3010 #[test]
3011 fn the_lookahead_steps_over_a_track_that_failed() {
3012 let state = SharedPlayerState::new();
3013 let playing = ready_item("playing");
3014 let failed = failed_item("failed");
3015 let after = ready_item("after");
3016 let (playing_id, after_id) = (playing.id, after.id);
3017 state.add_items(vec![playing, failed, after]);
3018
3019 assert_eq!(chosen_after(&state, playing_id), Some(after_id));
3020 }
3021
3022 fn repeating(state: &SharedPlayerState, repeat: Repeat) {
3025 state.set_play_mode(PlayMode {
3026 shuffle: false,
3027 repeat,
3028 });
3029 }
3030
3031 #[test]
3032 fn a_step_from_the_last_item_wraps_only_when_the_queue_repeats() {
3033 let state = SharedPlayerState::new();
3034 let items: Vec<_> = ["a", "b"].map(ready_item).into();
3035 let (a, b) = (items[0].id, items[1].id);
3036 state.add_items(items);
3037
3038 assert_eq!(state.lookahead_after(b).unwrap().next, None);
3039 repeating(&state, Repeat::Queue);
3040 let step = state.lookahead_after(b).unwrap();
3041 assert_eq!((step.next, step.wrapped), (Some(a), true));
3042 assert!(state.still_follows(&step));
3043
3044 let later = ready_item("later");
3045 state.add_items(vec![later.clone()]);
3046 assert!(!state.still_follows(&step), "something follows b now");
3047 state.remove_items(&[later.id]);
3048 repeating(&state, Repeat::Off);
3049 assert!(!state.still_follows(&step), "nor does repeat off wrap");
3050 }
3051
3052 #[test]
3053 fn repeating_one_steps_to_the_same_item_but_an_advance_moves_on() {
3054 let state = SharedPlayerState::new();
3055 let items: Vec<_> = ["a", "b"].map(ready_item).into();
3056 let (a, b) = (items[0].id, items[1].id);
3057 state.add_items(items);
3058 repeating(&state, Repeat::One);
3059
3060 let step = state.lookahead_after(a).unwrap();
3061 assert_eq!((step.next, step.wrapped), (Some(a), false));
3062 state.set_cursor(Some(a));
3063 assert_eq!(state.advance_cursor_loadable(), Some(b));
3064 assert_eq!(
3065 state.advance_cursor_loadable(),
3066 Some(a),
3067 "round, as repeating"
3068 );
3069 }
3070
3071 #[test]
3072 fn previous_from_the_first_item_wraps_only_while_repeating() {
3073 let state = SharedPlayerState::new();
3074 let items: Vec<_> = ["a", "b", "c"].map(ready_item).into();
3075 let (a, c) = (items[0].id, items[2].id);
3076 state.add_items(items);
3077
3078 state.set_cursor(Some(a));
3079 assert!(state.retreat_cursor().is_none());
3080 for repeat in [Repeat::Queue, Repeat::One] {
3081 repeating(&state, repeat);
3082 state.set_cursor(Some(a));
3083 assert_eq!(
3084 state.retreat_cursor().map(|(id, _)| id),
3085 Some(c),
3086 "{repeat:?}"
3087 );
3088 }
3089 }
3090
3091 #[test]
3092 fn a_removed_item_has_nothing_after_it_even_when_the_queue_repeats() {
3093 let state = SharedPlayerState::new();
3094 let items: Vec<_> = ["a", "b"].map(ready_item).into();
3095 let b = items[1].id;
3096 state.add_items(items);
3097 repeating(&state, Repeat::Queue);
3098
3099 state.set_cursor(Some(b));
3100 state.remove_item(b);
3101 assert!(state.lookahead_after(b).is_none());
3102 state.set_cursor(Some(b));
3103 assert_eq!(state.advance_cursor_loadable(), None);
3104 }
3105
3106 #[test]
3107 fn a_wrap_passes_over_failed_items() {
3108 let state = SharedPlayerState::new();
3109 let items = vec![failed_item("a"), ready_item("b"), ready_item("c")];
3110 let (b, c) = (items[1].id, items[2].id);
3111 state.add_items(items);
3112 repeating(&state, Repeat::Queue);
3113
3114 assert_eq!(state.lookahead_after(c).unwrap().next, Some(b));
3115 }
3116
3117 #[test]
3118 fn surviving_predecessor_skips_items_being_removed() {
3119 let state = SharedPlayerState::new();
3120 let items: Vec<_> = (0..4).map(|i| ready_item(&format!("track-{i}"))).collect();
3121 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
3122 state.add_items(items);
3123
3124 assert_eq!(
3126 state.surviving_item_before(ids[3], &ids[1..4]),
3127 Some(ids[0])
3128 );
3129 assert_eq!(state.surviving_item_before(ids[2], &ids), None);
3131 }
3132
3133 #[test]
3136 fn test_retreat_cursor_goes_to_previous_item() {
3137 let state = SharedPlayerState::new();
3138 let item0 = ready_item("track-0");
3139 let item1 = ready_item("track-1");
3140 let id0 = item0.id;
3141 let id1 = item1.id;
3142
3143 state.add_items(vec![item0, item1]);
3144 state.set_cursor(Some(id1));
3145
3146 let result = state.retreat_cursor();
3147 assert!(result.is_some(), "expected to retreat to previous item");
3148 assert_eq!(result.unwrap().0, id0, "should retreat to first item");
3149 assert_eq!(state.cursor(), Some(id0));
3150 }
3151
3152 #[test]
3153 fn test_retreat_cursor_returns_none_when_at_first_item() {
3154 let state = SharedPlayerState::new();
3155 let item0 = ready_item("only-track");
3156 let id0 = item0.id;
3157
3158 state.add_items(vec![item0]);
3159 state.set_cursor(Some(id0));
3160
3161 let result = state.retreat_cursor();
3162 assert!(result.is_none(), "cannot retreat before the first item");
3163 assert_eq!(state.cursor(), Some(id0));
3165 }
3166
3167 #[test]
3168 fn test_retreat_cursor_returns_none_when_cursor_is_unset() {
3169 let state = SharedPlayerState::new();
3170 state.add_items(vec![ready_item("track-0")]);
3171
3172 let result = state.retreat_cursor();
3173 assert!(
3174 result.is_none(),
3175 "retreat with no cursor should return None"
3176 );
3177 }
3178
3179 #[test]
3182 fn test_derive_visible_queue_statuses() {
3183 let state = SharedPlayerState::new();
3185 let item0 = ready_item("played-track");
3186 let item1 = ready_item("playing-track");
3187 let item2 = ready_item("queued-track");
3188 let (id0, id1) = (item0.id, item1.id);
3189
3190 state.add_items(vec![item0, item1, item2]);
3191 state.set_cursor(Some(id0));
3192 state.mark_played(id0);
3193 state.set_cursor(Some(id1));
3194
3195 let snap = state.derive_visible_queue();
3196
3197 assert_eq!(snap.entries.len(), 3);
3198 assert_eq!(snap.entries[0].status, QueueEntryStatus::Played);
3199 assert_eq!(snap.entries[1].status, QueueEntryStatus::Playing);
3200 assert_eq!(snap.entries[2].status, QueueEntryStatus::Queued);
3201 assert!(snap.has_playing);
3202 assert_eq!(snap.finished_count, 1);
3203 assert_eq!(snap.queue_count, 1);
3204 }
3205
3206 #[test]
3207 fn test_derive_visible_queue_downloading_statuses() {
3208 let state = SharedPlayerState::new();
3211 let (dl_cursor, _) = downloading_item(&state, "downloading-at-cursor", 1_000_000);
3212 let (dl_queued, _) = downloading_item(&state, "downloading-queued", 500_000);
3213 let id_cursor = dl_cursor.id;
3214
3215 state.add_items(vec![dl_cursor, dl_queued]);
3216 state.set_cursor(Some(id_cursor));
3217
3218 let snap = state.derive_visible_queue();
3219
3220 assert_eq!(snap.entries[0].status, QueueEntryStatus::Downloading);
3221 assert_eq!(snap.entries[1].status, QueueEntryStatus::Downloading);
3222 }
3223
3224 #[test]
3225 fn a_cursor_waiting_its_turn_is_priority_pending() {
3226 let state = SharedPlayerState::new();
3227 let item = pending_item("waiting");
3228 let id = item.id;
3229 state.add_items(vec![item]);
3230 state.set_cursor(Some(id));
3231
3232 let snap = state.derive_visible_queue();
3233 assert_eq!(snap.entries[0].status, QueueEntryStatus::PriorityPending);
3234 }
3235
3236 #[test]
3237 fn a_second_entry_for_a_track_reads_the_transfer_running_for_it() {
3238 let state = SharedPlayerState::new();
3241 let (first, bytes) = downloading_item(&state, "twice", 1_000_000);
3242 bytes.set(STREAM_THRESHOLD);
3243 let mut again = pending_item("twice");
3244 again.db_id = first.db_id;
3245 let again_id = again.id;
3246 state.add_items(vec![first, again]);
3247
3248 assert!(matches!(
3249 state.item_load_state(again_id),
3250 Some(LoadState::Downloading { .. })
3251 ));
3252 assert!(matches!(
3253 state.item_playback_source(again_id),
3254 Some(PlaybackSource::Streaming { .. })
3255 ));
3256 }
3257
3258 #[test]
3261 fn only_a_change_to_what_is_saved_moves_the_content_version() {
3262 let state = SharedPlayerState::new();
3263 let a = make_item("a", ItemState::Pending);
3264 let b = make_item("b", ItemState::Ready);
3265 let (a_id, b_id) = (a.id, b.id);
3266
3267 let start = state.content_version();
3268 state.add_items(vec![a, b]);
3269 let added = state.content_version();
3270 assert_ne!(added, start);
3271
3272 state.set_cursor(Some(a_id));
3273 state.update_item_state(a_id, ItemState::Ready);
3274 state.advance_cursor_loadable();
3275 state.retreat_cursor();
3276 assert_eq!(state.content_version(), added);
3277 assert_eq!(state.cursor_path(), Some(PathBuf::from("/music/a.flac")));
3278
3279 state.move_item_to(b_id, None);
3280 assert_ne!(state.content_version(), added);
3281 }
3282
3283 #[test]
3284 fn progress_follows_the_counter_without_touching_the_playlist() {
3285 let state = SharedPlayerState::new();
3289 let (item, bytes) = downloading_item(&state, "downloading", 1_000);
3290 state.add_items(vec![item]);
3291
3292 let version = state.playlist_version();
3293 bytes.set(250);
3294
3295 let snap = state.derive_visible_queue();
3296 assert_eq!(snap.entries[0].download_progress, Some((250, 1_000)));
3297 assert_eq!(
3298 state.playlist_version(),
3299 version,
3300 "progress must not read as a queue mutation"
3301 );
3302 assert_eq!(state.downloads().readings()[0].written, 250);
3303 }
3304
3305 #[test]
3306 fn test_derive_visible_queue_no_cursor_all_queued() {
3307 let state = SharedPlayerState::new();
3308 state.add_items(vec![ready_item("a"), ready_item("b"), ready_item("c")]);
3309
3310 let snap = state.derive_visible_queue();
3311
3312 assert_eq!(snap.entries.len(), 3);
3313 for entry in &snap.entries {
3314 assert_eq!(entry.status, QueueEntryStatus::Queued);
3315 }
3316 assert!(!snap.has_playing);
3317 assert_eq!(snap.finished_count, 0);
3318 assert_eq!(snap.queue_count, 3);
3319 }
3320
3321 fn make_album_item(title: &str, album: &str, album_artist: &str) -> PlaylistItem {
3324 PlaylistItem {
3325 playlist_entry_id: None,
3326 id: QueueItemId::new(),
3327 db_id: None,
3328 path: PathBuf::from(format!("/music/{title}.flac")),
3329 title: title.to_string(),
3330 artist: "Artist".to_string(),
3331 album_artist: album_artist.to_string(),
3332 album: album.to_string(),
3333 year: None,
3334 codec: Some("FLAC".to_string()),
3335 track_number: None,
3336 disc: None,
3337 duration_ms: Some(200_000),
3338 state: ItemState::Ready,
3339 played: false,
3340 }
3341 }
3342
3343 #[test]
3344 fn test_same_album_item_ids_returns_album_mates() {
3345 let state = SharedPlayerState::new();
3346 let a1 = make_album_item("A1", "Album A", "Artist A");
3347 let a2 = make_album_item("A2", "Album A", "Artist A");
3348 let b1 = make_album_item("B1", "Album B", "Artist B");
3349 let a3 = make_album_item("A3", "Album A", "Artist A");
3350
3351 let id_a1 = a1.id;
3352 let id_a2 = a2.id;
3353 let id_a3 = a3.id;
3354
3355 state.add_items(vec![a1, a2, b1, a3]);
3356
3357 let mates = state.same_album_item_ids(id_a1);
3358 assert_eq!(mates.len(), 2);
3359 assert!(mates.contains(&id_a2));
3360 assert!(mates.contains(&id_a3));
3361 }
3362
3363 #[test]
3364 fn test_same_album_item_ids_distinguishes_album_artists() {
3365 let state = SharedPlayerState::new();
3367 let a1 = make_album_item("A1", "Greatest Hits", "Artist A");
3368 let b1 = make_album_item("B1", "Greatest Hits", "Artist B");
3369
3370 let id_a1 = a1.id;
3371
3372 state.add_items(vec![a1, b1]);
3373
3374 let mates = state.same_album_item_ids(id_a1);
3375 assert!(mates.is_empty(), "different album_artist should not match");
3376 }
3377
3378 #[test]
3379 fn test_same_album_item_ids_unknown_id_returns_empty() {
3380 let state = SharedPlayerState::new();
3381 state.add_items(vec![ready_item("track-0")]);
3382
3383 let bogus = QueueItemId::new();
3384 let mates = state.same_album_item_ids(bogus);
3385 assert!(mates.is_empty());
3386 }
3387
3388 #[test]
3391 fn test_update_item_metadata_leaves_library_tags_alone() {
3392 let state = SharedPlayerState::new();
3393 let mut item = make_album_item("A1", "Nite Versions (mixed)", "Soulwax");
3394 item.db_id = Some(29615);
3395 let id = item.id;
3396 state.add_items(vec![item]);
3397
3398 state.update_item_metadata(
3399 id,
3400 "[unknown]".into(),
3401 "Soulwax".into(),
3402 "Soulwax".into(),
3403 "Nite Versions".into(),
3404 Some(54_000),
3405 );
3406
3407 let pl = state.playlist.read();
3408 assert_eq!(pl.items[0].album, "Nite Versions (mixed)");
3409 assert_eq!(pl.items[0].title, "A1");
3410 assert_eq!(pl.items[0].duration_ms, Some(54_000));
3411 }
3412
3413 #[test]
3414 fn test_update_item_metadata_fills_in_an_item_with_nothing_behind_it() {
3415 let state = SharedPlayerState::new();
3416 let item = make_album_item("A1", "", "");
3417 let id = item.id;
3418 state.add_items(vec![item]);
3419
3420 state.update_item_metadata(
3421 id,
3422 "Teachers".into(),
3423 "Soulwax".into(),
3424 "Soulwax".into(),
3425 "Nite Versions".into(),
3426 Some(148_000),
3427 );
3428
3429 let pl = state.playlist.read();
3430 assert_eq!(pl.items[0].title, "Teachers");
3431 assert_eq!(pl.items[0].album, "Nite Versions");
3432 assert_eq!(pl.items[0].duration_ms, Some(148_000));
3433 }
3434
3435 #[test]
3441 fn test_update_item_metadata_is_an_edit_only_for_new_tags() {
3442 let state = SharedPlayerState::new();
3443 let mut item = make_album_item("A1", "Nite Versions", "Soulwax");
3444 item.db_id = Some(1);
3445 let id = item.id;
3446 state.add_items(vec![item]);
3447 let content = state.content_version();
3448 let version = state.playlist_version();
3449
3450 let land = |duration_ms| {
3451 state.update_item_metadata(
3452 id,
3453 "A1".into(),
3454 "Soulwax".into(),
3455 "Soulwax".into(),
3456 "Nite Versions".into(),
3457 Some(duration_ms),
3458 )
3459 };
3460 land(200_000);
3461 assert_eq!(state.content_version(), content);
3462 assert_eq!(state.playlist_version(), version);
3463
3464 land(200_417);
3465 assert_eq!(state.content_version(), content);
3466 assert_ne!(state.playlist_version(), version);
3467 assert_eq!(state.queue_readings()[0].duration_ms, Some(200_417));
3468
3469 let mut untagged = make_album_item("", "", "");
3470 let untagged_id = untagged.id;
3471 untagged.db_id = None;
3472 state.add_items(vec![untagged]);
3473 let content = state.content_version();
3474 state.update_item_metadata(
3475 untagged_id,
3476 "Teachers".into(),
3477 "Soulwax".into(),
3478 "Soulwax".into(),
3479 "Nite Versions".into(),
3480 None,
3481 );
3482 assert_ne!(state.content_version(), content);
3483 }
3484
3485 #[test]
3488 fn test_move_item_to_reorders_playlist() {
3489 let state = SharedPlayerState::new();
3491 let item_a = ready_item("A");
3492 let item_b = ready_item("B");
3493 let item_c = ready_item("C");
3494 let id_a = item_a.id;
3495 let id_b = item_b.id;
3496 let id_c = item_c.id;
3497
3498 state.add_items(vec![item_a, item_b, item_c]);
3499 state.move_item_to(id_c, Some(id_a));
3500
3501 let (items, _) = state.snapshot_playlist();
3502 let titles: Vec<&str> = items.iter().map(|i| i.title.as_str()).collect();
3503 assert_eq!(titles, vec!["A", "C", "B"]);
3504 assert_eq!(items[0].id, id_a);
3505 assert_eq!(items[1].id, id_c);
3506 assert_eq!(items[2].id, id_b);
3507 }
3508
3509 #[test]
3510 fn test_move_item_to_front_when_after_is_none() {
3511 let state = SharedPlayerState::new();
3513 let item_a = ready_item("A");
3514 let item_b = ready_item("B");
3515 let item_c = ready_item("C");
3516 let id_c = item_c.id;
3517
3518 state.add_items(vec![item_a, item_b, item_c]);
3519 state.move_item_to(id_c, None);
3520
3521 let (items, _) = state.snapshot_playlist();
3522 let titles: Vec<&str> = items.iter().map(|i| i.title.as_str()).collect();
3523 assert_eq!(titles, vec!["C", "A", "B"]);
3524 }
3525
3526 #[test]
3529 fn test_move_items_batch_preserves_relative_order() {
3530 let state = SharedPlayerState::new();
3532 let item_a = ready_item("A");
3533 let item_b = ready_item("B");
3534 let item_c = ready_item("C");
3535 let item_d = ready_item("D");
3536 let id_a = item_a.id;
3537 let id_b = item_b.id;
3538 let id_c = item_c.id;
3539 let id_d = item_d.id;
3540
3541 state.add_items(vec![item_a, item_b, item_c, item_d]);
3542 state.move_items(&[id_a, id_c], id_d, true);
3543
3544 let (items, _) = state.snapshot_playlist();
3545 let titles: Vec<&str> = items.iter().map(|i| i.title.as_str()).collect();
3546 assert_eq!(titles, vec!["B", "D", "A", "C"]);
3547 assert_eq!(items[0].id, id_b);
3548 assert_eq!(items[1].id, id_d);
3549 assert_eq!(items[2].id, id_a);
3550 assert_eq!(items[3].id, id_c);
3551 }
3552
3553 #[test]
3556 fn test_pending_downloads_collects_pending_with_db_id() {
3557 let state = SharedPlayerState::new();
3558 let mut item_a = ready_item("local");
3559 item_a.db_id = None;
3560
3561 let mut item_b = pending_item("remote-1");
3562 item_b.db_id = Some(10);
3563 let id_b = item_b.id;
3564
3565 let mut item_c = ready_item("cached");
3566 item_c.db_id = Some(20);
3567
3568 let mut item_d = pending_item("remote-2");
3569 item_d.db_id = Some(30);
3570 let id_d = item_d.id;
3571
3572 let item_e = pending_item("orphan");
3574
3575 state.add_items(vec![item_a, item_b, item_c, item_d, item_e]);
3576
3577 let pending = state.pending_downloads();
3578 assert_eq!(pending.len(), 2);
3579 assert_eq!(pending[0], (10, id_b));
3580 assert_eq!(pending[1], (30, id_d));
3581
3582 state.set_cursor(Some(id_d));
3585 assert_eq!(state.pending_downloads(), vec![(30, id_d), (10, id_b)]);
3586 }
3587
3588 #[test]
3589 fn test_item_db_id_and_load_state() {
3590 let state = SharedPlayerState::new();
3591 let mut item = pending_item("track");
3592 item.db_id = Some(42);
3593 let id = item.id;
3594 state.add_items(vec![item]);
3595
3596 assert_eq!(state.item_db_id(id), Some(42));
3597 assert!(matches!(
3598 state.item_load_state(id),
3599 Some(LoadState::Pending)
3600 ));
3601
3602 state.update_item_state(id, ItemState::Ready);
3603 assert!(matches!(state.item_load_state(id), Some(LoadState::Ready)));
3604 }
3605}