kodik-rs 0.2.0

CLI tool for getting direct links to files from Kodik
use crate::cache::Cache;
use crate::config::{Config, Quality};
use anyhow::{self, Context as _, Result, bail};
use futures::future;
use kodik_parser::Links;
use reqwest::cookie::Jar;
use reqwest::{Client, Url};
use std::io::{self, BufWriter, Write as _};
use std::process::ExitCode;
use std::sync::Arc;

mod cache;
mod config;
mod logging;
mod shiki;

pub async fn run(args: Vec<String>) -> ExitCode {
    match run_impl(args).await {
        Ok(()) => ExitCode::SUCCESS,
        Err(err) => {
            log::error!("{err}");
            ExitCode::FAILURE
        }
    }
}

async fn run_impl(args: Vec<String>) -> Result<()> {
    let config = Arc::new(Config::build(args).unwrap_or_else(|e| e.exit()));
    logging::setup_logging(config.level_filter());
    let mut cache = Cache::load();

    if let Some(ref cache) = cache {
        cache.apply();
    }

    let jar = Arc::new(config.load_cookies()?);
    let client = Client::builder()
        .cookie_provider(Arc::clone(&jar))
        .gzip(true)
        .brotli(true)
        .zstd(true)
        .deflate(true)
        .build()?;

    let futures = config.urls.iter().map(|url| async {
        let client = &client;
        let config = &config;
        let urls = resolve_url(client, url, config, &jar).await?;

        let futures = urls.into_iter().map(|url| async move {
            let links = Links::fetch(client, url.as_str()).await?;
            Ok::<String, anyhow::Error>(match config.quality {
                Quality::P360 => links.p360,
                Quality::P480 => links.p480,
                Quality::P720 => links.p720,
            })
        });

        let mut links = Vec::new();
        for link in future::join_all(futures).await {
            let link = link?;
            links.push(link);
        }

        Ok::<Vec<String>, anyhow::Error>(links)
    });

    let mut all_links = Vec::new();
    for result in future::join_all(futures).await {
        let mut links = result?;
        all_links.append(&mut links);
    }

    let mut stdout = BufWriter::new(io::stdout());
    for link in all_links {
        writeln!(stdout, "{link}")?;
    }
    stdout.flush()?;

    if let Some(ref mut cache) = cache
        && cache.is_changed()
    {
        log::warn!("updating cache... in {}", cache.path.display());
        cache.update();
        cache.save();
    }

    Ok(())
}

async fn resolve_url(client: &Client, url: &Url, config: &Config, jar: &Jar) -> Result<Vec<String>> {
    let links = match url
        .host_str()
        .with_context(|| format!("url '{url}' is not supported"))?
        .split_once('.')
        .with_context(|| format!("url '{url}' is not supported"))?
        .0
    {
        "shikimori" => shiki::resolve_shiki(client, url, config, jar).await?,
        "kodikplayer" => vec![url.to_string()],
        _ => bail!("url '{url}' is not supported"),
    };

    Ok(links)
}