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