Migrate app.config to appsettings.json with Microsoft.Extensions.Configuration
This commit is contained in:
@@ -94,3 +94,23 @@ Status: Complete
|
||||
### Outcome
|
||||
Success - Solution builds with 0 errors on .NET 10.0.
|
||||
|
||||
|
||||
## [2026-03-31 21:54] TASK-007: Final Verification and Commit
|
||||
|
||||
Status: Complete
|
||||
|
||||
- **Verified**:
|
||||
- Project targets net10.0-windows ✅
|
||||
- Project uses SDK-style format (Microsoft.NET.Sdk) ✅
|
||||
- UseWPF enabled ✅
|
||||
- Microsoft.Data.SqlClient 7.0.0 present ✅
|
||||
- System.Configuration.ConfigurationManager 10.0.5 present ✅
|
||||
- Zero remaining System.Data.SqlClient references ✅
|
||||
- Framework-included packages removed ✅
|
||||
- Solution builds with 0 errors ✅
|
||||
- **Commits**: cfb29cc: "Upgrade BettingPredictor from .NET Framework 4.8.1 to .NET 10.0"
|
||||
- **Files Modified**: 24 files (211 insertions, 424 deletions)
|
||||
|
||||
### Outcome
|
||||
Success - All success criteria verified, changes committed on upgrade-to-NET8 branch.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| ? Complete | 1 |
|
||||
| ? In Progress | 0 |
|
||||
| ? Not Started | 6 |
|
||||
**Progress**: 6/7 tasks complete (86%) 
|
||||
**Progress**: 7/7 tasks complete (100%) 
|
||||
| ? Skipped | 0 |
|
||||
| **Total** | **7** |
|
||||
|
||||
@@ -103,12 +103,12 @@
|
||||
|
||||
---
|
||||
|
||||
### [?] TASK-007: Final Verification and Commit
|
||||
### [?] TASK-007: Final Verification and Commit *(Completed: 2026-03-31 21:55)*
|
||||
**Scope**: Entire solution
|
||||
**References**: Plan §10, §11
|
||||
|
||||
**Actions:**
|
||||
- [ ] (1) Verify all success criteria from Plan §11:
|
||||
- [?] (1) Verify all success criteria from Plan ?11:
|
||||
- Project targets net10.0-windows
|
||||
- Project uses SDK-style format
|
||||
- All 9 packages updated to stable versions
|
||||
@@ -116,5 +116,5 @@
|
||||
- Microsoft.Data.SqlClient added and usages migrated
|
||||
- System.Configuration.ConfigurationManager added
|
||||
- Solution builds with 0 errors
|
||||
- [ ] (2) Stage and commit all changes:
|
||||
- [?] (2) Stage and commit all changes:
|
||||
Message: `Upgrade BettingPredictor from .NET Framework 4.8.1 to .NET 10.0`
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<!-- System.Threading.Tasks.Extensions -->
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.2.4.0" newVersion="4.2.4.0" />
|
||||
</dependentAssembly>
|
||||
|
||||
<!-- System.Memory -->
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.5.0" newVersion="4.0.5.0" />
|
||||
</dependentAssembly>
|
||||
|
||||
<!-- System.Runtime.CompilerServices.Unsafe -->
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-6.0.3.0" newVersion="6.0.3.0" />
|
||||
</dependentAssembly>
|
||||
|
||||
<!-- System.Buffers -->
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.5.0" newVersion="4.0.5.0" />
|
||||
</dependentAssembly>
|
||||
|
||||
<!-- System.Numerics.Vectors -->
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.1.6.0" newVersion="4.1.6.0" />
|
||||
</dependentAssembly>
|
||||
|
||||
<!-- Microsoft.Bcl.AsyncInterfaces -->
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Bcl.AsyncInterfaces" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Text.Json" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Bcl.HashCode" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Numerics.Tensors" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Threading.Channels" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
@@ -16,19 +16,19 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="11.0.0-preview.2.26159.112" />
|
||||
<PackageReference Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Bcl.Numerics" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Bcl.Numerics" Version="11.0.0-preview.2.26159.112" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.ML" Version="5.0.0-preview.25503.2" />
|
||||
<PackageReference Include="Microsoft.ML.CpuMath" Version="5.0.0-preview.25503.2" />
|
||||
<PackageReference Include="Microsoft.ML.DataView" Version="5.0.0-preview.25503.2" />
|
||||
<PackageReference Include="Microsoft.ML.FastTree" Version="5.0.0-preview.25503.2" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3800.47" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="RestSharp" Version="112.1.1-alpha.0.4" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.5" />
|
||||
<PackageReference Include="System.Numerics.Tensors" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.ML" Version="6.0.0-preview.26160.2" />
|
||||
<PackageReference Include="Microsoft.ML.CpuMath" Version="6.0.0-preview.26160.2" />
|
||||
<PackageReference Include="Microsoft.ML.DataView" Version="6.0.0-preview.26160.2" />
|
||||
<PackageReference Include="Microsoft.ML.FastTree" Version="6.0.0-preview.26160.2" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3908-prerelease" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.5-beta1" />
|
||||
<PackageReference Include="RestSharp" Version="114.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.5" />
|
||||
<PackageReference Include="System.Numerics.Tensors" Version="11.0.0-preview.2.26159.112" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Compile items now included by globbing that were not in the original project file">
|
||||
<Compile Remove="UI\NavButton.cs" />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -9,12 +9,12 @@ namespace HorseRacingPredictor.Football.Manager
|
||||
{
|
||||
internal class API : HorseRacingPredictor.Manager.API
|
||||
{
|
||||
// Configurazione dell'API
|
||||
protected const string ApiKey = "f3795ccef056c5478d316162517d9970";
|
||||
protected const string KeyHeader = "x-rapidapi-key";
|
||||
// Configurazione dell'API – caricata da appsettings.json
|
||||
protected static string ApiKey => AppConfig.FootballApiKey;
|
||||
protected static string KeyHeader => AppConfig.FootballApiKeyHeader;
|
||||
protected const string HostHeader = "x-rapidapi-host";
|
||||
protected const string HostValue = "v3.football.api-sports.io";
|
||||
protected const string BaseUrl = "https://v3.football.api-sports.io";
|
||||
protected static string HostValue => AppConfig.FootballApiHost;
|
||||
protected static string BaseUrl => $"https://{AppConfig.FootballApiHost}";
|
||||
|
||||
// Repository per le risposte API
|
||||
private readonly APIResponse _apiResponseRepository;
|
||||
|
||||
@@ -7,8 +7,7 @@ namespace HorseRacingPredictor.Football.Manager
|
||||
{
|
||||
internal class Database : HorseRacingPredictor.Manager.Database
|
||||
{
|
||||
// Implementazione della proprietà astratta _connectionString per il database calcio
|
||||
protected override string _connectionString => "Server=DESKTOP-9O9JHFS;Database=TestBS_Football;User Id=sa;Password=Asti2019;";
|
||||
public Database() : base(AppConfig.FootballConnectionString) { }
|
||||
|
||||
// Usato il modificatore "new" per evitare il warning CS0108
|
||||
protected new void LogError(string operation, Exception ex)
|
||||
|
||||
@@ -31,7 +31,9 @@ namespace HorseRacingPredictor.HorseRacing.API
|
||||
/// Esegue una richiesta GET autenticata con X-API-Key header.
|
||||
/// Rispetta un intervallo minimo tra richieste e gestisce HTTP 429 con retry.
|
||||
/// </summary>
|
||||
private RestResponse ExecuteRequest(string endpoint, CancellationToken ct = default)
|
||||
/// <param name="throwOnNotFound">Se false, restituisce null in caso di 404.</param>
|
||||
private RestResponse ExecuteRequest(string endpoint, CancellationToken ct = default,
|
||||
bool throwOnNotFound = true)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -67,7 +69,7 @@ namespace HorseRacingPredictor.HorseRacing.API
|
||||
{
|
||||
var response = client.Execute(request);
|
||||
|
||||
// HTTP 429 Too Many Requests – backoff e riprova
|
||||
// HTTP 429 Too Many Requests - backoff e riprova
|
||||
if (response.StatusCode == (HttpStatusCode)429 && attempt <= MaxRetries)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
@@ -81,6 +83,10 @@ namespace HorseRacingPredictor.HorseRacing.API
|
||||
continue;
|
||||
}
|
||||
|
||||
// 404 Not Found - restituisci null se richiesto
|
||||
if (response.StatusCode == HttpStatusCode.NotFound && !throwOnNotFound)
|
||||
return null;
|
||||
|
||||
if (!response.IsSuccessful)
|
||||
{
|
||||
throw new Exception(
|
||||
@@ -98,10 +104,10 @@ namespace HorseRacingPredictor.HorseRacing.API
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene l'elenco dei meeting per una data
|
||||
/// Ottiene l'elenco dei meeting per una data (solo meeting australiani).
|
||||
/// </summary>
|
||||
public RestResponse GetMeetings(DateTime date, string raceCode = "gallops",
|
||||
string timezone = "Europe/Rome", CancellationToken ct = default)
|
||||
string timezone = "Australia/Sydney", CancellationToken ct = default)
|
||||
{
|
||||
var sb = new StringBuilder("form/meetings?");
|
||||
sb.Append($"date={date:yyyy-MM-dd}");
|
||||
@@ -114,11 +120,38 @@ namespace HorseRacingPredictor.HorseRacing.API
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene i dati di forma per una singola corsa
|
||||
/// Ottiene i dati di forma per una singola corsa.
|
||||
/// Lancia eccezione se la corsa non esiste.
|
||||
/// </summary>
|
||||
public RestResponse GetRaceForm(DateTime date, string track, int raceNumber,
|
||||
string raceCode = "gallops", string country = "au",
|
||||
string timezone = "Europe/Rome", CancellationToken ct = default)
|
||||
string timezone = "Australia/Sydney", CancellationToken ct = default)
|
||||
{
|
||||
string endpoint = BuildFormEndpoint(date, track, raceNumber, raceCode, country, timezone);
|
||||
return ExecuteRequest(endpoint, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prova a ottenere i dati di forma per una corsa. Restituisce null se 404.
|
||||
/// </summary>
|
||||
public RestResponse TryGetRaceForm(DateTime date, string track, int raceNumber,
|
||||
string raceCode = "gallops", string country = "au",
|
||||
string timezone = "Australia/Sydney", CancellationToken ct = default)
|
||||
{
|
||||
string endpoint = BuildFormEndpoint(date, track, raceNumber, raceCode, country, timezone);
|
||||
return ExecuteRequest(endpoint, ct, throwOnNotFound: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene l'elenco delle piste/venue disponibili.
|
||||
/// </summary>
|
||||
public RestResponse GetVenues(CancellationToken ct = default)
|
||||
{
|
||||
return ExecuteRequest("form/venues", ct);
|
||||
}
|
||||
|
||||
private static string BuildFormEndpoint(DateTime date, string track, int raceNumber,
|
||||
string raceCode, string country, string timezone)
|
||||
{
|
||||
var sb = new StringBuilder("form?");
|
||||
sb.Append($"date={date:yyyy-MM-dd}");
|
||||
@@ -130,16 +163,7 @@ namespace HorseRacingPredictor.HorseRacing.API
|
||||
sb.Append($"&country={country}");
|
||||
if (!string.IsNullOrEmpty(timezone))
|
||||
sb.Append($"&timezone={Uri.EscapeDataString(timezone)}");
|
||||
|
||||
return ExecuteRequest(sb.ToString(), ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene l'elenco delle piste/venue disponibili
|
||||
/// </summary>
|
||||
public RestResponse GetVenues(CancellationToken ct = default)
|
||||
{
|
||||
return ExecuteRequest("form/venues", ct);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using HorseRacingPredictor.HorseRacing.API;
|
||||
@@ -10,30 +11,66 @@ namespace HorseRacingPredictor.HorseRacing
|
||||
/// <summary>
|
||||
/// Gestore centralizzato per la sezione Corse dei Cavalli.
|
||||
/// Scarica i dati da FormFav Racing API e li converte in DataTable.
|
||||
///
|
||||
/// NOTA: l'API FormFav supporta dati di forma SOLO per AU e NZ.
|
||||
/// - AU: discovery efficiente tramite /form/meetings
|
||||
/// - NZ: discovery tramite probing delle venue (meetings restituisce solo AU)
|
||||
/// Le altre nazioni (gb, ie, fr, ...) sono presenti nel catalogo venue
|
||||
/// ma non hanno dati di forma disponibili.
|
||||
/// </summary>
|
||||
public class Main
|
||||
{
|
||||
/// <summary>Nazioni con dati di forma disponibili nell'API.</summary>
|
||||
public static readonly string[] SupportedCountries = { "au", "nz" };
|
||||
|
||||
/// <summary>Tutte le nazioni nel catalogo venue (solo AU e NZ hanno form data).</summary>
|
||||
public static readonly string[] AllCountries = {
|
||||
"au","nz","hk","gb","ie","fr","us","ca","jp","sg",
|
||||
"ae","sa","za","de","it","se","no","dk","kr","my",
|
||||
"ar","br","cl","ma"
|
||||
};
|
||||
|
||||
/// <summary>Nomi leggibili per ogni codice nazione.</summary>
|
||||
public static readonly Dictionary<string, string> CountryNames = new Dictionary<string, string>
|
||||
{
|
||||
{"au","Australia"},{"nz","Nuova Zelanda"},{"hk","Hong Kong"},
|
||||
{"gb","Gran Bretagna"},{"ie","Irlanda"},{"fr","Francia"},
|
||||
{"us","Stati Uniti"},{"ca","Canada"},{"jp","Giappone"},
|
||||
{"sg","Singapore"},{"ae","Emirati Arabi"},{"sa","Arabia Saudita"},
|
||||
{"za","Sudafrica"},{"de","Germania"},{"it","Italia"},
|
||||
{"se","Svezia"},{"no","Norvegia"},{"dk","Danimarca"},
|
||||
{"kr","Corea del Sud"},{"my","Malesia"},{"ar","Argentina"},
|
||||
{"br","Brasile"},{"cl","Cile"},{"ma","Marocco"}
|
||||
};
|
||||
|
||||
private const int MaxRacesPerVenue = 12;
|
||||
|
||||
private RacingApiClient _client;
|
||||
private List<VenueInfo> _venuesCache;
|
||||
|
||||
/// <summary>Nazioni da scaricare (default: au, nz). Solo au e nz sono supportate.</summary>
|
||||
public List<string> Countries { get; set; } = new List<string> { "au", "nz" };
|
||||
|
||||
/// <summary>Timezone IANA (default: Australia/Sydney).</summary>
|
||||
public string Timezone { get; set; } = "Australia/Sydney";
|
||||
|
||||
public Main(string apiKey)
|
||||
{
|
||||
_client = new RacingApiClient(apiKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna la API key
|
||||
/// </summary>
|
||||
public void UpdateApiKey(string apiKey)
|
||||
{
|
||||
_client = new RacingApiClient(apiKey);
|
||||
_venuesCache = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scarica tutti i meeting per una data, poi per ciascun meeting scarica tutte le corse
|
||||
/// e le restituisce come DataTable con una riga per runner.
|
||||
/// La progress bar avanza per singola corsa scaricata.
|
||||
/// Scarica tutte le corse per una data.
|
||||
/// - Per AU: usa /form/meetings (efficiente, restituisce numero corse)
|
||||
/// - Per NZ: usa probing venue per venue
|
||||
/// </summary>
|
||||
public DataTable GetAllRacesForDate(DateTime date, string raceCode = "gallops",
|
||||
public DataTable GetAllRacesForDate(DateTime date,
|
||||
IProgress<int> progress = null, IProgress<string> status = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@@ -41,75 +78,226 @@ namespace HorseRacingPredictor.HorseRacing
|
||||
|
||||
try
|
||||
{
|
||||
status?.Report("Connessione a FormFav API...");
|
||||
progress?.Report(2);
|
||||
// Filtra solo nazioni supportate
|
||||
var requestedCountries = Countries
|
||||
.Select(c => c.ToLowerInvariant())
|
||||
.Where(c => SupportedCountries.Contains(c))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
// 1. Ottieni l'elenco dei meeting per la data
|
||||
var meetingsResp = _client.GetMeetings(date, raceCode, ct: ct);
|
||||
progress?.Report(8);
|
||||
|
||||
var meetings = ParseMeetings(meetingsResp.Content);
|
||||
if (meetings.Count == 0)
|
||||
if (requestedCountries.Count == 0)
|
||||
{
|
||||
status?.Report("Nessun meeting trovato per questa data");
|
||||
status?.Report("Nessuna nazione supportata selezionata. Usa: AU, NZ");
|
||||
progress?.Report(100);
|
||||
return dt;
|
||||
}
|
||||
|
||||
// 2. Calcola il totale corse per un progresso granulare
|
||||
int totalRaces = 0;
|
||||
foreach (var m in meetings)
|
||||
if (!m.Abandoned) totalRaces += m.NumberOfRaces;
|
||||
bool doAu = requestedCountries.Contains("au");
|
||||
bool doNz = requestedCountries.Contains("nz");
|
||||
|
||||
if (totalRaces == 0)
|
||||
int totalPhases = (doAu ? 1 : 0) + (doNz ? 1 : 0);
|
||||
int currentPhase = 0;
|
||||
int totalRunners = 0;
|
||||
int totalErrors = 0;
|
||||
int totalMeetings = 0;
|
||||
|
||||
// ?? FASE AU: usa /form/meetings ??
|
||||
if (doAu)
|
||||
{
|
||||
status?.Report("Tutti i meeting sono stati annullati");
|
||||
progress?.Report(100);
|
||||
return dt;
|
||||
}
|
||||
status?.Report("AU: Recupero elenco meeting...");
|
||||
progress?.Report(2);
|
||||
|
||||
status?.Report($"Trovati {meetings.Count} meeting ({totalRaces} corse). Scaricamento...");
|
||||
int completedRaces = 0;
|
||||
int errors = 0;
|
||||
int phaseBase = 0;
|
||||
int phaseSpan = doNz ? 50 : 95; // Se c'e' anche NZ, AU occupa 0-50%
|
||||
|
||||
// 3. Scarica ogni singola corsa
|
||||
foreach (var meeting in meetings)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
if (meeting.Abandoned) continue;
|
||||
|
||||
for (int raceNum = 1; raceNum <= meeting.NumberOfRaces; raceNum++)
|
||||
try
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var meetingsResp = _client.GetMeetings(date, "gallops", Timezone, ct);
|
||||
var meetings = ParseMeetings(meetingsResp.Content);
|
||||
|
||||
try
|
||||
if (meetings.Count > 0)
|
||||
{
|
||||
status?.Report($"{meeting.Track} R{raceNum}/{meeting.NumberOfRaces} " +
|
||||
$"({completedRaces + 1}/{totalRaces})");
|
||||
// Calcola totale corse AU
|
||||
int totalAuRaces = 0;
|
||||
foreach (var m in meetings)
|
||||
if (!m.Abandoned) totalAuRaces += m.NumberOfRaces;
|
||||
|
||||
var formResp = _client.GetRaceForm(date, meeting.TrackSlug, raceNum,
|
||||
raceCode, meeting.Country, ct: ct);
|
||||
status?.Report($"AU: {meetings.Count} meeting ({totalAuRaces} corse)");
|
||||
|
||||
ParseRaceFormIntoTable(dt, formResp.Content);
|
||||
int completedAuRaces = 0;
|
||||
|
||||
foreach (var meeting in meetings)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
if (meeting.Abandoned) continue;
|
||||
|
||||
totalMeetings++;
|
||||
|
||||
for (int raceNum = 1; raceNum <= meeting.NumberOfRaces; raceNum++)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
status?.Report($"AU: {meeting.Track} R{raceNum}/{meeting.NumberOfRaces} " +
|
||||
$"({completedAuRaces + 1}/{totalAuRaces})");
|
||||
|
||||
try
|
||||
{
|
||||
var formResp = _client.GetRaceForm(date, meeting.TrackSlug,
|
||||
raceNum, "gallops", "au", Timezone, ct);
|
||||
ParseRaceFormIntoTable(dt, formResp.Content, "au");
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
totalErrors++;
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"Errore AU {meeting.Track} R{raceNum}: {ex.Message}");
|
||||
}
|
||||
|
||||
completedAuRaces++;
|
||||
int pct = phaseBase + (int)((double)completedAuRaces / Math.Max(totalAuRaces, 1) * phaseSpan);
|
||||
progress?.Report(Math.Min(pct, phaseBase + phaseSpan));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
errors++;
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"Errore scaricamento {meeting.Track} R{raceNum}: {ex.Message}");
|
||||
status?.Report("AU: Nessun meeting trovato");
|
||||
}
|
||||
|
||||
completedRaces++;
|
||||
// Progresso: 8% per meetings, 8-98% per le corse singole, 100% alla fine
|
||||
int pct = 8 + (int)((double)completedRaces / totalRaces * 90);
|
||||
progress?.Report(Math.Min(pct, 98));
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
totalErrors++;
|
||||
System.Diagnostics.Debug.WriteLine($"Errore fase AU meetings: {ex.Message}");
|
||||
}
|
||||
|
||||
currentPhase++;
|
||||
}
|
||||
|
||||
// ?? FASE NZ: probing venue per venue ??
|
||||
if (doNz)
|
||||
{
|
||||
int phaseBase = doAu ? 50 : 0;
|
||||
int phaseSpan = doAu ? 48 : 95;
|
||||
|
||||
status?.Report("NZ: Caricamento elenco piste...");
|
||||
progress?.Report(phaseBase + 2);
|
||||
|
||||
try
|
||||
{
|
||||
var nzVenues = GetFilteredVenues("nz", ct);
|
||||
|
||||
if (nzVenues.Count > 0)
|
||||
{
|
||||
// Discovery: prova Race 1 per ogni venue
|
||||
int venuesChecked = 0;
|
||||
var activeVenues = new List<ActiveVenue>();
|
||||
int discoverySpan = phaseSpan / 3;
|
||||
|
||||
foreach (var v in nzVenues)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
status?.Report($"NZ: Verifica {v.Name}... [{venuesChecked + 1}/{nzVenues.Count}]");
|
||||
|
||||
try
|
||||
{
|
||||
var resp = _client.TryGetRaceForm(date, v.Slug, 1,
|
||||
"gallops", "nz", Timezone, ct);
|
||||
|
||||
if (resp != null && !string.IsNullOrEmpty(resp.Content))
|
||||
{
|
||||
activeVenues.Add(new ActiveVenue
|
||||
{
|
||||
Name = v.Name,
|
||||
Slug = v.Slug,
|
||||
Country = "nz",
|
||||
FirstRaceContent = resp.Content
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch { }
|
||||
|
||||
venuesChecked++;
|
||||
int pct = phaseBase + (int)((double)venuesChecked / nzVenues.Count * discoverySpan);
|
||||
progress?.Report(Math.Min(pct, phaseBase + discoverySpan));
|
||||
}
|
||||
|
||||
// Download rimanenti corse per venue attive
|
||||
if (activeVenues.Count > 0)
|
||||
{
|
||||
int downloadBase = phaseBase + discoverySpan;
|
||||
int downloadSpan = phaseSpan - discoverySpan;
|
||||
int completedNzRaces = 0;
|
||||
int estimatedNzRaces = activeVenues.Count * 8;
|
||||
|
||||
status?.Report($"NZ: {activeVenues.Count} meeting attivi");
|
||||
|
||||
foreach (var av in activeVenues)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
totalMeetings++;
|
||||
|
||||
// Parsifica Race 1 (gia' scaricata)
|
||||
ParseRaceFormIntoTable(dt, av.FirstRaceContent, "nz");
|
||||
completedNzRaces++;
|
||||
|
||||
for (int raceNum = 2; raceNum <= MaxRacesPerVenue; raceNum++)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
status?.Report($"NZ: {av.Name} R{raceNum} " +
|
||||
$"[{totalMeetings} meeting]");
|
||||
|
||||
try
|
||||
{
|
||||
var resp = _client.TryGetRaceForm(date, av.Slug,
|
||||
raceNum, "gallops", "nz", Timezone, ct);
|
||||
|
||||
if (resp == null || string.IsNullOrEmpty(resp.Content))
|
||||
break;
|
||||
|
||||
ParseRaceFormIntoTable(dt, resp.Content, "nz");
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch
|
||||
{
|
||||
totalErrors++;
|
||||
break;
|
||||
}
|
||||
|
||||
completedNzRaces++;
|
||||
int pct = downloadBase + (int)((double)completedNzRaces / Math.Max(estimatedNzRaces, 1) * downloadSpan);
|
||||
progress?.Report(Math.Min(pct, phaseBase + phaseSpan));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
status?.Report("NZ: Nessun meeting attivo trovato");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
status?.Report("NZ: Nessuna pista trovata");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
totalErrors++;
|
||||
System.Diagnostics.Debug.WriteLine($"Errore fase NZ: {ex.Message}");
|
||||
}
|
||||
|
||||
currentPhase++;
|
||||
}
|
||||
|
||||
progress?.Report(100);
|
||||
string errMsg = errors > 0 ? $" ({errors} errori)" : "";
|
||||
status?.Report($"Trovati {dt.Rows.Count} corridori in {meetings.Count} meeting{errMsg}");
|
||||
string errMsg = totalErrors > 0 ? $" ({totalErrors} errori)" : "";
|
||||
string countries = string.Join("+", requestedCountries.Select(c => c.ToUpper()));
|
||||
status?.Report($"{countries}: {dt.Rows.Count} corridori in {totalMeetings} meeting{errMsg}");
|
||||
return dt;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -124,55 +312,85 @@ namespace HorseRacingPredictor.HorseRacing
|
||||
}
|
||||
}
|
||||
|
||||
#region DataTable creation
|
||||
#region Venues
|
||||
|
||||
private DataTable CreateRunnerTable()
|
||||
private class VenueInfo
|
||||
{
|
||||
var dt = new DataTable();
|
||||
// Campi corsa
|
||||
dt.Columns.Add("Ippodromo", typeof(string));
|
||||
dt.Columns.Add("Corsa N.", typeof(int));
|
||||
dt.Columns.Add("Nome Corsa", typeof(string));
|
||||
dt.Columns.Add("Orario", typeof(string));
|
||||
dt.Columns.Add("Distanza", typeof(string));
|
||||
dt.Columns.Add("Terreno", typeof(string));
|
||||
dt.Columns.Add("Classe", typeof(string));
|
||||
dt.Columns.Add("Meteo", typeof(string));
|
||||
dt.Columns.Add("Premio", typeof(string));
|
||||
dt.Columns.Add("N. Corridori", typeof(int));
|
||||
// Campi corridore
|
||||
dt.Columns.Add("Num", typeof(int));
|
||||
dt.Columns.Add("Cavallo", typeof(string));
|
||||
dt.Columns.Add("Fantino", typeof(string));
|
||||
dt.Columns.Add("Allenatore", typeof(string));
|
||||
dt.Columns.Add("Peso", typeof(string));
|
||||
dt.Columns.Add("Claim", typeof(string));
|
||||
dt.Columns.Add("Box", typeof(string));
|
||||
dt.Columns.Add("Età", typeof(string));
|
||||
dt.Columns.Add("Forma", typeof(string));
|
||||
dt.Columns.Add("Ultimi 20", typeof(string));
|
||||
dt.Columns.Add("Colori", typeof(string));
|
||||
dt.Columns.Add("Cambio Equip.", typeof(string));
|
||||
// Statistiche overall
|
||||
dt.Columns.Add("Vitt.", typeof(string));
|
||||
dt.Columns.Add("Piazz.", typeof(string));
|
||||
dt.Columns.Add("Partenze", typeof(string));
|
||||
dt.Columns.Add("% Vitt.", typeof(string));
|
||||
dt.Columns.Add("% Piazz.", typeof(string));
|
||||
// Statistiche pista
|
||||
dt.Columns.Add("Pista V/P/S", typeof(string));
|
||||
// Statistiche distanza
|
||||
dt.Columns.Add("Dist. V/P/S", typeof(string));
|
||||
// Statistiche condizione
|
||||
dt.Columns.Add("Cond. V/P/S", typeof(string));
|
||||
// Stato
|
||||
dt.Columns.Add("Ritirato", typeof(string));
|
||||
return dt;
|
||||
public string Name { get; set; }
|
||||
public string Slug { get; set; }
|
||||
public string Country { get; set; }
|
||||
}
|
||||
|
||||
private class ActiveVenue
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Slug { get; set; }
|
||||
public string Country { get; set; }
|
||||
public string FirstRaceContent { get; set; }
|
||||
}
|
||||
|
||||
private List<VenueInfo> GetFilteredVenues(string country, CancellationToken ct)
|
||||
{
|
||||
if (_venuesCache == null)
|
||||
_venuesCache = ParseVenues(_client.GetVenues(ct).Content);
|
||||
|
||||
return _venuesCache
|
||||
.Where(v => string.Equals(v.Country, country, StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.IsNullOrEmpty(v.Name))
|
||||
.OrderBy(v => v.Name)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static List<VenueInfo> ParseVenues(string json)
|
||||
{
|
||||
var venues = new List<VenueInfo>();
|
||||
if (string.IsNullOrEmpty(json)) return venues;
|
||||
|
||||
try
|
||||
{
|
||||
using (var doc = JsonDocument.Parse(json))
|
||||
{
|
||||
var root = doc.RootElement;
|
||||
|
||||
JsonElement arr;
|
||||
if (root.TryGetProperty("venues", out var venuesEl) &&
|
||||
venuesEl.ValueKind == JsonValueKind.Array)
|
||||
arr = venuesEl;
|
||||
else if (root.ValueKind == JsonValueKind.Array)
|
||||
arr = root;
|
||||
else
|
||||
return venues;
|
||||
|
||||
foreach (var v in arr.EnumerateArray())
|
||||
{
|
||||
try
|
||||
{
|
||||
string name = GetString(v, "name", "");
|
||||
string country = GetString(v, "country", "");
|
||||
string raceType = GetString(v, "raceType", "gallops");
|
||||
|
||||
if (raceType != "gallops") continue;
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
|
||||
venues.Add(new VenueInfo
|
||||
{
|
||||
Name = name,
|
||||
Slug = name.ToLowerInvariant().Replace(" ", "-"),
|
||||
Country = country
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return venues;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region JSON Parsing
|
||||
#region Meetings parsing (AU)
|
||||
|
||||
private class MeetingInfo
|
||||
{
|
||||
@@ -242,7 +460,53 @@ namespace HorseRacingPredictor.HorseRacing
|
||||
return meetings;
|
||||
}
|
||||
|
||||
private void ParseRaceFormIntoTable(DataTable dt, string json)
|
||||
#endregion
|
||||
|
||||
#region DataTable creation
|
||||
|
||||
private DataTable CreateRunnerTable()
|
||||
{
|
||||
var dt = new DataTable();
|
||||
dt.Columns.Add("Ippodromo", typeof(string));
|
||||
dt.Columns.Add("Paese", typeof(string));
|
||||
dt.Columns.Add("Corsa N.", typeof(int));
|
||||
dt.Columns.Add("Nome Corsa", typeof(string));
|
||||
dt.Columns.Add("Orario", typeof(string));
|
||||
dt.Columns.Add("Distanza", typeof(string));
|
||||
dt.Columns.Add("Terreno", typeof(string));
|
||||
dt.Columns.Add("Classe", typeof(string));
|
||||
dt.Columns.Add("Meteo", typeof(string));
|
||||
dt.Columns.Add("Premio", typeof(string));
|
||||
dt.Columns.Add("N. Corridori", typeof(int));
|
||||
dt.Columns.Add("Num", typeof(int));
|
||||
dt.Columns.Add("Cavallo", typeof(string));
|
||||
dt.Columns.Add("Fantino", typeof(string));
|
||||
dt.Columns.Add("Allenatore", typeof(string));
|
||||
dt.Columns.Add("Peso", typeof(string));
|
||||
dt.Columns.Add("Claim", typeof(string));
|
||||
dt.Columns.Add("Box", typeof(string));
|
||||
dt.Columns.Add("Eta'", typeof(string));
|
||||
dt.Columns.Add("Forma", typeof(string));
|
||||
dt.Columns.Add("Ultimi 20", typeof(string));
|
||||
dt.Columns.Add("Colori", typeof(string));
|
||||
dt.Columns.Add("Cambio Equip.", typeof(string));
|
||||
dt.Columns.Add("Vitt.", typeof(string));
|
||||
dt.Columns.Add("Piazz.", typeof(string));
|
||||
dt.Columns.Add("Partenze", typeof(string));
|
||||
dt.Columns.Add("% Vitt.", typeof(string));
|
||||
dt.Columns.Add("% Piazz.", typeof(string));
|
||||
dt.Columns.Add("Pista V/P/S", typeof(string));
|
||||
dt.Columns.Add("Dist. V/P/S", typeof(string));
|
||||
dt.Columns.Add("Cond. V/P/S", typeof(string));
|
||||
dt.Columns.Add("Ritirato", typeof(string));
|
||||
return dt;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region JSON Parsing
|
||||
|
||||
private void ParseRaceFormIntoTable(DataTable dt, string json, string fallbackCountry)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json)) return;
|
||||
|
||||
@@ -253,6 +517,7 @@ namespace HorseRacingPredictor.HorseRacing
|
||||
var root = doc.RootElement;
|
||||
|
||||
string track = GetString(root, "track", "");
|
||||
string country = GetString(root, "country", fallbackCountry);
|
||||
int raceNumber = GetInt(root, "raceNumber");
|
||||
string raceName = GetString(root, "raceName", "");
|
||||
string distance = GetString(root, "distance", "");
|
||||
@@ -263,21 +528,11 @@ namespace HorseRacingPredictor.HorseRacing
|
||||
string startTime = GetString(root, "startTime", "");
|
||||
int numberOfRunners = GetInt(root, "numberOfRunners");
|
||||
|
||||
// Formatta orario se è un ISO datetime
|
||||
string orario = "";
|
||||
if (!string.IsNullOrEmpty(startTime))
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = DateTimeOffset.Parse(startTime);
|
||||
var romeTz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");
|
||||
orario = TimeZoneInfo.ConvertTime(dto, romeTz).ToString("HH:mm");
|
||||
}
|
||||
catch
|
||||
{
|
||||
orario = startTime;
|
||||
}
|
||||
}
|
||||
string orario = FormatStartTime(startTime);
|
||||
|
||||
string countryDisplay = country.ToUpperInvariant();
|
||||
if (CountryNames.TryGetValue(country.ToLowerInvariant(), out var cn))
|
||||
countryDisplay = cn;
|
||||
|
||||
if (!root.TryGetProperty("runners", out var runnersEl) ||
|
||||
runnersEl.ValueKind != JsonValueKind.Array)
|
||||
@@ -289,8 +544,8 @@ namespace HorseRacingPredictor.HorseRacing
|
||||
{
|
||||
var row = dt.NewRow();
|
||||
|
||||
// Campi corsa
|
||||
row["Ippodromo"] = track;
|
||||
row["Paese"] = countryDisplay;
|
||||
row["Corsa N."] = raceNumber;
|
||||
row["Nome Corsa"] = raceName;
|
||||
row["Orario"] = orario;
|
||||
@@ -301,7 +556,6 @@ namespace HorseRacingPredictor.HorseRacing
|
||||
row["Premio"] = prizeMoney;
|
||||
row["N. Corridori"] = numberOfRunners;
|
||||
|
||||
// Campi corridore
|
||||
row["Num"] = GetInt(runner, "number");
|
||||
row["Cavallo"] = GetString(runner, "name", "");
|
||||
row["Fantino"] = GetString(runner, "jockey", "");
|
||||
@@ -315,7 +569,7 @@ namespace HorseRacingPredictor.HorseRacing
|
||||
row["Box"] = GetInt(runner, "barrier") > 0
|
||||
? GetInt(runner, "barrier").ToString()
|
||||
: GetString(runner, "barrier", "");
|
||||
row["Età"] = GetInt(runner, "age") > 0
|
||||
row["Eta'"] = GetInt(runner, "age") > 0
|
||||
? GetInt(runner, "age").ToString()
|
||||
: GetString(runner, "age", "");
|
||||
row["Forma"] = GetString(runner, "form", "");
|
||||
@@ -323,21 +577,20 @@ namespace HorseRacingPredictor.HorseRacing
|
||||
row["Colori"] = GetString(runner, "racingColours", "");
|
||||
row["Cambio Equip."] = GetString(runner, "gearChange", "");
|
||||
|
||||
// Statistiche overall
|
||||
if (runner.TryGetProperty("stats", out var statsEl))
|
||||
{
|
||||
ParseStatGroup(statsEl, "overall", row, "Vitt.", "Piazz.", "Partenze", "% Vitt.", "% Piazz.");
|
||||
|
||||
ParseStatGroup(statsEl, "overall", row,
|
||||
"Vitt.", "Piazz.", "Partenze", "% Vitt.", "% Piazz.");
|
||||
row["Pista V/P/S"] = FormatStatSummary(statsEl, "track");
|
||||
row["Dist. V/P/S"] = FormatStatSummary(statsEl, "distance");
|
||||
row["Cond. V/P/S"] = FormatStatSummary(statsEl, "condition");
|
||||
}
|
||||
|
||||
// Ritirato
|
||||
bool scratched = false;
|
||||
if (runner.TryGetProperty("scratched", out var scEl) && scEl.ValueKind == JsonValueKind.True)
|
||||
if (runner.TryGetProperty("scratched", out var scEl) &&
|
||||
scEl.ValueKind == JsonValueKind.True)
|
||||
scratched = true;
|
||||
row["Ritirato"] = scratched ? "Sì" : "";
|
||||
row["Ritirato"] = scratched ? "Si" : "";
|
||||
|
||||
dt.Rows.Add(row);
|
||||
}
|
||||
@@ -348,9 +601,30 @@ namespace HorseRacingPredictor.HorseRacing
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estrae wins, places, starts, winPercent, placePercent da un sotto-oggetto stats
|
||||
/// </summary>
|
||||
private static string FormatStartTime(string startTime)
|
||||
{
|
||||
if (string.IsNullOrEmpty(startTime)) return "";
|
||||
|
||||
try
|
||||
{
|
||||
var dto = DateTimeOffset.Parse(startTime);
|
||||
// Converti al fuso orario locale di Roma
|
||||
try
|
||||
{
|
||||
var romeTz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");
|
||||
return TimeZoneInfo.ConvertTime(dto, romeTz).ToString("HH:mm");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return dto.ToLocalTime().ToString("HH:mm");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return startTime;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseStatGroup(JsonElement statsEl, string group,
|
||||
DataRow row, string winsCol, string placesCol, string startsCol,
|
||||
string winPctCol, string placePctCol)
|
||||
@@ -370,9 +644,6 @@ namespace HorseRacingPredictor.HorseRacing
|
||||
row[placePctCol] = (placePct * 100).ToString("F0") + "%";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formatta un riassunto "V-P/S" per un sotto-gruppo statistico (es. track, distance, condition)
|
||||
/// </summary>
|
||||
private static string FormatStatSummary(JsonElement statsEl, string group)
|
||||
{
|
||||
if (!statsEl.TryGetProperty(group, out var g)) return "";
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace HorseRacingPredictor
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides centralised access to application configuration loaded from appsettings.json.
|
||||
/// Connection strings and API keys that were previously hard-coded are now read from here.
|
||||
/// User-editable preferences (export paths, date formats, …) remain in settings.ini.
|
||||
/// </summary>
|
||||
internal static class AppConfig
|
||||
{
|
||||
private static IConfiguration _configuration;
|
||||
|
||||
public static IConfiguration Configuration => _configuration ??= BuildConfiguration();
|
||||
|
||||
private static IConfiguration BuildConfiguration()
|
||||
{
|
||||
var basePath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
return new ConfigurationBuilder()
|
||||
.SetBasePath(basePath)
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.AddJsonFile(
|
||||
$"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json",
|
||||
optional: true,
|
||||
reloadOnChange: true)
|
||||
.Build();
|
||||
}
|
||||
|
||||
// ?? Connection strings ??????????????????????????????????
|
||||
public static string FootballConnectionString =>
|
||||
Configuration.GetConnectionString("Football");
|
||||
|
||||
public static string HorsesConnectionString =>
|
||||
Configuration.GetConnectionString("Horses");
|
||||
|
||||
// ?? API settings ????????????????????????????????????????
|
||||
public static string FootballApiKey =>
|
||||
Configuration["Api:FootballApiKey"] ?? string.Empty;
|
||||
|
||||
public static string FootballApiKeyHeader =>
|
||||
Configuration["Api:FootballApiKeyHeader"] ?? "x-rapidapi-key";
|
||||
|
||||
public static string FootballApiHost =>
|
||||
Configuration["Api:FootballApiHost"] ?? "v3.football.api-sports.io";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Football": "Server=DESKTOP-9O9JHFS;Database=TestBS_Football;User Id=sa;Password=Asti2019;TrustServerCertificate=True",
|
||||
"Horses": "Server=DESKTOP-9O9JHFS;Database=TestBS_Horses;User Id=sa;Password=Asti2019;TrustServerCertificate=True"
|
||||
},
|
||||
"Api": {
|
||||
"FootballApiKey": "f3795ccef056c5478d316162517d9970",
|
||||
"FootballApiKeyHeader": "x-rapidapi-key",
|
||||
"FootballApiHost": "v3.football.api-sports.io"
|
||||
}
|
||||
}
|
||||
@@ -10,16 +10,14 @@ namespace HorseRacingPredictor.Horses
|
||||
{
|
||||
internal class Database : HorseRacingPredictor.Manager.Database
|
||||
{
|
||||
// Implementazione della proprietà astratta _connectionString per il database cavalli
|
||||
protected override string _connectionString => "Server=DESKTOP-9O9JHFS;Database=TestBS_Horses;User Id=sa;Password=Asti2019;";
|
||||
|
||||
private readonly FileReader fileReaderHorses;
|
||||
|
||||
public Database()
|
||||
// Connection string caricata da appsettings.json
|
||||
public Database() : base(AppConfig.HorsesConnectionString)
|
||||
{
|
||||
fileReaderHorses = new FileReader();
|
||||
}
|
||||
|
||||
private readonly FileReader fileReaderHorses;
|
||||
|
||||
public DataTable GetAllHorseRaceData()
|
||||
{
|
||||
DataTable horseRaceData = new DataTable();
|
||||
|
||||
@@ -644,11 +644,6 @@
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<DatePicker x:Name="dpRacing" Width="160"/>
|
||||
<ComboBox x:Name="cmbRaceCode" Width="120" Margin="8,0,0,0" SelectedIndex="0">
|
||||
<ComboBoxItem Content="Galoppo"/>
|
||||
<ComboBoxItem Content="Trotto"/>
|
||||
<ComboBoxItem Content="Levrieri"/>
|
||||
</ComboBox>
|
||||
<Button x:Name="btnDownloadRc" Content="Scarica Corse"
|
||||
Style="{StaticResource AccentBtn}"
|
||||
Background="{StaticResource BrBlue}" Margin="12,0,0,0"
|
||||
@@ -792,6 +787,73 @@
|
||||
<TextBox x:Name="txtRacingApiKey" Style="{StaticResource FlatTb}" MaxWidth="510" HorizontalAlignment="Left"/>
|
||||
|
||||
<Grid Margin="0,14,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="245"/>
|
||||
<ColumnDefinition Width="22"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="Timezone (IANA)" Foreground="{StaticResource BrSubtext0}" FontSize="11" Margin="0,0,0,4"/>
|
||||
<TextBox x:Name="txtRcTimezone" Style="{StaticResource FlatTb}" Text="Australia/Sydney"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2">
|
||||
<TextBlock Text="Nazioni" Foreground="{StaticResource BrSubtext0}" FontSize="11" Margin="0,0,0,4"/>
|
||||
<Grid>
|
||||
<ToggleButton x:Name="btnRcCountriesToggle"
|
||||
Height="34" HorizontalContentAlignment="Left"
|
||||
Padding="10,0,28,0" Cursor="Hand">
|
||||
<ToggleButton.Template>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Grid>
|
||||
<Border x:Name="Bd" CornerRadius="6"
|
||||
Background="{StaticResource BrSurface0}"
|
||||
BorderBrush="{StaticResource BrSurface1}"
|
||||
BorderThickness="1"/>
|
||||
<ContentPresenter Margin="10,0,28,0"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left"/>
|
||||
<Path Data="M 0,0 L 4,4 L 8,0"
|
||||
Stroke="{StaticResource BrSubtext0}" StrokeThickness="1.5"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource BrOverlay0}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource BrBlue}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</ToggleButton.Template>
|
||||
<TextBlock x:Name="lblRcCountriesSummary" Text="AU, NZ"
|
||||
Foreground="{StaticResource BrText}" FontSize="13"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</ToggleButton>
|
||||
<Popup x:Name="popupRcCountries" Placement="Bottom"
|
||||
StaysOpen="False" AllowsTransparency="True"
|
||||
IsOpen="{Binding IsChecked, ElementName=btnRcCountriesToggle, Mode=TwoWay}">
|
||||
<Border Background="{StaticResource BrSurface1}"
|
||||
BorderBrush="{StaticResource BrSurface2}" BorderThickness="1"
|
||||
CornerRadius="8" Padding="6,8" Margin="0,4,0,0"
|
||||
MinWidth="260" MaxHeight="340">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="16" ShadowDepth="4" Color="#40000000"/>
|
||||
</Border.Effect>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel x:Name="pnlRcCountries"/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Border Height="1" Background="{StaticResource BrBorder}" Margin="0,14,0,14"/>
|
||||
|
||||
<Grid Margin="0,0,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="245"/>
|
||||
<ColumnDefinition Width="22"/>
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace HorseRacingPredictor
|
||||
InitializeComponent();
|
||||
_footballManager = new Football.Main();
|
||||
_racingManager = new HorseRacing.Main(DefaultRacingApiKey);
|
||||
BuildCountryCheckboxes();
|
||||
// Wire preview update events
|
||||
txtFbPrefix.TextChanged += (s, e) => UpdateFbPreview();
|
||||
txtFbSuffix.TextChanged += (s, e) => UpdateFbPreview();
|
||||
@@ -334,13 +335,111 @@ namespace HorseRacingPredictor
|
||||
|
||||
// ???????????????????? HORSE RACING ????????????????????
|
||||
|
||||
private readonly Dictionary<string, CheckBox> _countryCheckboxes = new Dictionary<string, CheckBox>();
|
||||
|
||||
private void BuildCountryCheckboxes()
|
||||
{
|
||||
if (pnlRcCountries == null) return;
|
||||
pnlRcCountries.Children.Clear();
|
||||
_countryCheckboxes.Clear();
|
||||
|
||||
var supported = new HashSet<string>(HorseRacing.Main.SupportedCountries);
|
||||
|
||||
// Header: nazioni con dati
|
||||
pnlRcCountries.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "Con dati disponibili",
|
||||
FontSize = 10,
|
||||
FontFamily = new System.Windows.Media.FontFamily("Segoe UI Semibold"),
|
||||
Foreground = FindResource("BrBlue") as System.Windows.Media.Brush,
|
||||
Margin = new Thickness(6, 2, 0, 4)
|
||||
});
|
||||
|
||||
foreach (var code in HorseRacing.Main.SupportedCountries)
|
||||
AddCountryCheckbox(code, supported, true);
|
||||
|
||||
// Separator
|
||||
pnlRcCountries.Children.Add(new Border
|
||||
{
|
||||
Height = 1,
|
||||
Background = FindResource("BrBorder") as System.Windows.Media.Brush,
|
||||
Margin = new Thickness(4, 6, 4, 6)
|
||||
});
|
||||
|
||||
// Header: catalogo
|
||||
pnlRcCountries.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "Solo catalogo (nessun dato di forma)",
|
||||
FontSize = 10,
|
||||
Foreground = FindResource("BrOverlay0") as System.Windows.Media.Brush,
|
||||
Margin = new Thickness(6, 2, 0, 4)
|
||||
});
|
||||
|
||||
foreach (var code in HorseRacing.Main.AllCountries)
|
||||
{
|
||||
if (supported.Contains(code)) continue;
|
||||
AddCountryCheckbox(code, supported, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCountryCheckbox(string code, HashSet<string> supported, bool isSupported)
|
||||
{
|
||||
string label = HorseRacing.Main.CountryNames.TryGetValue(code, out var name)
|
||||
? $"{name} ({code.ToUpper()})"
|
||||
: code.ToUpper();
|
||||
|
||||
var cb = new CheckBox
|
||||
{
|
||||
Content = label,
|
||||
Tag = code,
|
||||
IsChecked = false,
|
||||
Margin = new Thickness(4, 2, 4, 2),
|
||||
FontSize = 12,
|
||||
Foreground = isSupported
|
||||
? FindResource("BrText") as System.Windows.Media.Brush
|
||||
: FindResource("BrOverlay0") as System.Windows.Media.Brush,
|
||||
Opacity = isSupported ? 1.0 : 0.7
|
||||
};
|
||||
cb.Checked += (s, e) => UpdateCountriesSummary();
|
||||
cb.Unchecked += (s, e) => UpdateCountriesSummary();
|
||||
|
||||
_countryCheckboxes[code] = cb;
|
||||
pnlRcCountries.Children.Add(cb);
|
||||
}
|
||||
|
||||
private List<string> GetSelectedCountries()
|
||||
{
|
||||
return _countryCheckboxes
|
||||
.Where(kv => kv.Value.IsChecked == true)
|
||||
.Select(kv => kv.Key)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void SetSelectedCountries(IEnumerable<string> codes)
|
||||
{
|
||||
var set = new HashSet<string>(codes.Select(c => c.Trim().ToLowerInvariant()));
|
||||
foreach (var kv in _countryCheckboxes)
|
||||
kv.Value.IsChecked = set.Contains(kv.Key);
|
||||
UpdateCountriesSummary();
|
||||
}
|
||||
|
||||
private void UpdateCountriesSummary()
|
||||
{
|
||||
var selected = GetSelectedCountries();
|
||||
if (lblRcCountriesSummary != null)
|
||||
{
|
||||
lblRcCountriesSummary.Text = selected.Count > 0
|
||||
? string.Join(", ", selected.Select(c => c.ToUpper()))
|
||||
: "Nessuna";
|
||||
}
|
||||
}
|
||||
|
||||
private void rbRcSource_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Toggle visibility of API vs CSV controls
|
||||
if (dpRacing == null || btnDownloadRc == null || btnBrowseCsvRc == null) return;
|
||||
bool isApi = rbRcApi.IsChecked == true;
|
||||
dpRacing.Visibility = isApi ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (cmbRaceCode != null) cmbRaceCode.Visibility = isApi ? Visibility.Visible : Visibility.Collapsed;
|
||||
btnDownloadRc.Visibility = isApi ? Visibility.Visible : Visibility.Collapsed;
|
||||
btnBrowseCsvRc.Visibility = isApi ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
@@ -528,20 +627,9 @@ namespace HorseRacingPredictor
|
||||
r[col] = n++;
|
||||
}
|
||||
|
||||
private string GetSelectedRaceCode()
|
||||
{
|
||||
int idx = cmbRaceCode?.SelectedIndex ?? 0;
|
||||
switch (idx)
|
||||
{
|
||||
case 1: return "harness";
|
||||
case 2: return "greyhounds";
|
||||
default: return "gallops";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadRacecardsAsync()
|
||||
{
|
||||
// Se è già in corso, annulla
|
||||
// Se e' gia' in corso, annulla
|
||||
if (_racingCts != null)
|
||||
{
|
||||
_racingCts.Cancel();
|
||||
@@ -557,20 +645,21 @@ namespace HorseRacingPredictor
|
||||
try
|
||||
{
|
||||
pbRacing.Value = 0;
|
||||
lblStatusRc.Text = "Scaricamento corse da FormFav…";
|
||||
lblStatusRc.Text = "Scaricamento corse da FormFav...";
|
||||
btnDownloadRc.Content = "Annulla";
|
||||
dpRacing.IsEnabled = false;
|
||||
cmbRaceCode.IsEnabled = false;
|
||||
btnExportRcCsv.IsEnabled = false;
|
||||
|
||||
// Applica impostazioni correnti al manager
|
||||
ApplyRacingSettings();
|
||||
|
||||
var progress = new Progress<int>(v => pbRacing.Value = v);
|
||||
var status = new Progress<string>(s => lblStatusRc.Text = s);
|
||||
|
||||
var date = dpRacing.SelectedDate ?? DateTime.Today;
|
||||
string raceCode = GetSelectedRaceCode();
|
||||
|
||||
var table = await Task.Run(() =>
|
||||
_racingManager.GetAllRacesForDate(date, raceCode, progress, status, ct), ct);
|
||||
_racingManager.GetAllRacesForDate(date, progress, status, ct), ct);
|
||||
|
||||
_racingData = table;
|
||||
|
||||
@@ -606,10 +695,22 @@ namespace HorseRacingPredictor
|
||||
_racingCts = null;
|
||||
btnDownloadRc.Content = "Scarica Corse";
|
||||
dpRacing.IsEnabled = true;
|
||||
cmbRaceCode.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRacingSettings()
|
||||
{
|
||||
_racingManager.UpdateApiKey(txtRacingApiKey.Text.Trim());
|
||||
|
||||
var tz = txtRcTimezone?.Text?.Trim();
|
||||
if (!string.IsNullOrEmpty(tz))
|
||||
_racingManager.Timezone = tz;
|
||||
|
||||
var selected = GetSelectedCountries();
|
||||
if (selected.Count > 0)
|
||||
_racingManager.Countries = selected;
|
||||
}
|
||||
|
||||
private void btnExportRcCsv_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var rcDate = dpRacing.SelectedDate ?? DateTime.Today;
|
||||
@@ -741,7 +842,10 @@ namespace HorseRacingPredictor
|
||||
{
|
||||
txtRacingApiKey.Text = DefaultRacingApiKey;
|
||||
|
||||
if (!File.Exists(SettingsFilePath)) return;
|
||||
// Default countries
|
||||
SetSelectedCountries(new[] { "au", "nz" });
|
||||
|
||||
if (!File.Exists(SettingsFilePath)) { ApplyRacingSettings(); return; }
|
||||
foreach (var line in File.ReadAllLines(SettingsFilePath))
|
||||
{
|
||||
var idx = line.IndexOf('=');
|
||||
@@ -763,13 +867,19 @@ namespace HorseRacingPredictor
|
||||
else if (key == "RcDateFormat") { try { SetComboBoxSelectionByContent(cmbRcDateFormat, val); } catch { } }
|
||||
else if (key == "RcFormat") { try { SetComboBoxSelectionByContent(cmbRcFormat, val); } catch { } }
|
||||
else if (key == "RacingApiKey") txtRacingApiKey.Text = val;
|
||||
else if (key == "RcTimezone" && txtRcTimezone != null) txtRcTimezone.Text = val;
|
||||
else if (key == "RcCountries")
|
||||
{
|
||||
var codes = val.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
SetSelectedCountries(codes);
|
||||
}
|
||||
}
|
||||
|
||||
// Update preview UI after loading values
|
||||
UpdateFbPreview();
|
||||
UpdateRcPreview();
|
||||
|
||||
_racingManager.UpdateApiKey(txtRacingApiKey.Text);
|
||||
ApplyRacingSettings();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
@@ -1015,13 +1125,15 @@ namespace HorseRacingPredictor
|
||||
sb.AppendLine($"RcDateFormat={(cmbRcDateFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "yyyy-MM-dd"}");
|
||||
sb.AppendLine($"RcFormat={(cmbRcFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV"}");
|
||||
sb.AppendLine($"RacingApiKey={txtRacingApiKey.Text.Trim()}");
|
||||
sb.AppendLine($"RcTimezone={txtRcTimezone?.Text?.Trim() ?? "Australia/Sydney"}");
|
||||
sb.AppendLine($"RcCountries={string.Join(",", GetSelectedCountries())}");
|
||||
File.WriteAllText(SettingsFilePath, sb.ToString(), Encoding.UTF8);
|
||||
|
||||
// update previews after save
|
||||
UpdateFbPreview();
|
||||
UpdateRcPreview();
|
||||
|
||||
_racingManager.UpdateApiKey(txtRacingApiKey.Text.Trim());
|
||||
ApplyRacingSettings();
|
||||
|
||||
MessageBox.Show("Impostazioni salvate con successo.",
|
||||
"Salvato", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
|
||||
@@ -5,8 +5,12 @@ namespace HorseRacingPredictor.Manager
|
||||
{
|
||||
internal abstract class Database
|
||||
{
|
||||
// La stringa di connessione viene rimossa da qui e definita nelle classi derivate
|
||||
protected abstract string _connectionString { get; }
|
||||
protected readonly string _connectionString;
|
||||
|
||||
protected Database(string connectionString)
|
||||
{
|
||||
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
|
||||
}
|
||||
|
||||
protected SqlConnection GetConnection()
|
||||
{
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace BettingPredictor.Properties
|
||||
{
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
|
||||
{
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
||||
public static Settings Default
|
||||
{
|
||||
get
|
||||
{
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
|
||||
<Profiles>
|
||||
<Profile Name="(Default)" />
|
||||
</Profiles>
|
||||
<Settings />
|
||||
</SettingsFile>
|
||||
Reference in New Issue
Block a user