diff --git a/.gitignore b/.gitignore
index 7e2e97c..c7b6a23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -414,3 +414,9 @@ FodyWeavers.xsd
# Built Visual Studio Code Extensions
*.vsix
+
+# Secrets / credentials - never commit
+appsettings.json
+appsettings.*.json
+!appsettings.template.json
+settings.ini
diff --git a/HorseRacingPredictor/HorseRacingPredictor/App.xaml.cs b/HorseRacingPredictor/HorseRacingPredictor/App.xaml.cs
index 103c008..cc06030 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/App.xaml.cs
+++ b/HorseRacingPredictor/HorseRacingPredictor/App.xaml.cs
@@ -1,8 +1,38 @@
+using System;
using System.Windows;
+using System.Windows.Threading;
namespace HorseRacingPredictor
{
public partial class App : Application
{
+ protected override void OnStartup(StartupEventArgs e)
+ {
+ DispatcherUnhandledException += App_DispatcherUnhandledException;
+ AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
+ base.OnStartup(e);
+ }
+
+ private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
+ {
+ MessageBox.Show(
+ $"Errore non gestito:\n\n{e.Exception.GetType().Name}: {e.Exception.Message}\n\n{e.Exception.StackTrace}",
+ "Errore applicazione",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error);
+ e.Handled = true;
+ }
+
+ private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
+ {
+ if (e.ExceptionObject is Exception ex)
+ {
+ MessageBox.Show(
+ $"Errore fatale:\n\n{ex.GetType().Name}: {ex.Message}\n\n{ex.StackTrace}",
+ "Errore fatale",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error);
+ }
+ }
}
}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/BettingPredictor.csproj b/HorseRacingPredictor/HorseRacingPredictor/BettingPredictor.csproj
index 649c76e..86ee796 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/BettingPredictor.csproj
+++ b/HorseRacingPredictor/HorseRacingPredictor/BettingPredictor.csproj
@@ -27,7 +27,6 @@
-
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/Coaches.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Coaches.cs
new file mode 100644
index 0000000..d9b1462
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Coaches.cs
@@ -0,0 +1,26 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "coachs" dell'API-Football.
+ /// Restituisce informazioni sugli allenatori.
+ ///
+ internal class Coaches : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "coachs";
+
+ public RestResponse GetByTeam(int teamId)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?team={teamId}", ApiTypes.Teams);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore recupero allenatore team {teamId}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/PlayerStatistics.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/PlayerStatistics.cs
new file mode 100644
index 0000000..f84d834
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/PlayerStatistics.cs
@@ -0,0 +1,59 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "players" dell'API-Football.
+ /// Restituisce statistiche dettagliate dei giocatori per squadra/lega/stagione.
+ ///
+ internal class PlayerStatistics : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "players";
+
+ ///
+ /// Ottiene le statistiche dei giocatori per squadra e stagione (paginato, 20 per pagina).
+ ///
+ public RestResponse GetByTeamAndSeason(int teamId, int season, int page = 1)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?team={teamId}&season={season}&page={page}", ApiTypes.Teams);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore recupero statistiche giocatori team {teamId}, stagione {season}, pagina {page}: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Ottiene le statistiche di un singolo giocatore per stagione.
+ ///
+ public RestResponse GetByPlayerAndSeason(int playerId, int season)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?id={playerId}&season={season}", ApiTypes.Teams);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore recupero statistiche giocatore {playerId}, stagione {season}: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Ottiene le statistiche dei giocatori per lega e stagione (paginato, 20 per pagina).
+ ///
+ public RestResponse GetByLeagueAndSeason(int leagueId, int season, int page = 1)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?league={leagueId}&season={season}&page={page}", ApiTypes.Leagues);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore recupero statistiche giocatori lega {leagueId}, stagione {season}, pagina {page}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/Sidelined.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Sidelined.cs
new file mode 100644
index 0000000..ea0a7ab
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Sidelined.cs
@@ -0,0 +1,38 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "sidelined" dell'API-Football.
+ /// Restituisce lo storico di indisponibilità/squalifiche di un giocatore o allenatore.
+ ///
+ internal class Sidelined : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "sidelined";
+
+ public RestResponse GetByPlayer(int playerId)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?player={playerId}", ApiTypes.Teams);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore recupero sidelined giocatore {playerId}: {ex.Message}", ex);
+ }
+ }
+
+ public RestResponse GetByCoach(int coachId)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?coach={coachId}", ApiTypes.Teams);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore recupero sidelined allenatore {coachId}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/Squads.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Squads.cs
new file mode 100644
index 0000000..c43a637
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Squads.cs
@@ -0,0 +1,26 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "players/squads" dell'API-Football.
+ /// Restituisce la rosa corrente di una squadra.
+ ///
+ internal class Squads : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "players/squads";
+
+ public RestResponse GetSquad(int teamId)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?team={teamId}", ApiTypes.Teams);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore recupero rosa team {teamId}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/TeamStatistics.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/TeamStatistics.cs
new file mode 100644
index 0000000..276fe07
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/TeamStatistics.cs
@@ -0,0 +1,33 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "teams/statistics" dell'API-Football.
+ /// Restituisce statistiche aggregate di una squadra per campionato e stagione
+ /// (forma, gol, clean sheet, penalty, cartellini, formazioni usate, ecc.).
+ ///
+ internal class TeamStatistics : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "teams/statistics";
+
+ ///
+ /// Ottiene le statistiche di una squadra per lega, stagione e opzionalmente fino a una data.
+ ///
+ public RestResponse GetTeamStatistics(int teamId, int leagueId, int season, string dateTo = null)
+ {
+ try
+ {
+ string query = $"{Endpoint}?team={teamId}&league={leagueId}&season={season}";
+ if (!string.IsNullOrEmpty(dateTo))
+ query += $"&date={dateTo}";
+ return ExecuteRequest(query, ApiTypes.Teams);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore recupero statistiche team {teamId}, lega {leagueId}, stagione {season}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/TopAssists.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/TopAssists.cs
new file mode 100644
index 0000000..8d22a29
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/TopAssists.cs
@@ -0,0 +1,26 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "players/topassists" dell'API-Football.
+ /// Restituisce i 20 migliori assistman di una lega/stagione.
+ ///
+ internal class TopAssists : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "players/topassists";
+
+ public RestResponse GetTopAssists(int leagueId, int season)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?league={leagueId}&season={season}", ApiTypes.Leagues);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore recupero top assistman lega {leagueId}, stagione {season}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/TopCards.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/TopCards.cs
new file mode 100644
index 0000000..cdd8418
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/TopCards.cs
@@ -0,0 +1,39 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per gli endpoint "players/topyellowcards" e "players/topredcards" dell'API-Football.
+ /// Restituisce i 20 giocatori con più cartellini per lega/stagione.
+ ///
+ internal class TopCards : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string YellowEndpoint = "players/topyellowcards";
+ private const string RedEndpoint = "players/topredcards";
+
+ public RestResponse GetTopYellowCards(int leagueId, int season)
+ {
+ try
+ {
+ return ExecuteRequest($"{YellowEndpoint}?league={leagueId}&season={season}", ApiTypes.Leagues);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore recupero top cartellini gialli lega {leagueId}, stagione {season}: {ex.Message}", ex);
+ }
+ }
+
+ public RestResponse GetTopRedCards(int leagueId, int season)
+ {
+ try
+ {
+ return ExecuteRequest($"{RedEndpoint}?league={leagueId}&season={season}", ApiTypes.Leagues);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore recupero top cartellini rossi lega {leagueId}, stagione {season}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/TopScorers.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/TopScorers.cs
new file mode 100644
index 0000000..e80fbb6
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/TopScorers.cs
@@ -0,0 +1,26 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "players/topscorers" dell'API-Football.
+ /// Restituisce i 20 migliori marcatori di una lega/stagione.
+ ///
+ internal class TopScorers : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "players/topscorers";
+
+ public RestResponse GetTopScorers(int leagueId, int season)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?league={leagueId}&season={season}", ApiTypes.Leagues);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore recupero capocannonieri lega {leagueId}, stagione {season}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/Transfers.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Transfers.cs
new file mode 100644
index 0000000..d855f09
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Transfers.cs
@@ -0,0 +1,38 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "transfers" dell'API-Football.
+ /// Restituisce la cronologia trasferimenti di un giocatore o di una squadra.
+ ///
+ internal class Transfers : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "transfers";
+
+ public RestResponse GetByTeam(int teamId)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?team={teamId}", ApiTypes.Teams);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore recupero trasferimenti team {teamId}: {ex.Message}", ex);
+ }
+ }
+
+ public RestResponse GetByPlayer(int playerId)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?player={playerId}", ApiTypes.Teams);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore recupero trasferimenti giocatore {playerId}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/FootballDownloadOptions.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/FootballDownloadOptions.cs
index 2c0b6d4..f2f360d 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/Football/FootballDownloadOptions.cs
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/FootballDownloadOptions.cs
@@ -19,6 +19,39 @@ namespace HorseRacingPredictor.Football
public bool DownloadStatistics { get; set; } = false;
public bool DownloadInjuries { get; set; } = false;
+ // ?? Endpoint supplementari (CSV separati) ?????????????????
+ /// Scarica statistiche giocatori per squadra/stagione.
+ public bool DownloadPlayerStats { get; set; } = false;
+
+ /// Scarica statistiche aggregate delle squadre per lega/stagione.
+ public bool DownloadTeamStats { get; set; } = false;
+
+ /// Scarica classifica marcatori della lega.
+ public bool DownloadTopScorers { get; set; } = false;
+
+ /// Scarica classifica assistman della lega.
+ public bool DownloadTopAssists { get; set; } = false;
+
+ /// Scarica classifica cartellini (gialli e rossi) della lega.
+ public bool DownloadTopCards { get; set; } = false;
+
+ /// Scarica le rose attuali delle squadre.
+ public bool DownloadSquads { get; set; } = false;
+
+ /// Scarica informazioni sugli allenatori delle squadre.
+ public bool DownloadCoaches { get; set; } = false;
+
+ /// Scarica lo storico trasferimenti delle squadre.
+ public bool DownloadTransfers { get; set; } = false;
+
+ ///
+ /// Indica se almeno un endpoint supplementare (CSV separato) è selezionato.
+ ///
+ public bool AnySupplementarySelected =>
+ DownloadPlayerStats || DownloadTeamStats || DownloadTopScorers ||
+ DownloadTopAssists || DownloadTopCards || DownloadSquads ||
+ DownloadCoaches || DownloadTransfers;
+
// ?? Filtri ????????????????????????????????????????????????
///
/// Se non vuoto, scarica fixture solo per queste leghe (league IDs).
@@ -96,6 +129,19 @@ namespace HorseRacingPredictor.Football
if (DownloadStatistics) calls += fixtureCount;
if (DownloadInjuries) calls += 1;
if (CheckQuota) calls += 1; // status endpoint
+
+ // Supplementari (una chiamata per squadra coinvolta ? fixtureCount*2 unici, stima fixtureCount)
+ int teamEstimate = System.Math.Max(fixtureCount, 1);
+ int leagueEstimate = LeagueIds.Count > 0 ? LeagueIds.Count : 5;
+ if (DownloadPlayerStats) calls += teamEstimate; // 1 pagina per squadra (minimo)
+ if (DownloadTeamStats) calls += teamEstimate;
+ if (DownloadTopScorers) calls += leagueEstimate;
+ if (DownloadTopAssists) calls += leagueEstimate;
+ if (DownloadTopCards) calls += leagueEstimate * 2; // gialli + rossi
+ if (DownloadSquads) calls += teamEstimate;
+ if (DownloadCoaches) calls += teamEstimate;
+ if (DownloadTransfers) calls += teamEstimate;
+
return calls;
}
}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/Main.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/Main.cs
index b9a8d23..aa4eab6 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/Football/Main.cs
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/Main.cs
@@ -1878,5 +1878,40 @@ namespace HorseRacingPredictor.Football
}
#endregion
+
+ #region Supplementary Data
+
+ ///
+ /// Scarica i dati supplementari selezionati (statistiche giocatori, capocannonieri, ecc.)
+ /// e li esporta come CSV separati nella cartella specificata.
+ /// Richiede un DataTable di fixture già scaricato per estrarre teamId e leagueId coinvolti.
+ ///
+ public List DownloadSupplementaryData(
+ DataTable fixturesTable,
+ DateTime date,
+ string exportFolder,
+ FootballDownloadOptions options,
+ IProgress progressCallback = null,
+ IProgress statusCallback = null)
+ {
+ if (fixturesTable == null || fixturesTable.Rows.Count == 0 || !options.AnySupplementarySelected)
+ return new List();
+
+ // Estrai team IDs e league IDs unici dal DataTable fixture
+ var teamIds = new HashSet();
+ var leagueIds = new HashSet();
+
+ foreach (DataRow row in fixturesTable.Rows)
+ {
+ if (row["HomeTeamId"] != DBNull.Value) teamIds.Add(Convert.ToInt32(row["HomeTeamId"]));
+ if (row["AwayTeamId"] != DBNull.Value) teamIds.Add(Convert.ToInt32(row["AwayTeamId"]));
+ if (row["LeagueId"] != DBNull.Value) leagueIds.Add(Convert.ToInt32(row["LeagueId"]));
+ }
+
+ var exporter = new SupplementaryDataExporter();
+ return exporter.DownloadAndExport(date, exportFolder, options, teamIds, leagueIds, progressCallback, statusCallback);
+ }
+
+ #endregion
}
}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/SupplementaryDataExporter.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/SupplementaryDataExporter.cs
new file mode 100644
index 0000000..58ad2c2
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/SupplementaryDataExporter.cs
@@ -0,0 +1,733 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+
+namespace HorseRacingPredictor.Football
+{
+ ///
+ /// Scarica dati supplementari dall'API-Football e li esporta come CSV separati.
+ /// Ogni tipo di dato produce un file CSV indipendente che l'IA potrà poi unire
+ /// in fase di elaborazione usando le chiavi comuni (team_id, league_id, player_id).
+ ///
+ public class SupplementaryDataExporter
+ {
+ private readonly API.PlayerStatistics _playerStatsAPI = new();
+ private readonly API.TeamStatistics _teamStatsAPI = new();
+ private readonly API.TopScorers _topScorersAPI = new();
+ private readonly API.TopAssists _topAssistsAPI = new();
+ private readonly API.TopCards _topCardsAPI = new();
+ private readonly API.Squads _squadsAPI = new();
+ private readonly API.Coaches _coachesAPI = new();
+ private readonly API.Transfers _transfersAPI = new();
+ private readonly API.Status _statusAPI = new();
+ private readonly Manager.Database _database = new();
+
+ ///
+ /// Scarica tutti i dati supplementari selezionati e li salva come CSV nella cartella specificata.
+ /// Restituisce la lista di file CSV generati.
+ ///
+ public List DownloadAndExport(
+ DateTime date,
+ string exportFolder,
+ FootballDownloadOptions options,
+ HashSet teamIds,
+ HashSet leagueIds,
+ IProgress progressCallback = null,
+ IProgress statusCallback = null)
+ {
+ var generatedFiles = new List();
+ int season = options.GetEffectiveSeason();
+ string dateTag = date.ToString("yyyy-MM-dd");
+
+ // Calcola step totali per progress
+ int totalSteps = CountSteps(options, teamIds.Count, leagueIds.Count);
+ int currentStep = 0;
+
+ void Report(string msg)
+ {
+ currentStep++;
+ int pct = totalSteps > 0 ? (int)((double)currentStep / totalSteps * 100) : 0;
+ progressCallback?.Report(Math.Min(pct, 100));
+ statusCallback?.Report(msg);
+ }
+
+ // ?? Top Scorers ??
+ if (options.DownloadTopScorers)
+ {
+ Report("Scaricamento capocannonieri...");
+ var table = DownloadTopScorersTable(leagueIds, season, options.ApiDelayMs);
+ if (table.Rows.Count > 0)
+ {
+ string path = SaveCsv(table, exportFolder, $"TopScorers_{dateTag}.csv");
+ generatedFiles.Add(path);
+ }
+ }
+
+ // ?? Top Assists ??
+ if (options.DownloadTopAssists)
+ {
+ Report("Scaricamento top assistman...");
+ var table = DownloadTopAssistsTable(leagueIds, season, options.ApiDelayMs);
+ if (table.Rows.Count > 0)
+ {
+ string path = SaveCsv(table, exportFolder, $"TopAssists_{dateTag}.csv");
+ generatedFiles.Add(path);
+ }
+ }
+
+ // ?? Top Cards ??
+ if (options.DownloadTopCards)
+ {
+ Report("Scaricamento top cartellini...");
+ var table = DownloadTopCardsTable(leagueIds, season, options.ApiDelayMs);
+ if (table.Rows.Count > 0)
+ {
+ string path = SaveCsv(table, exportFolder, $"TopCards_{dateTag}.csv");
+ generatedFiles.Add(path);
+ }
+ }
+
+ // ?? Team Stats ??
+ if (options.DownloadTeamStats)
+ {
+ var table = new DataTable();
+ table.Columns.Add("TeamId", typeof(int));
+ table.Columns.Add("LeagueId", typeof(int));
+ table.Columns.Add("Season", typeof(int));
+ table.Columns.Add("Forma", typeof(string));
+ table.Columns.Add("Partite Totali", typeof(int));
+ table.Columns.Add("Vittorie Casa", typeof(int));
+ table.Columns.Add("Vittorie Trasf.", typeof(int));
+ table.Columns.Add("Pareggi Casa", typeof(int));
+ table.Columns.Add("Pareggi Trasf.", typeof(int));
+ table.Columns.Add("Sconfitte Casa", typeof(int));
+ table.Columns.Add("Sconfitte Trasf.", typeof(int));
+ table.Columns.Add("Gol Fatti Casa", typeof(int));
+ table.Columns.Add("Gol Fatti Trasf.", typeof(int));
+ table.Columns.Add("Gol Subiti Casa", typeof(int));
+ table.Columns.Add("Gol Subiti Trasf.", typeof(int));
+ table.Columns.Add("Clean Sheet Tot.", typeof(int));
+ table.Columns.Add("Penalty Segnati", typeof(int));
+ table.Columns.Add("Penalty Falliti", typeof(int));
+
+ foreach (int teamId in teamIds)
+ {
+ foreach (int leagueId in leagueIds)
+ {
+ Report($"Statistiche team {teamId} in lega {leagueId}...");
+ try
+ {
+ var resp = _teamStatsAPI.GetTeamStatistics(teamId, leagueId, season, dateTag);
+ if (resp?.IsSuccessful == true && !string.IsNullOrEmpty(resp.Content))
+ ParseTeamStatsRow(table, resp.Content, teamId, leagueId, season);
+ }
+ catch { /* continua */ }
+ if (options.ApiDelayMs > 0) System.Threading.Thread.Sleep(options.ApiDelayMs);
+ }
+ }
+ if (table.Rows.Count > 0)
+ {
+ string path = SaveCsv(table, exportFolder, $"TeamStats_{dateTag}.csv");
+ generatedFiles.Add(path);
+ }
+ }
+
+ // ?? Player Stats ??
+ if (options.DownloadPlayerStats)
+ {
+ Report("Scaricamento statistiche giocatori...");
+ var table = DownloadPlayerStatsTable(teamIds, season, options.ApiDelayMs, statusCallback);
+ if (table.Rows.Count > 0)
+ {
+ string path = SaveCsv(table, exportFolder, $"PlayerStats_{dateTag}.csv");
+ generatedFiles.Add(path);
+ }
+ }
+
+ // ?? Squads ??
+ if (options.DownloadSquads)
+ {
+ Report("Scaricamento rose squadre...");
+ var table = DownloadSquadsTable(teamIds, options.ApiDelayMs, statusCallback);
+ if (table.Rows.Count > 0)
+ {
+ string path = SaveCsv(table, exportFolder, $"Squads_{dateTag}.csv");
+ generatedFiles.Add(path);
+ }
+ }
+
+ // ?? Coaches ??
+ if (options.DownloadCoaches)
+ {
+ Report("Scaricamento allenatori...");
+ var table = DownloadCoachesTable(teamIds, options.ApiDelayMs, statusCallback);
+ if (table.Rows.Count > 0)
+ {
+ string path = SaveCsv(table, exportFolder, $"Coaches_{dateTag}.csv");
+ generatedFiles.Add(path);
+ }
+ }
+
+ // ?? Transfers ??
+ if (options.DownloadTransfers)
+ {
+ Report("Scaricamento trasferimenti...");
+ var table = DownloadTransfersTable(teamIds, options.ApiDelayMs, statusCallback);
+ if (table.Rows.Count > 0)
+ {
+ string path = SaveCsv(table, exportFolder, $"Transfers_{dateTag}.csv");
+ generatedFiles.Add(path);
+ }
+ }
+
+ progressCallback?.Report(100);
+ statusCallback?.Report($"Generati {generatedFiles.Count} CSV supplementari");
+ return generatedFiles;
+ }
+
+ #region Download & Parse Methods
+
+ private DataTable DownloadTopScorersTable(HashSet leagueIds, int season, int delayMs)
+ {
+ var table = CreateTopPlayersTable("Gol");
+ foreach (int leagueId in leagueIds)
+ {
+ try
+ {
+ var resp = _topScorersAPI.GetTopScorers(leagueId, season);
+ if (resp?.IsSuccessful == true && !string.IsNullOrEmpty(resp.Content))
+ ParseTopPlayersResponse(table, resp.Content, leagueId, season, "goals", "total");
+ }
+ catch { }
+ if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
+ }
+ return table;
+ }
+
+ private DataTable DownloadTopAssistsTable(HashSet leagueIds, int season, int delayMs)
+ {
+ var table = CreateTopPlayersTable("Assist");
+ foreach (int leagueId in leagueIds)
+ {
+ try
+ {
+ var resp = _topAssistsAPI.GetTopAssists(leagueId, season);
+ if (resp?.IsSuccessful == true && !string.IsNullOrEmpty(resp.Content))
+ ParseTopPlayersResponse(table, resp.Content, leagueId, season, "goals", "assists");
+ }
+ catch { }
+ if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
+ }
+ return table;
+ }
+
+ private DataTable DownloadTopCardsTable(HashSet leagueIds, int season, int delayMs)
+ {
+ var table = new DataTable();
+ table.Columns.Add("LeagueId", typeof(int));
+ table.Columns.Add("Season", typeof(int));
+ table.Columns.Add("Posizione", typeof(int));
+ table.Columns.Add("PlayerId", typeof(int));
+ table.Columns.Add("Giocatore", typeof(string));
+ table.Columns.Add("TeamId", typeof(int));
+ table.Columns.Add("Squadra", typeof(string));
+ table.Columns.Add("Tipo", typeof(string));
+ table.Columns.Add("Totale", typeof(int));
+
+ foreach (int leagueId in leagueIds)
+ {
+ try
+ {
+ var respY = _topCardsAPI.GetTopYellowCards(leagueId, season);
+ if (respY?.IsSuccessful == true && !string.IsNullOrEmpty(respY.Content))
+ ParseTopCardsResponse(table, respY.Content, leagueId, season, "Yellow");
+ }
+ catch { }
+ if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
+
+ try
+ {
+ var respR = _topCardsAPI.GetTopRedCards(leagueId, season);
+ if (respR?.IsSuccessful == true && !string.IsNullOrEmpty(respR.Content))
+ ParseTopCardsResponse(table, respR.Content, leagueId, season, "Red");
+ }
+ catch { }
+ if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
+ }
+ return table;
+ }
+
+ private DataTable DownloadPlayerStatsTable(HashSet teamIds, int season, int delayMs, IProgress statusCallback)
+ {
+ var table = new DataTable();
+ table.Columns.Add("PlayerId", typeof(int));
+ table.Columns.Add("Giocatore", typeof(string));
+ table.Columns.Add("Età", typeof(int));
+ table.Columns.Add("Nazionalità", typeof(string));
+ table.Columns.Add("TeamId", typeof(int));
+ table.Columns.Add("Squadra", typeof(string));
+ table.Columns.Add("LeagueId", typeof(int));
+ table.Columns.Add("Campionato", typeof(string));
+ table.Columns.Add("Presenze", typeof(int));
+ table.Columns.Add("Minuti", typeof(int));
+ table.Columns.Add("Rating", typeof(string));
+ table.Columns.Add("Gol", typeof(int));
+ table.Columns.Add("Assist", typeof(int));
+ table.Columns.Add("Gialli", typeof(int));
+ table.Columns.Add("Rossi", typeof(int));
+ table.Columns.Add("Tiri Totali", typeof(int));
+ table.Columns.Add("Tiri in Porta", typeof(int));
+ table.Columns.Add("Passaggi Chiave", typeof(int));
+ table.Columns.Add("Dribbling Riusciti", typeof(int));
+ table.Columns.Add("Falli Commessi", typeof(int));
+ table.Columns.Add("Falli Subiti", typeof(int));
+ table.Columns.Add("Ruolo", typeof(string));
+
+ int count = 0;
+ foreach (int teamId in teamIds)
+ {
+ count++;
+ statusCallback?.Report($"Statistiche giocatori team {count}/{teamIds.Count}...");
+ try
+ {
+ var resp = _playerStatsAPI.GetByTeamAndSeason(teamId, season);
+ if (resp?.IsSuccessful == true && !string.IsNullOrEmpty(resp.Content))
+ ParsePlayerStatsResponse(table, resp.Content);
+ }
+ catch { }
+ if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
+ }
+ return table;
+ }
+
+ private DataTable DownloadSquadsTable(HashSet teamIds, int delayMs, IProgress statusCallback)
+ {
+ var table = new DataTable();
+ table.Columns.Add("TeamId", typeof(int));
+ table.Columns.Add("Squadra", typeof(string));
+ table.Columns.Add("PlayerId", typeof(int));
+ table.Columns.Add("Giocatore", typeof(string));
+ table.Columns.Add("Età", typeof(int));
+ table.Columns.Add("Numero", typeof(int));
+ table.Columns.Add("Ruolo", typeof(string));
+
+ int count = 0;
+ foreach (int teamId in teamIds)
+ {
+ count++;
+ statusCallback?.Report($"Rosa team {count}/{teamIds.Count}...");
+ try
+ {
+ var resp = _squadsAPI.GetSquad(teamId);
+ if (resp?.IsSuccessful == true && !string.IsNullOrEmpty(resp.Content))
+ ParseSquadsResponse(table, resp.Content);
+ }
+ catch { }
+ if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
+ }
+ return table;
+ }
+
+ private DataTable DownloadCoachesTable(HashSet teamIds, int delayMs, IProgress statusCallback)
+ {
+ var table = new DataTable();
+ table.Columns.Add("CoachId", typeof(int));
+ table.Columns.Add("Nome", typeof(string));
+ table.Columns.Add("Età", typeof(int));
+ table.Columns.Add("Nazionalità", typeof(string));
+ table.Columns.Add("TeamId", typeof(int));
+ table.Columns.Add("Squadra", typeof(string));
+
+ int count = 0;
+ foreach (int teamId in teamIds)
+ {
+ count++;
+ statusCallback?.Report($"Allenatore team {count}/{teamIds.Count}...");
+ try
+ {
+ var resp = _coachesAPI.GetByTeam(teamId);
+ if (resp?.IsSuccessful == true && !string.IsNullOrEmpty(resp.Content))
+ ParseCoachesResponse(table, resp.Content, teamId);
+ }
+ catch { }
+ if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
+ }
+ return table;
+ }
+
+ private DataTable DownloadTransfersTable(HashSet teamIds, int delayMs, IProgress statusCallback)
+ {
+ var table = new DataTable();
+ table.Columns.Add("PlayerId", typeof(int));
+ table.Columns.Add("Giocatore", typeof(string));
+ table.Columns.Add("Data", typeof(string));
+ table.Columns.Add("Tipo", typeof(string));
+ table.Columns.Add("Da TeamId", typeof(int));
+ table.Columns.Add("Da Squadra", typeof(string));
+ table.Columns.Add("A TeamId", typeof(int));
+ table.Columns.Add("A Squadra", typeof(string));
+
+ int count = 0;
+ foreach (int teamId in teamIds)
+ {
+ count++;
+ statusCallback?.Report($"Trasferimenti team {count}/{teamIds.Count}...");
+ try
+ {
+ var resp = _transfersAPI.GetByTeam(teamId);
+ if (resp?.IsSuccessful == true && !string.IsNullOrEmpty(resp.Content))
+ ParseTransfersResponse(table, resp.Content);
+ }
+ catch { }
+ if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
+ }
+ return table;
+ }
+
+ #endregion
+
+ #region JSON Parsing Helpers
+
+ private static DataTable CreateTopPlayersTable(string statName)
+ {
+ var table = new DataTable();
+ table.Columns.Add("LeagueId", typeof(int));
+ table.Columns.Add("Season", typeof(int));
+ table.Columns.Add("Posizione", typeof(int));
+ table.Columns.Add("PlayerId", typeof(int));
+ table.Columns.Add("Giocatore", typeof(string));
+ table.Columns.Add("TeamId", typeof(int));
+ table.Columns.Add("Squadra", typeof(string));
+ table.Columns.Add(statName, typeof(int));
+ table.Columns.Add("Presenze", typeof(int));
+ table.Columns.Add("Rating", typeof(string));
+ return table;
+ }
+
+ private static void ParseTopPlayersResponse(DataTable table, string json, int leagueId, int season, string statSection, string statField)
+ {
+ var doc = JsonDocument.Parse(json).RootElement;
+ if (!doc.TryGetProperty("response", out var resp)) return;
+ int pos = 0;
+ foreach (var item in resp.EnumerateArray())
+ {
+ pos++;
+ var row = table.NewRow();
+ row["LeagueId"] = leagueId;
+ row["Season"] = season;
+ row["Posizione"] = pos;
+ row["PlayerId"] = GetInt(item, "player", "id");
+ row["Giocatore"] = GetStr(item, "player", "name");
+
+ // statistics[0]
+ if (item.TryGetProperty("statistics", out var stats))
+ {
+ foreach (var s in stats.EnumerateArray())
+ {
+ row["TeamId"] = GetInt(s, "team", "id");
+ row["Squadra"] = GetStr(s, "team", "name");
+ row[table.Columns[7].ColumnName] = GetIntNested(s, statSection, statField);
+ row["Presenze"] = GetIntNested(s, "games", "appearences");
+ row["Rating"] = GetStrNested(s, "games", "rating");
+ break;
+ }
+ }
+ table.Rows.Add(row);
+ }
+ }
+
+ private static void ParseTopCardsResponse(DataTable table, string json, int leagueId, int season, string cardType)
+ {
+ var doc = JsonDocument.Parse(json).RootElement;
+ if (!doc.TryGetProperty("response", out var resp)) return;
+ int pos = 0;
+ foreach (var item in resp.EnumerateArray())
+ {
+ pos++;
+ var row = table.NewRow();
+ row["LeagueId"] = leagueId;
+ row["Season"] = season;
+ row["Posizione"] = pos;
+ row["PlayerId"] = GetInt(item, "player", "id");
+ row["Giocatore"] = GetStr(item, "player", "name");
+ row["Tipo"] = cardType;
+
+ if (item.TryGetProperty("statistics", out var stats))
+ {
+ foreach (var s in stats.EnumerateArray())
+ {
+ row["TeamId"] = GetInt(s, "team", "id");
+ row["Squadra"] = GetStr(s, "team", "name");
+ string section = cardType == "Yellow" ? "yellow" : "red";
+ row["Totale"] = GetIntNested(s, "cards", section);
+ break;
+ }
+ }
+ table.Rows.Add(row);
+ }
+ }
+
+ private static void ParseTeamStatsRow(DataTable table, string json, int teamId, int leagueId, int season)
+ {
+ var doc = JsonDocument.Parse(json).RootElement;
+ if (!doc.TryGetProperty("response", out var resp)) return;
+
+ var row = table.NewRow();
+ row["TeamId"] = teamId;
+ row["LeagueId"] = leagueId;
+ row["Season"] = season;
+ row["Forma"] = resp.TryGetProperty("form", out var f) ? f.GetString() ?? "" : "";
+ row["Partite Totali"] = GetIntNested(resp, "fixtures", "played", "total");
+ row["Vittorie Casa"] = GetIntNested(resp, "fixtures", "wins", "home");
+ row["Vittorie Trasf."] = GetIntNested(resp, "fixtures", "wins", "away");
+ row["Pareggi Casa"] = GetIntNested(resp, "fixtures", "draws", "home");
+ row["Pareggi Trasf."] = GetIntNested(resp, "fixtures", "draws", "away");
+ row["Sconfitte Casa"] = GetIntNested(resp, "fixtures", "loses", "home");
+ row["Sconfitte Trasf."] = GetIntNested(resp, "fixtures", "loses", "away");
+ row["Gol Fatti Casa"] = GetIntNested(resp, "goals", "for", "total", "home");
+ row["Gol Fatti Trasf."] = GetIntNested(resp, "goals", "for", "total", "away");
+ row["Gol Subiti Casa"] = GetIntNested(resp, "goals", "against", "total", "home");
+ row["Gol Subiti Trasf."] = GetIntNested(resp, "goals", "against", "total", "away");
+ row["Clean Sheet Tot."] = GetIntNested(resp, "clean_sheet", "total");
+ row["Penalty Segnati"] = GetIntNested(resp, "penalty", "scored", "total");
+ row["Penalty Falliti"] = GetIntNested(resp, "penalty", "missed", "total");
+ table.Rows.Add(row);
+ }
+
+ private static void ParsePlayerStatsResponse(DataTable table, string json)
+ {
+ var doc = JsonDocument.Parse(json).RootElement;
+ if (!doc.TryGetProperty("response", out var resp)) return;
+
+ foreach (var item in resp.EnumerateArray())
+ {
+ int playerId = GetInt(item, "player", "id");
+ string name = GetStr(item, "player", "name");
+ int age = GetInt(item, "player", "age");
+ string nationality = GetStr(item, "player", "nationality");
+
+ if (!item.TryGetProperty("statistics", out var stats)) continue;
+ foreach (var s in stats.EnumerateArray())
+ {
+ var row = table.NewRow();
+ row["PlayerId"] = playerId;
+ row["Giocatore"] = name;
+ row["Età"] = age;
+ row["Nazionalità"] = nationality;
+ row["TeamId"] = GetInt(s, "team", "id");
+ row["Squadra"] = GetStr(s, "team", "name");
+ row["LeagueId"] = GetInt(s, "league", "id");
+ row["Campionato"] = GetStr(s, "league", "name");
+ row["Presenze"] = GetIntNested(s, "games", "appearences");
+ row["Minuti"] = GetIntNested(s, "games", "minutes");
+ row["Rating"] = GetStrNested(s, "games", "rating");
+ row["Gol"] = GetIntNested(s, "goals", "total");
+ row["Assist"] = GetIntNested(s, "goals", "assists");
+ row["Gialli"] = GetIntNested(s, "cards", "yellow");
+ row["Rossi"] = GetIntNested(s, "cards", "red");
+ row["Tiri Totali"] = GetIntNested(s, "shots", "total");
+ row["Tiri in Porta"] = GetIntNested(s, "shots", "on");
+ row["Passaggi Chiave"] = GetIntNested(s, "passes", "key");
+ row["Dribbling Riusciti"] = GetIntNested(s, "dribbles", "success");
+ row["Falli Commessi"] = GetIntNested(s, "fouls", "committed");
+ row["Falli Subiti"] = GetIntNested(s, "fouls", "drawn");
+ row["Ruolo"] = GetStrNested(s, "games", "position");
+ table.Rows.Add(row);
+ }
+ }
+ }
+
+ private static void ParseSquadsResponse(DataTable table, string json)
+ {
+ var doc = JsonDocument.Parse(json).RootElement;
+ if (!doc.TryGetProperty("response", out var resp)) return;
+
+ foreach (var teamEntry in resp.EnumerateArray())
+ {
+ int teamId = GetInt(teamEntry, "team", "id");
+ string teamName = GetStr(teamEntry, "team", "name");
+
+ if (!teamEntry.TryGetProperty("players", out var players)) continue;
+ foreach (var p in players.EnumerateArray())
+ {
+ var row = table.NewRow();
+ row["TeamId"] = teamId;
+ row["Squadra"] = teamName;
+ row["PlayerId"] = p.TryGetProperty("id", out var id) && id.ValueKind == JsonValueKind.Number ? id.GetInt32() : 0;
+ row["Giocatore"] = p.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "";
+ row["Età"] = p.TryGetProperty("age", out var a) && a.ValueKind == JsonValueKind.Number ? a.GetInt32() : 0;
+ row["Numero"] = p.TryGetProperty("number", out var num) && num.ValueKind == JsonValueKind.Number ? num.GetInt32() : 0;
+ row["Ruolo"] = p.TryGetProperty("position", out var pos) ? pos.GetString() ?? "" : "";
+ table.Rows.Add(row);
+ }
+ }
+ }
+
+ private static void ParseCoachesResponse(DataTable table, string json, int teamId)
+ {
+ var doc = JsonDocument.Parse(json).RootElement;
+ if (!doc.TryGetProperty("response", out var resp)) return;
+
+ foreach (var c in resp.EnumerateArray())
+ {
+ var row = table.NewRow();
+ row["CoachId"] = c.TryGetProperty("id", out var id) && id.ValueKind == JsonValueKind.Number ? id.GetInt32() : 0;
+ row["Nome"] = c.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "";
+ row["Età"] = c.TryGetProperty("age", out var a) && a.ValueKind == JsonValueKind.Number ? a.GetInt32() : 0;
+ row["Nazionalità"] = c.TryGetProperty("nationality", out var nat) ? nat.GetString() ?? "" : "";
+ row["TeamId"] = teamId;
+ row["Squadra"] = c.TryGetProperty("team", out var t) && t.TryGetProperty("name", out var tn) ? tn.GetString() ?? "" : "";
+ table.Rows.Add(row);
+ }
+ }
+
+ private static void ParseTransfersResponse(DataTable table, string json)
+ {
+ var doc = JsonDocument.Parse(json).RootElement;
+ if (!doc.TryGetProperty("response", out var resp)) return;
+
+ foreach (var entry in resp.EnumerateArray())
+ {
+ int playerId = GetInt(entry, "player", "id");
+ string playerName = GetStr(entry, "player", "name");
+
+ if (!entry.TryGetProperty("transfers", out var transfers)) continue;
+ foreach (var t in transfers.EnumerateArray())
+ {
+ var row = table.NewRow();
+ row["PlayerId"] = playerId;
+ row["Giocatore"] = playerName;
+ row["Data"] = t.TryGetProperty("date", out var d) ? d.GetString() ?? "" : "";
+ row["Tipo"] = t.TryGetProperty("type", out var tp) ? tp.GetString() ?? "" : "";
+ row["Da TeamId"] = GetInt(t, "teams", "out", "id");
+ row["Da Squadra"] = GetStr(t, "teams", "out", "name");
+ row["A TeamId"] = GetInt(t, "teams", "in", "id");
+ row["A Squadra"] = GetStr(t, "teams", "in", "name");
+ table.Rows.Add(row);
+ }
+ }
+ }
+
+ #endregion
+
+ #region JSON Utility Helpers
+
+ private static int GetInt(JsonElement el, string prop1, string prop2)
+ {
+ if (el.TryGetProperty(prop1, out var p1) && p1.TryGetProperty(prop2, out var p2) && p2.ValueKind == JsonValueKind.Number)
+ return p2.GetInt32();
+ return 0;
+ }
+
+ private static int GetInt(JsonElement el, string prop1, string prop2, string prop3)
+ {
+ if (el.TryGetProperty(prop1, out var p1) && p1.TryGetProperty(prop2, out var p2) && p2.TryGetProperty(prop3, out var p3) && p3.ValueKind == JsonValueKind.Number)
+ return p3.GetInt32();
+ return 0;
+ }
+
+ private static string GetStr(JsonElement el, string prop1, string prop2)
+ {
+ if (el.TryGetProperty(prop1, out var p1) && p1.TryGetProperty(prop2, out var p2))
+ return p2.GetString() ?? "";
+ return "";
+ }
+
+ private static string GetStr(JsonElement el, string prop1, string prop2, string prop3)
+ {
+ if (el.TryGetProperty(prop1, out var p1) && p1.TryGetProperty(prop2, out var p2) && p2.TryGetProperty(prop3, out var p3))
+ return p3.GetString() ?? "";
+ return "";
+ }
+
+ private static int GetIntNested(JsonElement el, string p1, string p2)
+ {
+ if (el.TryGetProperty(p1, out var v1) && v1.TryGetProperty(p2, out var v2) && v2.ValueKind == JsonValueKind.Number)
+ return v2.GetInt32();
+ return 0;
+ }
+
+ private static int GetIntNested(JsonElement el, string p1, string p2, string p3)
+ {
+ if (el.TryGetProperty(p1, out var v1) && v1.TryGetProperty(p2, out var v2) && v2.TryGetProperty(p3, out var v3) && v3.ValueKind == JsonValueKind.Number)
+ return v3.GetInt32();
+ return 0;
+ }
+
+ private static int GetIntNested(JsonElement el, string p1, string p2, string p3, string p4)
+ {
+ if (el.TryGetProperty(p1, out var v1) && v1.TryGetProperty(p2, out var v2) && v2.TryGetProperty(p3, out var v3) && v3.TryGetProperty(p4, out var v4) && v4.ValueKind == JsonValueKind.Number)
+ return v4.GetInt32();
+ return 0;
+ }
+
+ private static string GetStrNested(JsonElement el, string p1, string p2)
+ {
+ if (el.TryGetProperty(p1, out var v1) && v1.TryGetProperty(p2, out var v2))
+ return v2.GetString() ?? "";
+ return "";
+ }
+
+ #endregion
+
+ #region CSV & Utility
+
+ private static string SaveCsv(DataTable table, string folder, string fileName)
+ {
+ if (!Directory.Exists(folder))
+ Directory.CreateDirectory(folder);
+
+ string path = Path.Combine(folder, fileName);
+ var sb = new StringBuilder();
+
+ // Header
+ for (int i = 0; i < table.Columns.Count; i++)
+ {
+ if (i > 0) sb.Append(',');
+ sb.Append(EscapeCsvField(table.Columns[i].ColumnName));
+ }
+ sb.AppendLine();
+
+ // Rows
+ foreach (DataRow row in table.Rows)
+ {
+ for (int i = 0; i < table.Columns.Count; i++)
+ {
+ if (i > 0) sb.Append(',');
+ sb.Append(EscapeCsvField(row[i]?.ToString() ?? ""));
+ }
+ sb.AppendLine();
+ }
+
+ File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
+ return path;
+ }
+
+ private static string EscapeCsvField(string field)
+ {
+ if (field.Contains(',') || field.Contains('"') || field.Contains('\n'))
+ return "\"" + field.Replace("\"", "\"\"") + "\"";
+ return field;
+ }
+
+ private static int CountSteps(FootballDownloadOptions options, int teamCount, int leagueCount)
+ {
+ int steps = 0;
+ if (options.DownloadTopScorers) steps += leagueCount;
+ if (options.DownloadTopAssists) steps += leagueCount;
+ if (options.DownloadTopCards) steps += leagueCount * 2;
+ if (options.DownloadTeamStats) steps += teamCount * leagueCount;
+ if (options.DownloadPlayerStats) steps += teamCount;
+ if (options.DownloadSquads) steps += teamCount;
+ if (options.DownloadCoaches) steps += teamCount;
+ if (options.DownloadTransfers) steps += teamCount;
+ return Math.Max(steps, 1);
+ }
+
+ #endregion
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/HorseRacingPredictor/UserSettings.cs b/HorseRacingPredictor/HorseRacingPredictor/HorseRacingPredictor/UserSettings.cs
index 5b81a69..eb822de 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/HorseRacingPredictor/UserSettings.cs
+++ b/HorseRacingPredictor/HorseRacingPredictor/HorseRacingPredictor/UserSettings.cs
@@ -23,6 +23,7 @@ namespace HorseRacingPredictor
// ?? Football ??????????????????????????????????????????
public string ApiKey { get; set; } = string.Empty;
+ public string FbDataSource { get; set; } = "API - API-Football";
public string FbExportPath { get; set; } = string.Empty;
public string FbPrefix { get; set; } = string.Empty;
public string FbSuffix { get; set; } = string.Empty;
@@ -50,8 +51,19 @@ namespace HorseRacingPredictor
public bool FbCheckQuota { get; set; } = true;
public int FbMinRemainingQuota { get; set; } = 10;
+ // ?? Football Supplementary Downloads (CSV separati) ??????
+ public bool FbDownloadPlayerStats { get; set; } = false;
+ public bool FbDownloadTeamStats { get; set; } = false;
+ public bool FbDownloadTopScorers { get; set; } = false;
+ public bool FbDownloadTopAssists { get; set; } = false;
+ public bool FbDownloadTopCards { get; set; } = false;
+ public bool FbDownloadSquads { get; set; } = false;
+ public bool FbDownloadCoaches { get; set; } = false;
+ public bool FbDownloadTransfers { get; set; } = false;
+
// ?? Racing ???????????????????????????????????????????????
public string RacingApiKey { get; set; } = string.Empty;
+ public string RcDataSource { get; set; } = "API - FormFav";
public string RcExportPath { get; set; } = string.Empty;
public string RcPrefix { get; set; } = string.Empty;
public string RcSuffix { get; set; } = string.Empty;
@@ -87,7 +99,16 @@ namespace HorseRacingPredictor
MaxFixturesForDetails = FbMaxFixturesForDetails,
ApiDelayMs = FbApiDelayMs,
CheckQuota = FbCheckQuota,
- MinRemainingQuota = FbMinRemainingQuota
+ MinRemainingQuota = FbMinRemainingQuota,
+ // Supplementari
+ DownloadPlayerStats = FbDownloadPlayerStats,
+ DownloadTeamStats = FbDownloadTeamStats,
+ DownloadTopScorers = FbDownloadTopScorers,
+ DownloadTopAssists = FbDownloadTopAssists,
+ DownloadTopCards = FbDownloadTopCards,
+ DownloadSquads = FbDownloadSquads,
+ DownloadCoaches = FbDownloadCoaches,
+ DownloadTransfers = FbDownloadTransfers
};
}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/HorseRacingPredictor/appsettings.template.json b/HorseRacingPredictor/HorseRacingPredictor/HorseRacingPredictor/appsettings.template.json
new file mode 100644
index 0000000..232c3e3
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/HorseRacingPredictor/appsettings.template.json
@@ -0,0 +1,11 @@
+{
+ "ConnectionStrings": {
+ "Football": "Server=YOUR_SERVER;Database=YOUR_DB;User Id=YOUR_USER;Password=YOUR_PASSWORD;TrustServerCertificate=True",
+ "Horses": "Server=YOUR_SERVER;Database=YOUR_DB;User Id=YOUR_USER;Password=YOUR_PASSWORD;TrustServerCertificate=True"
+ },
+ "Api": {
+ "FootballApiKey": "",
+ "FootballApiKeyHeader": "x-rapidapi-key",
+ "FootballApiHost": "v3.football.api-sports.io"
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Main.cs b/HorseRacingPredictor/HorseRacingPredictor/Main.cs
index 3868745..9350b93 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/Main.cs
+++ b/HorseRacingPredictor/HorseRacingPredictor/Main.cs
@@ -1,4 +1,4 @@
-using BettingPredictor.UI;
+using BettingPredictor.UI;
using System;
using System.Data;
using System.IO;
@@ -16,8 +16,8 @@ namespace BettingPredictor
private DataTable racingData;
// Credenziali predefinite Racing API
- private const string DefaultRacingUser = "qi1mHOHPquDY9KNDASAeGipy";
- private const string DefaultRacingPass = "RXNFU1YX27R9rTnk8Vop8ZfH";
+ private const string DefaultRacingUser = "";
+ private const string DefaultRacingPass = "";
// Pagine e nav gestiti come array per semplificare la navigazione
private Panel[] pages;
diff --git a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml
index d16c7ba..89c1db1 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml
+++ b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml
@@ -1,7 +1,6 @@
-
+
+
+
@@ -311,7 +312,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
-
+
@@ -332,7 +333,9 @@
-
+
+
+
@@ -342,7 +345,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
-
+
@@ -484,8 +487,10 @@
+
+
-
+
@@ -495,7 +500,7 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
-
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
+
+
+
-
+
+
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Includi data
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Partite
- Quote
- Previsioni
- Classifiche
- Scontri diretti
- Eventi
- Formazioni
- Statistiche
- Infortuni
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Controlla quota
-
-
-
-
-
-
-
-
-
+
+
+
+
-
+
+
+
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Padding="20,14" Margin="0,0,0,16">
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Data
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
@@ -886,15 +1052,10 @@
-
-
+ BorderBrush="{StaticResource BrSurface1}" BorderThickness="1"/>
+
+
@@ -906,238 +1067,328 @@
-
+
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Quota
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
- Includi data
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Data
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
+ HorizontalAlignment="Left" Margin="0,4,0,24"
+ Click="btnSaveSettings_Click">
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs
index b92950f..201765b 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs
+++ b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs
@@ -8,9 +8,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
-using System.Collections.ObjectModel;
using System.Windows.Controls;
-using Microsoft.Web.WebView2.Core;
namespace HorseRacingPredictor
{
@@ -22,17 +20,21 @@ namespace HorseRacingPredictor
private DataTable _racingData;
private CancellationTokenSource _racingCts;
- // Virtual Football
- private readonly ObservableCollection _vfbResults = new ObservableCollection();
-
private const string DefaultRacingApiKey = "";
+ // Dynamic checkbox dictionaries for football endpoint/supplementary popups
+ private readonly Dictionary _fbEndpointCheckboxes = new();
+ private readonly Dictionary _fbSupplementaryCheckboxes = new();
+
public MainWindow()
{
InitializeComponent();
_footballManager = new Football.Main();
_racingManager = new HorseRacing.Main(DefaultRacingApiKey);
BuildCountryCheckboxes();
+ BuildFbEndpointCheckboxes();
+ BuildFbSupplementaryCheckboxes();
+ PopulateTimezoneComboBoxes();
// Wire preview update events
txtFbPrefix.TextChanged += (s, e) => UpdateFbPreview();
txtFbSuffix.TextChanged += (s, e) => UpdateFbPreview();
@@ -168,6 +170,44 @@ namespace HorseRacingPredictor
return sb.ToString();
}
+ // ———————————— TIMEZONE COMBOS ————————————
+
+ private List _ianaTimezones;
+
+ private void PopulateTimezoneComboBoxes()
+ {
+ _ianaTimezones = BuildIanaTimezoneList();
+ cmbFbTimezone.ItemsSource = _ianaTimezones;
+ cmbRcTimezone.ItemsSource = _ianaTimezones;
+ SetTimezoneSelection(cmbFbTimezone, "Europe/Rome");
+ SetTimezoneSelection(cmbRcTimezone, "Australia/Sydney");
+ }
+
+ private static List BuildIanaTimezoneList()
+ {
+ var ids = new SortedSet(StringComparer.Ordinal);
+ foreach (var tz in TimeZoneInfo.GetSystemTimeZones())
+ {
+ if (TimeZoneInfo.TryConvertWindowsIdToIanaId(tz.Id, out var ianaId))
+ ids.Add(ianaId);
+ }
+ return [.. ids];
+ }
+
+ private void SetTimezoneSelection(ComboBox combo, string ianaId)
+ {
+ if (combo == null || string.IsNullOrWhiteSpace(ianaId)) return;
+ // Add to list if not present (e.g. user saved a custom value)
+ if (_ianaTimezones != null && !_ianaTimezones.Contains(ianaId))
+ {
+ _ianaTimezones.Add(ianaId);
+ _ianaTimezones.Sort(StringComparer.Ordinal);
+ combo.ItemsSource = null;
+ combo.ItemsSource = _ianaTimezones;
+ }
+ combo.SelectedItem = ianaId;
+ }
+
// ???????????????????? LIFECYCLE ????????????????????
private void Window_Loaded(object sender, RoutedEventArgs e)
@@ -185,17 +225,24 @@ namespace HorseRacingPredictor
if (pageFootball != null) pageFootball.Visibility = name == "football" ? Visibility.Visible : Visibility.Collapsed;
if (pageRacing != null) pageRacing.Visibility = name == "racing" ? Visibility.Visible : Visibility.Collapsed;
if (pageSettings != null) pageSettings.Visibility = name == "settings" ? Visibility.Visible : Visibility.Collapsed;
- if (pageVirtualFb != null) pageVirtualFb.Visibility = name == "virtualfb" ? Visibility.Visible : Visibility.Collapsed;
- // Update title if available
+ // Update title and subtitle
if (lblTitle != null)
{
switch (name)
{
- case "football": lblTitle.Text = "Calcio"; break;
- case "racing": lblTitle.Text = "Corse Cavalli"; break;
- case "settings": lblTitle.Text = "Impostazioni"; break;
- case "virtualfb": lblTitle.Text = "Calcio Virtuale"; break;
+ case "football":
+ lblTitle.Text = "Calcio";
+ lblSubtitle.Text = "Dashboard eventi calcistici";
+ break;
+ case "racing":
+ lblTitle.Text = "Corse Cavalli";
+ lblSubtitle.Text = "Dashboard corse ippiche";
+ break;
+ case "settings":
+ lblTitle.Text = "Impostazioni";
+ lblSubtitle.Text = "Configurazione API, esportazione e parametri";
+ break;
}
}
}
@@ -203,55 +250,6 @@ namespace HorseRacingPredictor
private void navFootball_Checked(object sender, RoutedEventArgs e) => ShowPage("football");
private void navRacing_Checked(object sender, RoutedEventArgs e) => ShowPage("racing");
private void navSettings_Checked(object sender, RoutedEventArgs e) => ShowPage("settings");
- private bool _vfbInitialized;
- private async void navVirtualFb_Checked(object sender, RoutedEventArgs e)
- {
- ShowPage("virtualfb");
- // Bind results list once
- if (lbVfbResults.ItemsSource == null)
- lbVfbResults.ItemsSource = _vfbResults;
-
- if (!_vfbInitialized)
- {
- _vfbInitialized = true;
- try
- {
- // Persistent user-data folder so cookies, localStorage and session survive
- var userDataFolder = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
- "BettingPredictor", "WebView2Data");
-
- var env = await CoreWebView2Environment.CreateAsync(
- browserExecutableFolder: null,
- userDataFolder: userDataFolder,
- options: new CoreWebView2EnvironmentOptions());
-
- await wbVirtualFb.EnsureCoreWebView2Async(env);
-
- // Match the real Chrome User-Agent from the HAR capture
- wbVirtualFb.CoreWebView2.Settings.UserAgent =
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
- "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36";
-
- // Allow everything the SPA needs
- wbVirtualFb.CoreWebView2.Settings.IsScriptEnabled = true;
- wbVirtualFb.CoreWebView2.Settings.IsWebMessageEnabled = true;
- wbVirtualFb.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = true;
- wbVirtualFb.CoreWebView2.Settings.IsStatusBarEnabled = false;
-
- wbVirtualFb.CoreWebView2.Navigate(txtVfbUrl.Text);
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"[VFB] WebView2 init error: {ex.Message}");
- MessageBox.Show(
- $"Impossibile inizializzare WebView2.\n\n" +
- $"Assicurati che il Microsoft Edge WebView2 Runtime sia installato.\n\n{ex.Message}",
- "Errore WebView2", MessageBoxButton.OK, MessageBoxImage.Warning);
- _vfbInitialized = false;
- }
- }
- }
// ???????????????????? FOOTBALL ????????????????????
@@ -261,6 +259,61 @@ namespace HorseRacingPredictor
await DownloadFootballAsync(date);
}
+ private async void btnBrowseCsvFb_Click(object sender, RoutedEventArgs e)
+ {
+ using (var dlg = new System.Windows.Forms.FolderBrowserDialog())
+ {
+ dlg.Description = "Seleziona la cartella con i file CSV calcio";
+ if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
+
+ try
+ {
+ lblStatusFb.Text = "Caricamento file CSV…";
+ pbFootball.Value = 0;
+ btnExportFbCsv.IsEnabled = false;
+ btnBrowseCsvFb.IsEnabled = false;
+
+ var selectedPath = dlg.SelectedPath;
+ var progress = new Progress(v => pbFootball.Value = v);
+ var status = new Progress(s => lblStatusFb.Text = s);
+
+ var result = await Task.Run(() => LoadCsvFiles(selectedPath, progress, status));
+
+ if (result.table == null)
+ {
+ lblStatusFb.Text = result.message;
+ return;
+ }
+
+ InjectRowNumbers(result.table);
+ _footballData = result.table;
+ dgFootball.ItemsSource = _footballData?.DefaultView;
+ UpdateFootballStatCards();
+
+ if (_footballData.Rows.Count > 0)
+ {
+ btnExportFbCsv.IsEnabled = true;
+ lblStatusFb.Text = $"Caricati {_footballData.Rows.Count} righe da {result.fileCount} file CSV";
+ }
+ else
+ {
+ lblStatusFb.Text = "Nessun dato trovato nei file CSV";
+ }
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Errore durante il caricamento CSV:\n{ex.Message}",
+ "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
+ lblStatusFb.Text = "Errore nel caricamento CSV";
+ pbFootball.Value = 0;
+ }
+ finally
+ {
+ btnBrowseCsvFb.IsEnabled = true;
+ }
+ }
+ }
+
private async Task DownloadFootballAsync(DateTime date)
{
try
@@ -293,10 +346,31 @@ namespace HorseRacingPredictor
e.Cancel = true;
};
+ // Update stat cards
+ UpdateFootballStatCards();
+
if (_footballData != null && _footballData.Rows.Count > 0)
{
btnExportFbCsv.IsEnabled = true;
lblStatusFb.Text = $"Scaricate {_footballData.Rows.Count} partite";
+
+ // Scarica dati supplementari (CSV separati) se selezionati
+ if (options.AnySupplementarySelected)
+ {
+ string exportFolder = !string.IsNullOrWhiteSpace(txtFbExportPath.Text)
+ ? txtFbExportPath.Text.Trim()
+ : Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
+
+ lblStatusFb.Text = "Scaricamento dati supplementari…";
+ var suppFiles = await Task.Run(() =>
+ _footballManager.DownloadSupplementaryData(
+ _footballData, date, exportFolder, options, progress, status));
+
+ if (suppFiles.Count > 0)
+ lblStatusFb.Text = $"Scaricate {_footballData.Rows.Count} partite + {suppFiles.Count} CSV supplementari";
+ else
+ lblStatusFb.Text = $"Scaricate {_footballData.Rows.Count} partite (nessun dato supplementare trovato)";
+ }
}
else
{
@@ -443,14 +517,156 @@ namespace HorseRacingPredictor
}
}
- private void rbRcSource_Checked(object sender, RoutedEventArgs e)
+ private void ApplyDataSourceVisibility()
{
- // Toggle visibility of API vs CSV controls
- if (dpRacing == null || btnDownloadRc == null || btnBrowseCsvRc == null) return;
- bool isApi = rbRcApi.IsChecked == true;
- dpRacing.Visibility = isApi ? Visibility.Visible : Visibility.Collapsed;
- btnDownloadRc.Visibility = isApi ? Visibility.Visible : Visibility.Collapsed;
- btnBrowseCsvRc.Visibility = isApi ? Visibility.Collapsed : Visibility.Visible;
+ // Football: toggle API vs CSV controls
+ if (btnDownloadFb != null && btnBrowseCsvFb != null)
+ {
+ bool fbIsApi = IsFbApiSource();
+ dpFootball.Visibility = fbIsApi ? Visibility.Visible : Visibility.Collapsed;
+ btnDownloadFb.Visibility = fbIsApi ? Visibility.Visible : Visibility.Collapsed;
+ btnBrowseCsvFb.Visibility = fbIsApi ? Visibility.Collapsed : Visibility.Visible;
+ }
+
+ // Football: show/hide API-only settings (Endpoint, Supplementary, Advanced)
+ if (pnlFbApiOptions != null)
+ pnlFbApiOptions.Visibility = IsFbApiSource() ? Visibility.Visible : Visibility.Collapsed;
+
+ // Racing: toggle API vs CSV controls
+ if (btnDownloadRc != null && btnBrowseCsvRc != null)
+ {
+ bool rcIsApi = IsRcApiSource();
+ dpRacing.Visibility = rcIsApi ? Visibility.Visible : Visibility.Collapsed;
+ btnDownloadRc.Visibility = rcIsApi ? Visibility.Visible : Visibility.Collapsed;
+ btnBrowseCsvRc.Visibility = rcIsApi ? Visibility.Collapsed : Visibility.Visible;
+ }
+ }
+
+ private bool IsFbApiSource()
+ {
+ var src = (cmbFbDataSource?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "";
+ return src.StartsWith("API", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private bool IsRcApiSource()
+ {
+ var src = (cmbRcDataSource?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "";
+ return src.StartsWith("API", StringComparison.OrdinalIgnoreCase);
+ }
+
+ // ———————————— FOOTBALL ENDPOINT / SUPPLEMENTARY POPUPS ————————————
+
+ private static readonly (string key, string label, bool defaultChecked)[] FbEndpointItems =
+ [
+ ("Fixtures", "Partite", true),
+ ("Odds", "Quote", true),
+ ("Predictions","Previsioni", true),
+ ("Standings", "Classifiche", false),
+ ("H2H", "H2H", false),
+ ("Events", "Eventi", false),
+ ("Lineups", "Formazioni", false),
+ ("Statistics", "Statistiche", false),
+ ("Injuries", "Infortuni", false),
+ ];
+
+ private static readonly (string key, string label)[] FbSupplementaryItems =
+ [
+ ("PlayerStats", "Giocatori"),
+ ("TeamStats", "Squadre"),
+ ("TopScorers", "Marcatori"),
+ ("TopAssists", "Assist"),
+ ("TopCards", "Cartellini"),
+ ("Squads", "Rose"),
+ ("Coaches", "Allenatori"),
+ ("Transfers", "Trasferimenti"),
+ ];
+
+ private void BuildFbEndpointCheckboxes()
+ {
+ if (pnlFbEndpoints == null) return;
+ pnlFbEndpoints.Children.Clear();
+ _fbEndpointCheckboxes.Clear();
+
+ foreach (var (key, label, defaultChecked) in FbEndpointItems)
+ {
+ var cb = new CheckBox
+ {
+ Content = label,
+ Tag = key,
+ IsChecked = defaultChecked,
+ Margin = new Thickness(4, 2, 4, 2),
+ FontSize = 12,
+ Foreground = FindResource("BrText") as System.Windows.Media.Brush,
+ };
+ cb.Checked += (s, e) => UpdateFbEndpointsSummary();
+ cb.Unchecked += (s, e) => UpdateFbEndpointsSummary();
+ _fbEndpointCheckboxes[key] = cb;
+ pnlFbEndpoints.Children.Add(cb);
+ }
+ UpdateFbEndpointsSummary();
+ }
+
+ private void BuildFbSupplementaryCheckboxes()
+ {
+ if (pnlFbSupplementary == null) return;
+ pnlFbSupplementary.Children.Clear();
+ _fbSupplementaryCheckboxes.Clear();
+
+ foreach (var (key, label) in FbSupplementaryItems)
+ {
+ var cb = new CheckBox
+ {
+ Content = label,
+ Tag = key,
+ IsChecked = false,
+ Margin = new Thickness(4, 2, 4, 2),
+ FontSize = 12,
+ Foreground = FindResource("BrText") as System.Windows.Media.Brush,
+ };
+ cb.Checked += (s, e) => UpdateFbSupplementarySummary();
+ cb.Unchecked += (s, e) => UpdateFbSupplementarySummary();
+ _fbSupplementaryCheckboxes[key] = cb;
+ pnlFbSupplementary.Children.Add(cb);
+ }
+ UpdateFbSupplementarySummary();
+ }
+
+ private void UpdateFbEndpointsSummary()
+ {
+ var selected = _fbEndpointCheckboxes
+ .Where(kv => kv.Value.IsChecked == true)
+ .Select(kv => kv.Value.Content.ToString())
+ .ToList();
+ if (lblFbEndpointsSummary != null)
+ lblFbEndpointsSummary.Text = selected.Count > 0
+ ? string.Join(", ", selected) : "Nessuno";
+ }
+
+ private void UpdateFbSupplementarySummary()
+ {
+ var selected = _fbSupplementaryCheckboxes
+ .Where(kv => kv.Value.IsChecked == true)
+ .Select(kv => kv.Value.Content.ToString())
+ .ToList();
+ if (lblFbSupplementarySummary != null)
+ lblFbSupplementarySummary.Text = selected.Count > 0
+ ? string.Join(", ", selected) : "Nessuno";
+ }
+
+ private bool GetFbEndpoint(string key) =>
+ _fbEndpointCheckboxes.TryGetValue(key, out var cb) && cb.IsChecked == true;
+
+ private void SetFbEndpoint(string key, bool value)
+ {
+ if (_fbEndpointCheckboxes.TryGetValue(key, out var cb)) cb.IsChecked = value;
+ }
+
+ private bool GetFbSupplementary(string key) =>
+ _fbSupplementaryCheckboxes.TryGetValue(key, out var cb) && cb.IsChecked == true;
+
+ private void SetFbSupplementary(string key, bool value)
+ {
+ if (_fbSupplementaryCheckboxes.TryGetValue(key, out var cb)) cb.IsChecked = value;
}
private async void btnDownloadRc_Click(object sender, RoutedEventArgs e)
@@ -458,7 +674,7 @@ namespace HorseRacingPredictor
await DownloadRacecardsAsync();
}
- private void btnBrowseCsvRc_Click(object sender, RoutedEventArgs e)
+ private async void btnBrowseCsvRc_Click(object sender, RoutedEventArgs e)
{
using (var dlg = new System.Windows.Forms.FolderBrowserDialog())
{
@@ -470,90 +686,31 @@ namespace HorseRacingPredictor
lblStatusRc.Text = "Caricamento file CSV…";
pbRacing.Value = 0;
btnExportRcCsv.IsEnabled = false;
+ btnBrowseCsvRc.IsEnabled = false;
- var csvFiles = Directory.GetFiles(dlg.SelectedPath, "*.csv", SearchOption.AllDirectories)
- .OrderBy(f => f)
- .ToList();
+ var selectedPath = dlg.SelectedPath;
+ var progress = new Progress(v => pbRacing.Value = v);
+ var status = new Progress(s => lblStatusRc.Text = s);
- if (csvFiles.Count == 0)
+ var result = await Task.Run(() => LoadCsvFiles(selectedPath, progress, status));
+
+ if (result.table == null)
{
- lblStatusRc.Text = "Nessun file CSV trovato nella cartella selezionata";
+ lblStatusRc.Text = result.message;
return;
}
- // Merge all CSV files into a single DataTable preserving all original columns
- var table = new DataTable();
- table.Columns.Add("Meeting", typeof(string));
- table.Columns.Add("Race", typeof(int));
-
- int processed = 0;
- foreach (var file in csvFiles)
- {
- try
- {
- // Extract meeting name and race number from filename pattern YYYYMMDD-meeting-rXX.csv
- string fileName = Path.GetFileNameWithoutExtension(file);
- string meetingName = fileName;
- int raceNumber = 0;
- var m = Regex.Match(Path.GetFileName(file), @"^\d{8}-(.+)-r(\d+)\.csv$", RegexOptions.IgnoreCase);
- if (m.Success)
- {
- meetingName = string.Join(" ", m.Groups[1].Value.Split('-')
- .Select(s => s.Length > 0 ? char.ToUpper(s[0]) + s.Substring(1).ToLower() : s));
- int.TryParse(m.Groups[2].Value, out raceNumber);
- }
-
- // Read CSV with simple parser (comma-delimited, first row = header)
- var lines = File.ReadAllLines(file, Encoding.UTF8);
- if (lines.Length < 2) { processed++; continue; }
-
- var headers = ParseCsvLine(lines[0]);
-
- // Ensure all columns exist in the merged DataTable
- foreach (var h in headers)
- {
- string colName = h.Trim();
- if (string.IsNullOrWhiteSpace(colName)) continue;
- if (!table.Columns.Contains(colName))
- table.Columns.Add(colName, typeof(string));
- }
-
- // Parse data rows
- for (int i = 1; i < lines.Length; i++)
- {
- if (string.IsNullOrWhiteSpace(lines[i])) continue;
- var values = ParseCsvLine(lines[i]);
- var row = table.NewRow();
- row["Meeting"] = meetingName;
- row["Race"] = raceNumber;
- for (int c = 0; c < headers.Length && c < values.Length; c++)
- {
- string colName = headers[c].Trim();
- if (string.IsNullOrWhiteSpace(colName)) continue;
- if (table.Columns.Contains(colName))
- row[colName] = values[c]?.Trim() ?? "";
- }
- table.Rows.Add(row);
- }
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Errore CSV {file}: {ex.Message}");
- }
- processed++;
- pbRacing.Value = (int)((double)processed / csvFiles.Count * 100);
- }
-
// Add row numbers
- InjectRowNumbers(table);
+ InjectRowNumbers(result.table);
- _racingData = table;
+ _racingData = result.table;
dgRacing.ItemsSource = _racingData?.DefaultView;
+ UpdateRacingStatCards();
if (_racingData.Rows.Count > 0)
{
btnExportRcCsv.IsEnabled = true;
- lblStatusRc.Text = $"Caricati {_racingData.Rows.Count} cavalli da {csvFiles.Count} file CSV";
+ lblStatusRc.Text = $"Caricati {_racingData.Rows.Count} cavalli da {result.fileCount} file CSV";
}
else
{
@@ -567,9 +724,85 @@ namespace HorseRacingPredictor
lblStatusRc.Text = "Errore nel caricamento CSV";
pbRacing.Value = 0;
}
+ finally
+ {
+ btnBrowseCsvRc.IsEnabled = true;
+ }
}
}
+ private (DataTable table, int fileCount, string message) LoadCsvFiles(
+ string folderPath, IProgress progress, IProgress status)
+ {
+ var csvFiles = Directory.GetFiles(folderPath, "*.csv", SearchOption.AllDirectories)
+ .OrderBy(f => f)
+ .ToList();
+
+ if (csvFiles.Count == 0)
+ return (null, 0, "Nessun file CSV trovato nella cartella selezionata");
+
+ var table = new DataTable();
+ table.Columns.Add("Meeting", typeof(string));
+ table.Columns.Add("Race", typeof(int));
+
+ int processed = 0;
+ foreach (var file in csvFiles)
+ {
+ try
+ {
+ string fileName = Path.GetFileNameWithoutExtension(file);
+ string meetingName = fileName;
+ int raceNumber = 0;
+ var m = Regex.Match(Path.GetFileName(file), @"^\d{8}-(.+)-r(\d+)\.csv$", RegexOptions.IgnoreCase);
+ if (m.Success)
+ {
+ meetingName = string.Join(" ", m.Groups[1].Value.Split('-')
+ .Select(s => s.Length > 0 ? char.ToUpper(s[0]) + s.Substring(1).ToLower() : s));
+ int.TryParse(m.Groups[2].Value, out raceNumber);
+ }
+
+ var lines = File.ReadAllLines(file, Encoding.UTF8);
+ if (lines.Length < 2) { processed++; continue; }
+
+ var headers = ParseCsvLine(lines[0]);
+
+ foreach (var h in headers)
+ {
+ string colName = h.Trim();
+ if (string.IsNullOrWhiteSpace(colName)) continue;
+ if (!table.Columns.Contains(colName))
+ table.Columns.Add(colName, typeof(string));
+ }
+
+ for (int i = 1; i < lines.Length; i++)
+ {
+ if (string.IsNullOrWhiteSpace(lines[i])) continue;
+ var values = ParseCsvLine(lines[i]);
+ var row = table.NewRow();
+ row["Meeting"] = meetingName;
+ row["Race"] = raceNumber;
+ for (int c = 0; c < headers.Length && c < values.Length; c++)
+ {
+ string colName = headers[c].Trim();
+ if (string.IsNullOrWhiteSpace(colName)) continue;
+ if (table.Columns.Contains(colName))
+ row[colName] = values[c]?.Trim() ?? "";
+ }
+ table.Rows.Add(row);
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Errore CSV {file}: {ex.Message}");
+ }
+ processed++;
+ progress?.Report((int)((double)processed / csvFiles.Count * 100));
+ status?.Report($"Lettura CSV… {processed}/{csvFiles.Count}");
+ }
+
+ return (table, csvFiles.Count, null);
+ }
+
///
/// Parses a single CSV line respecting quoted fields (comma delimiter).
///
@@ -643,7 +876,8 @@ namespace HorseRacingPredictor
{
_racingCts.Cancel();
_racingCts = null;
- btnDownloadRc.Content = "Scarica Corse";
+ btnDownloadRcIcon.Text = "\u21E9";
+ btnDownloadRcText.Text = "Scarica";
lblStatusRc.Text = "Annullato";
return;
}
@@ -655,7 +889,8 @@ namespace HorseRacingPredictor
{
pbRacing.Value = 0;
lblStatusRc.Text = "Scaricamento corse da FormFav...";
- btnDownloadRc.Content = "Annulla";
+ btnDownloadRcIcon.Text = "\u2718";
+ btnDownloadRcText.Text = "Annulla";
dpRacing.IsEnabled = false;
btnExportRcCsv.IsEnabled = false;
@@ -676,6 +911,7 @@ namespace HorseRacingPredictor
InjectRowNumbers(_racingData);
dgRacing.ItemsSource = _racingData?.DefaultView;
+ UpdateRacingStatCards();
if (_racingData != null && _racingData.Rows.Count > 0)
{
@@ -702,7 +938,8 @@ namespace HorseRacingPredictor
finally
{
_racingCts = null;
- btnDownloadRc.Content = "Scarica Corse";
+ btnDownloadRcIcon.Text = "\u21E9";
+ btnDownloadRcText.Text = "Scarica";
dpRacing.IsEnabled = true;
}
}
@@ -711,7 +948,7 @@ namespace HorseRacingPredictor
{
_racingManager.UpdateApiKey(txtRacingApiKey.Text.Trim());
- var tz = txtRcTimezone?.Text?.Trim();
+ var tz = (cmbRcTimezone?.SelectedItem as string)?.Trim();
if (!string.IsNullOrEmpty(tz))
_racingManager.Timezone = tz;
@@ -849,6 +1086,7 @@ namespace HorseRacingPredictor
var s = UserSettings.Load();
txtApiKey.Text = s.ApiKey;
+ SetComboBoxSelectionByContent(cmbFbDataSource, s.FbDataSource);
txtFbExportPath.Text = s.FbExportPath;
txtFbPrefix.Text = s.FbPrefix;
txtFbSuffix.Text = s.FbSuffix;
@@ -856,20 +1094,33 @@ namespace HorseRacingPredictor
SetComboBoxSelectionByContent(cmbFbDateFormat, s.FbDateFormat);
SetComboBoxSelectionByContent(cmbFbFormat, s.FbFormat);
- // Football Download Options
- chkFbFixtures.IsChecked = s.FbDownloadFixtures;
- chkFbOdds.IsChecked = s.FbDownloadOdds;
- chkFbPredictions.IsChecked = s.FbDownloadPredictions;
- chkFbStandings.IsChecked = s.FbDownloadStandings;
- chkFbH2H.IsChecked = s.FbDownloadH2H;
- chkFbEvents.IsChecked = s.FbDownloadEvents;
- chkFbLineups.IsChecked = s.FbDownloadLineups;
- chkFbStatistics.IsChecked = s.FbDownloadStatistics;
- chkFbInjuries.IsChecked = s.FbDownloadInjuries;
+ // Football Download Options (dynamic popups)
+ SetFbEndpoint("Fixtures", s.FbDownloadFixtures);
+ SetFbEndpoint("Odds", s.FbDownloadOdds);
+ SetFbEndpoint("Predictions", s.FbDownloadPredictions);
+ SetFbEndpoint("Standings", s.FbDownloadStandings);
+ SetFbEndpoint("H2H", s.FbDownloadH2H);
+ SetFbEndpoint("Events", s.FbDownloadEvents);
+ SetFbEndpoint("Lineups", s.FbDownloadLineups);
+ SetFbEndpoint("Statistics", s.FbDownloadStatistics);
+ SetFbEndpoint("Injuries", s.FbDownloadInjuries);
+ UpdateFbEndpointsSummary();
+
+ // Football Supplementary Download Options (dynamic popups)
+ SetFbSupplementary("PlayerStats", s.FbDownloadPlayerStats);
+ SetFbSupplementary("TeamStats", s.FbDownloadTeamStats);
+ SetFbSupplementary("TopScorers", s.FbDownloadTopScorers);
+ SetFbSupplementary("TopAssists", s.FbDownloadTopAssists);
+ SetFbSupplementary("TopCards", s.FbDownloadTopCards);
+ SetFbSupplementary("Squads", s.FbDownloadSquads);
+ SetFbSupplementary("Coaches", s.FbDownloadCoaches);
+ SetFbSupplementary("Transfers", s.FbDownloadTransfers);
+ UpdateFbSupplementarySummary();
+
txtFbBookmakerId.Text = s.FbBookmakerId.ToString();
txtFbOddsMaxPages.Text = s.FbOddsMaxPages.ToString();
txtFbMaxFixtures.Text = s.FbMaxFixturesForDetails.ToString();
- if (txtFbTimezone != null) txtFbTimezone.Text = s.FbTimezone;
+ if (cmbFbTimezone != null) SetTimezoneSelection(cmbFbTimezone, s.FbTimezone);
txtFbLeagueIds.Text = s.FbLeagueIds.Count > 0 ? string.Join(",", s.FbLeagueIds) : "";
chkFbCheckQuota.IsChecked = s.FbCheckQuota;
txtFbMinQuota.Text = s.FbMinRemainingQuota.ToString();
@@ -884,12 +1135,14 @@ namespace HorseRacingPredictor
SetComboBoxSelectionByContent(cmbRcFormat, s.RcFormat);
txtRacingApiKey.Text = string.IsNullOrEmpty(s.RacingApiKey) ? DefaultRacingApiKey : s.RacingApiKey;
- if (txtRcTimezone != null) txtRcTimezone.Text = s.RcTimezone;
+ SetComboBoxSelectionByContent(cmbRcDataSource, s.RcDataSource);
+ if (cmbRcTimezone != null) SetTimezoneSelection(cmbRcTimezone, s.RcTimezone);
SetSelectedCountries(s.RcCountries.ToArray());
UpdateFbPreview();
UpdateRcPreview();
ApplyRacingSettings();
+ ApplyDataSourceVisibility();
}
catch { }
}
@@ -1127,26 +1380,36 @@ namespace HorseRacingPredictor
var s = new UserSettings
{
ApiKey = txtApiKey.Text.Trim(),
+ FbDataSource = (cmbFbDataSource?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "API - API-Football",
FbExportPath = txtFbExportPath.Text.Trim(),
FbPrefix = txtFbPrefix.Text.Trim(),
FbSuffix = txtFbSuffix.Text.Trim(),
FbIncludeDate = chkFbIncludeDate.IsChecked == true,
FbDateFormat = (cmbFbDateFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "yyyy-MM-dd",
FbFormat = (cmbFbFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV",
- // Football Download Options
- FbDownloadFixtures = chkFbFixtures.IsChecked == true,
- FbDownloadOdds = chkFbOdds.IsChecked == true,
- FbDownloadPredictions = chkFbPredictions.IsChecked == true,
- FbDownloadStandings = chkFbStandings.IsChecked == true,
- FbDownloadH2H = chkFbH2H.IsChecked == true,
- FbDownloadEvents = chkFbEvents.IsChecked == true,
- FbDownloadLineups = chkFbLineups.IsChecked == true,
- FbDownloadStatistics = chkFbStatistics.IsChecked == true,
- FbDownloadInjuries = chkFbInjuries.IsChecked == true,
+ // Football Download Options (from dynamic popups)
+ FbDownloadFixtures = GetFbEndpoint("Fixtures"),
+ FbDownloadOdds = GetFbEndpoint("Odds"),
+ FbDownloadPredictions = GetFbEndpoint("Predictions"),
+ FbDownloadStandings = GetFbEndpoint("Standings"),
+ FbDownloadH2H = GetFbEndpoint("H2H"),
+ FbDownloadEvents = GetFbEndpoint("Events"),
+ FbDownloadLineups = GetFbEndpoint("Lineups"),
+ FbDownloadStatistics = GetFbEndpoint("Statistics"),
+ FbDownloadInjuries = GetFbEndpoint("Injuries"),
+ // Football Supplementary Download Options (from dynamic popups)
+ FbDownloadPlayerStats = GetFbSupplementary("PlayerStats"),
+ FbDownloadTeamStats = GetFbSupplementary("TeamStats"),
+ FbDownloadTopScorers = GetFbSupplementary("TopScorers"),
+ FbDownloadTopAssists = GetFbSupplementary("TopAssists"),
+ FbDownloadTopCards = GetFbSupplementary("TopCards"),
+ FbDownloadSquads = GetFbSupplementary("Squads"),
+ FbDownloadCoaches = GetFbSupplementary("Coaches"),
+ FbDownloadTransfers = GetFbSupplementary("Transfers"),
FbBookmakerId = int.TryParse(txtFbBookmakerId.Text.Trim(), out var bId) ? bId : 8,
FbOddsMaxPages = int.TryParse(txtFbOddsMaxPages.Text.Trim(), out var omp) ? omp : 3,
FbMaxFixturesForDetails = int.TryParse(txtFbMaxFixtures.Text.Trim(), out var mf) ? mf : 50,
- FbTimezone = txtFbTimezone?.Text?.Trim() ?? "Europe/Rome",
+ FbTimezone = (cmbFbTimezone?.SelectedItem as string)?.Trim() ?? "Europe/Rome",
FbLeagueIds = ParseIntList(txtFbLeagueIds?.Text),
FbCheckQuota = chkFbCheckQuota.IsChecked == true,
FbMinRemainingQuota = int.TryParse(txtFbMinQuota.Text.Trim(), out var mq) ? mq : 10,
@@ -1159,7 +1422,8 @@ namespace HorseRacingPredictor
RcDateFormat = (cmbRcDateFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "yyyy-MM-dd",
RcFormat = (cmbRcFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV",
RacingApiKey = txtRacingApiKey.Text.Trim(),
- RcTimezone = txtRcTimezone?.Text?.Trim() ?? "Australia/Sydney",
+ RcDataSource = (cmbRcDataSource?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "API - FormFav",
+ RcTimezone = (cmbRcTimezone?.SelectedItem as string)?.Trim() ?? "Australia/Sydney",
RcCountries = new List(GetSelectedCountries())
};
s.Save();
@@ -1167,6 +1431,7 @@ namespace HorseRacingPredictor
UpdateFbPreview();
UpdateRcPreview();
ApplyRacingSettings();
+ ApplyDataSourceVisibility();
MessageBox.Show("Impostazioni salvate con successo.",
"Salvato", MessageBoxButton.OK, MessageBoxImage.Information);
@@ -1180,6 +1445,52 @@ namespace HorseRacingPredictor
// ???????????? FOOTBALL DOWNLOAD OPTIONS HELPERS ????????????
+ ///
+ /// Aggiorna le stat card nel dashboard Calcio.
+ ///
+ private void UpdateFootballStatCards()
+ {
+ int rows = _footballData?.Rows.Count ?? 0;
+ int cols = _footballData?.Columns.Count ?? 0;
+ lblFbCardCount.Text = rows.ToString();
+ lblFbCardCols.Text = cols > 0 ? cols.ToString() : "--";
+ lblFbCardFormat.Text = (cmbFbFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV";
+
+ if (rows > 0)
+ {
+ lblFbCardStatus.Text = "Caricato";
+ dotFbStatus.Fill = FindResource("BrGreen") as System.Windows.Media.SolidColorBrush;
+ }
+ else
+ {
+ lblFbCardStatus.Text = "Pronto";
+ dotFbStatus.Fill = FindResource("BrOverlay0") as System.Windows.Media.SolidColorBrush;
+ }
+ }
+
+ ///
+ /// Aggiorna le stat card nel dashboard Corse Cavalli.
+ ///
+ private void UpdateRacingStatCards()
+ {
+ int rows = _racingData?.Rows.Count ?? 0;
+ int cols = _racingData?.Columns.Count ?? 0;
+ lblRcCardCount.Text = rows.ToString();
+ lblRcCardCols.Text = cols > 0 ? cols.ToString() : "--";
+ lblRcCardSource.Text = IsRcApiSource() ? "API" : "CSV";
+
+ if (rows > 0)
+ {
+ lblRcCardStatus.Text = "Caricato";
+ dotRcStatus.Fill = FindResource("BrGreen") as System.Windows.Media.SolidColorBrush;
+ }
+ else
+ {
+ lblRcCardStatus.Text = "Pronto";
+ dotRcStatus.Fill = FindResource("BrOverlay0") as System.Windows.Media.SolidColorBrush;
+ }
+ }
+
///
/// Costruisce un FootballDownloadOptions leggendo i valori dalla UI.
///
@@ -1187,19 +1498,28 @@ namespace HorseRacingPredictor
{
return new Football.FootballDownloadOptions
{
- DownloadFixtures = chkFbFixtures.IsChecked == true,
- DownloadOdds = chkFbOdds.IsChecked == true,
- DownloadPredictions = chkFbPredictions.IsChecked == true,
- DownloadStandings = chkFbStandings.IsChecked == true,
- DownloadH2H = chkFbH2H.IsChecked == true,
- DownloadEvents = chkFbEvents.IsChecked == true,
- DownloadLineups = chkFbLineups.IsChecked == true,
- DownloadStatistics = chkFbStatistics.IsChecked == true,
- DownloadInjuries = chkFbInjuries.IsChecked == true,
+ DownloadFixtures = GetFbEndpoint("Fixtures"),
+ DownloadOdds = GetFbEndpoint("Odds"),
+ DownloadPredictions = GetFbEndpoint("Predictions"),
+ DownloadStandings = GetFbEndpoint("Standings"),
+ DownloadH2H = GetFbEndpoint("H2H"),
+ DownloadEvents = GetFbEndpoint("Events"),
+ DownloadLineups = GetFbEndpoint("Lineups"),
+ DownloadStatistics = GetFbEndpoint("Statistics"),
+ DownloadInjuries = GetFbEndpoint("Injuries"),
+ // Supplementari
+ DownloadPlayerStats = GetFbSupplementary("PlayerStats"),
+ DownloadTeamStats = GetFbSupplementary("TeamStats"),
+ DownloadTopScorers = GetFbSupplementary("TopScorers"),
+ DownloadTopAssists = GetFbSupplementary("TopAssists"),
+ DownloadTopCards = GetFbSupplementary("TopCards"),
+ DownloadSquads = GetFbSupplementary("Squads"),
+ DownloadCoaches = GetFbSupplementary("Coaches"),
+ DownloadTransfers = GetFbSupplementary("Transfers"),
BookmakerId = int.TryParse(txtFbBookmakerId.Text.Trim(), out var bId) ? bId : 8,
OddsMaxPages = int.TryParse(txtFbOddsMaxPages.Text.Trim(), out var omp) ? omp : 3,
MaxFixturesForDetails = int.TryParse(txtFbMaxFixtures.Text.Trim(), out var mf) ? mf : 50,
- Timezone = txtFbTimezone?.Text?.Trim() ?? "Europe/Rome",
+ Timezone = (cmbFbTimezone?.SelectedItem as string)?.Trim() ?? "Europe/Rome",
LeagueIds = ParseIntList(txtFbLeagueIds?.Text),
CheckQuota = chkFbCheckQuota.IsChecked == true,
MinRemainingQuota = int.TryParse(txtFbMinQuota.Text.Trim(), out var mq) ? mq : 10,
@@ -1221,114 +1541,5 @@ namespace HorseRacingPredictor
}
return result;
}
-
- // ???????????????????????? VIRTUAL FOOTBALL ????????????????????????
-
- private void btnVfbNavigate_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- var url = txtVfbUrl.Text?.Trim();
- if (!string.IsNullOrEmpty(url) && wbVirtualFb.CoreWebView2 != null)
- wbVirtualFb.CoreWebView2.Navigate(url);
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"[VFB] Navigate error: {ex.Message}");
- }
- }
-
- private void btnVfbRefresh_Click(object sender, RoutedEventArgs e)
- {
- try { wbVirtualFb.CoreWebView2?.Reload(); }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"[VFB] Refresh error: {ex.Message}");
- }
- }
-
- private void btnVfbAddResult_Click(object sender, RoutedEventArgs e)
- {
- if (!int.TryParse(txtVfbHomeGoals.Text, out int hg)) hg = 0;
- if (!int.TryParse(txtVfbAwayGoals.Text, out int ag)) ag = 0;
-
- var match = new VirtualFootball.VirtualMatch
- {
- Time = DateTime.Now.ToString("HH:mm"),
- Home = string.IsNullOrWhiteSpace(txtVfbHome.Text) ? "Casa" : txtVfbHome.Text.Trim(),
- HomeGoals = hg,
- AwayGoals = ag,
- Away = string.IsNullOrWhiteSpace(txtVfbAway.Text) ? "Ospite" : txtVfbAway.Text.Trim()
- };
-
- _vfbResults.Insert(0, match);
-
- // Reset input
- txtVfbHomeGoals.Text = "0";
- txtVfbAwayGoals.Text = "0";
-
- UpdateVfbStats();
- UpdateVfbSuggestion();
- }
-
- private void UpdateVfbStats()
- {
- if (_vfbResults.Count == 0) { lblVfbStats.Text = "Nessun dato"; return; }
-
- int total = _vfbResults.Count;
- int draws = _vfbResults.Count(m => m.Outcome == "X");
- int home = _vfbResults.Count(m => m.Outcome == "1");
- int away = _vfbResults.Count(m => m.Outcome == "2");
- double drawPct = (double)draws / total * 100;
- double homePct = (double)home / total * 100;
- double awayPct = (double)away / total * 100;
- int totalGoals = _vfbResults.Sum(m => m.HomeGoals + m.AwayGoals);
- double avgGoals = (double)totalGoals / total;
- int over25 = _vfbResults.Count(m => m.HomeGoals + m.AwayGoals > 2);
-
- lblVfbStats.Text = $"Partite: {total}\n" +
- $"1: {home} ({homePct:F1}%) X: {draws} ({drawPct:F1}%) 2: {away} ({awayPct:F1}%)\n" +
- $"Media gol: {avgGoals:F1} Over 2.5: {over25} ({(double)over25 / total * 100:F1}%)";
- }
-
- private void UpdateVfbSuggestion()
- {
- if (_vfbResults.Count < 5)
- {
- lblVfbSuggestion.Text = "Inserisci almeno 5 risultati";
- return;
- }
-
- // Count consecutive non-draw results (most recent first)
- int streak = 0;
- foreach (var m in _vfbResults)
- {
- if (m.Outcome != "X") streak++;
- else break;
- }
-
- // Simple strategy: after a long streak without draws, suggest betting on draw
- if (streak >= 8)
- {
- lblVfbSuggestion.Text = $"\u26A0 PUNTA X (pareggio) \u2014 {streak} partite consecutive senza pareggio!\n" +
- "Puntata alta consigliata.";
- }
- else if (streak >= 5)
- {
- lblVfbSuggestion.Text = $"\u2705 Punta X (pareggio) \u2014 {streak} partite senza pareggio.\n" +
- "Puntata media consigliata.";
- }
- else if (streak >= 3)
- {
- lblVfbSuggestion.Text = $"\u23F3 Possibile X \u2014 {streak} partite senza pareggio.\n" +
- "Puntata bassa o attendi.";
- }
- else
- {
- double drawPct = (double)_vfbResults.Count(m => m.Outcome == "X") / _vfbResults.Count * 100;
- lblVfbSuggestion.Text = $"Pareggio recente \u2014 attendi una serie senza X.\n" +
- $"Frequenza X attuale: {drawPct:F1}%";
- }
- }
}
}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/VirtualFootball/VirtualMatch.cs b/HorseRacingPredictor/HorseRacingPredictor/VirtualFootball/VirtualMatch.cs
deleted file mode 100644
index fdc35a6..0000000
--- a/HorseRacingPredictor/HorseRacingPredictor/VirtualFootball/VirtualMatch.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-namespace HorseRacingPredictor.VirtualFootball
-{
- ///
- /// Represents a single virtual football match result displayed in the results panel.
- ///
- public class VirtualMatch
- {
- public string Time { get; set; }
- public string Home { get; set; }
- public int HomeGoals { get; set; }
- public int AwayGoals { get; set; }
- public string Away { get; set; }
-
- public string Score => $"{HomeGoals} - {AwayGoals}";
-
- /// 1, X, or 2
- public string Outcome
- {
- get
- {
- if (HomeGoals > AwayGoals) return "1";
- if (HomeGoals < AwayGoals) return "2";
- return "X";
- }
- }
-
- /// Row background colour: green for draw, red for 1/2.
- public string RowColor => Outcome == "X" ? "#2A4A3A" : "#4A2A2A";
- }
-}