novel_api/ciweimao/
mod.rs

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