roblox_api/api/thumbnails/
v1.rs

1use serde::{Deserialize, Serialize, Serializer};
2use strum::{EnumIter, IntoEnumIterator};
3use strum_macros::{Display, EnumString};
4
5use crate::{Error, client::Client};
6
7pub const URL: &str = "https://thumbnails.roblox.com/v1";
8
9#[derive(Clone, Debug, Deserialize, PartialEq, Eq, EnumIter)]
10pub enum ThumbnailSize {
11    S30x30,
12    S48x48,
13    S50x50,
14    S60x60,
15    S75x75,
16
17    S100x100,
18    S110x110,
19    S128x128,
20    S140x140,
21    S150x150,
22    S180x180,
23    S250x250,
24    S256x256,
25    S352x352,
26    S420x420,
27    S512x512,
28    S720x720,
29
30    S256x144,
31    S384x216,
32    S480x270,
33    S576x324,
34    S768x432,
35
36    S1200x80,
37    S1440x456,
38}
39
40#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq, Display, EnumString)]
41pub enum ThumbnailFormat {
42    #[default]
43    Png,
44    Jpeg,
45    Webp,
46}
47
48#[derive(Clone, Default, Debug, PartialEq, Eq, Display, EnumString)]
49pub enum ReturnPolicy {
50    #[default]
51    PlaceHolder,
52    ForcePlaceHolder,
53    AutoGenerated,
54    ForceAutoGenerated,
55}
56
57#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Display, EnumString)]
58pub enum ThumbnailState {
59    Pending,
60    Blocked,
61    Completed,
62}
63
64#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Display, EnumString)]
65pub enum ThumbnailVersion {
66    TN3,
67}
68
69#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Display, EnumString)]
70pub enum ThumbnailRequestType {
71    Avatar = 1,
72    AvatarHeadShot,
73    GameIcon,
74    BadgeIcon,
75    GameThumbnail,
76    GamePass,
77    Asset,
78    BundleThumbnail,
79    Outfit,
80    GroupIcon,
81    DeveloperProduct,
82    AutoGeneratedAsset,
83    AvatarBust,
84    PlaceIcon,
85    AutoGeneratedGameIcon,
86    ForceAutoGeneratedGameIcon,
87    Look,
88    CreatorContextAsset,
89    Screenshot,
90}
91
92#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
93#[serde(rename_all = "camelCase")]
94pub struct ThumbnailResponse {
95    #[serde(rename = "targetId")]
96    pub id: u64,
97    pub image_url: String,
98    pub version: ThumbnailVersion,
99    pub state: ThumbnailState,
100}
101
102#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
103#[serde(rename_all = "camelCase")]
104pub struct ThumbnailResponseFromBatch {
105    #[serde(rename = "targetId")]
106    pub id: u64,
107    pub request_id: String,
108    pub image_url: String,
109    pub version: ThumbnailVersion,
110    pub state: ThumbnailState,
111    #[serde(rename = "errorMessage")]
112    pub error: String,
113    pub error_code: i32,
114}
115
116#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
117#[serde(rename_all = "camelCase")]
118pub struct ThumbnailBatchRequest<'a> {
119    #[serde(rename = "targetId")]
120    pub id: u64,
121    pub request_id: &'a str,
122    pub token: &'a str,
123    pub alias: &'a str,
124    #[serde(rename = "type")]
125    pub kind: ThumbnailRequestType,
126    pub size: ThumbnailSize,
127    pub format: ThumbnailFormat,
128    #[serde(rename = "isCircular")]
129    pub circular: bool,
130}
131
132impl Serialize for ThumbnailSize {
133    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
134    where
135        S: Serializer,
136    {
137        serializer.serialize_str(&self.to_string())
138    }
139}
140
141impl std::fmt::Display for ThumbnailSize {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        let content = format!("{:?}", self);
144        write!(f, "{}", content.strip_prefix('S').unwrap())
145    }
146}
147
148impl TryFrom<&str> for ThumbnailSize {
149    type Error = &'static str;
150
151    fn try_from(value: &str) -> Result<Self, Self::Error> {
152        for size in ThumbnailSize::iter() {
153            if size.to_string().as_str() == value {
154                return Ok(size);
155            }
156        }
157
158        Err("Failed to convert string to ThumbnailSize")
159    }
160}
161
162impl ThumbnailFormat {
163    pub fn extension(&self) -> &str {
164        match self {
165            ThumbnailFormat::Png => "png",
166            ThumbnailFormat::Jpeg => "jpeg",
167            ThumbnailFormat::Webp => "webp",
168        }
169    }
170}
171
172async fn generic_thumbnail_api(
173    client: &mut Client,
174    ids: &[u64],
175    asset_name: &str,
176    domain: &str,
177    size: ThumbnailSize,
178    format: ThumbnailFormat,
179    circular: bool,
180    return_policy: Option<ReturnPolicy>,
181    count_per_universe: Option<u32>,
182    defaults: Option<bool>,
183) -> Result<Vec<ThumbnailResponse>, Error> {
184    let ids = ids
185        .iter()
186        .map(|x| x.to_string())
187        .collect::<Vec<String>>()
188        .join(",");
189
190    let asset_ids_key = format!("{asset_name}Ids");
191    let mut query = vec![
192        (asset_ids_key.as_str(), ids),
193        ("size", size.to_string()),
194        ("format", format.to_string()),
195        ("isCircular", circular.to_string()),
196    ];
197
198    if let Some(return_policy) = return_policy {
199        query.push(("returnPolicy", return_policy.to_string()));
200    }
201
202    if let Some(count_per_universe) = count_per_universe {
203        query.push(("countPerUniverse", count_per_universe.to_string()));
204    }
205
206    if let Some(defaults) = defaults {
207        query.push(("defaults", defaults.to_string()));
208    }
209
210    let result = client
211        .requestor
212        .client
213        .get(format!("{URL}/{domain}"))
214        .query(&query)
215        .headers(client.requestor.default_headers.clone())
216        .send()
217        .await;
218
219    #[derive(Clone, Debug, Deserialize)]
220    struct Response {
221        #[serde(rename = "data")]
222        thumbnails: Vec<ThumbnailResponse>,
223    }
224
225    let response = client.requestor.validate_response(result).await?;
226    Ok(client
227        .requestor
228        .parse_json::<Response>(response)
229        .await?
230        .thumbnails)
231}
232
233pub async fn assets(
234    client: &mut Client,
235    ids: &[u64],
236    size: ThumbnailSize,
237    format: ThumbnailFormat,
238    return_policy: ReturnPolicy,
239    circular: bool,
240) -> Result<Vec<ThumbnailResponse>, Error> {
241    generic_thumbnail_api(
242        client,
243        ids,
244        "asset",
245        "assets",
246        size,
247        format,
248        circular,
249        Some(return_policy),
250        None,
251        None,
252    )
253    .await
254}
255
256pub async fn asset_3d(
257    client: &mut Client,
258    id: u64,
259    encode_gltf: bool,
260) -> Result<ThumbnailResponse, Error> {
261    let result = client
262        .requestor
263        .client
264        .get(format!("{URL}/assets-thumbnail-3d"))
265        .query(&[
266            ("assetId", id.to_string()),
267            ("useGltf", encode_gltf.to_string()),
268        ])
269        .headers(client.requestor.default_headers.clone())
270        .send()
271        .await;
272
273    let response = client.requestor.validate_response(result).await?;
274    client
275        .requestor
276        .parse_json::<ThumbnailResponse>(response)
277        .await
278}
279
280pub async fn badge_icons(
281    client: &mut Client,
282    ids: &[u64],
283    size: ThumbnailSize,
284    format: ThumbnailFormat,
285    circular: bool,
286) -> Result<Vec<ThumbnailResponse>, Error> {
287    generic_thumbnail_api(
288        client,
289        ids,
290        "badge",
291        "badges/icons",
292        size,
293        format,
294        circular,
295        None,
296        None,
297        None,
298    )
299    .await
300}
301
302pub async fn bundles(
303    client: &mut Client,
304    ids: &[u64],
305    size: ThumbnailSize,
306    format: ThumbnailFormat,
307    circular: bool,
308) -> Result<Vec<ThumbnailResponse>, Error> {
309    generic_thumbnail_api(
310        client,
311        ids,
312        "bundle",
313        "bundles/thumbnails",
314        size,
315        format,
316        circular,
317        None,
318        None,
319        None,
320    )
321    .await
322}
323
324pub async fn developer_prodcuts(
325    client: &mut Client,
326    ids: &[u64],
327    size: ThumbnailSize,
328    format: ThumbnailFormat,
329    circular: bool,
330) -> Result<Vec<ThumbnailResponse>, Error> {
331    generic_thumbnail_api(
332        client,
333        ids,
334        "developerProduct",
335        "developer-products/icons",
336        size,
337        format,
338        circular,
339        None,
340        None,
341        None,
342    )
343    .await
344}
345
346pub async fn gamepasses(
347    client: &mut Client,
348    ids: &[u64],
349    size: ThumbnailSize,
350    format: ThumbnailFormat,
351    circular: bool,
352) -> Result<Vec<ThumbnailResponse>, Error> {
353    generic_thumbnail_api(
354        client,
355        ids,
356        "gamePass",
357        "game-passes",
358        size,
359        format,
360        circular,
361        None,
362        None,
363        None,
364    )
365    .await
366}
367
368// what the fuck does this even mean?
369/// Fetches game thumbnail URLs for a list of universes' thumbnail ids. Ids that do not correspond to a valid thumbnail will be filtered out.
370pub async fn universe_thumbnails(
371    client: &mut Client,
372    universe_id: u64,
373    ids: &[u64],
374    size: ThumbnailSize,
375    format: ThumbnailFormat,
376    return_policy: ReturnPolicy,
377    circular: bool,
378) -> Result<Vec<ThumbnailResponse>, Error> {
379    generic_thumbnail_api(
380        client,
381        ids,
382        "thumbnail",
383        &format!("games/{universe_id}/thumbnails"),
384        size,
385        format,
386        circular,
387        Some(return_policy),
388        None,
389        None,
390    )
391    .await
392}
393
394pub async fn games(
395    client: &mut Client,
396    ids: &[u64],
397    size: ThumbnailSize,
398    format: ThumbnailFormat,
399    return_policy: ReturnPolicy,
400    circular: bool,
401    defaults: bool,          // defaults (if any) should be returned if no media exists
402    count_per_universe: u32, // max number of thumbnails to return per universe
403) -> Result<Vec<ThumbnailResponse>, Error> {
404    generic_thumbnail_api(
405        client,
406        ids,
407        "universe",
408        "games/multiget/thumbnails",
409        size,
410        format,
411        circular,
412        Some(return_policy),
413        Some(count_per_universe),
414        Some(defaults),
415    )
416    .await
417}
418
419pub async fn game_icons(
420    client: &mut Client,
421    ids: &[u64],
422    size: ThumbnailSize,
423    format: ThumbnailFormat,
424    return_policy: ReturnPolicy,
425    circular: bool,
426) -> Result<Vec<ThumbnailResponse>, Error> {
427    generic_thumbnail_api(
428        client,
429        ids,
430        "universe",
431        "games/icons",
432        size,
433        format,
434        circular,
435        Some(return_policy),
436        None,
437        None,
438    )
439    .await
440}
441
442pub async fn group_icons(
443    client: &mut Client,
444    ids: &[u64],
445    size: ThumbnailSize,
446    format: ThumbnailFormat,
447    circular: bool,
448) -> Result<Vec<ThumbnailResponse>, Error> {
449    generic_thumbnail_api(
450        client,
451        ids,
452        "group",
453        "groups/icons",
454        size,
455        format,
456        circular,
457        None,
458        None,
459        None,
460    )
461    .await
462}
463
464pub async fn place_icons(
465    client: &mut Client,
466    ids: &[u64],
467    size: ThumbnailSize,
468    format: ThumbnailFormat,
469    return_policy: ReturnPolicy,
470    circular: bool,
471) -> Result<Vec<ThumbnailResponse>, Error> {
472    generic_thumbnail_api(
473        client,
474        ids,
475        "place",
476        "places/gameicons",
477        size,
478        format,
479        circular,
480        Some(return_policy),
481        None,
482        None,
483    )
484    .await
485}
486
487pub async fn avatars(
488    client: &mut Client,
489    ids: &[u64],
490    size: ThumbnailSize,
491    format: ThumbnailFormat,
492    circular: bool,
493) -> Result<Vec<ThumbnailResponse>, Error> {
494    generic_thumbnail_api(
495        client,
496        ids,
497        "user",
498        "users/avatar",
499        size,
500        format,
501        circular,
502        None,
503        None,
504        None,
505    )
506    .await
507}
508
509pub async fn avatar_3d(client: &mut Client, id: u64) -> Result<ThumbnailResponse, Error> {
510    let result = client
511        .requestor
512        .client
513        .get(format!("{URL}/avatar-3d"))
514        .query(&[("userId", id)])
515        .headers(client.requestor.default_headers.clone())
516        .send()
517        .await;
518
519    let response = client.requestor.validate_response(result).await?;
520    client
521        .requestor
522        .parse_json::<ThumbnailResponse>(response)
523        .await
524}
525
526pub async fn avatar_busts(
527    client: &mut Client,
528    ids: &[u64],
529    size: ThumbnailSize,
530    format: ThumbnailFormat,
531    circular: bool,
532) -> Result<Vec<ThumbnailResponse>, Error> {
533    generic_thumbnail_api(
534        client,
535        ids,
536        "user",
537        "users/avatar-bust",
538        size,
539        format,
540        circular,
541        None,
542        None,
543        None,
544    )
545    .await
546}
547
548pub async fn avatar_headshots(
549    client: &mut Client,
550    ids: &[u64],
551    size: ThumbnailSize,
552    format: ThumbnailFormat,
553    circular: bool,
554) -> Result<Vec<ThumbnailResponse>, Error> {
555    generic_thumbnail_api(
556        client,
557        ids,
558        "user",
559        "users/avatar-headshot",
560        size,
561        format,
562        circular,
563        None,
564        None,
565        None,
566    )
567    .await
568}
569
570pub async fn outfit_3d(client: &mut Client, id: u64) -> Result<ThumbnailResponse, Error> {
571    let result = client
572        .requestor
573        .client
574        .get(format!("{URL}/outfit-3d"))
575        .query(&[("outfitId", id)])
576        .headers(client.requestor.default_headers.clone())
577        .send()
578        .await;
579
580    let response = client.requestor.validate_response(result).await?;
581    client
582        .requestor
583        .parse_json::<ThumbnailResponse>(response)
584        .await
585}
586
587pub async fn outfits(
588    client: &mut Client,
589    ids: &[u64],
590    size: ThumbnailSize,
591    format: ThumbnailFormat,
592    circular: bool,
593) -> Result<Vec<ThumbnailResponse>, Error> {
594    generic_thumbnail_api(
595        client,
596        ids,
597        "userOutfit",
598        "users/outfits",
599        size,
600        format,
601        circular,
602        None,
603        None,
604        None,
605    )
606    .await
607}
608
609pub async fn batch(
610    client: &mut Client,
611    requests: Vec<ThumbnailBatchRequest<'_>>,
612) -> Result<Vec<ThumbnailResponseFromBatch>, Error> {
613    let result = client
614        .requestor
615        .client
616        .post(format!("{URL}/batch"))
617        .headers(client.requestor.default_headers.clone())
618        .json(&requests)
619        .send()
620        .await;
621
622    #[derive(Clone, Debug, Deserialize)]
623    struct Response {
624        #[serde(rename = "data")]
625        thumbnails: Vec<ThumbnailResponseFromBatch>,
626    }
627
628    let response = client.requestor.validate_response(result).await?;
629    Ok(client
630        .requestor
631        .parse_json::<Response>(response)
632        .await?
633        .thumbnails)
634}
635
636// TODO: measurements api