novel_api/ciweimao/
mod.rs

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/// Ciweimao client, use it to access Apis
35#[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                    // always 1, from `/task/get_all_task_list`
141                    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            // 1 讨论
220            // 2 长评
221            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                    // 部分评论为空白字符
249                    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                    // 评论数量可能为负数,e.g. 100089784
255                    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                // e.g. 该章节未审核通过
340                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                    // mtime 应为更新时间,但是 Android 端存在 bug,似乎导致 mtime 为创建时间
351                    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            // When using keyword search, many irrelevant items will appear in the search results
623            // If you use sorting, you will not be able to obtain the target items
624            None
625        } else {
626            // 人气排序
627            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            // 返回的最大数量似乎为 3 个
721            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                    // always 5
941                    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            // NOTE 当前 success 总是 0,无法正常使用验证码
1010            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            // Remove non-system tags
1096            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}