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