use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use mineral_channel_core::MusicChannel;
use mineral_model::BitRate;
use mineral_playback::PlaybackRegistry;
use mineral_protocol::{DownloadId, DownloadStatus, DownloadSummary, SongDownloadView};
use parking_lot::Mutex;
use tokio::sync::Notify;
use tokio_util::sync::CancellationToken;
use super::state::ManagerState;
#[derive(Debug, thiserror::Error)]
#[error("download identity not found")]
pub(crate) struct StopError;
#[derive(Clone)]
pub(crate) struct DownloadRuntime {
pub(crate) music_dir: Option<PathBuf>,
pub(crate) channels: Vec<Arc<dyn MusicChannel>>,
pub(crate) playback: PlaybackRegistry,
pub(crate) hooks: crate::hook_bridge::HookGate,
pub(crate) tagging: crate::tagging::TaggingQueue,
pub(crate) notify: crate::notify::Notifier,
pub(crate) stats: crate::StatsRecorder,
pub(crate) speed_tick: Duration,
}
#[derive(Clone)]
pub(crate) struct DownloadManager {
pub(super) inner: Arc<ManagerInner>,
}
pub(super) struct ManagerInner {
pub(super) runtime: DownloadRuntime,
pub(super) state: Mutex<ManagerState>,
pub(super) wake: Notify,
pub(super) changes: tokio::sync::watch::Sender<u64>,
pub(super) change_seq: AtomicU64,
pub(super) quiesced: Notify,
pub(super) shutdown: CancellationToken,
}
impl DownloadManager {
pub(crate) fn spawn(runtime: DownloadRuntime, quality: BitRate, max_concurrent: usize) -> Self {
let (changes, _changes_rx) = tokio::sync::watch::channel(0_u64);
let manager = Self {
inner: Arc::new(ManagerInner {
runtime,
state: Mutex::new(ManagerState::new(quality, max_concurrent)),
wake: Notify::new(),
changes,
change_seq: AtomicU64::new(0),
quiesced: Notify::new(),
shutdown: CancellationToken::new(),
}),
};
if let Some(root) = manager.inner.runtime.music_dir.as_deref() {
crate::download::cleanup_orphan_partials(root);
}
let scheduler = manager.clone();
tokio::spawn(async move { scheduler.run_scheduler().await });
manager
}
pub(crate) fn set_config(&self, quality: BitRate, max_concurrent: usize) {
let mut state = self.inner.state.lock();
state.quality = quality;
state.max_concurrent = max_concurrent;
drop(state);
self.inner.wake.notify_one();
self.bump();
}
pub(crate) fn changes(&self) -> tokio::sync::watch::Receiver<u64> {
self.inner.changes.subscribe()
}
pub(super) fn bump(&self) {
let next = self
.inner
.change_seq
.fetch_add(1, Ordering::Relaxed)
.saturating_add(1);
let _ = self.inner.changes.send(next);
}
pub(crate) fn summary(&self) -> DownloadSummary {
self.inner.state.lock().summary()
}
pub(crate) fn snapshot(&self) -> Vec<SongDownloadView> {
self.inner.state.lock().snapshot()
}
pub(crate) fn stop(&self, id: &DownloadId) -> Result<(), StopError> {
let mut cancellation = None::<CancellationToken>;
let mut queued_stop = false;
let song_id = {
let mut state = self.inner.state.lock();
let row = state.rows.get(id).ok_or(StopError)?;
let status = row.view.status;
let song_id = row.view.song.id.clone();
match status {
DownloadStatus::Queued => {
queued_stop = true;
state.mark_terminal(id, DownloadStatus::Stopped, None);
}
DownloadStatus::Resolving
| DownloadStatus::Downloading
| DownloadStatus::Finalizing => {
if let Some(row) = state.rows.get_mut(id) {
row.view.status = DownloadStatus::Stopping;
row.view.speed_bps = 0;
}
cancellation = state.active.get(id).cloned();
}
DownloadStatus::Stopping
| DownloadStatus::Stopped
| DownloadStatus::Downloaded
| DownloadStatus::AlreadyPresent
| DownloadStatus::SkippedByHook
| DownloadStatus::Failed => {}
}
state.finish_wave_if_idle();
song_id
};
if let Some(token) = cancellation {
token.cancel();
}
mineral_log::info!(
target: "download",
download_id = %id,
song_id = %song_id.qualified(),
source = song_id.namespace().name(),
queued = queued_stop,
"download Stop accepted"
);
self.inner.wake.notify_one();
self.bump();
Ok(())
}
pub(crate) async fn shutdown(&self) {
self.inner.shutdown.cancel();
let tokens = {
let mut state = self.inner.state.lock();
let queued = state
.rows
.iter()
.filter(|(_, row)| row.view.status == DownloadStatus::Queued)
.map(|(id, _)| id.clone())
.collect::<Vec<_>>();
for id in queued {
state.mark_terminal(&id, DownloadStatus::Stopped, None);
}
let active_ids = state.active.keys().cloned().collect::<Vec<_>>();
for id in active_ids {
if let Some(row) = state.rows.get_mut(&id) {
row.view.status = DownloadStatus::Stopping;
row.view.speed_bps = 0;
}
}
state.active.values().cloned().collect::<Vec<_>>()
};
for token in tokens {
token.cancel();
}
loop {
let quiesced = self.inner.quiesced.notified();
if self.inner.state.lock().active.is_empty() {
break;
}
quiesced.await;
}
}
}