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)