Skip to main content

game_manager_lib/scrapers/
ea_play.rs

1//! Scraper - EA Play
2
3use crate::constants::GAME_PASS_BATCH_SIZE;
4use reqwest::Client;
5use serde::{Deserialize, Serialize};
6
7// IDs dos catálogos
8const EA_PLAY_SIGL: &str = "b8900d09-a491-44cc-916e-32b5acae621b";
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct EAPlayGame {
13    pub store_id: String,
14    pub title: String,
15    pub description: Option<String>,
16    pub image_poster: Option<String>, // capa 2:3 para cards
17    pub image_hero: Option<String>,   // banner 16:9 para carrossel
18    pub categories: Vec<String>,
19    pub developer: Option<String>,
20    pub store_url: String,
21}
22
23fn parse_ea_play_products(products: &serde_json::Value) -> Vec<EAPlayGame> {
24    let mut games = Vec::new();
25
26    let Some(products) = products.as_object() else {
27        return games;
28    };
29
30    for (store_id, product) in products {
31        // Filtra não-jogos
32        let product_type = product["ProductType"].as_str().unwrap_or("");
33        if product_type != "Game" {
34            continue;
35        }
36
37        // Categories
38        let categories = product["Categories"]
39            .as_array()
40            .map(|arr| {
41                arr.iter()
42                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
43                    .collect()
44            })
45            .unwrap_or_default();
46
47        // Build game
48        let game = EAPlayGame {
49            store_id: store_id.clone(),
50            title: product["ProductTitle"].as_str().unwrap_or("?").to_string(),
51            description: product["ProductDescription"]
52                .as_str()
53                .map(|s| s.to_string()),
54            image_poster: product["ImagePoster"]["URI"]
55                .as_str()
56                .map(|s| s.to_string()),
57            image_hero: product["ImageHero"]["URI"].as_str().map(|s| s.to_string()),
58            categories,
59            developer: product["DeveloperName"].as_str().map(|s| s.to_string()),
60            store_url: format!("https://www.xbox.com/pt-BR/games/store/_/{}", store_id),
61        };
62
63        games.push(game);
64    }
65
66    games
67}
68
69pub async fn fetch_ea_play_catalog(language: &str) -> Result<Vec<EAPlayGame>, String> {
70    let client = Client::new();
71
72    // Etapa 1 — lista de IDs
73    let url = format!(
74        "https://catalog.gamepass.com/sigls/v2?id={}&language={}&market=BR",
75        EA_PLAY_SIGL, language
76    );
77
78    let sigls: Vec<serde_json::Value> = client
79        .get(&url)
80        .send()
81        .await
82        .map_err(|e| e.to_string())?
83        .json()
84        .await
85        .map_err(|e| e.to_string())?;
86
87    // Extrai apenas os IDs (pula o primeiro objeto que é metadata do catálogo)
88    let ids: Vec<String> = sigls
89        .iter()
90        .filter_map(|v| v["id"].as_str().map(|s| s.to_string()))
91        .collect();
92
93    // Etapa 2 — detalhes em batches de 20
94    let mut games = Vec::new();
95
96    for chunk in ids.chunks(GAME_PASS_BATCH_SIZE) {
97        let body = serde_json::json!({ "Products": chunk });
98
99        let response: serde_json::Value = client
100            .post(format!(
101                "https://catalog.gamepass.com/products?market=BR&language={}&hydration=MobileDetailsForConsole",
102                language
103            ))
104            .header("Content-Type", "application/json")
105            .json(&body)
106            .send()
107            .await
108            .map_err(|e| e.to_string())?
109            .json()
110            .await
111            .map_err(|e| e.to_string())?;
112
113        games.extend(parse_ea_play_products(&response["Products"]));
114
115        // Delay entre batches para não sobrecarregar
116        if !chunk.is_empty() {
117            tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
118        }
119    }
120
121    Ok(games)
122}