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