game_manager_lib/commands/metadata/
shared.rs1use crate::constants::NOT_FOUND_MARKER;
6use crate::models::ImportConfidence;
7use crate::services::cache;
8use crate::services::integration::{rawg, steam_api, steamspy};
9use crate::utils::text::{is_likely_non_base_game, normalize_for_matching, strip_edition_suffix};
10
11#[derive(serde::Serialize, Clone)]
15pub struct EnrichProgress {
16 pub current: i32,
17 pub total_found: i32,
18 pub last_game: String,
19 pub status: String,
20}
21
22pub struct SteamIdResolution {
24 pub app_id: String,
25 pub confidence: ImportConfidence,
26}
27
28fn rawg_cache_key(name: &str) -> String {
31 format!("search_{}", name.to_lowercase())
32}
33
34pub(in crate::commands::metadata) fn rawg_not_found_cached(
35 name: &str,
36 cache_conn: &rusqlite::Connection,
37) -> bool {
38 let cache_key = rawg_cache_key(name);
39 cache::get_cached_api_data(cache_conn, "rawg", &cache_key)
40 .is_some_and(|cached| cached == NOT_FOUND_MARKER)
41}
42
43async fn fetch_rawg_metadata_inner(
44 api_key: &str,
45 name: &str,
46 cache_conn: &rusqlite::Connection,
47 bypass_cache: bool,
48) -> Option<rawg::GameDetails> {
49 let cache_key = rawg_cache_key(name);
50
51 if !bypass_cache {
52 if let Some(cached) = cache::get_cached_api_data(cache_conn, "rawg", &cache_key) {
53 if cached == NOT_FOUND_MARKER {
54 return None;
55 }
56 if let Ok(details) = serde_json::from_str::<rawg::GameDetails>(&cached) {
57 return Some(details);
58 }
59 }
60 }
61
62 match rawg::search_games(api_key, name).await {
63 Ok(results) => {
64 if let Some(best_match) = results.first() {
65 match rawg::fetch_game_details(api_key, best_match.id.to_string()).await {
66 Ok(details) => {
67 if let Ok(json) = serde_json::to_string(&details) {
68 let _ =
69 cache::save_cached_api_data(cache_conn, "rawg", &cache_key, &json);
70 }
71 Some(details)
72 }
73 Err(err) => {
74 if err.contains("não encontrado") || err.contains("404") {
75 let _ = cache::save_cached_api_data(
76 cache_conn,
77 "rawg",
78 &cache_key,
79 NOT_FOUND_MARKER,
80 );
81 }
82 None
83 }
84 }
85 } else {
86 let _ =
87 cache::save_cached_api_data(cache_conn, "rawg", &cache_key, NOT_FOUND_MARKER);
88 None
89 }
90 }
91 Err(_) => None,
92 }
93}
94
95pub async fn fetch_rawg_metadata(
102 api_key: &str,
103 name: &str,
104 cache_conn: &rusqlite::Connection,
105) -> Option<rawg::GameDetails> {
106 fetch_rawg_metadata_inner(api_key, name, cache_conn, false).await
107}
108
109pub async fn fetch_rawg_metadata_fresh(
115 api_key: &str,
116 name: &str,
117 cache_conn: &rusqlite::Connection,
118) -> Option<rawg::GameDetails> {
119 fetch_rawg_metadata_inner(api_key, name, cache_conn, true).await
120}
121
122pub async fn resolve_steam_app_id(
123 name: &str,
124 platform: &str,
125 platform_game_id: Option<&str>,
126 cache_conn: &rusqlite::Connection,
127) -> Option<SteamIdResolution> {
128 if platform.to_lowercase() == "steam" {
129 if let Some(id) = platform_game_id {
130 return Some(SteamIdResolution {
131 app_id: id.to_string(),
132 confidence: ImportConfidence::High,
133 });
134 }
135 }
136
137 let cache_key = format!("resolve_{}", normalize_for_matching(name));
138 if cache::get_cached_api_data(cache_conn, "steam_resolve", &cache_key)
139 .is_some_and(|v| v == NOT_FOUND_MARKER)
140 {
141 return None;
142 }
143
144 let candidates = steam_api::search_app_by_name(name).await.ok()?;
145 let target = normalize_for_matching(name);
146
147 let resolution = candidates
149 .iter()
150 .find(|item| normalize_for_matching(&item.name) == target)
151 .map(|item| SteamIdResolution {
152 app_id: item.id.to_string(),
153 confidence: ImportConfidence::High,
154 })
155 .or_else(|| {
157 let stripped_target = normalize_for_matching(&strip_edition_suffix(name));
158 candidates
159 .iter()
160 .find(|item| {
161 normalize_for_matching(&strip_edition_suffix(&item.name)) == stripped_target
162 })
163 .map(|item| SteamIdResolution {
164 app_id: item.id.to_string(),
165 confidence: ImportConfidence::Medium,
166 })
167 })
168 .or_else(|| {
171 candidates
172 .iter()
173 .find(|item| !is_likely_non_base_game(&item.name))
174 .map(|item| SteamIdResolution {
175 app_id: item.id.to_string(),
176 confidence: ImportConfidence::Low,
177 })
178 });
179
180 if resolution.is_none() {
181 let _ =
182 cache::save_cached_api_data(cache_conn, "steam_resolve", &cache_key, NOT_FOUND_MARKER);
183 }
184
185 resolution
186}
187
188pub(crate) async fn fetch_steam_store_data(
192 steam_id: &str,
193 cache_conn: &rusqlite::Connection,
194) -> Option<steam_api::SteamStoreData> {
195 let cache_key = format!("store_{}", steam_id);
196
197 if let Some(cached) = cache::get_cached_api_data(cache_conn, "steam", &cache_key) {
198 if let Ok(data) = serde_json::from_str::<steam_api::SteamStoreData>(&cached) {
199 return Some(data);
200 }
201 }
202
203 match steam_api::get_app_details(steam_id).await {
204 Ok(Some(data)) => {
205 if let Ok(json) = serde_json::to_string(&data) {
206 let _ = cache::save_cached_api_data(cache_conn, "steam", &cache_key, &json);
207 }
208 Some(data)
209 }
210 _ => None,
211 }
212}
213
214pub(crate) async fn fetch_steam_reviews(
216 steam_id: &str,
217 cache_conn: &rusqlite::Connection,
218) -> Option<steam_api::SteamReviewSummary> {
219 let cache_key = format!("reviews_{}", steam_id);
220
221 if let Some(cached) = cache::get_cached_api_data(cache_conn, "steam", &cache_key) {
222 if let Ok(reviews) = serde_json::from_str::<steam_api::SteamReviewSummary>(&cached) {
223 return Some(reviews);
224 }
225 }
226
227 match steam_api::get_app_reviews(steam_id).await {
228 Ok(Some(reviews)) => {
229 if let Ok(json) = serde_json::to_string(&reviews) {
230 let _ = cache::save_cached_api_data(cache_conn, "steam", &cache_key, &json);
231 }
232 Some(reviews)
233 }
234 _ => None,
235 }
236}
237
238pub(crate) async fn fetch_steam_playtime(
240 steam_id: &str,
241 cache_conn: &rusqlite::Connection,
242) -> Option<u32> {
243 let cache_key = format!("playtime_{}", steam_id);
244
245 if let Some(cached) = cache::get_cached_api_data(cache_conn, "steam", &cache_key) {
246 if let Ok(hours) = cached.parse::<u32>() {
247 return Some(hours);
248 }
249 }
250
251 match steamspy::get_median_playtime(steam_id).await {
252 Ok(Some(hours)) => {
253 let _ =
254 cache::save_cached_api_data(cache_conn, "steam", &cache_key, &hours.to_string());
255 Some(hours)
256 }
257 _ => None,
258 }
259}