Skip to main content

game_manager_lib/services/integration/gamebrain/
media.rs

1//! Busca de mídia (screenshots e videos) via GameBrain.
2
3use crate::database;
4use crate::utils::http_client::HTTP_CLIENT;
5
6use tauri::AppHandle;
7
8use super::cache::{
9    gamebrain_id_cache_key, gamebrain_media_cache_key, read_cached_json, read_stale_gamebrain_id,
10    save_cached_json,
11};
12use super::helpers::{build_game_media, resolve_gamebrain_id};
13use super::models::GameMedia;
14use super::raw::RawGameDetail;
15
16/// Busca a mídia (screenshots, trailers, embeds YouTube) de um jogo via GameBrain.
17///
18/// **Fluxo:**
19/// ```text
20/// playlite_game_id (UUID)
21///   → resolve gamebrain_id (mesmo caminho de fetch_similar_games)
22///   → cache sqlite gamebrain_media:{gamebrain_id}?  → GameMedia
23///   → se não: GET /v1/games/{gamebrain_id}
24///             → extrai apenas campos de mídia
25///             → classifica vídeos em trailers vs youtube_embeds
26///             → salva no cache
27/// ```
28///
29/// **Cache:** TTL configurado em `cache.rs` pela chave `gamebrain_media:`.
30/// Adicionar ao `get_ttl_for_cache_type` e `cleanup_expired_cache` em cache.rs
31/// (ver comentário no final deste arquivo).
32///
33/// **Parâmetros:**
34/// - `playlite_game_id`: UUID interno do Playlite
35/// - `game_name`: nome do jogo, usado como fallback para resolver o gamebrain_id via /suggestions se não estiver em cache
36pub async fn fetch_game_media(
37    app: &AppHandle,
38    playlite_game_id: &str,
39    game_name: &str,
40) -> Result<GameMedia, String> {
41    let api_key = database::get_secret(app, "gamebrain_api_key").map_err(|e| e.to_string())?;
42
43    if api_key.trim().is_empty() {
44        return Err("GameBrain API key não configurada".into());
45    }
46
47    // Etapa 1: resolver gamebrain_id
48    // Reutiliza o mesmo cache de ID da fetch_similar_games —
49    // se o usuário já abriu a aba Descoberta antes, o ID já está em cache.
50    let gamebrain_id = {
51        let id_cache_key = gamebrain_id_cache_key(playlite_game_id);
52
53        if let Some(id) = read_cached_json::<u64>(app, "gamebrain", &id_cache_key, false)? {
54            tracing::debug!(
55                "GameBrain media: ID cache hit => game_id='{}' gamebrain_id={}",
56                playlite_game_id,
57                id
58            );
59            id
60        } else {
61            tracing::debug!(
62                "GameBrain media: ID cache miss => resolvendo '{}' via suggestions",
63                game_name
64            );
65
66            match resolve_gamebrain_id(&api_key, game_name).await {
67                Ok(Some(id)) => {
68                    let _ = save_cached_json(app, "gamebrain", &id_cache_key, &id);
69                    id
70                }
71                Ok(None) => {
72                    // Tenta ID stale antes de falhar
73                    read_stale_gamebrain_id(app, &id_cache_key, playlite_game_id)?.ok_or_else(
74                        || format!("Jogo '{}' não encontrado na GameBrain", game_name),
75                    )?
76                }
77                Err(err) => {
78                    read_stale_gamebrain_id(app, &id_cache_key, playlite_game_id)?.ok_or(err)?
79                }
80            }
81        }
82    };
83
84    // Etapa 2: buscar mídia do game detail
85    let media_cache_key = gamebrain_media_cache_key(gamebrain_id);
86
87    if let Some(cached) = read_cached_json::<GameMedia>(app, "gamebrain", &media_cache_key, false)?
88    {
89        tracing::debug!("GameBrain media cache hit => gamebrain_id={}", gamebrain_id);
90        return Ok(cached);
91    }
92
93    // Cache miss: chama GET /v1/games/{id}
94    tracing::debug!(
95        "GameBrain media cache miss => chamando /v1/games/{} para mídia",
96        gamebrain_id
97    );
98
99    let url = format!("https://api.gamebrain.co/v1/games/{}", gamebrain_id);
100
101    let response = HTTP_CLIENT
102        .get(&url)
103        .header("x-api-key", &api_key)
104        .send()
105        .await
106        .map_err(|e| {
107            tracing::error!("GameBrain game detail request error: {}", e);
108            e.to_string()
109        });
110
111    let response = match response {
112        Ok(r) => r,
113        Err(err) => {
114            // Stale fallback em caso de falha de rede
115            return read_cached_json::<GameMedia>(app, "gamebrain", &media_cache_key, true)?
116                .ok_or(err);
117        }
118    };
119
120    let status = response.status();
121
122    if !status.is_success() {
123        let body = response.text().await.unwrap_or_default();
124        tracing::error!(
125            "GameBrain game detail HTTP Error => status={} body={}",
126            status,
127            body
128        );
129        return read_cached_json::<GameMedia>(app, "gamebrain", &media_cache_key, true)?
130            .ok_or_else(|| format!("Erro GameBrain game detail: {}", status));
131    }
132
133    let text = response.text().await.map_err(|e| e.to_string())?;
134
135    tracing::debug!("GameBrain game detail response size: {} bytes", text.len());
136
137    let raw: RawGameDetail = serde_json::from_str(&text).map_err(|e| {
138        tracing::error!("GameBrain game detail JSON parse error: {}", e);
139        format!("Erro JSON GameBrain game detail: {}", e)
140    })?;
141
142    let media = build_game_media(raw);
143
144    tracing::debug!(
145        "GameBrain media => {} screenshots, {} trailers, {} embeds para gamebrain_id={}",
146        media.screenshots.len(),
147        media.trailers.len(),
148        media.youtube_embeds.len(),
149        gamebrain_id
150    );
151
152    let _ = save_cached_json(app, "gamebrain", &media_cache_key, &media);
153
154    Ok(media)
155}