novel_cli/cmd/
bookshelf.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4use clap::Args;
5use color_eyre::eyre::{self, Result};
6use fluent_templates::Loader;
7use novel_api::{CiweimaoClient, CiyuanjiClient, Client, SfacgClient};
8use tokio::sync::Semaphore;
9use url::Url;
10
11use crate::cmd::{Convert, Source};
12use crate::{LANG_ID, LOCALES, utils};
13
14#[must_use]
15#[derive(Args)]
16#[command(about = LOCALES.lookup(&LANG_ID, "bookshelf_command"))]
17pub struct Bookshelf {
18    #[arg(short, long,
19        help = LOCALES.lookup(&LANG_ID, "source"))]
20    pub source: Source,
21
22    #[arg(short, long, value_enum, value_delimiter = ',',
23        help = LOCALES.lookup(&LANG_ID, "converts"))]
24    pub converts: Vec<Convert>,
25
26    #[arg(long, default_value_t = false,
27        help = LOCALES.lookup(&LANG_ID, "ignore_keyring"))]
28    pub ignore_keyring: bool,
29
30    #[arg(short, long, default_value_t = 8, value_parser = clap::value_parser!(u8).range(1..=8),
31        help = LOCALES.lookup(&LANG_ID, "maximum_concurrency"))]
32    pub maximum_concurrency: u8,
33
34    #[arg(long, num_args = 0..=1, default_missing_value = super::DEFAULT_PROXY,
35        help = LOCALES.lookup(&LANG_ID, "proxy"))]
36    pub proxy: Option<Url>,
37
38    #[arg(long, default_value_t = false,
39        help = LOCALES.lookup(&LANG_ID, "no_proxy"))]
40    pub no_proxy: bool,
41
42    #[arg(long, num_args = 0..=1, default_missing_value = super::default_cert_path(),
43        help = super::cert_help_msg())]
44    pub cert: Option<PathBuf>,
45}
46
47pub async fn execute(config: Bookshelf) -> Result<()> {
48    match config.source {
49        Source::Sfacg => {
50            let mut client = SfacgClient::new().await?;
51            super::set_options(&mut client, &config.proxy, &config.no_proxy, &config.cert);
52            utils::log_in(&client, &config.source, config.ignore_keyring).await?;
53            do_execute(client, config).await?;
54        }
55        Source::Ciweimao => {
56            let mut client = CiweimaoClient::new().await?;
57            super::set_options(&mut client, &config.proxy, &config.no_proxy, &config.cert);
58            utils::log_in(&client, &config.source, config.ignore_keyring).await?;
59            do_execute(client, config).await?;
60        }
61        Source::Ciyuanji => {
62            let mut client = CiyuanjiClient::new().await?;
63            super::set_options(&mut client, &config.proxy, &config.no_proxy, &config.cert);
64            utils::log_in_without_password(&client).await?;
65            do_execute(client, config).await?;
66        }
67    }
68
69    Ok(())
70}
71
72async fn do_execute<T>(client: T, config: Bookshelf) -> Result<()>
73where
74    T: Client + Send + Sync + 'static,
75{
76    let client = Arc::new(client);
77    super::handle_shutdown_signal(&client);
78
79    let novel_ids = client.bookshelf_infos().await?;
80    if novel_ids.is_empty() {
81        println!("There are no books in the bookshelf");
82        return Ok(());
83    }
84
85    let semaphore = Arc::new(Semaphore::new(config.maximum_concurrency as usize));
86    let mut handles = Vec::with_capacity(16);
87    for novel_id in novel_ids {
88        let client = Arc::clone(&client);
89        let permit = semaphore.clone().acquire_owned().await.unwrap();
90
91        handles.push(tokio::spawn(async move {
92            let novel_info = client.novel_info(novel_id).await?;
93            drop(permit);
94            eyre::Ok((novel_id, novel_info))
95        }));
96    }
97
98    let mut novel_infos = Vec::with_capacity(handles.len());
99    for handle in handles {
100        let (novel_id, novel_info) = handle.await??;
101
102        if let Some(info) = novel_info {
103            novel_infos.push(info);
104        } else {
105            tracing::error!(
106                "The novel does not exist, and it may have been taken down: {novel_id}"
107            );
108        }
109    }
110
111    utils::print_novel_infos(novel_infos, &config.converts)?;
112
113    Ok(())
114}