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 option.category.is_some() {
611 category_index = option.category.as_ref().unwrap().id.unwrap();
612 }
613
614 let mut tags = Vec::new();
615 if option.tags.is_some() {
616 for tag in option.tags.as_ref().unwrap() {
617 tags.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 option.word_count.is_some() {
632 match option.word_count.as_ref().unwrap() {
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 option.update_days.is_some() {
657 let update_days = *option.update_days.as_ref().unwrap();
658
659 if update_days <= 3 {
660 filter_uptime = Some(1)
661 } else if update_days <= 7 {
662 filter_uptime = Some(2)
663 } else if update_days <= 15 {
664 filter_uptime = Some(3)
665 } else if update_days <= 30 {
666 filter_uptime = Some(4)
667 }
668 }
669
670 let order = if option.keyword.is_some() {
671 None
674 } else {
675 Some("week_click")
677 };
678
679 let response: SearchResponse = self
680 .post(
681 "/bookcity/get_filter_search_book_list",
682 SearchRequest {
683 count: size,
684 page,
685 order,
686 category_index,
687 tags: sonic_rs::json!(tags).to_string(),
688 key: option.keyword.clone(),
689 is_paid,
690 up_status,
691 filter_uptime,
692 filter_word,
693 },
694 )
695 .await?;
696 utils::check_response_success(response.code, response.tip)?;
697
698 let book_list = response.data.unwrap().book_list;
699 if book_list.is_empty() {
700 return Ok(None);
701 }
702
703 let mut result = Vec::new();
704 let sys_tags = self.tags().await?;
705
706 for novel_info in book_list {
707 let mut tag_names = Vec::new();
708 for tag in novel_info.tag_list {
709 if let Some(sys_tag) = sys_tags.iter().find(|x| x.name == tag.tag_name.trim()) {
710 tag_names.push(sys_tag.name.clone());
711 }
712 }
713
714 if CiweimaoClient::match_update_days(option, novel_info.uptime)
715 && CiweimaoClient::match_excluded_tags(option, tag_names)
716 && CiweimaoClient::match_word_count(option, novel_info.total_word_count.parse()?)
717 {
718 result.push(novel_info.book_id.parse()?);
719 }
720 }
721
722 Ok(Some(result))
723 }
724
725 fn has_this_type_of_comments(comment_type: CommentType) -> bool {
726 match comment_type {
727 CommentType::Short => true,
728 CommentType::Long => true,
729 }
730 }
731}
732
733#[must_use]
734enum VerifyType {
735 None,
736 Geetest,
737 VerifyCode,
738}
739
740impl CiweimaoClient {
741 async fn review_comment(
742 &self,
743 review_id: u32,
744 comment_amount: u16,
745 ) -> Result<Option<Vec<ShortComment>>, Error> {
746 let response: ReviewCommentResponse = self
747 .post(
748 "/book/get_review_comment_list",
749 ReviewCommentRequest {
750 review_id,
751 page: 0,
752 count: comment_amount,
753 },
754 )
755 .await?;
756 utils::check_response_success(response.code, response.tip)?;
757 let review_comment_list = response.data.unwrap().review_comment_list;
758
759 let mut result = Vec::with_capacity(review_comment_list.len());
760
761 for comment in review_comment_list {
762 let Some(content) = super::parse_multi_line(comment.comment_content) else {
763 continue;
764 };
765
766 let replies = if comment.review_comment_reply_list.is_empty() {
767 None
768 } else if
769 comment.review_comment_reply_list.len() <= 2 {
771 let mut result = Vec::with_capacity(2);
772
773 for reply in comment.review_comment_reply_list {
774 let Some(content) = super::parse_multi_line(reply.reply_content) else {
775 continue;
776 };
777
778 result.push(ShortComment {
779 id: reply.reply_id.parse()?,
780 user: UserInfo {
781 nickname: reply.reader_info.reader_name.trim().to_string(),
782 avatar: reply.reader_info.avatar_url,
783 },
784 content,
785 create_time: Some(reply.ctime),
786 like_count: None,
787 replies: None,
788 })
789 }
790
791 if result.is_empty() {
792 None
793 } else {
794 result.sort_unstable_by_key(|x| x.create_time.unwrap());
795 result.dedup();
796 Some(result)
797 }
798 } else {
799 self.review_comment_reply(comment.comment_id.parse()?)
800 .await?
801 };
802
803 result.push(ShortComment {
804 id: comment.comment_id.parse()?,
805 user: UserInfo {
806 nickname: comment.reader_info.reader_name.trim().to_string(),
807 avatar: comment.reader_info.avatar_url,
808 },
809 content,
810 create_time: Some(comment.ctime),
811 like_count: None,
812 replies,
813 });
814 }
815
816 if result.is_empty() {
817 Ok(None)
818 } else {
819 result.sort_unstable_by_key(|x| x.create_time.unwrap());
820 result.dedup();
821 Ok(Some(result))
822 }
823 }
824
825 async fn review_comment_reply(
826 &self,
827 comment_id: u32,
828 ) -> Result<Option<Vec<ShortComment>>, Error> {
829 let response: ReviewCommentReplyResponse = self
830 .post(
831 "/book/get_review_comment_reply_list",
832 ReviewCommentReplyRequest {
833 comment_id,
834 page: 0,
835 count: 9999,
836 },
837 )
838 .await?;
839 utils::check_response_success(response.code, response.tip)?;
840
841 let mut result = Vec::with_capacity(4);
842
843 for reply in response.data.unwrap().review_comment_reply_list {
844 let Some(content) = super::parse_multi_line(reply.reply_content) else {
845 continue;
846 };
847
848 let comment = ShortComment {
849 id: reply.reply_id.parse()?,
850 user: UserInfo {
851 nickname: reply.reader_info.reader_name.trim().to_string(),
852 avatar: reply.reader_info.avatar_url,
853 },
854 content,
855 create_time: Some(reply.ctime),
856 like_count: None,
857 replies: None,
858 };
859
860 result.push(comment);
861 }
862
863 if result.is_empty() {
864 Ok(None)
865 } else {
866 result.sort_unstable_by_key(|x| x.create_time.unwrap());
867 result.dedup();
868 Ok(Some(result))
869 }
870 }
871
872 async fn verify_type<T>(&self, username: T) -> Result<VerifyType, Error>
873 where
874 T: AsRef<str>,
875 {
876 let response: UseGeetestResponse = self
877 .post(
878 "/signup/use_geetest",
879 UseGeetestRequest {
880 login_name: username.as_ref().to_string(),
881 },
882 )
883 .await?;
884 utils::check_response_success(response.code, response.tip)?;
885
886 let need_use_geetest = response.data.unwrap().need_use_geetest;
887 if need_use_geetest == "0" {
888 Ok(VerifyType::None)
889 } else if need_use_geetest == "1" {
890 Ok(VerifyType::Geetest)
891 } else if need_use_geetest == "2" {
892 Ok(VerifyType::VerifyCode)
893 } else {
894 unreachable!("The value range of need_use_geetest is 0..=2");
895 }
896 }
897
898 async fn no_verification_login(
899 &self,
900 username: String,
901 password: String,
902 ) -> Result<Config, Error> {
903 let response: LoginResponse = self
904 .post(
905 "/signup/login",
906 LoginRequest {
907 login_name: username.clone(),
908 passwd: CiweimaoClient::rsa_encrypt(&password)?,
909 sign: CiweimaoClient::rsa_encrypt(&format!("{username}_{password}"))?,
910 },
911 )
912 .await?;
913 utils::check_response_success(response.code, response.tip)?;
914
915 let data = response.data.unwrap();
916
917 Ok(Config {
918 account: data.reader_info.account,
919 login_token: data.login_token,
920 reader_id: data.reader_info.reader_id.parse().unwrap(),
921 })
922 }
923
924 async fn geetest_login(&self, username: String, password: String) -> Result<Config, Error> {
925 let info = self.geetest_info(&username).await?;
926 let geetest_challenge = info.challenge.clone();
927
928 let validate = if info.success == 1 {
929 server::run_geetest(info).await?
930 } else {
931 geetest_challenge.clone()
932 };
933
934 let response: LoginResponse = self
935 .post(
936 "/signup/login",
937 LoginCaptchaRequest {
938 login_name: username.clone(),
939 passwd: CiweimaoClient::rsa_encrypt(&password)?,
940 sign: CiweimaoClient::rsa_encrypt(&format!("{username}_{password}"))?,
941 geetest_seccode: validate.clone() + "|jordan",
942 geetest_validate: validate,
943 geetest_challenge,
944 },
945 )
946 .await?;
947 utils::check_response_success(response.code, response.tip)?;
948
949 let data = response.data.unwrap();
950
951 Ok(Config {
952 account: data.reader_info.account,
953 login_token: data.login_token,
954 reader_id: data.reader_info.reader_id.parse().unwrap(),
955 })
956 }
957
958 async fn geetest_info<T>(&self, username: T) -> Result<GeetestInfoResponse, Error>
959 where
960 T: AsRef<str>,
961 {
962 let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
963
964 let response: GeetestInfoResponse = self
965 .get_query(
966 "/signup/geetest_first_register",
967 GeetestInfoRequest {
968 t: timestamp,
969 user_id: username.as_ref().to_string(),
970 },
971 )
972 .await?;
973
974 Ok(response)
975 }
976
977 async fn sms_login(&self, username: String, password: String) -> Result<Config, Error> {
978 let timestamp = SystemTime::now()
979 .duration_since(UNIX_EPOCH)
980 .unwrap()
981 .as_millis();
982
983 let response: SendVerifyCodeResponse = self
984 .post(
985 "/signup/send_verify_code",
986 SendVerifyCodeRequest {
987 login_name: username.clone(),
988 timestamp,
989 verify_type: 5,
991 hashvalue: self.hashvalue(timestamp),
992 },
993 )
994 .await?;
995 utils::check_response_success(response.code, response.tip)?;
996
997 let response: LoginResponse = self
998 .post(
999 "/signup/login",
1000 LoginSMSRequest {
1001 login_name: username.clone(),
1002 passwd: CiweimaoClient::rsa_encrypt(&password)?,
1003 sign: CiweimaoClient::rsa_encrypt(&format!("{username}_{password}"))?,
1004 to_code: response.data.unwrap().to_code,
1005 ver_code: crate::input("Please enter SMS verification code")?,
1006 },
1007 )
1008 .await?;
1009 utils::check_response_success(response.code, response.tip)?;
1010
1011 let data = response.data.unwrap();
1012
1013 Ok(Config {
1014 account: data.reader_info.account,
1015 login_token: data.login_token,
1016 reader_id: data.reader_info.reader_id.parse().unwrap(),
1017 })
1018 }
1019
1020 async fn shelf_list(&self) -> Result<Vec<u32>, Error> {
1021 let response: ShelfListResponse = self
1022 .post("/bookshelf/get_shelf_list", EmptyRequest {})
1023 .await?;
1024 utils::check_response_success(response.code, response.tip)?;
1025
1026 let mut result = Vec::new();
1027 for shelf in response.data.unwrap().shelf_list {
1028 result.push(shelf.shelf_id.parse()?);
1029 }
1030
1031 Ok(result)
1032 }
1033
1034 async fn chapter_prices(&self, novel_id: u32) -> Result<HashMap<u32, u16>, Error> {
1035 let response: PriceResponse = self
1036 .post(
1037 "/chapter/get_chapter_permission_list",
1038 PriceRequest { book_id: novel_id },
1039 )
1040 .await?;
1041 utils::check_response_success(response.code, response.tip)?;
1042 let chapter_permission_list = response.data.unwrap().chapter_permission_list;
1043
1044 let mut result = HashMap::new();
1045
1046 for item in chapter_permission_list {
1047 result.insert(item.chapter_id.parse()?, item.unit_hlb.parse()?);
1048 }
1049
1050 Ok(result)
1051 }
1052
1053 async fn check_download_cpt(&self) -> Result<(), Error> {
1054 let response: GenericResponse = self
1055 .post("/chapter/check_download_cpt", EmptyRequest {})
1056 .await?;
1057 if response.code == CiweimaoClient::NEED_TO_UPGRADE_VERSION {
1058 let _ = self.geetest_info(self.try_account()).await?;
1060 } else {
1061 utils::check_response_success(response.code, response.tip)?;
1062 }
1063
1064 Ok(())
1065 }
1066
1067 async fn chapter_cmd(&self, ids: &[u32]) -> Result<String, Error> {
1068 let response: ChapterCmdResponse = self
1069 .post(
1070 "/chapter/get_chapter_download_cmd",
1071 ChapterCmdRequest {
1072 chapter_id: itertools::join(ids, ","),
1073 },
1074 )
1075 .await?;
1076 utils::check_response_success(response.code, response.tip)?;
1077
1078 Ok(response.data.unwrap().command)
1079 }
1080
1081 fn match_update_days(option: &Options, update_time: NaiveDateTime) -> bool {
1082 if option.update_days.is_none() {
1083 return true;
1084 }
1085
1086 let other_time = Shanghai.from_local_datetime(&update_time).unwrap()
1087 + Duration::try_days(*option.update_days.as_ref().unwrap() as i64).unwrap();
1088
1089 Local::now() <= other_time
1090 }
1091
1092 fn match_word_count(option: &Options, word_count: u32) -> bool {
1093 if option.word_count.is_none() {
1094 return true;
1095 }
1096
1097 match option.word_count.as_ref().unwrap() {
1098 WordCountRange::RangeTo(range_to) => word_count <= range_to.end,
1099 WordCountRange::Range(range) => range.start <= word_count && word_count <= range.end,
1100 WordCountRange::RangeFrom(range_from) => range_from.start <= word_count,
1101 }
1102 }
1103
1104 fn match_excluded_tags(option: &Options, tag_ids: Vec<String>) -> bool {
1105 if option.excluded_tags.is_none() {
1106 return true;
1107 }
1108
1109 tag_ids.iter().all(|name| {
1110 !option
1111 .excluded_tags
1112 .as_ref()
1113 .unwrap()
1114 .iter()
1115 .any(|tag| tag.name == *name)
1116 })
1117 }
1118
1119 fn parse_url<T>(str: T) -> Option<Url>
1120 where
1121 T: AsRef<str>,
1122 {
1123 let str = str.as_ref();
1124 if str.is_empty() {
1125 return None;
1126 }
1127
1128 match Url::parse(str) {
1129 Ok(url) => Some(url),
1130 Err(error) => {
1131 tracing::error!("Url parse failed: {error}, content: {str}");
1132 None
1133 }
1134 }
1135 }
1136
1137 async fn parse_tags(&self, tag_list: Vec<NovelInfoTag>) -> Result<Option<Vec<Tag>>, Error> {
1138 let sys_tags = self.tags().await?;
1139
1140 let mut result = Vec::new();
1141 for tag in tag_list {
1142 let name = tag.tag_name.trim().to_string();
1143
1144 if sys_tags.iter().any(|item| item.name == name) {
1146 result.push(Tag { id: None, name });
1147 } else {
1148 tracing::info!("This tag is not a system tag and is ignored: {name}");
1149 }
1150 }
1151
1152 if result.is_empty() {
1153 Ok(None)
1154 } else {
1155 Ok(Some(result))
1156 }
1157 }
1158
1159 async fn parse_category<T>(&self, str: T) -> Result<Option<Category>, Error>
1160 where
1161 T: AsRef<str>,
1162 {
1163 let str = str.as_ref();
1164 if str.is_empty() {
1165 return Ok(None);
1166 }
1167
1168 let categories = self.categories().await?;
1169
1170 match str.parse::<u16>() {
1171 Ok(index) => match categories.iter().find(|item| item.id == Some(index)) {
1172 Some(category) => Ok(Some(category.clone())),
1173 None => {
1174 tracing::error!("The category index does not exist: {str}");
1175 Ok(None)
1176 }
1177 },
1178 Err(error) => {
1179 tracing::error!("category_index parse failed: {error}");
1180 Ok(None)
1181 }
1182 }
1183 }
1184
1185 fn parse_image_url<T>(str: T) -> Option<Url>
1186 where
1187 T: AsRef<str>,
1188 {
1189 let str = str.as_ref();
1190 if str.is_empty() {
1191 return None;
1192 }
1193
1194 let fragment = Html::parse_fragment(str);
1195 let selector = Selector::parse("img").unwrap();
1196
1197 let element = fragment.select(&selector).next();
1198 if element.is_none() {
1199 tracing::error!("No `img` element exists: {str}");
1200 return None;
1201 }
1202 let element = element.unwrap();
1203
1204 let url = element.value().attr("src");
1205 if url.is_none() {
1206 tracing::error!("No `src` attribute exists: {str}");
1207 return None;
1208 }
1209 let url = url.unwrap();
1210
1211 CiweimaoClient::parse_url(url.trim())
1212 }
1213}