Migrate app.config to appsettings.json with Microsoft.Extensions.Configuration
This commit is contained in:
@@ -94,3 +94,23 @@ Status: Complete
|
|||||||
### Outcome
|
### Outcome
|
||||||
Success - Solution builds with 0 errors on .NET 10.0.
|
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 |
|
| ? Complete | 1 |
|
||||||
| ? In Progress | 0 |
|
| ? In Progress | 0 |
|
||||||
| ? Not Started | 6 |
|
| ? Not Started | 6 |
|
||||||
**Progress**: 6/7 tasks complete (86%) 
|
**Progress**: 7/7 tasks complete (100%) 
|
||||||
| ? Skipped | 0 |
|
| ? Skipped | 0 |
|
||||||
| **Total** | **7** |
|
| **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
|
**Scope**: Entire solution
|
||||||
**References**: Plan §10, §11
|
**References**: Plan §10, §11
|
||||||
|
|
||||||
**Actions:**
|
**Actions:**
|
||||||
- [ ] (1) Verify all success criteria from Plan §11:
|
- [?] (1) Verify all success criteria from Plan ?11:
|
||||||
- Project targets net10.0-windows
|
- Project targets net10.0-windows
|
||||||
- Project uses SDK-style format
|
- Project uses SDK-style format
|
||||||
- All 9 packages updated to stable versions
|
- All 9 packages updated to stable versions
|
||||||
@@ -116,5 +116,5 @@
|
|||||||
- Microsoft.Data.SqlClient added and usages migrated
|
- Microsoft.Data.SqlClient added and usages migrated
|
||||||
- System.Configuration.ConfigurationManager added
|
- System.Configuration.ConfigurationManager added
|
||||||
- Solution builds with 0 errors
|
- 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`
|
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>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
<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.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.Data.SqlClient" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.ML" Version="5.0.0-preview.25503.2" />
|
<PackageReference Include="Microsoft.ML" Version="6.0.0-preview.26160.2" />
|
||||||
<PackageReference Include="Microsoft.ML.CpuMath" Version="5.0.0-preview.25503.2" />
|
<PackageReference Include="Microsoft.ML.CpuMath" Version="6.0.0-preview.26160.2" />
|
||||||
<PackageReference Include="Microsoft.ML.DataView" Version="5.0.0-preview.25503.2" />
|
<PackageReference Include="Microsoft.ML.DataView" Version="6.0.0-preview.26160.2" />
|
||||||
<PackageReference Include="Microsoft.ML.FastTree" Version="5.0.0-preview.25503.2" />
|
<PackageReference Include="Microsoft.ML.FastTree" Version="6.0.0-preview.26160.2" />
|
||||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3800.47" />
|
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3908-prerelease" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.5-beta1" />
|
||||||
<PackageReference Include="RestSharp" Version="112.1.1-alpha.0.4" />
|
<PackageReference Include="RestSharp" Version="114.0.0" />
|
||||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.5" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.5" />
|
||||||
<PackageReference Include="System.Numerics.Tensors" Version="10.0.5" />
|
<PackageReference Include="System.Numerics.Tensors" Version="11.0.0-preview.2.26159.112" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Compile items now included by globbing that were not in the original project file">
|
<ItemGroup Label="Compile items now included by globbing that were not in the original project file">
|
||||||
<Compile Remove="UI\NavButton.cs" />
|
<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
|
internal class API : HorseRacingPredictor.Manager.API
|
||||||
{
|
{
|
||||||
// Configurazione dell'API
|
// Configurazione dell'API – caricata da appsettings.json
|
||||||
protected const string ApiKey = "f3795ccef056c5478d316162517d9970";
|
protected static string ApiKey => AppConfig.FootballApiKey;
|
||||||
protected const string KeyHeader = "x-rapidapi-key";
|
protected static string KeyHeader => AppConfig.FootballApiKeyHeader;
|
||||||
protected const string HostHeader = "x-rapidapi-host";
|
protected const string HostHeader = "x-rapidapi-host";
|
||||||
protected const string HostValue = "v3.football.api-sports.io";
|
protected static string HostValue => AppConfig.FootballApiHost;
|
||||||
protected const string BaseUrl = "https://v3.football.api-sports.io";
|
protected static string BaseUrl => $"https://{AppConfig.FootballApiHost}";
|
||||||
|
|
||||||
// Repository per le risposte API
|
// Repository per le risposte API
|
||||||
private readonly APIResponse _apiResponseRepository;
|
private readonly APIResponse _apiResponseRepository;
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ namespace HorseRacingPredictor.Football.Manager
|
|||||||
{
|
{
|
||||||
internal class Database : HorseRacingPredictor.Manager.Database
|
internal class Database : HorseRacingPredictor.Manager.Database
|
||||||
{
|
{
|
||||||
// Implementazione della proprietà astratta _connectionString per il database calcio
|
public Database() : base(AppConfig.FootballConnectionString) { }
|
||||||
protected override string _connectionString => "Server=DESKTOP-9O9JHFS;Database=TestBS_Football;User Id=sa;Password=Asti2019;";
|
|
||||||
|
|
||||||
// Usato il modificatore "new" per evitare il warning CS0108
|
// Usato il modificatore "new" per evitare il warning CS0108
|
||||||
protected new void LogError(string operation, Exception ex)
|
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.
|
/// Esegue una richiesta GET autenticata con X-API-Key header.
|
||||||
/// Rispetta un intervallo minimo tra richieste e gestisce HTTP 429 con retry.
|
/// Rispetta un intervallo minimo tra richieste e gestisce HTTP 429 con retry.
|
||||||
/// </summary>
|
/// </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();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
@@ -67,7 +69,7 @@ namespace HorseRacingPredictor.HorseRacing.API
|
|||||||
{
|
{
|
||||||
var response = client.Execute(request);
|
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)
|
if (response.StatusCode == (HttpStatusCode)429 && attempt <= MaxRetries)
|
||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine(
|
System.Diagnostics.Debug.WriteLine(
|
||||||
@@ -81,6 +83,10 @@ namespace HorseRacingPredictor.HorseRacing.API
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 404 Not Found - restituisci null se richiesto
|
||||||
|
if (response.StatusCode == HttpStatusCode.NotFound && !throwOnNotFound)
|
||||||
|
return null;
|
||||||
|
|
||||||
if (!response.IsSuccessful)
|
if (!response.IsSuccessful)
|
||||||
{
|
{
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
@@ -98,10 +104,10 @@ namespace HorseRacingPredictor.HorseRacing.API
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ottiene l'elenco dei meeting per una data
|
/// Ottiene l'elenco dei meeting per una data (solo meeting australiani).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public RestResponse GetMeetings(DateTime date, string raceCode = "gallops",
|
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?");
|
var sb = new StringBuilder("form/meetings?");
|
||||||
sb.Append($"date={date:yyyy-MM-dd}");
|
sb.Append($"date={date:yyyy-MM-dd}");
|
||||||
@@ -114,11 +120,38 @@ namespace HorseRacingPredictor.HorseRacing.API
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public RestResponse GetRaceForm(DateTime date, string track, int raceNumber,
|
public RestResponse GetRaceForm(DateTime date, string track, int raceNumber,
|
||||||
string raceCode = "gallops", string country = "au",
|
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?");
|
var sb = new StringBuilder("form?");
|
||||||
sb.Append($"date={date:yyyy-MM-dd}");
|
sb.Append($"date={date:yyyy-MM-dd}");
|
||||||
@@ -130,16 +163,7 @@ namespace HorseRacingPredictor.HorseRacing.API
|
|||||||
sb.Append($"&country={country}");
|
sb.Append($"&country={country}");
|
||||||
if (!string.IsNullOrEmpty(timezone))
|
if (!string.IsNullOrEmpty(timezone))
|
||||||
sb.Append($"&timezone={Uri.EscapeDataString(timezone)}");
|
sb.Append($"&timezone={Uri.EscapeDataString(timezone)}");
|
||||||
|
return sb.ToString();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using HorseRacingPredictor.HorseRacing.API;
|
using HorseRacingPredictor.HorseRacing.API;
|
||||||
@@ -10,30 +11,66 @@ namespace HorseRacingPredictor.HorseRacing
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gestore centralizzato per la sezione Corse dei Cavalli.
|
/// Gestore centralizzato per la sezione Corse dei Cavalli.
|
||||||
/// Scarica i dati da FormFav Racing API e li converte in DataTable.
|
/// 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>
|
/// </summary>
|
||||||
public class Main
|
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 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)
|
public Main(string apiKey)
|
||||||
{
|
{
|
||||||
_client = new RacingApiClient(apiKey);
|
_client = new RacingApiClient(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Aggiorna la API key
|
|
||||||
/// </summary>
|
|
||||||
public void UpdateApiKey(string apiKey)
|
public void UpdateApiKey(string apiKey)
|
||||||
{
|
{
|
||||||
_client = new RacingApiClient(apiKey);
|
_client = new RacingApiClient(apiKey);
|
||||||
|
_venuesCache = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Scarica tutti i meeting per una data, poi per ciascun meeting scarica tutte le corse
|
/// Scarica tutte le corse per una data.
|
||||||
/// e le restituisce come DataTable con una riga per runner.
|
/// - Per AU: usa /form/meetings (efficiente, restituisce numero corse)
|
||||||
/// La progress bar avanza per singola corsa scaricata.
|
/// - Per NZ: usa probing venue per venue
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DataTable GetAllRacesForDate(DateTime date, string raceCode = "gallops",
|
public DataTable GetAllRacesForDate(DateTime date,
|
||||||
IProgress<int> progress = null, IProgress<string> status = null,
|
IProgress<int> progress = null, IProgress<string> status = null,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -41,75 +78,226 @@ namespace HorseRacingPredictor.HorseRacing
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
status?.Report("Connessione a FormFav API...");
|
// Filtra solo nazioni supportate
|
||||||
|
var requestedCountries = Countries
|
||||||
|
.Select(c => c.ToLowerInvariant())
|
||||||
|
.Where(c => SupportedCountries.Contains(c))
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (requestedCountries.Count == 0)
|
||||||
|
{
|
||||||
|
status?.Report("Nessuna nazione supportata selezionata. Usa: AU, NZ");
|
||||||
|
progress?.Report(100);
|
||||||
|
return dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool doAu = requestedCountries.Contains("au");
|
||||||
|
bool doNz = requestedCountries.Contains("nz");
|
||||||
|
|
||||||
|
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("AU: Recupero elenco meeting...");
|
||||||
progress?.Report(2);
|
progress?.Report(2);
|
||||||
|
|
||||||
// 1. Ottieni l'elenco dei meeting per la data
|
int phaseBase = 0;
|
||||||
var meetingsResp = _client.GetMeetings(date, raceCode, ct: ct);
|
int phaseSpan = doNz ? 50 : 95; // Se c'e' anche NZ, AU occupa 0-50%
|
||||||
progress?.Report(8);
|
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var meetingsResp = _client.GetMeetings(date, "gallops", Timezone, ct);
|
||||||
var meetings = ParseMeetings(meetingsResp.Content);
|
var meetings = ParseMeetings(meetingsResp.Content);
|
||||||
if (meetings.Count == 0)
|
|
||||||
{
|
|
||||||
status?.Report("Nessun meeting trovato per questa data");
|
|
||||||
progress?.Report(100);
|
|
||||||
return dt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Calcola il totale corse per un progresso granulare
|
if (meetings.Count > 0)
|
||||||
int totalRaces = 0;
|
{
|
||||||
|
// Calcola totale corse AU
|
||||||
|
int totalAuRaces = 0;
|
||||||
foreach (var m in meetings)
|
foreach (var m in meetings)
|
||||||
if (!m.Abandoned) totalRaces += m.NumberOfRaces;
|
if (!m.Abandoned) totalAuRaces += m.NumberOfRaces;
|
||||||
|
|
||||||
if (totalRaces == 0)
|
status?.Report($"AU: {meetings.Count} meeting ({totalAuRaces} corse)");
|
||||||
{
|
|
||||||
status?.Report("Tutti i meeting sono stati annullati");
|
|
||||||
progress?.Report(100);
|
|
||||||
return dt;
|
|
||||||
}
|
|
||||||
|
|
||||||
status?.Report($"Trovati {meetings.Count} meeting ({totalRaces} corse). Scaricamento...");
|
int completedAuRaces = 0;
|
||||||
int completedRaces = 0;
|
|
||||||
int errors = 0;
|
|
||||||
|
|
||||||
// 3. Scarica ogni singola corsa
|
|
||||||
foreach (var meeting in meetings)
|
foreach (var meeting in meetings)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
if (meeting.Abandoned) continue;
|
if (meeting.Abandoned) continue;
|
||||||
|
|
||||||
|
totalMeetings++;
|
||||||
|
|
||||||
for (int raceNum = 1; raceNum <= meeting.NumberOfRaces; raceNum++)
|
for (int raceNum = 1; raceNum <= meeting.NumberOfRaces; raceNum++)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
status?.Report($"AU: {meeting.Track} R{raceNum}/{meeting.NumberOfRaces} " +
|
||||||
|
$"({completedAuRaces + 1}/{totalAuRaces})");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
status?.Report($"{meeting.Track} R{raceNum}/{meeting.NumberOfRaces} " +
|
var formResp = _client.GetRaceForm(date, meeting.TrackSlug,
|
||||||
$"({completedRaces + 1}/{totalRaces})");
|
raceNum, "gallops", "au", Timezone, ct);
|
||||||
|
ParseRaceFormIntoTable(dt, formResp.Content, "au");
|
||||||
var formResp = _client.GetRaceForm(date, meeting.TrackSlug, raceNum,
|
|
||||||
raceCode, meeting.Country, ct: ct);
|
|
||||||
|
|
||||||
ParseRaceFormIntoTable(dt, formResp.Content);
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { throw; }
|
catch (OperationCanceledException) { throw; }
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
errors++;
|
totalErrors++;
|
||||||
System.Diagnostics.Debug.WriteLine(
|
System.Diagnostics.Debug.WriteLine(
|
||||||
$"Errore scaricamento {meeting.Track} R{raceNum}: {ex.Message}");
|
$"Errore AU {meeting.Track} R{raceNum}: {ex.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
completedRaces++;
|
completedAuRaces++;
|
||||||
// Progresso: 8% per meetings, 8-98% per le corse singole, 100% alla fine
|
int pct = phaseBase + (int)((double)completedAuRaces / Math.Max(totalAuRaces, 1) * phaseSpan);
|
||||||
int pct = 8 + (int)((double)completedRaces / totalRaces * 90);
|
progress?.Report(Math.Min(pct, phaseBase + phaseSpan));
|
||||||
progress?.Report(Math.Min(pct, 98));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
status?.Report("AU: Nessun meeting trovato");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
progress?.Report(100);
|
||||||
string errMsg = errors > 0 ? $" ({errors} errori)" : "";
|
string errMsg = totalErrors > 0 ? $" ({totalErrors} errori)" : "";
|
||||||
status?.Report($"Trovati {dt.Rows.Count} corridori in {meetings.Count} meeting{errMsg}");
|
string countries = string.Join("+", requestedCountries.Select(c => c.ToUpper()));
|
||||||
|
status?.Report($"{countries}: {dt.Rows.Count} corridori in {totalMeetings} meeting{errMsg}");
|
||||||
return dt;
|
return dt;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
@@ -124,55 +312,85 @@ namespace HorseRacingPredictor.HorseRacing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region DataTable creation
|
#region Venues
|
||||||
|
|
||||||
private DataTable CreateRunnerTable()
|
private class VenueInfo
|
||||||
{
|
{
|
||||||
var dt = new DataTable();
|
public string Name { get; set; }
|
||||||
// Campi corsa
|
public string Slug { get; set; }
|
||||||
dt.Columns.Add("Ippodromo", typeof(string));
|
public string Country { get; set; }
|
||||||
dt.Columns.Add("Corsa N.", typeof(int));
|
}
|
||||||
dt.Columns.Add("Nome Corsa", typeof(string));
|
|
||||||
dt.Columns.Add("Orario", typeof(string));
|
private class ActiveVenue
|
||||||
dt.Columns.Add("Distanza", typeof(string));
|
{
|
||||||
dt.Columns.Add("Terreno", typeof(string));
|
public string Name { get; set; }
|
||||||
dt.Columns.Add("Classe", typeof(string));
|
public string Slug { get; set; }
|
||||||
dt.Columns.Add("Meteo", typeof(string));
|
public string Country { get; set; }
|
||||||
dt.Columns.Add("Premio", typeof(string));
|
public string FirstRaceContent { get; set; }
|
||||||
dt.Columns.Add("N. Corridori", typeof(int));
|
}
|
||||||
// Campi corridore
|
|
||||||
dt.Columns.Add("Num", typeof(int));
|
private List<VenueInfo> GetFilteredVenues(string country, CancellationToken ct)
|
||||||
dt.Columns.Add("Cavallo", typeof(string));
|
{
|
||||||
dt.Columns.Add("Fantino", typeof(string));
|
if (_venuesCache == null)
|
||||||
dt.Columns.Add("Allenatore", typeof(string));
|
_venuesCache = ParseVenues(_client.GetVenues(ct).Content);
|
||||||
dt.Columns.Add("Peso", typeof(string));
|
|
||||||
dt.Columns.Add("Claim", typeof(string));
|
return _venuesCache
|
||||||
dt.Columns.Add("Box", typeof(string));
|
.Where(v => string.Equals(v.Country, country, StringComparison.OrdinalIgnoreCase)
|
||||||
dt.Columns.Add("Età", typeof(string));
|
&& !string.IsNullOrEmpty(v.Name))
|
||||||
dt.Columns.Add("Forma", typeof(string));
|
.OrderBy(v => v.Name)
|
||||||
dt.Columns.Add("Ultimi 20", typeof(string));
|
.ToList();
|
||||||
dt.Columns.Add("Colori", typeof(string));
|
}
|
||||||
dt.Columns.Add("Cambio Equip.", typeof(string));
|
|
||||||
// Statistiche overall
|
private static List<VenueInfo> ParseVenues(string json)
|
||||||
dt.Columns.Add("Vitt.", typeof(string));
|
{
|
||||||
dt.Columns.Add("Piazz.", typeof(string));
|
var venues = new List<VenueInfo>();
|
||||||
dt.Columns.Add("Partenze", typeof(string));
|
if (string.IsNullOrEmpty(json)) return venues;
|
||||||
dt.Columns.Add("% Vitt.", typeof(string));
|
|
||||||
dt.Columns.Add("% Piazz.", typeof(string));
|
try
|
||||||
// Statistiche pista
|
{
|
||||||
dt.Columns.Add("Pista V/P/S", typeof(string));
|
using (var doc = JsonDocument.Parse(json))
|
||||||
// Statistiche distanza
|
{
|
||||||
dt.Columns.Add("Dist. V/P/S", typeof(string));
|
var root = doc.RootElement;
|
||||||
// Statistiche condizione
|
|
||||||
dt.Columns.Add("Cond. V/P/S", typeof(string));
|
JsonElement arr;
|
||||||
// Stato
|
if (root.TryGetProperty("venues", out var venuesEl) &&
|
||||||
dt.Columns.Add("Ritirato", typeof(string));
|
venuesEl.ValueKind == JsonValueKind.Array)
|
||||||
return dt;
|
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
|
#endregion
|
||||||
|
|
||||||
#region JSON Parsing
|
#region Meetings parsing (AU)
|
||||||
|
|
||||||
private class MeetingInfo
|
private class MeetingInfo
|
||||||
{
|
{
|
||||||
@@ -242,7 +460,53 @@ namespace HorseRacingPredictor.HorseRacing
|
|||||||
return meetings;
|
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;
|
if (string.IsNullOrEmpty(json)) return;
|
||||||
|
|
||||||
@@ -253,6 +517,7 @@ namespace HorseRacingPredictor.HorseRacing
|
|||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
|
|
||||||
string track = GetString(root, "track", "");
|
string track = GetString(root, "track", "");
|
||||||
|
string country = GetString(root, "country", fallbackCountry);
|
||||||
int raceNumber = GetInt(root, "raceNumber");
|
int raceNumber = GetInt(root, "raceNumber");
|
||||||
string raceName = GetString(root, "raceName", "");
|
string raceName = GetString(root, "raceName", "");
|
||||||
string distance = GetString(root, "distance", "");
|
string distance = GetString(root, "distance", "");
|
||||||
@@ -263,21 +528,11 @@ namespace HorseRacingPredictor.HorseRacing
|
|||||||
string startTime = GetString(root, "startTime", "");
|
string startTime = GetString(root, "startTime", "");
|
||||||
int numberOfRunners = GetInt(root, "numberOfRunners");
|
int numberOfRunners = GetInt(root, "numberOfRunners");
|
||||||
|
|
||||||
// Formatta orario se è un ISO datetime
|
string orario = FormatStartTime(startTime);
|
||||||
string orario = "";
|
|
||||||
if (!string.IsNullOrEmpty(startTime))
|
string countryDisplay = country.ToUpperInvariant();
|
||||||
{
|
if (CountryNames.TryGetValue(country.ToLowerInvariant(), out var cn))
|
||||||
try
|
countryDisplay = cn;
|
||||||
{
|
|
||||||
var dto = DateTimeOffset.Parse(startTime);
|
|
||||||
var romeTz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");
|
|
||||||
orario = TimeZoneInfo.ConvertTime(dto, romeTz).ToString("HH:mm");
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
orario = startTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!root.TryGetProperty("runners", out var runnersEl) ||
|
if (!root.TryGetProperty("runners", out var runnersEl) ||
|
||||||
runnersEl.ValueKind != JsonValueKind.Array)
|
runnersEl.ValueKind != JsonValueKind.Array)
|
||||||
@@ -289,8 +544,8 @@ namespace HorseRacingPredictor.HorseRacing
|
|||||||
{
|
{
|
||||||
var row = dt.NewRow();
|
var row = dt.NewRow();
|
||||||
|
|
||||||
// Campi corsa
|
|
||||||
row["Ippodromo"] = track;
|
row["Ippodromo"] = track;
|
||||||
|
row["Paese"] = countryDisplay;
|
||||||
row["Corsa N."] = raceNumber;
|
row["Corsa N."] = raceNumber;
|
||||||
row["Nome Corsa"] = raceName;
|
row["Nome Corsa"] = raceName;
|
||||||
row["Orario"] = orario;
|
row["Orario"] = orario;
|
||||||
@@ -301,7 +556,6 @@ namespace HorseRacingPredictor.HorseRacing
|
|||||||
row["Premio"] = prizeMoney;
|
row["Premio"] = prizeMoney;
|
||||||
row["N. Corridori"] = numberOfRunners;
|
row["N. Corridori"] = numberOfRunners;
|
||||||
|
|
||||||
// Campi corridore
|
|
||||||
row["Num"] = GetInt(runner, "number");
|
row["Num"] = GetInt(runner, "number");
|
||||||
row["Cavallo"] = GetString(runner, "name", "");
|
row["Cavallo"] = GetString(runner, "name", "");
|
||||||
row["Fantino"] = GetString(runner, "jockey", "");
|
row["Fantino"] = GetString(runner, "jockey", "");
|
||||||
@@ -315,7 +569,7 @@ namespace HorseRacingPredictor.HorseRacing
|
|||||||
row["Box"] = GetInt(runner, "barrier") > 0
|
row["Box"] = GetInt(runner, "barrier") > 0
|
||||||
? GetInt(runner, "barrier").ToString()
|
? GetInt(runner, "barrier").ToString()
|
||||||
: GetString(runner, "barrier", "");
|
: GetString(runner, "barrier", "");
|
||||||
row["Età"] = GetInt(runner, "age") > 0
|
row["Eta'"] = GetInt(runner, "age") > 0
|
||||||
? GetInt(runner, "age").ToString()
|
? GetInt(runner, "age").ToString()
|
||||||
: GetString(runner, "age", "");
|
: GetString(runner, "age", "");
|
||||||
row["Forma"] = GetString(runner, "form", "");
|
row["Forma"] = GetString(runner, "form", "");
|
||||||
@@ -323,21 +577,20 @@ namespace HorseRacingPredictor.HorseRacing
|
|||||||
row["Colori"] = GetString(runner, "racingColours", "");
|
row["Colori"] = GetString(runner, "racingColours", "");
|
||||||
row["Cambio Equip."] = GetString(runner, "gearChange", "");
|
row["Cambio Equip."] = GetString(runner, "gearChange", "");
|
||||||
|
|
||||||
// Statistiche overall
|
|
||||||
if (runner.TryGetProperty("stats", out var statsEl))
|
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["Pista V/P/S"] = FormatStatSummary(statsEl, "track");
|
||||||
row["Dist. V/P/S"] = FormatStatSummary(statsEl, "distance");
|
row["Dist. V/P/S"] = FormatStatSummary(statsEl, "distance");
|
||||||
row["Cond. V/P/S"] = FormatStatSummary(statsEl, "condition");
|
row["Cond. V/P/S"] = FormatStatSummary(statsEl, "condition");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ritirato
|
|
||||||
bool scratched = false;
|
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;
|
scratched = true;
|
||||||
row["Ritirato"] = scratched ? "Sì" : "";
|
row["Ritirato"] = scratched ? "Si" : "";
|
||||||
|
|
||||||
dt.Rows.Add(row);
|
dt.Rows.Add(row);
|
||||||
}
|
}
|
||||||
@@ -348,9 +601,30 @@ namespace HorseRacingPredictor.HorseRacing
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static string FormatStartTime(string startTime)
|
||||||
/// Estrae wins, places, starts, winPercent, placePercent da un sotto-oggetto stats
|
{
|
||||||
/// </summary>
|
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,
|
private static void ParseStatGroup(JsonElement statsEl, string group,
|
||||||
DataRow row, string winsCol, string placesCol, string startsCol,
|
DataRow row, string winsCol, string placesCol, string startsCol,
|
||||||
string winPctCol, string placePctCol)
|
string winPctCol, string placePctCol)
|
||||||
@@ -370,9 +644,6 @@ namespace HorseRacingPredictor.HorseRacing
|
|||||||
row[placePctCol] = (placePct * 100).ToString("F0") + "%";
|
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)
|
private static string FormatStatSummary(JsonElement statsEl, string group)
|
||||||
{
|
{
|
||||||
if (!statsEl.TryGetProperty(group, out var g)) return "";
|
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
|
internal class Database : HorseRacingPredictor.Manager.Database
|
||||||
{
|
{
|
||||||
// Implementazione della proprietà astratta _connectionString per il database cavalli
|
// Connection string caricata da appsettings.json
|
||||||
protected override string _connectionString => "Server=DESKTOP-9O9JHFS;Database=TestBS_Horses;User Id=sa;Password=Asti2019;";
|
public Database() : base(AppConfig.HorsesConnectionString)
|
||||||
|
|
||||||
private readonly FileReader fileReaderHorses;
|
|
||||||
|
|
||||||
public Database()
|
|
||||||
{
|
{
|
||||||
fileReaderHorses = new FileReader();
|
fileReaderHorses = new FileReader();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly FileReader fileReaderHorses;
|
||||||
|
|
||||||
public DataTable GetAllHorseRaceData()
|
public DataTable GetAllHorseRaceData()
|
||||||
{
|
{
|
||||||
DataTable horseRaceData = new DataTable();
|
DataTable horseRaceData = new DataTable();
|
||||||
|
|||||||
@@ -644,11 +644,6 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<DatePicker x:Name="dpRacing" Width="160"/>
|
<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"
|
<Button x:Name="btnDownloadRc" Content="Scarica Corse"
|
||||||
Style="{StaticResource AccentBtn}"
|
Style="{StaticResource AccentBtn}"
|
||||||
Background="{StaticResource BrBlue}" Margin="12,0,0,0"
|
Background="{StaticResource BrBlue}" Margin="12,0,0,0"
|
||||||
@@ -792,6 +787,73 @@
|
|||||||
<TextBox x:Name="txtRacingApiKey" Style="{StaticResource FlatTb}" MaxWidth="510" HorizontalAlignment="Left"/>
|
<TextBox x:Name="txtRacingApiKey" Style="{StaticResource FlatTb}" MaxWidth="510" HorizontalAlignment="Left"/>
|
||||||
|
|
||||||
<Grid Margin="0,14,0,0">
|
<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>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="245"/>
|
<ColumnDefinition Width="245"/>
|
||||||
<ColumnDefinition Width="22"/>
|
<ColumnDefinition Width="22"/>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ namespace HorseRacingPredictor
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_footballManager = new Football.Main();
|
_footballManager = new Football.Main();
|
||||||
_racingManager = new HorseRacing.Main(DefaultRacingApiKey);
|
_racingManager = new HorseRacing.Main(DefaultRacingApiKey);
|
||||||
|
BuildCountryCheckboxes();
|
||||||
// Wire preview update events
|
// Wire preview update events
|
||||||
txtFbPrefix.TextChanged += (s, e) => UpdateFbPreview();
|
txtFbPrefix.TextChanged += (s, e) => UpdateFbPreview();
|
||||||
txtFbSuffix.TextChanged += (s, e) => UpdateFbPreview();
|
txtFbSuffix.TextChanged += (s, e) => UpdateFbPreview();
|
||||||
@@ -334,13 +335,111 @@ namespace HorseRacingPredictor
|
|||||||
|
|
||||||
// ???????????????????? HORSE RACING ????????????????????
|
// ???????????????????? 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)
|
private void rbRcSource_Checked(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// Toggle visibility of API vs CSV controls
|
// Toggle visibility of API vs CSV controls
|
||||||
if (dpRacing == null || btnDownloadRc == null || btnBrowseCsvRc == null) return;
|
if (dpRacing == null || btnDownloadRc == null || btnBrowseCsvRc == null) return;
|
||||||
bool isApi = rbRcApi.IsChecked == true;
|
bool isApi = rbRcApi.IsChecked == true;
|
||||||
dpRacing.Visibility = isApi ? Visibility.Visible : Visibility.Collapsed;
|
dpRacing.Visibility = isApi ? Visibility.Visible : Visibility.Collapsed;
|
||||||
if (cmbRaceCode != null) cmbRaceCode.Visibility = isApi ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
btnDownloadRc.Visibility = isApi ? Visibility.Visible : Visibility.Collapsed;
|
btnDownloadRc.Visibility = isApi ? Visibility.Visible : Visibility.Collapsed;
|
||||||
btnBrowseCsvRc.Visibility = isApi ? Visibility.Collapsed : Visibility.Visible;
|
btnBrowseCsvRc.Visibility = isApi ? Visibility.Collapsed : Visibility.Visible;
|
||||||
}
|
}
|
||||||
@@ -528,20 +627,9 @@ namespace HorseRacingPredictor
|
|||||||
r[col] = n++;
|
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()
|
private async Task DownloadRacecardsAsync()
|
||||||
{
|
{
|
||||||
// Se è già in corso, annulla
|
// Se e' gia' in corso, annulla
|
||||||
if (_racingCts != null)
|
if (_racingCts != null)
|
||||||
{
|
{
|
||||||
_racingCts.Cancel();
|
_racingCts.Cancel();
|
||||||
@@ -557,20 +645,21 @@ namespace HorseRacingPredictor
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
pbRacing.Value = 0;
|
pbRacing.Value = 0;
|
||||||
lblStatusRc.Text = "Scaricamento corse da FormFav…";
|
lblStatusRc.Text = "Scaricamento corse da FormFav...";
|
||||||
btnDownloadRc.Content = "Annulla";
|
btnDownloadRc.Content = "Annulla";
|
||||||
dpRacing.IsEnabled = false;
|
dpRacing.IsEnabled = false;
|
||||||
cmbRaceCode.IsEnabled = false;
|
|
||||||
btnExportRcCsv.IsEnabled = false;
|
btnExportRcCsv.IsEnabled = false;
|
||||||
|
|
||||||
|
// Applica impostazioni correnti al manager
|
||||||
|
ApplyRacingSettings();
|
||||||
|
|
||||||
var progress = new Progress<int>(v => pbRacing.Value = v);
|
var progress = new Progress<int>(v => pbRacing.Value = v);
|
||||||
var status = new Progress<string>(s => lblStatusRc.Text = s);
|
var status = new Progress<string>(s => lblStatusRc.Text = s);
|
||||||
|
|
||||||
var date = dpRacing.SelectedDate ?? DateTime.Today;
|
var date = dpRacing.SelectedDate ?? DateTime.Today;
|
||||||
string raceCode = GetSelectedRaceCode();
|
|
||||||
|
|
||||||
var table = await Task.Run(() =>
|
var table = await Task.Run(() =>
|
||||||
_racingManager.GetAllRacesForDate(date, raceCode, progress, status, ct), ct);
|
_racingManager.GetAllRacesForDate(date, progress, status, ct), ct);
|
||||||
|
|
||||||
_racingData = table;
|
_racingData = table;
|
||||||
|
|
||||||
@@ -606,10 +695,22 @@ namespace HorseRacingPredictor
|
|||||||
_racingCts = null;
|
_racingCts = null;
|
||||||
btnDownloadRc.Content = "Scarica Corse";
|
btnDownloadRc.Content = "Scarica Corse";
|
||||||
dpRacing.IsEnabled = true;
|
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)
|
private void btnExportRcCsv_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var rcDate = dpRacing.SelectedDate ?? DateTime.Today;
|
var rcDate = dpRacing.SelectedDate ?? DateTime.Today;
|
||||||
@@ -741,7 +842,10 @@ namespace HorseRacingPredictor
|
|||||||
{
|
{
|
||||||
txtRacingApiKey.Text = DefaultRacingApiKey;
|
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))
|
foreach (var line in File.ReadAllLines(SettingsFilePath))
|
||||||
{
|
{
|
||||||
var idx = line.IndexOf('=');
|
var idx = line.IndexOf('=');
|
||||||
@@ -763,13 +867,19 @@ namespace HorseRacingPredictor
|
|||||||
else if (key == "RcDateFormat") { try { SetComboBoxSelectionByContent(cmbRcDateFormat, val); } catch { } }
|
else if (key == "RcDateFormat") { try { SetComboBoxSelectionByContent(cmbRcDateFormat, val); } catch { } }
|
||||||
else if (key == "RcFormat") { try { SetComboBoxSelectionByContent(cmbRcFormat, val); } catch { } }
|
else if (key == "RcFormat") { try { SetComboBoxSelectionByContent(cmbRcFormat, val); } catch { } }
|
||||||
else if (key == "RacingApiKey") txtRacingApiKey.Text = val;
|
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
|
// Update preview UI after loading values
|
||||||
UpdateFbPreview();
|
UpdateFbPreview();
|
||||||
UpdateRcPreview();
|
UpdateRcPreview();
|
||||||
|
|
||||||
_racingManager.UpdateApiKey(txtRacingApiKey.Text);
|
ApplyRacingSettings();
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
@@ -1015,13 +1125,15 @@ namespace HorseRacingPredictor
|
|||||||
sb.AppendLine($"RcDateFormat={(cmbRcDateFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "yyyy-MM-dd"}");
|
sb.AppendLine($"RcDateFormat={(cmbRcDateFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "yyyy-MM-dd"}");
|
||||||
sb.AppendLine($"RcFormat={(cmbRcFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV"}");
|
sb.AppendLine($"RcFormat={(cmbRcFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV"}");
|
||||||
sb.AppendLine($"RacingApiKey={txtRacingApiKey.Text.Trim()}");
|
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);
|
File.WriteAllText(SettingsFilePath, sb.ToString(), Encoding.UTF8);
|
||||||
|
|
||||||
// update previews after save
|
// update previews after save
|
||||||
UpdateFbPreview();
|
UpdateFbPreview();
|
||||||
UpdateRcPreview();
|
UpdateRcPreview();
|
||||||
|
|
||||||
_racingManager.UpdateApiKey(txtRacingApiKey.Text.Trim());
|
ApplyRacingSettings();
|
||||||
|
|
||||||
MessageBox.Show("Impostazioni salvate con successo.",
|
MessageBox.Show("Impostazioni salvate con successo.",
|
||||||
"Salvato", MessageBoxButton.OK, MessageBoxImage.Information);
|
"Salvato", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ namespace HorseRacingPredictor.Manager
|
|||||||
{
|
{
|
||||||
internal abstract class Database
|
internal abstract class Database
|
||||||
{
|
{
|
||||||
// La stringa di connessione viene rimossa da qui e definita nelle classi derivate
|
protected readonly string _connectionString;
|
||||||
protected abstract string _connectionString { get; }
|
|
||||||
|
protected Database(string connectionString)
|
||||||
|
{
|
||||||
|
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
|
||||||
|
}
|
||||||
|
|
||||||
protected SqlConnection GetConnection()
|
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