libpixiv 0.1.2

pixiv.net API client library
Documentation
use crate::client::{params, PixivAppClient};
use crate::models::{Comment, Illustration, PixivResult};
use crate::PixivAppError;
use chrono::{DateTime, Utc};
use reqwest::header::CONTENT_TYPE;
use reqwest::Method;
use std::error::Error;

impl PixivAppClient {
    /// Fetch illustration data.
    pub async fn illust_details(
        &self,
        illust_id: u32,
    ) -> Result<Illustration, Box<dyn Error + Send + Sync>> {
        let url = format!("{}/v1/illust/detail", self.host);

        let req = self
            .auth_request(Method::GET, url)
            .await
            .query(&[("illust_id", illust_id)]);

        let res = self.process_request::<PixivResult>(req).await?;
        res.illust.ok_or(res.error.map_or_else(
            || Box::<dyn Error + Sync + Send>::from(PixivAppError::Unknown),
            |v| Box::new(v),
        ))
    }

    /// Fetch the newest illustrations of users the user follows.
    pub async fn illust_follow(
        &self,
        restrict: params::Visibility,
        offset: u32,
    ) -> Result<Vec<Illustration>, Box<dyn Error + Send + Sync>> {
        let url = format!("{}/v2/illust/follow", self.host);

        let req = self.auth_request(Method::GET, url).await.query(&[
            ("restrict", restrict.to_string()),
            ("offset", offset.to_string()),
        ]);

        let res = self.process_request::<PixivResult>(req).await?;
        res.illusts.ok_or(res.error.map_or_else(
            || Box::<dyn Error + Sync + Send>::from(PixivAppError::Unknown),
            |v| Box::new(v),
        ))
    }

    /// Fetch the highest ranked illustrations.
    pub async fn illust_ranking(
        &self,
        mode: params::RankingMode,
        date: Option<DateTime<Utc>>,
        offset: Option<u32>,
    ) -> Result<Vec<Illustration>, Box<dyn Error + Send + Sync>> {
        let url = format!("{}/v1/illust/ranking", self.host);
        let mut params = vec![
            ("mode", mode.to_string()),
            ("offset", offset.unwrap_or(0).to_string()),
            ("filter", format!("for_{}", self.platform)),
        ];
        if let Some(date) = date {
            params.push(("date", date.format("%Y-%M-%d").to_string()))
        }

        let req = self.auth_request(Method::GET, url).await.query(&params);

        let res = self.process_request::<PixivResult>(req).await?;
        res.illusts.ok_or(res.error.map_or_else(
            || Box::<dyn Error + Sync + Send>::from(PixivAppError::Unknown),
            |v| Box::new(v),
        ))
    }

    /// Fetch related illustrations.
    pub async fn illust_related(
        &self,
        illust_id: u32,
        seed_illust_ids: Option<Vec<u32>>,
        viewed: Option<Vec<u32>>,
        offset: Option<u32>,
    ) -> Result<Vec<Illustration>, Box<dyn Error + Send + Sync>> {
        let url = format!("{}/v2/illust/related", self.host);
        let mut params = vec![
            ("illust_id", illust_id.to_string()),
            ("offset", offset.unwrap_or(0).to_string()),
            ("filter", format!("for_{}", self.platform)),
        ];
        if let Some(seed_illust_ids) = seed_illust_ids {
            for id in seed_illust_ids {
                params.push(("seed_illust_ids[]", id.to_string()));
            }
        }
        if let Some(viewed) = viewed {
            for id in viewed {
                params.push(("viewed[]", id.to_string()));
            }
        }

        let req = self.auth_request(Method::GET, url).await.query(&params);

        let res = self.process_request::<PixivResult>(req).await?;
        res.illusts.ok_or(res.error.clone().map_or_else(
            || Box::<dyn Error + Sync + Send>::from(PixivAppError::Unknown),
            |v| Box::new(v),
        ))
    }

    /// Fetch comments for some illustration.
    pub async fn illust_comments(
        &self,
        illust_id: u32,
        include_total_comments: Option<bool>,
        offset: Option<u32>,
    ) -> Result<Vec<Comment>, Box<dyn Error + Send + Sync>> {
        let url = format!("{}/v3/illust/comments", self.host);
        let mut params = vec![
            ("illust_id", illust_id.to_string()),
            ("offset", offset.unwrap_or(0).to_string()),
        ];
        if let Some(include_total_comments) = include_total_comments {
            params.push(("include_total_comments", include_total_comments.to_string()));
        }

        let req = self.auth_request(Method::GET, url).await.query(&params);

        let res = self.process_request::<PixivResult>(req).await?;
        res.comments.ok_or(res.error.clone().map_or_else(
            || Box::<dyn Error + Sync + Send>::from(PixivAppError::Unknown),
            |v| Box::new(v),
        ))
    }

    //pub async fn illust_add_comment(&self) -> Result<PixivResult, Box<dyn Error + Send + Sync>> {}

    /// Fetch recommended illustrations for the logged in user.
    pub async fn illust_recommended(
        &self,
        content_type: &'static str,
        include_ranking_label: Option<bool>,
        offset: Option<u32>,
    ) -> Result<Vec<Illustration>, Box<dyn Error + Send + Sync>> {
        let url = format!("{}/v1/illust/recommended", self.host);
        let params = vec![
            ("content_type", content_type.to_string()),
            (
                "include_ranking_label",
                include_ranking_label.unwrap_or(false).to_string(),
            ),
            ("filter", format!("for_{}", self.platform)),
            ("offset", offset.unwrap_or(0).to_string()),
        ];

        let req = self.auth_request(Method::GET, url).await.query(&params);

        let res = self.process_request::<PixivResult>(req).await?;
        res.illusts.ok_or(res.error.map_or_else(
            || Box::<dyn Error + Sync + Send>::from(PixivAppError::Unknown),
            |v| Box::new(v),
        ))
    }

    /// Bookmark some illustrations.
    pub async fn illust_bookmark_add(
        &self,
        illust_id: u32,
        restrict: params::Visibility,
        tags: Option<Vec<String>>,
    ) -> Result<(), Box<dyn Error + Send + Sync>> {
        let url = format!("{}/v2/illust/bookmark/add", self.host);
        let mut params = vec![
            ("illust_id", illust_id.to_string()),
            ("restrict", restrict.to_string()),
        ];
        if let Some(tags) = tags {
            params.push(("tags", tags.join(" ")));
        }
        let body = params
            .iter()
            .map(|e| format!("{}={}", e.0, e.1))
            .collect::<Vec<_>>()
            .join("&");

        let req = self
            .auth_request(Method::POST, url)
            .await
            .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
            .body(body);

        let res = self.process_request::<PixivResult>(req).await?;
        if let Some(error) = res.error {
            Err(Box::new(error))
        } else {
            Ok(())
        }
    }

    /// Remove the bookmark for some illustrations.
    pub async fn illust_bookmark_delete(
        &self,
        illust_id: u32,
    ) -> Result<(), Box<dyn Error + Send + Sync>> {
        let url = format!("{}/v1/illust/bookmark/delete", self.host);

        let req = self
            .auth_request(Method::POST, url)
            .await
            .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
            .body(format!("illust_id={}", illust_id));

        let res = self.process_request::<PixivResult>(req).await?;
        if let Some(error) = res.error {
            Err(Box::new(error))
        } else {
            Ok(())
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::client::tests::*;
    use serial_test::serial;
    use tokio::test;

    use super::*;

    #[test]
    async fn test_details() {
        let _illust = get_client().await.illust_details(ILLUST_ID).await;
        let Ok(illust) = _illust else {
            panic!(
                "Failed to fetch details for illustration ID {}: {}",
                ILLUST_ID,
                _illust.err().unwrap()
            );
        };
        assert_eq!(illust.id, ILLUST_ID);
    }

    #[test]
    async fn test_follow() {
        let result = get_client()
            .await
            .illust_follow(params::Visibility::Public, 0)
            .await;
        assert!(
            result.is_ok(),
            "Failed to fetch illustrations of followed users: {}",
            result.err().unwrap()
        );
    }

    #[test]
    async fn test_ranking() {
        let result = get_client()
            .await
            .illust_ranking(params::RankingMode::Day, None, None)
            .await;
        assert!(
            result.is_ok(),
            "Failed to fetch ranked illustrations: {}",
            result.err().unwrap()
        );
    }

    #[test]
    async fn test_related() {
        let result = get_client()
            .await
            .illust_related(ILLUST_ID, None, None, None)
            .await;
        assert!(
            result.is_ok(),
            "Failed to fetch related illustrations: {}",
            result.err().unwrap()
        )
    }

    #[test]
    async fn test_recommended() {
        let result = get_client()
            .await
            .illust_recommended("illust", None, None)
            .await;
        assert!(
            result.is_ok(),
            "Failed to fetch recommended illustrations: {}",
            result.err().unwrap()
        )
    }

    #[test]
    #[serial]
    async fn test_adding_bookmarks() {
        let result = get_client()
            .await
            .illust_bookmark_add(ILLUST_ID, params::Visibility::Public, None)
            .await;
        assert!(
            result.is_ok(),
            "Failed to bookmark: {}",
            result.err().unwrap()
        )
    }

    #[test]
    #[serial]
    async fn test_removing_bookmarks() {
        let result = get_client().await.illust_bookmark_delete(ILLUST_ID).await;
        assert!(
            result.is_ok(),
            "Failed to remove bookmark: {}",
            result.err().unwrap()
        )
    }

    #[test]
    async fn test_fetching_comments() {
        let result = get_client()
            .await
            .illust_comments(134633764, None, None)
            .await;
        assert!(
            result.is_ok(),
            "Failed to remove bookmark: {}",
            result.err().unwrap()
        );
    }
}