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 config.category.is_some() {
192 let categories = client.categories().await?;
193 let categories_name = config.category.as_ref().unwrap();
194
195 match categories
196 .iter()
197 .find(|category| category.name == *categories_name)
198 {
199 Some(category) => options.category = Some(category.clone()),
200 None => {
201 eyre::bail!(
202 "The category was not found: `{categories_name}`, all available categories are: `{}`",
203 vec_to_string(categories)?
204 );
205 }
206 }
207 }
208
209 if !config.tags.is_empty() {
210 options.tags = Some(to_tags(client, &config.tags).await?)
211 }
212
213 if !config.excluded_tags.is_empty() {
214 options.excluded_tags = Some(to_tags(client, &config.excluded_tags).await?)
215 }
216
217 if config.min_word_count.is_some() && config.max_word_count.is_none() {
218 options.word_count = Some(WordCountRange::RangeFrom(config.min_word_count.unwrap()..));
219 } else if config.min_word_count.is_none() && config.max_word_count.is_some() {
220 options.word_count = Some(WordCountRange::RangeTo(..config.max_word_count.unwrap()));
221 } else if config.min_word_count.is_some() && config.max_word_count.is_some() {
222 options.word_count = Some(WordCountRange::Range(
223 config.min_word_count.unwrap()..config.max_word_count.unwrap(),
224 ));
225 }
226
227 Ok(options)
228}
229
230async fn to_tags<T>(client: &Arc<T>, tag_names: &Vec<String>) -> Result<Vec<Tag>>
231where
232 T: Client,
233{
234 let mut result = Vec::new();
235
236 let tags = client.tags().await?;
237 for tag_name in tag_names {
238 match tags.iter().find(|tag| tag.name == *tag_name) {
239 Some(tag) => result.push(tag.clone()),
240 None => {
241 eyre::bail!(
242 "The tag was not found: `{tag_name}`, all available tags are: `{}`",
243 vec_to_string(tags)?
244 );
245 }
246 }
247 }
248
249 Ok(result)
250}
251
252fn vec_to_string<T>(vec: &[T]) -> Result<String>
253where
254 T: ToString,
255{
256 let result = vec
257 .iter()
258 .map(|item| item.to_string())
259 .collect::<Vec<String>>()
260 .join("、");
261
262 Ok(result)
263}