Skip to main content

game_manager_lib/services/integration/pcgamingwiki/
fetch.rs

1//! Busca e montagem dos dados técnicos de um jogo no PCGamingWiki.
2//!
3//! Orquestra 7 queries sequenciais à Cargo API, cobrindo as tabelas:
4//! `Infobox_game`, `API`, `Video`, `Input`, `Audio`, `L10n` e `Tags`.
5//!
6//! O rate limiting é aplicado automaticamente em cada query via `cargo_query`.
7
8use crate::errors::AppError;
9use crate::models::{GameExtras, PcgwScrapedData};
10use crate::services::integration::pcgamingwiki::client::{build_http_client, cargo_query};
11use crate::services::integration::pcgamingwiki::parsers::{
12    extract_field, normalize_bool_field, parse_l10n_rows,
13};
14use crate::services::integration::pcgamingwiki::scraper::scrape_pcgw_page;
15use chrono::Utc;
16use tracing::info;
17
18/// Busca todos os dados técnicos de um jogo no PCGamingWiki pelo Steam AppID.
19///
20/// Faz até 8 queries sequenciais (com delay entre elas) cobrindo todas as
21/// tabelas Cargo relevantes. Retorna `None` se o jogo não for encontrado na PCGW.
22///
23/// # Erros
24/// - `AppError::NetworkError` — falha de rede (sem internet, timeout, rate limit)
25/// - `AppError::ParseError` — resposta inesperada da API
26/// - `AppError::NotFound` — jogo não encontrado na PCGW pelo AppID fornecido
27pub async fn fetch_pcgw_data(
28    steam_app_id: &str,
29) -> Result<(GameExtras, Option<PcgwScrapedData>), AppError> {
30    let client = build_http_client()?;
31
32    // ------------------------------------------------------------------
33    // 1. Infobox_game — resolve page_id, page_name e engine
34    // ------------------------------------------------------------------
35    let infobox_rows = cargo_query(
36        &client,
37        "Infobox_game",
38        "Infobox_game._pageID=pageID,\
39         Infobox_game._pageName=pageName,\
40         Infobox_game.Engines=engine, \
41         Infobox_game.Available_on=available_on",
42        &format!("Infobox_game.Steam_AppID HOLDS \"{}\"", steam_app_id),
43        3, // Limit baixo porque esperamos no máximo 1 resultado; 3 é margem para casos inesperados
44    )
45    .await?;
46
47    if infobox_rows.is_empty() {
48        return Err(AppError::NotFound(format!(
49            "Jogo com Steam AppID {} não encontrado no PCGamingWiki",
50            steam_app_id
51        )));
52    }
53
54    let page_id = extract_field(&infobox_rows, "pageID").ok_or_else(|| {
55        AppError::ParseError("pageID ausente na resposta da Infobox_game".to_string())
56    })?;
57
58    let page_name = extract_field(&infobox_rows, "pageName");
59    let engine = extract_field(&infobox_rows, "engine");
60
61    info!(
62        "PCGW: jogo encontrado — page_id={}, name={:?}",
63        page_id, page_name
64    );
65
66    // Filtro por page_id usado nas demais queries
67    let page_filter = format!("_pageID=\"{}\"", page_id);
68
69    // Busca wikitext (system requirements + game data paths) em paralelo com as
70    // queries Cargo restantes — salvo separadamente pelo chamador via save_scraped_data
71    let scraped = scrape_pcgw_page(&client, &page_id, steam_app_id).await.ok();
72
73    // ------------------------------------------------------------------
74    // 2. OS — suporte a sistemas operacionais
75    // ------------------------------------------------------------------
76    let api_rows = cargo_query(
77        &client,
78        "API",
79        "API.Direct3D_versions=dx_versions,\
80     API.Vulkan_versions=vulkan_versions,\
81     API.OpenGL_versions=opengl_versions,\
82     API.Windows_64bit_executable=win64,\
83     API.Linux_64bit_executable=linux64,\
84     API.macOS_ARM_app=macos_arm,\
85     API.macOS_Intel_64bit_app=macos_intel64",
86        &page_filter,
87        3,
88    )
89    .await?;
90
91    let available_on = extract_field(&infobox_rows, "available_on");
92    let dx_versions = extract_field(&api_rows, "dx_versions");
93    let vulkan_versions = extract_field(&api_rows, "vulkan_versions");
94    let opengl_versions = extract_field(&api_rows, "opengl_versions");
95    let win64 = normalize_bool_field(extract_field(&api_rows, "win64"));
96    let linux64 = normalize_bool_field(extract_field(&api_rows, "linux64"));
97    let macos_arm = normalize_bool_field(extract_field(&api_rows, "macos_arm"));
98    let macos_intel64 = normalize_bool_field(extract_field(&api_rows, "macos_intel64"));
99
100    // ------------------------------------------------------------------
101    // 3. Video_settings — gráficos, display e acessibilidade visual
102    // ------------------------------------------------------------------
103    let video_rows = cargo_query(
104        &client,
105        "Video",
106        "Video.Ray_tracing=ray_tracing,\
107     Video.Upscaling=upscaling,\
108     Video.Frame_gen=frame_gen,\
109     Video.Ultrawidescreen=ultrawide,\
110     Video.4K_Ultra_HD=four_k,\
111     Video.HDR=hdr,\
112     Video.120_FPS=high_fps,\
113     Video.Field_of_view=fov,\
114     Video.Borderless_fullscreen_windowed=borderless,\
115     Video.Color_blind=color_blind",
116        &page_filter,
117        3,
118    )
119    .await?;
120
121    let ray_tracing = normalize_bool_field(extract_field(&video_rows, "ray_tracing"));
122    let upscaling = extract_field(&video_rows, "upscaling");
123    let frame_gen = extract_field(&video_rows, "frame_gen");
124    let ultrawidescreen = normalize_bool_field(extract_field(&video_rows, "ultrawide"));
125    let four_k_support = normalize_bool_field(extract_field(&video_rows, "four_k"));
126    let hdr = normalize_bool_field(extract_field(&video_rows, "hdr"));
127    let high_fps = normalize_bool_field(extract_field(&video_rows, "high_fps"));
128    let fov = normalize_bool_field(extract_field(&video_rows, "fov"));
129    let borderless_windowed = normalize_bool_field(extract_field(&video_rows, "borderless"));
130    let color_blind = normalize_bool_field(extract_field(&video_rows, "color_blind"));
131
132    // ------------------------------------------------------------------
133    // 4. Input — suporte a controles
134    // ------------------------------------------------------------------
135    let input_rows = cargo_query(
136        &client,
137        "Input",
138        "Input.Controller_support=ctrl_support,\
139         Input.Full_controller_support=full_ctrl,\
140         Input.Playstation_controller_support=ps_ctrl,\
141         Input.XInput_controller_support=xinput",
142        &page_filter,
143        3,
144    )
145    .await?;
146
147    let controller_support = normalize_bool_field(extract_field(&input_rows, "ctrl_support"));
148    let full_controller = normalize_bool_field(extract_field(&input_rows, "full_ctrl"));
149    let playstation_controllers = normalize_bool_field(extract_field(&input_rows, "ps_ctrl"));
150    let xinput_controllers = normalize_bool_field(extract_field(&input_rows, "xinput"));
151
152    // ------------------------------------------------------------------
153    // 5. Audio_settings — surround sound, legendas e acessibilidade
154    // ------------------------------------------------------------------
155    let audio_rows = cargo_query(
156        &client,
157        "Audio",
158        "Audio.Surround_sound=surround,\
159         Audio.Subtitles=subtitles,\
160         Audio.Closed_captions=closed_captions",
161        &page_filter,
162        3,
163    )
164    .await?;
165
166    let surround_sound = normalize_bool_field(extract_field(&audio_rows, "surround"));
167    let subtitles = normalize_bool_field(extract_field(&audio_rows, "subtitles"));
168    let closed_captions = normalize_bool_field(extract_field(&audio_rows, "closed_captions"));
169
170    // ------------------------------------------------------------------------------------------
171    // 6. L10n — idiomas suportados - A tabela L10n tem uma linha por idioma - Agrupa por tipo.
172    // ------------------------------------------------------------------------------------------
173    let l10n_rows = cargo_query(
174        &client,
175        "L10n",
176        "L10n.Language=language,\
177         L10n.Interface=interface,\
178         L10n.Audio=audio,\
179         L10n.Subtitles=subtitles",
180        &page_filter,
181        50, // Muitos idiomas possíveis
182    )
183    .await?;
184
185    let (languages_interface, languages_audio, languages_subtitles) = parse_l10n_rows(&l10n_rows);
186
187    // ------------------------------------------------
188    // 7. Tags — presença de dados de save e config
189    // ------------------------------------------------
190    let tags_rows = cargo_query(
191        &client,
192        "Tags",
193        "Tags.Save_data=save_data,\
194     Tags.Config_data=config_data",
195        &page_filter,
196        3,
197    )
198    .await?;
199
200    let has_save_data = normalize_bool_field(extract_field(&tags_rows, "save_data"));
201    let has_config_data = normalize_bool_field(extract_field(&tags_rows, "config_data"));
202
203    // ------------------------------------------------------------------
204    // Montagem do struct final
205    // ------------------------------------------------------------------
206    let fetched_at = Utc::now().to_rfc3339();
207
208    Ok((
209        GameExtras {
210            steam_app_id: steam_app_id.to_string(),
211            pcgw_page_id: Some(page_id),
212            pcgw_page_name: page_name,
213            engine,
214            available_on,
215            // API
216            dx_versions,
217            vulkan_versions,
218            opengl_versions,
219            win64,
220            linux64,
221            macos_arm,
222            macos_intel64,
223            // Video
224            ray_tracing,
225            upscaling,
226            frame_gen,
227            ultrawidescreen,
228            four_k_support,
229            hdr,
230            high_fps,
231            fov,
232            borderless_windowed,
233            color_blind,
234            // Input
235            controller_support,
236            full_controller,
237            playstation_controllers,
238            xinput_controllers,
239            // Audio
240            surround_sound,
241            subtitles,
242            closed_captions,
243            // Tags
244            has_save_data,
245            has_config_data,
246            // L10n
247            languages_interface,
248            languages_audio,
249            languages_subtitles,
250            fetched_at: Some(fetched_at),
251        },
252        scraped,
253    ))
254}