diff --git a/HorseRacingPredictor/HorseRacingPredictor/BettingPredictor.csproj b/HorseRacingPredictor/HorseRacingPredictor/BettingPredictor.csproj
index 86ee796..509b273 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/BettingPredictor.csproj
+++ b/HorseRacingPredictor/HorseRacingPredictor/BettingPredictor.csproj
@@ -18,6 +18,7 @@
bin\x64\Release\
+
diff --git a/HorseRacingPredictor/HorseRacingPredictor/HorseRacingPredictor/HorseRacing/Scraping/PuntersScraper.cs b/HorseRacingPredictor/HorseRacingPredictor/HorseRacingPredictor/HorseRacing/Scraping/PuntersScraper.cs
new file mode 100644
index 0000000..c2c371d
--- /dev/null
+++ b/HorseRacingPredictor/HorseRacingPredictor/HorseRacingPredictor/HorseRacing/Scraping/PuntersScraper.cs
@@ -0,0 +1,720 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using AngleSharp;
+using AngleSharp.Dom;
+using AngleSharp.Html.Parser;
+
+namespace HorseRacingPredictor.HorseRacing.Scraping
+{
+ ///
+ /// Scraper per Punters.com.au – estrae dati delle corse dei cavalli
+ /// direttamente dal sito web (SSR HTML) senza scaricare file CSV.
+ ///
+ /// Flusso:
+ /// 1. Discovery: accede a /form-guide/ e scopre le corse del giorno
+ /// per le nazioni richieste (es. GB, IE).
+ /// 2. Extraction: per ogni corsa, accede alla pagina dettaglio ed estrae
+ /// i dati dei corridori dalla tabella HTML renderizzata lato server.
+ /// 3. Il risultato è un DataTable pronto per DataGrid ed esportazione.
+ ///
+ internal sealed class PuntersScraper
+ {
+ private const string BaseUrl = "https://www.punters.com.au";
+ private const string FormGuideUrl = BaseUrl + "/form-guide/";
+
+ // Delay tra richieste per sembrare un utente reale (ms)
+ private const int MinDelayMs = 1500;
+ private const int MaxDelayMs = 3500;
+
+ private static readonly Random Rng = new();
+
+ private readonly HttpClient _http;
+ private readonly HtmlParser _parser = new();
+
+ /// Codici paese da scaricare (es. "GB", "IE").
+ public List Countries { get; set; } = new() { "GB", "IE" };
+
+ public PuntersScraper()
+ {
+ var handler = new HttpClientHandler
+ {
+ AutomaticDecompression = DecompressionMethods.GZip
+ | DecompressionMethods.Deflate
+ | DecompressionMethods.Brotli,
+ UseCookies = true,
+ CookieContainer = new CookieContainer()
+ };
+
+ _http = new HttpClient(handler)
+ {
+ Timeout = TimeSpan.FromSeconds(60)
+ };
+
+ // Headers che mimano un browser reale (dal file HAR)
+ _http.DefaultRequestHeaders.Add("User-Agent",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36");
+ _http.DefaultRequestHeaders.Add("Accept",
+ "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
+ _http.DefaultRequestHeaders.Add("Accept-Language", "en-US,en;q=0.9");
+ _http.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br");
+ _http.DefaultRequestHeaders.Add("sec-ch-ua",
+ "\"Chromium\";v=\"146\", \"Not-A.Brand\";v=\"24\", \"Google Chrome\";v=\"146\"");
+ _http.DefaultRequestHeaders.Add("sec-ch-ua-mobile", "?0");
+ _http.DefaultRequestHeaders.Add("sec-ch-ua-platform", "\"Windows\"");
+ _http.DefaultRequestHeaders.Add("sec-fetch-dest", "document");
+ _http.DefaultRequestHeaders.Add("sec-fetch-mode", "navigate");
+ _http.DefaultRequestHeaders.Add("sec-fetch-site", "none");
+ _http.DefaultRequestHeaders.Add("sec-fetch-user", "?1");
+ _http.DefaultRequestHeaders.Add("upgrade-insecure-requests", "1");
+ }
+
+ ///
+ /// Scarica tutti i dati delle corse per una data specifica.
+ ///
+ public async Task ScrapeAsync(DateTime date,
+ IProgress progress = null,
+ IProgress status = null,
+ CancellationToken ct = default)
+ {
+ var dt = CreateTable();
+
+ try
+ {
+ // === FASE 1: Discovery – scarica la pagina form-guide ===
+ status?.Report("Punters: Accesso al palinsesto...");
+ progress?.Report(2);
+
+ var meetings = await DiscoverMeetingsAsync(date, ct);
+
+ if (meetings.Count == 0)
+ {
+ status?.Report("Punters: Nessun meeting trovato per le nazioni selezionate");
+ progress?.Report(100);
+ return dt;
+ }
+
+ // Conta totale corse per progresso
+ int totalRaces = meetings.Sum(m => m.Races.Count);
+ status?.Report($"Punters: {meetings.Count} meeting, {totalRaces} corse trovate");
+ progress?.Report(10);
+
+ // === FASE 2: Extraction – scarica ogni pagina corsa ===
+ int completed = 0;
+ int errors = 0;
+
+ foreach (var meeting in meetings)
+ {
+ foreach (var race in meeting.Races)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ status?.Report($"Punters: {meeting.Track} R{race.Number}/{meeting.Races.Count} " +
+ $"({completed + 1}/{totalRaces})");
+
+ try
+ {
+ await RandomDelayAsync(ct);
+ await FetchRaceRunners(dt, race.Url, meeting, race, ct);
+ }
+ catch (OperationCanceledException) { throw; }
+ catch (Exception ex)
+ {
+ errors++;
+ System.Diagnostics.Debug.WriteLine(
+ $"Errore Punters {meeting.Track} R{race.Number}: {ex.Message}");
+ }
+
+ completed++;
+ int pct = 10 + (int)((double)completed / Math.Max(totalRaces, 1) * 88);
+ progress?.Report(Math.Min(pct, 98));
+ }
+ }
+
+ progress?.Report(100);
+ string errMsg = errors > 0 ? $" ({errors} errori)" : "";
+ string countriesStr = string.Join("+", Countries.Select(c => c.ToUpper()));
+ status?.Report($"Punters [{countriesStr}]: {dt.Rows.Count} corridori in " +
+ $"{meetings.Count} meeting{errMsg}");
+ }
+ catch (OperationCanceledException)
+ {
+ status?.Report("Scraping annullato");
+ }
+ catch (Exception ex)
+ {
+ status?.Report($"Errore Punters: {ex.Message}");
+ }
+
+ return dt;
+ }
+
+ #region Discovery
+
+ ///
+ /// Accede alla pagina form-guide e scopre i meeting per le nazioni selezionate.
+ /// La pagina Punters ha sezioni raggruppate per paese con id="country-XX".
+ ///
+ private async Task> DiscoverMeetingsAsync(DateTime date, CancellationToken ct)
+ {
+ var meetings = new List();
+
+ // Punters mostra "Today" di default; la data è basata sul timezone australiano.
+ // Per UK/IE con date specifiche, proviamo il percorso con data.
+ string html = await FetchPageAsync(FormGuideUrl, ct);
+ if (string.IsNullOrEmpty(html)) return meetings;
+
+ var doc = await ParseHtmlAsync(html);
+
+ // Cerca le sezioni per ogni nazione richiesta
+ foreach (string country in Countries)
+ {
+ string countryUpper = country.ToUpper();
+ string selectorId = $"country-{countryUpper}";
+
+ // Cerca la tabella con id="country-XX"
+ var countrySection = doc.QuerySelector($"#{selectorId}");
+
+ if (countrySection == null)
+ {
+ // Prova a cercare in tutto il documento per sezioni paese
+ // che contengono il codice nel testo
+ var allMeetingStates = doc.QuerySelectorAll(
+ ".race-meetings-desktop__meeting-state");
+ foreach (var stateEl in allMeetingStates)
+ {
+ if (string.Equals(stateEl.TextContent.Trim(), countryUpper,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ // Risali alla riga del meeting
+ var row = stateEl.Closest("tr");
+ if (row != null)
+ {
+ var meetingInfo = ParseMeetingRow(row, countryUpper);
+ if (meetingInfo != null)
+ meetings.Add(meetingInfo);
+ }
+ }
+ }
+ continue;
+ }
+
+ // Parsa le righe della tabella meeting
+ var rows = countrySection.QuerySelectorAll(
+ "tbody tr.race-meetings-desktop__races-row");
+
+ foreach (var row in rows)
+ {
+ var meetingInfo = ParseMeetingRow(row, countryUpper);
+ if (meetingInfo != null)
+ meetings.Add(meetingInfo);
+ }
+ }
+
+ return meetings;
+ }
+
+ ///
+ /// Parsifica una riga di meeting nella tabella form-guide.
+ /// Estrae nome track e URL di ogni singola corsa.
+ ///
+ private MeetingInfo ParseMeetingRow(IElement row, string country)
+ {
+ // Nome track
+ var nameLink = row.QuerySelector(
+ ".race-meetings-desktop__meeting-name a");
+ if (nameLink == null) return null;
+
+ string trackName = nameLink.TextContent.Trim();
+ string trackUrl = nameLink.GetAttribute("href") ?? "";
+
+ // Condizioni pista
+ string condition = "";
+ var condEl = row.QuerySelector("[class*='track-condition']");
+ if (condEl != null)
+ condition = condEl.TextContent.Trim();
+
+ // Meteo
+ string weather = "";
+ var weatherImg = row.QuerySelector(".weather__icon img");
+ if (weatherImg != null)
+ weather = weatherImg.GetAttribute("alt")?.Replace("Icon ", "") ?? "";
+
+ // Raccogli i link delle corse individuali
+ var raceCards = row.QuerySelectorAll("td.race-meetings-desktop__event");
+ var races = new List();
+ int raceNum = 0;
+
+ foreach (var td in raceCards)
+ {
+ raceNum++;
+ var link = td.QuerySelector("a.event-card");
+ if (link == null) continue;
+
+ string href = link.GetAttribute("href");
+ if (string.IsNullOrEmpty(href)) continue;
+
+ // Controlla se la corsa ha dati (non è "no-data")
+ if (link.ClassList.Contains("event-card--no-data")) continue;
+
+ races.Add(new RaceRef
+ {
+ Number = raceNum,
+ Url = href.StartsWith("http") ? href : BaseUrl + href
+ });
+ }
+
+ if (races.Count == 0) return null;
+
+ return new MeetingInfo
+ {
+ Track = trackName,
+ TrackUrl = trackUrl.StartsWith("http") ? trackUrl : BaseUrl + trackUrl,
+ Country = country,
+ Condition = condition,
+ Weather = weather,
+ Races = races
+ };
+ }
+
+ #endregion
+
+ #region Race Page Extraction
+
+ ///
+ /// Scarica la pagina di una singola corsa ed estrae i dati dei corridori.
+ /// La pagina Punters SSR contiene tabelle/liste con i dettagli di ogni cavallo.
+ ///
+ private async Task FetchRaceRunners(DataTable dt, string raceUrl,
+ MeetingInfo meeting, RaceRef race, CancellationToken ct)
+ {
+ string html = await FetchPageAsync(raceUrl, ct);
+ if (string.IsNullOrEmpty(html)) return;
+
+ var doc = await ParseHtmlAsync(html);
+
+ // Estrai nome corsa dall'header della pagina
+ string raceName = ExtractRaceName(doc);
+ string distance = ExtractMetadata(doc, "distance", "dist");
+ string raceClass = ExtractMetadata(doc, "class", "grade");
+ string prize = ExtractMetadata(doc, "prize", "prizemoney", "purse");
+ string startTime = ExtractMetadata(doc, "time", "start");
+
+ // Cerca la tabella/lista dei corridori.
+ // Punters usa diverse strutture: tabelle, card, liste.
+ // Proviamo diverse strategie di parsing.
+ var runners = ExtractRunners(doc);
+
+ if (runners.Count == 0)
+ {
+ // Fallback: cerca qualsiasi tabella con dati di cavalli
+ runners = ExtractRunnersFromTable(doc);
+ }
+
+ foreach (var runner in runners)
+ {
+ var row = dt.NewRow();
+ row["Meeting"] = meeting.Track;
+ row["Paese"] = meeting.Country;
+ row["Corsa N."] = race.Number;
+ row["Nome Corsa"] = raceName;
+ row["Orario"] = startTime;
+ row["Distanza"] = distance;
+ row["Terreno"] = meeting.Condition;
+ row["Classe"] = raceClass;
+ row["Meteo"] = meeting.Weather;
+ row["Premio"] = prize;
+ row["N. Corridori"] = runners.Count;
+ row["Num"] = runner.Number;
+ row["Cavallo"] = runner.Name;
+ row["Fantino"] = runner.Jockey;
+ row["Allenatore"] = runner.Trainer;
+ row["Peso"] = runner.Weight;
+ row["Box"] = runner.Barrier;
+ row["Forma"] = runner.Form;
+ row["Eta'"] = runner.Age;
+ row["Ritirato"] = runner.Scratched ? "Si" : "";
+ dt.Rows.Add(row);
+ }
+ }
+
+ private string ExtractRaceName(IDocument doc)
+ {
+ // Prova vari selettori per il nome della corsa
+ var candidates = new[]
+ {
+ "h1", ".race-header__name", ".race-name",
+ "[data-test='race-name']", ".event-header__title"
+ };
+
+ foreach (var sel in candidates)
+ {
+ var el = doc.QuerySelector(sel);
+ if (el != null)
+ {
+ string text = el.TextContent.Trim();
+ if (!string.IsNullOrEmpty(text) && text.Length < 200)
+ return text;
+ }
+ }
+
+ return "";
+ }
+
+ ///
+ /// Estrae un metadato della corsa cercando tra diversi selettori e pattern.
+ ///
+ private string ExtractMetadata(IDocument doc, params string[] keywords)
+ {
+ // Cerca nelle sezioni di info/dettaglio della corsa
+ var infoElements = doc.QuerySelectorAll(
+ ".race-info dt, .race-info dd, .race-details span, " +
+ ".race-header__info span, .event-info span, " +
+ "[class*='race-detail'], [class*='event-detail']");
+
+ foreach (var el in infoElements)
+ {
+ string cls = el.ClassName ?? "";
+ string text = el.TextContent.Trim();
+
+ foreach (string kw in keywords)
+ {
+ if (cls.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
+ text.StartsWith(kw, StringComparison.OrdinalIgnoreCase))
+ {
+ // Se è un dt, cerca il successivo dd
+ if (el.TagName.Equals("DT", StringComparison.OrdinalIgnoreCase))
+ {
+ var dd = el.NextElementSibling;
+ return dd?.TextContent.Trim() ?? "";
+ }
+ return text;
+ }
+ }
+ }
+
+ return "";
+ }
+
+ ///
+ /// Estrae i corridori cercando strutture note del sito Punters.
+ /// Il sito usa card/righe per ogni corridore con classi CSS riconoscibili.
+ ///
+ private List ExtractRunners(IDocument doc)
+ {
+ var runners = new List();
+
+ // Strategia 1: cerca elementi con classi runner-like
+ var runnerSelectors = new[]
+ {
+ ".runner-row", ".runner-card", ".runner",
+ "[data-test*='runner']", ".form-guide-runner",
+ ".runner-list__item", "tr[class*='runner']",
+ ".horse-row", ".selection-row"
+ };
+
+ IEnumerable runnerElements = null;
+
+ foreach (var sel in runnerSelectors)
+ {
+ var elements = doc.QuerySelectorAll(sel);
+ if (elements.Length > 0)
+ {
+ runnerElements = elements;
+ break;
+ }
+ }
+
+ if (runnerElements == null) return runners;
+
+ foreach (var el in runnerElements)
+ {
+ var runner = ParseRunnerElement(el);
+ if (runner != null)
+ runners.Add(runner);
+ }
+
+ return runners;
+ }
+
+ ///
+ /// Fallback: estrae corridori da una tabella HTML generica.
+ ///
+ private List ExtractRunnersFromTable(IDocument doc)
+ {
+ var runners = new List();
+
+ // Cerca tabelle che sembrano contenere dati di corse
+ var tables = doc.QuerySelectorAll("table");
+
+ foreach (var table in tables)
+ {
+ var headers = table.QuerySelectorAll("th")
+ .Select(th => th.TextContent.Trim().ToLower())
+ .ToList();
+
+ // Identifica se la tabella contiene dati di corridori
+ bool hasHorseColumn = headers.Any(h =>
+ h.Contains("horse") || h.Contains("runner") ||
+ h.Contains("name") || h.Contains("cavallo"));
+
+ if (!hasHorseColumn) continue;
+
+ int nameIdx = headers.FindIndex(h =>
+ h.Contains("horse") || h.Contains("runner") || h.Contains("name"));
+ int numIdx = headers.FindIndex(h =>
+ h == "no" || h == "#" || h.Contains("number") || h.Contains("tab"));
+ int jockeyIdx = headers.FindIndex(h =>
+ h.Contains("jockey") || h.Contains("rider"));
+ int trainerIdx = headers.FindIndex(h =>
+ h.Contains("trainer"));
+ int weightIdx = headers.FindIndex(h =>
+ h.Contains("weight") || h.Contains("wgt"));
+ int barrierIdx = headers.FindIndex(h =>
+ h.Contains("barrier") || h.Contains("gate") || h.Contains("draw"));
+ int formIdx = headers.FindIndex(h =>
+ h.Contains("form") || h.Contains("last"));
+
+ var rows = table.QuerySelectorAll("tbody tr");
+ foreach (var row in rows)
+ {
+ var cells = row.QuerySelectorAll("td").ToList();
+ if (cells.Count < 2) continue;
+
+ var runner = new RunnerData();
+
+ if (nameIdx >= 0 && nameIdx < cells.Count)
+ runner.Name = CleanText(cells[nameIdx].TextContent);
+ if (numIdx >= 0 && numIdx < cells.Count)
+ runner.Number = ParseInt(cells[numIdx].TextContent);
+ if (jockeyIdx >= 0 && jockeyIdx < cells.Count)
+ runner.Jockey = CleanText(cells[jockeyIdx].TextContent);
+ if (trainerIdx >= 0 && trainerIdx < cells.Count)
+ runner.Trainer = CleanText(cells[trainerIdx].TextContent);
+ if (weightIdx >= 0 && weightIdx < cells.Count)
+ runner.Weight = CleanText(cells[weightIdx].TextContent);
+ if (barrierIdx >= 0 && barrierIdx < cells.Count)
+ runner.Barrier = CleanText(cells[barrierIdx].TextContent);
+ if (formIdx >= 0 && formIdx < cells.Count)
+ runner.Form = CleanText(cells[formIdx].TextContent);
+
+ if (!string.IsNullOrEmpty(runner.Name))
+ runners.Add(runner);
+ }
+
+ if (runners.Count > 0) break; // Usa la prima tabella valida
+ }
+
+ return runners;
+ }
+
+ ///
+ /// Parsifica un singolo elemento corridore (card, riga, div) estraendo
+ /// i dati con selettori e pattern euristici.
+ ///
+ private RunnerData ParseRunnerElement(IElement el)
+ {
+ var runner = new RunnerData();
+
+ // Numero
+ var numEl = el.QuerySelector(
+ "[class*='number'], [class*='tab-no'], .runner-number, " +
+ "[data-test*='number'], .saddle-cloth");
+ if (numEl != null)
+ runner.Number = ParseInt(numEl.TextContent);
+
+ // Nome cavallo
+ var nameEl = el.QuerySelector(
+ "[class*='horse-name'], [class*='runner-name'], .horse a, " +
+ "[data-test*='horse-name'], [class*='selection-name'], h3 a, h4 a");
+ if (nameEl != null)
+ runner.Name = CleanText(nameEl.TextContent);
+ else
+ {
+ // Fallback: il primo link o header
+ var link = el.QuerySelector("a[href*='/horses/']");
+ if (link != null)
+ runner.Name = CleanText(link.TextContent);
+ }
+
+ if (string.IsNullOrEmpty(runner.Name)) return null;
+
+ // Fantino
+ var jockeyEl = el.QuerySelector(
+ "[class*='jockey'], a[href*='/jockeys/'], [data-test*='jockey']");
+ if (jockeyEl != null)
+ runner.Jockey = CleanText(jockeyEl.TextContent);
+
+ // Allenatore
+ var trainerEl = el.QuerySelector(
+ "[class*='trainer'], a[href*='/trainers/'], [data-test*='trainer']");
+ if (trainerEl != null)
+ runner.Trainer = CleanText(trainerEl.TextContent);
+
+ // Peso
+ var weightEl = el.QuerySelector(
+ "[class*='weight'], [data-test*='weight']");
+ if (weightEl != null)
+ runner.Weight = CleanText(weightEl.TextContent);
+
+ // Barrier/Gate
+ var barrierEl = el.QuerySelector(
+ "[class*='barrier'], [class*='gate'], [class*='draw'], " +
+ "[data-test*='barrier']");
+ if (barrierEl != null)
+ runner.Barrier = CleanText(barrierEl.TextContent);
+
+ // Forma
+ var formEl = el.QuerySelector(
+ "[class*='form'], [class*='last-starts'], [data-test*='form']");
+ if (formEl != null)
+ runner.Form = CleanText(formEl.TextContent);
+
+ // Età
+ var ageEl = el.QuerySelector("[class*='age']");
+ if (ageEl != null)
+ runner.Age = CleanText(ageEl.TextContent);
+
+ // Ritirato (scratched)
+ runner.Scratched = el.ClassList.Any(c =>
+ c.Contains("scratched", StringComparison.OrdinalIgnoreCase)) ||
+ el.QuerySelector("[class*='scratched']") != null;
+
+ return runner;
+ }
+
+ #endregion
+
+ #region HTTP & Parsing
+
+ private async Task FetchPageAsync(string url, CancellationToken ct)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ try
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Get, url);
+ request.Headers.Add("Referer", BaseUrl + "/form-guide/");
+
+ using var response = await _http.SendAsync(request, ct);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ System.Diagnostics.Debug.WriteLine(
+ $"[Punters] HTTP {(int)response.StatusCode} per {url}");
+ return null;
+ }
+
+ return await response.Content.ReadAsStringAsync(ct);
+ }
+ catch (OperationCanceledException) { throw; }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[Punters] Errore fetch {url}: {ex.Message}");
+ return null;
+ }
+ }
+
+ private async Task ParseHtmlAsync(string html)
+ {
+ var config = Configuration.Default;
+ var context = BrowsingContext.New(config);
+ return await context.OpenAsync(req => req.Content(html));
+ }
+
+ private static async Task RandomDelayAsync(CancellationToken ct)
+ {
+ int delay = Rng.Next(MinDelayMs, MaxDelayMs);
+ await Task.Delay(delay, ct);
+ }
+
+ #endregion
+
+ #region DataTable
+
+ private DataTable CreateTable()
+ {
+ var dt = new DataTable();
+ dt.Columns.Add("Meeting", 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("Box", typeof(string));
+ dt.Columns.Add("Forma", typeof(string));
+ dt.Columns.Add("Eta'", typeof(string));
+ dt.Columns.Add("Ritirato", typeof(string));
+ return dt;
+ }
+
+ #endregion
+
+ #region Models
+
+ private class MeetingInfo
+ {
+ public string Track { get; set; }
+ public string TrackUrl { get; set; }
+ public string Country { get; set; }
+ public string Condition { get; set; }
+ public string Weather { get; set; }
+ public List Races { get; set; } = new();
+ }
+
+ private class RaceRef
+ {
+ public int Number { get; set; }
+ public string Url { get; set; }
+ }
+
+ private class RunnerData
+ {
+ public int Number { get; set; }
+ public string Name { get; set; }
+ public string Jockey { get; set; }
+ public string Trainer { get; set; }
+ public string Weight { get; set; }
+ public string Barrier { get; set; }
+ public string Form { get; set; }
+ public string Age { get; set; }
+ public bool Scratched { get; set; }
+ }
+
+ #endregion
+
+ #region Helpers
+
+ private static string CleanText(string text)
+ {
+ if (string.IsNullOrEmpty(text)) return "";
+ return text.Trim().Replace("\n", " ").Replace("\r", "")
+ .Replace(" ", " ").Trim();
+ }
+
+ private static int ParseInt(string text)
+ {
+ if (string.IsNullOrEmpty(text)) return 0;
+ var cleaned = new string(text.Where(c => char.IsDigit(c)).ToArray());
+ return int.TryParse(cleaned, out int val) ? val : 0;
+ }
+
+ #endregion
+ }
+}
diff --git a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml
index 89c1db1..c25fdc0 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml
+++ b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml
@@ -1238,6 +1238,7 @@
+
diff --git a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs
index 201765b..78c3d9c 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs
+++ b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs
@@ -532,13 +532,13 @@ namespace HorseRacingPredictor
if (pnlFbApiOptions != null)
pnlFbApiOptions.Visibility = IsFbApiSource() ? Visibility.Visible : Visibility.Collapsed;
- // Racing: toggle API vs CSV controls
+ // Racing: toggle API/Scraping vs CSV controls
if (btnDownloadRc != null && btnBrowseCsvRc != null)
{
- bool rcIsApi = IsRcApiSource();
- dpRacing.Visibility = rcIsApi ? Visibility.Visible : Visibility.Collapsed;
- btnDownloadRc.Visibility = rcIsApi ? Visibility.Visible : Visibility.Collapsed;
- btnBrowseCsvRc.Visibility = rcIsApi ? Visibility.Collapsed : Visibility.Visible;
+ bool rcIsDownload = IsRcDownloadSource();
+ dpRacing.Visibility = rcIsDownload ? Visibility.Visible : Visibility.Collapsed;
+ btnDownloadRc.Visibility = rcIsDownload ? Visibility.Visible : Visibility.Collapsed;
+ btnBrowseCsvRc.Visibility = rcIsDownload ? Visibility.Collapsed : Visibility.Visible;
}
}
@@ -554,6 +554,18 @@ namespace HorseRacingPredictor
return src.StartsWith("API", StringComparison.OrdinalIgnoreCase);
}
+ private bool IsRcScrapingSource()
+ {
+ var src = (cmbRcDataSource?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "";
+ return src.StartsWith("Scraping", StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// True per sorgenti che usano il pulsante Scarica (API + Scraping).
+ private bool IsRcDownloadSource()
+ {
+ return IsRcApiSource() || IsRcScrapingSource();
+ }
+
// ———————————— FOOTBALL ENDPOINT / SUPPLEMENTARY POPUPS ————————————
private static readonly (string key, string label, bool defaultChecked)[] FbEndpointItems =
@@ -888,22 +900,38 @@ namespace HorseRacingPredictor
try
{
pbRacing.Value = 0;
- lblStatusRc.Text = "Scaricamento corse da FormFav...";
btnDownloadRcIcon.Text = "\u2718";
btnDownloadRcText.Text = "Annulla";
dpRacing.IsEnabled = false;
btnExportRcCsv.IsEnabled = false;
- // Applica impostazioni correnti al manager
- ApplyRacingSettings();
-
var progress = new Progress(v => pbRacing.Value = v);
var status = new Progress(s => lblStatusRc.Text = s);
-
var date = dpRacing.SelectedDate ?? DateTime.Today;
- var table = await Task.Run(() =>
- _racingManager.GetAllRacesForDate(date, progress, status, ct), ct);
+ DataTable table;
+
+ if (IsRcScrapingSource())
+ {
+ // === SCRAPING PUNTERS ===
+ lblStatusRc.Text = "Scraping dati da Punters.com.au...";
+ var scraper = new HorseRacing.Scraping.PuntersScraper();
+ scraper.Countries = GetSelectedCountries()
+ .Select(c => c.ToUpper())
+ .ToList();
+ if (scraper.Countries.Count == 0)
+ scraper.Countries = new List { "GB", "IE" };
+
+ table = await scraper.ScrapeAsync(date, progress, status, ct);
+ }
+ else
+ {
+ // === API (FormFav / RacingAPI) ===
+ lblStatusRc.Text = "Scaricamento corse da FormFav...";
+ ApplyRacingSettings();
+ table = await Task.Run(() =>
+ _racingManager.GetAllRacesForDate(date, progress, status, ct), ct);
+ }
_racingData = table;