1mod structure;
2mod utils;
3
4use std::io::Cursor;
5use std::path::PathBuf;
6use std::slice;
7use std::sync::RwLock;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use hashbrown::HashMap;
11use image::{DynamicImage, ImageReader};
12use jiff::{Timestamp, ToSpan};
13use scraper::{Html, Selector};
14use serde::{Deserialize, Serialize};
15use tokio::sync::OnceCell;
16use url::Url;
17
18use self::structure::*;
19use crate::{
20 Category, ChapterInfo, Client, Comment, CommentType, ContentInfo, ContentInfos, Error,
21 FindImageResult, FindTextResult, HTTPClient, LongComment, NovelDB, NovelInfo, Options,
22 ShortComment, Tag, UserInfo, VolumeInfo, VolumeInfos, WordCountRange,
23};
24
25#[must_use]
26#[derive(Serialize, Deserialize)]
27pub(crate) struct Config {
28 account: String,
29 login_token: String,
30 reader_id: u32,
31}
32
33#[must_use]
35pub struct CiweimaoClient {
36 proxy: Option<Url>,
37 no_proxy: bool,
38 cert_path: Option<PathBuf>,
39
40 client: OnceCell<HTTPClient>,
41 client_rss: OnceCell<HTTPClient>,
42
43 db: OnceCell<NovelDB>,
44
45 config: RwLock<Option<Config>>,
46}
47
48impl Client for CiweimaoClient {
49 fn proxy(&mut self, proxy: Url) {
50 self.proxy = Some(proxy);
51 }
52
53 fn no_proxy(&mut self) {
54 self.no_proxy = true;
55 }
56
57 fn cert(&mut self, cert_path: PathBuf) {
58 self.cert_path = Some(cert_path);
59 }
60
61 async fn shutdown(&self) -> Result<(), Error> {
62 self.client().await?.save_cookies()?;
63 self.do_shutdown()?;
64 Ok(())
65 }
66
67 async fn add_cookie(&self, cookie_str: &str, url: &Url) -> Result<(), Error> {
68 self.client().await?.add_cookie(cookie_str, url)
69 }
70
71 async fn log_in(&self, username: String, password: Option<String>) -> Result<(), Error> {
72 assert!(!username.is_empty());
73 assert!(password.is_some());
74
75 let password = password.unwrap();
76
77 let config = match self.verify_type(&username).await? {
78 VerifyType::None => {
79 tracing::info!("No verification required");
80 self.no_verification_login(username, password).await?
81 }
82 VerifyType::Geetest => {
83 tracing::info!("Verify with Geetest");
84 self.geetest_login(username, password).await?
85 }
86 VerifyType::VerifyCode => {
87 tracing::info!("Verify with SMS verification code");
88 self.sms_login(username, password).await?
89 }
90 };
91
92 self.save_token(config);
93
94 Ok(())
95 }
96
97 async fn logged_in(&self) -> Result<bool, Error> {
98 if !self.has_token() {
99 return Ok(false);
100 }
101
102 let response: GenericResponse = self.post("/reader/get_my_info", EmptyRequest {}).await?;
103
104 if response.code == CiweimaoClient::LOGIN_EXPIRED {
105 Ok(false)
106 } else {
107 utils::check_response_success(response.code, response.tip)?;
108 Ok(true)
109 }
110 }
111
112 async fn user_info(&self) -> Result<UserInfo, Error> {
113 let response: UserInfoResponse = self.post("/reader/get_my_info", EmptyRequest {}).await?;
114 utils::check_response_success(response.code, response.tip)?;
115 let reader_info = response.data.unwrap().reader_info;
116
117 let user_info = UserInfo {
118 nickname: reader_info.reader_name.trim().to_string(),
119 avatar: reader_info.avatar_url,
120 };
121
122 Ok(user_info)
123 }
124
125 async fn money(&self) -> Result<u32, Error> {
126 let response: PropInfoResponse =
127 self.post("/reader/get_prop_info", EmptyRequest {}).await?;
128 utils::check_response_success(response.code, response.tip)?;
129 let prop_info = response.data.unwrap().prop_info;
130
131 Ok(prop_info.rest_hlb.parse()?)
132 }
133
134 async fn sign_in(&self) -> Result<(), Error> {
135 let response: GenericResponse = self
136 .post(
137 "/reader/get_task_bonus_with_sign_recommend",
138 SignRequest {
139 task_type: 1,
141 },
142 )
143 .await?;
144 if utils::check_already_signed_in(&response.code) {
145 tracing::info!("{}", response.tip.unwrap().trim());
146 } else {
147 utils::check_response_success(response.code, response.tip)?;
148 }
149
150 Ok(())
151 }
152
153 async fn bookshelf_infos(&self) -> Result<Vec<u32>, Error> {
154 let shelf_ids = self.shelf_list().await?;
155 let mut result = Vec::new();
156
157 for shelf_id in shelf_ids {
158 let response: BookshelfResponse = self
159 .post(
160 "/bookshelf/get_shelf_book_list_new",
161 BookshelfRequest {
162 shelf_id,
163 count: 9999,
164 page: 0,
165 order: "last_read_time",
166 },
167 )
168 .await?;
169 utils::check_response_success(response.code, response.tip)?;
170
171 for novel_info in response.data.unwrap().book_list {
172 result.push(novel_info.book_info.book_id.parse()?);
173 }
174 }
175
176 Ok(result)
177 }
178
179 async fn novel_info(&self, id: u32) -> Result<Option<NovelInfo>, Error> {
180 assert!(id > 0);
181
182 let response: NovelInfoResponse = self
183 .post("/book/get_info_by_id", NovelInfoRequest { book_id: id })
184 .await?;
185 if response.code == CiweimaoClient::NOT_FOUND {
186 return Ok(None);
187 }
188 utils::check_response_success(response.code, response.tip)?;
189
190 let data = response.data.unwrap().book_info;
191 let novel_info = NovelInfo {
192 id,
193 name: data.book_name.trim().to_string(),
194 author_name: data.author_name.trim().to_string(),
195 cover_url: data.cover,
196 introduction: super::parse_multi_line(data.description),
197 word_count: Some(data.total_word_count.parse()?),
198 is_vip: Some(data.is_paid),
199 is_finished: Some(data.up_status),
200 create_time: data.newtime,
201 update_time: Some(data.uptime),
202 category: self.parse_category(data.category_index).await?,
203 tags: self.parse_tags(data.tag_list).await?,
204 };
205
206 Ok(Some(novel_info))
207 }
208
209 async fn comments(
210 &self,
211 id: u32,
212 comment_type: CommentType,
213 need_replies: bool,
214 page: u16,
215 size: u16,
216 ) -> Result<Option<Vec<Comment>>, Error> {
217 let r#type = match comment_type {
218 CommentType::Short => 1,
221 CommentType::Long => 2,
222 };
223
224 let response: ReviewResponse = self
225 .post(
226 "/book/get_review_list",
227 ReviewRequest {
228 book_id: id,
229 r#type,
230 page,
231 count: size,
232 },
233 )
234 .await?;
235 utils::check_response_success(response.code, response.tip)?;
236 let review_list = response.data.unwrap().review_list;
237
238 if review_list.is_empty() {
239 return Ok(None);
240 }
241
242 let mut result = Vec::with_capacity(review_list.len());
243
244 match comment_type {
245 CommentType::Short => {
246 for review in review_list {
247 let Some(content) = super::parse_multi_line(review.review_content) else {
249 continue;
250 };
251
252 let review_id: u32 = review.review_id.parse()?;
253 let comment_amount = review.comment_amount.parse::<u16>().unwrap_or(0);
255
256 let replies = if need_replies && comment_amount > 0 {
257 self.review_comment(review_id, comment_amount).await?
258 } else {
259 None
260 };
261
262 let comment = ShortComment {
263 id: review_id,
264 user: UserInfo {
265 nickname: review.reader_info.reader_name.trim().to_string(),
266 avatar: review.reader_info.avatar_url,
267 },
268 content,
269 create_time: Some(review.ctime),
270 like_count: Some(review.like_amount.parse()?),
271 replies,
272 };
273
274 result.push(Comment::Short(comment));
275 }
276 }
277 CommentType::Long => {
278 for review in review_list {
279 let Some(content) = super::parse_multi_line(review.review_content) else {
280 continue;
281 };
282
283 let review_id: u32 = review.review_id.parse()?;
284 let comment_amount: u16 = review.comment_amount.parse()?;
285
286 let replies = if need_replies && comment_amount > 0 {
287 self.review_comment(review_id, comment_amount).await?
288 } else {
289 None
290 };
291
292 let comment = LongComment {
293 id: review_id,
294 user: UserInfo {
295 nickname: review.reader_info.reader_name.trim().to_string(),
296 avatar: review.reader_info.avatar_url,
297 },
298 title: review.title.trim().to_string(),
299 content,
300 create_time: Some(review.ctime),
301 like_count: Some(review.like_amount.parse()?),
302 replies,
303 };
304
305 result.push(Comment::Long(comment));
306 }
307 }
308 }
309
310 Ok(Some(result))
311 }
312
313 async fn volume_infos(&self, id: u32) -> Result<Option<VolumeInfos>, Error> {
314 let response: VolumesResponse = self
315 .post(
316 "/chapter/get_updated_chapter_by_division_new",
317 VolumesRequest { book_id: id },
318 )
319 .await?;
320 utils::check_response_success(response.code, response.tip)?;
321 let chapter_list = response.data.unwrap().chapter_list;
322
323 let chapter_prices = self.chapter_prices(id).await?;
324
325 let mut volume_infos = VolumeInfos::new();
326 for item in chapter_list {
327 let mut volume_info = VolumeInfo {
328 id: item.division_id.parse()?,
329 title: item.division_name.trim().to_string(),
330 chapter_infos: Vec::new(),
331 };
332
333 for chapter in item.chapter_list {
334 let chapter_id: u32 = chapter.chapter_id.parse()?;
335 let price = chapter_prices.get(&chapter_id).copied();
336 let mut is_valid = true;
337
338 if price.is_none() {
340 tracing::info!("Price not found: {chapter_id}");
341 is_valid = false;
342 }
343
344 let chapter_info = ChapterInfo {
345 novel_id: Some(id),
346 id: chapter_id,
347 title: chapter.chapter_title.trim().to_string(),
348 word_count: Some(chapter.word_count.parse()?),
349 create_time: Some(chapter.mtime),
351 update_time: None,
352 is_vip: Some(chapter.is_paid),
353 price,
354 payment_required: Some(!chapter.auth_access),
355 is_valid: Some(chapter.is_valid && is_valid),
356 };
357
358 volume_info.chapter_infos.push(chapter_info);
359 }
360
361 volume_infos.push(volume_info);
362 }
363
364 Ok(Some(volume_infos))
365 }
366
367 async fn content_infos(&self, info: &ChapterInfo) -> Result<ContentInfos, Error> {
368 Ok(self
369 .content_infos_multiple(slice::from_ref(info))
370 .await?
371 .remove(0))
372 }
373
374 async fn content_infos_multiple(
375 &self,
376 infos: &[ChapterInfo],
377 ) -> Result<Vec<ContentInfos>, Error> {
378 let mut contents = HashMap::new();
379 let mut need_to_download = Vec::new();
380
381 for info in infos {
382 match self.db().await?.find_text(info).await? {
383 FindTextResult::Ok(content) => {
384 contents.insert(info.id, content);
385 }
386 _ => {
387 need_to_download.push(info.id);
388 }
389 }
390 }
391
392 if !need_to_download.is_empty() {
393 self.check_download_cpt().await?;
394
395 let cmd = self.chapter_cmd(&need_to_download).await?;
396 let key = crate::sha256(cmd.as_bytes());
397
398 let response: ChapsResponse = self
399 .post(
400 "/chapter/download_cpt",
401 ChapsRequest {
402 chapter_id: itertools::join(&need_to_download, ","),
403 chapter_command: cmd,
404 },
405 )
406 .await?;
407 utils::check_response_success(response.code, response.tip)?;
408
409 let chapter_infos =
410 simdutf8::basic::from_utf8(&crate::aes_256_cbc_no_iv_base64_decrypt(
411 key,
412 &response.data.unwrap().chapter_infos,
413 )?)?
414 .to_string();
415 let chapter_infos: Vec<ChapsInfo> = sonic_rs::from_str(&chapter_infos)?;
416 if chapter_infos.len() != need_to_download.len() {
417 return Err(Error::NovelApi(String::from(
418 "The number of chapter downloads is insufficient",
419 )));
420 }
421
422 for (index, id) in need_to_download.iter().enumerate() {
423 let content = chapter_infos[index].txt_content.clone();
424 if content.trim().is_empty() {
425 return Err(Error::NovelApi(String::from("Content is empty")));
426 }
427
428 contents.insert(*id, content);
429 }
430
431 for info in infos {
432 match self.db().await?.find_text(info).await? {
433 FindTextResult::Ok(_) => (),
434 other => match other {
435 FindTextResult::None => {
436 self.db()
437 .await?
438 .insert_text(info, contents.get(&info.id).unwrap())
439 .await?
440 }
441 FindTextResult::Outdate => {
442 self.db()
443 .await?
444 .update_text(info, contents.get(&info.id).unwrap())
445 .await?
446 }
447 FindTextResult::Ok(_) => (),
448 },
449 }
450 }
451 }
452
453 let mut result = Vec::new();
454 for info in infos {
455 let mut content_infos = ContentInfos::new();
456 for line in contents
457 .get(&info.id)
458 .unwrap()
459 .lines()
460 .map(|line| line.trim())
461 .filter(|line| !line.is_empty())
462 {
463 if line.starts_with("<img") {
464 if let Some(url) = CiweimaoClient::parse_image_url(line) {
465 content_infos.push(ContentInfo::Image(url));
466 }
467 } else {
468 content_infos.push(ContentInfo::Text(line.to_string()));
469 }
470 }
471
472 result.push(content_infos);
473 }
474
475 Ok(result)
476 }
477
478 async fn order_chapter(&self, info: &ChapterInfo) -> Result<(), Error> {
479 let response: GenericResponse = self
480 .post(
481 "/chapter/buy",
482 OrderChapterRequest {
483 chapter_id: info.id.to_string(),
484 },
485 )
486 .await?;
487 utils::check_response_success(response.code, response.tip)?;
488
489 Ok(())
490 }
491
492 async fn order_novel(&self, id: u32, infos: &VolumeInfos) -> Result<(), Error> {
493 assert!(id > 0);
494
495 let mut chapter_id_list = Vec::new();
496 for volume in infos {
497 for chapter in &volume.chapter_infos {
498 if chapter.payment_required() {
499 chapter_id_list.push(chapter.id.to_string());
500 }
501 }
502 }
503 if chapter_id_list.is_empty() {
504 return Ok(());
505 }
506
507 let chapter_id_list = sonic_rs::json!(chapter_id_list).to_string();
508
509 let response: GenericResponse = self
510 .post("/chapter/buy_multi", OrderNovelRequest { chapter_id_list })
511 .await?;
512 utils::check_response_success(response.code, response.tip)?;
513
514 Ok(())
515 }
516
517 async fn image(&self, url: &Url) -> Result<DynamicImage, Error> {
518 match self.db().await?.find_image(url).await? {
519 FindImageResult::Ok(image) => Ok(image),
520 FindImageResult::None => {
521 let response = self.get_rss(url).await?;
522 let bytes = response.bytes().await?;
523
524 let image = ImageReader::new(Cursor::new(&bytes))
525 .with_guessed_format()?
526 .decode()?;
527
528 self.db().await?.insert_image(url, bytes).await?;
529
530 Ok(image)
531 }
532 }
533 }
534
535 async fn categories(&self) -> Result<&Vec<Category>, Error> {
536 static CATEGORIES: OnceCell<Vec<Category>> = OnceCell::const_new();
537
538 CATEGORIES
539 .get_or_try_init(|| async {
540 let response: CategoryResponse =
541 self.post("/meta/get_meta_data", EmptyRequest {}).await?;
542 utils::check_response_success(response.code, response.tip)?;
543
544 let mut result = Vec::new();
545 for category in response.data.unwrap().category_list {
546 for category_detail in category.category_detail {
547 result.push(Category {
548 id: Some(category_detail.category_index.parse()?),
549 parent_id: None,
550 name: category_detail.category_name.trim().to_string(),
551 });
552 }
553 }
554
555 result.sort_unstable_by_key(|x| x.id.unwrap());
556
557 Ok(result)
558 })
559 .await
560 }
561
562 async fn tags(&self) -> Result<&Vec<Tag>, Error> {
563 static TAGS: OnceCell<Vec<Tag>> = OnceCell::const_new();
564
565 TAGS.get_or_try_init(|| async {
566 let response: TagResponse = self
567 .post("/book/get_official_tag_list", EmptyRequest {})
568 .await?;
569 utils::check_response_success(response.code, response.tip)?;
570
571 let mut result = Vec::new();
572 for tag in response.data.unwrap().official_tag_list {
573 result.push(Tag {
574 id: None,
575 name: tag.tag_name.trim().to_string(),
576 });
577 }
578
579 result.push(Tag {
580 id: None,
581 name: String::from("橘子"),
582 });
583 result.push(Tag {
584 id: None,
585 name: String::from("变身"),
586 });
587 result.push(Tag {
588 id: None,
589 name: String::from("性转"),
590 });
591 result.push(Tag {
592 id: None,
593 name: String::from("纯百"),
594 });
595
596 Ok(result)
597 })
598 .await
599 }
600
601 async fn search_infos(
602 &self,
603 option: &Options,
604 page: u16,
605 size: u16,
606 ) -> Result<Option<Vec<u32>>, Error> {
607 let mut category_index = 0;
608 if let Some(category) = &option.category {
609 category_index = category.id.unwrap();
610 }
611
612 let mut tags_vec = Vec::new();
613 if let Some(tags) = &option.tags {
614 for tag in tags {
615 tags_vec.push(sonic_rs::json!({
616 "tag": tag.name,
617 "filter": "1"
618 }));
619 }
620 }
621
622 let is_paid = option.is_vip.map(|is_vip| if is_vip { 1 } else { 0 });
623
624 let up_status = option
625 .is_finished
626 .map(|is_finished| if is_finished { 1 } else { 0 });
627
628 let mut filter_word = None;
629 if let Some(word_count) = &option.word_count {
630 match word_count {
631 WordCountRange::RangeTo(range_to) => {
632 if range_to.end < 30_0000 {
633 filter_word = Some(1);
634 }
635 }
636 WordCountRange::Range(range) => {
637 if range.start >= 30_0000 && range.end < 50_0000 {
638 filter_word = Some(2);
639 } else if range.start >= 50_0000 && range.end < 100_0000 {
640 filter_word = Some(3);
641 } else if range.start >= 100_0000 && range.end < 200_0000 {
642 filter_word = Some(4);
643 }
644 }
645 WordCountRange::RangeFrom(range_from) => {
646 if range_from.start >= 200_0000 {
647 filter_word = Some(5);
648 }
649 }
650 }
651 }
652
653 let mut filter_uptime = None;
654 if let Some(update_days) = option.update_days {
655 if update_days <= 3 {
656 filter_uptime = Some(1)
657 } else if update_days <= 7 {
658 filter_uptime = Some(2)
659 } else if update_days <= 15 {
660 filter_uptime = Some(3)
661 } else if update_days <= 30 {
662 filter_uptime = Some(4)
663 }
664 }
665
666 let order = if option.keyword.is_some() {
667 None
670 } else {
671 Some("week_click")
673 };
674
675 let response: SearchResponse = self
676 .post(
677 "/bookcity/get_filter_search_book_list",
678 SearchRequest {
679 count: size,
680 page,
681 order,
682 category_index,
683 tags: sonic_rs::json!(tags_vec).to_string(),
684 key: option.keyword.clone(),
685 is_paid,
686 up_status,
687 filter_uptime,
688 filter_word,
689 },
690 )
691 .await?;
692 utils::check_response_success(response.code, response.tip)?;
693
694 let book_list = response.data.unwrap().book_list;
695 if book_list.is_empty() {
696 return Ok(None);
697 }
698
699 let mut result = Vec::new();
700 let sys_tags = self.tags().await?;
701
702 for novel_info in book_list {
703 let mut tag_names = Vec::new();
704 for tag in novel_info.tag_list {
705 if let Some(sys_tag) = sys_tags.iter().find(|x| x.name == tag.tag_name.trim()) {
706 tag_names.push(sys_tag.name.clone());
707 }
708 }
709
710 if CiweimaoClient::match_update_days(option, novel_info.uptime)
711 && CiweimaoClient::match_excluded_tags(option, tag_names)
712 && CiweimaoClient::match_word_count(option, novel_info.total_word_count.parse()?)
713 {
714 result.push(novel_info.book_id.parse()?);
715 }
716 }
717
718 Ok(Some(result))
719 }
720
721 fn has_this_type_of_comments(comment_type: CommentType) -> bool {
722 match comment_type {
723 CommentType::Short => true,
724 CommentType::Long => true,
725 }
726 }
727}
728
729#[must_use]
730enum VerifyType {
731 None,
732 Geetest,
733 VerifyCode,
734}
735
736impl CiweimaoClient {
737 async fn review_comment(
738 &self,
739 review_id: u32,
740 comment_amount: u16,
741 ) -> Result<Option<Vec<ShortComment>>, Error> {
742 let response: ReviewCommentResponse = self
743 .post(
744 "/book/get_review_comment_list",
745 ReviewCommentRequest {
746 review_id,
747 page: 0,
748 count: comment_amount,
749 },
750 )
751 .await?;
752 utils::check_response_success(response.code, response.tip)?;
753 let review_comment_list = response.data.unwrap().review_comment_list;
754
755 let mut result = Vec::with_capacity(review_comment_list.len());
756
757 for comment in review_comment_list {
758 let Some(content) = super::parse_multi_line(comment.comment_content) else {
759 continue;
760 };
761
762 let replies = if comment.review_comment_reply_list.is_empty() {
763 None
764 } else if
765 comment.review_comment_reply_list.len() <= 2 {
767 let mut result = Vec::with_capacity(2);
768
769 for reply in comment.review_comment_reply_list {
770 let Some(content) = super::parse_multi_line(reply.reply_content) else {
771 continue;
772 };
773
774 result.push(ShortComment {
775 id: reply.reply_id.parse()?,
776 user: UserInfo {
777 nickname: reply.reader_info.reader_name.trim().to_string(),
778 avatar: reply.reader_info.avatar_url,
779 },
780 content,
781 create_time: Some(reply.ctime),
782 like_count: None,
783 replies: None,
784 })
785 }
786
787 if result.is_empty() {
788 None
789 } else {
790 result.sort_unstable_by_key(|x| x.create_time.unwrap());
791 result.dedup();
792 Some(result)
793 }
794 } else {
795 self.review_comment_reply(comment.comment_id.parse()?)
796 .await?
797 };
798
799 result.push(ShortComment {
800 id: comment.comment_id.parse()?,
801 user: UserInfo {
802 nickname: comment.reader_info.reader_name.trim().to_string(),
803 avatar: comment.reader_info.avatar_url,
804 },
805 content,
806 create_time: Some(comment.ctime),
807 like_count: None,
808 replies,
809 });
810 }
811
812 if result.is_empty() {
813 Ok(None)
814 } else {
815 result.sort_unstable_by_key(|x| x.create_time.unwrap());
816 result.dedup();
817 Ok(Some(result))
818 }
819 }
820
821 async fn review_comment_reply(
822 &self,
823 comment_id: u32,
824 ) -> Result<Option<Vec<ShortComment>>, Error> {
825 let response: ReviewCommentReplyResponse = self
826 .post(
827 "/book/get_review_comment_reply_list",
828 ReviewCommentReplyRequest {
829 comment_id,
830 page: 0,
831 count: 9999,
832 },
833 )
834 .await?;
835 utils::check_response_success(response.code, response.tip)?;
836
837 let mut result = Vec::with_capacity(4);
838
839 for reply in response.data.unwrap().review_comment_reply_list {
840 let Some(content) = super::parse_multi_line(reply.reply_content) else {
841 continue;
842 };
843
844 let comment = ShortComment {
845 id: reply.reply_id.parse()?,
846 user: UserInfo {
847 nickname: reply.reader_info.reader_name.trim().to_string(),
848 avatar: reply.reader_info.avatar_url,
849 },
850 content,
851 create_time: Some(reply.ctime),
852 like_count: None,
853 replies: None,
854 };
855
856 result.push(comment);
857 }
858
859 if result.is_empty() {
860 Ok(None)
861 } else {
862 result.sort_unstable_by_key(|x| x.create_time.unwrap());
863 result.dedup();
864 Ok(Some(result))
865 }
866 }
867
868 async fn verify_type<T>(&self, username: T) -> Result<VerifyType, Error>
869 where
870 T: AsRef<str>,
871 {
872 let response: UseGeetestResponse = self
873 .post(
874 "/signup/use_geetest",
875 UseGeetestRequest {
876 login_name: username.as_ref().to_string(),
877 },
878 )
879 .await?;
880 utils::check_response_success(response.code, response.tip)?;
881
882 let need_use_geetest = response.data.unwrap().need_use_geetest;
883 if need_use_geetest == "0" {
884 Ok(VerifyType::None)
885 } else if need_use_geetest == "1" {
886 Ok(VerifyType::Geetest)
887 } else if need_use_geetest == "2" {
888 Ok(VerifyType::VerifyCode)
889 } else {
890 unreachable!("The value range of need_use_geetest is 0..=2");
891 }
892 }
893
894 async fn no_verification_login(
895 &self,
896 username: String,
897 password: String,
898 ) -> Result<Config, Error> {
899 let response: LoginResponse = self
900 .post(
901 "/signup/login",
902 LoginRequest {
903 login_name: username.clone(),
904 passwd: CiweimaoClient::rsa_encrypt(&password)?,
905 sign: CiweimaoClient::rsa_encrypt(&format!("{username}_{password}"))?,
906 },
907 )
908 .await?;
909 utils::check_response_success(response.code, response.tip)?;
910
911 let data = response.data.unwrap();
912
913 Ok(Config {
914 account: data.reader_info.account,
915 login_token: data.login_token,
916 reader_id: data.reader_info.reader_id.parse().unwrap(),
917 })
918 }
919
920 async fn geetest_login(&self, username: String, password: String) -> Result<Config, Error> {
921 let info = self.geetest_info(&username).await?;
922 let geetest_challenge = info.challenge.clone();
923
924 let validate = if info.success == 1 {
925 unreachable!("The verification code cannot be enabled at this time");
927 } else {
928 geetest_challenge.clone()
929 };
930
931 let response: LoginResponse = self
932 .post(
933 "/signup/login",
934 LoginCaptchaRequest {
935 login_name: username.clone(),
936 passwd: CiweimaoClient::rsa_encrypt(&password)?,
937 sign: CiweimaoClient::rsa_encrypt(&format!("{username}_{password}"))?,
938 geetest_seccode: validate.clone() + "|jordan",
939 geetest_validate: validate,
940 geetest_challenge,
941 },
942 )
943 .await?;
944 utils::check_response_success(response.code, response.tip)?;
945
946 let data = response.data.unwrap();
947
948 Ok(Config {
949 account: data.reader_info.account,
950 login_token: data.login_token,
951 reader_id: data.reader_info.reader_id.parse().unwrap(),
952 })
953 }
954
955 async fn geetest_info<T>(&self, username: T) -> Result<GeetestInfoResponse, Error>
956 where
957 T: AsRef<str>,
958 {
959 let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
960
961 let response: GeetestInfoResponse = self
962 .get_query(
963 "/signup/geetest_first_register",
964 GeetestInfoRequest {
965 t: timestamp,
966 user_id: username.as_ref().to_string(),
967 },
968 )
969 .await?;
970
971 Ok(response)
972 }
973
974 async fn sms_login(&self, username: String, password: String) -> Result<Config, Error> {
975 let timestamp = SystemTime::now()
976 .duration_since(UNIX_EPOCH)
977 .unwrap()
978 .as_millis();
979
980 let response: SendVerifyCodeResponse = self
981 .post(
982 "/signup/send_verify_code",
983 SendVerifyCodeRequest {
984 login_name: username.clone(),
985 timestamp,
986 verify_type: 5,
988 hashvalue: self.hashvalue(timestamp)?,
989 },
990 )
991 .await?;
992 utils::check_response_success(response.code, response.tip)?;
993
994 let response: LoginResponse = self
995 .post(
996 "/signup/login",
997 LoginSMSRequest {
998 login_name: username.clone(),
999 passwd: CiweimaoClient::rsa_encrypt(&password)?,
1000 sign: CiweimaoClient::rsa_encrypt(&format!("{username}_{password}"))?,
1001 to_code: response.data.unwrap().to_code,
1002 ver_code: crate::input("Please enter SMS verification code")?,
1003 },
1004 )
1005 .await?;
1006 utils::check_response_success(response.code, response.tip)?;
1007
1008 let data = response.data.unwrap();
1009
1010 Ok(Config {
1011 account: data.reader_info.account,
1012 login_token: data.login_token,
1013 reader_id: data.reader_info.reader_id.parse().unwrap(),
1014 })
1015 }
1016
1017 async fn shelf_list(&self) -> Result<Vec<u32>, Error> {
1018 let response: ShelfListResponse = self
1019 .post("/bookshelf/get_shelf_list", EmptyRequest {})
1020 .await?;
1021 utils::check_response_success(response.code, response.tip)?;
1022
1023 let mut result = Vec::new();
1024 for shelf in response.data.unwrap().shelf_list {
1025 result.push(shelf.shelf_id.parse()?);
1026 }
1027
1028 Ok(result)
1029 }
1030
1031 async fn chapter_prices(&self, novel_id: u32) -> Result<HashMap<u32, u16>, Error> {
1032 let response: PriceResponse = self
1033 .post(
1034 "/chapter/get_chapter_permission_list",
1035 PriceRequest { book_id: novel_id },
1036 )
1037 .await?;
1038 utils::check_response_success(response.code, response.tip)?;
1039 let chapter_permission_list = response.data.unwrap().chapter_permission_list;
1040
1041 let mut result = HashMap::new();
1042
1043 for item in chapter_permission_list {
1044 result.insert(item.chapter_id.parse()?, item.unit_hlb.parse()?);
1045 }
1046
1047 Ok(result)
1048 }
1049
1050 async fn check_download_cpt(&self) -> Result<(), Error> {
1051 let response: GenericResponse = self
1052 .post("/chapter/check_download_cpt", EmptyRequest {})
1053 .await?;
1054 if response.code == CiweimaoClient::NEED_TO_UPGRADE_VERSION {
1055 let _ = self.geetest_info(self.try_account()).await?;
1057 } else {
1058 utils::check_response_success(response.code, response.tip)?;
1059 }
1060
1061 Ok(())
1062 }
1063
1064 async fn chapter_cmd(&self, ids: &[u32]) -> Result<String, Error> {
1065 let response: ChapterCmdResponse = self
1066 .post(
1067 "/chapter/get_chapter_download_cmd",
1068 ChapterCmdRequest {
1069 chapter_id: itertools::join(ids, ","),
1070 },
1071 )
1072 .await?;
1073 utils::check_response_success(response.code, response.tip)?;
1074
1075 Ok(response.data.unwrap().command)
1076 }
1077
1078 fn match_update_days(option: &Options, update_time: Timestamp) -> bool {
1079 if option.update_days.is_none() {
1080 return true;
1081 }
1082
1083 let other_time = update_time
1084 .checked_add((*option.update_days.as_ref().unwrap() as i32).days())
1085 .unwrap();
1086
1087 Timestamp::now() <= other_time
1088 }
1089
1090 fn match_word_count(option: &Options, word_count: u32) -> bool {
1091 if option.word_count.is_none() {
1092 return true;
1093 }
1094
1095 match option.word_count.as_ref().unwrap() {
1096 WordCountRange::RangeTo(range_to) => word_count <= range_to.end,
1097 WordCountRange::Range(range) => range.start <= word_count && word_count <= range.end,
1098 WordCountRange::RangeFrom(range_from) => range_from.start <= word_count,
1099 }
1100 }
1101
1102 fn match_excluded_tags(option: &Options, tag_ids: Vec<String>) -> bool {
1103 if option.excluded_tags.is_none() {
1104 return true;
1105 }
1106
1107 tag_ids.iter().all(|name| {
1108 !option
1109 .excluded_tags
1110 .as_ref()
1111 .unwrap()
1112 .iter()
1113 .any(|tag| tag.name == *name)
1114 })
1115 }
1116
1117 fn parse_url<T>(str: T) -> Option<Url>
1118 where
1119 T: AsRef<str>,
1120 {
1121 let str = str.as_ref();
1122 if str.is_empty() {
1123 return None;
1124 }
1125
1126 match Url::parse(str) {
1127 Ok(url) => Some(url),
1128 Err(error) => {
1129 tracing::error!("Url parse failed: {error}, content: {str}");
1130 None
1131 }
1132 }
1133 }
1134
1135 async fn parse_tags(&self, tag_list: Vec<NovelInfoTag>) -> Result<Option<Vec<Tag>>, Error> {
1136 let sys_tags = self.tags().await?;
1137
1138 let mut result = Vec::new();
1139 for tag in tag_list {
1140 let name = tag.tag_name.trim().to_string();
1141
1142 if sys_tags.iter().any(|item| item.name == name) {
1144 result.push(Tag { id: None, name });
1145 } else {
1146 tracing::info!("This tag is not a system tag and is ignored: {name}");
1147 }
1148 }
1149
1150 if result.is_empty() {
1151 Ok(None)
1152 } else {
1153 Ok(Some(result))
1154 }
1155 }
1156
1157 async fn parse_category<T>(&self, str: T) -> Result<Option<Category>, Error>
1158 where
1159 T: AsRef<str>,
1160 {
1161 let str = str.as_ref();
1162 if str.is_empty() {
1163 return Ok(None);
1164 }
1165
1166 let categories = self.categories().await?;
1167
1168 match str.parse::<u16>() {
1169 Ok(index) => match categories.iter().find(|item| item.id == Some(index)) {
1170 Some(category) => Ok(Some(category.clone())),
1171 None => {
1172 tracing::error!("The category index does not exist: {str}");
1173 Ok(None)
1174 }
1175 },
1176 Err(error) => {
1177 tracing::error!("category_index parse failed: {error}");
1178 Ok(None)
1179 }
1180 }
1181 }
1182
1183 fn parse_image_url<T>(str: T) -> Option<Url>
1184 where
1185 T: AsRef<str>,
1186 {
1187 let str = str.as_ref();
1188 if str.is_empty() {
1189 return None;
1190 }
1191
1192 let fragment = Html::parse_fragment(str);
1193 let selector = Selector::parse("img").unwrap();
1194
1195 let element = fragment.select(&selector).next();
1196 if element.is_none() {
1197 tracing::error!("No `img` element exists: {str}");
1198 return None;
1199 }
1200 let element = element.unwrap();
1201
1202 let url = element.value().attr("src");
1203 if url.is_none() {
1204 tracing::error!("No `src` attribute exists: {str}");
1205 return None;
1206 }
1207 let url = url.unwrap();
1208
1209 CiweimaoClient::parse_url(url.trim())
1210 }
1211}