libpixiv 0.2.1

pixiv.net API client library
Documentation
use reqwest_middleware::{
    reqwest::{
        header::{ACCEPT_LANGUAGE, USER_AGENT},
        ClientBuilder, Response, StatusCode,
    },
    ClientWithMiddleware,
};
use serde::Deserialize;
use std::error::Error;
use std::{collections::HashMap, sync::Arc};

use crate::{
    tokens::{Session, SessionManager},
    PixivAppError,
};

pub struct PixivAppClient {
    /// bearer token
    pub(crate) session: Arc<SessionManager>,
    pub(crate) http_client: ClientWithMiddleware,
    pub(crate) host: String,
    pub(crate) platform: params::Platform,
}

pub mod params {
    use serde::{Deserialize, Serialize};
    use std::fmt::{Display, Formatter, Result};

    /// Platform
    #[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
    #[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
    pub enum Platform {
        IOS,
        Android,
    }

    impl Display for Platform {
        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
            write!(
                f,
                "{}",
                match self {
                    Self::IOS => "ios",
                    Self::Android => "android",
                }
            )
        }
    }

    /// Visibility for content
    #[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
    #[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
    pub enum Visibility {
        /// Visible for everyone (given no other filters apply)
        Public,
        /// Only visible by logged in user or other "close" groups if any
        Private,
    }

    impl Display for Visibility {
        fn fmt(&self, f: &mut Formatter) -> Result {
            write!(
                f,
                "{}",
                match self {
                    Self::Public => "public",
                    Self::Private => "private",
                }
            )
        }
    }

    /// Sort mode when search for content
    ///
    /// Some api routes only work with date related modes when
    /// the user has no premium subscription.
    #[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
    #[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
    pub enum SortMode {
        /// date, oldest to newest
        DateAscending,
        /// date, newest to oldest
        DateDescending,
        /// popularity, most to least popular
        PopularDescending,
    }

    impl Display for SortMode {
        fn fmt(&self, f: &mut Formatter) -> Result {
            write!(
                f,
                "{}",
                match self {
                    Self::DateAscending => "date_asc",
                    Self::DateDescending => "date_desc",
                    Self::PopularDescending => "popular_desc",
                }
            )
        }
    }

    /// Seach mode for illustration search.
    #[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
    #[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
    pub enum SearchMode {
        /// requires match by part of the queried tags
        PartialMatchForTags,
        /// only matches exact tags
        ExactMatchForTags,
        /// searches in title and caption of the content
        TitleAndCaption,
    }

    impl Display for SearchMode {
        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
            write!(
                f,
                "{}",
                match self {
                    Self::PartialMatchForTags => "partial_match_for_tags",
                    Self::ExactMatchForTags => "exact_match_for_tags",
                    Self::TitleAndCaption => "title_and_caption",
                }
            )
        }
    }

    /// Period for ranking search
    #[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
    #[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
    pub enum RankingMode {
        /// Ranking by last day
        Day,
        /// Ranking by last week
        Week,
        /// Ranking by last month
        Month,
    }

    impl Display for RankingMode {
        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
            write!(
                f,
                "{}",
                match self {
                    Self::Day => String::from("day"),
                    Self::Week => String::from("week"),
                    Self::Month => String::from("month"),
                }
            )
        }
    }
}

/// The client structure for easy API access.
///
/// This struct provides bindings to the (known) pixiv API.
///
/// # Examples
///
/// This is the basic usage:
/// ```
/// # use libpixiv::client::PixivAppClient;
/// # async fn f() -> Result<(), Box<dyn std::error::Error>> {
/// let mut client = PixivAppClient::new("some_refresh_token".into());
/// client.illust_details(25308802).await;
/// # Ok(())
/// # }
/// ```
impl PixivAppClient {
    /// Create a new client using a refresh token
    pub fn new(token: String) -> Self {
        Self::_new(Arc::new(SessionManager::new(token)))
    }

    /// Restore an existing session
    pub fn restore(session: Session) -> Self {
        Self::_new(Arc::new(SessionManager::restore(session)))
    }

    fn _new(sessionman: Arc<SessionManager>) -> Self {
        Self {
            session: sessionman.clone(),
            http_client: reqwest_middleware::ClientBuilder::new(
                ClientBuilder::new()
                    .default_headers(
                        (&HashMap::from([
                            ("app-os".to_string(), "ios".to_string()),
                            ("app-os-version".to_string(), "12.2".to_string()),
                            ("app-version".to_string(), "7.6.2".to_string()),
                            (
                                USER_AGENT.to_string(),
                                "PixivIOSApp/7.6.2 (iOS 12.2; iPhone9,1)".to_string(),
                            ),
                        ]))
                            .try_into()
                            .unwrap(),
                    )
                    .build()
                    .unwrap(),
            )
            .with_arc(sessionman.clone())
            .build(),
            host: String::from("https://app-api.pixiv.net"),
            platform: params::Platform::IOS,
        }
    }

    /// Returns the session data. This instance gets detached from the internal session management,
    /// so external session changes are only possible during initialization.
    pub async fn session(&self) -> Session {
        self.session.session.read().await.clone()
    }

    /// Process a request built in another function and return the appropriate type
    pub(crate) async fn process_request<T: for<'de> Deserialize<'de>>(
        &self,
        reqb: reqwest_middleware::RequestBuilder,
    ) -> Result<T, Box<dyn Error + Send + Sync>> {
        Ok(reqb
            .header(ACCEPT_LANGUAGE, "en-US")
            .send()
            .await
            .map_or_else(
                |e| Err::<Response, Box<dyn Error + Send + Sync>>(Box::new(e)),
                |r| match r.status() {
                    x if StatusCode::is_success(&x) || StatusCode::is_redirection(&x) => Ok(r),
                    x if StatusCode::is_client_error(&x) => Err(Box::new(match x {
                        StatusCode::BAD_REQUEST => PixivAppError::RequestFailed,
                        StatusCode::UNAUTHORIZED => PixivAppError::MissingLogin,
                        StatusCode::NOT_FOUND => PixivAppError::TargetNotFound,
                        StatusCode::TOO_MANY_REQUESTS => PixivAppError::RateLimitReached,
                        s => PixivAppError::UnhandledStatus(s),
                    })),
                    x => Err(Box::new(PixivAppError::UnhandledStatus(x))),
                },
            )?
            .text()
            .await
            .map(|r| async move {
                if cfg!(test) {
                    eprintln!("{}", r);
                }
                let d = &mut serde_json::Deserializer::from_str(&r);
                let r = serde_path_to_error::deserialize(d);
                r
            })
            .map_err(Box::new)?
            .await?)
    }
}

#[cfg(test)]
pub mod tests {
    use once_cell::sync::Lazy;

    use super::*;
    use tokio::test;

    pub static USER: &str = "Aio";
    pub static USER_ID: u32 = 25308802;
    pub static ILLUST_ID: u32 = 132610892;
    pub static _STUB: Lazy<Arc<()>> = Lazy::new(|| Arc::new(env_logger::init()));

    pub fn get_client() -> PixivAppClient {
        let _ = _STUB.clone();
        let client = PixivAppClient::new(env!("PIXIV_REFRESH_TOKEN").to_string());
        client
    }

    #[test]
    async fn test_session_restore() {
        let client = get_client();
        assert!(client.illust_details(ILLUST_ID).await.is_ok());
        let client2 = PixivAppClient::restore(client.session().await);
        assert!(client2.illust_details(ILLUST_ID).await.is_ok());
    }
}