diff --git a/HorseRacingPredictor/HorseRacingPredictor/BettingPredictor.csproj b/HorseRacingPredictor/HorseRacingPredictor/BettingPredictor.csproj
index 634fef8..649c76e 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/BettingPredictor.csproj
+++ b/HorseRacingPredictor/HorseRacingPredictor/BettingPredictor.csproj
@@ -54,4 +54,9 @@
+
+
+ PreserveNewest
+
+
\ No newline at end of file
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/Events.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Events.cs
new file mode 100644
index 0000000..4629589
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Events.cs
@@ -0,0 +1,29 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "fixtures/events" dell'API-Football.
+ /// Restituisce gli eventi di una partita (gol, cartellini, sostituzioni, VAR).
+ ///
+ internal class Events : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "fixtures/events";
+
+ ///
+ /// Ottiene tutti gli eventi per una partita
+ ///
+ public RestResponse GetEventsByFixture(int fixtureId)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?fixture={fixtureId}", ApiTypes.Fixtures);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore durante il recupero degli eventi per la partita {fixtureId}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/FixtureStatistics.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/FixtureStatistics.cs
new file mode 100644
index 0000000..64831b8
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/FixtureStatistics.cs
@@ -0,0 +1,29 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "fixtures/statistics" dell'API-Football.
+ /// Restituisce le statistiche di una partita (possesso, tiri, falli, ecc.).
+ ///
+ internal class FixtureStatistics : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "fixtures/statistics";
+
+ ///
+ /// Ottiene le statistiche per una partita
+ ///
+ public RestResponse GetStatisticsByFixture(int fixtureId)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?fixture={fixtureId}", ApiTypes.Fixtures);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore durante il recupero delle statistiche per la partita {fixtureId}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/HeadToHead.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/HeadToHead.cs
new file mode 100644
index 0000000..423d8bf
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/HeadToHead.cs
@@ -0,0 +1,32 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "fixtures/headtohead" dell'API-Football.
+ /// Restituisce lo storico degli scontri diretti tra due squadre.
+ ///
+ internal class HeadToHead : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "fixtures/headtohead";
+
+ ///
+ /// Ottiene gli scontri diretti tra due squadre
+ ///
+ /// ID della prima squadra
+ /// ID della seconda squadra
+ /// Numero di ultimi scontri da recuperare (default 5)
+ public RestResponse GetH2H(int teamId1, int teamId2, int last = 5)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?h2h={teamId1}-{teamId2}&last={last}", ApiTypes.Fixtures);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore durante il recupero H2H per {teamId1} vs {teamId2}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/Injuries.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Injuries.cs
new file mode 100644
index 0000000..5978b2e
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Injuries.cs
@@ -0,0 +1,60 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "injuries" dell'API-Football.
+ /// Restituisce la lista di giocatori infortunati o squalificati.
+ ///
+ internal class Injuries : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "injuries";
+
+ ///
+ /// Ottiene gli infortunati per una data specifica
+ ///
+ public RestResponse GetInjuriesByDate(DateTime date)
+ {
+ try
+ {
+ string dateStr = date.ToString("yyyy-MM-dd");
+ return ExecuteRequest($"{Endpoint}?date={dateStr}", ApiTypes.Fixtures);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore durante il recupero degli infortunati per la data {date.ToShortDateString()}: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Ottiene gli infortunati per una partita specifica
+ ///
+ public RestResponse GetInjuriesByFixture(int fixtureId)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?fixture={fixtureId}", ApiTypes.Fixtures);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore durante il recupero degli infortunati per la partita {fixtureId}: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Ottiene gli infortunati per una lega e stagione
+ ///
+ public RestResponse GetInjuriesByLeague(int leagueId, int season)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?league={leagueId}&season={season}", ApiTypes.Fixtures);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore durante il recupero degli infortunati per lega {leagueId}, stagione {season}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/Lineups.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Lineups.cs
new file mode 100644
index 0000000..9e8d1f5
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Lineups.cs
@@ -0,0 +1,29 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "fixtures/lineups" dell'API-Football.
+ /// Restituisce formazioni, modulo e titolari di una partita.
+ ///
+ internal class Lineups : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "fixtures/lineups";
+
+ ///
+ /// Ottiene le formazioni per una partita
+ ///
+ public RestResponse GetLineupsByFixture(int fixtureId)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?fixture={fixtureId}", ApiTypes.Fixtures);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore durante il recupero delle formazioni per la partita {fixtureId}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/Standings.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Standings.cs
new file mode 100644
index 0000000..5513755
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Standings.cs
@@ -0,0 +1,44 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "standings" dell'API-Football.
+ /// Restituisce la classifica di un campionato per una stagione.
+ ///
+ internal class Standings : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "standings";
+
+ ///
+ /// Ottiene la classifica per una lega e una stagione
+ ///
+ public RestResponse GetStandings(int leagueId, int season)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?league={leagueId}&season={season}", ApiTypes.Leagues);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore durante il recupero della classifica per lega {leagueId}, stagione {season}: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Ottiene la classifica per un team e una stagione
+ ///
+ public RestResponse GetStandingsByTeam(int teamId, int season)
+ {
+ try
+ {
+ return ExecuteRequest($"{Endpoint}?team={teamId}&season={season}", ApiTypes.Teams);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore durante il recupero della classifica per team {teamId}, stagione {season}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/API/Status.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Status.cs
new file mode 100644
index 0000000..26bcc27
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/API/Status.cs
@@ -0,0 +1,30 @@
+using System;
+using RestSharp;
+
+namespace HorseRacingPredictor.Football.API
+{
+ ///
+ /// Client per l'endpoint "status" dell'API-Football.
+ /// Restituisce la quota residua giornaliera e le informazioni sull'account.
+ /// Non conta come chiamata nella quota giornaliera.
+ ///
+ internal class Status : HorseRacingPredictor.Football.Manager.API
+ {
+ private const string Endpoint = "status";
+
+ ///
+ /// Ottiene lo stato dell'account e la quota residua
+ ///
+ public RestResponse GetStatus()
+ {
+ try
+ {
+ return ExecuteRequest(Endpoint);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Errore durante il recupero dello stato API: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/FootballDownloadOptions.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/FootballDownloadOptions.cs
new file mode 100644
index 0000000..2c0b6d4
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/FootballDownloadOptions.cs
@@ -0,0 +1,102 @@
+using System.Collections.Generic;
+
+namespace HorseRacingPredictor.Football
+{
+ ///
+ /// Parametri configurabili per controllare quali endpoint API-Football scaricare
+ /// e come filtrare i dati risultanti.
+ ///
+ public class FootballDownloadOptions
+ {
+ // ?? Endpoint da scaricare ?????????????????????????????????
+ public bool DownloadFixtures { get; set; } = true;
+ public bool DownloadOdds { get; set; } = true;
+ public bool DownloadPredictions { get; set; } = true;
+ public bool DownloadStandings { get; set; } = false;
+ public bool DownloadH2H { get; set; } = false;
+ public bool DownloadEvents { get; set; } = false;
+ public bool DownloadLineups { get; set; } = false;
+ public bool DownloadStatistics { get; set; } = false;
+ public bool DownloadInjuries { get; set; } = false;
+
+ // ?? Filtri ????????????????????????????????????????????????
+ ///
+ /// Se non vuoto, scarica fixture solo per queste leghe (league IDs).
+ /// Lista vuota = tutte le leghe.
+ ///
+ public List LeagueIds { get; set; } = new();
+
+ ///
+ /// ID del bookmaker per le quote (default 8 = Bet365).
+ ///
+ public int BookmakerId { get; set; } = 8;
+
+ ///
+ /// Numero massimo di pagine da scaricare per le quote.
+ ///
+ public int OddsMaxPages { get; set; } = 3;
+
+ ///
+ /// Timezone IANA per le date delle fixture (es. "Europe/Rome").
+ ///
+ public string Timezone { get; set; } = "Europe/Rome";
+
+ ///
+ /// Stagione corrente per le classifiche (calcolata automaticamente se 0).
+ ///
+ public int Season { get; set; } = 0;
+
+ ///
+ /// Numero massimo di fixture per cui scaricare dati aggiuntivi
+ /// (H2H, events, lineups, statistics) per evitare troppe chiamate.
+ /// 0 = nessun limite.
+ ///
+ public int MaxFixturesForDetails { get; set; } = 50;
+
+ ///
+ /// Ritardo in millisecondi tra una chiamata API e l'altra per rispettare il rate limit.
+ ///
+ public int ApiDelayMs { get; set; } = 300;
+
+ ///
+ /// Se true, controlla la quota residua prima di iniziare e si ferma quando insufficiente.
+ ///
+ public bool CheckQuota { get; set; } = true;
+
+ ///
+ /// Numero minimo di richieste residue prima di fermare il download.
+ ///
+ public int MinRemainingQuota { get; set; } = 10;
+
+ ///
+ /// Restituisce la stagione corrente calcolata (anno corrente o anno-1 se prima di luglio).
+ ///
+ public int GetEffectiveSeason()
+ {
+ if (Season > 0) return Season;
+ var now = System.DateTime.Now;
+ return now.Month >= 7 ? now.Year : now.Year - 1;
+ }
+
+ ///
+ /// Conta il numero approssimativo di chiamate API previste per una data.
+ /// Utile per mostrare all'utente una stima.
+ ///
+ public int EstimateApiCalls(int fixtureCount)
+ {
+ int calls = 0;
+ if (DownloadFixtures) calls += 1;
+ if (DownloadOdds) calls += OddsMaxPages;
+ if (DownloadPredictions) calls += fixtureCount;
+ if (DownloadStandings && LeagueIds.Count > 0) calls += LeagueIds.Count;
+ else if (DownloadStandings) calls += 5; // stima
+ if (DownloadH2H) calls += fixtureCount;
+ if (DownloadEvents) calls += fixtureCount;
+ if (DownloadLineups) calls += fixtureCount;
+ if (DownloadStatistics) calls += fixtureCount;
+ if (DownloadInjuries) calls += 1;
+ if (CheckQuota) calls += 1; // status endpoint
+ return calls;
+ }
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/Main.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/Main.cs
index d1f9635..b9a8d23 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/Football/Main.cs
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/Main.cs
@@ -12,6 +12,23 @@ using Microsoft.Data.SqlClient;
namespace HorseRacingPredictor.Football
{
+ ///
+ /// Informazioni sulla quota API residua.
+ ///
+ public struct QuotaInfo
+ {
+ public int CurrentUsed { get; set; }
+ public int DailyLimit { get; set; }
+ public int Remaining => DailyLimit - CurrentUsed;
+ public bool IsValid { get; set; }
+
+ public override string ToString()
+ {
+ if (!IsValid) return "Quota: N/D";
+ return $"Quota: {Remaining}/{DailyLimit} rimaste ({CurrentUsed} usate)";
+ }
+ }
+
///
/// Classe centralizzata per la gestione delle API di Football
///
@@ -22,6 +39,15 @@ namespace HorseRacingPredictor.Football
private readonly API.Fixture _fixtureAPI;
private readonly API.Odds _oddsAPI;
+ // Nuovi API client per endpoint aggiuntivi
+ private readonly API.Status _statusAPI;
+ private readonly API.Standings _standingsAPI;
+ private readonly API.HeadToHead _h2hAPI;
+ private readonly API.Events _eventsAPI;
+ private readonly API.Lineups _lineupsAPI;
+ private readonly API.FixtureStatistics _fixtureStatsAPI;
+ private readonly API.Injuries _injuriesAPI;
+
// Repository utilizzati per la gestione dei dati
private readonly Football.Database.League _leagueRepository;
private readonly Football.Database.Fixture _fixtureRepository;
@@ -43,6 +69,9 @@ namespace HorseRacingPredictor.Football
// Aggiungi il repository per le risposte API
private readonly Football.Database.APIResponse _apiResponseRepository;
+ // Ultima quota letta
+ private QuotaInfo _lastQuota;
+
public Main()
{
_database = new Manager.Database();
@@ -50,6 +79,15 @@ namespace HorseRacingPredictor.Football
_fixtureAPI = new API.Fixture();
_oddsAPI = new API.Odds();
+ // Nuovi API client
+ _statusAPI = new API.Status();
+ _standingsAPI = new API.Standings();
+ _h2hAPI = new API.HeadToHead();
+ _eventsAPI = new API.Events();
+ _lineupsAPI = new API.Lineups();
+ _fixtureStatsAPI = new API.FixtureStatistics();
+ _injuriesAPI = new API.Injuries();
+
// Inizializzazione dei repository qui invece che nella classe Database
_leagueRepository = new Football.Database.League();
_fixtureRepository = new Football.Database.Fixture();
@@ -99,31 +137,137 @@ namespace HorseRacingPredictor.Football
}
///
- /// Recupera solo l'elenco delle partite per la data specificata e le restituisce come DataTable semplice
+ /// Recupera solo l'elenco delle partite per la data specificata e le restituisce come DataTable semplice.
+ /// Overload retrocompatibile senza opzioni.
///
public DataTable GetTodayFixtures(DateTime date, IProgress progressCallback = null, IProgress statusCallback = null)
+ {
+ return GetTodayFixtures(date, new FootballDownloadOptions(), progressCallback, statusCallback);
+ }
+
+ ///
+ /// Recupera le partite per la data specificata con controllo granulare degli endpoint da scaricare.
+ /// La progress bar avanza proporzionalmente al numero di step attivi.
+ ///
+ public DataTable GetTodayFixtures(DateTime date, FootballDownloadOptions options, IProgress progressCallback = null, IProgress statusCallback = null)
{
try
{
- statusCallback?.Report("Scaricamento elenco partite...");
- progressCallback?.Report(10);
+ // Calcola il numero totale di step per una progress bar proporzionale
+ var steps = BuildProgressSteps(options);
+ int currentStep = 0;
+ int totalSteps = steps.Count;
- var fixturesResponse = GetFixtures(date);
- progressCallback?.Report(40);
+ void ReportProgress(string status)
+ {
+ currentStep++;
+ int pct = totalSteps > 0 ? (int)((double)currentStep / totalSteps * 100) : 0;
+ pct = Math.Min(pct, 100);
+ progressCallback?.Report(pct);
+ statusCallback?.Report(status);
+ }
- statusCallback?.Report("Elaborazione partite...");
- var table = CreateFixturesDataTable(fixturesResponse);
- progressCallback?.Report(50);
+ // ?? Step 0: Controllo quota API ??
+ if (options.CheckQuota)
+ {
+ statusCallback?.Report("Verifica quota API residua...");
+ progressCallback?.Report(0);
+ _lastQuota = CheckApiQuota();
+ if (_lastQuota.IsValid)
+ {
+ statusCallback?.Report($"{_lastQuota} — Avvio scaricamento...");
+ if (_lastQuota.Remaining <= options.MinRemainingQuota)
+ {
+ statusCallback?.Report($"? Quota insufficiente: {_lastQuota.Remaining} rimaste (minimo {options.MinRemainingQuota}). Scaricamento annullato.");
+ progressCallback?.Report(100);
+ return CreateEmptyFixturesDataTable(options);
+ }
+ }
+ }
- statusCallback?.Report("Scaricamento quote...");
- var oddsResponses = GetOdds(date);
- progressCallback?.Report(80);
+ // ?? Step 1: Fixtures ??
+ RestResponse fixturesResponse = null;
+ DataTable table;
+ if (options.DownloadFixtures)
+ {
+ ReportProgress("Scaricamento elenco partite...");
+ fixturesResponse = GetFixtures(date);
+ table = CreateFixturesDataTable(fixturesResponse, options);
+ }
+ else
+ {
+ table = CreateEmptyFixturesDataTable(options);
+ }
- statusCallback?.Report("Integrazione quote...");
- ParseOddsIntoTable(table, oddsResponses);
+ int fixtureCount = table.Rows.Count;
+
+ // ?? Step 2: Quote ??
+ if (options.DownloadOdds)
+ {
+ ReportProgress($"Scaricamento quote (max {options.OddsMaxPages} pagine, bookmaker {options.BookmakerId})...");
+ var oddsResponses = GetOdds(date, options);
+ ParseOddsIntoTable(table, oddsResponses, options.BookmakerId);
+ }
+
+ // ?? Step 3: Previsioni ??
+ if (options.DownloadPredictions && fixturesResponse != null)
+ {
+ int maxPred = options.MaxFixturesForDetails > 0
+ ? Math.Min(fixtureCount, options.MaxFixturesForDetails)
+ : fixtureCount;
+ ReportProgress($"Scaricamento previsioni per {maxPred} partite...");
+ EnrichWithPredictions(table, fixturesResponse, options, statusCallback, progressCallback, ref currentStep, totalSteps);
+ }
+
+ // ?? Step 4: Classifiche ??
+ if (options.DownloadStandings)
+ {
+ ReportProgress("Scaricamento classifiche...");
+ EnrichWithStandings(table, options);
+ }
+
+ // ?? Step 5: H2H ??
+ if (options.DownloadH2H)
+ {
+ int maxH2H = options.MaxFixturesForDetails > 0
+ ? Math.Min(fixtureCount, options.MaxFixturesForDetails)
+ : fixtureCount;
+ ReportProgress($"Scaricamento scontri diretti per {maxH2H} partite...");
+ EnrichWithH2H(table, options, statusCallback, progressCallback, ref currentStep, totalSteps);
+ }
+
+ // ?? Step 6: Eventi ??
+ if (options.DownloadEvents)
+ {
+ ReportProgress("Scaricamento eventi partite...");
+ EnrichWithEvents(table, options, statusCallback, progressCallback, ref currentStep, totalSteps);
+ }
+
+ // ?? Step 7: Formazioni ??
+ if (options.DownloadLineups)
+ {
+ ReportProgress("Scaricamento formazioni...");
+ EnrichWithLineups(table, options, statusCallback, progressCallback, ref currentStep, totalSteps);
+ }
+
+ // ?? Step 8: Statistiche ??
+ if (options.DownloadStatistics)
+ {
+ ReportProgress("Scaricamento statistiche partite...");
+ EnrichWithStatistics(table, options, statusCallback, progressCallback, ref currentStep, totalSteps);
+ }
+
+ // ?? Step 9: Infortuni ??
+ if (options.DownloadInjuries)
+ {
+ ReportProgress("Scaricamento infortunati...");
+ EnrichWithInjuries(table, date, options);
+ }
+
+ // ?? Completamento ??
progressCallback?.Report(100);
-
- statusCallback?.Report($"Trovate {table.Rows.Count} partite con quote");
+ string quotaMsg = _lastQuota.IsValid ? $" — {_lastQuota}" : "";
+ statusCallback?.Report($"Trovate {table.Rows.Count} partite{quotaMsg}");
return table;
}
catch (Exception ex)
@@ -134,6 +278,54 @@ namespace HorseRacingPredictor.Football
}
}
+ ///
+ /// Costruisce la lista degli step attivi in base alle opzioni per calcolare il progresso proporzionale.
+ ///
+ private static List BuildProgressSteps(FootballDownloadOptions options)
+ {
+ var steps = new List();
+ if (options.CheckQuota) steps.Add("quota");
+ if (options.DownloadFixtures) steps.Add("fixtures");
+ if (options.DownloadOdds) steps.Add("odds");
+ if (options.DownloadPredictions) steps.Add("predictions");
+ if (options.DownloadStandings) steps.Add("standings");
+ if (options.DownloadH2H) steps.Add("h2h");
+ if (options.DownloadEvents) steps.Add("events");
+ if (options.DownloadLineups) steps.Add("lineups");
+ if (options.DownloadStatistics) steps.Add("statistics");
+ if (options.DownloadInjuries) steps.Add("injuries");
+ return steps;
+ }
+
+ ///
+ /// Interroga l'endpoint /status per verificare la quota API residua.
+ /// Questa chiamata non conta nella quota giornaliera.
+ ///
+ public QuotaInfo CheckApiQuota()
+ {
+ var info = new QuotaInfo { IsValid = false };
+ try
+ {
+ var response = _statusAPI.GetStatus();
+ if (response == null || !response.IsSuccessful || string.IsNullOrEmpty(response.Content))
+ return info;
+
+ var json = JsonDocument.Parse(response.Content).RootElement;
+ if (json.TryGetProperty("response", out var resp) &&
+ resp.TryGetProperty("requests", out var reqs))
+ {
+ info.CurrentUsed = reqs.TryGetProperty("current", out var cur) ? cur.GetInt32() : 0;
+ info.DailyLimit = reqs.TryGetProperty("limit_day", out var lim) ? lim.GetInt32() : 100;
+ info.IsValid = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ _database.LogError("verifica quota API", ex);
+ }
+ return info;
+ }
+
///
/// Scarica i dati dalle API e li salva nella tabella di frontiera API_Response
///
@@ -276,19 +468,21 @@ namespace HorseRacingPredictor.Football
}
///
- /// Recupera le quote per la data specificata (potenzialmente più pagine) utilizzando API.Odds
+ /// Recupera le quote per la data specificata (potenzialmente più pagine) utilizzando API.Odds.
+ /// Usa i parametri dalle opzioni di download.
///
- private List GetOdds(DateTime date)
+ private List GetOdds(DateTime date, FootballDownloadOptions options = null)
{
+ int bookmakerId = options?.BookmakerId ?? 8;
+ int maxPages = options?.OddsMaxPages ?? 3;
var responses = new List();
int currentPage = 1;
- int maxPages = 3; // Limita a 3 pagine per evitare troppe chiamate API
bool hasMorePages = true;
while (hasMorePages && currentPage <= maxPages)
{
- var response = _oddsAPI.GetOddsByDate(date, 8, currentPage); // ID 8 = Bet365
+ var response = _oddsAPI.GetOddsByDate(date, bookmakerId, currentPage);
responses.Add(response);
// Controlla se ci sono altre pagine
@@ -587,33 +781,109 @@ namespace HorseRacingPredictor.Football
}
///
- /// Crea un DataTable vuoto per i fixture
+ /// Crea un DataTable vuoto per i fixture (retrocompatibile)
///
private DataTable CreateEmptyFixturesDataTable()
+ {
+ return CreateEmptyFixturesDataTable(new FootballDownloadOptions());
+ }
+
+ ///
+ /// Crea un DataTable vuoto per i fixture con colonne opzionali in base alle opzioni.
+ ///
+ private DataTable CreateEmptyFixturesDataTable(FootballDownloadOptions options)
{
var dataTable = new DataTable();
- // Aggiungi colonne
+ // Colonne base (sempre presenti)
dataTable.Columns.Add("ID", typeof(int));
dataTable.Columns.Add("Paese", typeof(string));
dataTable.Columns.Add("Campionato", typeof(string));
+ dataTable.Columns.Add("LeagueId", typeof(int));
dataTable.Columns.Add("Data / Ora", typeof(DateTime));
dataTable.Columns.Add("Stato", typeof(string));
dataTable.Columns.Add("Casa", typeof(string));
+ dataTable.Columns.Add("HomeTeamId", typeof(int));
dataTable.Columns.Add("Trasferta", typeof(string));
+ dataTable.Columns.Add("AwayTeamId", typeof(int));
dataTable.Columns.Add("Goals Casa", typeof(int));
dataTable.Columns.Add("Goals Trasferta", typeof(int));
- dataTable.Columns.Add("Quota Casa", typeof(string));
- dataTable.Columns.Add("Quota Pareggio", typeof(string));
- dataTable.Columns.Add("Quota Trasferta", typeof(string));
- dataTable.Columns.Add("Over 2.5", typeof(string));
- dataTable.Columns.Add("Under 2.5", typeof(string));
- dataTable.Columns.Add("BTTS Sì", typeof(string));
- dataTable.Columns.Add("BTTS No", typeof(string));
- dataTable.Columns.Add("Doppia Casa/X", typeof(string));
- dataTable.Columns.Add("Doppia Casa/Trasf", typeof(string));
- dataTable.Columns.Add("Doppia X/Trasf", typeof(string));
- dataTable.Columns.Add("Previsione", typeof(string));
+
+ // Colonne quote (se DownloadOdds attivo)
+ if (options.DownloadOdds)
+ {
+ dataTable.Columns.Add("Quota Casa", typeof(string));
+ dataTable.Columns.Add("Quota Pareggio", typeof(string));
+ dataTable.Columns.Add("Quota Trasferta", typeof(string));
+ dataTable.Columns.Add("Over 2.5", typeof(string));
+ dataTable.Columns.Add("Under 2.5", typeof(string));
+ dataTable.Columns.Add("BTTS Sì", typeof(string));
+ dataTable.Columns.Add("BTTS No", typeof(string));
+ dataTable.Columns.Add("Doppia Casa/X", typeof(string));
+ dataTable.Columns.Add("Doppia Casa/Trasf", typeof(string));
+ dataTable.Columns.Add("Doppia X/Trasf", typeof(string));
+ }
+
+ // Colonna previsione (se DownloadPredictions attivo)
+ if (options.DownloadPredictions)
+ {
+ dataTable.Columns.Add("Previsione", typeof(string));
+ dataTable.Columns.Add("Consiglio", typeof(string));
+ dataTable.Columns.Add("% Vittoria Casa", typeof(string));
+ dataTable.Columns.Add("% Pareggio", typeof(string));
+ dataTable.Columns.Add("% Vittoria Trasferta", typeof(string));
+ }
+
+ // Colonne classifica (se DownloadStandings attivo)
+ if (options.DownloadStandings)
+ {
+ dataTable.Columns.Add("Pos. Casa", typeof(int));
+ dataTable.Columns.Add("Punti Casa", typeof(int));
+ dataTable.Columns.Add("Pos. Trasferta", typeof(int));
+ dataTable.Columns.Add("Punti Trasferta", typeof(int));
+ }
+
+ // Colonne H2H (se DownloadH2H attivo)
+ if (options.DownloadH2H)
+ {
+ dataTable.Columns.Add("H2H Vitt. Casa", typeof(int));
+ dataTable.Columns.Add("H2H Pareggi", typeof(int));
+ dataTable.Columns.Add("H2H Vitt. Trasf.", typeof(int));
+ dataTable.Columns.Add("H2H Totali", typeof(int));
+ }
+
+ // Colonne eventi (se DownloadEvents attivo)
+ if (options.DownloadEvents)
+ {
+ dataTable.Columns.Add("N. Eventi", typeof(int));
+ dataTable.Columns.Add("Cartellini Casa", typeof(int));
+ dataTable.Columns.Add("Cartellini Trasf.", typeof(int));
+ }
+
+ // Colonne formazioni (se DownloadLineups attivo)
+ if (options.DownloadLineups)
+ {
+ dataTable.Columns.Add("Modulo Casa", typeof(string));
+ dataTable.Columns.Add("Modulo Trasferta", typeof(string));
+ }
+
+ // Colonne statistiche (se DownloadStatistics attivo)
+ if (options.DownloadStatistics)
+ {
+ dataTable.Columns.Add("Possesso Casa", typeof(string));
+ dataTable.Columns.Add("Possesso Trasf.", typeof(string));
+ dataTable.Columns.Add("Tiri Porta Casa", typeof(int));
+ dataTable.Columns.Add("Tiri Porta Trasf.", typeof(int));
+ dataTable.Columns.Add("Corner Casa", typeof(int));
+ dataTable.Columns.Add("Corner Trasf.", typeof(int));
+ }
+
+ // Colonne infortuni (se DownloadInjuries attivo)
+ if (options.DownloadInjuries)
+ {
+ dataTable.Columns.Add("Infortunati Casa", typeof(int));
+ dataTable.Columns.Add("Infortunati Trasf.", typeof(int));
+ }
return dataTable;
}
@@ -621,9 +891,10 @@ namespace HorseRacingPredictor.Football
///
/// Crea un DataTable con le partite dalla risposta API
///
- private DataTable CreateFixturesDataTable(RestResponse response)
+ private DataTable CreateFixturesDataTable(RestResponse response, FootballDownloadOptions options = null)
{
- var dataTable = CreateEmptyFixturesDataTable();
+ options ??= new FootballDownloadOptions();
+ var dataTable = CreateEmptyFixturesDataTable(options);
// Verifica che la risposta sia valida
if (response == null || !response.IsSuccessful || string.IsNullOrEmpty(response.Content))
@@ -654,11 +925,19 @@ namespace HorseRacingPredictor.Football
continue;
}
+ // Filtra per league IDs se specificato
+ int leagueId = leagueEl.TryGetProperty("id", out var lIdEl) ? lIdEl.GetInt32() : 0;
+ if (options.LeagueIds.Count > 0 && !options.LeagueIds.Contains(leagueId))
+ {
+ continue;
+ }
+
var row = dataTable.NewRow();
row["ID"] = fixtureEl.TryGetProperty("id", out var idEl) ? idEl.GetInt32() : 0;
row["Paese"] = leagueEl.TryGetProperty("country", out var countryEl) ? countryEl.GetString() ?? "" : "";
row["Campionato"] = leagueEl.TryGetProperty("name", out var leagueNameEl) ? leagueNameEl.GetString() ?? "" : "";
+ row["LeagueId"] = leagueId;
if (fixtureEl.TryGetProperty("date", out var dateEl) && dateEl.ValueKind == JsonValueKind.String)
{
@@ -674,12 +953,22 @@ namespace HorseRacingPredictor.Football
statusEl.TryGetProperty("long", out var statusLong)
? statusLong.GetString() ?? "" : "";
- row["Casa"] = teamsEl.TryGetProperty("home", out var homeEl) &&
- homeEl.TryGetProperty("name", out var homeNameEl)
- ? homeNameEl.GetString() ?? "" : "";
- row["Trasferta"] = teamsEl.TryGetProperty("away", out var awayEl) &&
- awayEl.TryGetProperty("name", out var awayNameEl)
- ? awayNameEl.GetString() ?? "" : "";
+ int homeTeamId = 0;
+ int awayTeamId = 0;
+
+ if (teamsEl.TryGetProperty("home", out var homeEl))
+ {
+ row["Casa"] = homeEl.TryGetProperty("name", out var homeNameEl) ? homeNameEl.GetString() ?? "" : "";
+ homeTeamId = homeEl.TryGetProperty("id", out var htIdEl) ? htIdEl.GetInt32() : 0;
+ }
+ row["HomeTeamId"] = homeTeamId;
+
+ if (teamsEl.TryGetProperty("away", out var awayEl))
+ {
+ row["Trasferta"] = awayEl.TryGetProperty("name", out var awayNameEl) ? awayNameEl.GetString() ?? "" : "";
+ awayTeamId = awayEl.TryGetProperty("id", out var atIdEl) ? atIdEl.GetInt32() : 0;
+ }
+ row["AwayTeamId"] = awayTeamId;
// Goals (possono essere null per partite non iniziate)
if (item.TryGetProperty("goals", out var goalsElement))
@@ -695,18 +984,6 @@ namespace HorseRacingPredictor.Football
row["Goals Trasferta"] = 0;
}
- row["Quota Casa"] = DBNull.Value;
- row["Quota Pareggio"] = DBNull.Value;
- row["Quota Trasferta"] = DBNull.Value;
- row["Over 2.5"] = DBNull.Value;
- row["Under 2.5"] = DBNull.Value;
- row["BTTS Sì"] = DBNull.Value;
- row["BTTS No"] = DBNull.Value;
- row["Doppia Casa/X"] = DBNull.Value;
- row["Doppia Casa/Trasf"] = DBNull.Value;
- row["Doppia X/Trasf"] = DBNull.Value;
- row["Previsione"] = DBNull.Value;
-
dataTable.Rows.Add(row);
}
catch (Exception ex)
@@ -875,9 +1152,9 @@ namespace HorseRacingPredictor.Football
///
/// Analizza le risposte delle quote dall'API e le inserisce direttamente nella DataTable delle partite.
- /// Utilizza Bet365 (ID 8) come bookmaker principale; se non presente, usa il primo bookmaker disponibile.
+ /// Utilizza il bookmaker specificato come principale; se non presente, usa il primo bookmaker disponibile.
///
- private void ParseOddsIntoTable(DataTable fixturesTable, List oddsResponses)
+ private void ParseOddsIntoTable(DataTable fixturesTable, List oddsResponses, int bookmakerId = 8)
{
if (oddsResponses == null || oddsResponses.Count == 0 || fixturesTable == null || fixturesTable.Rows.Count == 0)
return;
@@ -911,13 +1188,13 @@ namespace HorseRacingPredictor.Football
if (!item.TryGetProperty("bookmakers", out var bookmakersEl))
continue;
- // Cerca Bet365 (ID 8), altrimenti usa il primo bookmaker
+ // Cerca il bookmaker specificato, altrimenti usa il primo disponibile
JsonElement selectedBookmaker = default;
bool found = false;
foreach (var bm in bookmakersEl.EnumerateArray())
{
- if (bm.TryGetProperty("id", out var bmId) && bmId.GetInt32() == 8)
+ if (bm.TryGetProperty("id", out var bmId) && bmId.GetInt32() == bookmakerId)
{
selectedBookmaker = bm;
found = true;
@@ -1089,5 +1366,517 @@ namespace HorseRacingPredictor.Football
return CreateEmptyFixturesDataTable();
}
}
+
+ #region Enrichment Methods
+
+ ///
+ /// Arricchisce la tabella con le previsioni per ogni partita.
+ ///
+ private void EnrichWithPredictions(DataTable table, RestResponse fixturesResponse, FootballDownloadOptions options,
+ IProgress statusCallback, IProgress progressCallback, ref int currentStep, int totalSteps)
+ {
+ if (!table.Columns.Contains("Previsione")) return;
+
+ try
+ {
+ if (fixturesResponse == null || !fixturesResponse.IsSuccessful || string.IsNullOrEmpty(fixturesResponse.Content))
+ return;
+
+ var json = JsonDocument.Parse(fixturesResponse.Content).RootElement;
+ if (!json.TryGetProperty("response", out var responseElement))
+ return;
+
+ int count = 0;
+ int max = options.MaxFixturesForDetails > 0
+ ? Math.Min(table.Rows.Count, options.MaxFixturesForDetails)
+ : table.Rows.Count;
+
+ foreach (DataRow row in table.Rows)
+ {
+ if (count >= max) break;
+ count++;
+
+ try
+ {
+ int fixtureId = Convert.ToInt32(row["ID"]);
+ statusCallback?.Report($"Previsione {count}/{max} (fixture {fixtureId})...");
+
+ var predResponse = _predictionManager.GetPredictionByFixture(fixtureId);
+ if (predResponse == null || !predResponse.IsSuccessful || string.IsNullOrEmpty(predResponse.Content))
+ continue;
+
+ var predJson = JsonDocument.Parse(predResponse.Content).RootElement;
+ if (!predJson.TryGetProperty("response", out var predResp))
+ continue;
+
+ foreach (var pred in predResp.EnumerateArray())
+ {
+ if (pred.TryGetProperty("predictions", out var preds))
+ {
+ if (preds.TryGetProperty("winner", out var winner) &&
+ winner.TryGetProperty("name", out var winnerName))
+ {
+ row["Previsione"] = winnerName.GetString() ?? "";
+ }
+
+ if (preds.TryGetProperty("advice", out var advice))
+ {
+ row["Consiglio"] = advice.GetString() ?? "";
+ }
+
+ if (preds.TryGetProperty("percent", out var pct))
+ {
+ row["% Vittoria Casa"] = pct.TryGetProperty("home", out var ph) ? ph.GetString() ?? "" : "";
+ row["% Pareggio"] = pct.TryGetProperty("draw", out var pd) ? pd.GetString() ?? "" : "";
+ row["% Vittoria Trasferta"] = pct.TryGetProperty("away", out var pa) ? pa.GetString() ?? "" : "";
+ }
+ }
+ break; // solo il primo risultato
+ }
+
+ if (options.ApiDelayMs > 0)
+ System.Threading.Thread.Sleep(options.ApiDelayMs);
+ }
+ catch (Exception ex)
+ {
+ _database.LogError($"enrichment previsione", ex);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _database.LogError("enrichment previsioni", ex);
+ }
+ }
+
+ ///
+ /// Arricchisce la tabella con le classifiche dei campionati.
+ ///
+ private void EnrichWithStandings(DataTable table, FootballDownloadOptions options)
+ {
+ if (!table.Columns.Contains("Pos. Casa")) return;
+
+ try
+ {
+ int season = options.GetEffectiveSeason();
+
+ // Raccogli le leghe uniche dalla tabella
+ var leagueIds = new HashSet();
+ foreach (DataRow row in table.Rows)
+ {
+ if (row["LeagueId"] != DBNull.Value)
+ leagueIds.Add(Convert.ToInt32(row["LeagueId"]));
+ }
+
+ // Cache delle classifiche: leagueId -> { teamId -> (rank, points) }
+ var standingsCache = new Dictionary>();
+
+ foreach (int leagueId in leagueIds)
+ {
+ try
+ {
+ var response = _standingsAPI.GetStandings(leagueId, season);
+ if (response == null || !response.IsSuccessful || string.IsNullOrEmpty(response.Content))
+ continue;
+
+ var json = JsonDocument.Parse(response.Content).RootElement;
+ if (!json.TryGetProperty("response", out var resp)) continue;
+
+ var teamMap = new Dictionary();
+
+ foreach (var leagueItem in resp.EnumerateArray())
+ {
+ if (!leagueItem.TryGetProperty("league", out var lgEl) ||
+ !lgEl.TryGetProperty("standings", out var standingsEl))
+ continue;
+
+ foreach (var group in standingsEl.EnumerateArray())
+ {
+ foreach (var entry in group.EnumerateArray())
+ {
+ if (entry.TryGetProperty("team", out var teamEl) &&
+ teamEl.TryGetProperty("id", out var tIdEl))
+ {
+ int teamId = tIdEl.GetInt32();
+ int rank = entry.TryGetProperty("rank", out var rkEl) ? rkEl.GetInt32() : 0;
+ int points = entry.TryGetProperty("points", out var ptEl) ? ptEl.GetInt32() : 0;
+ teamMap[teamId] = (rank, points);
+ }
+ }
+ }
+ }
+
+ standingsCache[leagueId] = teamMap;
+
+ if (options.ApiDelayMs > 0)
+ System.Threading.Thread.Sleep(options.ApiDelayMs);
+ }
+ catch (Exception ex)
+ {
+ _database.LogError($"enrichment classifica lega {leagueId}", ex);
+ }
+ }
+
+ // Applica alla tabella
+ foreach (DataRow row in table.Rows)
+ {
+ if (row["LeagueId"] == DBNull.Value) continue;
+ int lgId = Convert.ToInt32(row["LeagueId"]);
+ int homeId = row["HomeTeamId"] != DBNull.Value ? Convert.ToInt32(row["HomeTeamId"]) : 0;
+ int awayId = row["AwayTeamId"] != DBNull.Value ? Convert.ToInt32(row["AwayTeamId"]) : 0;
+
+ if (standingsCache.TryGetValue(lgId, out var map))
+ {
+ if (map.TryGetValue(homeId, out var homeSt))
+ {
+ row["Pos. Casa"] = homeSt.rank;
+ row["Punti Casa"] = homeSt.points;
+ }
+ if (map.TryGetValue(awayId, out var awaySt))
+ {
+ row["Pos. Trasferta"] = awaySt.rank;
+ row["Punti Trasferta"] = awaySt.points;
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _database.LogError("enrichment classifiche", ex);
+ }
+ }
+
+ ///
+ /// Arricchisce la tabella con i dati degli scontri diretti.
+ ///
+ private void EnrichWithH2H(DataTable table, FootballDownloadOptions options,
+ IProgress statusCallback, IProgress progressCallback, ref int currentStep, int totalSteps)
+ {
+ if (!table.Columns.Contains("H2H Vitt. Casa")) return;
+
+ try
+ {
+ int count = 0;
+ int max = options.MaxFixturesForDetails > 0
+ ? Math.Min(table.Rows.Count, options.MaxFixturesForDetails)
+ : table.Rows.Count;
+
+ foreach (DataRow row in table.Rows)
+ {
+ if (count >= max) break;
+ count++;
+
+ try
+ {
+ int homeId = row["HomeTeamId"] != DBNull.Value ? Convert.ToInt32(row["HomeTeamId"]) : 0;
+ int awayId = row["AwayTeamId"] != DBNull.Value ? Convert.ToInt32(row["AwayTeamId"]) : 0;
+ if (homeId == 0 || awayId == 0) continue;
+
+ statusCallback?.Report($"H2H {count}/{max} ({row["Casa"]} vs {row["Trasferta"]})...");
+
+ var response = _h2hAPI.GetH2H(homeId, awayId, 10);
+ if (response == null || !response.IsSuccessful || string.IsNullOrEmpty(response.Content))
+ continue;
+
+ var json = JsonDocument.Parse(response.Content).RootElement;
+ if (!json.TryGetProperty("response", out var resp)) continue;
+
+ int homeWins = 0, draws = 0, awayWins = 0, total = 0;
+ foreach (var match in resp.EnumerateArray())
+ {
+ total++;
+ if (!match.TryGetProperty("teams", out var teams)) continue;
+
+ bool? homeWin = null;
+ if (teams.TryGetProperty("home", out var hTeam) &&
+ hTeam.TryGetProperty("winner", out var hWin) && hWin.ValueKind == JsonValueKind.True)
+ homeWin = true;
+ if (teams.TryGetProperty("away", out var aTeam) &&
+ aTeam.TryGetProperty("winner", out var aWin) && aWin.ValueKind == JsonValueKind.True)
+ homeWin = false;
+
+ if (homeWin == true) homeWins++;
+ else if (homeWin == false) awayWins++;
+ else draws++;
+ }
+
+ row["H2H Vitt. Casa"] = homeWins;
+ row["H2H Pareggi"] = draws;
+ row["H2H Vitt. Trasf."] = awayWins;
+ row["H2H Totali"] = total;
+
+ if (options.ApiDelayMs > 0)
+ System.Threading.Thread.Sleep(options.ApiDelayMs);
+ }
+ catch (Exception ex)
+ {
+ _database.LogError($"enrichment H2H", ex);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _database.LogError("enrichment H2H globale", ex);
+ }
+ }
+
+ ///
+ /// Arricchisce la tabella con il conteggio eventi (gol, cartellini).
+ ///
+ private void EnrichWithEvents(DataTable table, FootballDownloadOptions options,
+ IProgress statusCallback, IProgress progressCallback, ref int currentStep, int totalSteps)
+ {
+ if (!table.Columns.Contains("N. Eventi")) return;
+
+ try
+ {
+ int count = 0;
+ int max = options.MaxFixturesForDetails > 0
+ ? Math.Min(table.Rows.Count, options.MaxFixturesForDetails)
+ : table.Rows.Count;
+
+ foreach (DataRow row in table.Rows)
+ {
+ if (count >= max) break;
+ count++;
+
+ try
+ {
+ int fixtureId = Convert.ToInt32(row["ID"]);
+ statusCallback?.Report($"Eventi {count}/{max} (fixture {fixtureId})...");
+
+ var response = _eventsAPI.GetEventsByFixture(fixtureId);
+ if (response == null || !response.IsSuccessful || string.IsNullOrEmpty(response.Content))
+ continue;
+
+ var json = JsonDocument.Parse(response.Content).RootElement;
+ if (!json.TryGetProperty("response", out var resp)) continue;
+
+ int homeTeamId = row["HomeTeamId"] != DBNull.Value ? Convert.ToInt32(row["HomeTeamId"]) : 0;
+ int totalEvents = 0, homeCards = 0, awayCards = 0;
+
+ foreach (var evt in resp.EnumerateArray())
+ {
+ totalEvents++;
+ if (evt.TryGetProperty("type", out var typeEl))
+ {
+ string evtType = typeEl.GetString() ?? "";
+ if (evtType == "Card")
+ {
+ bool isHome = evt.TryGetProperty("team", out var tEl) &&
+ tEl.TryGetProperty("id", out var tIdEl) &&
+ tIdEl.GetInt32() == homeTeamId;
+ if (isHome) homeCards++;
+ else awayCards++;
+ }
+ }
+ }
+
+ row["N. Eventi"] = totalEvents;
+ row["Cartellini Casa"] = homeCards;
+ row["Cartellini Trasf."] = awayCards;
+
+ if (options.ApiDelayMs > 0)
+ System.Threading.Thread.Sleep(options.ApiDelayMs);
+ }
+ catch (Exception ex)
+ {
+ _database.LogError($"enrichment eventi", ex);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _database.LogError("enrichment eventi globale", ex);
+ }
+ }
+
+ ///
+ /// Arricchisce la tabella con i moduli tattici (formazione).
+ ///
+ private void EnrichWithLineups(DataTable table, FootballDownloadOptions options,
+ IProgress statusCallback, IProgress progressCallback, ref int currentStep, int totalSteps)
+ {
+ if (!table.Columns.Contains("Modulo Casa")) return;
+
+ try
+ {
+ int count = 0;
+ int max = options.MaxFixturesForDetails > 0
+ ? Math.Min(table.Rows.Count, options.MaxFixturesForDetails)
+ : table.Rows.Count;
+
+ foreach (DataRow row in table.Rows)
+ {
+ if (count >= max) break;
+ count++;
+
+ try
+ {
+ int fixtureId = Convert.ToInt32(row["ID"]);
+ statusCallback?.Report($"Formazioni {count}/{max} (fixture {fixtureId})...");
+
+ var response = _lineupsAPI.GetLineupsByFixture(fixtureId);
+ if (response == null || !response.IsSuccessful || string.IsNullOrEmpty(response.Content))
+ continue;
+
+ var json = JsonDocument.Parse(response.Content).RootElement;
+ if (!json.TryGetProperty("response", out var resp)) continue;
+
+ int homeTeamId = row["HomeTeamId"] != DBNull.Value ? Convert.ToInt32(row["HomeTeamId"]) : 0;
+
+ foreach (var lineup in resp.EnumerateArray())
+ {
+ string formation = lineup.TryGetProperty("formation", out var fEl) ? fEl.GetString() ?? "" : "";
+ bool isHome = lineup.TryGetProperty("team", out var tEl) &&
+ tEl.TryGetProperty("id", out var tIdEl) &&
+ tIdEl.GetInt32() == homeTeamId;
+
+ if (isHome) row["Modulo Casa"] = formation;
+ else row["Modulo Trasferta"] = formation;
+ }
+
+ if (options.ApiDelayMs > 0)
+ System.Threading.Thread.Sleep(options.ApiDelayMs);
+ }
+ catch (Exception ex)
+ {
+ _database.LogError($"enrichment formazioni", ex);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _database.LogError("enrichment formazioni globale", ex);
+ }
+ }
+
+ ///
+ /// Arricchisce la tabella con le statistiche delle partite (possesso, tiri, corner).
+ ///
+ private void EnrichWithStatistics(DataTable table, FootballDownloadOptions options,
+ IProgress statusCallback, IProgress progressCallback, ref int currentStep, int totalSteps)
+ {
+ if (!table.Columns.Contains("Possesso Casa")) return;
+
+ try
+ {
+ int count = 0;
+ int max = options.MaxFixturesForDetails > 0
+ ? Math.Min(table.Rows.Count, options.MaxFixturesForDetails)
+ : table.Rows.Count;
+
+ foreach (DataRow row in table.Rows)
+ {
+ if (count >= max) break;
+ count++;
+
+ try
+ {
+ int fixtureId = Convert.ToInt32(row["ID"]);
+ statusCallback?.Report($"Statistiche {count}/{max} (fixture {fixtureId})...");
+
+ var response = _fixtureStatsAPI.GetStatisticsByFixture(fixtureId);
+ if (response == null || !response.IsSuccessful || string.IsNullOrEmpty(response.Content))
+ continue;
+
+ var json = JsonDocument.Parse(response.Content).RootElement;
+ if (!json.TryGetProperty("response", out var resp)) continue;
+
+ int homeTeamId = row["HomeTeamId"] != DBNull.Value ? Convert.ToInt32(row["HomeTeamId"]) : 0;
+
+ foreach (var teamStats in resp.EnumerateArray())
+ {
+ bool isHome = teamStats.TryGetProperty("team", out var tEl) &&
+ tEl.TryGetProperty("id", out var tIdEl) &&
+ tIdEl.GetInt32() == homeTeamId;
+
+ if (!teamStats.TryGetProperty("statistics", out var statsArr)) continue;
+
+ foreach (var stat in statsArr.EnumerateArray())
+ {
+ string type = stat.TryGetProperty("type", out var stEl) ? stEl.GetString() ?? "" : "";
+ string value = stat.TryGetProperty("value", out var vEl) && vEl.ValueKind != JsonValueKind.Null
+ ? vEl.ToString() : "";
+
+ switch (type)
+ {
+ case "Ball Possession":
+ if (isHome) row["Possesso Casa"] = value;
+ else row["Possesso Trasf."] = value;
+ break;
+ case "Shots on Goal":
+ int shots = int.TryParse(value, out var sv) ? sv : 0;
+ if (isHome) row["Tiri Porta Casa"] = shots;
+ else row["Tiri Porta Trasf."] = shots;
+ break;
+ case "Corner Kicks":
+ int corners = int.TryParse(value, out var cv) ? cv : 0;
+ if (isHome) row["Corner Casa"] = corners;
+ else row["Corner Trasf."] = corners;
+ break;
+ }
+ }
+ }
+
+ if (options.ApiDelayMs > 0)
+ System.Threading.Thread.Sleep(options.ApiDelayMs);
+ }
+ catch (Exception ex)
+ {
+ _database.LogError($"enrichment statistiche", ex);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _database.LogError("enrichment statistiche globale", ex);
+ }
+ }
+
+ ///
+ /// Arricchisce la tabella con il conteggio degli infortunati per squadra.
+ ///
+ private void EnrichWithInjuries(DataTable table, DateTime date, FootballDownloadOptions options)
+ {
+ if (!table.Columns.Contains("Infortunati Casa")) return;
+
+ try
+ {
+ var response = _injuriesAPI.GetInjuriesByDate(date);
+ if (response == null || !response.IsSuccessful || string.IsNullOrEmpty(response.Content))
+ return;
+
+ var json = JsonDocument.Parse(response.Content).RootElement;
+ if (!json.TryGetProperty("response", out var resp)) return;
+
+ // Mappa: teamId -> count di infortunati
+ var injuryCount = new Dictionary();
+ foreach (var injury in resp.EnumerateArray())
+ {
+ if (injury.TryGetProperty("team", out var tEl) &&
+ tEl.TryGetProperty("id", out var tIdEl))
+ {
+ int teamId = tIdEl.GetInt32();
+ injuryCount[teamId] = injuryCount.GetValueOrDefault(teamId) + 1;
+ }
+ }
+
+ foreach (DataRow row in table.Rows)
+ {
+ int homeId = row["HomeTeamId"] != DBNull.Value ? Convert.ToInt32(row["HomeTeamId"]) : 0;
+ int awayId = row["AwayTeamId"] != DBNull.Value ? Convert.ToInt32(row["AwayTeamId"]) : 0;
+
+ row["Infortunati Casa"] = injuryCount.GetValueOrDefault(homeId);
+ row["Infortunati Trasf."] = injuryCount.GetValueOrDefault(awayId);
+ }
+ }
+ catch (Exception ex)
+ {
+ _database.LogError("enrichment infortuni", ex);
+ }
+ }
+
+ #endregion
}
}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/HorseRacingPredictor/UserSettings.cs b/HorseRacingPredictor/HorseRacingPredictor/HorseRacingPredictor/UserSettings.cs
index 910c10a..5b81a69 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/HorseRacingPredictor/UserSettings.cs
+++ b/HorseRacingPredictor/HorseRacingPredictor/HorseRacingPredictor/UserSettings.cs
@@ -21,7 +21,7 @@ namespace HorseRacingPredictor
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
- // ?? Football ????????????????????????????????????????????
+ // ?? Football ??????????????????????????????????????????
public string ApiKey { get; set; } = string.Empty;
public string FbExportPath { get; set; } = string.Empty;
public string FbPrefix { get; set; } = string.Empty;
@@ -30,7 +30,27 @@ namespace HorseRacingPredictor
public string FbDateFormat { get; set; } = "yyyy-MM-dd";
public string FbFormat { get; set; } = "CSV";
- // ?? Racing ??????????????????????????????????????????????
+ // ?? Football Download Options ????????????????????????????
+ public bool FbDownloadFixtures { get; set; } = true;
+ public bool FbDownloadOdds { get; set; } = true;
+ public bool FbDownloadPredictions { get; set; } = true;
+ public bool FbDownloadStandings { get; set; } = false;
+ public bool FbDownloadH2H { get; set; } = false;
+ public bool FbDownloadEvents { get; set; } = false;
+ public bool FbDownloadLineups { get; set; } = false;
+ public bool FbDownloadStatistics { get; set; } = false;
+ public bool FbDownloadInjuries { get; set; } = false;
+ public List FbLeagueIds { get; set; } = new();
+ public int FbBookmakerId { get; set; } = 8;
+ public int FbOddsMaxPages { get; set; } = 3;
+ public string FbTimezone { get; set; } = "Europe/Rome";
+ public int FbSeason { get; set; } = 0;
+ public int FbMaxFixturesForDetails { get; set; } = 50;
+ public int FbApiDelayMs { get; set; } = 300;
+ public bool FbCheckQuota { get; set; } = true;
+ public int FbMinRemainingQuota { get; set; } = 10;
+
+ // ?? Racing ???????????????????????????????????????????????
public string RacingApiKey { get; set; } = string.Empty;
public string RcExportPath { get; set; } = string.Empty;
public string RcPrefix { get; set; } = string.Empty;
@@ -41,7 +61,35 @@ namespace HorseRacingPredictor
public string RcTimezone { get; set; } = "Australia/Sydney";
public List RcCountries { get; set; } = new() { "au", "nz" };
- // ?? Persistence ?????????????????????????????????????????
+ // ?? Persistence ??????????????????????????????????????
+
+ ///
+ /// Costruisce un FootballDownloadOptions a partire dalle impostazioni salvate.
+ ///
+ public Football.FootballDownloadOptions ToFootballDownloadOptions()
+ {
+ return new Football.FootballDownloadOptions
+ {
+ DownloadFixtures = FbDownloadFixtures,
+ DownloadOdds = FbDownloadOdds,
+ DownloadPredictions = FbDownloadPredictions,
+ DownloadStandings = FbDownloadStandings,
+ DownloadH2H = FbDownloadH2H,
+ DownloadEvents = FbDownloadEvents,
+ DownloadLineups = FbDownloadLineups,
+ DownloadStatistics = FbDownloadStatistics,
+ DownloadInjuries = FbDownloadInjuries,
+ LeagueIds = new List(FbLeagueIds),
+ BookmakerId = FbBookmakerId,
+ OddsMaxPages = FbOddsMaxPages,
+ Timezone = FbTimezone,
+ Season = FbSeason,
+ MaxFixturesForDetails = FbMaxFixturesForDetails,
+ ApiDelayMs = FbApiDelayMs,
+ CheckQuota = FbCheckQuota,
+ MinRemainingQuota = FbMinRemainingQuota
+ };
+ }
public static UserSettings Load()
{
diff --git a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml
index c85f52a..d16c7ba 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml
+++ b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml
@@ -765,6 +765,85 @@
Foreground="{StaticResource BrText}"
Click="btnBrowseFbExport_Click"/>
+
+
+
+
+
+
+ Partite
+ Quote
+ Previsioni
+ Classifiche
+ Scontri diretti
+ Eventi
+ Formazioni
+ Statistiche
+ Infortuni
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Controlla quota
+
+
+
+
+
+
+
+
+
diff --git a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs
index b925c73..b92950f 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs
+++ b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs
@@ -266,23 +266,32 @@ namespace HorseRacingPredictor
try
{
pbFootball.Value = 0;
- lblStatusFb.Text = "Scaricamento elenco partite…";
+ lblStatusFb.Text = "Preparazione opzioni di download…";
btnDownloadFb.IsEnabled = false;
dpFootball.IsEnabled = false;
btnExportFbCsv.IsEnabled = false;
+ // Costruisci le opzioni di download dalla UI
+ var options = BuildFootballDownloadOptions();
+
var progress = new Progress(v => pbFootball.Value = v);
var status = new Progress(s => lblStatusFb.Text = s);
var table = await Task.Run(() =>
- _footballManager.GetTodayFixtures(date, progress, status));
+ _footballManager.GetTodayFixtures(date, options, progress, status));
_footballData = table;
// Ensure the start time column exists and populate it (no timezone label)
InjectRomeStartTimeColumn(_footballData, "Inizio");
+ // Nascondi le colonne interne (ID di supporto) dal DataGrid
dgFootball.ItemsSource = _footballData?.DefaultView;
+ dgFootball.AutoGeneratingColumn += (s, e) =>
+ {
+ if (e.PropertyName is "LeagueId" or "HomeTeamId" or "AwayTeamId")
+ e.Cancel = true;
+ };
if (_footballData != null && _footballData.Rows.Count > 0)
{
@@ -847,6 +856,26 @@ 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;
+ txtFbBookmakerId.Text = s.FbBookmakerId.ToString();
+ txtFbOddsMaxPages.Text = s.FbOddsMaxPages.ToString();
+ txtFbMaxFixtures.Text = s.FbMaxFixturesForDetails.ToString();
+ if (txtFbTimezone != null) txtFbTimezone.Text = s.FbTimezone;
+ txtFbLeagueIds.Text = s.FbLeagueIds.Count > 0 ? string.Join(",", s.FbLeagueIds) : "";
+ chkFbCheckQuota.IsChecked = s.FbCheckQuota;
+ txtFbMinQuota.Text = s.FbMinRemainingQuota.ToString();
+ txtFbApiDelay.Text = s.FbApiDelayMs.ToString();
+
+ // Racing
txtRcExportPath.Text = s.RcExportPath;
txtRcPrefix.Text = s.RcPrefix;
txtRcSuffix.Text = s.RcSuffix;
@@ -1091,6 +1120,10 @@ namespace HorseRacingPredictor
{
try
{
+ // Costruisci e salva le opzioni di download in un FootballDownloadOptions
+ // per validazione prima del salvataggio
+ var options = BuildFootballDownloadOptions();
+
var s = new UserSettings
{
ApiKey = txtApiKey.Text.Trim(),
@@ -1100,6 +1133,25 @@ namespace HorseRacingPredictor
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,
+ 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",
+ FbLeagueIds = ParseIntList(txtFbLeagueIds?.Text),
+ FbCheckQuota = chkFbCheckQuota.IsChecked == true,
+ FbMinRemainingQuota = int.TryParse(txtFbMinQuota.Text.Trim(), out var mq) ? mq : 10,
+ FbApiDelayMs = int.TryParse(txtFbApiDelay.Text.Trim(), out var ad) ? ad : 300,
+ // Racing
RcExportPath = txtRcExportPath.Text.Trim(),
RcPrefix = txtRcPrefix.Text.Trim(),
RcSuffix = txtRcSuffix.Text.Trim(),
@@ -1126,6 +1178,50 @@ namespace HorseRacingPredictor
}
}
+ // ???????????? FOOTBALL DOWNLOAD OPTIONS HELPERS ????????????
+
+ ///
+ /// Costruisce un FootballDownloadOptions leggendo i valori dalla UI.
+ ///
+ private Football.FootballDownloadOptions BuildFootballDownloadOptions()
+ {
+ 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,
+ 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",
+ LeagueIds = ParseIntList(txtFbLeagueIds?.Text),
+ CheckQuota = chkFbCheckQuota.IsChecked == true,
+ MinRemainingQuota = int.TryParse(txtFbMinQuota.Text.Trim(), out var mq) ? mq : 10,
+ ApiDelayMs = int.TryParse(txtFbApiDelay.Text.Trim(), out var ad) ? ad : 300
+ };
+ }
+
+ ///
+ /// Analizza una stringa di interi separati da virgola e restituisce una lista.
+ ///
+ private static List ParseIntList(string text)
+ {
+ var result = new List();
+ if (string.IsNullOrWhiteSpace(text)) return result;
+ foreach (var part in text.Split(',', ';', ' '))
+ {
+ if (int.TryParse(part.Trim(), out var val))
+ result.Add(val);
+ }
+ return result;
+ }
+
// ???????????????????????? VIRTUAL FOOTBALL ????????????????????????
private void btnVfbNavigate_Click(object sender, RoutedEventArgs e)