game_manager_lib/scrapers/
ubisoft_plus.rs1use reqwest::Client;
10use serde::{Deserialize, Serialize};
11
12const ALGOLIA_BASE_URL: &str = "https://xely3u4lod-dsn.algolia.net/1/indexes/\
15 production__br_ubisoft__products__pt_BR__release_date/query";
16
17const ALGOLIA_AGENT: &str = "Algolia for JavaScript (4.13.1); Browser (lite)";
18const ALGOLIA_API_KEY: &str = "5638539fd9edb8f2c6b024b49ec375bd";
19const ALGOLIA_APP_ID: &str = "XELY3U4LOD";
20
21const UBISOFT_PLUS_PREMIUM_OFFER_ID: &str = "5f44de7b5cdf9a0c2027ca78";
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct UbisoftGame {
29 pub title: String,
30 pub short_title: Option<String>,
31 pub edition: Option<String>,
32 pub genre: Option<String>,
33 pub image_url: Option<String>,
34 pub store_url: String,
35 pub release_date: Option<String>,
36 pub subscription_expiration_date: Option<String>,
38 pub adult: bool,
40 pub streaming_platforms: Vec<String>,
42}
43
44#[derive(Debug, Deserialize)]
47struct AlgoliaResponse {
48 hits: Vec<AlgoliaHit>,
49}
50
51#[derive(Debug, Deserialize)]
52struct AlgoliaHit {
53 title: Option<String>,
54 short_title: Option<String>,
56 #[serde(rename = "Edition")]
57 edition: Option<String>,
58 #[serde(rename = "Genre")]
59 genre: Option<String>,
60 image_link: Option<String>,
63 id: Option<String>,
64 release_date: Option<String>,
65 #[serde(rename = "subscriptionExpirationDate")]
67 subscription_expiration_date: Option<String>,
68 adult: Option<String>,
70 #[serde(default, rename = "anywherePlatforms")]
71 anywhere_platforms: Vec<String>,
72}
73
74pub async fn fetch_ubisoft_plus_catalog() -> Result<Vec<UbisoftGame>, String> {
81 let client = Client::builder()
82 .user_agent(
83 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
84 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
85 )
86 .build()
87 .map_err(|e| e.to_string())?;
88
89 let payload = serde_json::json!({
91 "query": "",
92 "attributesToRetrieve": [
93 "title", "image_link", "short_title", "id", "MasterID",
94 "Genre", "release_date", "partOfUbisoftPlus", "anywherePlatforms",
95 "subscriptionExpirationDate", "Edition", "adult", "partofSubscriptionOffer"
96 ],
97 "hitsPerPage": 9999,
98 "facetFilters": [
99 ["partOfUbisoftPlus:true"],
100 [],
101 [],
102 [format!("partofSubscriptionOfferID:{UBISOFT_PLUS_PREMIUM_OFFER_ID}")]
103 ],
104 "clickAnalytics": true
105 });
106
107 let url = format!(
108 "{}?x-algolia-agent={}&x-algolia-api-key={}&x-algolia-application-id={}",
109 ALGOLIA_BASE_URL,
110 urlencoding::encode(ALGOLIA_AGENT),
111 ALGOLIA_API_KEY,
112 ALGOLIA_APP_ID,
113 );
114
115 let response: AlgoliaResponse = client
116 .post(&url)
117 .header("Content-Type", "application/x-www-form-urlencoded")
118 .header("Origin", "https://store.ubisoft.com")
119 .header("Referer", "https://store.ubisoft.com/")
120 .json(&payload)
121 .send()
122 .await
123 .map_err(|e| format!("Erro na requisição: {e}"))?
124 .json()
125 .await
126 .map_err(|e| format!("Erro ao parsear resposta: {e}"))?;
127
128 let games = response
129 .hits
130 .into_iter()
131 .filter_map(|hit| {
132 let title = hit.title?;
134 let id = hit.id?;
135
136 let store_url = format!("https://store.ubisoft.com/br/game/{id}.html");
138
139 Some(UbisoftGame {
140 title,
141 short_title: hit.short_title,
142 edition: hit.edition,
143 genre: hit.genre,
144 image_url: hit.image_link,
145 store_url,
146 release_date: hit.release_date,
147 subscription_expiration_date: hit
148 .subscription_expiration_date
149 .filter(|s| !s.is_empty()),
150 adult: hit.adult.as_deref() == Some("TRUE"),
151 streaming_platforms: hit.anywhere_platforms,
152 })
153 })
154 .collect();
155
156 Ok(games)
157}