game_manager_lib/scrapers/
amazon_luna.rs1use 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() };
50
51 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 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}