wallhaven_rs/
client.rs

1use std::{str::FromStr, sync::LazyLock};
2
3use futures::Stream;
4use reqwest::{Client, Url, header};
5
6mod error;
7mod has_path;
8
9pub use error::*;
10use has_path::*;
11
12use crate::models::{raw_models::*, *};
13
14/// A wallhaven api client.
15///
16/// There are two ways of constructing this object, with or without an api key,
17/// see [`WallhavenClient::new`] and [`WallhavenClient::with_key`] for more details
18///
19/// ## Examples
20///
21/// ```rust,ignore
22/// client.wallpaper("someid").await?;
23/// client.search(None).await?;
24///
25/// let req = SearchRequestBuilder::default().build()?;
26/// client.search(Some(req)).await?;
27/// ```
28///
29/// And so on! Check out each methods/models for more informations
30#[derive(Clone)]
31pub struct WallhavenClient {
32    client: Client,
33}
34
35static BASE_URL: LazyLock<Url> =
36    LazyLock::new(|| Url::from_str("https://wallhaven.cc/api/v1/").unwrap());
37
38macro_rules! request_errors {
39        () => {
40            "
41# Errors
42
43- [`Error::UrlParsingError`] when the URL cannot be parsed
44- [`Error::SendingRequest`] an error while sending the request, you can get more details with the underlying error
45- [`Error::DecodingJson`] an error decoding the JSON, either wallhaven sent a wrong json, or we wrote a bad model
46- [`Error::UnknownRequestError`] - an unknown error when sending/receiving the request, you can match further the underlying error"
47        };
48    }
49
50macro_rules! download_errors {
51        () => {
52            "
53# Errors
54
55- [`Error::SendingRequest`] an error while sending the request, you can get more details with the underlying error
56- [`Error::UnknownRequestError`] - an unknown error when sending/receiving the request, you can match further the underlying error"
57    };
58}
59
60impl WallhavenClient {
61    /// Constructs [`WallhavenClient`] with an api key
62    /// Check out [`WallhavenClient::new`] to construct an instance without an api key.
63    /// The api key will be passed to the X-API-Key headers.
64    ///
65    /// # Errors
66    ///
67    /// - [`Error::InvalidApiKey`] when an invalid api key is passed as argument
68    /// - [`Error::BuildingClient`] when something went wrong with the Client builder, this shouldn't happen and you probably should file an issue
69    pub fn with_key(api_key: impl AsRef<str>) -> Result<Self, Error> {
70        let mut auth_header = header::HeaderValue::from_str(api_key.as_ref())?;
71        auth_header.set_sensitive(true);
72
73        let mut headers = header::HeaderMap::with_capacity(1);
74        headers.insert("X-API-Key", auth_header);
75
76        let client = Client::builder().default_headers(headers).build()?;
77        Ok(Self { client })
78    }
79
80    /// Constructs [`WallhavenClient`] without an api key.
81    /// Check out [`WallhavenClient::with_key`] to construct an instance with an api key.
82    ///
83    /// # Errors
84    ///
85    /// - [`Error::BuildingClient`] when something went wrong with the Client builder, this shouldn't happen and you probably should file an issue
86    pub fn new() -> Result<Self, Error> {
87        let client = Client::builder().build()?;
88        Ok(Self { client })
89    }
90
91    /// Fetches a wallpaper by id
92    #[doc = request_errors!()]
93    pub async fn wallpaper(&self, id: impl AsRef<str>) -> Result<WallpaperDetails, Error> {
94        let url = BASE_URL.join(&format!("w/{}", id.as_ref()))?;
95
96        let res = self.client.get(url).send().await?;
97        let raw_json: RawWallpaperDetails = res.json().await?;
98
99        Ok(raw_json.data)
100    }
101
102    /// Searches for wallpapers matching the request
103    #[doc = request_errors!()]
104    pub async fn search(&self, params: Option<SearchRequest>) -> Result<SearchResult, Error> {
105        let url = BASE_URL.join("search")?;
106
107        // Build the request builder first
108        let res = self.client.get(url).query(&params).send().await?;
109
110        Ok(res.json().await?)
111    }
112
113    /// Fetches all the [`UserCollection`] of a certain user
114    ///
115    /// Defaults to the current if no username is provided and an api key is used
116    #[doc = request_errors!()]
117    pub async fn collections(
118        &self,
119        username: impl Into<Option<String>>,
120    ) -> Result<Vec<UserCollection>, Error> {
121        let url = username.into().map_or_else(
122            || "collections".to_string(),
123            |username| format!("collections/{username}"),
124        );
125
126        let url = BASE_URL.join(&url)?;
127
128        let res = self.client.get(url).send().await?;
129        let raw_json: RawUserCollection = res.json().await?;
130
131        Ok(raw_json.data)
132    }
133
134    /// Gets the collection's wallpapers.
135    #[doc = request_errors!()]
136    pub async fn collection_items(
137        &self,
138        username: impl AsRef<str>,
139        id: u64,
140        params: Option<CollectionItemsRequest>,
141    ) -> Result<SearchResult, Error> {
142        let url = BASE_URL.join(&format!("collections/{}/{id}", username.as_ref()))?;
143
144        let res = self.client.get(url).query(&params).send().await?;
145
146        Ok(res.json().await?)
147    }
148
149    /// Fetches a [`Tag`] by its id
150    #[doc = request_errors!()]
151    pub async fn tag(&self, id: u64) -> Result<Tag, Error> {
152        let url = BASE_URL.join(&format!("tag/{id}"))?;
153
154        let res = self.client.get(url).send().await?;
155        let raw_json: RawTagInfo = res.json().await?;
156
157        Ok(raw_json.data)
158    }
159
160    /// Fetches the [`UserSettings`] of the current user. Only works if the api key was provided.
161    #[doc = request_errors!()]
162    pub async fn user_settings(&self) -> Result<UserSettings, Error> {
163        let url = BASE_URL.join("settings")?;
164
165        let res = self.client.get(url).send().await?;
166        let raw_json: RawUserSettings = res.json().await?;
167
168        Ok(raw_json.data)
169    }
170
171    /// Downloads a any wallpaper type (e.g. `WallhavenDetails` or `WallhavenSummary`)
172    #[doc = download_errors!()]
173    pub async fn download_wallpaper(
174        &self,
175        wallpaper: &impl HasPath,
176    ) -> Result<impl Stream<Item = reqwest::Result<bytes::Bytes>>, Error> {
177        let resp = self.client.get(wallpaper.path()).send().await?;
178        Ok(resp.bytes_stream())
179    }
180
181    /// Downloads a [`Thumbnails`]
182    #[doc = download_errors!()]
183    pub async fn download_thumbnail(
184        &self,
185        thumbnail: &Thumbnails,
186        resolution: ThumbnailResolution,
187    ) -> Result<impl Stream<Item = reqwest::Result<bytes::Bytes>>, Error> {
188        let url = match resolution {
189            ThumbnailResolution::Large => thumbnail.large.clone(),
190            ThumbnailResolution::Original => thumbnail.original.clone(),
191            ThumbnailResolution::Small => thumbnail.small.clone(),
192        };
193
194        let resp = self.client.get(url).send().await?;
195        Ok(resp.bytes_stream())
196    }
197}