use std::sync::Arc;
use mineral_model::{BitRate, Song};
use mineral_protocol::{DownloadId, DownloadStatus};
use tokio_util::sync::CancellationToken;
use super::DownloadManager;
use super::state::{ManagerState, pop_queued};
use crate::download::{DownloadAttempt, DownloadEnv, TransferUpdate, download_song};
pub(super) struct AdmittedAttempt {
pub(super) id: DownloadId,
pub(super) song: Song,
pub(super) quality: BitRate,
pub(super) cancellation: CancellationToken,
}
impl DownloadManager {
pub(super) async fn run_scheduler(&self) {
loop {
tokio::select! {
() = self.inner.shutdown.cancelled() => break,
() = self.inner.wake.notified() => {
while let Some(attempt) = self.take_next() {
let manager = self.clone();
tokio::spawn(async move { manager.run_attempt(attempt).await });
}
}
}
}
}
pub(super) fn take_next(&self) -> Option<AdmittedAttempt> {
let mut state = self.inner.state.lock();
if state.active.len() >= state.max_concurrent {
return None;
}
let id = take_queued_id(&mut state)?;
let row = state.rows.get_mut(&id)?;
row.view.status = DownloadStatus::Resolving;
row.view.failure = None;
let song = row.view.song.as_ref().clone();
let quality = row.view.quality;
let cancellation = CancellationToken::new();
state.active.insert(id.clone(), cancellation.clone());
mineral_log::info!(
target: "download",
download_id = %id,
song_id = %song.id.qualified(),
source = song.source().name(),
"download attempt started"
);
Some(AdmittedAttempt {
id,
song,
quality,
cancellation,
})
}
async fn run_attempt(&self, attempt: AdmittedAttempt) {
let manager = self.clone();
let id = attempt.id.clone();
let reporter = Arc::new(move |update| manager.report(&id, update));
let outcome = match self.inner.runtime.music_dir.as_deref() {
Some(music_dir) => {
download_song(
&self.inner.runtime.playback,
&DownloadEnv {
music_dir,
hooks: &self.inner.runtime.hooks,
},
&attempt.song,
attempt.quality,
DownloadAttempt {
id: &attempt.id,
cancellation: &attempt.cancellation,
},
reporter,
self.inner.runtime.speed_tick,
)
.await
}
None => Err(crate::download::TransferError::Unavailable),
};
self.finish_attempt(&attempt, outcome);
}
pub(super) fn report(&self, id: &DownloadId, update: TransferUpdate) {
let mut state = self.inner.state.lock();
if !state.active.contains_key(id) {
return;
}
let Some(row) = state.rows.get_mut(id) else {
return;
};
if row.view.status == DownloadStatus::Stopping {
return;
}
match update {
TransferUpdate::Downloading {
quality,
bytes_done,
bytes_total,
speed_bps,
} => {
row.view.status = DownloadStatus::Downloading;
row.view.quality = quality;
row.view.bytes_done = bytes_done;
row.view.bytes_total = bytes_total;
row.view.speed_bps = speed_bps;
}
TransferUpdate::Finalizing => {
row.view.status = DownloadStatus::Finalizing;
row.view.speed_bps = 0;
}
}
drop(state);
self.bump();
}
}
fn take_queued_id(state: &mut ManagerState) -> Option<DownloadId> {
pop_queued(&mut state.direct_queue, &state.rows)
.or_else(|| pop_queued(&mut state.playlist_queue, &state.rows))
}