Skip to main content

novel_cli/cmd/
search.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::{
8    CiweimaoClient, CiyuanjiClient, Client, Options, SfacgClient, Tag, WordCountRange,
9};
10use tokio::sync::Semaphore;
11use url::Url;
12
13use crate::cmd::{Convert, Source};
14use crate::{LANG_ID, LOCALES, utils};
15
16#[must_use]
17#[derive(Args)]
18#[command(arg_required_else_help = true,
19    about = LOCALES.lookup(&LANG_ID, "search_command"))]
20pub struct Search {
21    #[arg(short, long,
22        help = LOCALES.lookup(&LANG_ID, "source"))]
23    pub source: Source,
24
25    #[arg(long, default_value_t = false,
26        help = LOCALES.lookup(&LANG_ID, "show_categories"))]
27    pub show_categories: bool,
28
29    #[arg(long, default_value_t = false,
30        help = LOCALES.lookup(&LANG_ID, "show_tags"))]
31    pub show_tags: bool,
32
33    #[arg(help = LOCALES.lookup(&LANG_ID, "keyword"))]
34    pub keyword: Option<String>,
35
36    #[arg(long, help = LOCALES.lookup(&LANG_ID, "min_word_count"))]
37    pub min_word_count: Option<u32>,
38
39    #[arg(long, help = LOCALES.lookup(&LANG_ID, "max_word_count"))]
40    pub max_word_count: Option<u32>,
41
42    #[arg(long, help = LOCALES.lookup(&LANG_ID, "update_days"))]
43    pub update_days: Option<u8>,
44
45    #[arg(long, help = LOCALES.lookup(&LANG_ID, "is_finished"))]
46    pub is_finished: Option<bool>,
47
48    #[arg(long, help = LOCALES.lookup(&LANG_ID, "is_vip"))]
49    pub is_vip: Option<bool>,
50
51    #[arg(long, help = LOCALES.lookup(&LANG_ID, "category"))]
52    pub category: Option<String>,
53
54    #[arg(long, value_delimiter = ',',
55        help = LOCALES.lookup(&LANG_ID, "tags"))]
56    pub tags: Vec<String>,
57
58    #[arg(long, value_delimiter = ',',
59    help = LOCALES.lookup(&LANG_ID, "excluded_tags"))]
60    pub excluded_tags: Vec<String>,
61
62    #[arg(long, default_value_t = 10, value_parser = clap::value_parser!(u8).range(1..=100),
63      help = LOCALES.lookup(&LANG_ID, "limit"))]
64    pub limit: u8,
65
66    #[arg(short, long, value_enum, value_delimiter = ',',
67        help = LOCALES.lookup(&LANG_ID, "converts"))]
68    pub converts: Vec<Convert>,
69
70    #[arg(long, default_value_t = false,
71        help = LOCALES.lookup(&LANG_ID, "ignore_keyring"))]
72    pub ignore_keyring: bool,
73
74    #[arg(short, long, default_value_t = 8, value_parser = clap::value_parser!(u8).range(1..=8),
75    help = LOCALES.lookup(&LANG_ID, "maximum_concurrency"))]
76    pub maximum_concurrency: u8,
77
78    #[arg(long, num_args = 0..=1, default_missing_value = super::DEFAULT_PROXY,
79        help = LOCALES.lookup(&LANG_ID, "proxy"))]
80    pub proxy: Option<Url>,
81
82    #[arg(long, default_value_t = false,
83        help = LOCALES.lookup(&LANG_ID, "no_proxy"))]
84    pub no_proxy: bool,
85
86    #[arg(long, num_args = 0..=1, default_missing_value = super::default_cert_path(),
87        help = super::cert_help_msg())]
88    pub cert: Option<PathBuf>,
89}
90
91pub async fn execute(config: Search) -> Result<()> {
92    match config.source {
93        Source::Sfacg => {
94            let mut client = SfacgClient::new().await?;
95            super::set_options(&mut client, &config.proxy, &config.no_proxy, &config.cert);
96            do_execute(client, config).await?;
97        }
98        Source::Ciweimao => {
99            let mut client = CiweimaoClient::new().await?;
100            super::set_options(&mut client, &config.proxy, &config.no_proxy, &config.cert);
101            utils::log_in(&client, &config.source, config.ignore_keyring).await?;
102            do_execute(client, config).await?;
103        }
104        Source::Ciyuanji => {
105            let mut client = CiyuanjiClient::new().await?;
106            super::set_options(&mut client, &config.proxy, &config.no_proxy, &config.cert);
107            utils::log_in_without_password(&client).await?;
108            do_execute(client, config).await?;
109        }
110    }
111
112    Ok(())
113}
114
115async fn do_execute<T>(client: T, config: Search) -> Result<()>
116where
117    T: Client + Send + Sync + 'static,
118{
119    let client = Arc::new(client);
120    super::handle_shutdown_signal(&client);
121
122    if config.show_categories {
123        let categories = client.categories().await?;
124        println!("{}", vec_to_string(categories)?);
125    } else if config.show_tags {
126        let tags = client.tags().await?;
127        println!("{}", vec_to_string(tags)?);
128    } else {
129        let mut page = 0;
130        let semaphore = Arc::new(Semaphore::new(config.maximum_concurrency as usize));
131
132        let options = create_options(&client, &config).await?;
133        tracing::debug!("{options:#?}");
134
135        let mut novel_infos = Vec::new();
136        loop {
137            let size = u16::clamp(config.limit as u16 - novel_infos.len() as u16, 10, 50);
138
139            let novel_ids = client.search_infos(&options, page, size).await?;
140            page += 1;
141
142            if novel_ids.is_none() {
143                break;
144            }
145
146            let mut handles = Vec::new();
147            for novel_id in novel_ids.unwrap() {
148                let client = Arc::clone(&client);
149                let permit = semaphore.clone().acquire_owned().await.unwrap();
150
151                handles.push(tokio::spawn(async move {
152                    let novel_info = utils::novel_info(&client, novel_id).await?;
153                    drop(permit);
154                    eyre::Ok(novel_info)
155                }));
156            }
157
158            for handle in handles {
159                let novel_info = handle.await??;
160
161                if !novel_infos.contains(&novel_info) {
162                    novel_infos.push(novel_info);
163                }
164            }
165
166            if novel_infos.len() >= config.limit as usize {
167                break;
168            }
169        }
170
171        novel_infos.truncate(config.limit as usize);
172
173        utils::print_novel_infos(novel_infos, &config.converts)?;
174    }
175
176    Ok(())
177}
178
179async fn create_options<T>(client: &Arc<T>, config: &Search) -> Result<Options>
180where
181    T: Client,
182{
183    let mut options = Options {
184        keyword: config.keyword.clone(),
185        is_finished: config.is_finished,
186        is_vip: config.is_vip,
187        update_days: config.update_days,
188        ..Default::default()
189    };
190
191    if let Some(categories_name) = &config.category {
192        let categories = client.categories().await?;
193
194        match categories
195            .iter()
196            .find(|category| category.name == *categories_name)
197        {
198            Some(category) => options.category = Some(category.clone()),
199            None => {
200                eyre::bail!(
201                    "The category was not found: `{categories_name}`, all available categories are: `{}`",
202                    vec_to_string(categories)?
203                );
204            }
205        }
206    }
207
208    if !config.tags.is_empty() {
209        options.tags = Some(to_tags(client, &config.tags).await?)
210    }
211
212    if !config.excluded_tags.is_empty() {
213        options.excluded_tags = Some(to_tags(client, &config.excluded_tags).await?)
214    }
215
216    if let Some(min_word_count) = config.min_word_count
217        && config.max_word_count.is_none()
218    {
219        options.word_count = Some(WordCountRange::RangeFrom(min_word_count..));
220    } else if config.min_word_count.is_none()
221        && let Some(max_word_count) = config.max_word_count
222    {
223        options.word_count = Some(WordCountRange::RangeTo(..max_word_count));
224    } else if let Some(min_word_count) = config.min_word_count
225        && let Some(max_word_count) = config.max_word_count
226    {
227        options.word_count = Some(WordCountRange::Range(min_word_count..max_word_count));
228    }
229
230    Ok(options)
231}
232
233async fn to_tags<T>(client: &Arc<T>, tag_names: &Vec<String>) -> Result<Vec<Tag>>
234where
235    T: Client,
236{
237    let mut result = Vec::new();
238
239    let tags = client.tags().await?;
240    for tag_name in tag_names {
241        match tags.iter().find(|tag| tag.name == *tag_name) {
242            Some(tag) => result.push(tag.clone()),
243            None => {
244                eyre::bail!(
245                    "The tag was not found: `{tag_name}`, all available tags are: `{}`",
246                    vec_to_string(tags)?
247                );
248            }
249        }
250    }
251
252    Ok(result)
253}
254
255fn vec_to_string<T>(vec: &[T]) -> Result<String>
256where
257    T: ToString,
258{
259    let result = vec
260        .iter()
261        .map(|item| item.to_string())
262        .collect::<Vec<String>>()
263        .join("、");
264
265    Ok(result)
266}