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