steamroom-cli 0.1.0

Command-line tool for downloading Steam depot content
use indicatif::MultiProgress;
use indicatif::ProgressBar;
use indicatif::ProgressStyle;
use steamroom_client::event::DownloadEvent;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;

pub fn spawn_progress_renderer(
    mut rx: mpsc::UnboundedReceiver<DownloadEvent>,
    show_bars: bool,
) -> JoinHandle<()> {
    tokio::spawn(async move {
        if show_bars {
            run_with_bars(&mut rx).await;
        } else {
            run_quiet(&mut rx).await;
        }
    })
}

async fn run_with_bars(rx: &mut mpsc::UnboundedReceiver<DownloadEvent>) {
    let mp = MultiProgress::new();

    let total_bar = mp.add(ProgressBar::hidden());
    let file_bar = mp.add(ProgressBar::new_spinner());
    file_bar.set_style(
        ProgressStyle::default_spinner()
            .template("{spinner:.green} {wide_msg}")
            .unwrap(),
    );

    while let Some(event) = rx.recv().await {
        match event {
            DownloadEvent::DownloadStarted {
                total_bytes,
                total_files,
            } => {
                total_bar.set_length(total_bytes);
                total_bar.set_style(
                    ProgressStyle::default_bar()
                        .template("{spinner:.green} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
                        .unwrap()
                        .progress_chars("=> "),
                );
                total_bar.reset();
                mp.println(format!(
                    "downloading {total_files} files ({})",
                    fmt_bytes(total_bytes)
                ))
                .ok();
            }
            DownloadEvent::FileStarted { filename } => {
                file_bar.set_message(filename);
            }
            DownloadEvent::FileCompleted { filename } => {
                file_bar.set_message(filename);
            }
            DownloadEvent::FileSkipped { .. } => {}
            DownloadEvent::FileRemoved { filename } => {
                mp.println(format!("removed {filename}")).ok();
            }
            DownloadEvent::ChunkCompleted { bytes } => {
                total_bar.inc(bytes);
            }
            DownloadEvent::ChunkFailed { error } => {
                mp.println(format!("warning: chunk failed (retrying): {error}"))
                    .ok();
            }
            _ => {}
        }
    }

    total_bar.finish_and_clear();
    file_bar.finish_and_clear();
}

async fn run_quiet(rx: &mut mpsc::UnboundedReceiver<DownloadEvent>) {
    let mut completed: u64 = 0;
    let mut total: u64 = 0;
    while let Some(event) = rx.recv().await {
        match event {
            DownloadEvent::DownloadStarted { total_bytes, .. } => {
                total = total_bytes;
            }
            DownloadEvent::FileCompleted { filename } => {
                let pct = if total > 0 {
                    completed as f64 / total as f64 * 100.0
                } else {
                    0.0
                };
                tracing::info!("[{pct:.1}%] {filename}");
            }
            DownloadEvent::ChunkCompleted { bytes } => {
                completed += bytes;
            }
            DownloadEvent::FileRemoved { filename } => {
                tracing::info!("removed {filename}");
            }
            DownloadEvent::ChunkFailed { error } => {
                tracing::warn!("chunk failed (retrying): {error}");
            }
            _ => {}
        }
    }
}

fn fmt_bytes(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = 1024 * KB;
    const GB: u64 = 1024 * MB;
    if bytes >= GB {
        format!("{:.1} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.1} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.1} KB", bytes as f64 / KB as f64)
    } else {
        format!("{bytes} B")
    }
}