novel_api/sfacg/
mod.rs

1mod structure;
2mod utils;
3
4use std::io::Cursor;
5use std::path::PathBuf;
6
7use chrono::{DateTime, Utc};
8use chrono_tz::Asia::Shanghai;
9use chrono_tz::Tz;
10use image::{DynamicImage, ImageReader};
11use tokio::sync::OnceCell;
12use url::Url;
13
14use self::structure::*;
15use crate::{
16    Category, ChapterInfo, Client, Comment, CommentType, ContentInfo, ContentInfos, Error,
17    FindImageResult, FindTextResult, HTTPClient, LongComment, NovelDB, NovelInfo, Options,
18    ShortComment, Tag, UserInfo, VolumeInfo, VolumeInfos, WordCountRange,
19};
20
21/// Sfacg client, use it to access Apis
22#[must_use]
23pub struct SfacgClient {
24    proxy: Option<Url>,
25    no_proxy: bool,
26    cert_path: Option<PathBuf>,
27
28    client: OnceCell<HTTPClient>,
29    client_rss: OnceCell<HTTPClient>,
30
31    db: OnceCell<NovelDB>,
32}
33
34impl Client for SfacgClient {
35    fn proxy(&mut self, proxy: Url) {
36        self.proxy = Some(proxy);
37    }
38
39    fn no_proxy(&mut self) {
40        self.no_proxy = true;
41    }
42
43    fn cert(&mut self, cert_path: PathBuf) {
44        self.cert_path = Some(cert_path);
45    }
46
47    async fn shutdown(&self) -> Result<(), Error> {
48        self.client().await?.save_cookies()
49    }
50
51    async fn add_cookie(&self, cookie_str: &str, url: &Url) -> Result<(), Error> {
52        self.client().await?.add_cookie(cookie_str, url)
53    }
54
55    async fn log_in(&self, username: String, password: Option<String>) -> Result<(), Error> {
56        assert!(!username.is_empty());
57        assert!(password.is_some());
58
59        let password = password.unwrap();
60
61        let response: GenericResponse = self
62            .post("/sessions", LogInRequest { username, password })
63            .await?;
64        response.status.check()?;
65
66        Ok(())
67    }
68
69    async fn logged_in(&self) -> Result<bool, Error> {
70        let response: GenericResponse = self.get("/user").await?;
71
72        if response.status.unauthorized() {
73            Ok(false)
74        } else {
75            response.status.check()?;
76            Ok(true)
77        }
78    }
79
80    async fn user_info(&self) -> Result<UserInfo, Error> {
81        let response: UserInfoResponse = self.get("/user").await?;
82        response.status.check()?;
83        let data = response.data.unwrap();
84
85        Ok(UserInfo {
86            nickname: data.nick_name.trim().to_string(),
87            avatar: Some(data.avatar),
88        })
89    }
90
91    async fn money(&self) -> Result<u32, Error> {
92        let response: MoneyResponse = self.get("/user/money").await?;
93        response.status.check()?;
94        let data = response.data.unwrap();
95
96        Ok(data.fire_money_remain + data.coupons_remain)
97    }
98
99    async fn sign_in(&self) -> Result<(), Error> {
100        let now: DateTime<Tz> = Utc::now().with_timezone(&Shanghai);
101
102        let response: GenericResponse = self
103            .put(
104                "/user/newSignInfo",
105                SignRequest {
106                    sign_date: now.format("%Y-%m-%d").to_string(),
107                },
108            )
109            .await?;
110        if response.status.already_signed_in() {
111            tracing::info!("{}", response.status.msg.unwrap().trim())
112        } else {
113            response.status.check()?;
114        }
115
116        Ok(())
117    }
118
119    async fn bookshelf_infos(&self) -> Result<Vec<u32>, Error> {
120        let response: BookshelfInfoResponse = self
121            .get_query("/user/Pockets", BookshelfInfoRequest { expand: "novels" })
122            .await?;
123        response.status.check()?;
124        let data = response.data.unwrap();
125
126        let mut result = Vec::with_capacity(32);
127        for info in data {
128            if info.expand.is_some() {
129                let novels = info.expand.unwrap().novels;
130
131                if let Some(novels) = novels {
132                    for novel_info in novels {
133                        result.push(novel_info.novel_id);
134                    }
135                }
136            }
137        }
138
139        Ok(result)
140    }
141
142    async fn novel_info(&self, id: u32) -> Result<Option<NovelInfo>, Error> {
143        assert!(id > 0 && id <= i32::MAX as u32);
144
145        let response: NovelInfoResponse = self
146            .get_query(
147                format!("/novels/{id}"),
148                NovelInfoRequest {
149                    expand: "intro,typeName,sysTags",
150                },
151            )
152            .await?;
153        if response.status.not_found() {
154            return Ok(None);
155        }
156        response.status.check()?;
157        let data = response.data.unwrap();
158
159        let category = Category {
160            id: Some(data.type_id),
161            parent_id: None,
162            name: data.expand.type_name.trim().to_string(),
163        };
164
165        let novel_info = NovelInfo {
166            id,
167            name: data.novel_name.trim().to_string(),
168            author_name: data.author_name.trim().to_string(),
169            cover_url: Some(data.novel_cover),
170            introduction: super::parse_multi_line(data.expand.intro),
171            word_count: SfacgClient::parse_word_count(data.char_count),
172            is_vip: Some(data.sign_status == "VIP"),
173            is_finished: Some(data.is_finish),
174            create_time: Some(data.add_time),
175            update_time: Some(data.last_update_time),
176            category: Some(category),
177            tags: self.parse_tags(data.expand.sys_tags).await?,
178        };
179
180        Ok(Some(novel_info))
181    }
182
183    async fn comments(
184        &self,
185        id: u32,
186        comment_type: CommentType,
187        need_replies: bool,
188        page: u16,
189        size: u16,
190    ) -> Result<Option<Vec<Comment>>, Error> {
191        assert!(id <= i32::MAX as u32);
192
193        match comment_type {
194            CommentType::Short => self.do_short_comments(id, need_replies, page, size).await,
195            CommentType::Long => self.do_long_comments(id, need_replies, page, size).await,
196        }
197    }
198
199    async fn volume_infos(&self, id: u32) -> Result<Option<VolumeInfos>, Error> {
200        assert!(id <= i32::MAX as u32);
201
202        let response: VolumeInfosResponse = self.get(format!("/novels/{id}/dirs")).await?;
203
204        if response.status.not_available() {
205            return Ok(None);
206        }
207
208        response.status.check()?;
209        let data = response.data.unwrap();
210
211        let mut volumes = VolumeInfos::with_capacity(8);
212        for volume in data.volume_list {
213            let mut volume_info = VolumeInfo {
214                id: volume.volume_id,
215                title: volume.title.trim().to_string(),
216                chapter_infos: Vec::with_capacity(volume.chapter_list.len()),
217            };
218
219            for chapter in volume.chapter_list {
220                let chapter_info = ChapterInfo {
221                    novel_id: Some(chapter.novel_id),
222                    id: chapter.chap_id,
223                    title: chapter.title.trim().to_string(),
224                    word_count: Some(chapter.char_count),
225                    create_time: Some(chapter.add_time),
226                    update_time: chapter.update_time,
227                    is_vip: Some(chapter.is_vip),
228                    price: Some(chapter.need_fire_money),
229                    payment_required: Some(chapter.need_fire_money != 0),
230                    is_valid: None,
231                };
232
233                volume_info.chapter_infos.push(chapter_info);
234            }
235
236            volumes.push(volume_info);
237        }
238
239        Ok(Some(volumes))
240    }
241
242    async fn content_infos(&self, info: &ChapterInfo) -> Result<ContentInfos, Error> {
243        let content;
244
245        match self.db().await?.find_text(info).await? {
246            FindTextResult::Ok(str) => {
247                content = str;
248            }
249            other => {
250                let response: ContentInfosResponse = self
251                    .get_query(
252                        format!("/Chaps/{}", info.id),
253                        ContentInfosRequest {
254                            expand: "content,isContentEncrypted",
255                        },
256                    )
257                    .await?;
258                response.status.check()?;
259                let data = response.data.unwrap();
260
261                if data.expand.is_content_encrypted {
262                    content = SfacgClient::convert(data.expand.content);
263                } else {
264                    content = data.expand.content;
265                }
266
267                if content.trim().is_empty() {
268                    return Err(Error::NovelApi(String::from("Content is empty")));
269                }
270
271                match other {
272                    FindTextResult::None => self.db().await?.insert_text(info, &content).await?,
273                    FindTextResult::Outdate => self.db().await?.update_text(info, &content).await?,
274                    FindTextResult::Ok(_) => (),
275                }
276            }
277        }
278
279        let mut content_infos = ContentInfos::with_capacity(128);
280        for line in content
281            .lines()
282            .map(|line| line.trim())
283            .filter(|line| !line.is_empty())
284        {
285            if line.starts_with("[img") {
286                match SfacgClient::parse_image_url(line) {
287                    Ok(url) => content_infos.push(ContentInfo::Image(url)),
288                    Err(err) => tracing::error!("{err}"),
289                }
290            } else {
291                content_infos.push(ContentInfo::Text(line.to_string()));
292            }
293        }
294
295        Ok(content_infos)
296    }
297
298    async fn content_infos_multiple(
299        &self,
300        infos: &[ChapterInfo],
301    ) -> Result<Vec<ContentInfos>, Error> {
302        let mut result = Vec::new();
303
304        for info in infos {
305            result.push(self.content_infos(info).await?);
306        }
307
308        Ok(result)
309    }
310
311    async fn order_chapter(&self, info: &ChapterInfo) -> Result<(), Error> {
312        let response: GenericResponse = self
313            .post(
314                &format!("/novels/{}/orderedchaps", info.novel_id.unwrap()),
315                OrderRequest {
316                    order_all: false,
317                    auto_order: false,
318                    chap_ids: vec![info.id],
319                    order_type: "readOrder",
320                },
321            )
322            .await?;
323        if response.status.already_ordered() {
324            tracing::info!("{}", response.status.msg.unwrap().trim())
325        } else {
326            response.status.check()?;
327        }
328
329        Ok(())
330    }
331
332    async fn order_novel(&self, id: u32, _: &VolumeInfos) -> Result<(), Error> {
333        assert!(id > 0 && id <= i32::MAX as u32);
334
335        let response: GenericResponse = self
336            .post(
337                &format!("/novels/{id}/orderedchaps",),
338                OrderRequest {
339                    order_all: true,
340                    auto_order: false,
341                    chap_ids: vec![],
342                    order_type: "readOrder",
343                },
344            )
345            .await?;
346        if response.status.already_ordered() {
347            tracing::info!("{}", response.status.msg.unwrap().trim())
348        } else {
349            response.status.check()?;
350        }
351
352        Ok(())
353    }
354
355    async fn image(&self, url: &Url) -> Result<DynamicImage, Error> {
356        match self.db().await?.find_image(url).await? {
357            FindImageResult::Ok(image) => Ok(image),
358            FindImageResult::None => {
359                let response = self.get_rss(url).await?;
360                let bytes = response.bytes().await?;
361
362                let image = ImageReader::new(Cursor::new(&bytes))
363                    .with_guessed_format()?
364                    .decode()?;
365
366                self.db().await?.insert_image(url, bytes).await?;
367
368                Ok(image)
369            }
370        }
371    }
372
373    async fn categories(&self) -> Result<&Vec<Category>, Error> {
374        static CATEGORIES: OnceCell<Vec<Category>> = OnceCell::const_new();
375
376        CATEGORIES
377            .get_or_try_init(|| async {
378                let response: CategoryResponse = self.get("/noveltypes").await?;
379                response.status.check()?;
380                let data = response.data.unwrap();
381
382                let mut result = Vec::with_capacity(8);
383                for tag_data in data {
384                    result.push(Category {
385                        id: Some(tag_data.type_id),
386                        parent_id: None,
387                        name: tag_data.type_name.trim().to_string(),
388                    });
389                }
390
391                result.sort_unstable_by_key(|x| x.id.unwrap());
392
393                Ok(result)
394            })
395            .await
396    }
397
398    async fn tags(&self) -> Result<&Vec<Tag>, Error> {
399        static TAGS: OnceCell<Vec<Tag>> = OnceCell::const_new();
400
401        TAGS.get_or_try_init(|| async {
402            let response: TagResponse = self.get("/novels/0/sysTags").await?;
403            response.status.check()?;
404            let data = response.data.unwrap();
405
406            let mut result = Vec::with_capacity(64);
407            for tag_data in data {
408                result.push(Tag {
409                    id: Some(tag_data.sys_tag_id),
410                    name: tag_data.tag_name.trim().to_string(),
411                });
412            }
413
414            // Tag that have been removed, but can still be used
415            result.push(Tag {
416                id: Some(74),
417                name: "百合".to_string(),
418            });
419
420            result.sort_unstable_by_key(|x| x.id.unwrap());
421
422            Ok(result)
423        })
424        .await
425    }
426
427    async fn search_infos(
428        &self,
429        option: &Options,
430        page: u16,
431        size: u16,
432    ) -> Result<Option<Vec<u32>>, Error> {
433        assert!(size <= 50, "The maximum number of items per page is 50");
434
435        if option.keyword.is_some() {
436            self.do_search_with_keyword(option, page, size).await
437        } else {
438            self.do_search_without_keyword(option, page, size).await
439        }
440    }
441
442    fn has_this_type_of_comments(comment_type: CommentType) -> bool {
443        match comment_type {
444            CommentType::Short => true,
445            CommentType::Long => true,
446        }
447    }
448}
449
450impl SfacgClient {
451    async fn do_short_comments(
452        &self,
453        id: u32,
454        need_replies: bool,
455        page: u16,
456        size: u16,
457    ) -> Result<Option<Vec<Comment>>, Error> {
458        assert!(size <= 50);
459
460        let response: CommentResponse = self
461            .get_query(
462                format!("/novels/{id}/Cmts"),
463                ShortCommentRequest {
464                    page,
465                    size,
466                    r#type: "clear",
467                    sort: "smart",
468                },
469            )
470            .await?;
471        response.status.check()?;
472        let data = response.data.unwrap();
473
474        if data.is_empty() {
475            return Ok(None);
476        }
477
478        let mut result = Vec::with_capacity(data.len());
479
480        for comment in data {
481            let Some(content) = super::parse_multi_line(comment.content) else {
482                continue;
483            };
484
485            result.push(Comment::Short(ShortComment {
486                id: comment.comment_id,
487                user: UserInfo {
488                    nickname: comment.display_name.trim().to_string(),
489                    avatar: Some(comment.avatar),
490                },
491                content,
492                create_time: Some(comment.create_time),
493                like_count: Some(comment.fav_count),
494                replies: if need_replies && comment.reply_num > 0 {
495                    self.comment_replies(comment.comment_id, CommentType::Short, comment.reply_num)
496                        .await?
497                } else {
498                    None
499                },
500            }));
501        }
502
503        Ok(Some(result))
504    }
505
506    async fn do_long_comments(
507        &self,
508        id: u32,
509        need_replies: bool,
510        page: u16,
511        size: u16,
512    ) -> Result<Option<Vec<Comment>>, Error> {
513        assert!(size <= 20);
514
515        let response: CommentResponse = self
516            .get_query(
517                format!("/novels/{id}/lcmts"),
518                LongCommentRequest {
519                    page,
520                    size,
521                    charlen: 140,
522                    sort: "addtime",
523                },
524            )
525            .await?;
526        response.status.check()?;
527        let data = response.data.unwrap();
528
529        if data.is_empty() {
530            return Ok(None);
531        }
532
533        let mut result = Vec::with_capacity(data.len());
534
535        for comment in data {
536            result.push(Comment::Long(LongComment {
537                id: comment.comment_id,
538                user: UserInfo {
539                    nickname: comment.display_name.trim().to_string(),
540                    avatar: Some(comment.avatar),
541                },
542                title: comment.title.unwrap().trim().to_string(),
543                content: self.long_comment_content(comment.comment_id).await?,
544                create_time: Some(comment.create_time),
545                like_count: Some(comment.fav_count),
546                replies: if need_replies && comment.reply_num > 0 {
547                    self.comment_replies(comment.comment_id, CommentType::Long, comment.reply_num)
548                        .await?
549                } else {
550                    None
551                },
552            }));
553        }
554
555        Ok(Some(result))
556    }
557
558    async fn long_comment_content(&self, comment_id: u32) -> Result<Vec<String>, Error> {
559        let response: LongCommentContentResponse = self.get(format!("/lcmts/{comment_id}")).await?;
560        response.status.check()?;
561        let data = response.data.unwrap();
562
563        Ok(super::parse_multi_line(data.content).unwrap())
564    }
565
566    async fn comment_replies(
567        &self,
568        comment_id: u32,
569        comment_type: CommentType,
570        total: u16,
571    ) -> Result<Option<Vec<ShortComment>>, Error> {
572        let url = match comment_type {
573            CommentType::Short => format!("/cmts/{comment_id}/replys"),
574            CommentType::Long => format!("/lcmts/{comment_id}/replys"),
575        };
576
577        let mut page = 0;
578        let size = 50;
579        let total_page = if total.is_multiple_of(size) {
580            total / size
581        } else {
582            total / size + 1
583        };
584        let mut reply_list = Vec::with_capacity(total as usize);
585
586        while page < total_page {
587            let response: ReplyResponse = self.get_query(&url, ReplyRequest { page, size }).await?;
588            response.status.check()?;
589            let data = response.data.unwrap();
590
591            for reply in data {
592                let Some(content) = super::parse_multi_line(reply.content) else {
593                    continue;
594                };
595
596                reply_list.push(ShortComment {
597                    id: reply.reply_id,
598                    user: UserInfo {
599                        nickname: reply.display_name.trim().to_string(),
600                        avatar: Some(reply.avatar),
601                    },
602                    content,
603                    create_time: Some(reply.create_time),
604                    like_count: None,
605                    replies: None,
606                });
607            }
608
609            page += 1;
610        }
611
612        if reply_list.is_empty() {
613            Ok(None)
614        } else {
615            reply_list.sort_unstable_by_key(|x| x.create_time.unwrap());
616            reply_list.dedup();
617            Ok(Some(reply_list))
618        }
619    }
620
621    async fn do_search_with_keyword(
622        &self,
623        option: &Options,
624        page: u16,
625        size: u16,
626    ) -> Result<Option<Vec<u32>>, Error> {
627        // 0 连载中
628        // 1 已完结
629        // -1 不限
630        let is_finish = if option.is_finished.is_none() {
631            -1
632        } else if *option.is_finished.as_ref().unwrap() {
633            1
634        } else {
635            0
636        };
637
638        // -1 不限
639        let update_days = if option.update_days.is_none() {
640            -1
641        } else {
642            option.update_days.unwrap() as i8
643        };
644
645        let response: SearchResponse = self
646            .get_query(
647                "/search/novels/result/new",
648                SearchRequest {
649                    q: option.keyword.as_ref().unwrap().to_string(),
650                    is_finish,
651                    update_days,
652                    systagids: SfacgClient::tag_ids(&option.tags),
653                    page,
654                    size,
655                    // hot 人气最高
656                    // update 最新更新
657                    // marknum 收藏最高
658                    // ticket 月票最多
659                    // charcount 更新最多
660                    sort: "hot",
661                    expand: "sysTags",
662                },
663            )
664            .await?;
665        response.status.check()?;
666        let data = response.data.unwrap();
667
668        if data.novels.is_empty() {
669            return Ok(None);
670        }
671
672        let mut result = Vec::new();
673        let sys_tags = self.tags().await?;
674
675        for novel_info in data.novels {
676            let mut tag_ids = vec![];
677
678            for tag in novel_info.expand.sys_tags {
679                if let Some(sys_tag) = sys_tags.iter().find(|x| x.id.unwrap() == tag.sys_tag_id) {
680                    tag_ids.push(sys_tag.id.unwrap());
681                }
682            }
683
684            if SfacgClient::match_category(option, novel_info.type_id)
685                && SfacgClient::match_excluded_tags(option, tag_ids)
686                && SfacgClient::match_vip(option, &novel_info.sign_status)
687                && SfacgClient::match_word_count(option, novel_info.char_count)
688            {
689                result.push(novel_info.novel_id);
690            }
691        }
692
693        Ok(Some(result))
694    }
695
696    async fn do_search_without_keyword(
697        &self,
698        option: &Options,
699        page: u16,
700        size: u16,
701    ) -> Result<Option<Vec<u32>>, Error> {
702        let mut category_id = 0;
703        if option.category.is_some() {
704            category_id = option.category.as_ref().unwrap().id.unwrap();
705        }
706
707        // -1 不限
708        let updatedays = if option.update_days.is_none() {
709            -1
710        } else {
711            option.update_days.unwrap() as i8
712        };
713
714        let isfinish = SfacgClient::bool_to_str(&option.is_finished);
715        let isfree = SfacgClient::bool_to_str(&option.is_vip.as_ref().map(|x| !x));
716
717        let systagids = SfacgClient::tag_ids(&option.tags);
718        let notexcludesystagids = SfacgClient::tag_ids(&option.excluded_tags);
719
720        let mut charcountbegin = 0;
721        let mut charcountend = 0;
722
723        if option.word_count.is_some() {
724            match option.word_count.as_ref().unwrap() {
725                WordCountRange::Range(range) => {
726                    charcountbegin = range.start;
727                    charcountend = range.end;
728                }
729                WordCountRange::RangeFrom(range_from) => charcountbegin = range_from.start,
730                WordCountRange::RangeTo(range_to) => charcountend = range_to.end,
731            }
732        }
733
734        let response: NovelsResponse = self
735            .get_query(
736                format!("/novels/{category_id}/sysTags/novels"),
737                NovelsRequest {
738                    charcountbegin,
739                    charcountend,
740                    isfinish,
741                    isfree,
742                    systagids,
743                    notexcludesystagids,
744                    updatedays,
745                    page,
746                    size,
747                    // latest 最新更新
748                    // viewtimes 人气最高
749                    // bookmark 收藏最高
750                    // ticket 月票最多
751                    // charcount 更新最多
752                    sort: "viewtimes",
753                },
754            )
755            .await?;
756        response.status.check()?;
757        let data = response.data.unwrap();
758
759        if data.is_empty() {
760            return Ok(None);
761        }
762
763        let mut result = Vec::new();
764        for novel_data in data {
765            result.push(novel_data.novel_id);
766        }
767
768        Ok(Some(result))
769    }
770
771    fn parse_word_count(word_count: i32) -> Option<u32> {
772        // Some novels have negative word counts
773        if word_count <= 0 {
774            None
775        } else {
776            Some(word_count as u32)
777        }
778    }
779
780    async fn parse_tags(&self, tag_list: Vec<NovelInfoSysTag>) -> Result<Option<Vec<Tag>>, Error> {
781        let sys_tags = self.tags().await?;
782
783        let mut result = Vec::new();
784        for tag in tag_list {
785            let id = tag.sys_tag_id;
786            let name = tag.tag_name.trim().to_string();
787
788            // Remove non-system tags
789            if sys_tags.iter().any(|sys_tag| sys_tag.id.unwrap() == id) {
790                result.push(Tag { id: Some(id), name });
791            } else {
792                tracing::info!("This tag is not a system tag and is ignored: {name}");
793            }
794        }
795
796        if result.is_empty() {
797            Ok(None)
798        } else {
799            result.sort_unstable_by_key(|x| x.id.unwrap());
800            Ok(Some(result))
801        }
802    }
803
804    fn parse_image_url(line: &str) -> Result<Url, Error> {
805        let begin = line.find("http");
806        let end = line.find("[/img]");
807
808        if begin.is_none() || end.is_none() {
809            return Err(Error::NovelApi(format!(
810                "Image URL format is incorrect: {line}"
811            )));
812        }
813
814        let begin = begin.unwrap();
815        let end = end.unwrap();
816
817        let url = line
818            .chars()
819            .skip(begin)
820            .take(end - begin)
821            .collect::<String>()
822            .trim()
823            .to_string();
824
825        match Url::parse(&url) {
826            Ok(url) => Ok(url),
827            Err(error) => Err(Error::NovelApi(format!(
828                "Image URL parse failed: {error}, content: {line}"
829            ))),
830        }
831    }
832
833    fn bool_to_str(flag: &Option<bool>) -> &'static str {
834        if flag.is_some() {
835            if *flag.as_ref().unwrap() { "is" } else { "not" }
836        } else {
837            "both"
838        }
839    }
840
841    fn tag_ids(tags: &Option<Vec<Tag>>) -> Option<String> {
842        tags.as_ref().map(|tags| {
843            tags.iter()
844                .map(|tag| tag.id.unwrap().to_string())
845                .collect::<Vec<String>>()
846                .join(",")
847        })
848    }
849
850    fn match_vip(option: &Options, sign_status: &str) -> bool {
851        if option.is_vip.is_none() {
852            return true;
853        }
854
855        if *option.is_vip.as_ref().unwrap() {
856            sign_status == "VIP"
857        } else {
858            sign_status != "VIP"
859        }
860    }
861
862    fn match_excluded_tags(option: &Options, tag_ids: Vec<u16>) -> bool {
863        if option.excluded_tags.is_none() {
864            return true;
865        }
866
867        tag_ids.iter().all(|id| {
868            !option
869                .excluded_tags
870                .as_ref()
871                .unwrap()
872                .iter()
873                .any(|tag| tag.id.unwrap() == *id)
874        })
875    }
876
877    fn match_category(option: &Options, category_id: u16) -> bool {
878        if option.category.is_none() {
879            return true;
880        }
881
882        let category = option.category.as_ref().unwrap();
883        category.id.unwrap() == category_id
884    }
885
886    fn match_word_count(option: &Options, word_count: i32) -> bool {
887        if option.word_count.is_none() {
888            return true;
889        }
890
891        if word_count <= 0 {
892            return true;
893        }
894
895        let word_count = word_count as u32;
896        match option.word_count.as_ref().unwrap() {
897            WordCountRange::Range(range) => {
898                if word_count >= range.start && word_count < range.end {
899                    return true;
900                }
901            }
902            WordCountRange::RangeFrom(range_from) => {
903                if word_count >= range_from.start {
904                    return true;
905                }
906            }
907            WordCountRange::RangeTo(rang_to) => {
908                if word_count < rang_to.end {
909                    return true;
910                }
911            }
912        }
913
914        false
915    }
916}