1use std::sync::PoisonError;
2
3use kithara_bufpool::HasPool;
4use kithara_events::TrackId;
5use kithara_play::SelectionPlayback;
6use smallvec::SmallVec;
7
8use super::{
9 QueueControl,
10 types::{
11 CachedPosition, CrossfadeArm, PendingSelect, Placement, SelectPhase, Transition,
12 extract_track_name,
13 },
14};
15use crate::{
16 attempts::LoadClass,
17 error::QueueError,
18 event::{AdvanceReason, QueueEvent},
19 navigation::{NavigationState, PlaybackOrder},
20 track::{TrackRecord, TrackSource},
21};
22
23impl<S> QueueControl<S>
24where
25 S: HasPool<u8> + HasPool<f32> + Send + Sync + 'static,
26{
27 pub fn append<T: Into<TrackSource<S>>>(&self, source: T) -> Result<TrackId, QueueError> {
36 let source = source.into();
37 self.with_open(|queue| queue.insert_entry(TrackId::allocate(), source, Placement::Append))
38 .map_err(QueueError::from)
39 }
40
41 pub fn append_with_id<T: Into<TrackSource<S>>>(
51 &self,
52 id: TrackId,
53 source: T,
54 ) -> Result<TrackId, QueueError> {
55 let source = source.into();
56 self.with_open(|queue| queue.insert_entry(id, source, Placement::Append))
57 .map_err(QueueError::from)
58 }
59
60 pub fn clear(&self) {
63 self.command(Self::clear_inner);
64 }
65
66 fn clear_inner(&self) {
67 let ids: Vec<TrackId> = {
68 let _apply = self.lock_select_apply();
69 let mut guard = self.lock_tracks_mut();
70 let ids = guard.iter().map(|r| r.id).collect();
71 guard.clear();
72 drop(guard);
73
74 *self.lock_pending_select_mut() = SelectPhase::Idle;
75 let mut navigation = self.lock_navigation_mut();
76 let repeat = navigation.repeat_mode();
77 let order = navigation.playback_order();
78 *navigation = NavigationState::new(navigation.history_limit());
79 navigation.set_repeat(repeat);
80 navigation.set_playback_order(order, &[]);
81 drop(navigation);
82 self.write_armed_for(CrossfadeArm::Disarmed);
83 self.write_cached_position(CachedPosition::Unknown);
84 self.autoplay_target.store(CrossfadeArm::Disarmed);
85 self.player.remove_all_items();
86 ids
87 };
88 *self
89 .player_rx
90 .lock()
91 .unwrap_or_else(PoisonError::into_inner) = self.bus.subscribe();
92 for id in ids {
93 self.bus.publish(QueueEvent::TrackRemoved { id });
94 }
95 }
96
97 pub fn insert<T: Into<TrackSource<S>>>(
104 &self,
105 source: T,
106 after: Option<TrackId>,
107 ) -> Result<TrackId, QueueError> {
108 let source = source.into();
109 self.with_open_result(|queue| {
110 queue.insert_with_id_inner(TrackId::allocate(), source, after)
111 })
112 }
113
114 pub(super) fn insert_entry(
116 &self,
117 id: TrackId,
118 source: TrackSource<S>,
119 placement: Placement,
120 ) -> TrackId {
121 let record = TrackRecord::new(id, extract_track_name(&source), source.clone());
122 if self.current().is_none() && self.autoplay_target.arm_if_disarmed(id) {
123 self.override_pending_select(PendingSelect {
124 id,
125 settings: Transition::None.settings(self.crossfade_settings()),
126 playback: if self.should_autoplay {
127 SelectionPlayback::Play
128 } else {
129 SelectionPlayback::Pause
130 },
131 reason: AdvanceReason::InitialLoad,
132 });
133 }
134
135 let index = {
136 let mut guard = self.lock_tracks_mut();
137 match placement {
138 Placement::Append => {
139 guard.push(record);
140 guard.len() - 1
141 }
142 Placement::At(pos) => {
143 guard.insert(pos, record);
144 pos
145 }
146 }
147 };
148 self.player.reserve_slots(self.len());
149 self.bus.publish(QueueEvent::TrackAdded { id, index });
150 let ids = self
151 .tracks()
152 .into_iter()
153 .map(|track| track.id)
154 .collect::<SmallVec<[_; 16]>>();
155 let mut navigation = self.lock_navigation_mut();
156 navigation.reconcile(&ids);
157 navigation.insert(id);
158 drop(navigation);
159 self.spawn_apply_after_load(id, source, LoadClass::Prefetch);
160 id
161 }
162
163 pub fn insert_with_id<T: Into<TrackSource<S>>>(
171 &self,
172 id: TrackId,
173 source: T,
174 after: Option<TrackId>,
175 ) -> Result<TrackId, QueueError> {
176 let source = source.into();
177 self.with_open_result(|queue| queue.insert_with_id_inner(id, source, after))
178 }
179
180 fn insert_with_id_inner(
181 &self,
182 id: TrackId,
183 source: TrackSource<S>,
184 after: Option<TrackId>,
185 ) -> Result<TrackId, QueueError> {
186 let pos = {
187 let guard = self.lock_tracks();
188 match after {
189 None => 0,
190 Some(after_id) => guard
191 .iter()
192 .position(|e| e.id == after_id)
193 .map(|i| i + 1)
194 .ok_or(QueueError::UnknownTrackId(after_id))?,
195 }
196 };
197 Ok(self.insert_entry(id, source, Placement::At(pos)))
198 }
199
200 pub fn remove(&self, id: TrackId) -> Result<(), QueueError> {
210 self.with_open_result(|queue| queue.remove_inner(id))
211 }
212
213 fn remove_inner(&self, id: TrackId) -> Result<(), QueueError> {
214 let was_current = self.current().map(|e| e.id) == Some(id);
215 let playback = if self.player.is_playing() {
216 SelectionPlayback::Play
217 } else {
218 SelectionPlayback::Pause
219 };
220 let successor_id = if was_current {
221 let guard = self.lock_tracks();
222 let pos = guard.iter().position(|e| e.id == id);
223 let result = pos.and_then(|p| {
224 let next = guard.get(p + 1);
225 let prev = if p > 0 { guard.get(p - 1) } else { None };
226 next.or(prev).map(|e| e.id)
227 });
228 drop(guard);
229 result
230 } else {
231 None
232 };
233
234 let index = {
235 let mut guard = self.lock_tracks_mut();
236 let pos = guard
237 .iter()
238 .position(|e| e.id == id)
239 .ok_or(QueueError::UnknownTrackId(id))?;
240 guard.remove(pos);
241 pos
242 };
243 let _ = self.player.remove_at(index)?;
244 self.bus.publish(QueueEvent::TrackRemoved { id });
245
246 let entries = self.tracks();
247 let ids = entries
248 .iter()
249 .map(|entry| entry.id)
250 .collect::<SmallVec<[_; 16]>>();
251 let order = self.lock_navigation().playback_order();
252 self.lock_navigation_mut().reconcile(&ids);
253
254 if was_current {
255 let replacement = match order {
256 PlaybackOrder::Sequential => {
257 successor_id.filter(|candidate| ids.contains(candidate))
258 }
259 PlaybackOrder::Shuffle => self.lock_navigation_mut().next(&ids, false, false),
260 };
261 if let Some(next) = replacement {
262 self.select_with(
263 next,
264 Transition::None,
265 AdvanceReason::RemovedCurrent,
266 playback,
267 )?;
268 } else {
269 self.player.pause();
270 }
271 }
272 Ok(())
273 }
274
275 pub fn set_tracks<I, T>(&self, sources: I)
277 where
278 I: IntoIterator<Item = T>,
279 T: Into<TrackSource<S>>,
280 {
281 self.command(|queue| {
282 queue.clear_inner();
283 for source in sources {
284 queue.insert_entry(TrackId::allocate(), source.into(), Placement::Append);
285 }
286 });
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use kithara_platform::sync::Arc;
293 use kithara_play::{ItemRole, PlayerEvent, SlotId, TrackRef};
294 use kithara_test_utils::kithara;
295
296 use super::*;
297 use crate::{
298 event::QueueEvent,
299 queue::state::tests::{make_queue, wait_for_queue_event},
300 };
301
302 fn append(queue: &crate::Queue<crate::test_pools::TestPools>, source: &str) -> TrackId {
303 queue
304 .append(source)
305 .expect("BUG: open queue must accept a track")
306 }
307
308 #[kithara::test(tokio)]
309 async fn len_is_empty_reflect_append() {
310 let queue = make_queue();
311 assert!(queue.is_empty());
312 let _ = append(&queue, "https://example.com/a.mp3");
313 let _ = append(&queue, "https://example.com/b.mp3");
314 assert_eq!(queue.len(), 2);
315 }
316
317 #[kithara::test(tokio)]
318 async fn append_returns_monotonic_ids_and_emits_track_added() {
319 let queue = make_queue();
320 let mut rx = queue.subscribe();
321 let a = append(&queue, "https://example.com/a.mp3");
322 let b = append(&queue, "https://example.com/b.mp3");
323 assert_ne!(a, b);
324 assert!(a.as_u64() < b.as_u64());
325
326 let mut seen = 0;
327 while wait_for_queue_event(
328 &mut rx,
329 |ev| matches!(ev, QueueEvent::TrackAdded { .. }),
330 200,
331 )
332 .await
333 {
334 seen += 1;
335 if seen == 2 {
336 break;
337 }
338 }
339 assert_eq!(seen, 2);
340 }
341
342 #[kithara::test(tokio)]
343 async fn remove_drops_from_queue_and_emits() {
344 let queue = make_queue();
345 let a = append(&queue, "https://example.com/a.mp3");
346 let _b = append(&queue, "https://example.com/b.mp3");
347 let mut rx = queue.subscribe();
348
349 queue
350 .remove(a)
351 .expect("BUG: just-appended track must be removable");
352 assert_eq!(queue.len(), 1);
353 let saw_removed = wait_for_queue_event(
354 &mut rx,
355 |ev| matches!(ev, QueueEvent::TrackRemoved { id } if id == &a),
356 300,
357 )
358 .await;
359 assert!(saw_removed);
360 }
361
362 #[kithara::test(tokio)]
363 async fn clear_empties_queue() {
364 let queue = make_queue();
365 let _a = append(&queue, "https://example.com/a.mp3");
366 let _b = append(&queue, "https://example.com/b.mp3");
367 assert_eq!(queue.len(), 2);
368 queue.clear();
369 assert_eq!(queue.len(), 0);
370 }
371
372 #[kithara::test(tokio)]
373 async fn clear_discards_old_eof_before_reinsert() {
374 let queue = make_queue();
375 let old = queue
376 .append("https://example.com/old.mp3")
377 .expect("open queue accepts a track");
378 queue.lock_navigation_mut().select(old, &[old]);
379 queue.player.bus().publish(PlayerEvent::ItemDidPlayToEnd {
380 item: ItemRole::Leading(TrackRef::new(
381 old,
382 SlotId::new(0),
383 Arc::from(format!("test://memory/{}", old.as_u64())),
384 )),
385 });
386
387 queue.clear();
388 let replacement = queue
389 .append("https://example.com/replacement.mp3")
390 .expect("open queue accepts a replacement track");
391 queue
392 .lock_navigation_mut()
393 .select(replacement, &[replacement]);
394 queue.player.set_rate(1.0);
395
396 queue
397 .tick()
398 .expect("tick must accept a freshly reinserted queue");
399
400 assert_eq!(
401 queue.current().map(|entry| entry.id),
402 Some(replacement),
403 "an EOF queued before clear must not end the replacement queue"
404 );
405 }
406
407 #[kithara::test(tokio)]
408 async fn set_tracks_replaces_queue() {
409 let queue = make_queue();
410 let _a = append(&queue, "https://example.com/a.mp3");
411 queue.set_tracks([
412 "https://example.com/1.mp3",
413 "https://example.com/2.mp3",
414 "https://example.com/3.mp3",
415 ]);
416 assert_eq!(queue.len(), 3);
417 }
418
419 #[kithara::test(tokio)]
420 async fn insert_after_id_places_next() {
421 let queue = make_queue();
422 let a = append(&queue, "https://example.com/a.mp3");
423 let b = append(&queue, "https://example.com/b.mp3");
424 let mid = queue
425 .insert("https://example.com/mid.mp3", Some(a))
426 .expect("BUG: insert relative to existing track");
427 let snapshot = queue.tracks();
428 let ids: Vec<TrackId> = snapshot.iter().map(|e| e.id).collect();
429 assert_eq!(ids, vec![a, mid, b]);
430 }
431
432 #[kithara::test(tokio)]
433 async fn track_source_is_keyed_by_id_across_removal() {
434 let queue = make_queue();
435 let a = append(&queue, "https://example.com/a.mp3");
436 let b = append(&queue, "https://example.com/b.mp3");
437
438 assert_eq!(
439 queue
440 .track_source(a)
441 .and_then(|s| s.uri().map(str::to_string)),
442 Some("https://example.com/a.mp3".to_string()),
443 "source resolves by identity"
444 );
445
446 queue.remove(a).expect("BUG: remove existing track");
449 assert!(
450 queue.track_source(a).is_none(),
451 "removed track has no source"
452 );
453 assert_eq!(
454 queue
455 .track_source(b)
456 .and_then(|s| s.uri().map(str::to_string)),
457 Some("https://example.com/b.mp3".to_string()),
458 "surviving track still resolves to its own source by id"
459 );
460 }
461}