hyperfetch 0.1.3

A superfast multithreaded downloader
Documentation
use clap::Parser;
use console::style;
use hyperfetch::{
    cli::Cli,
    downloader::Downloader,
    models::{DownloadConfig, DownloadProgress},
    utils::{extract_filename_from_url, format_bytes, format_duration, format_speed, parse_size},
};
use indicatif::{ProgressBar, ProgressStyle};
use std::{collections::HashMap, fs, io::Write, path::Path, time::Duration};
use tokio::time::Instant;

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let cli = Cli::parse();

    let url = cli.url;
    let output_path = cli
        .output
        .clone()
        .unwrap_or_else(|| extract_filename_from_url(&url));

    let max_connections = cli.connections;

    let chunk_size = parse_size(&cli.chunk_size)?;

    let mut headers = HashMap::new();
    if let Some(header_values) = cli.header {
        for header in header_values {
            if let Some((name, value)) = header.split_once(":") {
                headers.insert(name.trim().to_string(), value.trim().to_string());
            }
        }
    }

    let retry_attempts = cli.retries;

    let _timeout_secs = cli.timeout;

    let mut config = DownloadConfig {
        max_connections,
        chunk_size,
        headers,
        retry_attempts,
        retry_delay: Duration::from_secs(1),
        resume_support: !cli.no_resume,
        adaptive_chunking: !cli.no_adaptive,
        ..Default::default()
    };

    if let Some(user_agent) = cli.user_agent {
        config.user_agent = user_agent.clone();
    }

    let quite = cli.quiet;
    let verbose = cli.verbose;

    let downloader = Downloader::new(config);

    if verbose {
        println!("{}", style("Dart Downloader").bold().cyan());
        println!("URL: {}", url);
        println!("Output: {}", output_path);
        println!("Max connections: {}", max_connections);
        println!("Chunk size: {}", format_bytes(chunk_size));
        println!("Resume support: {}", !cli.no_resume);
        println!("Adaptive chunking: {}", !cli.no_adaptive);
        println!();
    }

    let progress_bar = if !quite {
        let pb = ProgressBar::new(0);
        pb.set_style(
            ProgressStyle::default_bar().
            template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}) ETA: {eta} | {msg}")
            ?.progress_chars("##-"),
        );
        Some(pb)
    } else {
        None
    };

    let start_time = Instant::now();

    let progress_callback = if let Some(pb) = progress_bar.clone() {
        let pb_clone = pb.clone();
        let _start_time = start_time;

        Some(Box::new(move |progress: DownloadProgress| {
            pb_clone.set_length(progress.total_bytes);
            pb_clone.set_position(progress.downloaded_bytes);

            let percentage = if progress.total_bytes > 0 {
                (progress.downloaded_bytes as f64 / progress.total_bytes as f64) * 100.0
            } else {
                0.0
            };

            let message = format!(
                "{:.1}% | {} connections | {}/{} chunks",
                percentage,
                progress.active_connections,
                progress.chunks_completed,
                progress.chunks_total
            );
            pb_clone.set_message(message);
        }) as Box<dyn Fn(DownloadProgress) + Send + Sync>)
    } else if verbose {
        Some(Box::new(move |progress: DownloadProgress| {
            let percentage = if progress.total_bytes > 0 {
                (progress.downloaded_bytes as f64 / progress.total_bytes as f64) * 100.0
            } else {
                0.0
            };

            print!(
                "\r{:.1}% {} / {} ({}) ETA: {} | {} connections",
                percentage,
                format_bytes(progress.downloaded_bytes),
                format_bytes(progress.total_bytes),
                format_speed(progress.speed_bps),
                format_duration(progress.eta_seconds),
                progress.active_connections
            );
            let _ = std::io::stdout().flush();
        }) as Box<dyn Fn(DownloadProgress) + Send + Sync>)
    } else {
        None
    };

    let result = downloader
        .download(&url, &output_path, progress_callback)
        .await;

    if let Some(pb) = progress_bar.as_ref() {
        pb.finish();
    }

    match result {
        Ok(_) => {
            let elapsed = start_time.elapsed();
            let final_progress = downloader.get_progress();
            let mut final_path = output_path.clone();

            let path = Path::new(&output_path);
            if path.extension().is_none() {
                println!(
                    "DEBUG: Filename '{}' has no extension. Attempting to infer type from content...",
                    output_path
                );
            }
            match infer::get_from_path(&output_path) {
                Ok(Some(kind)) => {
                    let extension = kind.extension();
                    let new_path = format!("{}.{}", output_path, extension);
                    if fs::rename(output_path, &new_path).is_ok() {
                        final_path = new_path;
                        println!("DEBUG: Renamed file to '{}'", final_path);
                    } else {
                        eprintln!("DEBUG: Error: Failed to rename the file.");
                    }
                }
                Ok(None) => {
                    println!(
                        "DEBUG: Could not infer the file type. The format is unknown or not supported by 'infer'."
                    );
                }
                Err(e) => {
                    eprintln!(
                        "DEBUG: An error occurred while trying to read the file for inference: {}",
                        e
                    );
                }
            }

            if !quite {
                println!();
                println!(
                    "{}",
                    style("Download completed successfully!").green().bold()
                );
                println!("File: {}", final_path);
                println!("Size: {}", format_bytes(final_progress.total_bytes));
                println!("Time: {}", format_duration(elapsed.as_secs()));
                println!(
                    "Average speed: {}",
                    format_speed(final_progress.total_bytes as f64 / elapsed.as_secs_f64())
                );
            }
        }
        Err(e) => {
            if !quite {
                println!();
                eprintln!("{} {}", style("Error:").red().bold(), e);
            }

            std::process::exit(1);
        }
    }

    Ok(())
}