game_manager_lib/scrapers/
game_pass.rs1use crate::constants::GAME_PASS_BATCH_SIZE;
4use reqwest::Client;
5use serde::{Deserialize, Serialize};
6
7const GAME_PASS_PC_SIGL: &str = "fdd9e2a7-0fee-49f6-ad69-4354098401ff";
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct GamePassGame {
13 pub store_id: String,
14 pub title: String,
15 pub description: Option<String>,
16 pub image_poster: Option<String>, pub image_hero: Option<String>, pub categories: Vec<String>,
19 pub developer: Option<String>,
20 pub is_ea_play: bool,
21 pub store_url: String,
22}
23
24fn parse_game_pass_products(
25 products: &serde_json::Value,
26 exclude_ea_play: bool,
27) -> Vec<GamePassGame> {
28 let mut games = Vec::new();
29
30 let Some(products) = products.as_object() else {
31 return games;
32 };
33
34 for (store_id, product) in products {
35 if product["ProductType"].as_str() != Some("Game") {
37 continue;
38 }
39
40 let is_ea_play = product["IsEAPlay"].as_bool().unwrap_or(false);
42 if exclude_ea_play && is_ea_play {
43 continue;
44 }
45
46 let platforms = product["AllowedPlatforms"]
48 .as_array()
49 .map(|p| p.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
50 .unwrap_or_default();
51
52 if !platforms.contains(&"Windows.Desktop") {
53 continue;
54 }
55
56 let categories = product["Categories"]
57 .as_array()
58 .map(|arr| {
59 arr.iter()
60 .filter_map(|v| v.as_str().map(|s| s.to_string()))
61 .collect()
62 })
63 .unwrap_or_default();
64
65 games.push(GamePassGame {
66 store_id: store_id.clone(),
67 title: product["ProductTitle"].as_str().unwrap_or("?").to_string(),
68 description: product["ProductDescription"]
69 .as_str()
70 .map(|s| s.to_string()),
71 image_poster: product["ImagePoster"]["URI"]
72 .as_str()
73 .map(|s| s.to_string()),
74 image_hero: product["ImageHero"]["URI"].as_str().map(|s| s.to_string()),
75 categories,
76 developer: product["DeveloperName"].as_str().map(|s| s.to_string()),
77 is_ea_play,
78 store_url: format!("https://www.xbox.com/pt-BR/games/store/_/{}", store_id),
79 });
80 }
81
82 games
83}
84
85pub async fn fetch_game_pass_pc_catalog(
86 exclude_ea_play: bool,
87 language: &str,
88) -> Result<Vec<GamePassGame>, String> {
89 let client = Client::new();
90
91 let url = format!(
93 "https://catalog.gamepass.com/sigls/v2?id={}&language={}&market=BR",
94 GAME_PASS_PC_SIGL, language
95 );
96
97 let sigls: Vec<serde_json::Value> = client
98 .get(&url)
99 .send()
100 .await
101 .map_err(|e| e.to_string())?
102 .json()
103 .await
104 .map_err(|e| e.to_string())?;
105
106 let ids: Vec<String> = sigls
108 .iter()
109 .filter_map(|v| v["id"].as_str().map(|s| s.to_string()))
110 .collect();
111
112 let mut games = Vec::new();
114
115 for chunk in ids.chunks(GAME_PASS_BATCH_SIZE) {
116 let body = serde_json::json!({ "Products": chunk });
117
118 let response: serde_json::Value = client
119 .post(format!(
120 "https://catalog.gamepass.com/products?market=BR&language={}&hydration=MobileDetailsForConsole",
121 language
122 ))
123 .header("Content-Type", "application/json")
124 .json(&body)
125 .send()
126 .await
127 .map_err(|e| e.to_string())?
128 .json()
129 .await
130 .map_err(|e| e.to_string())?;
131
132 games.extend(parse_game_pass_products(
133 &response["Products"],
134 exclude_ea_play,
135 ));
136
137 if !chunk.is_empty() {
139 tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
140 }
141 }
142
143 Ok(games)
144}