game_manager_lib/commands/recommendation/
analysis.rs1use crate::constants::MINUTES_PER_HOUR_F32;
7use crate::database::AppState;
8use crate::errors::AppError;
9use crate::models::Platform;
10use crate::services::recommendation::{
11 calculate_user_profile, export_games_csv, export_report_json, export_report_txt,
12 generate_analysis_report, parse_release_year, GameWithDetails, RecommendationConfig,
13 UserSettings,
14};
15use serde::Serialize;
16use std::collections::HashSet;
17use tauri::{AppHandle, Manager, State};
18
19type PreparedRecommendationData = (Vec<GameWithDetails>, Vec<GameWithDetails>, HashSet<String>);
21
22#[derive(Debug, Serialize)]
24pub struct AnalysisResponse {
25 pub success: bool,
26 pub json_path: Option<String>,
27 pub csv_path: Option<String>,
28 pub txt_path: Option<String>,
29 pub message: String,
30}
31
32#[tauri::command]
41pub async fn generate_recommendation_analysis(
42 app: AppHandle,
43 limit: Option<usize>,
44) -> Result<AnalysisResponse, String> {
45 tracing::info!("Gerando análise de recomendação...");
46
47 let analysis_dir = setup_analysis_directory(&app)?;
48 let (json_path, txt_path, csv_path) = create_analysis_file_paths(&analysis_dir)?;
49
50 let state: State<AppState> = app.state();
51 let (candidates_with_details, all_games_with_details, already_played_ids) =
52 fetch_and_prepare_data(&state)?;
53
54 let profile = calculate_user_profile(&all_games_with_details, &HashSet::new());
55 let (cf_scores, _) = crate::services::recommendation::cf_aggregator::build_cf_candidates(
56 &all_games_with_details,
57 );
58
59 let config = RecommendationConfig::default();
60 let user_settings = UserSettings::default();
61
62 let report = generate_analysis_report(
63 &profile,
64 &candidates_with_details,
65 &cf_scores,
66 &already_played_ids,
67 config,
68 user_settings,
69 );
70
71 let limited_report = limit_report(report, limit);
72
73 export_analysis_reports(&limited_report, &json_path, &txt_path, &csv_path)?;
74
75 log_success(&json_path, &txt_path, &csv_path);
76
77 Ok(AnalysisResponse {
78 success: true,
79 json_path: Some(json_path.to_string_lossy().to_string()),
80 txt_path: Some(txt_path.to_string_lossy().to_string()),
81 csv_path: Some(csv_path.to_string_lossy().to_string()),
82 message: format!(
83 "Análise gerada com sucesso! {} jogos analisados.",
84 limited_report.games.len()
85 ),
86 })
87}
88
89fn setup_analysis_directory(app: &AppHandle) -> Result<std::path::PathBuf, String> {
92 let analysis_dir = app
93 .path()
94 .app_data_dir()
95 .map_err(|e| format!("Erro ao obter diretório de dados: {}", e))?
96 .join("analysis");
97
98 std::fs::create_dir_all(&analysis_dir)
99 .map_err(|e| format!("Erro ao criar diretório de análise: {}", e))?;
100
101 Ok(analysis_dir)
102}
103
104fn create_analysis_file_paths(
105 analysis_dir: &std::path::Path,
106) -> Result<(std::path::PathBuf, std::path::PathBuf, std::path::PathBuf), String> {
107 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
108 let json_path = analysis_dir.join(format!("recommendation_analysis_{}.json", timestamp));
109 let txt_path = analysis_dir.join(format!("recommendation_analysis_{}.txt", timestamp));
110 let csv_path = analysis_dir.join(format!("recommendation_ranking_{}.csv", timestamp));
111
112 Ok((json_path, txt_path, csv_path))
113}
114
115fn fetch_and_prepare_data(state: &State<AppState>) -> Result<PreparedRecommendationData, String> {
116 let library_games = crate::commands::games::get_games(state.clone())
117 .map_err(|e| format!("Erro ao buscar jogos da biblioteca: {}", e))?;
118
119 tracing::info!("Total de jogos na biblioteca: {}", library_games.len());
120
121 let already_played_ids: HashSet<String> = library_games
122 .iter()
123 .filter(|g| {
124 let hours = g.playtime.unwrap_or(0) as f32 / MINUTES_PER_HOUR_F32;
125 hours > 5.0 || g.favorite
126 })
127 .map(|g| g.id.clone())
128 .collect();
129
130 let candidate_games: Vec<_> = library_games
131 .iter()
132 .filter(|g| !already_played_ids.contains(&g.id))
133 .cloned()
134 .collect();
135
136 tracing::info!("Candidatos para recomendação: {}", candidate_games.len());
137
138 let candidates_with_details = fetch_games_with_details(&candidate_games, state)
139 .map_err(|e| format!("Erro ao processar candidatos: {}", e))?;
140
141 let all_games_with_details = fetch_games_with_details(&library_games, state)
142 .map_err(|e| format!("Erro ao processar biblioteca completa: {}", e))?;
143
144 Ok((
145 candidates_with_details,
146 all_games_with_details,
147 already_played_ids,
148 ))
149}
150
151fn fetch_games_with_details(
152 _games: &[crate::models::Game],
153 state: &State<AppState>,
154) -> Result<Vec<GameWithDetails>, AppError> {
155 let conn = state.games_db.lock()?;
156
157 let mut stmt = conn.prepare(
158 "SELECT
159 g.id, g.name, g.slug, g.playtime, g.favorite, g.user_rating, g.cover_url,
160 g.platform_game_id, g.last_played, g.added_at, g.platform, g.playtime_source, g.alternative_names,
161 gd.genres, gd.steam_app_id, gd.release_date, gd.series, gd.tags
162 FROM games g
163 LEFT JOIN game_details gd ON g.id = gd.game_id
164 ORDER BY g.name ASC",
165 )?;
166
167 let games_with_details: Result<Vec<GameWithDetails>, _> = stmt
168 .query_map([], |row| {
169 let alt_names_json: Option<String> = row.get(12)?;
170 let alternative_names = alt_names_json.and_then(|s| serde_json::from_str(&s).ok());
171
172 let game = crate::models::Game {
173 id: row.get(0)?,
174 name: row.get(1)?,
175 slug: row.get(2)?,
176 playtime: row.get(3)?,
177 favorite: row.get(4)?,
178 user_rating: row.get(5)?,
179 cover_url: row.get(6)?,
180 platform_game_id: row.get(7)?,
181 last_played: row.get(8)?,
182 added_at: row.get(9)?,
183 platform: row.get::<_, String>(10)?.parse().unwrap_or(Platform::Outra),
184 alternative_names,
185 installed: false,
186 import_confidence: None,
187 install_path: None,
188 executable_path: None,
189 launch_args: None,
190 status: None,
191 playtime_source: row
192 .get::<_, Option<String>>(11)?
193 .and_then(|s| s.parse().ok()),
194 genres: None,
195 developer: None,
196 is_adult: false,
197 };
198
199 let genres_json: Option<String> = row.get(13)?;
200 let genres: Vec<String> = genres_json
201 .as_ref()
202 .map(|s| {
203 if let Ok(vec) = serde_json::from_str::<Vec<String>>(s) {
204 vec
205 } else {
206 s.split(',')
207 .map(|g| g.trim().to_string())
208 .filter(|g| !g.is_empty())
209 .collect()
210 }
211 })
212 .unwrap_or_default();
213
214 let steam_app_id_str: Option<String> = row.get(14)?;
215 let steam_app_id: Option<u32> = steam_app_id_str.and_then(|s| s.parse().ok());
216
217 let release_date: Option<String> = row.get(15)?;
218 let release_year = release_date.and_then(|d| parse_release_year(&d));
219 let series: Option<String> = row.get(16)?;
220
221 let tags_json: Option<String> = row.get(17)?;
222 let tags: Vec<crate::models::GameTag> = tags_json
223 .as_ref()
224 .and_then(|s| serde_json::from_str(s).ok())
225 .unwrap_or_default();
226
227 Ok(GameWithDetails {
228 game,
229 genres,
230 tags,
231 series,
232 release_year,
233 steam_app_id,
234 })
235 })?
236 .collect();
237
238 games_with_details.map_err(|e| e.into())
239}
240
241fn limit_report(
242 mut report: crate::services::recommendation::RecommendationAnalysisReport,
243 limit: Option<usize>,
244) -> crate::services::recommendation::RecommendationAnalysisReport {
245 if let Some(limit) = limit {
246 report.games.truncate(limit);
247 }
248 report
249}
250
251fn export_analysis_reports(
252 report: &crate::services::recommendation::RecommendationAnalysisReport,
253 json_path: &std::path::Path,
254 txt_path: &std::path::Path,
255 csv_path: &std::path::Path,
256) -> Result<(), String> {
257 export_report_json(report, json_path.to_str().unwrap())
258 .map_err(|e| format!("Erro ao salvar JSON: {}", e))?;
259
260 export_report_txt(report, txt_path.to_str().unwrap())
261 .map_err(|e| format!("Erro ao salvar TXT: {}", e))?;
262
263 export_games_csv(&report.games, csv_path.to_str().unwrap())
264 .map_err(|e| format!("Erro ao salvar CSV: {}", e))?;
265
266 Ok(())
267}
268
269fn log_success(
270 json_path: &std::path::Path,
271 txt_path: &std::path::Path,
272 csv_path: &std::path::Path,
273) {
274 tracing::info!("Análise gerada com sucesso!");
275 tracing::info!(" JSON: {:?}", json_path);
276 tracing::info!(" TXT: {:?}", txt_path);
277 tracing::info!(" CSV: {:?}", csv_path);
278}