Skip to main content

game_manager_lib/commands/recommendation/
analysis.rs

1//! Comandos para análise e debug do sistema de recomendação
2//!
3//! Este módulo fornece comandos Tauri para gerar relatórios de análise
4//! do sistema de recomendação, úteis para debug e ajuste fino dos algoritmos.
5
6use 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
19/// Type alias para dados de recomendação preparados
20type PreparedRecommendationData = (Vec<GameWithDetails>, Vec<GameWithDetails>, HashSet<String>);
21
22/// Resposta do comando de análise
23#[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/// Gera análise completa do sistema de recomendação
33///
34/// Cria três arquivos:
35/// - `recommendation_analysis_TIMESTAMP.json` - Análise completa em JSON
36/// - `recommendation_analysis_TIMESTAMP.txt` - Relatório legível em texto
37/// - `recommendation_ranking_TIMESTAMP.csv` - Ranking em CSV para Excel
38///
39/// Os arquivos são salvos em `AppData/Local/Playlite/analysis/`
40#[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
89// === FUNÇÕES AUXILIARES ===
90
91fn 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}