1mod server;
2mod structure;
3mod utils;
4
5use std::io::Cursor;
6use std::path::PathBuf;
7use std::sync::RwLock;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use chrono::{Duration, Local, NaiveDateTime, TimeZone};
11use chrono_tz::Asia::Shanghai;
12use hashbrown::HashMap;
13use image::{DynamicImage, ImageReader};
14use scraper::{Html, Selector};
15use serde::{Deserialize, Serialize};
16use tokio::sync::OnceCell;
17use url::Url;
18
19use self::structure::*;
20use crate::{
21 Category, ChapterInfo, Client, Comment, CommentType, ContentInfo, ContentInfos, Error,
22 FindImageResult, FindTextResult, HTTPClient, LongComment, NovelDB, NovelInfo, Options,
23 ShortComment, Tag, UserInfo, VolumeInfo, VolumeInfos, WordCountRange,
24};
25
26#[must_use]
27#[derive(Serialize, Deserialize)]
28pub(crate) struct Config {
29 account: String,
30 login_token: String,
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 let content;
369
370 match self.db().await?.find_text(info).await? {
371 FindTextResult::Ok(str) => {
372 content = str;
373 }
374 other => {
375 let cmd = self.chapter_cmd(info.id).await?;
376 let key = crate::sha256(cmd.as_bytes());
377
378 let response: ChapsResponse = self
379 .post(
380 "/chapter/get_cpt_ifm",
381 ChapsRequest {
382 chapter_id: info.id.to_string(),
383 chapter_command: cmd,
384 },
385 )
386 .await?;
387 utils::check_response_success(response.code, response.tip)?;
388
389 content = simdutf8::basic::from_utf8(&crate::aes_256_cbc_no_iv_base64_decrypt(
390 key,
391 response.data.unwrap().chapter_info.txt_content,
392 )?)?
393 .to_string();
394
395 if content.trim().is_empty() {
396 return Err(Error::NovelApi(String::from("Content is empty")));
397 }
398
399 match other {
400 FindTextResult::None => self.db().await?.insert_text(info, &content).await?,
401 FindTextResult::Outdate => self.db().await?.update_text(info, &content).await?,
402 FindTextResult::Ok(_) => (),
403 }
404 }
405 }
406
407 let mut content_infos = ContentInfos::new();
408 for line in content
409 .lines()
410 .map(|line| line.trim())
411 .filter(|line| !line.is_empty())
412 {
413 if line.starts_with("<img") {
414 if let Some(url) = CiweimaoClient::parse_image_url(line) {
415 content_infos.push(ContentInfo::Image(url));
416 }
417 } else {
418 content_infos.push(ContentInfo::Text(line.to_string()));
419 }
420 }
421
422 Ok(content_infos)
423 }
424
425 async fn order_chapter(&self, info: &ChapterInfo) -> Result<(), Error> {
426 let response: GenericResponse = self
427 .post(
428 "/chapter/buy",
429 OrderChapterRequest {
430 chapter_id: info.id.to_string(),
431 },
432 )
433 .await?;
434 utils::check_response_success(response.code, response.tip)?;
435
436 Ok(())
437 }
438
439 async fn order_novel(&self, id: u32, infos: &VolumeInfos) -> Result<(), Error> {
440 assert!(id > 0);
441
442 let mut chapter_id_list = Vec::new();
443 for volume in infos {
444 for chapter in &volume.chapter_infos {
445 if chapter.payment_required() {
446 chapter_id_list.push(chapter.id.to_string());
447 }
448 }
449 }
450 if chapter_id_list.is_empty() {
451 return Ok(());
452 }
453
454 let chapter_id_list = sonic_rs::json!(chapter_id_list).to_string();
455
456 let response: GenericResponse = self
457 .post("/chapter/buy_multi", OrderNovelRequest { chapter_id_list })
458 .await?;
459 utils::check_response_success(response.code, response.tip)?;
460
461 Ok(())
462 }
463
464 async fn image(&self, url: &Url) -> Result<DynamicImage, Error> {
465 match self.db().await?.find_image(url).await? {
466 FindImageResult::Ok(image) => Ok(image),
467 FindImageResult::None => {
468 let response = self.get_rss(url).await?;
469 let bytes = response.bytes().await?;
470
471 let image = ImageReader::new(Cursor::new(&bytes))
472 .with_guessed_format()?
473 .decode()?;
474
475 self.db().await?.insert_image(url, bytes).await?;
476
477 Ok(image)
478 }
479 }
480 }
481
482 async fn categories(&self) -> Result<&Vec<Category>, Error> {
483 static CATEGORIES: OnceCell<Vec<Category>> = OnceCell::const_new();
484
485 CATEGORIES
486 .get_or_try_init(|| async {
487 let response: CategoryResponse =
488 self.post("/meta/get_meta_data", EmptyRequest {}).await?;
489 utils::check_response_success(response.code, response.tip)?;
490
491 let mut result = Vec::new();
492 for category in response.data.unwrap().category_list {
493 for category_detail in category.category_detail {
494 result.push(Category {
495 id: Some(category_detail.category_index.parse()?),
496 parent_id: None,
497 name: category_detail.category_name.trim().to_string(),
498 });
499 }
500 }
501
502 result.sort_unstable_by_key(|x| x.id.unwrap());
503
504 Ok(result)
505 })
506 .await
507 }
508
509 async fn tags(&self) -> Result<&Vec<Tag>, Error> {
510 static TAGS: OnceCell<Vec<Tag>> = OnceCell::const_new();
511
512 TAGS.get_or_try_init(|| async {
513 let response: TagResponse = self
514 .post("/book/get_official_tag_list", EmptyRequest {})
515 .await?;
516 utils::check_response_success(response.code, response.tip)?;
517
518 let mut result = Vec::new();
519 for tag in response.data.unwrap().official_tag_list {
520 result.push(Tag {
521 id: None,
522 name: tag.tag_name.trim().to_string(),
523 });
524 }
525
526 result.push(Tag {
527 id: None,
528 name: String::from("橘子"),
529 });
530 result.push(Tag {
531 id: None,
532 name: String::from("变身"),
533 });
534 result.push(Tag {
535 id: None,
536 name: String::from("性转"),
537 });
538 result.push(Tag {
539 id: None,
540 name: String::from("纯百"),
541 });
542
543 Ok(result)
544 })
545 .await
546 }
547
548 async fn search_infos(
549 &self,
550 option: &Options,
551 page: u16,
552 size: u16,
553 ) -> Result<Option<Vec<u32>>, Error> {
554 let mut category_index = 0;
555 if option.category.is_some() {
556 category_index = option.category.as_ref().unwrap().id.unwrap();
557 }
558
559 let mut tags = Vec::new();
560 if option.tags.is_some() {
561 for tag in option.tags.as_ref().unwrap() {
562 tags.push(sonic_rs::json!({
563 "tag": tag.name,
564 "filter": "1"
565 }));
566 }
567 }
568
569 let is_paid = option.is_vip.map(|is_vip| if is_vip { 1 } else { 0 });
570
571 let up_status = option
572 .is_finished
573 .map(|is_finished| if is_finished { 1 } else { 0 });
574
575 let mut filter_word = None;
576 if option.word_count.is_some() {
577 match option.word_count.as_ref().unwrap() {
578 WordCountRange::RangeTo(range_to) => {
579 if range_to.end < 30_0000 {
580 filter_word = Some(1);
581 }
582 }
583 WordCountRange::Range(range) => {
584 if range.start >= 30_0000 && range.end < 50_0000 {
585 filter_word = Some(2);
586 } else if range.start >= 50_0000 && range.end < 100_0000 {
587 filter_word = Some(3);
588 } else if range.start >= 100_0000 && range.end < 200_0000 {
589 filter_word = Some(4);
590 }
591 }
592 WordCountRange::RangeFrom(range_from) => {
593 if range_from.start >= 200_0000 {
594 filter_word = Some(5);
595 }
596 }
597 }
598 }
599
600 let mut filter_uptime = None;
601 if option.update_days.is_some() {
602 let update_days = *option.update_days.as_ref().unwrap();
603
604 if update_days <= 3 {
605 filter_uptime = Some(1)
606 } else if update_days <= 7 {
607 filter_uptime = Some(2)
608 } else if update_days <= 15 {
609 filter_uptime = Some(3)
610 } else if update_days <= 30 {
611 filter_uptime = Some(4)
612 }
613 }
614
615 let order = if option.keyword.is_some() {
616 None
619 } else {
620 Some("week_click")
622 };
623
624 let response: SearchResponse = self
625 .post(
626 "/bookcity/get_filter_search_book_list",
627 SearchRequest {
628 count: size,
629 page,
630 order,
631 category_index,
632 tags: sonic_rs::json!(tags).to_string(),
633 key: option.keyword.clone(),
634 is_paid,
635 up_status,
636 filter_uptime,
637 filter_word,
638 },
639 )
640 .await?;
641 utils::check_response_success(response.code, response.tip)?;
642
643 let book_list = response.data.unwrap().book_list;
644 if book_list.is_empty() {
645 return Ok(None);
646 }
647
648 let mut result = Vec::new();
649 let sys_tags = self.tags().await?;
650
651 for novel_info in book_list {
652 let mut tag_names = Vec::new();
653 for tag in novel_info.tag_list {
654 if let Some(sys_tag) = sys_tags.iter().find(|x| x.name == tag.tag_name.trim()) {
655 tag_names.push(sys_tag.name.clone());
656 }
657 }
658
659 if CiweimaoClient::match_update_days(option, novel_info.uptime)
660 && CiweimaoClient::match_excluded_tags(option, tag_names)
661 && CiweimaoClient::match_word_count(option, novel_info.total_word_count.parse()?)
662 {
663 result.push(novel_info.book_id.parse()?);
664 }
665 }
666
667 Ok(Some(result))
668 }
669
670 fn has_this_type_of_comments(comment_type: CommentType) -> bool {
671 match comment_type {
672 CommentType::Short => true,
673 CommentType::Long => true,
674 }
675 }
676}
677
678#[must_use]
679enum VerifyType {
680 None,
681 Geetest,
682 VerifyCode,
683}
684
685impl CiweimaoClient {
686 async fn review_comment(
687 &self,
688 review_id: u32,
689 comment_amount: u16,
690 ) -> Result<Option<Vec<ShortComment>>, Error> {
691 let response: ReviewCommentResponse = self
692 .post(
693 "/book/get_review_comment_list",
694 ReviewCommentRequest {
695 review_id,
696 page: 0,
697 count: comment_amount,
698 },
699 )
700 .await?;
701 utils::check_response_success(response.code, response.tip)?;
702 let review_comment_list = response.data.unwrap().review_comment_list;
703
704 let mut result = Vec::with_capacity(review_comment_list.len());
705
706 for comment in review_comment_list {
707 let Some(content) = super::parse_multi_line(comment.comment_content) else {
708 continue;
709 };
710
711 let replies = if comment.review_comment_reply_list.is_empty() {
712 None
713 } else if
714 comment.review_comment_reply_list.len() <= 2 {
716 let mut result = Vec::with_capacity(2);
717
718 for reply in comment.review_comment_reply_list {
719 let Some(content) = super::parse_multi_line(reply.reply_content) else {
720 continue;
721 };
722
723 result.push(ShortComment {
724 id: reply.reply_id.parse()?,
725 user: UserInfo {
726 nickname: reply.reader_info.reader_name.trim().to_string(),
727 avatar: reply.reader_info.avatar_url,
728 },
729 content,
730 create_time: Some(reply.ctime),
731 like_count: None,
732 replies: None,
733 })
734 }
735
736 if result.is_empty() {
737 None
738 } else {
739 result.sort_unstable_by_key(|x| x.create_time.unwrap());
740 result.dedup();
741 Some(result)
742 }
743 } else {
744 self.review_comment_reply(comment.comment_id.parse()?)
745 .await?
746 };
747
748 result.push(ShortComment {
749 id: comment.comment_id.parse()?,
750 user: UserInfo {
751 nickname: comment.reader_info.reader_name.trim().to_string(),
752 avatar: comment.reader_info.avatar_url,
753 },
754 content,
755 create_time: Some(comment.ctime),
756 like_count: None,
757 replies,
758 });
759 }
760
761 if result.is_empty() {
762 Ok(None)
763 } else {
764 result.sort_unstable_by_key(|x| x.create_time.unwrap());
765 result.dedup();
766 Ok(Some(result))
767 }
768 }
769
770 async fn review_comment_reply(
771 &self,
772 comment_id: u32,
773 ) -> Result<Option<Vec<ShortComment>>, Error> {
774 let response: ReviewCommentReplyResponse = self
775 .post(
776 "/book/get_review_comment_reply_list",
777 ReviewCommentReplyRequest {
778 comment_id,
779 page: 0,
780 count: 9999,
781 },
782 )
783 .await?;
784 utils::check_response_success(response.code, response.tip)?;
785
786 let mut result = Vec::with_capacity(4);
787
788 for reply in response.data.unwrap().review_comment_reply_list {
789 let Some(content) = super::parse_multi_line(reply.reply_content) else {
790 continue;
791 };
792
793 let comment = ShortComment {
794 id: reply.reply_id.parse()?,
795 user: UserInfo {
796 nickname: reply.reader_info.reader_name.trim().to_string(),
797 avatar: reply.reader_info.avatar_url,
798 },
799 content,
800 create_time: Some(reply.ctime),
801 like_count: None,
802 replies: None,
803 };
804
805 result.push(comment);
806 }
807
808 if result.is_empty() {
809 Ok(None)
810 } else {
811 result.sort_unstable_by_key(|x| x.create_time.unwrap());
812 result.dedup();
813 Ok(Some(result))
814 }
815 }
816
817 async fn verify_type<T>(&self, username: T) -> Result<VerifyType, Error>
818 where
819 T: AsRef<str>,
820 {
821 let response: UseGeetestResponse = self
822 .post(
823 "/signup/use_geetest",
824 UseGeetestRequest {
825 login_name: username.as_ref().to_string(),
826 },
827 )
828 .await?;
829 utils::check_response_success(response.code, response.tip)?;
830
831 let need_use_geetest = response.data.unwrap().need_use_geetest;
832 if need_use_geetest == "0" {
833 Ok(VerifyType::None)
834 } else if need_use_geetest == "1" {
835 Ok(VerifyType::Geetest)
836 } else if need_use_geetest == "2" {
837 Ok(VerifyType::VerifyCode)
838 } else {
839 unreachable!("The value range of need_use_geetest is 0..=2");
840 }
841 }
842
843 async fn no_verification_login(
844 &self,
845 username: String,
846 password: String,
847 ) -> Result<Config, Error> {
848 let response: LoginResponse = self
849 .post(
850 "/signup/login",
851 LoginRequest {
852 login_name: username,
853 passwd: password,
854 },
855 )
856 .await?;
857 utils::check_response_success(response.code, response.tip)?;
858
859 let data = response.data.unwrap();
860
861 Ok(Config {
862 account: data.reader_info.account,
863 login_token: data.login_token,
864 })
865 }
866
867 async fn geetest_login(&self, username: String, password: String) -> Result<Config, Error> {
868 let info = self.geetest_info(&username).await?;
869 let geetest_challenge = info.challenge.clone();
870
871 let validate = if info.success == 1 {
872 server::run_geetest(info).await?
873 } else {
874 geetest_challenge.clone()
875 };
876
877 let response: LoginResponse = self
878 .post(
879 "/signup/login",
880 LoginCaptchaRequest {
881 login_name: username,
882 passwd: password,
883 geetest_seccode: validate.clone() + "|jordan",
884 geetest_validate: validate,
885 geetest_challenge,
886 },
887 )
888 .await?;
889 utils::check_response_success(response.code, response.tip)?;
890
891 let data = response.data.unwrap();
892
893 Ok(Config {
894 account: data.reader_info.account,
895 login_token: data.login_token,
896 })
897 }
898
899 async fn geetest_info<T>(&self, username: T) -> Result<GeetestInfoResponse, Error>
900 where
901 T: AsRef<str>,
902 {
903 let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
904
905 let response: GeetestInfoResponse = self
906 .get_query(
907 "/signup/geetest_first_register",
908 GeetestInfoRequest {
909 t: timestamp,
910 user_id: username.as_ref().to_string(),
911 },
912 )
913 .await?;
914
915 Ok(response)
916 }
917
918 async fn sms_login(&self, username: String, password: String) -> Result<Config, Error> {
919 let timestamp = SystemTime::now()
920 .duration_since(UNIX_EPOCH)
921 .unwrap()
922 .as_millis();
923
924 let response: SendVerifyCodeResponse = self
925 .post(
926 "/signup/send_verify_code",
927 SendVerifyCodeRequest {
928 login_name: username.clone(),
929 timestamp,
930 verify_type: 5,
932 },
933 )
934 .await?;
935 utils::check_response_success(response.code, response.tip)?;
936
937 let response: LoginResponse = self
938 .post(
939 "/signup/login",
940 LoginSMSRequest {
941 login_name: username,
942 passwd: password,
943 to_code: response.data.unwrap().to_code,
944 ver_code: crate::input("Please enter SMS verification code")?,
945 },
946 )
947 .await?;
948 utils::check_response_success(response.code, response.tip)?;
949
950 let data = response.data.unwrap();
951
952 Ok(Config {
953 account: data.reader_info.account,
954 login_token: data.login_token,
955 })
956 }
957
958 async fn shelf_list(&self) -> Result<Vec<u32>, Error> {
959 let response: ShelfListResponse = self
960 .post("/bookshelf/get_shelf_list", EmptyRequest {})
961 .await?;
962 utils::check_response_success(response.code, response.tip)?;
963
964 let mut result = Vec::new();
965 for shelf in response.data.unwrap().shelf_list {
966 result.push(shelf.shelf_id.parse()?);
967 }
968
969 Ok(result)
970 }
971
972 async fn chapter_prices(&self, novel_id: u32) -> Result<HashMap<u32, u16>, Error> {
973 let response: PriceResponse = self
974 .post(
975 "/chapter/get_chapter_permission_list",
976 PriceRequest { book_id: novel_id },
977 )
978 .await?;
979 utils::check_response_success(response.code, response.tip)?;
980 let chapter_permission_list = response.data.unwrap().chapter_permission_list;
981
982 let mut result = HashMap::new();
983
984 for item in chapter_permission_list {
985 result.insert(item.chapter_id.parse()?, item.unit_hlb.parse()?);
986 }
987
988 Ok(result)
989 }
990
991 async fn chapter_cmd(&self, id: u32) -> Result<String, Error> {
992 let response: ChapterCmdResponse = self
993 .post(
994 "/chapter/get_chapter_cmd",
995 ChapterCmdRequest {
996 chapter_id: id.to_string(),
997 },
998 )
999 .await?;
1000 utils::check_response_success(response.code, response.tip)?;
1001
1002 Ok(response.data.unwrap().command)
1003 }
1004
1005 fn match_update_days(option: &Options, update_time: NaiveDateTime) -> bool {
1006 if option.update_days.is_none() {
1007 return true;
1008 }
1009
1010 let other_time = Shanghai.from_local_datetime(&update_time).unwrap()
1011 + Duration::try_days(*option.update_days.as_ref().unwrap() as i64).unwrap();
1012
1013 Local::now() <= other_time
1014 }
1015
1016 fn match_word_count(option: &Options, word_count: u32) -> bool {
1017 if option.word_count.is_none() {
1018 return true;
1019 }
1020
1021 match option.word_count.as_ref().unwrap() {
1022 WordCountRange::RangeTo(range_to) => word_count <= range_to.end,
1023 WordCountRange::Range(range) => range.start <= word_count && word_count <= range.end,
1024 WordCountRange::RangeFrom(range_from) => range_from.start <= word_count,
1025 }
1026 }
1027
1028 fn match_excluded_tags(option: &Options, tag_ids: Vec<String>) -> bool {
1029 if option.excluded_tags.is_none() {
1030 return true;
1031 }
1032
1033 tag_ids.iter().all(|name| {
1034 !option
1035 .excluded_tags
1036 .as_ref()
1037 .unwrap()
1038 .iter()
1039 .any(|tag| tag.name == *name)
1040 })
1041 }
1042
1043 fn parse_url<T>(str: T) -> Option<Url>
1044 where
1045 T: AsRef<str>,
1046 {
1047 let str = str.as_ref();
1048 if str.is_empty() {
1049 return None;
1050 }
1051
1052 match Url::parse(str) {
1053 Ok(url) => Some(url),
1054 Err(error) => {
1055 tracing::error!("Url parse failed: {error}, content: {str}");
1056 None
1057 }
1058 }
1059 }
1060
1061 async fn parse_tags(&self, tag_list: Vec<NovelInfoTag>) -> Result<Option<Vec<Tag>>, Error> {
1062 let sys_tags = self.tags().await?;
1063
1064 let mut result = Vec::new();
1065 for tag in tag_list {
1066 let name = tag.tag_name.trim().to_string();
1067
1068 if sys_tags.iter().any(|item| item.name == name) {
1070 result.push(Tag { id: None, name });
1071 } else {
1072 tracing::info!("This tag is not a system tag and is ignored: {name}");
1073 }
1074 }
1075
1076 if result.is_empty() {
1077 Ok(None)
1078 } else {
1079 Ok(Some(result))
1080 }
1081 }
1082
1083 async fn parse_category<T>(&self, str: T) -> Result<Option<Category>, Error>
1084 where
1085 T: AsRef<str>,
1086 {
1087 let str = str.as_ref();
1088 if str.is_empty() {
1089 return Ok(None);
1090 }
1091
1092 let categories = self.categories().await?;
1093
1094 match str.parse::<u16>() {
1095 Ok(index) => match categories.iter().find(|item| item.id == Some(index)) {
1096 Some(category) => Ok(Some(category.clone())),
1097 None => {
1098 tracing::error!("The category index does not exist: {str}");
1099 Ok(None)
1100 }
1101 },
1102 Err(error) => {
1103 tracing::error!("category_index parse failed: {error}");
1104 Ok(None)
1105 }
1106 }
1107 }
1108
1109 fn parse_image_url<T>(str: T) -> Option<Url>
1110 where
1111 T: AsRef<str>,
1112 {
1113 let str = str.as_ref();
1114 if str.is_empty() {
1115 return None;
1116 }
1117
1118 let fragment = Html::parse_fragment(str);
1119 let selector = Selector::parse("img").unwrap();
1120
1121 let element = fragment.select(&selector).next();
1122 if element.is_none() {
1123 tracing::error!("No `img` element exists: {str}");
1124 return None;
1125 }
1126 let element = element.unwrap();
1127
1128 let url = element.value().attr("src");
1129 if url.is_none() {
1130 tracing::error!("No `src` attribute exists: {str}");
1131 return None;
1132 }
1133 let url = url.unwrap();
1134
1135 CiweimaoClient::parse_url(url.trim())
1136 }
1137}