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