novel-cli 0.17.0

A set of tools for downloading novels from the web, manipulating text, and generating EPUB
Documentation
use std::path::PathBuf;
use std::sync::Arc;

use clap::Args;
use color_eyre::eyre::{self, Result};
use fluent_templates::Loader;
use novel_api::{CiweimaoClient, CiyuanjiClient, Client, SfacgClient};
use tokio::sync::Semaphore;
use url::Url;

use crate::cmd::{Convert, Source};
use crate::{LANG_ID, LOCALES, utils};

#[must_use]
#[derive(Args)]
#[command(about = LOCALES.lookup(&LANG_ID, "bookshelf_command"))]
pub struct Bookshelf {
    #[arg(short, long,
        help = LOCALES.lookup(&LANG_ID, "source"))]
    pub source: Source,

    #[arg(short, long, value_enum, value_delimiter = ',',
        help = LOCALES.lookup(&LANG_ID, "converts"))]
    pub converts: Vec<Convert>,

    #[arg(long, default_value_t = false,
        help = LOCALES.lookup(&LANG_ID, "ignore_keyring"))]
    pub ignore_keyring: bool,

    #[arg(short, long, default_value_t = 8, value_parser = clap::value_parser!(u8).range(1..=8),
        help = LOCALES.lookup(&LANG_ID, "maximum_concurrency"))]
    pub maximum_concurrency: u8,

    #[arg(long, num_args = 0..=1, default_missing_value = super::DEFAULT_PROXY,
        help = LOCALES.lookup(&LANG_ID, "proxy"))]
    pub proxy: Option<Url>,

    #[arg(long, default_value_t = false,
        help = LOCALES.lookup(&LANG_ID, "no_proxy"))]
    pub no_proxy: bool,

    #[arg(long, num_args = 0..=1, default_missing_value = super::default_cert_path(),
        help = super::cert_help_msg())]
    pub cert: Option<PathBuf>,
}

pub async fn execute(config: Bookshelf) -> Result<()> {
    match config.source {
        Source::Sfacg => {
            let mut client = SfacgClient::new().await?;
            super::set_options(&mut client, &config.proxy, &config.no_proxy, &config.cert);
            utils::log_in(&client, &config.source, config.ignore_keyring).await?;
            do_execute(client, config).await?;
        }
        Source::Ciweimao => {
            let mut client = CiweimaoClient::new().await?;
            super::set_options(&mut client, &config.proxy, &config.no_proxy, &config.cert);
            utils::log_in(&client, &config.source, config.ignore_keyring).await?;
            do_execute(client, config).await?;
        }
        Source::Ciyuanji => {
            let mut client = CiyuanjiClient::new().await?;
            super::set_options(&mut client, &config.proxy, &config.no_proxy, &config.cert);
            utils::log_in_without_password(&client).await?;
            do_execute(client, config).await?;
        }
    }

    Ok(())
}

async fn do_execute<T>(client: T, config: Bookshelf) -> Result<()>
where
    T: Client + Send + Sync + 'static,
{
    let client = Arc::new(client);
    super::handle_shutdown_signal(&client);

    let novel_ids = client.bookshelf_infos().await?;
    if novel_ids.is_empty() {
        println!("There are no books in the bookshelf");
        return Ok(());
    }

    let semaphore = Arc::new(Semaphore::new(config.maximum_concurrency as usize));
    let mut handles = Vec::with_capacity(16);
    for novel_id in novel_ids {
        let client = Arc::clone(&client);
        let permit = semaphore.clone().acquire_owned().await.unwrap();

        handles.push(tokio::spawn(async move {
            let novel_info = client.novel_info(novel_id).await?;
            drop(permit);
            eyre::Ok((novel_id, novel_info))
        }));
    }

    let mut novel_infos = Vec::with_capacity(handles.len());
    for handle in handles {
        let (novel_id, novel_info) = handle.await??;

        if let Some(info) = novel_info {
            novel_infos.push(info);
        } else {
            tracing::error!(
                "The novel does not exist, and it may have been taken down: {novel_id}"
            );
        }
    }

    utils::print_novel_infos(novel_infos, &config.converts)?;

    Ok(())
}