Files
Tritone/HorseRacingPredictor/HorseRacingPredictor/Football/Main.cs
T
Alby96 0d3769db79 Aggiunta download CSV supplementari e restyling impostazioni
- Introdotto supporto per download dati supplementari Calcio (statistiche giocatori, squadre, marcatori, assist, cartellini, rose, allenatori, trasferimenti) con esportazione CSV separati
- Nuova classe SupplementaryDataExporter e client API dedicati per ogni endpoint
- UI impostazioni completamente rinnovata: selezione dinamica endpoint/supplementari, sorgente dati (API/CSV), parametri avanzati
- Migliorata dashboard con stat card, header e sidebar moderni
- Gestione errori globali migliorata
- Refactoring: rimosso codice VirtualFootball/WebView2, unificato caricamento CSV, gestione timezone con ComboBox IANA
- Aggiornato .gitignore e aggiunto template appsettings per sicurezza/configurazione
2026-04-14 18:07:31 +02:00

1918 lines
88 KiB
C#

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RestSharp;
using System.Text.Json;
using System.Text.Json.Nodes;
using Newtonsoft.Json.Linq;
using Microsoft.Data.SqlClient;
namespace HorseRacingPredictor.Football
{
/// <summary>
/// Informazioni sulla quota API residua.
/// </summary>
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)";
}
}
/// <summary>
/// Classe centralizzata per la gestione delle API di Football
/// </summary>
public class Main
{
private readonly Manager.Database _database;
private readonly API.Prediction _predictionManager;
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;
private readonly Football.Database.Team _teamRepository;
private readonly Football.Database.Goals _goalsRepository;
private readonly Football.Database.Score _scoreRepository;
private readonly Football.Database.Odds _oddsRepository;
private readonly Football.Database.Prediction _predictionRepository;
// Nuovi repository aggiunti
private readonly Football.Database.BetType _betTypeRepository;
private readonly Football.Database.Bookmaker _bookmakerRepository;
private readonly Football.Database.FixtureLeague _fixtureLeagueRepository;
private readonly Football.Database.Comparison _comparisonRepository;
private readonly Football.Database.H2H _h2hRepository;
private readonly Football.Database.LeagueStats _leagueStatsRepository;
private readonly Football.Database.TeamStats _teamStatsRepository;
// 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();
_predictionManager = new API.Prediction();
_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();
_teamRepository = new Football.Database.Team();
_goalsRepository = new Football.Database.Goals();
_scoreRepository = new Football.Database.Score();
_oddsRepository = new Football.Database.Odds();
_predictionRepository = new Football.Database.Prediction();
// Inizializzazione dei nuovi repository
_betTypeRepository = new Football.Database.BetType();
_bookmakerRepository = new Football.Database.Bookmaker();
_fixtureLeagueRepository = new Football.Database.FixtureLeague();
_comparisonRepository = new Football.Database.Comparison();
_h2hRepository = new Football.Database.H2H();
_leagueStatsRepository = new Football.Database.LeagueStats();
_teamStatsRepository = new Football.Database.TeamStats();
// Inizializzazione del repository per le risposte API
_apiResponseRepository = new Football.Database.APIResponse();
}
/// <summary>
/// Metodo centralizzato per ottenere tutte le partite e le relative quote per una data specifica
/// Utilizzo della tabella API_Response come intermediario
/// </summary>
/// <param name="date">Data per cui recuperare le partite</param>
/// <param name="progressCallback">Callback per aggiornare il progresso dell'operazione</param>
/// <param name="statusCallback">Callback per aggiornare lo stato dell'operazione</param>
/// <returns>DataTable contenente tutte le partite con le relative quote (quando disponibili)</returns>
public DataTable GetFixturesAndOdds(DateTime date, IProgress<int> progressCallback = null, IProgress<string> statusCallback = null)
{
try
{
// Prima scarica i dati
DownloadFixturesAndOdds(date, progressCallback, statusCallback).Wait();
// Poi importa i dati
return ImportFromApiResponses(progressCallback, statusCallback);
}
catch (Exception ex)
{
_database.LogError("recupero centralizzato di partite e quote", ex);
statusCallback?.Report($"Errore: {ex.Message}");
return CreateEmptyResultTable();
}
}
/// <summary>
/// Recupera solo l'elenco delle partite per la data specificata e le restituisce come DataTable semplice.
/// Overload retrocompatibile senza opzioni.
/// </summary>
public DataTable GetTodayFixtures(DateTime date, IProgress<int> progressCallback = null, IProgress<string> statusCallback = null)
{
return GetTodayFixtures(date, new FootballDownloadOptions(), progressCallback, statusCallback);
}
/// <summary>
/// Recupera le partite per la data specificata con controllo granulare degli endpoint da scaricare.
/// La progress bar avanza proporzionalmente al numero di step attivi.
/// </summary>
public DataTable GetTodayFixtures(DateTime date, FootballDownloadOptions options, IProgress<int> progressCallback = null, IProgress<string> statusCallback = null)
{
try
{
// Calcola il numero totale di step per una progress bar proporzionale
var steps = BuildProgressSteps(options);
int currentStep = 0;
int totalSteps = steps.Count;
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);
}
// ?? 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);
}
}
}
// ?? 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);
}
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);
string quotaMsg = _lastQuota.IsValid ? $" — {_lastQuota}" : "";
statusCallback?.Report($"Trovate {table.Rows.Count} partite{quotaMsg}");
return table;
}
catch (Exception ex)
{
_database.LogError("recupero partite del giorno", ex);
statusCallback?.Report($"Errore: {ex.Message}");
return CreateEmptyResultTable();
}
}
/// <summary>
/// Costruisce la lista degli step attivi in base alle opzioni per calcolare il progresso proporzionale.
/// </summary>
private static List<string> BuildProgressSteps(FootballDownloadOptions options)
{
var steps = new List<string>();
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;
}
/// <summary>
/// Interroga l'endpoint /status per verificare la quota API residua.
/// Questa chiamata non conta nella quota giornaliera.
/// </summary>
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;
}
/// <summary>
/// Scarica i dati dalle API e li salva nella tabella di frontiera API_Response
/// </summary>
/// <param name="date">Data per cui recuperare le partite</param>
/// <param name="progressCallback">Callback per aggiornare il progresso dell'operazione</param>
/// <param name="statusCallback">Callback per aggiornare lo stato dell'operazione</param>
public async Task DownloadFixturesAndOdds(DateTime date, IProgress<int> progressCallback = null, IProgress<string> statusCallback = null)
{
try
{
// Step 1: Informare l'utente che stiamo ottenendo le partite
statusCallback?.Report("Scaricamento delle partite in corso...");
progressCallback?.Report(0);
// Step 2: Recuperare tutte le partite per la data selezionata
var fixturesResponse = GetFixtures(date);
progressCallback?.Report(20);
// Step 3: Informare l'utente che stiamo recuperando le quote
statusCallback?.Report("Scaricamento delle quote in corso...");
progressCallback?.Report(40);
// Step 4: Recuperare le quote per la data selezionata
var oddsResponses = GetOdds(date);
progressCallback?.Report(60);
// Step 5: Informare l'utente che stiamo recuperando le previsioni
statusCallback?.Report("Scaricamento delle previsioni in corso...");
progressCallback?.Report(70);
// Step 6: Recuperare le previsioni per le partite
await GetPredictionsForFixtures(fixturesResponse);
progressCallback?.Report(90);
// Step 7: Operazione completata
statusCallback?.Report("Scaricamento completato. Dati salvati nella tabella di frontiera.");
progressCallback?.Report(100);
}
catch (Exception ex)
{
_database.LogError("scaricamento centralizzato di partite e quote", ex);
statusCallback?.Report($"Errore: {ex.Message}");
throw;
}
}
/// <summary>
/// Recupera le previsioni per le partite specificate
/// </summary>
private async Task GetPredictionsForFixtures(RestResponse fixturesResponse)
{
try
{
// Verifica che la risposta sia valida
if (fixturesResponse == null || !fixturesResponse.IsSuccessful || string.IsNullOrEmpty(fixturesResponse.Content))
{
return;
}
var json = JsonDocument.Parse(fixturesResponse.Content).RootElement;
// Verifica che la risposta contenga dati validi
if (!json.TryGetProperty("response", out var responseElement))
{
return;
}
// Per ogni partita, recupera la previsione
foreach (var item in responseElement.EnumerateArray())
{
try
{
int fixtureId = item.GetProperty("fixture").GetProperty("id").GetInt32();
// Utilizza il prediction manager per ottenere la previsione
_predictionManager.GetPredictionByFixture(fixtureId);
// Breve pausa per non sovraccaricare l'API
await Task.Delay(100);
}
catch (Exception ex)
{
_database.LogError($"recupero previsione", ex);
// Continua con la prossima partita
}
}
}
catch (Exception ex)
{
_database.LogError("recupero previsioni per partite", ex);
}
}
/// <summary>
/// Importa i dati dalla tabella di frontiera API_Response alle tabelle finali
/// </summary>
/// <param name="progressCallback">Callback per aggiornare il progresso dell'operazione</param>
/// <param name="statusCallback">Callback per aggiornare lo stato dell'operazione</param>
/// <returns>DataTable contenente tutte le partite con le relative quote (quando disponibili)</returns>
public DataTable ImportFromApiResponses(IProgress<int> progressCallback = null, IProgress<string> statusCallback = null)
{
try
{
// Step 1: Informare l'utente che stiamo elaborando le risposte API
statusCallback?.Report("Elaborazione delle risposte API in corso...");
progressCallback?.Report(0);
// Step 2: Elaborare le risposte API non elaborate dal database
ProcessUnprocessedApiResponses();
progressCallback?.Report(50);
// Step 3: Creare un DataTable vuoto per i fixture
var fixturesTable = CreateEmptyFixturesDataTable();
// Step 4: Recuperare i dati elaborati dal database per visualizzarli
statusCallback?.Report("Preparazione dei dati per la visualizzazione...");
var combinedTable = GetProcessedFixturesFromDatabase();
progressCallback?.Report(90);
// Step 5: Operazione completata
statusCallback?.Report("Importazione completata.");
progressCallback?.Report(100);
return combinedTable;
}
catch (Exception ex)
{
_database.LogError("importazione centralizzata di partite e quote", ex);
statusCallback?.Report($"Errore: {ex.Message}");
return CreateEmptyResultTable();
}
}
/// <summary>
/// Recupera le partite per la data specificata utilizzando API.Fixture
/// </summary>
private RestResponse GetFixtures(DateTime date)
{
return _fixtureAPI.GetFixturesByDate(date);
}
/// <summary>
/// Recupera le quote per la data specificata (potenzialmente più pagine) utilizzando API.Odds.
/// Usa i parametri dalle opzioni di download.
/// </summary>
private List<RestResponse> GetOdds(DateTime date, FootballDownloadOptions options = null)
{
int bookmakerId = options?.BookmakerId ?? 8;
int maxPages = options?.OddsMaxPages ?? 3;
var responses = new List<RestResponse>();
int currentPage = 1;
bool hasMorePages = true;
while (hasMorePages && currentPage <= maxPages)
{
var response = _oddsAPI.GetOddsByDate(date, bookmakerId, currentPage);
responses.Add(response);
// Controlla se ci sono altre pagine
var json = JsonDocument.Parse(response.Content).RootElement;
if (json.TryGetProperty("paging", out var pagingElement) &&
pagingElement.TryGetProperty("total", out var totalElement))
{
int totalPages = totalElement.GetInt32();
hasMorePages = currentPage < totalPages;
}
else
{
hasMorePages = false;
}
currentPage++;
}
return responses;
}
/// <summary>
/// Elabora i dati JSON e li inserisce nel database utilizzando transazioni e disabilitazione temporanea dei vincoli
/// </summary>
/// <param name="jsonResponse">Risposta JSON da elaborare</param>
public void ProcessAndInsertData(string jsonResponse)
{
try
{
// Parse JSON using System.Text.Json and convert to Newtonsoft.JToken where needed
var jsonNode = JsonNode.Parse(jsonResponse);
var jsonObject = JObject.Parse(jsonNode.ToJsonString());
// Helper to enumerate array-like tokens (compat replacement for JsonNode.AsArray())
var responseArray = jsonObject["response"] as JArray;
Func<JToken, IEnumerable<JToken>> asArray = t => t is JArray a ? a : (t != null ? t.Children() : Enumerable.Empty<JToken>());
_database.ExecuteTransactionalQuery("l'elaborazione dei dati calcistici", (connection, transaction) =>
{
try
{
// Disabilita temporaneamente i vincoli di chiave esterna
((Manager.Database)_database).DisableAllConstraints(connection, transaction);
// FASE 1: Inserisci le leghe
foreach (var responseItem in asArray(jsonObject["response"]))
{
var league = responseItem["league"];
if (league != null && league["id"] != null)
{
_leagueRepository.Upsert(connection, transaction, league);
}
}
// FASE 2: Inserisci i bookmakers e i tipi di scommessa
foreach (var responseItem in asArray(jsonObject["response"]))
{
if (responseItem["bookmakers"] != null)
{
foreach (var bookmaker in asArray(responseItem["bookmakers"]))
{
if (bookmaker["id"] != null)
{
_bookmakerRepository.Upsert(connection, bookmaker);
// Tipi di scommessa
if (bookmaker["bets"] != null)
{
foreach (var bet in asArray(bookmaker["bets"]))
{
if (bet["id"] != null && bet["name"] != null)
{
_betTypeRepository.Upsert(connection, bet);
}
}
}
}
}
}
}
// FASE 3: Inserisci le squadre (senza relazione con fixture)
foreach (var responseItem in asArray(jsonObject["response"]))
{
var teams = responseItem["teams"];
if (teams != null && teams["home"] != null && teams["away"] != null &&
teams["home"]["id"] != null && teams["away"]["id"] != null)
{
// Usa 0 come placeholder per fixtureId - in questa fase inseriamo solo le squadre
_teamRepository.UpsertTeams(connection, transaction, teams, 0);
}
}
// FASE 4: Inserisci i fixture e venue
foreach (var responseItem in asArray(jsonObject["response"]))
{
var fixture = responseItem["fixture"];
if (fixture != null && fixture["id"] != null)
{
_fixtureRepository.Upsert(connection, transaction, fixture);
}
}
// FASE 5: Inserisci relazioni tra entità e dati dipendenti
foreach (var responseItem in asArray(jsonObject["response"]))
{
int? fixtureId = null;
try
{
var fixture = responseItem["fixture"];
if (fixture != null && fixture["id"] != null)
{
fixtureId = fixture["id"].Value<int>();
// Relazioni fixture-team
var teams = responseItem["teams"];
if (teams != null && teams["home"] != null && teams["away"] != null &&
teams["home"]["id"] != null && teams["away"]["id"] != null)
{
_teamRepository.UpsertTeams(connection, transaction, teams, fixtureId.Value);
}
// Relazioni fixture-league
var league = responseItem["league"];
if (league != null && league["id"] != null)
{
int leagueId = league["id"].Value<int>();
_fixtureLeagueRepository.Upsert(connection, transaction, fixtureId.Value, leagueId, league);
}
// Goals
var goals = responseItem["goals"];
if (goals != null)
{
_goalsRepository.Upsert(connection, goals, fixtureId.Value);
}
// Score
var score = responseItem["score"];
if (score != null)
{
_scoreRepository.Upsert(connection, score, fixtureId.Value);
}
}
}
catch (Exception ex)
{
_database.LogError($"inserimento dati dipendenti per fixture {fixtureId}", ex);
// Continuiamo con il prossimo elemento, non interrompiamo l'intera transazione
}
}
// FASE 6: Inserisci dati che richiedono fixture e teams: quote e previsioni
foreach (var responseItem in asArray(jsonObject["response"]))
{
int? fixtureId = null;
try
{
var fixture = responseItem["fixture"];
if (fixture != null && fixture["id"] != null)
{
fixtureId = fixture["id"].Value<int>();
// Quote
if (responseItem["bookmakers"] != null)
{
_oddsRepository.Upsert(connection, responseItem["bookmakers"], fixtureId.Value);
}
// Previsioni
var predictions = responseItem["predictions"];
if (predictions != null)
{
_predictionRepository.Upsert(connection, predictions, fixtureId.Value);
// Recupera ID previsione
int? predictionId = null;
using (var cmd = new SqlCommand("SELECT prediction_id FROM Prediction WHERE fixture_id = @fixture_id", connection, transaction))
{
cmd.Parameters.AddWithValue("@fixture_id", fixtureId.Value);
var result = cmd.ExecuteScalar();
if (result != null && result != DBNull.Value)
{
predictionId = Convert.ToInt32(result);
}
}
if (predictionId.HasValue)
{
// Confronto
if (predictions["comparison"] != null)
{
_comparisonRepository.Upsert(connection, predictionId.Value, predictions["comparison"]);
}
// Head-to-head
if (predictions["h2h"] != null && asArray(predictions["h2h"]).Any())
{
_h2hRepository.DeleteForPrediction(connection, predictionId.Value);
foreach (var h2hFixture in asArray(predictions["h2h"]))
{
if (h2hFixture["fixture"] != null && h2hFixture["fixture"]["id"] != null)
{
_h2hRepository.Insert(connection, predictionId.Value, h2hFixture["fixture"]["id"].Value<int>());
}
}
}
// Statistiche
var teams = responseItem["teams"];
if (predictions["teams"] != null && teams != null)
{
// Home team stats
if (teams["home"] != null && teams["home"]["id"] != null && predictions["teams"]["home"] != null)
{
int homeTeamId = teams["home"]["id"].Value<int>();
if (predictions["teams"]["home"]["team"] != null)
{
_teamStatsRepository.Insert(connection, homeTeamId, predictionId.Value, true,
predictions["teams"]["home"]["team"]);
}
if (predictions["teams"]["home"]["league"] != null)
{
_leagueStatsRepository.Insert(connection, homeTeamId, predictionId.Value, true,
predictions["teams"]["home"]["league"]);
}
}
// Away team stats
if (teams["away"] != null && teams["away"]["id"] != null && predictions["teams"]["away"] != null)
{
int awayTeamId = teams["away"]["id"].Value<int>();
if (predictions["teams"]["away"]["team"] != null)
{
_teamStatsRepository.Insert(connection, awayTeamId, predictionId.Value, false,
predictions["teams"]["away"]["team"]);
}
if (predictions["teams"]["away"]["league"] != null)
{
_leagueStatsRepository.Insert(connection, awayTeamId, predictionId.Value, false,
predictions["teams"]["away"]["league"]);
}
}
}
}
}
}
}
catch (Exception ex)
{
_database.LogError($"inserimento previsioni e quote per fixture {fixtureId}", ex);
// Continuiamo con il prossimo elemento
}
}
// Riattiva i vincoli
((Manager.Database)_database).EnableAllConstraints(connection, transaction);
}
catch (Exception ex)
{
_database.LogError("processamento dati in transazione", ex);
throw; // Rilancia l'eccezione per il rollback della transazione
}
});
}
catch (Exception ex)
{
_database.LogError("l'elaborazione dei dati calcistici", ex);
}
}
/// <summary>
/// Aggiorna il database con i dati di partite e quote utilizzando le classi del namespace Football.Database
/// </summary>
private void UpdateDatabase(RestResponse fixturesResponse, List<RestResponse> oddsResponses)
{
try
{
// In questo metodo non elaboriamo più direttamente le risposte
// Le risposte sono già state salvate nel database dalla classe API
// e verranno elaborate dal metodo ProcessUnprocessedApiResponses
// Processa le risposte non elaborate
ProcessUnprocessedApiResponses();
}
catch (Exception ex)
{
_database.LogError("aggiornamento database", ex);
throw; // Propaga l'eccezione per gestirla al livello superiore
}
}
/// <summary>
/// Crea un DataTable vuoto per i fixture (retrocompatibile)
/// </summary>
private DataTable CreateEmptyFixturesDataTable()
{
return CreateEmptyFixturesDataTable(new FootballDownloadOptions());
}
/// <summary>
/// Crea un DataTable vuoto per i fixture con colonne opzionali in base alle opzioni.
/// </summary>
private DataTable CreateEmptyFixturesDataTable(FootballDownloadOptions options)
{
var dataTable = new DataTable();
// 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));
// 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;
}
/// <summary>
/// Crea un DataTable con le partite dalla risposta API
/// </summary>
private DataTable CreateFixturesDataTable(RestResponse response, FootballDownloadOptions options = null)
{
options ??= new FootballDownloadOptions();
var dataTable = CreateEmptyFixturesDataTable(options);
// Verifica che la risposta sia valida
if (response == null || !response.IsSuccessful || string.IsNullOrEmpty(response.Content))
{
return dataTable;
}
try
{
var json = JsonDocument.Parse(response.Content).RootElement;
// Verifica che la risposta contenga dati validi
if (!json.TryGetProperty("response", out var responseElement))
{
return dataTable;
}
// Aggiungi righe
foreach (var item in responseElement.EnumerateArray())
{
try
{
// Verifica che le proprietà essenziali esistano
if (!item.TryGetProperty("fixture", out var fixtureEl) ||
!item.TryGetProperty("league", out var leagueEl) ||
!item.TryGetProperty("teams", out var teamsEl))
{
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)
{
DateTime.TryParse(dateEl.GetString(), out var parsedDate);
row["Data / Ora"] = parsedDate;
}
else
{
row["Data / Ora"] = DBNull.Value;
}
row["Stato"] = fixtureEl.TryGetProperty("status", out var statusEl) &&
statusEl.TryGetProperty("long", out var statusLong)
? statusLong.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))
{
row["Goals Casa"] = goalsElement.TryGetProperty("home", out var ghEl) && ghEl.ValueKind == JsonValueKind.Number
? ghEl.GetInt32() : 0;
row["Goals Trasferta"] = goalsElement.TryGetProperty("away", out var gaEl) && gaEl.ValueKind == JsonValueKind.Number
? gaEl.GetInt32() : 0;
}
else
{
row["Goals Casa"] = 0;
row["Goals Trasferta"] = 0;
}
dataTable.Rows.Add(row);
}
catch (Exception ex)
{
_database.LogError($"elaborazione riga partita", ex);
}
}
}
catch (Exception ex)
{
_database.LogError("creazione tabella partite", ex);
}
return dataTable;
}
/// <summary>
/// Combina i dati delle partite con le quote
/// </summary>
private DataTable CombineFixturesAndOdds(DataTable fixturesTable, List<RestResponse> oddsResponses)
{
// Crea una copia del DataTable delle partite
var combinedTable = fixturesTable.Copy();
// Se non ci sono risposte di quote o la tabella delle partite è vuota, ritorna la tabella originale
if (oddsResponses == null || oddsResponses.Count == 0 || combinedTable.Rows.Count == 0)
{
return combinedTable;
}
try
{
// Elabora ogni risposta delle quote
foreach (var response in oddsResponses)
{
if (!response.IsSuccessful || string.IsNullOrEmpty(response.Content))
{
continue;
}
var json = JsonDocument.Parse(response.Content).RootElement;
if (!json.TryGetProperty("response", out var responseElement))
{
continue;
}
foreach (var item in responseElement.EnumerateArray())
{
try
{
int fixtureId = item.GetProperty("fixture").GetProperty("id").GetInt32();
// Trova la riga corrispondente nella tabella delle partite
DataRow[] matchingRows = combinedTable.Select($"ID = {fixtureId}");
if (matchingRows.Length == 0) continue;
DataRow row = matchingRows[0];
// Cerca le quote del bookmaker (assumiamo Bet365 con ID 8)
if (item.TryGetProperty("bookmakers", out var bookmakersElement))
{
foreach (var bookmaker in bookmakersElement.EnumerateArray())
{
if (bookmaker.GetProperty("id").GetInt32() == 8) // Bet365
{
if (bookmaker.TryGetProperty("bets", out var betsElement))
{
foreach (var bet in betsElement.EnumerateArray())
{
if (bet.GetProperty("name").GetString() == "Match Winner")
{
// Elabora le quote 1X2
foreach (var value in bet.GetProperty("values").EnumerateArray())
{
string betType = value.GetProperty("value").GetString();
string odd = value.GetProperty("odd").GetString();
switch (betType)
{
case "Home":
row["Quota Casa"] = odd;
break;
case "Draw":
row["Quota Pareggio"] = odd;
break;
case "Away":
row["Quota Trasferta"] = odd;
break;
}
}
break;
}
}
}
break;
}
}
}
// Recupera la previsione per questa partita
try
{
var predictionResponse = _predictionManager.GetPredictionByFixture(fixtureId);
if (predictionResponse != null && predictionResponse.IsSuccessful)
{
var predictionJson = JsonDocument.Parse(predictionResponse.Content).RootElement;
if (predictionJson.TryGetProperty("response", out var predResponse) &&
predResponse.EnumerateArray().Any())
{
var prediction = predResponse.EnumerateArray().First();
if (prediction.TryGetProperty("predictions", out var predsElement) &&
predsElement.TryGetProperty("winner", out var winnerElement) &&
winnerElement.TryGetProperty("name", out var winnerNameElement))
{
row["Previsione"] = winnerNameElement.GetString();
}
}
}
}
catch (Exception ex)
{
_database.LogError($"recupero previsione per partita ID: {fixtureId}", ex);
// Continua con la prossima partita
}
}
catch (Exception ex)
{
_database.LogError("elaborazione quote partita", ex);
// Continua con la prossima partita
}
}
}
}
catch (Exception ex)
{
_database.LogError("combinazione partite e quote", ex);
// In caso di errore, ritorna la tabella originale
}
// Ordina le righe per data
try
{
combinedTable.DefaultView.Sort = "Data / Ora ASC";
return combinedTable.DefaultView.ToTable();
}
catch
{
return combinedTable; // In caso di errore nell'ordinamento, ritorna la tabella non ordinata
}
}
/// <summary>
/// Crea un DataTable vuoto in caso di errore
/// </summary>
private DataTable CreateEmptyResultTable()
{
var table = new DataTable();
table.Columns.Add("Errore", typeof(string));
var row = table.NewRow();
row["Errore"] = "Si è verificato un errore durante il recupero dei dati.";
table.Rows.Add(row);
return table;
}
/// <summary>
/// Analizza le risposte delle quote dall'API e le inserisce direttamente nella DataTable delle partite.
/// Utilizza il bookmaker specificato come principale; se non presente, usa il primo bookmaker disponibile.
/// </summary>
private void ParseOddsIntoTable(DataTable fixturesTable, List<RestResponse> oddsResponses, int bookmakerId = 8)
{
if (oddsResponses == null || oddsResponses.Count == 0 || fixturesTable == null || fixturesTable.Rows.Count == 0)
return;
try
{
foreach (var response in oddsResponses)
{
if (response == null || !response.IsSuccessful || string.IsNullOrEmpty(response.Content))
continue;
var json = JsonDocument.Parse(response.Content).RootElement;
if (!json.TryGetProperty("response", out var responseElement))
continue;
foreach (var item in responseElement.EnumerateArray())
{
try
{
if (!item.TryGetProperty("fixture", out var fixtureEl))
continue;
int fixtureId = fixtureEl.GetProperty("id").GetInt32();
DataRow[] matchingRows = fixturesTable.Select($"ID = {fixtureId}");
if (matchingRows.Length == 0)
continue;
DataRow row = matchingRows[0];
if (!item.TryGetProperty("bookmakers", out var bookmakersEl))
continue;
// 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() == bookmakerId)
{
selectedBookmaker = bm;
found = true;
break;
}
}
if (!found)
{
var enumerator = bookmakersEl.EnumerateArray();
if (enumerator.MoveNext())
{
selectedBookmaker = enumerator.Current;
found = true;
}
}
if (!found || !selectedBookmaker.TryGetProperty("bets", out var betsEl))
continue;
foreach (var bet in betsEl.EnumerateArray())
{
string betName = bet.TryGetProperty("name", out var nameEl) ? nameEl.GetString() : "";
if (string.IsNullOrEmpty(betName) || !bet.TryGetProperty("values", out var valuesEl))
continue;
switch (betName)
{
case "Match Winner":
foreach (var v in valuesEl.EnumerateArray())
{
string val = GetOddValueString(v, "value");
string odd = GetOddValueString(v, "odd");
if (val == "Home") row["Quota Casa"] = odd;
else if (val == "Draw") row["Quota Pareggio"] = odd;
else if (val == "Away") row["Quota Trasferta"] = odd;
}
break;
case "Goals Over/Under":
foreach (var v in valuesEl.EnumerateArray())
{
string val = GetOddValueString(v, "value");
string odd = GetOddValueString(v, "odd");
if (val == "Over 2.5") row["Over 2.5"] = odd;
else if (val == "Under 2.5") row["Under 2.5"] = odd;
}
break;
case "Both Teams Score":
foreach (var v in valuesEl.EnumerateArray())
{
string val = GetOddValueString(v, "value");
string odd = GetOddValueString(v, "odd");
if (val == "Yes") row["BTTS Sì"] = odd;
else if (val == "No") row["BTTS No"] = odd;
}
break;
case "Double Chance":
foreach (var v in valuesEl.EnumerateArray())
{
string val = GetOddValueString(v, "value");
string odd = GetOddValueString(v, "odd");
if (val == "Home/Draw") row["Doppia Casa/X"] = odd;
else if (val == "Home/Away") row["Doppia Casa/Trasf"] = odd;
else if (val == "Draw/Away") row["Doppia X/Trasf"] = odd;
}
break;
}
}
}
catch (Exception ex)
{
_database.LogError("elaborazione quote per singola partita", ex);
}
}
}
}
catch (Exception ex)
{
_database.LogError("parsing quote nella tabella", ex);
}
}
/// <summary>
/// Estrae un valore stringa da un JsonElement in modo sicuro
/// </summary>
private string GetOddValueString(JsonElement element, string propertyName)
{
try
{
if (!element.TryGetProperty(propertyName, out var prop))
return "";
switch (prop.ValueKind)
{
case JsonValueKind.String:
return prop.GetString() ?? "";
case JsonValueKind.Number:
return prop.GetDecimal().ToString(System.Globalization.CultureInfo.InvariantCulture);
default:
return prop.ToString();
}
}
catch
{
return "";
}
}
/// <summary>
/// Elabora le risposte API non elaborate presenti nel database
/// </summary>
private void ProcessUnprocessedApiResponses()
{
try
{
// Recupera tutte le risposte non elaborate
var unprocessedResponses = _apiResponseRepository.GetUnprocessedResponses();
foreach (DataRow responseRow in unprocessedResponses.Rows)
{
int responseId = Convert.ToInt32(responseRow["id"]);
string content = responseRow["response_content"].ToString();
string apiType = responseRow["api_type"].ToString();
try
{
// Processa il contenuto della risposta API
ProcessAndInsertData(content);
// Aggiorna lo stato della risposta come elaborata
_apiResponseRepository.UpdateProcessingStatus(responseId, true);
}
catch (Exception ex)
{
// Aggiorna lo stato della risposta con l'errore
_apiResponseRepository.UpdateProcessingStatus(responseId, false, ex.Message);
_database.LogError($"elaborazione risposta API ID: {responseId}", ex);
}
}
}
catch (Exception ex)
{
_database.LogError("elaborazione risposte API non elaborate", ex);
throw;
}
}
/// <summary>
/// Recupera i fixture elaborati dal database
/// </summary>
private DataTable GetProcessedFixturesFromDatabase()
{
try
{
// Step 1: Creare un DataTable vuoto per i fixture
var result = CreateEmptyFixturesDataTable();
// Step 2: Delegare alla classe repository appropriata il recupero dei dati
// Utilizziamo principalmente il repository Fixture che può coordinarsi con gli altri repository
return _fixtureRepository.GetProcessedFixtures();
}
catch (Exception ex)
{
_database.LogError("recupero partite elaborate dal database", ex);
// Return empty table in case of error
return CreateEmptyFixturesDataTable();
}
}
#region Enrichment Methods
/// <summary>
/// Arricchisce la tabella con le previsioni per ogni partita.
/// </summary>
private void EnrichWithPredictions(DataTable table, RestResponse fixturesResponse, FootballDownloadOptions options,
IProgress<string> statusCallback, IProgress<int> 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);
}
}
/// <summary>
/// Arricchisce la tabella con le classifiche dei campionati.
/// </summary>
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<int>();
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<int, Dictionary<int, (int rank, int points)>>();
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<int, (int rank, int points)>();
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);
}
}
/// <summary>
/// Arricchisce la tabella con i dati degli scontri diretti.
/// </summary>
private void EnrichWithH2H(DataTable table, FootballDownloadOptions options,
IProgress<string> statusCallback, IProgress<int> 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);
}
}
/// <summary>
/// Arricchisce la tabella con il conteggio eventi (gol, cartellini).
/// </summary>
private void EnrichWithEvents(DataTable table, FootballDownloadOptions options,
IProgress<string> statusCallback, IProgress<int> 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);
}
}
/// <summary>
/// Arricchisce la tabella con i moduli tattici (formazione).
/// </summary>
private void EnrichWithLineups(DataTable table, FootballDownloadOptions options,
IProgress<string> statusCallback, IProgress<int> 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);
}
}
/// <summary>
/// Arricchisce la tabella con le statistiche delle partite (possesso, tiri, corner).
/// </summary>
private void EnrichWithStatistics(DataTable table, FootballDownloadOptions options,
IProgress<string> statusCallback, IProgress<int> 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);
}
}
/// <summary>
/// Arricchisce la tabella con il conteggio degli infortunati per squadra.
/// </summary>
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<int, int>();
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
#region Supplementary Data
/// <summary>
/// Scarica i dati supplementari selezionati (statistiche giocatori, capocannonieri, ecc.)
/// e li esporta come CSV separati nella cartella specificata.
/// Richiede un DataTable di fixture già scaricato per estrarre teamId e leagueId coinvolti.
/// </summary>
public List<string> DownloadSupplementaryData(
DataTable fixturesTable,
DateTime date,
string exportFolder,
FootballDownloadOptions options,
IProgress<int> progressCallback = null,
IProgress<string> statusCallback = null)
{
if (fixturesTable == null || fixturesTable.Rows.Count == 0 || !options.AnySupplementarySelected)
return new List<string>();
// Estrai team IDs e league IDs unici dal DataTable fixture
var teamIds = new HashSet<int>();
var leagueIds = new HashSet<int>();
foreach (DataRow row in fixturesTable.Rows)
{
if (row["HomeTeamId"] != DBNull.Value) teamIds.Add(Convert.ToInt32(row["HomeTeamId"]));
if (row["AwayTeamId"] != DBNull.Value) teamIds.Add(Convert.ToInt32(row["AwayTeamId"]));
if (row["LeagueId"] != DBNull.Value) leagueIds.Add(Convert.ToInt32(row["LeagueId"]));
}
var exporter = new SupplementaryDataExporter();
return exporter.DownloadAndExport(date, exportFolder, options, teamIds, leagueIds, progressCallback, statusCallback);
}
#endregion
}
}