rpixiv/client/
app.rs

1use crate::error::{PixivError, Result};
2use crate::models::app::{
3    Comment, CommentsResponse, ContentType, Duration, Filter, FollowRestrict, IllustBookmarkResponse,
4    IllustDetail, IllustFollowResponse, RankingMode, RankingResponse, RecommendedResponse,
5    SearchIllustResponse, SearchTarget, Sort, TrendingTagsResponse, UgoiraMetadataResponse,
6    UserFollowingResponse, UserFollowerResponse, UserMypixivResponse,
7};
8use crate::network::HttpClient;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use tracing::debug;
12
13/// App API client for interacting with Pixiv App API
14#[derive(Debug, Clone)]
15pub struct AppClient {
16    /// HTTP client
17    http_client: HttpClient,
18    /// API base URL
19    base_url: String,
20}
21
22impl AppClient {
23    /// Create new App API client instance
24    pub fn new(http_client: HttpClient) -> Self {
25        Self {
26            http_client,
27            base_url: "https://app-api.pixiv.net".to_string(),
28        }
29    }
30
31    /// Set API base URL
32    pub fn set_base_url(&mut self, url: String) {
33        self.base_url = url;
34    }
35
36    /// Get API base URL
37    pub fn base_url(&self) -> &str {
38        &self.base_url
39    }
40
41    /// Get illustration details
42    ///
43    /// # Arguments
44    /// * `illust_id` - Illustration ID
45    ///
46    /// # Returns
47    /// Returns illustration detail information
48    ///
49    /// # Example
50    /// ```rust
51    /// let client = AppClient::new(http_client);
52    /// let detail = client.illust_detail(12345678).await?;
53    /// ```
54    pub async fn illust_detail(&self, illust_id: u64) -> Result<IllustDetail> {
55        debug!(illust_id = %illust_id, "Fetching illustration detail");
56        
57        let url = format!("{}/v1/illust/detail", self.base_url);
58        let params = [("illust_id", illust_id.to_string())];
59        
60        let response = self
61            .http_client
62            .send_request(reqwest::Method::GET, &url, Some(&params))
63            .await?;
64        
65        let text = response.text().await?;
66        let detail: IllustDetail = serde_json::from_str(&text)?;
67        
68        Ok(detail)
69    }
70
71    /// Get illustration ranking
72    ///
73    /// # Arguments
74    /// * `mode` - Ranking mode
75    /// * `filter` - Filter
76    /// * `date` - Date (format: YYYY-MM-DD)
77    /// * `offset` - Offset
78    ///
79    /// # Returns
80    /// Returns ranking response
81    ///
82    /// # Example
83    /// ```rust
84    /// let client = AppClient::new(http_client);
85    /// let ranking = client.illust_ranking(
86    ///     RankingMode::Day,
87    ///     Filter::ForIOS,
88    ///     Some("2023-01-01"),
89    ///     Some(0)
90    /// ).await?;
91    /// ```
92    pub async fn illust_ranking(
93        &self,
94        mode: RankingMode,
95        filter: Filter,
96        date: Option<&str>,
97        offset: Option<u32>,
98    ) -> Result<RankingResponse> {
99        debug!(
100            mode = %mode.to_string(),
101            filter = %filter.to_string(),
102            date = ?date,
103            offset = ?offset,
104            "Fetching illustration ranking"
105        );
106        
107        let url = format!("{}/v1/illust/ranking", self.base_url);
108        let mut params = Vec::new();
109        params.push(("mode", mode.to_string()));
110        params.push(("filter", filter.to_string()));
111        
112        if let Some(date) = date {
113            params.push(("date", date.to_string()));
114        }
115        
116        if let Some(offset) = offset {
117            params.push(("offset", offset.to_string()));
118        }
119        
120        let response = self
121            .http_client
122            .send_request(reqwest::Method::GET, &url, Some(&params))
123            .await?;
124        
125        let text = response.text().await?;
126        let ranking: RankingResponse = serde_json::from_str(&text)?;
127        
128        Ok(ranking)
129    }
130
131    /// Get recommended illustrations
132    ///
133    /// # Arguments
134    /// * `content_type` - Content type
135    /// * `include_ranking_label` - Whether to include ranking label
136    /// * `filter` - Filter
137    /// * `max_bookmark_id_for_recommend` - Maximum bookmark ID for recommendation
138    /// * `min_bookmark_id_for_recent_illust` - Minimum bookmark ID for recent illustrations
139    /// * `offset` - Offset
140    /// * `include_ranking_illusts` - Whether to include ranking illustrations
141    /// * `bookmark_illust_ids` - List of bookmarked illustration IDs
142    /// * `viewed` - List of viewed illustration IDs
143    ///
144    /// # Returns
145    /// Returns recommendation response
146    ///
147    /// # Example
148    /// ```rust
149    /// let client = AppClient::new(http_client);
150    /// let recommended = client.illust_recommended(
151    ///     ContentType::Illust,
152    ///     true,
153    ///     Filter::ForIOS,
154    ///     None,
155    ///     None,
156    ///     None,
157    ///     None,
158    ///     None,
159    ///     None
160    /// ).await?;
161    /// ```
162    pub async fn illust_recommended(
163        &self,
164        content_type: ContentType,
165        include_ranking_label: bool,
166        filter: Filter,
167        max_bookmark_id_for_recommend: Option<u64>,
168        min_bookmark_id_for_recent_illust: Option<u64>,
169        offset: Option<u32>,
170        include_ranking_illusts: Option<bool>,
171        bookmark_illust_ids: Option<Vec<u64>>,
172        viewed: Option<Vec<String>>,
173    ) -> Result<RecommendedResponse> {
174        debug!(
175            content_type = %content_type.to_string(),
176            include_ranking_label = %include_ranking_label,
177            filter = %filter.to_string(),
178            max_bookmark_id_for_recommend = ?max_bookmark_id_for_recommend,
179            min_bookmark_id_for_recent_illust = ?min_bookmark_id_for_recent_illust,
180            offset = ?offset,
181            include_ranking_illusts = ?include_ranking_illusts,
182            bookmark_illust_ids = ?bookmark_illust_ids,
183            viewed = ?viewed,
184            "Fetching recommended illustrations"
185        );
186        
187        let url = format!("{}/v1/illust/recommended", self.base_url);
188        let mut params = Vec::new();
189        params.push(("content_type".to_string(), content_type.to_string()));
190        params.push(("include_ranking_label".to_string(), include_ranking_label.to_string()));
191        params.push(("filter".to_string(), filter.to_string()));
192        
193        if let Some(max_bookmark_id_for_recommend) = max_bookmark_id_for_recommend {
194            params.push(("max_bookmark_id_for_recommend".to_string(), max_bookmark_id_for_recommend.to_string()));
195        }
196        
197        if let Some(min_bookmark_id_for_recent_illust) = min_bookmark_id_for_recent_illust {
198            params.push(("min_bookmark_id_for_recent_illust".to_string(), min_bookmark_id_for_recent_illust.to_string()));
199        }
200        
201        if let Some(offset) = offset {
202            params.push(("offset".to_string(), offset.to_string()));
203        }
204        
205        if let Some(include_ranking_illusts) = include_ranking_illusts {
206            params.push(("include_ranking_illusts".to_string(), include_ranking_illusts.to_string()));
207        }
208        
209        if let Some(bookmark_illust_ids) = bookmark_illust_ids {
210            let ids = bookmark_illust_ids
211                .iter()
212                .map(|id| id.to_string())
213                .collect::<Vec<_>>()
214                .join(",");
215            params.push(("bookmark_illust_ids".to_string(), ids));
216        }
217        
218        if let Some(viewed) = viewed {
219            for (i, viewed_id) in viewed.iter().enumerate() {
220                let key = format!("viewed[{}]", i);
221                params.push((key, viewed_id.to_string()));
222            }
223        }
224        
225        let response = self
226            .http_client
227            .send_request(reqwest::Method::GET, &url, Some(&params))
228            .await?;
229        
230        let text = response.text().await?;
231        let recommended: RecommendedResponse = serde_json::from_str(&text)?;
232        
233        Ok(recommended)
234    }
235
236    /// Search illustrations
237    ///
238    /// # Arguments
239    /// * `word` - Search keyword
240    /// * `search_target` - Search target
241    /// * `sort` - Sort method
242    /// * `duration` - Search duration
243    /// * `start_date` - Start date (format: YYYY-MM-DD)
244    /// * `end_date` - End date (format: YYYY-MM-DD)
245    /// * `filter` - Filter
246    /// * `search_ai_type` - AI type (0: Filter AI-generated works, 1: Show AI-generated works)
247    /// * `offset` - Offset
248    ///
249    /// # Returns
250    /// Returns search response
251    ///
252    /// # Example
253    /// ```rust
254    /// let client = AppClient::new(http_client);
255    /// let search_result = client.search_illust(
256    ///     "original",
257    ///     SearchTarget::PartialMatchForTags,
258    ///     Sort::DateDesc,
259    ///     None,
260    ///     None,
261    ///     None,
262    ///     Filter::ForIOS,
263    ///     None,
264    ///     None
265    /// ).await?;
266    /// ```
267    pub async fn search_illust(
268        &self,
269        word: &str,
270        search_target: SearchTarget,
271        sort: Sort,
272        duration: Option<Duration>,
273        start_date: Option<&str>,
274        end_date: Option<&str>,
275        filter: Filter,
276        search_ai_type: Option<u32>,
277        offset: Option<u32>,
278    ) -> Result<SearchIllustResponse> {
279        debug!(
280            word = %word,
281            search_target = %search_target.to_string(),
282            sort = %sort.to_string(),
283            duration = ?duration,
284            start_date = ?start_date,
285            end_date = ?end_date,
286            filter = %filter.to_string(),
287            search_ai_type = ?search_ai_type,
288            offset = ?offset,
289            "Searching illustrations"
290        );
291        
292        let url = format!("{}/v1/search/illust", self.base_url);
293        let mut params = Vec::new();
294        params.push(("word", word.to_string()));
295        params.push(("search_target", search_target.to_string()));
296        params.push(("sort", sort.to_string()));
297        params.push(("filter", filter.to_string()));
298        
299        if let Some(duration) = duration {
300            params.push(("duration", duration.to_string()));
301        }
302        
303        if let Some(start_date) = start_date {
304            params.push(("start_date", start_date.to_string()));
305        }
306        
307        if let Some(end_date) = end_date {
308            params.push(("end_date", end_date.to_string()));
309        }
310        
311        if let Some(search_ai_type) = search_ai_type {
312            params.push(("search_ai_type", search_ai_type.to_string()));
313        }
314        
315        if let Some(offset) = offset {
316            params.push(("offset", offset.to_string()));
317        }
318        
319        let response = self
320            .http_client
321            .send_request(reqwest::Method::GET, &url, Some(&params))
322            .await?;
323        
324        let text = response.text().await?;
325        let search_result: SearchIllustResponse = serde_json::from_str(&text)?;
326        
327        Ok(search_result)
328    }
329
330    /// Get illustrations from followed users
331    ///
332    /// # Arguments
333    /// * `restrict` - Follow restriction (public/private)
334    /// * `offset` - Offset
335    ///
336    /// # Returns
337    /// Returns response with illustrations from followed users
338    ///
339    /// # Example
340    /// ```rust
341    /// let client = AppClient::new(http_client);
342    /// let follow_illusts = client.illust_follow(
343    ///     FollowRestrict::Public,
344    ///     Some(0)
345    /// ).await?;
346    /// ```
347    pub async fn illust_follow(
348        &self,
349        restrict: FollowRestrict,
350        offset: Option<u32>,
351    ) -> Result<IllustFollowResponse> {
352        debug!(
353            restrict = %restrict.to_string(),
354            offset = ?offset,
355            "Fetching follow illustrations"
356        );
357        
358        let url = format!("{}/v2/illust/follow", self.base_url);
359        let mut params = Vec::new();
360        params.push(("restrict", restrict.to_string()));
361        
362        if let Some(offset) = offset {
363            params.push(("offset", offset.to_string()));
364        }
365        
366        let response = self
367            .http_client
368            .send_request(reqwest::Method::GET, &url, Some(&params))
369            .await?;
370        
371        let text = response.text().await?;
372        let follow_response: IllustFollowResponse = serde_json::from_str(&text)?;
373        
374        Ok(follow_response)
375    }
376
377    /// Get illustration comments
378    ///
379    /// # Arguments
380    /// * `illust_id` - Illustration ID
381    /// * `offset` - Offset
382    /// * `include_total_comments` - Whether to include total comment count
383    ///
384    /// # Returns
385    /// Returns comment response
386    ///
387    /// # Example
388    /// ```rust
389    /// let client = AppClient::new(http_client);
390    /// let comments = client.illust_comments(
391    ///     12345678,
392    ///     Some(0),
393    ///     Some(true)
394    /// ).await?;
395    /// ```
396    pub async fn illust_comments(
397        &self,
398        illust_id: u64,
399        offset: Option<u32>,
400        include_total_comments: Option<bool>,
401    ) -> Result<CommentsResponse> {
402        debug!(
403            illust_id = %illust_id,
404            offset = ?offset,
405            include_total_comments = ?include_total_comments,
406            "Fetching illustration comments"
407        );
408        
409        let url = format!("{}/v1/illust/comments", self.base_url);
410        let mut params = Vec::new();
411        params.push(("illust_id", illust_id.to_string()));
412        
413        if let Some(offset) = offset {
414            params.push(("offset", offset.to_string()));
415        }
416        
417        if let Some(include_total_comments) = include_total_comments {
418            params.push(("include_total_comments", include_total_comments.to_string()));
419        }
420        
421        let response = self
422            .http_client
423            .send_request(reqwest::Method::GET, &url, Some(&params))
424            .await?;
425        
426        let text = response.text().await?;
427        let comments: CommentsResponse = serde_json::from_str(&text)?;
428        
429        Ok(comments)
430    }
431
432    /// Get user following list
433    ///
434    /// # Arguments
435    /// * `user_id` - User ID
436    /// * `restrict` - Follow restriction (public/private)
437    /// * `offset` - Offset
438    ///
439    /// # Returns
440    /// Returns user following response
441    ///
442    /// # Example
443    /// ```rust
444    /// let client = AppClient::new(http_client);
445    /// let following = client.user_following(
446    ///     12345678,
447    ///     FollowRestrict::Public,
448    ///     Some(0)
449    /// ).await?;
450    /// ```
451    pub async fn user_following(
452        &self,
453        user_id: u64,
454        restrict: FollowRestrict,
455        offset: Option<u32>,
456    ) -> Result<UserFollowingResponse> {
457        debug!(
458            user_id = %user_id,
459            restrict = %restrict.to_string(),
460            offset = ?offset,
461            "Fetching user following"
462        );
463        
464        let url = format!("{}/v1/user/following", self.base_url);
465        let mut params = Vec::new();
466        params.push(("user_id", user_id.to_string()));
467        params.push(("restrict", restrict.to_string()));
468        
469        if let Some(offset) = offset {
470            params.push(("offset", offset.to_string()));
471        }
472        
473        let response = self
474            .http_client
475            .send_request(reqwest::Method::GET, &url, Some(&params))
476            .await?;
477        
478        let text = response.text().await?;
479        let following: UserFollowingResponse = serde_json::from_str(&text)?;
480        
481        Ok(following)
482    }
483
484    /// Get user followers list
485    ///
486    /// # Arguments
487    /// * `user_id` - User ID
488    /// * `filter` - Filter
489    /// * `offset` - Offset
490    ///
491    /// # Returns
492    /// Returns user followers response
493    ///
494    /// # Example
495    /// ```rust
496    /// let client = AppClient::new(http_client);
497    /// let followers = client.user_follower(
498    ///     12345678,
499    ///     Filter::ForIOS,
500    ///     Some(0)
501    /// ).await?;
502    /// ```
503    pub async fn user_follower(
504        &self,
505        user_id: u64,
506        filter: Filter,
507        offset: Option<u32>,
508    ) -> Result<UserFollowerResponse> {
509        debug!(
510            user_id = %user_id,
511            filter = %filter.to_string(),
512            offset = ?offset,
513            "Fetching user followers"
514        );
515        
516        let url = format!("{}/v1/user/follower", self.base_url);
517        let mut params = Vec::new();
518        params.push(("user_id", user_id.to_string()));
519        params.push(("filter", filter.to_string()));
520        
521        if let Some(offset) = offset {
522            params.push(("offset", offset.to_string()));
523        }
524        
525        let response = self
526            .http_client
527            .send_request(reqwest::Method::GET, &url, Some(&params))
528            .await?;
529        
530        let text = response.text().await?;
531        let followers: UserFollowerResponse = serde_json::from_str(&text)?;
532        
533        Ok(followers)
534    }
535
536    /// Get user mypixiv list
537    ///
538    /// # Arguments
539    /// * `user_id` - User ID
540    /// * `offset` - Offset
541    ///
542    /// # Returns
543    /// Returns user mypixiv response
544    ///
545    /// # Example
546    /// ```rust
547    /// let client = AppClient::new(http_client);
548    /// let mypixiv = client.user_mypixiv(
549    ///     12345678,
550    ///     Some(0)
551    /// ).await?;
552    /// ```
553    pub async fn user_mypixiv(
554        &self,
555        user_id: u64,
556        offset: Option<u32>,
557    ) -> Result<UserMypixivResponse> {
558        debug!(
559            user_id = %user_id,
560            offset = ?offset,
561            "Fetching user mypixiv"
562        );
563        
564        let url = format!("{}/v1/user/mypixiv", self.base_url);
565        let mut params = Vec::new();
566        params.push(("user_id", user_id.to_string()));
567        
568        if let Some(offset) = offset {
569            params.push(("offset", offset.to_string()));
570        }
571        
572        let response = self
573            .http_client
574            .send_request(reqwest::Method::GET, &url, Some(&params))
575            .await?;
576        
577        let text = response.text().await?;
578        let mypixiv: UserMypixivResponse = serde_json::from_str(&text)?;
579        
580        Ok(mypixiv)
581    }
582
583    /// Add illustration bookmark
584    ///
585    /// # Arguments
586    /// * `illust_id` - Illustration ID
587    /// * `restrict` - Bookmark restriction (public/private)
588    /// * `tags` - Tag list
589    ///
590    /// # Returns
591    /// Returns bookmark response
592    ///
593    /// # Example
594    /// ```rust
595    /// let client = AppClient::new(http_client);
596    /// let result = client.illust_bookmark_add(
597    ///     12345678,
598    ///     FollowRestrict::Public,
599    ///     Some(vec!["tag1".to_string(), "tag2".to_string()])
600    /// ).await?;
601    /// ```
602    pub async fn illust_bookmark_add(
603        &self,
604        illust_id: u64,
605        restrict: FollowRestrict,
606        tags: Option<Vec<String>>,
607    ) -> Result<IllustBookmarkResponse> {
608        debug!(
609            illust_id = %illust_id,
610            restrict = %restrict.to_string(),
611            tags = ?tags,
612            "Adding illustration bookmark"
613        );
614        
615        let url = format!("{}/v2/illust/bookmark/add", self.base_url);
616        let mut data = HashMap::new();
617        data.insert("illust_id", illust_id.to_string());
618        data.insert("restrict", restrict.to_string());
619        
620        if let Some(tags) = tags {
621            data.insert("tags", tags.join(" "));
622        }
623        
624        let response = self
625            .http_client
626            .send_request(reqwest::Method::POST, &url, Some(&data))
627            .await?;
628        
629        let text = response.text().await?;
630        let bookmark_response: IllustBookmarkResponse = serde_json::from_str(&text)?;
631        
632        Ok(bookmark_response)
633    }
634
635    /// Delete illustration bookmark
636    ///
637    /// # Arguments
638    /// * `illust_id` - Illustration ID
639    ///
640    /// # Returns
641    /// Returns bookmark response
642    ///
643    /// # Example
644    /// ```rust
645    /// let client = AppClient::new(http_client);
646    /// let result = client.illust_bookmark_delete(12345678).await?;
647    /// ```
648    pub async fn illust_bookmark_delete(
649        &self,
650        illust_id: u64,
651    ) -> Result<IllustBookmarkResponse> {
652        debug!(
653            illust_id = %illust_id,
654            "Deleting illustration bookmark"
655        );
656        
657        let url = format!("{}/v1/illust/bookmark/delete", self.base_url);
658        let mut data = HashMap::new();
659        data.insert("illust_id", illust_id.to_string());
660        
661        let response = self
662            .http_client
663            .send_request(reqwest::Method::POST, &url, Some(&data))
664            .await?;
665        
666        let text = response.text().await?;
667        let bookmark_response: IllustBookmarkResponse = serde_json::from_str(&text)?;
668        
669        Ok(bookmark_response)
670    }
671
672    /// Get trending tags
673    ///
674    /// # Arguments
675    /// * `filter` - Filter
676    ///
677    /// # Returns
678    /// Returns trending tags response
679    ///
680    /// # Example
681    /// ```rust
682    /// let client = AppClient::new(http_client);
683    /// let trending = client.trending_tags_illust(Filter::ForIOS).await?;
684    /// ```
685    pub async fn trending_tags_illust(
686        &self,
687        filter: Filter,
688    ) -> Result<TrendingTagsResponse> {
689        debug!(
690            filter = %filter.to_string(),
691            "Fetching trending tags"
692        );
693        
694        let url = format!("{}/v1/trending-tags/illust", self.base_url);
695        let params = [("filter", filter.to_string())];
696        
697        let response = self
698            .http_client
699            .send_request(reqwest::Method::GET, &url, Some(&params))
700            .await?;
701        
702        let text = response.text().await?;
703        let trending: TrendingTagsResponse = serde_json::from_str(&text)?;
704        
705        Ok(trending)
706    }
707
708    /// Get Ugoira metadata
709    ///
710    /// # Arguments
711    /// * `illust_id` - Illustration ID
712    ///
713    /// # Returns
714    /// Returns Ugoira metadata response
715    ///
716    /// # Example
717    /// ```rust
718    /// let client = AppClient::new(http_client);
719    /// let metadata = client.ugoira_metadata(12345678).await?;
720    /// ```
721    pub async fn ugoira_metadata(
722        &self,
723        illust_id: u64,
724    ) -> Result<UgoiraMetadataResponse> {
725        debug!(
726            illust_id = %illust_id,
727            "Fetching ugoira metadata"
728        );
729        
730        let url = format!("{}/v1/ugoira/metadata", self.base_url);
731        let params = [("illust_id", illust_id.to_string())];
732        
733        let response = self
734            .http_client
735            .send_request(reqwest::Method::GET, &url, Some(&params))
736            .await?;
737        
738        let text = response.text().await?;
739        let metadata: UgoiraMetadataResponse = serde_json::from_str(&text)?;
740        
741        Ok(metadata)
742    }
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748
749    #[test]
750    fn test_search_target_to_string() {
751        assert_eq!(SearchTarget::PartialMatchForTags.to_string(), "partial_match_for_tags");
752        assert_eq!(SearchTarget::ExactMatchForTags.to_string(), "exact_match_for_tags");
753        assert_eq!(SearchTarget::TitleAndCaption.to_string(), "title_and_caption");
754        assert_eq!(SearchTarget::Keyword.to_string(), "keyword");
755    }
756
757    #[test]
758    fn test_sort_to_string() {
759        assert_eq!(Sort::DateDesc.to_string(), "date_desc");
760        assert_eq!(Sort::DateAsc.to_string(), "date_asc");
761        assert_eq!(Sort::PopularDesc.to_string(), "popular_desc");
762    }
763
764    #[test]
765    fn test_ranking_mode_to_string() {
766        assert_eq!(RankingMode::Day.to_string(), "day");
767        assert_eq!(RankingMode::Week.to_string(), "week");
768        assert_eq!(RankingMode::Month.to_string(), "month");
769        assert_eq!(RankingMode::DayMale.to_string(), "day_male");
770        assert_eq!(RankingMode::DayFemale.to_string(), "day_female");
771        assert_eq!(RankingMode::WeekOriginal.to_string(), "week_original");
772        assert_eq!(RankingMode::WeekRookie.to_string(), "week_rookie");
773        assert_eq!(RankingMode::DayManga.to_string(), "day_manga");
774        assert_eq!(RankingMode::DayR18.to_string(), "day_r18");
775        assert_eq!(RankingMode::DayMaleR18.to_string(), "day_male_r18");
776        assert_eq!(RankingMode::DayFemaleR18.to_string(), "day_female_r18");
777        assert_eq!(RankingMode::WeekR18.to_string(), "week_r18");
778        assert_eq!(RankingMode::WeekR18g.to_string(), "week_r18g");
779    }
780
781    #[test]
782    fn test_content_type_to_string() {
783        assert_eq!(ContentType::Illust.to_string(), "illust");
784        assert_eq!(ContentType::Manga.to_string(), "manga");
785    }
786
787    #[test]
788    fn test_filter_to_string() {
789        assert_eq!(Filter::ForIOS.to_string(), "for_ios");
790        assert_eq!(Filter::None.to_string(), "");
791    }
792
793    #[test]
794    fn test_duration_to_string() {
795        assert_eq!(Duration::WithinLastDay.to_string(), "within_last_day");
796        assert_eq!(Duration::WithinLastWeek.to_string(), "within_last_week");
797        assert_eq!(Duration::WithinLastMonth.to_string(), "within_last_month");
798    }
799}