novel_cli/cmd/
bookshelf.rs1use 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}