Migrate app.config to appsettings.json with Microsoft.Extensions.Configuration

This commit is contained in:
2026-04-07 14:27:32 +02:00
parent cfb29cc264
commit 0d715081c7
19 changed files with 297473 additions and 299 deletions
+20
View File
@@ -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.
+4 -4
View File
@@ -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%) ![86%](https://progress-bar.xyz/86) **Progress**: 7/7 tasks complete (100%) ![100%](https://progress-bar.xyz/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
progress?.Report(2); var requestedCountries = Countries
.Select(c => c.ToLowerInvariant())
.Where(c => SupportedCountries.Contains(c))
.Distinct()
.ToList();
// 1. Ottieni l'elenco dei meeting per la data if (requestedCountries.Count == 0)
var meetingsResp = _client.GetMeetings(date, raceCode, ct: ct);
progress?.Report(8);
var meetings = ParseMeetings(meetingsResp.Content);
if (meetings.Count == 0)
{ {
status?.Report("Nessun meeting trovato per questa data"); status?.Report("Nessuna nazione supportata selezionata. Usa: AU, NZ");
progress?.Report(100); progress?.Report(100);
return dt; return dt;
} }
// 2. Calcola il totale corse per un progresso granulare bool doAu = requestedCountries.Contains("au");
int totalRaces = 0; bool doNz = requestedCountries.Contains("nz");
foreach (var m in meetings)
if (!m.Abandoned) totalRaces += m.NumberOfRaces;
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"); status?.Report("AU: Recupero elenco meeting...");
progress?.Report(100); progress?.Report(2);
return dt;
}
status?.Report($"Trovati {meetings.Count} meeting ({totalRaces} corse). Scaricamento..."); int phaseBase = 0;
int completedRaces = 0; int phaseSpan = doNz ? 50 : 95; // Se c'e' anche NZ, AU occupa 0-50%
int errors = 0;
// 3. Scarica ogni singola corsa try
foreach (var meeting in meetings)
{
ct.ThrowIfCancellationRequested();
if (meeting.Abandoned) continue;
for (int raceNum = 1; raceNum <= meeting.NumberOfRaces; raceNum++)
{ {
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} " + // Calcola totale corse AU
$"({completedRaces + 1}/{totalRaces})"); int totalAuRaces = 0;
foreach (var m in meetings)
if (!m.Abandoned) totalAuRaces += m.NumberOfRaces;
var formResp = _client.GetRaceForm(date, meeting.TrackSlug, raceNum, status?.Report($"AU: {meetings.Count} meeting ({totalAuRaces} corse)");
raceCode, meeting.Country, ct: ct);
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; } else
catch (Exception ex)
{ {
errors++; status?.Report("AU: Nessun meeting trovato");
System.Diagnostics.Debug.WriteLine(
$"Errore scaricamento {meeting.Track} R{raceNum}: {ex.Message}");
} }
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); 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>