use std::collections::VecDeque;
use std::path::PathBuf;
use mineral_model::{BitRate, SongId};
use mineral_protocol::{
DownloadFailure, DownloadId, DownloadStatus, DownloadSummary, DownloadWave, SongDownloadView,
};
use rustc_hash::FxHashMap;
use tokio_util::sync::CancellationToken;
const TERMINAL_LIMIT: usize = 100;
pub(super) struct ManagerState {
pub(super) rows: FxHashMap<DownloadId, SongDownload>,
pub(super) direct_queue: VecDeque<DownloadId>,
pub(super) playlist_queue: VecDeque<DownloadId>,
pub(super) active: FxHashMap<DownloadId, CancellationToken>,
pub(super) dedup: FxHashMap<DownloadKey, DownloadId>,
pub(super) terminal: VecDeque<DownloadId>,
pub(super) next_admission: u64,
pub(super) quality: BitRate,
pub(super) max_concurrent: usize,
pub(super) preparing_playlists: usize,
pub(super) wave_open: bool,
pub(super) wave: DownloadWave,
pub(super) latest_wave: Option<DownloadWave>,
}
pub(super) struct SongDownload {
pub(super) view: SongDownloadView,
pub(super) key: DownloadKey,
pub(super) admission_order: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(super) struct DownloadKey {
pub(super) export_root: Option<PathBuf>,
pub(super) song_id: SongId,
pub(super) quality: BitRate,
}
impl ManagerState {
pub(super) fn new(quality: BitRate, max_concurrent: usize) -> Self {
Self {
rows: FxHashMap::default(),
direct_queue: VecDeque::new(),
playlist_queue: VecDeque::new(),
active: FxHashMap::default(),
dedup: FxHashMap::default(),
terminal: VecDeque::new(),
next_admission: 0,
quality,
max_concurrent,
preparing_playlists: 0,
wave_open: false,
wave: DownloadWave::default(),
latest_wave: None,
}
}
pub(super) fn summary(&self) -> DownloadSummary {
let queued = self
.rows
.values()
.filter(|row| row.view.status == DownloadStatus::Queued)
.count();
let speed_bps = self
.rows
.values()
.filter(|row| row.view.status == DownloadStatus::Downloading)
.fold(0u64, |sum, row| sum.saturating_add(row.view.speed_bps));
DownloadSummary {
active: self.active.len(),
queued,
preparing_playlists: self.preparing_playlists,
speed_bps,
latest_wave: self.latest_wave.clone(),
}
}
pub(super) fn snapshot(&self) -> Vec<SongDownloadView> {
let mut rows = self
.rows
.values()
.filter(|row| !row.view.status.terminal())
.collect::<Vec<_>>();
rows.sort_by_key(|row| {
(
row.view.status == DownloadStatus::Queued,
row.admission_order,
)
});
rows.into_iter()
.chain(
self.terminal
.iter()
.rev()
.filter_map(|id| self.rows.get(id)),
)
.map(|row| row.view.clone())
.collect()
}
pub(super) fn open_wave(&mut self) {
if !self.wave_open {
self.wave_open = true;
self.wave = DownloadWave::default();
}
}
pub(super) fn mark_terminal(
&mut self,
id: &DownloadId,
status: DownloadStatus,
failure: Option<DownloadFailure>,
) {
let Some(row) = self.rows.get_mut(id) else {
return;
};
if row.view.status.terminal() {
return;
}
row.view.status = status;
row.view.speed_bps = 0;
row.view.failure = failure;
let key = row.key.clone();
self.dedup.remove(&key);
self.terminal.push_back(id.clone());
match status {
DownloadStatus::Downloaded => {
self.wave.downloaded = self.wave.downloaded.saturating_add(1);
}
DownloadStatus::AlreadyPresent => {
self.wave.already_present = self.wave.already_present.saturating_add(1);
}
DownloadStatus::SkippedByHook => {
self.wave.skipped_by_hook = self.wave.skipped_by_hook.saturating_add(1);
}
DownloadStatus::Failed => {
self.wave.failed = self.wave.failed.saturating_add(1);
}
DownloadStatus::Stopped => {
self.wave.stopped = self.wave.stopped.saturating_add(1);
}
DownloadStatus::Queued
| DownloadStatus::Resolving
| DownloadStatus::Downloading
| DownloadStatus::Finalizing
| DownloadStatus::Stopping => {}
}
while self.terminal.len() > TERMINAL_LIMIT {
if let Some(oldest) = self.terminal.pop_front() {
self.rows.remove(&oldest);
}
}
}
pub(super) fn finish_wave_if_idle(&mut self) {
let queued = self
.rows
.values()
.any(|row| row.view.status == DownloadStatus::Queued);
if !self.wave_open || queued || !self.active.is_empty() || self.preparing_playlists > 0 {
return;
}
let previous = self.latest_wave.as_ref().map_or(0, |wave| wave.sequence);
self.wave.sequence = previous.wrapping_add(1);
self.latest_wave = Some(self.wave.clone());
self.wave_open = false;
}
}
pub(super) fn pop_queued(
queue: &mut VecDeque<DownloadId>,
rows: &FxHashMap<DownloadId, SongDownload>,
) -> Option<DownloadId> {
while let Some(id) = queue.pop_front() {
if rows
.get(&id)
.is_some_and(|row| row.view.status == DownloadStatus::Queued)
{
return Some(id);
}
}
None
}