Skip to main content

game_manager_lib/scrapers/
amazon_luna.rs

1//! Scraper - Amazon Luna
2//!
3//! - URL: https://luna.amazon.com/claims/home
4//! - Descrição: Scraper para obter a lista de jogos gratuitos disponíveis na Amazon Luna,
5//!   incluindo título, descrição, imagem, link de resgate e data de término da oferta.
6//! - Método: Acesso à página de reivindicações da Amazon Luna para obter cookies de sessão e token CSRF,
7//!   seguido por uma requisição POST GraphQL para a API interna da Amazon Luna, utilizando os cookies
8//!   e token para autenticação.
9
10use reqwest::{cookie::Jar, Client};
11use scraper::{Html, Selector};
12use serde::{Deserialize, Serialize};
13use std::sync::Arc;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct LunaGame {
17    pub title: String,
18    pub description: Option<String>,
19    pub image_url: Option<String>,
20    pub claim_url: String,
21    pub end_time: Option<String>,
22}
23pub async fn fetch_amazon_luna_catalog(language: &str) -> Result<Vec<LunaGame>, String> {
24    let jar = Arc::new(Jar::default());
25    let client = Client::builder()
26        .cookie_provider(jar)
27        .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
28        .build()
29        .map_err(|e| e.to_string())?;
30
31    let html = client
32        .get("https://luna.amazon.com/claims/home")
33        .send()
34        .await
35        .map_err(|e| e.to_string())?
36        .text()
37        .await
38        .map_err(|e| e.to_string())?;
39
40    let csrf_token = {
41        let document = Html::parse_document(&html);
42        let selector = Selector::parse("input[name='csrf-key']").map_err(|e| e.to_string())?;
43        document
44            .select(&selector)
45            .next()
46            .and_then(|el| el.value().attr("value"))
47            .ok_or("csrf-key não encontrado")?
48            .to_string() // .to_string() move o valor para fora do escopo
49    };
50
51    // Payload GraphQL mínimo para solicitar os campos usados no parsing abaixo.
52    let payload = serde_json::json!({
53        "extensions": {},
54        "operationName": "OffersContext_Offers_And_Items",
55        "query": "query OffersContext_Offers_And_Items($pageSize: Int) {\n  games: items(collectionType: FREE_GAMES, pageSize: $pageSize) {\n    items {\n      id\n      assets {\n        title\n        externalClaimLink\n        shortformDescription\n        cardMedia { defaultMedia { src1x src2x __typename } __typename }\n        __typename\n      }\n      offers { id startTime endTime __typename }\n      __typename\n    }\n    __typename\n  }\n}",
56        "variables": { "pageSize": 999 }
57    });
58
59    let response: serde_json::Value = client
60        .post("https://luna.amazon.com/graphql")
61        .header("Content-Type", "application/json")
62        .header("Client-Id", "CarboniteApp")
63        .header("Origin", "https://luna.amazon.com")
64        .header("Referer", "https://luna.amazon.com/claims/home")
65        .header("prime-gaming-language", language)
66        .header("csrf-token", csrf_token)
67        .json(&payload)
68        .send()
69        .await
70        .map_err(|e| e.to_string())?
71        .json()
72        .await
73        .map_err(|e| e.to_string())?;
74
75    // Parseia os jogos
76    let items = response["data"]["games"]["items"]
77        .as_array()
78        .ok_or("Campo games não encontrado")?;
79
80    let games = items
81        .iter()
82        .filter_map(|item| {
83            let assets = item["assets"].as_object()?;
84            let offer = item["offers"].as_array()?.first()?;
85
86            let card_media = assets.get("cardMedia")?.as_object()?;
87            let default_media = card_media.get("defaultMedia")?.as_object()?;
88
89            Some(LunaGame {
90                title: assets.get("title")?.as_str()?.to_string(),
91                description: assets
92                    .get("shortformDescription")
93                    .and_then(|value| value.as_str())
94                    .map(|value| value.to_string()),
95                image_url: default_media
96                    .get("src2x")
97                    .and_then(|value| value.as_str())
98                    .or_else(|| default_media.get("src1x").and_then(|value| value.as_str()))
99                    .map(|value| value.to_string()),
100                claim_url: assets.get("externalClaimLink")?.as_str()?.to_string(),
101                end_time: offer["endTime"].as_str().map(|s| s.to_string()),
102            })
103        })
104        .collect();
105
106    Ok(games)
107}