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#[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 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 pub fn new() -> Result<Self, Error> {
87 let client = Client::builder().build()?;
88 Ok(Self { client })
89 }
90
91 #[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 #[doc = request_errors!()]
104 pub async fn search(&self, params: Option<SearchRequest>) -> Result<SearchResult, Error> {
105 let url = BASE_URL.join("search")?;
106
107 let res = self.client.get(url).query(¶ms).send().await?;
109
110 Ok(res.json().await?)
111 }
112
113 #[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 #[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(¶ms).send().await?;
145
146 Ok(res.json().await?)
147 }
148
149 #[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 #[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 #[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 #[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}