Skip to main content

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}