1use std::collections::HashMap;
15use std::path::PathBuf;
16use std::sync::Arc;
17use std::sync::atomic::{AtomicU64, Ordering};
18use std::time::{Duration, Instant};
19
20use crate::player::state::QueueItemId;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum DownloadState {
25 Queued,
27 Running,
29 Done,
31 Failed(String),
34}
35
36impl DownloadState {
37 pub fn is_settled(&self) -> bool {
38 matches!(self, Self::Done | Self::Failed(_))
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct Download {
45 pub id: QueueItemId,
48 pub track_id: i64,
49 pub title: String,
50 pub artist: String,
51 pub source: PathBuf,
53 pub dest: PathBuf,
55 pub total: u64,
57 pub written: Arc<ByteFeed>,
59 pub state: DownloadState,
60 pub bytes_per_second: u64,
64}
65
66impl Download {
67 pub fn fraction(&self) -> Option<f64> {
69 (self.total > 0)
70 .then(|| self.written.load(Ordering::Relaxed) as f64 / self.total as f64)
71 .map(|f| f.clamp(0.0, 1.0))
72 }
73
74 pub fn bytes_written(&self) -> u64 {
75 self.written.load(Ordering::Relaxed)
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct Phase {
82 pub state: PhaseKind,
83 pub written: u64,
84 pub total: u64,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum PhaseKind {
89 Queued,
90 Running,
91 Done,
92 Failed,
93}
94
95impl Phase {
96 pub fn is_running(&self) -> bool {
97 matches!(self.state, PhaseKind::Queued | PhaseKind::Running)
98 }
99}
100
101#[derive(Debug, Default)]
110pub struct ByteFeed {
111 written: AtomicU64,
112 at: parking_lot::Mutex<()>,
116 more: parking_lot::Condvar,
117}
118
119impl ByteFeed {
120 pub fn new() -> Arc<Self> {
121 Arc::new(Self::default())
122 }
123
124 pub fn load(&self, order: Ordering) -> u64 {
126 self.written.load(order)
127 }
128
129 pub fn set(&self, bytes: u64) {
131 let _at = self.at.lock();
132 self.written.store(bytes, Ordering::Release);
133 self.more.notify_all();
134 }
135
136 pub fn advance(&self, bytes: u64) {
138 let _at = self.at.lock();
139 self.written.fetch_add(bytes, Ordering::Release);
140 self.more.notify_all();
141 }
142
143 pub fn done(&self) {
146 let _at = self.at.lock();
147 self.more.notify_all();
148 }
149
150 pub fn wait_past(&self, seen: u64, deadline: Instant) -> u64 {
158 let mut at = self.at.lock();
159 let written = self.written.load(Ordering::Acquire);
160 if written > seen {
161 return written;
162 }
163 let Some(left) = deadline.checked_duration_since(Instant::now()) else {
164 return written;
165 };
166 self.more.wait_for(&mut at, left);
167 self.written.load(Ordering::Acquire)
168 }
169}
170
171#[derive(Debug, Default)]
173pub struct DownloadStore {
174 entries: parking_lot::RwLock<Vec<Download>>,
175 version: AtomicU64,
176 samples: parking_lot::Mutex<HashMap<QueueItemId, Sample>>,
180 last_sample: parking_lot::Mutex<Option<Instant>>,
187 figures: AtomicU64,
188 settled_limit: usize,
191}
192
193impl DownloadStore {
194 pub fn new() -> Arc<Self> {
195 Arc::new(Self {
196 entries: parking_lot::RwLock::new(Vec::new()),
197 version: AtomicU64::new(0),
198 samples: parking_lot::Mutex::new(HashMap::new()),
199 last_sample: parking_lot::Mutex::new(None),
200 figures: AtomicU64::new(0),
201 settled_limit: 50,
202 })
203 }
204
205 pub fn version(&self) -> u64 {
209 self.version.load(Ordering::Acquire)
210 }
211
212 pub fn figures(&self) -> u64 {
216 self.figures.load(Ordering::Acquire)
217 }
218
219 pub fn all(&self) -> Vec<Download> {
221 self.entries.read().clone()
222 }
223
224 pub fn get(&self, id: QueueItemId) -> Option<Download> {
231 self.entries.read().iter().find(|d| d.id == id).cloned()
232 }
233
234 pub fn phase_of(&self, id: QueueItemId) -> Option<Phase> {
238 self.entries
239 .read()
240 .iter()
241 .find(|d| d.id == id)
242 .map(|d| Phase {
243 state: match &d.state {
244 DownloadState::Queued => PhaseKind::Queued,
245 DownloadState::Running => PhaseKind::Running,
246 DownloadState::Done => PhaseKind::Done,
247 DownloadState::Failed(_) => PhaseKind::Failed,
248 },
249 written: d.bytes_written(),
250 total: d.total,
251 })
252 }
253
254 pub fn active(&self) -> usize {
256 self.entries
257 .read()
258 .iter()
259 .filter(|d| !d.state.is_settled())
260 .count()
261 }
262
263 pub fn queued(&self, download: Download) {
267 let mut entries = self.entries.write();
268 entries.retain(|d| d.id != download.id);
269 entries.insert(0, download);
270 drop(entries);
271 self.settle();
272 }
273
274 pub fn started(&self, id: QueueItemId, total: u64, written: Arc<ByteFeed>) {
276 let mut entries = self.entries.write();
277 if let Some(entry) = entries.iter_mut().find(|d| d.id == id) {
278 entry.total = total;
279 entry.written = written;
280 entry.state = DownloadState::Running;
281 }
282 drop(entries);
283 self.bump();
284 }
285
286 pub fn finished(&self, id: QueueItemId) {
288 self.settle_one(id, DownloadState::Done);
289 }
290
291 pub fn failed(&self, id: QueueItemId, reason: String) {
293 self.settle_one(id, DownloadState::Failed(reason));
294 }
295
296 pub fn clear_settled(&self) {
299 let mut entries = self.entries.write();
300 let before = entries.len();
301 entries.retain(|d| !d.state.is_settled());
302 let changed = entries.len() != before;
303 drop(entries);
304 if changed {
305 self.bump();
306 }
307 }
308
309 fn settle_one(&self, id: QueueItemId, state: DownloadState) {
310 let mut entries = self.entries.write();
311 if let Some(entry) = entries.iter_mut().find(|d| d.id == id) {
312 entry.state = state;
313 entry.bytes_per_second = 0;
317 }
318 drop(entries);
319 self.samples.lock().remove(&id);
320 self.figures.fetch_add(1, Ordering::Release);
321 self.settle();
323 }
324
325 fn settle(&self) {
327 let mut entries = self.entries.write();
328 let (mut running, settled): (Vec<_>, Vec<_>) =
330 entries.drain(..).partition(|d| !d.state.is_settled());
331 running.extend(settled.into_iter().take(self.settled_limit));
332 *entries = running;
333 drop(entries);
334 self.bump();
335 }
336
337 fn bump(&self) {
338 self.version.fetch_add(1, Ordering::Release);
339 crate::signal::engine_changed().bump();
340 }
341}
342
343#[derive(Debug)]
345struct Sample {
346 at: Instant,
347 bytes: u64,
348 bps: f64,
350}
351
352const RATE_SMOOTHING: f64 = 0.3;
356
357const MIN_SAMPLE_GAP: Duration = Duration::from_millis(250);
360
361impl DownloadStore {
362 pub fn progressed(&self) {
378 let now = Instant::now();
379 {
380 let mut last = self.last_sample.lock();
381 if last.is_some_and(|at| now.saturating_duration_since(at) < MIN_SAMPLE_GAP) {
382 return;
383 }
384 *last = Some(now);
385 }
386 self.sample_rates_at(now);
387 }
388
389 fn sample_rates_at(&self, now: Instant) {
390 let mut entries = self.entries.write();
391 let mut samples = self.samples.lock();
392
393 for entry in entries.iter_mut() {
394 if entry.state.is_settled() {
395 entry.bytes_per_second = 0;
396 samples.remove(&entry.id);
397 continue;
398 }
399 let bytes = entry.written.load(Ordering::Relaxed);
400 match samples.get_mut(&entry.id) {
401 Some(previous) => {
402 let elapsed = now.saturating_duration_since(previous.at);
403 if elapsed < MIN_SAMPLE_GAP {
404 entry.bytes_per_second = previous.bps as u64;
405 continue;
406 }
407 let moved = bytes.saturating_sub(previous.bytes) as f64;
408 let instant = moved / elapsed.as_secs_f64();
409 previous.bps = previous.bps * (1.0 - RATE_SMOOTHING) + instant * RATE_SMOOTHING;
410 previous.at = now;
411 previous.bytes = bytes;
412 entry.bytes_per_second = previous.bps as u64;
413 }
414 None => {
415 samples.insert(
416 entry.id,
417 Sample {
418 at: now,
419 bytes,
420 bps: 0.0,
421 },
422 );
423 entry.bytes_per_second = 0;
424 }
425 }
426 }
427
428 let live: std::collections::HashSet<QueueItemId> = entries.iter().map(|e| e.id).collect();
430 samples.retain(|id, _| live.contains(id));
431
432 self.figures.fetch_add(1, Ordering::Release);
435 crate::signal::engine_changed().bump();
436 }
437}
438
439pub fn store() -> &'static Arc<DownloadStore> {
445 static STORE: std::sync::OnceLock<Arc<DownloadStore>> = std::sync::OnceLock::new();
446 STORE.get_or_init(DownloadStore::new)
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452
453 fn download(title: &str) -> Download {
454 Download {
455 id: QueueItemId::new(),
456 track_id: 1,
457 title: title.into(),
458 artist: "Artist".into(),
459 source: PathBuf::from(format!("/cache/{title}.opus.part")),
460 dest: PathBuf::from(format!("/cache/{title}.opus")),
461 total: 0,
462 written: ByteFeed::new(),
463 state: DownloadState::Queued,
464 bytes_per_second: 0,
465 }
466 }
467
468 #[test]
469 fn a_transfer_runs_then_settles() {
470 let store = DownloadStore::new();
471 let entry = download("train");
472 let id = entry.id;
473 store.queued(entry);
474 assert_eq!(store.active(), 1);
475
476 let written = ByteFeed::new();
477 store.started(id, 400, written.clone());
478 written.set(100);
479 assert_eq!(store.all()[0].fraction(), Some(0.25));
480
481 store.finished(id);
482 assert_eq!(store.active(), 0);
483 assert_eq!(store.all()[0].state, DownloadState::Done);
484 }
485
486 #[test]
487 fn progress_does_not_move_the_version() {
488 let store = DownloadStore::new();
492 let entry = download("train");
493 let id = entry.id;
494 store.queued(entry);
495 let written = ByteFeed::new();
496 store.started(id, 1000, written.clone());
497
498 let before = store.version();
499 written.set(500);
500 assert_eq!(store.version(), before);
501 assert_eq!(store.all()[0].bytes_written(), 500);
502 }
503
504 #[test]
505 fn no_content_length_means_no_fraction() {
506 let store = DownloadStore::new();
508 let entry = download("chunked");
509 let id = entry.id;
510 store.queued(entry);
511 store.started(id, 0, {
512 let feed = ByteFeed::new();
513 feed.set(9000);
514 feed
515 });
516 assert_eq!(store.all()[0].fraction(), None);
517 assert_eq!(store.all()[0].bytes_written(), 9000);
518 }
519
520 #[test]
521 fn fetching_the_same_item_again_restarts_its_row() {
522 let store = DownloadStore::new();
525 let first = download("train");
526 let id = first.id;
527 store.queued(first);
528 store.finished(id);
529
530 let mut again = download("train");
531 again.id = id;
532 store.queued(again);
533
534 assert_eq!(store.all().len(), 1);
535 assert_eq!(store.all()[0].state, DownloadState::Queued);
536 }
537
538 #[test]
539 fn running_transfers_sort_above_settled_ones() {
540 let store = DownloadStore::new();
541 let done = download("done");
542 let done_id = done.id;
543 store.queued(done);
544 let running = download("running");
545 store.queued(running);
546 store.finished(done_id);
547
548 let all = store.all();
549 assert_eq!(all[0].title, "running");
550 assert_eq!(all[1].title, "done");
551 }
552
553 #[test]
554 fn a_failure_keeps_its_reason() {
555 let store = DownloadStore::new();
556 let entry = download("gone");
557 let id = entry.id;
558 store.queued(entry);
559 store.failed(id, "server returned 404".into());
560 assert_eq!(
561 store.all()[0].state,
562 DownloadState::Failed("server returned 404".into())
563 );
564 }
565
566 #[test]
567 fn a_rate_needs_two_readings_and_a_gap_between_them() {
568 let store = DownloadStore::new();
569 let entry = download("train");
570 let (id, written) = (entry.id, entry.written.clone());
571 store.queued(entry);
572 store.started(id, 1_000_000, written.clone());
573
574 let start = Instant::now();
575 store.sample_rates_at(start);
576 assert_eq!(
577 store.all()[0].bytes_per_second,
578 0,
579 "one reading is not a rate"
580 );
581
582 written.set(100_000);
584 store.sample_rates_at(start + Duration::from_millis(50));
585 assert_eq!(store.all()[0].bytes_per_second, 0);
586
587 store.sample_rates_at(start + Duration::from_secs(1));
589 let bps = store.all()[0].bytes_per_second;
590 assert!(bps > 0, "a rate should have been worked out, got {bps}");
591 assert!(bps < 100_000, "and smoothed rather than taken whole: {bps}");
592 }
593
594 #[test]
595 fn a_settled_transfer_has_no_rate() {
596 let store = DownloadStore::new();
598 let entry = download("train");
599 let (id, written) = (entry.id, entry.written.clone());
600 store.queued(entry);
601 store.started(id, 1000, written.clone());
602 let start = Instant::now();
603 store.sample_rates_at(start);
604 written.set(500);
605 store.sample_rates_at(start + Duration::from_secs(1));
606 assert!(store.all()[0].bytes_per_second > 0);
607
608 store.finished(id);
609 store.sample_rates_at(start + Duration::from_secs(2));
610 assert_eq!(store.all()[0].bytes_per_second, 0);
611 }
612
613 #[test]
614 fn a_transfer_can_be_found_by_its_queue_item() {
615 let store = DownloadStore::new();
616 let entry = download("train");
617 let (id, written) = (entry.id, entry.written.clone());
618 store.queued(entry);
619 store.started(id, 400, written.clone());
620 written.set(100);
621
622 let phase = store.phase_of(id).expect("the transfer is there");
623 assert_eq!(phase.state, PhaseKind::Running);
624 assert_eq!(phase.written, 100);
625 assert_eq!(phase.total, 400);
626 assert!(phase.is_running());
627
628 assert!(
629 store.phase_of(QueueItemId::new()).is_none(),
630 "and only that one"
631 );
632 }
633
634 #[test]
635 fn clearing_settled_leaves_the_running_alone() {
636 let store = DownloadStore::new();
637 let done = download("done");
638 let done_id = done.id;
639 store.queued(done);
640 store.queued(download("running"));
641 store.finished(done_id);
642
643 store.clear_settled();
644 let all = store.all();
645 assert_eq!(all.len(), 1);
646 assert_eq!(all[0].title, "running");
647 }
648}