roblox_api/api/thumbnails/
v1.rs

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