Skip to main content

game_manager_lib/commands/metadata/
pcgamingwiki.rs

1//! Comandos Tauri para obtenção e atualização de dados técnicos de jogos via PCGamingWiki.
2//!
3//! Implementa um fluxo **offline-first**: dados são servidos do cache SQLite sempre
4//! que disponíveis, e buscados online apenas quando necessário. Falhas de rede são
5//! tratadas graciosamente — o frontend recebe `None` em vez de um erro.
6
7use crate::database::AppState;
8use crate::errors::AppError;
9use crate::models::{GameDataPath, GameExtras, SystemRequirements};
10use crate::services::integration::pcgamingwiki::client::{search_pcgw_by_name, PcgwSearchResult};
11use crate::services::integration::pcgamingwiki::db::{
12    get_game_data_paths, get_pcgw_data, get_system_requirements, invalidate_pcgw_data,
13    save_pcgw_data, save_scraped_data,
14};
15use crate::services::integration::pcgamingwiki::fetch::fetch_pcgw_data;
16use chrono::Utc;
17use tracing::warn;
18
19// === ESTRUTURAS ===
20
21/// Dados completos do scraper para o frontend.
22///
23/// Retornados separadamente do `GameExtras` (Cargo) porque têm
24/// cardinalidade N:1 — múltiplas linhas por jogo.
25#[derive(Debug, serde::Serialize)]
26#[serde(rename_all = "camelCase")]
27pub struct PcgwScrapedResponse {
28    pub system_requirements: Vec<SystemRequirements>,
29    pub config_paths: Vec<GameDataPath>,
30    pub save_paths: Vec<GameDataPath>,
31}
32
33// === Comandos Tauri - API (fetch.rs) ===
34
35/// Retorna dados do PCGamingWiki para um jogo.
36///
37/// Fluxo offline-first:
38/// 1. Se existe no banco → retorna imediatamente (sem rede)
39/// 2. Se não existe → busca online e persiste
40/// 3. Se offline e sem dados → retorna erro tratável pelo frontend
41///
42/// O frontend deve tratar `AppError::NetworkError` exibindo a seção de
43/// dados técnicos como indisponível, sem quebrar o resto da view do jogo.
44#[tauri::command]
45pub async fn get_or_fetch_pcgw_data(
46    steam_app_id: String,
47    conn: tauri::State<'_, AppState>,
48) -> Result<Option<GameExtras>, String> {
49    {
50        let db = conn.games_db.lock().map_err(|e| e.to_string())?;
51        if let Some(data) = get_pcgw_data(&db, &steam_app_id) {
52            return Ok(Some(data));
53        }
54    }
55
56    match fetch_pcgw_data(&steam_app_id).await {
57        Ok((data, scraped)) => {
58            let db = conn.games_db.lock().map_err(|e| e.to_string())?;
59            save_pcgw_data(&db, &data).map_err(|e| e.to_string())?;
60
61            if let Some(scraped_data) = scraped {
62                save_scraped_data(&db, &data.steam_app_id, &scraped_data)
63                    .map_err(|e| e.to_string())?;
64            }
65
66            Ok(Some(data))
67        }
68        // Jogo não existe na PCGW — persiste linha vazia para não tentar novamente
69        Err(AppError::NotFound(msg)) => {
70            warn!(
71                "pcgw_data: jogo {} não encontrado na PCGW — {}",
72                steam_app_id, msg
73            );
74            let empty = GameExtras {
75                steam_app_id: steam_app_id.clone(),
76                fetched_at: Some(Utc::now().to_rfc3339()),
77                ..Default::default()
78            };
79            let db = conn.games_db.lock().map_err(|e| e.to_string())?;
80            save_pcgw_data(&db, &empty).map_err(|e| e.to_string())?;
81            Ok(None)
82        }
83        // Falha de rede — não persiste nada, frontend trata graciosamente
84        Err(AppError::NetworkError(msg)) => {
85            warn!("pcgw_data: falha de rede para {} — {}", steam_app_id, msg);
86            Ok(None)
87        }
88        Err(e) => Err(e.to_string()),
89    }
90}
91
92/// Força a re-busca dos dados do PCGamingWiki para um jogo.
93///
94/// Invalida o cache existente e dispara nova busca online imediatamente.
95/// Útil quando o usuário quer atualizar manualmente (ex.: após um patch grande).
96#[tauri::command]
97pub async fn refresh_pcgw_data(
98    steam_app_id: String,
99    conn: tauri::State<'_, AppState>,
100) -> Result<Option<GameExtras>, String> {
101    {
102        let db = conn.games_db.lock().map_err(|e| e.to_string())?;
103        invalidate_pcgw_data(&db, &steam_app_id).map_err(|e| e.to_string())?;
104    }
105
106    // Reutiliza o fluxo completo de busca
107    get_or_fetch_pcgw_data(steam_app_id, conn).await
108}
109
110/// Busca candidatos no PCGamingWiki por nome de jogo.
111///
112/// Retorna até 5 resultados ordenados por relevância para o usuário confirmar
113/// o jogo correto — útil quando o Steam AppID não está disponível ou a busca
114/// por AppID não retornou resultados.
115///
116/// # Erros
117/// Retorna `Err(String)` em caso de falha de rede ou resposta inesperada da API.
118#[tauri::command]
119pub async fn search_pcgw_games(game_name: String) -> Result<Vec<PcgwSearchResult>, String> {
120    search_pcgw_by_name(&game_name)
121        .await
122        .map_err(|e| e.to_string())
123}
124
125// === Comandos Tauri - scraper.rs ===
126
127/// Retorna os dados do scraper (requisitos e caminhos) para um jogo.
128///
129/// Fluxo:
130///   1. Consulta SQLite — se existir, retorna imediatamente
131///   2. Se não existir e `steam_app_id` estiver disponível → busca online
132///      via `fetch_pcgw_data` (que já salva scraper + cargo juntos)
133///   3. Retorna `None` se o jogo não foi encontrado na PCGW
134///      ou se não há Steam AppID disponível
135#[tauri::command]
136pub async fn get_pcgw_scraped_data(
137    steam_app_id: String,
138    conn: tauri::State<'_, AppState>,
139) -> Result<Option<PcgwScrapedResponse>, String> {
140    if steam_app_id.trim().is_empty() {
141        return Ok(None);
142    }
143
144    // 1. Verifica se há dados no banco (scraper já foi executado antes)
145    {
146        let db = conn.games_db.lock().map_err(|e| e.to_string())?;
147
148        // Se pcgw_data existe, o fetch já ocorreu — retorna scraper do cache
149        if get_pcgw_data(&db, &steam_app_id).is_some() {
150            let sysreqs = get_system_requirements(&db, &steam_app_id);
151            let all_paths = get_game_data_paths(&db, &steam_app_id);
152            let (config_paths, save_paths) = split_paths(all_paths);
153            return Ok(Some(PcgwScrapedResponse {
154                system_requirements: sysreqs,
155                config_paths,
156                save_paths,
157            }));
158        }
159    }
160
161    // 2. Não há dados — dispara fetch completo (cargo + scraper). Reutiliza get_or_fetch_pcgw_data
162    match fetch_pcgw_data(&steam_app_id).await {
163        Ok((pcgw_data, scraped)) => {
164            let db = conn.games_db.lock().map_err(|e| e.to_string())?;
165
166            // Persiste cargo data (pode já existir — idempotente via REPLACE)
167            save_pcgw_data(&db, &pcgw_data).map_err(|e| e.to_string())?;
168
169            if let Some(scraped_data) = scraped {
170                save_scraped_data(&db, &steam_app_id, &scraped_data).map_err(|e| e.to_string())?;
171
172                let (config_paths, save_paths) = split_paths(scraped_data.game_data_paths);
173
174                return Ok(Some(PcgwScrapedResponse {
175                    system_requirements: scraped_data.system_requirements,
176                    config_paths,
177                    save_paths,
178                }));
179            }
180
181            // fetch ok mas scraper retornou None (falha de rede no wikitext)
182            Ok(None)
183        }
184        Err(AppError::NotFound(_)) => Ok(None),
185        Err(AppError::NetworkError(msg)) => {
186            warn!("get_pcgw_scraped_data: falha de rede — {}", msg);
187            Ok(None)
188        }
189        Err(e) => Err(e.to_string()),
190    }
191}
192
193// === HELPERS ===
194
195/// Separa `Vec<GameDataPath>` em (config_paths, save_paths).
196fn split_paths(paths: Vec<GameDataPath>) -> (Vec<GameDataPath>, Vec<GameDataPath>) {
197    let config = paths
198        .iter()
199        .filter(|p| p.kind == "config")
200        .cloned()
201        .collect();
202    let saves = paths
203        .iter()
204        .filter(|p| p.kind == "saves")
205        .cloned()
206        .collect();
207    (config, saves)
208}