pahe-cli 0.1.8-alpha.3

pahe's cli
use std::time::Duration;

use pahe::errors::*;

use crate::constants::*;

#[derive(Debug, Clone)]
pub struct NormalizedSeriesInput {
    pub anime_id: String,
    pub anime_link: String,
    pub session_id: Option<String>,
}

pub fn estimate_eta(downloaded: u64, total: u64, elapsed: Duration) -> Option<Duration> {
    if downloaded == 0 || total <= downloaded || elapsed.is_zero() {
        return None;
    }

    let speed = downloaded as f64 / elapsed.as_secs_f64();
    if speed <= 0.0 {
        return None;
    }

    let remaining = (total - downloaded) as f64 / speed;
    Some(Duration::from_secs_f64(remaining.max(0.0)))
}

pub fn format_duration(duration: Duration) -> String {
    let secs = duration.as_secs();
    let mins = secs / 60;
    let rem = secs % 60;
    format!("{mins:02}:{rem:02}")
}

pub fn format_bytes(bytes: u64) -> String {
    const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
    let mut value = bytes as f64;
    let mut unit = 0usize;

    while value >= 1024.0 && unit < UNITS.len() - 1 {
        value /= 1024.0;
        unit += 1;
    }

    if unit == 0 {
        format!("{} {}", bytes, UNITS[unit])
    } else {
        format!("{value:.2} {}", UNITS[unit])
    }
}

pub fn format_bytes_f64(bytes: f64) -> String {
    const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
    let mut value = bytes;
    let mut unit = 0usize;

    while value >= 1024.0 && unit < UNITS.len() - 1 {
        value /= 1024.0;
        unit += 1;
    }

    if unit == 0 {
        format!("{:.0} {}", value, UNITS[unit])
    } else {
        format!("{value:.2} {}", UNITS[unit])
    }
}

#[cfg(test)]
pub fn normalize_series_link(raw: &str) -> Result<String> {
    Ok(normalize_series_input(raw)?.anime_link)
}

pub fn normalize_series_input(raw: &str) -> Result<NormalizedSeriesInput> {
    let input = raw.trim();
    let normalized = input
        .strip_prefix("https://")
        .or_else(|| input.strip_prefix("http://"))
        .unwrap_or(input);
    let normalized = normalized.strip_prefix("www.").unwrap_or(normalized);
    let normalized = normalized
        .strip_prefix(ANIMEPAHE_DOMAIN)
        .unwrap_or(normalized);
    let normalized = normalized.strip_prefix('/').unwrap_or(normalized);

    if UUID_RE.is_match(input) {
        return Ok(NormalizedSeriesInput {
            anime_id: input.to_string(),
            anime_link: format!("https://{ANIMEPAHE_DOMAIN}/anime/{input}"),
            session_id: None,
        });
    }

    if let Some((anime_id, session_id)) = normalized.split_once('/')
        && UUID_RE.is_match(anime_id)
        && SESSION_ID_RE.is_match(session_id)
    {
        return Ok(NormalizedSeriesInput {
            anime_id: anime_id.to_string(),
            anime_link: format!("https://{ANIMEPAHE_DOMAIN}/anime/{anime_id}"),
            session_id: Some(session_id.to_string()),
        });
    }

    if let Some(play_path) = normalized.strip_prefix("play/")
        && let Some((anime_id, session_id)) = play_path.split_once('/')
        && UUID_RE.is_match(anime_id)
        && SESSION_ID_RE.is_match(session_id)
    {
        return Ok(NormalizedSeriesInput {
            anime_id: anime_id.to_string(),
            anime_link: format!("https://{ANIMEPAHE_DOMAIN}/anime/{anime_id}"),
            session_id: Some(session_id.to_string()),
        });
    }

    if let Some(anime_id) = normalized.strip_prefix("anime/")
        && UUID_RE.is_match(anime_id)
    {
        return Ok(NormalizedSeriesInput {
            anime_id: anime_id.to_string(),
            anime_link: format!("https://{ANIMEPAHE_DOMAIN}/anime/{anime_id}"),
            session_id: None,
        });
    }

    if let Some(caps) = ANIME_LINK_RE.captures(input)
        && let Some(anime_id) = caps.get(1).map(|m| m.as_str())
    {
        return Ok(NormalizedSeriesInput {
            anime_id: anime_id.to_string(),
            anime_link: format!("https://{ANIMEPAHE_DOMAIN}/anime/{anime_id}"),
            session_id: None,
        });
    }

    if let Some(caps) = PLAY_LINK_RE.captures(input)
        && let Some(anime_id) = caps.get(1).map(|m| m.as_str())
        && let Some(session_id) = caps.get(2).map(|m| m.as_str())
    {
        return Ok(NormalizedSeriesInput {
            anime_id: anime_id.to_string(),
            anime_link: format!("https://{ANIMEPAHE_DOMAIN}/anime/{anime_id}"),
            session_id: Some(session_id.to_string()),
        });
    }

    Err(PaheError::Message(
        "invalid --series value: expected anime id/url or anime+session id/url".to_string(),
    ))
}