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}
54
55#[derive(
57 Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
58)]
59#[serde(rename_all = "lowercase")]
60pub enum Repeat {
61 #[default]
63 Off,
64 Queue,
66 One,
68}
69
70impl Repeat {
71 pub fn is_off(&self) -> bool {
72 *self == Repeat::Off
73 }
74
75 pub fn as_str(self) -> &'static str {
76 match self {
77 Repeat::Off => "off",
78 Repeat::Queue => "queue",
79 Repeat::One => "one",
80 }
81 }
82
83 pub fn parse(s: &str) -> Option<Self> {
84 match s {
85 "off" => Some(Repeat::Off),
86 "queue" => Some(Repeat::Queue),
87 "one" => Some(Repeat::One),
88 _ => None,
89 }
90 }
91
92 pub fn cycled(self) -> Self {
94 match self {
95 Repeat::Off => Repeat::Queue,
96 Repeat::Queue => Repeat::One,
97 Repeat::One => Repeat::Off,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
105pub struct PlayMode {
106 pub shuffle: bool,
107 pub repeat: Repeat,
108}
109
110impl PlayMode {
111 fn to_bits(self) -> u8 {
112 let repeat = match self.repeat {
113 Repeat::Off => 0,
114 Repeat::Queue => 1,
115 Repeat::One => 2,
116 };
117 repeat << 1 | self.shuffle as u8
118 }
119
120 fn from_bits(bits: u8) -> Self {
121 Self {
122 shuffle: bits & 1 != 0,
123 repeat: match bits >> 1 {
124 1 => Repeat::Queue,
125 2 => Repeat::One,
126 _ => Repeat::Off,
127 },
128 }
129 }
130}
131
132fn follows(
141 items: &[PlaylistItem],
142 after: QueueItemId,
143 repeat: Repeat,
144) -> Option<(Option<&PlaylistItem>, bool)> {
145 let at = items.iter().position(|item| item.id == after)?;
146 let playable = |item: &&PlaylistItem| !matches!(item.state, ItemState::Failed(_));
147 if repeat == Repeat::One && playable(&&items[at]) {
148 return Some((Some(&items[at]), false));
149 }
150 if let Some(next) = items[at + 1..].iter().find(playable) {
151 return Some((Some(next), false));
152 }
153 if repeat == Repeat::Off {
154 return Some((None, false));
155 }
156 Some((items[..=at].iter().find(playable), true))
157}
158
159const WAITING: u8 = 0x80;
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165#[repr(u8)]
166pub enum PlaybackState {
167 Stopped = 0,
168 Playing = 1,
169 Paused = 2,
170}
171
172impl PlaybackState {
173 pub fn from_u8(v: u8) -> Self {
174 match v {
175 1 => Self::Playing,
176 2 => Self::Paused,
177 _ => Self::Stopped,
178 }
179 }
180}
181
182#[derive(Debug, Clone, PartialEq)]
184pub struct TrackInfo {
185 pub id: QueueItemId,
186 pub path: PathBuf,
187 pub codec: String,
188 pub sample_rate: u32,
189 pub bit_depth: Option<u16>,
190 pub bitrate_kbps: Option<u32>,
191 pub channels: u16,
192 pub duration_ms: u64,
193}
194
195pub const STREAM_THRESHOLD: u64 = 256 * 1024;
199
200pub const SEEK_SAFETY_MS: u64 = 2_000;
206
207#[derive(Debug, Clone, Default, PartialEq, Eq)]
218pub enum ItemState {
219 #[default]
221 Pending,
222 Ready,
224 Failed(String),
228}
229
230#[derive(Debug, Clone)]
236pub enum LoadState {
237 Pending,
238 Downloading {
239 path: PathBuf,
242 total: u64,
244 bytes_written: Arc<ByteFeed>,
247 },
248 Ready,
249 Failed(String),
250}
251
252impl LoadState {
253 pub fn of(item: &PlaylistItem, downloads: &DownloadStore) -> Self {
261 match &item.state {
262 ItemState::Ready => Self::Ready,
263 ItemState::Failed(reason) => Self::Failed(reason.clone()),
264 ItemState::Pending => match item.db_id.and_then(|id| downloads.live(id)) {
265 Some(live) => Self::Downloading {
266 path: live.source,
267 total: live.total,
268 bytes_written: live.written,
269 },
270 None => Self::Pending,
271 },
272 }
273 }
274}
275
276pub enum PlaybackSource {
278 Ready(PathBuf),
280 Streaming {
282 path: PathBuf,
283 bytes_written: Arc<crate::remote::downloads::ByteFeed>,
284 total: u64,
285 },
286}
287
288#[derive(Debug, Clone)]
290pub struct PlaylistItem {
291 pub id: QueueItemId,
292 pub db_id: Option<i64>,
294 pub playlist_entry_id: Option<i64>,
301 pub path: PathBuf,
302 pub title: String,
303 pub artist: String,
304 pub album_artist: String,
305 pub album: String,
306 pub year: Option<String>,
307 pub codec: Option<String>,
308 pub track_number: Option<i64>,
309 pub disc: Option<i64>,
310 pub duration_ms: Option<u64>,
311 pub state: ItemState,
314 pub pre_shuffle: Option<u32>,
318}
319
320#[derive(Debug, Clone, Default)]
322pub struct Playlist {
323 pub items: Vec<PlaylistItem>,
324 pub cursor: Option<QueueItemId>,
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub enum QueueEntryStatus {
332 Queued,
333 Playing,
334 Played,
335 Downloading,
336 PriorityPending,
338 Failed,
339}
340
341impl QueueEntryStatus {
342 pub fn at_cursor(state: &ItemState, transferring: bool) -> Self {
348 match state {
349 ItemState::Ready => Self::Playing,
350 ItemState::Failed(_) => Self::Failed,
351 ItemState::Pending if transferring => Self::Downloading,
352 ItemState::Pending => Self::PriorityPending,
353 }
354 }
355}
356
357#[derive(Debug, Clone)]
359pub struct QueueEntry {
360 pub id: QueueItemId,
361 pub db_id: Option<i64>,
363 pub playlist_entry_id: Option<i64>,
365 pub path: PathBuf,
366 pub title: String,
367 pub artist: String,
368 pub album_artist: String,
369 pub album: String,
370 pub year: Option<String>,
371 pub codec: Option<String>,
372 pub track_number: Option<i64>,
373 pub disc: Option<i64>,
374 pub duration_ms: Option<u64>,
375 pub status: QueueEntryStatus,
376 pub download_progress: Option<(u64, u64)>,
377 pub error: Option<String>,
379}
380
381#[derive(Debug, Clone, Default)]
383pub struct VisibleQueueSnapshot {
384 pub entries: Vec<QueueEntry>,
385 pub finished_count: usize,
386 pub has_playing: bool,
387 pub queue_count: usize,
388}
389
390#[derive(Debug)]
394pub struct SharedPlayerState {
395 state: AtomicU8,
399 position_ms: AtomicU64,
402 timeline: std::sync::OnceLock<Arc<crate::audio::buffer::PlaybackTimeline>>,
403 track_info: parking_lot::RwLock<Option<TrackInfo>>,
404
405 playlist: parking_lot::RwLock<Playlist>,
407
408 playlist_version: AtomicU64,
410
411 content_version: AtomicU64,
415
416 pending_version: AtomicU64,
421
422 downloads: Arc<DownloadStore>,
424
425 quit_requested: AtomicBool,
427
428 metadata_refresh_pending: AtomicBool,
431
432 output_sample_rate: AtomicU64,
439
440 dsp: parking_lot::RwLock<Option<crate::audio::dsp::DspStatus>>,
443 renderer_clock: parking_lot::Mutex<Option<RendererClock>>,
447
448 renderer: parking_lot::RwLock<Option<crate::upnp::Output>>,
450
451 play_mode: AtomicU8,
455}
456
457#[derive(Debug, Clone, Copy, PartialEq)]
460pub struct RendererClock {
461 pub position_ms: u64,
462 pub running: Option<std::time::Instant>,
463}
464
465impl RendererClock {
466 pub fn now_ms(&self) -> u64 {
467 self.position_ms + self.running.map_or(0, |at| at.elapsed().as_millis() as u64)
468 }
469}
470
471impl SharedPlayerState {
472 pub fn new() -> Arc<Self> {
473 Arc::new(Self {
474 state: AtomicU8::new(PlaybackState::Stopped as u8),
475 position_ms: AtomicU64::new(0),
476 timeline: std::sync::OnceLock::new(),
477 track_info: parking_lot::RwLock::new(None),
478 playlist: parking_lot::RwLock::new(Playlist::default()),
479 playlist_version: AtomicU64::new(0),
480 content_version: AtomicU64::new(0),
481 pending_version: AtomicU64::new(0),
482 downloads: DownloadStore::new(),
483 quit_requested: AtomicBool::new(false),
484 metadata_refresh_pending: AtomicBool::new(false),
485 output_sample_rate: AtomicU64::new(0),
486 dsp: parking_lot::RwLock::new(None),
487 renderer_clock: parking_lot::Mutex::new(None),
488 renderer: parking_lot::RwLock::new(None),
489 play_mode: AtomicU8::new(0),
490 })
491 }
492
493 fn transport(&self) -> (PlaybackState, bool) {
496 let bits = self.state.load(Ordering::Acquire);
497 (PlaybackState::from_u8(bits & !WAITING), bits & WAITING != 0)
498 }
499
500 pub fn playback_state(&self) -> PlaybackState {
501 self.transport().0
502 }
503
504 pub fn set_playback_state(&self, state: PlaybackState) {
505 self.set_transport(state, false);
506 }
507
508 pub fn is_waiting(&self) -> bool {
511 self.transport().1
512 }
513
514 pub fn wants_to_play(&self) -> bool {
517 matches!(
518 self.transport(),
519 (PlaybackState::Playing, _) | (PlaybackState::Stopped, true)
520 )
521 }
522
523 pub fn is_idle(&self) -> bool {
525 self.transport() == (PlaybackState::Stopped, false)
526 }
527
528 pub fn set_transport(&self, state: PlaybackState, waiting: bool) {
530 let bits = state as u8 | if waiting { WAITING } else { 0 };
531 if self.state.swap(bits, Ordering::AcqRel) != bits {
532 self.changed();
533 }
534 }
535
536 pub fn position_ms(&self) -> u64 {
539 let clock = *self.renderer_clock.lock();
540 if let Some(clock) = clock {
541 let at = clock.now_ms();
542 let duration = self.duration_ms();
543 return if duration > 0 { at.min(duration) } else { at };
544 }
545 if self.playback_state() != PlaybackState::Stopped
546 && let Some(playhead) = self.timeline.get().and_then(|t| t.playhead())
547 {
548 return playhead.position_ms;
549 }
550 self.position_ms.load(Ordering::Acquire)
551 }
552
553 pub(crate) fn attach_timeline(&self, timeline: Arc<crate::audio::buffer::PlaybackTimeline>) {
555 let _ = self.timeline.set(timeline);
556 }
557
558 pub fn set_position_ms(&self, pos: u64) {
559 self.position_ms.store(pos, Ordering::Release);
560 }
566
567 pub fn playhead_moving(&self) -> bool {
577 let clock =
578 (*self.renderer_clock.lock()).or_else(|| self.timeline.get().and_then(|t| t.clock()));
579 match clock {
580 Some(clock) => clock.running.is_some(),
581 None => self.playback_state() == PlaybackState::Playing,
582 }
583 }
584
585 pub(crate) fn renderer_clock(&self) -> Option<RendererClock> {
586 *self.renderer_clock.lock()
587 }
588
589 pub(crate) fn set_renderer_clock(&self, clock: Option<RendererClock>) {
590 let mut guard = self.renderer_clock.lock();
591 if *guard != clock {
592 *guard = clock;
593 drop(guard);
594 self.changed();
595 }
596 }
597
598 pub fn renderer(&self) -> Option<crate::upnp::Output> {
600 self.renderer.read().clone()
601 }
602
603 pub(crate) fn set_renderer(&self, output: Option<crate::upnp::Output>) {
604 *self.renderer.write() = output;
605 self.changed();
606 }
607
608 pub(crate) fn update_renderer(&self, f: impl FnOnce(&mut crate::upnp::Output)) {
609 let changed = match self.renderer.write().as_mut() {
610 Some(out) => {
611 let before = out.clone();
612 f(out);
613 *out != before
614 }
615 None => false,
616 };
617 if changed {
618 self.changed();
619 }
620 }
621
622 pub fn track_info(&self) -> Option<TrackInfo> {
623 self.track_info.read().clone()
624 }
625
626 pub fn set_track_info(&self, info: Option<TrackInfo>) {
627 *self.track_info.write() = info;
628 self.changed();
629 }
630
631 pub fn seekable_ms(&self) -> u64 {
642 let Some(info) = self.track_info.read().clone() else {
643 return 0;
644 };
645
646 let pl = self.playlist.read();
649 let Some(item) = pl.items.iter().find(|item| item.id == info.id) else {
650 return info.duration_ms;
651 };
652
653 let LoadState::Downloading {
654 total,
655 bytes_written,
656 ..
657 } = self.load_state(item)
658 else {
659 return info.duration_ms;
660 };
661
662 if info.duration_ms == 0 {
667 return 0;
668 }
669
670 let written = bytes_written.load(Ordering::Acquire);
671 let reached = if total > 0 && info.duration_ms > 0 {
672 ((written as f64 / total as f64) * info.duration_ms as f64) as u64
673 } else if let Some(kbps) = info.bitrate_kbps.filter(|k| *k > 0) {
674 written.saturating_mul(8) / kbps as u64
678 } else {
679 return self.position_ms();
682 };
683
684 reached.saturating_sub(SEEK_SAFETY_MS).min(info.duration_ms)
685 }
686
687 pub fn duration_ms(&self) -> u64 {
695 let Some(info) = self.track_info.read().clone() else {
696 return 0;
697 };
698 if info.duration_ms > 0 {
699 return info.duration_ms;
700 }
701 self.playlist
703 .read()
704 .items
705 .iter()
706 .find(|item| item.id == info.id)
707 .and_then(|item| item.duration_ms)
708 .unwrap_or(0)
709 }
710
711 pub fn seek_ceiling_ms(&self) -> Option<u64> {
715 let duration = self.duration_ms();
716 if duration == 0 {
717 return None;
718 }
719 let seekable = self.seekable_ms();
720 (seekable < duration).then_some(seekable)
721 }
722
723 pub fn current_download_fraction(&self) -> Option<f64> {
726 let id = self.track_info.read().as_ref()?.id;
729 let pl = self.playlist.read();
730 pl.items
731 .iter()
732 .find(|item| item.id == id)
733 .and_then(|item| match self.load_state(item) {
734 LoadState::Downloading {
735 bytes_written,
736 total,
737 ..
738 } => {
739 let written = bytes_written.load(Ordering::Acquire);
740 (total > 0).then(|| (written as f64 / total as f64).min(1.0))
741 }
742 _ => None,
743 })
744 }
745
746 pub fn request_quit(&self) {
749 self.quit_requested.store(true, Ordering::Release);
750 }
751
752 pub fn quit_requested(&self) -> bool {
753 self.quit_requested.load(Ordering::Acquire)
754 }
755
756 pub fn signal_metadata_refresh(&self) {
762 self.metadata_refresh_pending.store(true, Ordering::Release);
763 self.changed();
764 }
765
766 pub fn take_metadata_refresh(&self) -> bool {
768 self.metadata_refresh_pending
769 .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire)
770 .is_ok()
771 }
772
773 pub fn output_sample_rate(&self) -> Option<u32> {
777 match self.output_sample_rate.load(Ordering::Acquire) {
778 0 => None,
779 rate => Some(rate as u32),
780 }
781 }
782
783 pub fn set_output_sample_rate(&self, rate: u32) {
784 self.output_sample_rate
785 .store(u64::from(rate), Ordering::Release);
786 self.changed();
787 }
788
789 pub fn clear_output_sample_rate(&self) {
794 self.output_sample_rate.store(0, Ordering::Release);
795 }
796
797 pub fn dsp(&self) -> Option<crate::audio::dsp::DspStatus> {
800 self.dsp.read().clone()
801 }
802
803 pub fn set_dsp(&self, status: Option<crate::audio::dsp::DspStatus>) {
804 let mut dsp = self.dsp.write();
805 if *dsp != status {
806 *dsp = status;
807 drop(dsp);
808 self.changed();
809 }
810 }
811
812 pub fn play_mode(&self) -> PlayMode {
815 PlayMode::from_bits(self.play_mode.load(Ordering::Acquire))
816 }
817
818 pub fn set_play_mode(&self, mode: PlayMode) {
823 if self.play_mode.swap(mode.to_bits(), Ordering::AcqRel) != mode.to_bits() {
824 self.content_version.fetch_add(1, Ordering::AcqRel);
825 self.bump_version();
826 }
827 }
828
829 pub fn playlist_version(&self) -> u64 {
832 self.playlist_version.load(Ordering::Acquire)
833 }
834
835 fn bump_version(&self) {
836 self.playlist_version.fetch_add(1, Ordering::AcqRel);
837 self.changed();
838 }
839
840 pub fn content_version(&self) -> u64 {
841 self.content_version.load(Ordering::Acquire)
842 }
843
844 fn bump_content(&self) {
846 self.content_version.fetch_add(1, Ordering::AcqRel);
847 self.pending_version.fetch_add(1, Ordering::AcqRel);
848 self.bump_version();
849 }
850
851 pub fn pending_version(&self) -> u64 {
854 self.pending_version.load(Ordering::Acquire)
855 }
856
857 pub fn downloads(&self) -> &Arc<DownloadStore> {
859 &self.downloads
860 }
861
862 fn load_state(&self, item: &PlaylistItem) -> LoadState {
863 LoadState::of(item, &self.downloads)
864 }
865
866 pub fn changed(&self) {
872 crate::signal::engine_changed().bump();
873 }
874
875 pub fn add_items(&self, items: Vec<PlaylistItem>) {
879 let mut pl = self.playlist.write();
880 pl.items.extend(items);
881 drop(pl);
882 self.bump_content();
883 }
884
885 pub fn insert_items_after(&self, items: Vec<PlaylistItem>, after: QueueItemId) {
887 let mut pl = self.playlist.write();
888 let insert_at = match pl.items.iter().position(|item| item.id == after) {
889 Some(pos) => pos + 1,
890 None => pl.items.len(), };
892 for (i, item) in items.into_iter().enumerate() {
893 pl.items.insert(insert_at + i, item);
894 }
895 drop(pl);
896 self.bump_content();
897 }
898
899 pub fn update_paths(&self, updates: &[(QueueItemId, PathBuf)]) {
901 let mut pl = self.playlist.write();
902 for (id, new_path) in updates {
903 if let Some(item) = pl.items.iter_mut().find(|item| item.id == *id) {
904 item.path = new_path.clone();
905 }
906 }
907 drop(pl);
908 self.bump_content();
909 }
910
911 pub fn remove_item(&self, id: QueueItemId) {
913 let mut pl = self.playlist.write();
914 pl.items.retain(|item| item.id != id);
915 if pl.cursor == Some(id) {
917 pl.cursor = None;
918 }
919 drop(pl);
920 self.bump_content();
921 }
922
923 pub fn move_item(&self, id: QueueItemId, target: QueueItemId, after: bool) {
925 let mut pl = self.playlist.write();
926 let Some(from) = pl.items.iter().position(|item| item.id == id) else {
927 return;
928 };
929 let item = pl.items.remove(from);
930 let Some(to) = pl.items.iter().position(|item| item.id == target) else {
931 let pos = from.min(pl.items.len());
933 pl.items.insert(pos, item);
934 return;
935 };
936 let insert_at = if after { to + 1 } else { to };
937 pl.items.insert(insert_at, item);
938 drop(pl);
939 self.bump_content();
940 }
941
942 pub fn move_items(&self, ids: &[QueueItemId], target: QueueItemId, after: bool) {
945 use std::collections::HashSet;
946 let id_set: HashSet<QueueItemId> = ids.iter().copied().collect();
947
948 let mut pl = self.playlist.write();
949
950 let mut remaining = Vec::with_capacity(pl.items.len());
952 let mut moved = Vec::with_capacity(ids.len());
953 for item in pl.items.drain(..) {
954 if id_set.contains(&item.id) {
955 moved.push(item);
956 } else {
957 remaining.push(item);
958 }
959 }
960
961 let insert_at = match remaining.iter().position(|item| item.id == target) {
963 Some(pos) => {
964 if after {
965 pos + 1
966 } else {
967 pos
968 }
969 }
970 None => remaining.len(),
971 };
972
973 for (i, item) in moved.into_iter().enumerate() {
975 remaining.insert(insert_at + i, item);
976 }
977
978 pl.items = remaining;
979 drop(pl);
980 self.bump_content();
981 }
982
983 pub fn set_cursor(&self, id: Option<QueueItemId>) {
985 let mut pl = self.playlist.write();
986 pl.cursor = id;
987 drop(pl);
988 self.bump_version();
989 }
990
991 pub fn is_empty(&self) -> bool {
992 self.playlist.read().items.is_empty()
993 }
994
995 pub fn cursor(&self) -> Option<QueueItemId> {
996 self.playlist.read().cursor
997 }
998
999 pub fn cursor_path(&self) -> Option<PathBuf> {
1001 let pl = self.playlist.read();
1002 let cursor = pl.cursor?;
1003 pl.items
1004 .iter()
1005 .find(|item| item.id == cursor)
1006 .map(|item| item.path.clone())
1007 }
1008
1009 pub fn replace_playlist(
1019 &self,
1020 items: Vec<PlaylistItem>,
1021 ) -> (Vec<PlaylistItem>, Option<QueueItemId>) {
1022 let mut pl = self.playlist.write();
1023 let old = std::mem::replace(&mut pl.items, items);
1024 let cursor = pl.cursor.take();
1025 drop(pl);
1026 self.bump_content();
1027 (old, cursor)
1028 }
1029
1030 pub fn clear_playlist(&self) {
1031 let mut pl = self.playlist.write();
1032 pl.items.clear();
1033 pl.cursor = None;
1034 drop(pl);
1035 self.bump_content();
1036 }
1037
1038 pub fn advance_cursor_loadable(&self) -> Option<QueueItemId> {
1054 let repeat = match self.play_mode().repeat {
1055 Repeat::Off => Repeat::Off,
1056 Repeat::Queue | Repeat::One => Repeat::Queue,
1057 };
1058 let mut pl = self.playlist.write();
1059 let next = match pl.cursor {
1060 Some(cid) => follows(&pl.items, cid, repeat)?.0?.id,
1061 None => {
1062 pl.items
1063 .iter()
1064 .find(|item| !matches!(item.state, ItemState::Failed(_)))?
1065 .id
1066 }
1067 };
1068
1069 pl.cursor = Some(next);
1070 drop(pl);
1071 self.bump_version();
1072 Some(next)
1073 }
1074
1075 pub fn lookahead_after(&self, after_id: QueueItemId) -> Option<Lookahead> {
1092 let repeat = self.play_mode().repeat;
1093 let pl = self.playlist.read();
1094 let (next, wrapped) = follows(&pl.items, after_id, repeat)?;
1095 Some(Lookahead {
1096 after: after_id,
1097 next: next.map(|item| item.id),
1098 chosen: next
1099 .filter(|item| matches!(item.state, ItemState::Ready))
1100 .map(|item| (item.id, item.path.clone())),
1101 wrapped,
1102 boundary: 0,
1103 })
1104 }
1105
1106 pub fn still_follows(&self, step: &Lookahead) -> bool {
1113 let repeat = self.play_mode().repeat;
1114 let pl = self.playlist.read();
1115 follows(&pl.items, step.after, repeat)
1116 .is_some_and(|(next, _)| next.map(|item| item.id) == step.next)
1117 }
1118
1119 pub fn retreat_cursor(&self) -> Option<(QueueItemId, PathBuf)> {
1123 let mut pl = self.playlist.write();
1124 let cursor_pos = match pl.cursor {
1125 Some(cid) => pl.items.iter().position(|item| item.id == cid),
1126 None => None,
1127 };
1128
1129 let wraps = self.play_mode().repeat != Repeat::Off && pl.items.len() > 1;
1132 let prev_pos = cursor_pos.and_then(|p| match p.checked_sub(1) {
1133 None if wraps => Some(pl.items.len() - 1),
1134 prev => prev,
1135 });
1136
1137 match prev_pos {
1138 Some(pos) => {
1139 let item = &pl.items[pos];
1140 let result = (item.id, item.path.clone());
1141 pl.cursor = Some(item.id);
1142 drop(pl);
1143 self.bump_version();
1144 Some(result)
1145 }
1146 None => None,
1147 }
1148 }
1149
1150 pub fn update_item_state(&self, id: QueueItemId, new_state: ItemState) {
1154 let pending = new_state == ItemState::Pending;
1155 let mut pl = self.playlist.write();
1156 if let Some(item) = pl.items.iter_mut().find(|item| item.id == id) {
1157 item.state = new_state;
1158 }
1159 drop(pl);
1160 if pending {
1161 self.pending_version.fetch_add(1, Ordering::AcqRel);
1162 }
1163 self.bump_version();
1164 }
1165
1166 pub fn item_state(&self, id: QueueItemId) -> Option<ItemState> {
1168 let pl = self.playlist.read();
1169 pl.items
1170 .iter()
1171 .find(|item| item.id == id)
1172 .map(|item| item.state.clone())
1173 }
1174
1175 pub fn update_item_metadata(
1185 &self,
1186 id: QueueItemId,
1187 title: String,
1188 artist: String,
1189 album_artist: String,
1190 album: String,
1191 duration_ms: Option<u64>,
1192 ) {
1193 let mut pl = self.playlist.write();
1194 if let Some(item) = pl.items.iter_mut().find(|item| item.id == id) {
1195 if item.db_id.is_none() {
1196 item.title = title;
1197 item.artist = artist;
1198 item.album_artist = album_artist;
1199 item.album = album;
1200 }
1201 if let Some(dur) = duration_ms {
1202 item.duration_ms = Some(dur);
1203 }
1204 }
1205 drop(pl);
1206 self.bump_content();
1207 }
1208
1209 pub fn item_playback_source(&self, id: QueueItemId) -> Option<PlaybackSource> {
1212 let pl = self.playlist.read();
1213 pl.items
1214 .iter()
1215 .find(|item| item.id == id)
1216 .and_then(|item| match self.load_state(item) {
1217 LoadState::Ready => Some(PlaybackSource::Ready(item.path.clone())),
1218 LoadState::Downloading {
1219 path,
1220 total,
1221 bytes_written,
1222 } => {
1223 let written = bytes_written.load(Ordering::Acquire);
1224 (written >= STREAM_THRESHOLD).then_some(PlaybackSource::Streaming {
1225 path,
1226 bytes_written,
1227 total,
1228 })
1229 }
1230 _ => None,
1231 })
1232 }
1233
1234 pub fn reset_items_with_missing_files(&self) -> Vec<(i64, QueueItemId)> {
1244 let mut pl = self.playlist.write();
1245 let mut reset = Vec::new();
1246 for item in pl.items.iter_mut() {
1247 let Some(db_id) = item.db_id else { continue };
1248 if !matches!(item.state, ItemState::Ready) {
1249 continue;
1250 }
1251 if item.path.exists() {
1252 continue;
1253 }
1254 item.state = ItemState::Pending;
1255 reset.push((db_id, item.id));
1256 }
1257 drop(pl);
1258 if !reset.is_empty() {
1259 self.pending_version.fetch_add(1, Ordering::AcqRel);
1260 self.bump_version();
1261 }
1262 reset
1263 }
1264
1265 pub fn item_path_if_ready(&self, id: QueueItemId) -> Option<PathBuf> {
1267 let pl = self.playlist.read();
1268 pl.items.iter().find(|item| item.id == id).and_then(|item| {
1269 if matches!(item.state, ItemState::Ready) {
1270 Some(item.path.clone())
1271 } else {
1272 None
1273 }
1274 })
1275 }
1276
1277 pub fn is_cursor(&self, id: QueueItemId) -> bool {
1278 self.playlist.read().cursor == Some(id)
1279 }
1280
1281 pub fn same_album_item_ids(&self, id: QueueItemId) -> Vec<QueueItemId> {
1285 let pl = self.playlist.read();
1286 let Some(cursor) = pl.items.iter().find(|item| item.id == id) else {
1287 return vec![];
1288 };
1289 let album = cursor.album.clone();
1290 let album_artist = cursor.album_artist.clone();
1291 pl.items
1292 .iter()
1293 .filter(|item| {
1294 item.id != id && item.album == album && item.album_artist == album_artist
1295 })
1296 .map(|item| item.id)
1297 .collect()
1298 }
1299
1300 pub fn pending_downloads(&self) -> Vec<(i64, QueueItemId)> {
1306 let pl = self.playlist.read();
1307 let from = pl
1308 .cursor
1309 .and_then(|c| pl.items.iter().position(|item| item.id == c))
1310 .unwrap_or(0);
1311 let (before, after) = pl.items.split_at(from);
1312 after
1313 .iter()
1314 .chain(before)
1315 .filter(|item| matches!(item.state, ItemState::Pending))
1316 .filter_map(|item| item.db_id.map(|db_id| (db_id, item.id)))
1317 .collect()
1318 }
1319
1320 pub fn playback_order(&self) -> Vec<(i64, QueueItemId)> {
1324 let pl = self.playlist.read();
1325 let from = pl
1326 .cursor
1327 .and_then(|c| pl.items.iter().position(|item| item.id == c))
1328 .unwrap_or(0);
1329 let (before, after) = pl.items.split_at(from);
1330 after
1331 .iter()
1332 .chain(before)
1333 .filter_map(|item| item.db_id.map(|db_id| (db_id, item.id)))
1334 .collect()
1335 }
1336
1337 pub fn item_db_id(&self, id: QueueItemId) -> Option<i64> {
1339 let pl = self.playlist.read();
1340 pl.items
1341 .iter()
1342 .find(|item| item.id == id)
1343 .and_then(|item| item.db_id)
1344 }
1345
1346 pub fn item_load_state(&self, id: QueueItemId) -> Option<LoadState> {
1348 let pl = self.playlist.read();
1349 pl.items
1350 .iter()
1351 .find(|item| item.id == id)
1352 .map(|item| self.load_state(item))
1353 }
1354
1355 pub fn snapshot_playlist(&self) -> (Vec<PlaylistItem>, Option<QueueItemId>) {
1359 let pl = self.playlist.read();
1360 (pl.items.clone(), pl.cursor)
1361 }
1362
1363 pub fn get_item(&self, id: QueueItemId) -> Option<PlaylistItem> {
1365 let pl = self.playlist.read();
1366 pl.items.iter().find(|item| item.id == id).cloned()
1367 }
1368
1369 pub fn item_before(&self, id: QueueItemId) -> Option<QueueItemId> {
1371 let pl = self.playlist.read();
1372 let pos = pl.items.iter().position(|item| item.id == id)?;
1373 if pos == 0 {
1374 None
1375 } else {
1376 Some(pl.items[pos - 1].id)
1377 }
1378 }
1379
1380 pub fn reorder_to(&self, order: &[QueueItemId]) {
1387 let mut pl = self.playlist.write();
1388 let mut taken: Vec<Option<PlaylistItem>> = pl.items.drain(..).map(Some).collect();
1389 let mut sorted = Vec::with_capacity(taken.len());
1390 for id in order {
1391 if let Some(slot) = taken
1392 .iter_mut()
1393 .find(|i| i.as_ref().is_some_and(|i| i.id == *id))
1394 && let Some(item) = slot.take()
1395 {
1396 sorted.push(item);
1397 }
1398 }
1399 sorted.extend(taken.into_iter().flatten());
1400 pl.items = sorted;
1401 drop(pl);
1402 self.bump_content();
1403 }
1404
1405 pub fn shuffle_order(&self) -> Vec<(QueueItemId, Option<u32>)> {
1408 let pl = self.playlist.read();
1409 pl.items
1410 .iter()
1411 .map(|item| (item.id, item.pre_shuffle))
1412 .collect()
1413 }
1414
1415 pub fn restore_shuffle_order(&self, order: &[(QueueItemId, Option<u32>)]) {
1418 let ids: Vec<QueueItemId> = order.iter().map(|(id, _)| *id).collect();
1419 self.reorder_to(&ids);
1420 let was: HashMap<QueueItemId, Option<u32>> = order.iter().copied().collect();
1421 let mut pl = self.playlist.write();
1422 for item in pl.items.iter_mut() {
1423 if let Some(pre) = was.get(&item.id) {
1424 item.pre_shuffle = *pre;
1425 }
1426 }
1427 drop(pl);
1428 self.bump_content();
1429 }
1430
1431 pub fn shuffle_after_cursor(&self) {
1435 let mut pl = self.playlist.write();
1436 for (at, item) in pl.items.iter_mut().enumerate() {
1437 item.pre_shuffle = Some(at as u32);
1438 }
1439 let from = pl
1440 .cursor
1441 .and_then(|c| pl.items.iter().position(|item| item.id == c))
1442 .map_or(0, |at| at + 1);
1443 crate::helpers::shuffle(&mut pl.items[from..]);
1444 drop(pl);
1445 self.bump_content();
1446 }
1447
1448 pub fn shuffle_from(&self, start: QueueItemId) {
1454 let mut pl = self.playlist.write();
1455 if pl.items.iter().any(|item| item.pre_shuffle.is_some()) {
1456 return;
1457 }
1458 for (at, item) in pl.items.iter_mut().enumerate() {
1459 item.pre_shuffle = Some(at as u32);
1460 }
1461 if let Some(at) = pl.items.iter().position(|item| item.id == start) {
1462 let item = pl.items.remove(at);
1463 pl.items.insert(0, item);
1464 }
1465 crate::helpers::shuffle(&mut pl.items[1..]);
1466 drop(pl);
1467 self.bump_content();
1468 }
1469
1470 pub fn unshuffle(&self) {
1473 let mut pl = self.playlist.write();
1474 let slots: Vec<usize> = (0..pl.items.len())
1475 .filter(|&at| pl.items[at].pre_shuffle.is_some())
1476 .collect();
1477 let mut moved: Vec<PlaylistItem> = slots.iter().map(|&at| pl.items[at].clone()).collect();
1478 moved.sort_by_key(|item| item.pre_shuffle);
1479 for (at, mut item) in slots.into_iter().zip(moved) {
1480 item.pre_shuffle = None;
1481 pl.items[at] = item;
1482 }
1483 drop(pl);
1484 self.bump_content();
1485 }
1486
1487 pub fn items_before(&self, ids: &[QueueItemId]) -> Vec<(QueueItemId, Option<QueueItemId>)> {
1494 use std::collections::HashSet;
1495 let wanted: HashSet<QueueItemId> = ids.iter().copied().collect();
1496 let pl = self.playlist.read();
1497 pl.items
1498 .iter()
1499 .enumerate()
1500 .filter(|(_, item)| wanted.contains(&item.id))
1501 .map(|(pos, item)| {
1502 let before = if pos == 0 {
1503 None
1504 } else {
1505 Some(pl.items[pos - 1].id)
1506 };
1507 (item.id, before)
1508 })
1509 .collect()
1510 }
1511
1512 pub fn surviving_item_before(
1516 &self,
1517 id: QueueItemId,
1518 removed: &[QueueItemId],
1519 ) -> Option<QueueItemId> {
1520 use std::collections::HashSet;
1521 let removed: HashSet<QueueItemId> = removed.iter().copied().collect();
1522 let pl = self.playlist.read();
1523 let pos = pl.items.iter().position(|item| item.id == id)?;
1524 pl.items[..pos]
1525 .iter()
1526 .rev()
1527 .find(|item| !removed.contains(&item.id))
1528 .map(|item| item.id)
1529 }
1530
1531 pub fn restore_playlist(&self, items: Vec<PlaylistItem>, cursor: Option<QueueItemId>) {
1533 let mut pl = self.playlist.write();
1534 pl.items = items;
1535 pl.cursor = cursor;
1536 drop(pl);
1537 self.bump_content();
1538 }
1539
1540 pub fn remove_items(&self, ids: &[QueueItemId]) {
1542 use std::collections::HashSet;
1543 let id_set: HashSet<QueueItemId> = ids.iter().copied().collect();
1544 let mut pl = self.playlist.write();
1545 pl.items.retain(|item| !id_set.contains(&item.id));
1546 if let Some(cursor) = pl.cursor
1547 && id_set.contains(&cursor)
1548 {
1549 pl.cursor = None;
1550 }
1551 drop(pl);
1552 self.bump_content();
1553 }
1554
1555 pub fn insert_item_at(&self, item: PlaylistItem, after: Option<QueueItemId>) {
1557 let mut pl = self.playlist.write();
1558 let insert_at = match after {
1559 Some(after_id) => {
1560 match pl.items.iter().position(|i| i.id == after_id) {
1561 Some(pos) => pos + 1,
1562 None => pl.items.len(), }
1564 }
1565 None => 0,
1566 };
1567 pl.items.insert(insert_at, item);
1568 drop(pl);
1569 self.bump_content();
1570 }
1571
1572 pub fn move_item_to(&self, id: QueueItemId, after: Option<QueueItemId>) {
1574 let mut pl = self.playlist.write();
1575 let Some(from) = pl.items.iter().position(|item| item.id == id) else {
1576 return;
1577 };
1578 let item = pl.items.remove(from);
1579 let insert_at = match after {
1580 Some(after_id) => match pl.items.iter().position(|i| i.id == after_id) {
1581 Some(pos) => pos + 1,
1582 None => pl.items.len(),
1583 },
1584 None => 0,
1585 };
1586 pl.items.insert(insert_at, item);
1587 drop(pl);
1588 self.bump_content();
1589 }
1590
1591 pub fn move_items_to(&self, entries: &[(QueueItemId, Option<QueueItemId>)]) {
1594 for &(id, after) in entries {
1595 self.move_item_to(id, after);
1596 }
1597 }
1598
1599 pub fn derive_visible_queue(&self) -> VisibleQueueSnapshot {
1604 let playing_duration_ms = self.track_info.read().as_ref().map(|ti| ti.duration_ms);
1606 let transfers: HashMap<i64, (u64, u64)> = self
1609 .downloads
1610 .readings()
1611 .into_iter()
1612 .map(|r| (r.track_id, (r.written, r.total)))
1613 .collect();
1614 let pl = self.playlist.read();
1615
1616 let cursor_pos = match pl.cursor {
1617 Some(cid) => pl.items.iter().position(|item| item.id == cid),
1618 None => None,
1619 };
1620
1621 let mut entries = Vec::with_capacity(pl.items.len());
1622 let mut finished_count = 0;
1623 let mut has_playing = false;
1624 let mut queue_count = 0;
1625
1626 for (i, item) in pl.items.iter().enumerate() {
1627 let is_cursor = cursor_pos == Some(i);
1628 let is_before_cursor = cursor_pos.is_some_and(|cp| i < cp);
1629
1630 let dl_progress = match item.state {
1634 ItemState::Pending => item.db_id.and_then(|id| transfers.get(&id).copied()),
1635 _ => None,
1636 };
1637 let transferring = dl_progress.is_some();
1638
1639 let status = if is_cursor {
1640 has_playing = true;
1641 QueueEntryStatus::at_cursor(&item.state, transferring)
1642 } else if is_before_cursor {
1643 finished_count += 1;
1644 match &item.state {
1645 ItemState::Ready => QueueEntryStatus::Played,
1646 ItemState::Pending if transferring => QueueEntryStatus::Downloading,
1650 ItemState::Pending => QueueEntryStatus::Queued,
1651 ItemState::Failed(_) => QueueEntryStatus::Failed,
1652 }
1653 } else {
1654 queue_count += 1;
1655 match &item.state {
1656 ItemState::Ready => QueueEntryStatus::Queued,
1657 ItemState::Pending if transferring => QueueEntryStatus::Downloading,
1658 ItemState::Pending => QueueEntryStatus::Queued,
1661 ItemState::Failed(_) => QueueEntryStatus::Failed,
1662 }
1663 };
1664
1665 let duration_ms =
1667 if has_playing && status == QueueEntryStatus::Playing && item.duration_ms.is_none()
1668 {
1669 playing_duration_ms
1670 } else {
1671 item.duration_ms
1672 };
1673
1674 entries.push(QueueEntry {
1675 id: item.id,
1676 db_id: item.db_id,
1677 playlist_entry_id: item.playlist_entry_id,
1678 path: item.path.clone(),
1679 title: item.title.clone(),
1680 artist: item.artist.clone(),
1681 album_artist: item.album_artist.clone(),
1682 album: item.album.clone(),
1683 year: item.year.clone(),
1684 codec: item.codec.clone(),
1685 track_number: item.track_number,
1686 disc: item.disc,
1687 duration_ms,
1688 status,
1689 download_progress: dl_progress,
1690 error: match &item.state {
1691 ItemState::Failed(reason) => Some(reason.clone()),
1692 _ => None,
1693 },
1694 });
1695 }
1696
1697 VisibleQueueSnapshot {
1698 entries,
1699 finished_count,
1700 has_playing,
1701 queue_count,
1702 }
1703 }
1704}
1705
1706#[cfg(test)]
1707mod tests {
1708 use super::*;
1709
1710 fn make_item(title: &str, state: ItemState) -> PlaylistItem {
1713 PlaylistItem {
1714 playlist_entry_id: None,
1715 id: QueueItemId::new(),
1716 db_id: None,
1717 path: PathBuf::from(format!("/music/{title}.flac")),
1718 title: title.to_string(),
1719 artist: "Artist".to_string(),
1720 album_artist: "Artist".to_string(),
1721 album: "Album".to_string(),
1722 year: None,
1723 codec: Some("FLAC".to_string()),
1724 track_number: None,
1725 disc: None,
1726 duration_ms: Some(200_000),
1727 state,
1728 pre_shuffle: None,
1729 }
1730 }
1731
1732 fn downloading_item(
1735 state: &SharedPlayerState,
1736 title: &str,
1737 total: u64,
1738 ) -> (PlaylistItem, Arc<ByteFeed>) {
1739 static NEXT_TRACK: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(1);
1740 let mut item = make_item(title, ItemState::Pending);
1741 item.db_id = Some(NEXT_TRACK.fetch_add(1, Ordering::Relaxed));
1742 let feed = start_transfer(state, &item, total);
1743 (item, feed)
1744 }
1745
1746 fn start_transfer(state: &SharedPlayerState, item: &PlaylistItem, total: u64) -> Arc<ByteFeed> {
1748 let track_id = item.db_id.expect("a transfer is for a library track");
1749 let store = state.downloads();
1750 store.claim(track_id, Some(item.id));
1751 let feed = store.announce(
1752 track_id,
1753 item.title.clone(),
1754 String::new(),
1755 PathBuf::from(format!("/cache/{}.flac.part", item.title)),
1756 PathBuf::from(format!("/cache/{}.flac", item.title)),
1757 );
1758 store.started(track_id, total);
1759 feed
1760 }
1761
1762 fn ready_item(title: &str) -> PlaylistItem {
1763 make_item(title, ItemState::Ready)
1764 }
1765
1766 fn pending_item(title: &str) -> PlaylistItem {
1767 make_item(title, ItemState::Pending)
1768 }
1769
1770 fn failed_item(title: &str) -> PlaylistItem {
1771 make_item(title, ItemState::Failed("nope".into()))
1772 }
1773
1774 const DURATION_MS: u64 = 32_523_787;
1775
1776 fn streaming_state(
1779 downloaded: u64,
1780 total: u64,
1781 bitrate_kbps: Option<u32>,
1782 ) -> Arc<SharedPlayerState> {
1783 streaming_state_with_duration(downloaded, total, bitrate_kbps, DURATION_MS)
1784 }
1785
1786 fn streaming_state_with_duration(
1789 downloaded: u64,
1790 total: u64,
1791 bitrate_kbps: Option<u32>,
1792 container_duration_ms: u64,
1793 ) -> Arc<SharedPlayerState> {
1794 let mut item = make_item("train", ItemState::Pending);
1795 item.db_id = Some(1);
1796 let id = item.id;
1797 let path = item.path.clone();
1798
1799 let state = SharedPlayerState::new();
1800 start_transfer(&state, &item, total).set(downloaded);
1801 state.add_items(vec![item]);
1802 state.set_cursor(Some(id));
1803 state.set_track_info(Some(TrackInfo {
1804 id,
1805 path,
1806 codec: "Opus".into(),
1807 sample_rate: 48_000,
1808 bit_depth: None,
1809 bitrate_kbps,
1810 channels: 2,
1811 duration_ms: container_duration_ms,
1812 }));
1813 state
1814 }
1815
1816 #[test]
1819 fn a_track_on_disk_is_seekable_end_to_end() {
1820 let item = ready_item("done");
1821 let id = item.id;
1822 let path = item.path.clone();
1823 let state = SharedPlayerState::new();
1824 state.add_items(vec![item]);
1825 state.set_cursor(Some(id));
1826 state.set_track_info(Some(TrackInfo {
1827 id,
1828 path,
1829 codec: "FLAC".into(),
1830 sample_rate: 44_100,
1831 bit_depth: Some(16),
1832 bitrate_kbps: None,
1833 channels: 2,
1834 duration_ms: 200_000,
1835 }));
1836
1837 assert_eq!(state.seekable_ms(), 200_000);
1838 assert_eq!(state.seek_ceiling_ms(), None);
1840 }
1841
1842 #[test]
1843 fn a_downloading_track_is_seekable_as_far_as_its_bytes_reach() {
1844 let state = streaming_state(100, 400, None);
1847 assert_eq!(state.seekable_ms(), 32_523_787 / 4 - SEEK_SAFETY_MS);
1848 assert_eq!(
1849 state.seek_ceiling_ms(),
1850 Some(32_523_787 / 4 - SEEK_SAFETY_MS)
1851 );
1852 }
1853
1854 #[test]
1855 fn a_transfer_without_a_content_length_falls_back_to_bitrate() {
1856 let state = streaming_state(1024 * 1024, 0, Some(128));
1859 assert_eq!(state.seekable_ms(), 1024 * 1024 * 8 / 128 - SEEK_SAFETY_MS);
1860 }
1861
1862 #[test]
1863 fn nothing_to_estimate_from_allows_no_forward_seek() {
1864 let state = streaming_state(1024 * 1024, 0, None);
1867 state.set_position_ms(12_000);
1868 assert_eq!(state.seekable_ms(), 12_000);
1869 }
1870
1871 #[test]
1872 fn the_seekable_extent_never_exceeds_the_track() {
1873 let state = streaming_state(500, 400, None);
1876 assert_eq!(state.seekable_ms(), 32_523_787);
1877 }
1878
1879 #[test]
1880 fn nothing_playing_is_seekable_nowhere() {
1881 assert_eq!(SharedPlayerState::new().seekable_ms(), 0);
1882 assert_eq!(SharedPlayerState::new().seek_ceiling_ms(), None);
1883 }
1884
1885 #[test]
1886 fn a_container_that_cannot_state_its_duration_cannot_be_seeked() {
1887 let state = streaming_state_with_duration(200, 400, Some(128), 0);
1891 assert_eq!(state.seekable_ms(), 0);
1892 }
1893
1894 #[test]
1895 fn the_library_duration_stands_in_for_a_silent_container() {
1896 let state = streaming_state_with_duration(200, 400, Some(128), 0);
1899 assert_eq!(state.duration_ms(), 200_000, "the item's own figure");
1900 assert_eq!(state.seekable_ms(), 0);
1902 assert_eq!(state.seek_ceiling_ms(), Some(0));
1903 }
1904
1905 #[test]
1906 fn the_container_duration_wins_where_there_is_one() {
1907 let state = streaming_state(200, 400, None);
1908 assert_eq!(state.duration_ms(), DURATION_MS);
1909 }
1910
1911 #[test]
1912 fn the_download_landing_restores_seeking() {
1913 let state = streaming_state_with_duration(400, 400, Some(128), 0);
1917 assert_eq!(state.seekable_ms(), 0);
1918
1919 let id = state.cursor().expect("cursor");
1923 let _ =
1924 crate::remote::downloads::settle(&state, 1, &Ok(PathBuf::from("/cache/train.flac")));
1925 assert_eq!(state.item_state(id), Some(ItemState::Ready));
1926 let info = state.track_info().expect("track info");
1927 state.set_track_info(Some(TrackInfo {
1928 duration_ms: DURATION_MS,
1929 ..info
1930 }));
1931
1932 assert_eq!(state.seekable_ms(), DURATION_MS);
1933 assert_eq!(state.seek_ceiling_ms(), None, "no boundary left to draw");
1934 }
1935
1936 #[test]
1939 fn advance_parks_on_a_still_downloading_track() {
1940 let state = SharedPlayerState::new();
1944 let item0 = ready_item("track-0");
1945 let item1 = pending_item("track-1");
1946 let item2 = ready_item("track-2");
1947 let (id0, id1) = (item0.id, item1.id);
1948
1949 state.add_items(vec![item0, item1, item2]);
1950
1951 assert_eq!(state.advance_cursor_loadable(), Some(id0));
1952 assert_eq!(state.advance_cursor_loadable(), Some(id1));
1953 assert_eq!(state.cursor(), Some(id1));
1954 }
1955
1956 #[test]
1957 fn advance_skips_failed_items() {
1958 let state = SharedPlayerState::new();
1959 let item0 = ready_item("track-0");
1960 let item1 = failed_item("track-1");
1961 let item2 = ready_item("track-2");
1962 let (id0, id2) = (item0.id, item2.id);
1963
1964 state.add_items(vec![item0, item1, item2]);
1965 state.set_cursor(Some(id0));
1966
1967 assert_eq!(state.advance_cursor_loadable(), Some(id2));
1968 }
1969
1970 #[test]
1971 fn advance_stops_at_end_of_playlist() {
1972 let state = SharedPlayerState::new();
1973 let item0 = ready_item("track-0");
1974 let item1 = ready_item("track-1");
1975 let id1 = item1.id;
1976
1977 state.add_items(vec![item0, item1]);
1978 state.set_cursor(Some(id1));
1979
1980 assert_eq!(state.advance_cursor_loadable(), None);
1981 assert_eq!(
1982 state.cursor(),
1983 Some(id1),
1984 "cursor unchanged on a failed advance"
1985 );
1986 }
1987
1988 #[test]
1989 fn advance_with_only_failed_items_returns_none() {
1990 let state = SharedPlayerState::new();
1991 state.add_items(vec![failed_item("bad-0"), failed_item("bad-1")]);
1992
1993 assert_eq!(state.advance_cursor_loadable(), None);
1994 }
1995
1996 #[test]
1997 fn advance_from_a_vanished_cursor_does_not_restart_the_queue() {
1998 let state = SharedPlayerState::new();
1999 let item0 = ready_item("track-0");
2000 let item1 = ready_item("track-1");
2001 let id0 = item0.id;
2002
2003 state.add_items(vec![item0, item1]);
2004 let ghost = QueueItemId::new();
2005 state.set_cursor(Some(ghost));
2006
2007 assert_eq!(state.advance_cursor_loadable(), None);
2008 assert_ne!(state.cursor(), Some(id0));
2009 }
2010
2011 fn chosen_after(state: &SharedPlayerState, id: QueueItemId) -> Option<QueueItemId> {
2014 state
2015 .lookahead_after(id)
2016 .and_then(|step| step.chosen)
2017 .map(|(id, _)| id)
2018 }
2019
2020 #[test]
2021 fn a_step_stops_at_a_track_still_arriving() {
2022 let state = SharedPlayerState::new();
2023 let a = ready_item("a");
2024 let b = PlaylistItem {
2025 state: ItemState::Pending,
2026 ..ready_item("b")
2027 };
2028 let c = ready_item("c");
2029 let (ida, idb) = (a.id, b.id);
2030 state.add_items(vec![a, b, c]);
2031
2032 let step = state.lookahead_after(ida).unwrap();
2033 assert_eq!(step.next, Some(idb));
2034 assert!(step.chosen.is_none(), "c is not queued over it");
2035
2036 state.update_item_state(idb, ItemState::Ready);
2037 assert!(
2038 state.still_follows(&step),
2039 "its landing is not an edit: the advance plays it"
2040 );
2041
2042 state.add_items(vec![ready_item("d")]);
2043 assert!(state.still_follows(&step), "nor is adding after it");
2044
2045 state.insert_items_after(vec![ready_item("next")], ida);
2046 assert!(!state.still_follows(&step), "a track put before it is");
2047 }
2048
2049 #[test]
2050 fn a_step_passes_over_failed_tracks() {
2051 let state = SharedPlayerState::new();
2052 let a = ready_item("a");
2053 let failed = PlaylistItem {
2054 state: ItemState::Failed("gone".into()),
2055 ..ready_item("failed")
2056 };
2057 let c = ready_item("c");
2058 let (ida, idc) = (a.id, c.id);
2059 state.add_items(vec![a, failed, c]);
2060
2061 let step = state.lookahead_after(ida).unwrap();
2062 assert_eq!(step.chosen.as_ref().map(|(id, _)| *id), Some(idc));
2063
2064 let another = PlaylistItem {
2065 state: ItemState::Failed("gone".into()),
2066 ..ready_item("another")
2067 };
2068 state.insert_items_after(vec![another], ida);
2069 assert!(
2070 state.still_follows(&step),
2071 "another failed track before it changes nothing"
2072 );
2073
2074 let arriving = PlaylistItem {
2075 state: ItemState::Pending,
2076 ..ready_item("arriving")
2077 };
2078 state.insert_items_after(vec![arriving], ida);
2079 assert!(!state.still_follows(&step), "a track still arriving does");
2080 }
2081
2082 #[test]
2083 fn a_step_breaks_when_what_it_chose_moves_ahead_of_it() {
2084 let state = SharedPlayerState::new();
2085 let (a, c) = (ready_item("a"), ready_item("c"));
2086 let (ida, idc) = (a.id, c.id);
2087 state.add_items(vec![a, c]);
2088 let step = state.lookahead_after(ida).unwrap();
2089 state.move_item(idc, ida, false);
2090 assert!(!state.still_follows(&step));
2091 }
2092
2093 #[test]
2094 fn a_step_to_the_end_of_the_queue_breaks_when_a_track_is_added() {
2095 let state = SharedPlayerState::new();
2096 let a = ready_item("a");
2097 let ida = a.id;
2098 state.add_items(vec![a]);
2099 let step = state.lookahead_after(ida).unwrap();
2100 assert!(step.chosen.is_none());
2101 assert!(state.still_follows(&step));
2102 state.add_items(vec![ready_item("b")]);
2103 assert!(!state.still_follows(&step));
2104 }
2105
2106 #[test]
2107 fn peek_after_a_removed_item_returns_none() {
2108 let state = SharedPlayerState::new();
2112 let item0 = ready_item("track-0");
2113 let item1 = ready_item("track-1");
2114 let item2 = ready_item("track-2");
2115 let (id0, id1, id2) = (item0.id, item1.id, item2.id);
2116
2117 state.add_items(vec![item0, item1, item2]);
2118 assert_eq!(chosen_after(&state, id1), Some(id2));
2119
2120 state.remove_item(id2);
2121 assert!(
2122 state.lookahead_after(id2).is_none(),
2123 "a vanished reference must not resolve to the head of the queue"
2124 );
2125 assert_ne!(chosen_after(&state, id2), Some(id0));
2126 }
2127
2128 #[test]
2129 fn the_lookahead_steps_over_a_track_that_failed() {
2130 let state = SharedPlayerState::new();
2131 let playing = ready_item("playing");
2132 let failed = failed_item("failed");
2133 let after = ready_item("after");
2134 let (playing_id, after_id) = (playing.id, after.id);
2135 state.add_items(vec![playing, failed, after]);
2136
2137 assert_eq!(chosen_after(&state, playing_id), Some(after_id));
2138 }
2139
2140 fn repeating(state: &SharedPlayerState, repeat: Repeat) {
2143 state.set_play_mode(PlayMode {
2144 shuffle: false,
2145 repeat,
2146 });
2147 }
2148
2149 #[test]
2150 fn a_step_from_the_last_item_wraps_only_when_the_queue_repeats() {
2151 let state = SharedPlayerState::new();
2152 let items: Vec<_> = ["a", "b"].map(ready_item).into();
2153 let (a, b) = (items[0].id, items[1].id);
2154 state.add_items(items);
2155
2156 assert_eq!(state.lookahead_after(b).unwrap().next, None);
2157 repeating(&state, Repeat::Queue);
2158 let step = state.lookahead_after(b).unwrap();
2159 assert_eq!((step.next, step.wrapped), (Some(a), true));
2160 assert!(state.still_follows(&step));
2161
2162 let later = ready_item("later");
2163 state.add_items(vec![later.clone()]);
2164 assert!(!state.still_follows(&step), "something follows b now");
2165 state.remove_items(&[later.id]);
2166 repeating(&state, Repeat::Off);
2167 assert!(!state.still_follows(&step), "nor does repeat off wrap");
2168 }
2169
2170 #[test]
2171 fn repeating_one_steps_to_the_same_item_but_an_advance_moves_on() {
2172 let state = SharedPlayerState::new();
2173 let items: Vec<_> = ["a", "b"].map(ready_item).into();
2174 let (a, b) = (items[0].id, items[1].id);
2175 state.add_items(items);
2176 repeating(&state, Repeat::One);
2177
2178 let step = state.lookahead_after(a).unwrap();
2179 assert_eq!((step.next, step.wrapped), (Some(a), false));
2180 state.set_cursor(Some(a));
2181 assert_eq!(state.advance_cursor_loadable(), Some(b));
2182 assert_eq!(
2183 state.advance_cursor_loadable(),
2184 Some(a),
2185 "round, as repeating"
2186 );
2187 }
2188
2189 #[test]
2190 fn previous_from_the_first_item_wraps_only_while_repeating() {
2191 let state = SharedPlayerState::new();
2192 let items: Vec<_> = ["a", "b", "c"].map(ready_item).into();
2193 let (a, c) = (items[0].id, items[2].id);
2194 state.add_items(items);
2195
2196 state.set_cursor(Some(a));
2197 assert!(state.retreat_cursor().is_none());
2198 for repeat in [Repeat::Queue, Repeat::One] {
2199 repeating(&state, repeat);
2200 state.set_cursor(Some(a));
2201 assert_eq!(
2202 state.retreat_cursor().map(|(id, _)| id),
2203 Some(c),
2204 "{repeat:?}"
2205 );
2206 }
2207 }
2208
2209 #[test]
2210 fn a_removed_item_has_nothing_after_it_even_when_the_queue_repeats() {
2211 let state = SharedPlayerState::new();
2212 let items: Vec<_> = ["a", "b"].map(ready_item).into();
2213 let b = items[1].id;
2214 state.add_items(items);
2215 repeating(&state, Repeat::Queue);
2216
2217 state.set_cursor(Some(b));
2218 state.remove_item(b);
2219 assert!(state.lookahead_after(b).is_none());
2220 state.set_cursor(Some(b));
2221 assert_eq!(state.advance_cursor_loadable(), None);
2222 }
2223
2224 #[test]
2225 fn a_wrap_passes_over_failed_items() {
2226 let state = SharedPlayerState::new();
2227 let items = vec![failed_item("a"), ready_item("b"), ready_item("c")];
2228 let (b, c) = (items[1].id, items[2].id);
2229 state.add_items(items);
2230 repeating(&state, Repeat::Queue);
2231
2232 assert_eq!(state.lookahead_after(c).unwrap().next, Some(b));
2233 }
2234
2235 #[test]
2236 fn surviving_predecessor_skips_items_being_removed() {
2237 let state = SharedPlayerState::new();
2238 let items: Vec<_> = (0..4).map(|i| ready_item(&format!("track-{i}"))).collect();
2239 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
2240 state.add_items(items);
2241
2242 assert_eq!(
2244 state.surviving_item_before(ids[3], &ids[1..4]),
2245 Some(ids[0])
2246 );
2247 assert_eq!(state.surviving_item_before(ids[2], &ids), None);
2249 }
2250
2251 #[test]
2254 fn test_retreat_cursor_goes_to_previous_item() {
2255 let state = SharedPlayerState::new();
2256 let item0 = ready_item("track-0");
2257 let item1 = ready_item("track-1");
2258 let id0 = item0.id;
2259 let id1 = item1.id;
2260
2261 state.add_items(vec![item0, item1]);
2262 state.set_cursor(Some(id1));
2263
2264 let result = state.retreat_cursor();
2265 assert!(result.is_some(), "expected to retreat to previous item");
2266 assert_eq!(result.unwrap().0, id0, "should retreat to first item");
2267 assert_eq!(state.cursor(), Some(id0));
2268 }
2269
2270 #[test]
2271 fn test_retreat_cursor_returns_none_when_at_first_item() {
2272 let state = SharedPlayerState::new();
2273 let item0 = ready_item("only-track");
2274 let id0 = item0.id;
2275
2276 state.add_items(vec![item0]);
2277 state.set_cursor(Some(id0));
2278
2279 let result = state.retreat_cursor();
2280 assert!(result.is_none(), "cannot retreat before the first item");
2281 assert_eq!(state.cursor(), Some(id0));
2283 }
2284
2285 #[test]
2286 fn test_retreat_cursor_returns_none_when_cursor_is_unset() {
2287 let state = SharedPlayerState::new();
2288 state.add_items(vec![ready_item("track-0")]);
2289
2290 let result = state.retreat_cursor();
2291 assert!(
2292 result.is_none(),
2293 "retreat with no cursor should return None"
2294 );
2295 }
2296
2297 #[test]
2300 fn test_derive_visible_queue_statuses() {
2301 let state = SharedPlayerState::new();
2303 let item0 = ready_item("played-track");
2304 let item1 = ready_item("playing-track");
2305 let item2 = ready_item("queued-track");
2306 let id1 = item1.id;
2307
2308 state.add_items(vec![item0, item1, item2]);
2309 state.set_cursor(Some(id1));
2310
2311 let snap = state.derive_visible_queue();
2312
2313 assert_eq!(snap.entries.len(), 3);
2314 assert_eq!(snap.entries[0].status, QueueEntryStatus::Played);
2315 assert_eq!(snap.entries[1].status, QueueEntryStatus::Playing);
2316 assert_eq!(snap.entries[2].status, QueueEntryStatus::Queued);
2317 assert!(snap.has_playing);
2318 assert_eq!(snap.finished_count, 1);
2319 assert_eq!(snap.queue_count, 1);
2320 }
2321
2322 #[test]
2323 fn test_derive_visible_queue_downloading_statuses() {
2324 let state = SharedPlayerState::new();
2327 let (dl_cursor, _) = downloading_item(&state, "downloading-at-cursor", 1_000_000);
2328 let (dl_queued, _) = downloading_item(&state, "downloading-queued", 500_000);
2329 let id_cursor = dl_cursor.id;
2330
2331 state.add_items(vec![dl_cursor, dl_queued]);
2332 state.set_cursor(Some(id_cursor));
2333
2334 let snap = state.derive_visible_queue();
2335
2336 assert_eq!(snap.entries[0].status, QueueEntryStatus::Downloading);
2337 assert_eq!(snap.entries[1].status, QueueEntryStatus::Downloading);
2338 }
2339
2340 #[test]
2341 fn a_cursor_waiting_its_turn_is_priority_pending() {
2342 let state = SharedPlayerState::new();
2343 let item = pending_item("waiting");
2344 let id = item.id;
2345 state.add_items(vec![item]);
2346 state.set_cursor(Some(id));
2347
2348 let snap = state.derive_visible_queue();
2349 assert_eq!(snap.entries[0].status, QueueEntryStatus::PriorityPending);
2350 }
2351
2352 #[test]
2353 fn a_second_entry_for_a_track_reads_the_transfer_running_for_it() {
2354 let state = SharedPlayerState::new();
2357 let (first, bytes) = downloading_item(&state, "twice", 1_000_000);
2358 bytes.set(STREAM_THRESHOLD);
2359 let mut again = pending_item("twice");
2360 again.db_id = first.db_id;
2361 let again_id = again.id;
2362 state.add_items(vec![first, again]);
2363
2364 assert!(matches!(
2365 state.item_load_state(again_id),
2366 Some(LoadState::Downloading { .. })
2367 ));
2368 assert!(matches!(
2369 state.item_playback_source(again_id),
2370 Some(PlaybackSource::Streaming { .. })
2371 ));
2372 }
2373
2374 #[test]
2377 fn only_a_change_to_what_is_saved_moves_the_content_version() {
2378 let state = SharedPlayerState::new();
2379 let a = make_item("a", ItemState::Pending);
2380 let b = make_item("b", ItemState::Ready);
2381 let (a_id, b_id) = (a.id, b.id);
2382
2383 let start = state.content_version();
2384 state.add_items(vec![a, b]);
2385 let added = state.content_version();
2386 assert_ne!(added, start);
2387
2388 state.set_cursor(Some(a_id));
2389 state.update_item_state(a_id, ItemState::Ready);
2390 state.advance_cursor_loadable();
2391 state.retreat_cursor();
2392 assert_eq!(state.content_version(), added);
2393 assert_eq!(state.cursor_path(), Some(PathBuf::from("/music/a.flac")));
2394
2395 state.move_item_to(b_id, None);
2396 assert_ne!(state.content_version(), added);
2397 }
2398
2399 #[test]
2400 fn progress_follows_the_counter_without_touching_the_playlist() {
2401 let state = SharedPlayerState::new();
2405 let (item, bytes) = downloading_item(&state, "downloading", 1_000);
2406 state.add_items(vec![item]);
2407
2408 let version = state.playlist_version();
2409 bytes.set(250);
2410
2411 let snap = state.derive_visible_queue();
2412 assert_eq!(snap.entries[0].download_progress, Some((250, 1_000)));
2413 assert_eq!(
2414 state.playlist_version(),
2415 version,
2416 "progress must not read as a queue mutation"
2417 );
2418 assert_eq!(state.downloads().readings()[0].written, 250);
2419 }
2420
2421 #[test]
2422 fn test_derive_visible_queue_no_cursor_all_queued() {
2423 let state = SharedPlayerState::new();
2424 state.add_items(vec![ready_item("a"), ready_item("b"), ready_item("c")]);
2425
2426 let snap = state.derive_visible_queue();
2427
2428 assert_eq!(snap.entries.len(), 3);
2429 for entry in &snap.entries {
2430 assert_eq!(entry.status, QueueEntryStatus::Queued);
2431 }
2432 assert!(!snap.has_playing);
2433 assert_eq!(snap.finished_count, 0);
2434 assert_eq!(snap.queue_count, 3);
2435 }
2436
2437 fn make_album_item(title: &str, album: &str, album_artist: &str) -> PlaylistItem {
2440 PlaylistItem {
2441 playlist_entry_id: None,
2442 id: QueueItemId::new(),
2443 db_id: None,
2444 path: PathBuf::from(format!("/music/{title}.flac")),
2445 title: title.to_string(),
2446 artist: "Artist".to_string(),
2447 album_artist: album_artist.to_string(),
2448 album: album.to_string(),
2449 year: None,
2450 codec: Some("FLAC".to_string()),
2451 track_number: None,
2452 disc: None,
2453 duration_ms: Some(200_000),
2454 state: ItemState::Ready,
2455 pre_shuffle: None,
2456 }
2457 }
2458
2459 #[test]
2460 fn test_same_album_item_ids_returns_album_mates() {
2461 let state = SharedPlayerState::new();
2462 let a1 = make_album_item("A1", "Album A", "Artist A");
2463 let a2 = make_album_item("A2", "Album A", "Artist A");
2464 let b1 = make_album_item("B1", "Album B", "Artist B");
2465 let a3 = make_album_item("A3", "Album A", "Artist A");
2466
2467 let id_a1 = a1.id;
2468 let id_a2 = a2.id;
2469 let id_a3 = a3.id;
2470
2471 state.add_items(vec![a1, a2, b1, a3]);
2472
2473 let mates = state.same_album_item_ids(id_a1);
2474 assert_eq!(mates.len(), 2);
2475 assert!(mates.contains(&id_a2));
2476 assert!(mates.contains(&id_a3));
2477 }
2478
2479 #[test]
2480 fn test_same_album_item_ids_distinguishes_album_artists() {
2481 let state = SharedPlayerState::new();
2483 let a1 = make_album_item("A1", "Greatest Hits", "Artist A");
2484 let b1 = make_album_item("B1", "Greatest Hits", "Artist B");
2485
2486 let id_a1 = a1.id;
2487
2488 state.add_items(vec![a1, b1]);
2489
2490 let mates = state.same_album_item_ids(id_a1);
2491 assert!(mates.is_empty(), "different album_artist should not match");
2492 }
2493
2494 #[test]
2495 fn test_same_album_item_ids_unknown_id_returns_empty() {
2496 let state = SharedPlayerState::new();
2497 state.add_items(vec![ready_item("track-0")]);
2498
2499 let bogus = QueueItemId::new();
2500 let mates = state.same_album_item_ids(bogus);
2501 assert!(mates.is_empty());
2502 }
2503
2504 #[test]
2507 fn test_update_item_metadata_leaves_library_tags_alone() {
2508 let state = SharedPlayerState::new();
2509 let mut item = make_album_item("A1", "Nite Versions (mixed)", "Soulwax");
2510 item.db_id = Some(29615);
2511 let id = item.id;
2512 state.add_items(vec![item]);
2513
2514 state.update_item_metadata(
2515 id,
2516 "[unknown]".into(),
2517 "Soulwax".into(),
2518 "Soulwax".into(),
2519 "Nite Versions".into(),
2520 Some(54_000),
2521 );
2522
2523 let pl = state.playlist.read();
2524 assert_eq!(pl.items[0].album, "Nite Versions (mixed)");
2525 assert_eq!(pl.items[0].title, "A1");
2526 assert_eq!(pl.items[0].duration_ms, Some(54_000));
2527 }
2528
2529 #[test]
2530 fn test_update_item_metadata_fills_in_an_item_with_nothing_behind_it() {
2531 let state = SharedPlayerState::new();
2532 let item = make_album_item("A1", "", "");
2533 let id = item.id;
2534 state.add_items(vec![item]);
2535
2536 state.update_item_metadata(
2537 id,
2538 "Teachers".into(),
2539 "Soulwax".into(),
2540 "Soulwax".into(),
2541 "Nite Versions".into(),
2542 Some(148_000),
2543 );
2544
2545 let pl = state.playlist.read();
2546 assert_eq!(pl.items[0].title, "Teachers");
2547 assert_eq!(pl.items[0].album, "Nite Versions");
2548 assert_eq!(pl.items[0].duration_ms, Some(148_000));
2549 }
2550
2551 #[test]
2554 fn test_move_item_to_reorders_playlist() {
2555 let state = SharedPlayerState::new();
2557 let item_a = ready_item("A");
2558 let item_b = ready_item("B");
2559 let item_c = ready_item("C");
2560 let id_a = item_a.id;
2561 let id_b = item_b.id;
2562 let id_c = item_c.id;
2563
2564 state.add_items(vec![item_a, item_b, item_c]);
2565 state.move_item_to(id_c, Some(id_a));
2566
2567 let (items, _) = state.snapshot_playlist();
2568 let titles: Vec<&str> = items.iter().map(|i| i.title.as_str()).collect();
2569 assert_eq!(titles, vec!["A", "C", "B"]);
2570 assert_eq!(items[0].id, id_a);
2571 assert_eq!(items[1].id, id_c);
2572 assert_eq!(items[2].id, id_b);
2573 }
2574
2575 #[test]
2576 fn test_move_item_to_front_when_after_is_none() {
2577 let state = SharedPlayerState::new();
2579 let item_a = ready_item("A");
2580 let item_b = ready_item("B");
2581 let item_c = ready_item("C");
2582 let id_c = item_c.id;
2583
2584 state.add_items(vec![item_a, item_b, item_c]);
2585 state.move_item_to(id_c, None);
2586
2587 let (items, _) = state.snapshot_playlist();
2588 let titles: Vec<&str> = items.iter().map(|i| i.title.as_str()).collect();
2589 assert_eq!(titles, vec!["C", "A", "B"]);
2590 }
2591
2592 #[test]
2595 fn test_move_items_batch_preserves_relative_order() {
2596 let state = SharedPlayerState::new();
2598 let item_a = ready_item("A");
2599 let item_b = ready_item("B");
2600 let item_c = ready_item("C");
2601 let item_d = ready_item("D");
2602 let id_a = item_a.id;
2603 let id_b = item_b.id;
2604 let id_c = item_c.id;
2605 let id_d = item_d.id;
2606
2607 state.add_items(vec![item_a, item_b, item_c, item_d]);
2608 state.move_items(&[id_a, id_c], id_d, true);
2609
2610 let (items, _) = state.snapshot_playlist();
2611 let titles: Vec<&str> = items.iter().map(|i| i.title.as_str()).collect();
2612 assert_eq!(titles, vec!["B", "D", "A", "C"]);
2613 assert_eq!(items[0].id, id_b);
2614 assert_eq!(items[1].id, id_d);
2615 assert_eq!(items[2].id, id_a);
2616 assert_eq!(items[3].id, id_c);
2617 }
2618
2619 #[test]
2622 fn test_pending_downloads_collects_pending_with_db_id() {
2623 let state = SharedPlayerState::new();
2624 let mut item_a = ready_item("local");
2625 item_a.db_id = None;
2626
2627 let mut item_b = pending_item("remote-1");
2628 item_b.db_id = Some(10);
2629 let id_b = item_b.id;
2630
2631 let mut item_c = ready_item("cached");
2632 item_c.db_id = Some(20);
2633
2634 let mut item_d = pending_item("remote-2");
2635 item_d.db_id = Some(30);
2636 let id_d = item_d.id;
2637
2638 let item_e = pending_item("orphan");
2640
2641 state.add_items(vec![item_a, item_b, item_c, item_d, item_e]);
2642
2643 let pending = state.pending_downloads();
2644 assert_eq!(pending.len(), 2);
2645 assert_eq!(pending[0], (10, id_b));
2646 assert_eq!(pending[1], (30, id_d));
2647
2648 state.set_cursor(Some(id_d));
2651 assert_eq!(state.pending_downloads(), vec![(30, id_d), (10, id_b)]);
2652 }
2653
2654 #[test]
2655 fn test_item_db_id_and_load_state() {
2656 let state = SharedPlayerState::new();
2657 let mut item = pending_item("track");
2658 item.db_id = Some(42);
2659 let id = item.id;
2660 state.add_items(vec![item]);
2661
2662 assert_eq!(state.item_db_id(id), Some(42));
2663 assert!(matches!(
2664 state.item_load_state(id),
2665 Some(LoadState::Pending)
2666 ));
2667
2668 state.update_item_state(id, ItemState::Ready);
2669 assert!(matches!(state.item_load_state(id), Some(LoadState::Ready)));
2670 }
2671}