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