0d3769db79
- 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
1918 lines
88 KiB
C#
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
|
|
}
|
|
}
|