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