Skip to main content

game_manager_lib/services/
subscriptions.rs

1//! Modulo para buscar games de serviços de assinatura e gerenciar quais serviços o usuário assina.
2
3use crate::constants::{
4    AMAZON_LUNA_CACHE_KEY, AMAZON_LUNA_CACHE_SOURCE, EA_PLAY_CACHE_KEY, EA_PLAY_CACHE_SOURCE,
5    GAME_PASS_CACHE_SOURCE, GAME_PASS_FULL_CACHE_KEY, UBISOFT_PLUS_CACHE_KEY,
6    UBISOFT_PLUS_CACHE_SOURCE,
7};
8use crate::database::AppState;
9use crate::scrapers::amazon_luna::{fetch_amazon_luna_catalog, LunaGame};
10use crate::scrapers::game_pass::{fetch_game_pass_pc_catalog, GamePassGame};
11use crate::scrapers::ubisoft_plus::{fetch_ubisoft_plus_catalog, UbisoftGame};
12use crate::scrapers::{fetch_ea_play_catalog, EAPlayGame};
13use crate::services::cache;
14use rusqlite::params;
15use tauri::State;
16
17/// Normaliza tags de idioma para formato padrão (ex: pt -> pt-BR, en -> en-US)
18fn normalize_lang_tag(lang: &str) -> String {
19    let lower = lang.to_lowercase();
20    if lower.starts_with("pt") {
21        "pt-BR".to_string()
22    } else {
23        "en-US".to_string() // fallback
24    }
25}
26
27/// Mapeia idioma para o header prime-gaming-language (Amazon Luna)
28fn amazon_luna_lang_tag(lang: &str) -> String {
29    let normalized = normalize_lang_tag(lang);
30    if normalized.starts_with("pt") {
31        "pt-PT".to_string() // Amazon Luna usa pt-PT, não pt-BR
32    } else {
33        "en-US".to_string()
34    }
35}
36
37/// Constrói chave de cache specific a idioma
38fn catalog_cache_key(base_key: &str, lang: &str) -> String {
39    let normalized = normalize_lang_tag(lang);
40    format!("{}_{}", base_key, normalized)
41}
42
43/// Retorna catálogo do Amazon Luna (do cache ou scraping)
44pub async fn get_amazon_luna_games(
45    state: &State<'_, AppState>,
46    lang: &str,
47) -> Result<Vec<LunaGame>, String> {
48    let _normalized_lang = normalize_lang_tag(lang);
49    let cache_key = catalog_cache_key(AMAZON_LUNA_CACHE_KEY, lang);
50    let luna_lang = amazon_luna_lang_tag(lang);
51
52    let cached = {
53        let conn = state.cache_db.lock().map_err(|e| e.to_string())?;
54        cache::get_cached_api_data(&conn, AMAZON_LUNA_CACHE_SOURCE, &cache_key)
55    };
56
57    if let Some(cached) = cached {
58        if let Ok(games) = serde_json::from_str::<Vec<LunaGame>>(&cached) {
59            return Ok(games);
60        }
61    }
62
63    let games = fetch_amazon_luna_catalog(&luna_lang).await?;
64
65    {
66        let conn = state.cache_db.lock().map_err(|e| e.to_string())?;
67        let payload = serde_json::to_string(&games).map_err(|e| e.to_string())?;
68        cache::save_cached_api_data(&conn, AMAZON_LUNA_CACHE_SOURCE, &cache_key, &payload)?;
69    }
70
71    Ok(games)
72}
73
74/// Retorna catálogo do Game Pass PC (do cache ou scraping).
75/// Quando `exclude_ea_play = true`, remove jogos EA Play do resultado.
76pub async fn get_game_pass_games(
77    state: &State<'_, AppState>,
78    exclude_ea_play: bool,
79    lang: &str,
80) -> Result<Vec<GamePassGame>, String> {
81    let normalized_lang = normalize_lang_tag(lang);
82    let cache_key = catalog_cache_key(GAME_PASS_FULL_CACHE_KEY, lang);
83
84    // Tenta cache primeiro — sempre armazena o catálogo COMPLETO
85    let cached = {
86        let conn = state.cache_db.lock().map_err(|e| e.to_string())?;
87        cache::get_cached_api_data(&conn, GAME_PASS_CACHE_SOURCE, &cache_key)
88    };
89
90    let all_games: Vec<GamePassGame> = if let Some(data) = cached {
91        serde_json::from_str(&data).unwrap_or_default()
92    } else {
93        // Cache miss — busca catálogo completo (`exclude_ea_play = false`).
94        let games = fetch_game_pass_pc_catalog(false, &normalized_lang).await?;
95        let payload = serde_json::to_string(&games).map_err(|e| e.to_string())?;
96        let conn = state.cache_db.lock().map_err(|e| e.to_string())?;
97        cache::save_cached_api_data(&conn, GAME_PASS_CACHE_SOURCE, &cache_key, &payload)?;
98        games
99    };
100
101    // O filtro final fica na camada de serviço para reaproveitar o mesmo cache.
102    if exclude_ea_play {
103        Ok(all_games.into_iter().filter(|g| !g.is_ea_play).collect())
104    } else {
105        Ok(all_games)
106    }
107}
108
109/// Retorna catálogo do EA Play
110pub async fn get_ea_play_games(
111    state: &State<'_, AppState>,
112    lang: &str,
113) -> Result<Vec<EAPlayGame>, String> {
114    let normalized_lang = normalize_lang_tag(lang);
115    let cache_key = catalog_cache_key(EA_PLAY_CACHE_KEY, lang);
116
117    let cached = {
118        let conn = state.cache_db.lock().map_err(|e| e.to_string())?;
119        cache::get_cached_api_data(&conn, EA_PLAY_CACHE_SOURCE, &cache_key)
120    };
121
122    let all_games: Vec<EAPlayGame> = if let Some(data) = cached {
123        serde_json::from_str(&data).unwrap_or_default()
124    } else {
125        let games = fetch_ea_play_catalog(&normalized_lang).await?;
126        let payload = serde_json::to_string(&games).map_err(|e| e.to_string())?;
127        let conn = state.cache_db.lock().map_err(|e| e.to_string())?;
128        cache::save_cached_api_data(&conn, EA_PLAY_CACHE_SOURCE, &cache_key, &payload)?;
129        games
130    };
131
132    Ok(all_games)
133}
134
135/// Retorna catálogo do Ubisoft+ (do cache ou scraping)
136pub async fn get_ubisoft_plus_games(
137    state: &State<'_, AppState>,
138) -> Result<Vec<UbisoftGame>, String> {
139    let cached = {
140        let conn = state.cache_db.lock().map_err(|e| e.to_string())?;
141        cache::get_cached_api_data(&conn, UBISOFT_PLUS_CACHE_SOURCE, UBISOFT_PLUS_CACHE_KEY)
142    };
143
144    if let Some(cached) = cached {
145        if let Ok(games) = serde_json::from_str::<Vec<UbisoftGame>>(&cached) {
146            return Ok(games);
147        }
148    }
149
150    let games = fetch_ubisoft_plus_catalog().await?;
151
152    {
153        let conn = state.cache_db.lock().map_err(|e| e.to_string())?;
154        let payload = serde_json::to_string(&games).map_err(|e| e.to_string())?;
155        cache::save_cached_api_data(
156            &conn,
157            UBISOFT_PLUS_CACHE_SOURCE,
158            UBISOFT_PLUS_CACHE_KEY,
159            &payload,
160        )?;
161    }
162
163    Ok(games)
164}
165
166/// Obtém a lista de serviços assinados pelo usuário (Amazon Luna, Game Pass, EA Play, Ubisoft+)
167pub fn get_enabled_services(state: &State<'_, AppState>) -> Result<Vec<String>, String> {
168    let conn = state.games_db.lock().map_err(|e| e.to_string())?;
169    let mut stmt = conn
170        .prepare("SELECT service FROM subscriptions WHERE enabled = 1")
171        .map_err(|e| e.to_string())?;
172
173    let services = stmt
174        .query_map([], |row| row.get::<_, String>(0))
175        .map_err(|e| e.to_string())?
176        .filter_map(|r| r.ok())
177        .collect();
178
179    Ok(services)
180}
181
182/// Salva quais serviços o usuário assina
183pub fn set_enabled_services(
184    state: &State<'_, AppState>,
185    services: Vec<String>,
186) -> Result<(), String> {
187    let conn = state.games_db.lock().map_err(|e| e.to_string())?;
188
189    // Reseta todos para disabled
190    conn.execute("UPDATE subscriptions SET enabled = 0", [])
191        .map_err(|e| e.to_string())?;
192
193    // Habilita os selecionados (upsert)
194    for service in services {
195        conn.execute(
196            "INSERT INTO subscriptions (service, enabled)
197             VALUES (?1, 1)
198             ON CONFLICT(service) DO UPDATE SET enabled = 1",
199            params![service],
200        )
201        .map_err(|e| e.to_string())?;
202    }
203
204    Ok(())
205}