pahe-cli 0.1.8-alpha.3

pahe's cli
use owo_colors::OwoColorize;

use pahe::client::EpisodeVariant;
use pahe::errors::*;
use pahe::prelude::PaheBuilder;

use crate::args::*;
use crate::constants::*;
use crate::logger::*;
use crate::prompt::*;
use crate::utils::*;

#[derive(Debug, Clone)]
pub struct EpisodeURL {
    pub referer: String,
    pub url: String,
}

enum QualityPreference {
    Highest,
    Lowest,
    Exact(i32),
}

pub async fn resolve_episode_urls(
    args: ResolveArgs,
    logger: &CliLogger,
) -> Result<Vec<EpisodeURL>> {
    let mut runtime = match args {
        args if args.app_args.interactive => prompt_for_args(args)?,
        ResolveArgs {
            series: Some(series),
            cookies: Some(cookies),
            episodes,
            quality,
            lang,
            ..
        } => RuntimeArgs::new(series, cookies, episodes, quality, lang),
        args => prompt_for_args(args)?,
    };
    let normalized_series = normalize_series_input(&runtime.series)?;
    runtime.series = normalized_series.anime_link.clone();
    if let Some(session_id) = normalized_series.session_id {
        runtime.episodes = EpisodeRange::Session {
            anime_id: Some(normalized_series.anime_id),
            session_id,
        };
    }

    logger.loading("initializing");
    let pahe = PaheBuilder::new().cookies_str(&runtime.cookies).build()?;
    logger.success("initialized");

    let info = logger
        .while_loading(
            format!("getting info from: {}", runtime.series.yellow()),
            pahe.get_series_metadata(&runtime.series),
        )
        .await?;
    logger.success(format!(
        "title: {}",
        info.title
            .clone()
            .unwrap_or_else(|| "unknown".to_string())
            .trim()
            .yellow()
    ));

    let links = match &runtime.episodes {
        EpisodeRange::Range { start, end } => {
            logger
                .while_loading(
                    format!("retrieving {} episodes", (end - start).yellow()),
                    pahe.fetch_series_episode_links(&info.id, *start, *end),
                )
                .await?
        }
        EpisodeRange::Session {
            anime_id,
            session_id,
        } => {
            let anime_id = anime_id.as_deref().unwrap_or(&info.id);
            let link = format!("https://{ANIMEPAHE_DOMAIN}/play/{anime_id}/{session_id}");
            let episode = pahe.fetch_episode_index(&link).await?;
            vec![(episode, link)]
        }
    };

    if links.is_empty() {
        return match runtime.episodes {
            EpisodeRange::Range { start, .. } => Err(PaheError::EpisodeNotFound(start)),
            EpisodeRange::Session { .. } => Err(PaheError::Message(
                "episode not found for given session input".to_string(),
            )),
        };
    }

    let mut results = Vec::new();

    for (n, link) in links.iter() {
        logger.loading(format!("processing episode {}", n.yellow()));
        logger.debug("episode", format!("link: {}", link.yellow()));

        let variants = logger
            .while_loading(
                format!("fetching variants for episode {}", n.yellow()),
                pahe.fetch_episode_variants(link),
            )
            .await?;
        let selected = select_quality(variants, &runtime.quality, &runtime.lang, logger)?;
        let quality = format!("{}p", selected.resolution);
        let resolved = logger
            .while_loading(
                format!("resolving direct link for episode {}", n.yellow()),
                pahe.resolve_direct_link(&selected),
            )
            .await?;

        results.push(EpisodeURL {
            referer: resolved.referer,
            url: resolved.direct_link,
        });

        logger.success(format!("episode: {}", n.yellow()));
        logger.success(format!("language: {}", selected.lang.yellow()));
        logger.success(format!("quality: {}", quality.yellow()));
        logger.success(format!("bluray: {}", selected.bluray.yellow()));
    }

    Ok(results)
}

fn select_quality(
    variants: Vec<EpisodeVariant>,
    quality: &str,
    audio_lang: &str,
    logger: &CliLogger,
) -> Result<EpisodeVariant> {
    let pool: Vec<EpisodeVariant> = variants
        .iter()
        .filter(|variant| match audio_lang {
            "en" => variant.lang == "en",
            "jp" => variant.lang == "jp",
            "zh" => variant.lang == "zh",
            "any" => true,
            _ => false,
        })
        .cloned()
        .collect();

    if pool.is_empty() {
        return Err(PaheError::NoSelectableVariant);
    }

    logger.debug(
        "episode",
        format!(
            "selecting quality from {} variant(s) with quality={} and lang={}",
            pool.len(),
            quality,
            audio_lang
        ),
    );

    let preference = parse_quality(quality).ok_or(PaheError::NoSelectableVariant)?;

    let selected = match preference {
        QualityPreference::Highest => pool.into_iter().max_by_key(|variant| variant.resolution),
        QualityPreference::Lowest => pool.into_iter().min_by_key(|variant| variant.resolution),
        QualityPreference::Exact(target) => pool
            .iter()
            .find(|variant| variant.resolution == target)
            .cloned()
            .or_else(|| pool.into_iter().max_by_key(|variant| variant.resolution)),
    };

    selected.ok_or(PaheError::NoSelectableVariant)
}

fn parse_quality(raw_quality: &str) -> Option<QualityPreference> {
    let normalized = raw_quality.trim().to_ascii_lowercase();
    match normalized.as_str() {
        "highest" => Some(QualityPreference::Highest),
        "lowest" => Some(QualityPreference::Lowest),
        _ => {
            let digits = normalized.trim_end_matches('p');
            digits.parse::<i32>().ok().map(QualityPreference::Exact)
        }
    }
}