roboat/thumbnails/mod.rs
1use crate::{Client, RoboatError};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5mod request_types;
6
7const THUMBNAIL_API_URL: &str = "https://thumbnails.roblox.com/v1/batch";
8
9/// A size for an asset thumbnail.
10///
11/// Sizes are taken from <https://thumbnails.roblox.com/docs/index.html#operations-Assets-get_v1_assets>.
12#[allow(missing_docs)]
13#[derive(
14 Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize, Copy,
15)]
16pub enum ThumbnailSize {
17 S30x30,
18 S42x42,
19 S50x50,
20 S60x62,
21 S75x75,
22 S110x110,
23 S140x140,
24 S150x150,
25 S160x100,
26 S160x600,
27 S250x250,
28 S256x144,
29 S300x250,
30 S304x166,
31 S384x216,
32 S396x216,
33 #[default]
34 S420x420,
35 S480x270,
36 S512x512,
37 S576x324,
38 S700x700,
39 S728x90,
40 S768x432,
41 S1200x80,
42}
43
44/// Used to convey which type of thumbnail to fetch. A full list can be found under the batch endpoint at
45/// <https://thumbnails.roblox.com/docs/index.html>
46#[allow(missing_docs)]
47#[derive(
48 Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize, Copy,
49)]
50pub enum ThumbnailType {
51 Avatar,
52 AvatarHeadshot,
53 #[default]
54 Asset,
55}
56
57impl fmt::Display for ThumbnailSize {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 Self::S30x30 => write!(f, "30x30"),
61 Self::S42x42 => write!(f, "42x42"),
62 Self::S50x50 => write!(f, "50x50"),
63 Self::S60x62 => write!(f, "60x62"),
64 Self::S75x75 => write!(f, "75x75"),
65 Self::S110x110 => write!(f, "110x110"),
66 Self::S140x140 => write!(f, "140x140"),
67 Self::S150x150 => write!(f, "150x150"),
68 Self::S160x100 => write!(f, "160x100"),
69 Self::S160x600 => write!(f, "160x600"),
70 Self::S250x250 => write!(f, "250x250"),
71 Self::S256x144 => write!(f, "256x144"),
72 Self::S300x250 => write!(f, "300x250"),
73 Self::S304x166 => write!(f, "304x166"),
74 Self::S384x216 => write!(f, "384x216"),
75 Self::S396x216 => write!(f, "396x216"),
76 Self::S420x420 => write!(f, "420x420"),
77 Self::S480x270 => write!(f, "480x270"),
78 Self::S512x512 => write!(f, "512x512"),
79 Self::S576x324 => write!(f, "576x324"),
80 Self::S700x700 => write!(f, "700x700"),
81 Self::S728x90 => write!(f, "728x90"),
82 Self::S768x432 => write!(f, "768x432"),
83 Self::S1200x80 => write!(f, "1200x80"),
84 }
85 }
86}
87
88impl Client {
89 /// Fetches multiple thumbnails of a specified size and type using <https://thumbnails.roblox.com/v1/batch>.
90 ///
91 /// # Notes
92 /// * Does not require a valid roblosecurity.
93 /// * Can handle up to 100 asset ids at once.
94 /// * Does not appear to have a rate limit.
95 /// * Note all types are implemented, the full list can be found [here](https://thumbnails.roblox.com/docs/index.html)
96 /// and the implemented ones can be found in [`ThumbnailType`].
97 ///
98 /// # Errors
99 /// * All errors under [Standard Errors](#standard-errors).
100 ///
101 /// # Example
102 ///
103 /// ```no_run
104 /// use roboat::ClientBuilder;
105 /// use roboat::thumbnails::{ThumbnailSize, ThumbnailType};
106 ///
107 /// # #[tokio::main]
108 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
109 /// let client = ClientBuilder::new().build();
110 ///
111 /// let size = ThumbnailSize::S420x420;
112 /// let thumbnail_type = ThumbnailType::Avatar;
113 ///
114 /// let avatar_id_1 = 20418400;
115 /// let avatar_id_2 = 12660007639;
116 ///
117 /// let urls = client
118 /// .thumbnail_url_bulk(vec![avatar_id_1, avatar_id_2], size, thumbnail_type)
119 /// .await?;
120 ///
121 /// println!("Avatar {} thumbnail url: {}", avatar_id_1, urls[0]);
122 /// println!("Avatar {} thumbnail url: {}", avatar_id_2, urls[1]);
123 ///
124 /// let size = ThumbnailSize::S420x420;
125 /// let thumbnail_type = ThumbnailType::AvatarHeadshot;
126 ///
127 /// let avatar_id_1 = 20418400;
128 /// let avatar_id_2 = 12660007639;
129 ///
130 /// let urls = client
131 /// .thumbnail_url_bulk(vec![avatar_id_1, avatar_id_2], size, thumbnail_type)
132 /// .await?;
133 ///
134 /// println!("Avatar headshot {} thumbnail url: {}", avatar_id_1, urls[0]);
135 /// println!("Avatar headshot {} thumbnail url: {}", avatar_id_2, urls[1]);
136 ///
137 /// let size = ThumbnailSize::S420x420;
138 /// let thumbnail_type = ThumbnailType::Asset;
139 ///
140 /// let asset_id_1 = 20418400;
141 /// let asset_id_2 = 12660007639;
142 ///
143 /// let urls = client
144 /// .thumbnail_url_bulk(vec![asset_id_1, asset_id_2], size, thumbnail_type)
145 /// .await?;
146 ///
147 /// println!("Asset {} thumbnail url: {}", asset_id_1, urls[0]);
148 /// println!("Asset {} thumbnail url: {}", asset_id_2, urls[1]);
149 ///
150 /// # Ok(())
151 /// # }
152 /// ```
153 pub async fn thumbnail_url_bulk(
154 &self,
155 ids: Vec<u64>,
156 size: ThumbnailSize,
157 thumbnail_type: ThumbnailType,
158 ) -> Result<Vec<String>, RoboatError> {
159 let mut json_item_requests = Vec::new();
160
161 for id in &ids {
162 json_item_requests.push(serde_json::json!({
163 "requestId": generate_request_id_string(thumbnail_type, *id, size),
164 "type": generate_thumbnail_type_string(thumbnail_type),
165 "targetId": id,
166 "format": generate_format(thumbnail_type),
167 "size": size.to_string(),
168 }));
169 }
170
171 let body = serde_json::json!(json_item_requests);
172
173 let request_result = self
174 .reqwest_client
175 .post(THUMBNAIL_API_URL)
176 .json(&body)
177 .send()
178 .await;
179
180 let response = Self::validate_request_result(request_result).await?;
181 let mut raw =
182 Self::parse_to_raw::<request_types::AssetThumbnailUrlResponse>(response).await?;
183
184 sort_url_datas_by_argument_order(&mut raw.data, &ids);
185
186 let mut urls = Vec::new();
187
188 for data in raw.data {
189 urls.push(data.image_url);
190 }
191
192 Ok(urls)
193 }
194
195 /// Fetches a thumbnail of a specified size and type using <https://thumbnails.roblox.com/v1/batch>.
196 ///
197 /// # Notes
198 /// * Does not require a valid roblosecurity.
199 /// * Can handle up to 100 asset ids at once.
200 /// * Does not appear to have a rate limit.
201 /// * Note all types are implemented, the full list can be found [here](https://thumbnails.roblox.com/docs/index.html)
202 /// and the implemented ones can be found in [`ThumbnailType`].
203 ///
204 /// # Errors
205 /// * All errors under [Standard Errors](#standard-errors).
206 ///
207 /// # Example
208 ///
209 /// ```no_run
210 /// use roboat::ClientBuilder;
211 /// use roboat::thumbnails::{ThumbnailSize, ThumbnailType};
212 ///
213 /// # #[tokio::main]
214 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
215 /// let client = ClientBuilder::new().build();
216 ///
217 /// let size = ThumbnailSize::S420x420;
218 /// let thumbnail_type = ThumbnailType::Avatar;
219 ///
220 /// let avatar_id = 20418400;
221 ///
222 /// let url = client
223 /// .thumbnail_url(avatar_id, size, thumbnail_type)
224 /// .await?;
225 ///
226 /// println!("Avatar {} thumbnail url: {}", avatar_id, url);
227 ///
228 /// let size = ThumbnailSize::S420x420;
229 /// let thumbnail_type = ThumbnailType::AvatarHeadshot;
230 ///
231 /// let avatar_id = 20418400;
232 ///
233 /// let url = client
234 /// .thumbnail_url(avatar_id, size, thumbnail_type)
235 /// .await?;
236 ///
237 /// println!("Avatar headshot {} thumbnail url: {}", avatar_id, url);
238 ///
239 /// let size = ThumbnailSize::S420x420;
240 /// let thumbnail_type = ThumbnailType::Asset;
241 ///
242 /// let asset_id = 20418400;
243 ///
244 /// let url = client
245 /// .thumbnail_url(asset_id, size, thumbnail_type)
246 /// .await?;
247 ///
248 /// println!("Asset {} thumbnail url: {}", asset_id, url);
249 ///
250 /// # Ok(())
251 /// # }
252 /// ```
253 pub async fn thumbnail_url(
254 &self,
255 id: u64,
256 size: ThumbnailSize,
257 thumbnail_type: ThumbnailType,
258 ) -> Result<String, RoboatError> {
259 let urls = self
260 .thumbnail_url_bulk(vec![id], size, thumbnail_type)
261 .await?;
262 let url = urls.first().ok_or(RoboatError::MalformedResponse)?;
263 Ok(url.to_owned())
264 }
265}
266
267/// Makes sure that the url datas are in the same order as the arguments.
268fn sort_url_datas_by_argument_order(
269 url_datas: &mut [request_types::AssetThumbnailUrlDataRaw],
270 arguments: &[u64],
271) {
272 url_datas.sort_by(|a, b| {
273 let a_index = arguments
274 .iter()
275 .position(|id| *id == a.target_id as u64)
276 .unwrap_or(usize::MAX);
277
278 let b_index = arguments
279 .iter()
280 .position(|id| *id == b.target_id as u64)
281 .unwrap_or(usize::MAX);
282
283 a_index.cmp(&b_index)
284 });
285}
286
287fn generate_request_id_string(
288 thumbnail_type: ThumbnailType,
289 id: u64,
290 size: ThumbnailSize,
291) -> String {
292 match thumbnail_type {
293 ThumbnailType::Avatar => format!("{}:undefined:Avatar:{}:null:regular", id, size),
294 ThumbnailType::AvatarHeadshot => {
295 format!("{}:undefined:AvatarHeadshot:{}:null:regular", id, size)
296 }
297 ThumbnailType::Asset => format!("{}::Asset:{}:png:regular", id, size),
298 }
299}
300
301fn generate_format(thumbnail_type: ThumbnailType) -> Option<String> {
302 match thumbnail_type {
303 ThumbnailType::Avatar => None::<String>,
304 ThumbnailType::AvatarHeadshot => None::<String>,
305 ThumbnailType::Asset => Some("png".to_string()),
306 }
307}
308
309fn generate_thumbnail_type_string(thumbnail_type: ThumbnailType) -> String {
310 match thumbnail_type {
311 ThumbnailType::Avatar => "Avatar".to_string(),
312 ThumbnailType::AvatarHeadshot => "AvatarHeadShot".to_string(),
313 ThumbnailType::Asset => "Asset".to_string(),
314 }
315}