Aggiunta sezione impostazioni per manutenzione database (auto-salvataggio, pulizia duplicati/incompleti, retention, ottimizzazione). Implementati metodi asincroni in DatabaseService per pulizia e statistiche. Pulizia automatica all’avvio secondo impostazioni. Rimossa la proprietà MaxClicks da modello, UI e logica. Migliorata la sicurezza thread-safe e la trasparenza nella gestione dati. Spostato il badge versione nelle info applicazione.
737 lines
31 KiB
C#
737 lines
31 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using AutoBidder.Models;
|
|
|
|
namespace AutoBidder.Services
|
|
{
|
|
/// <summary>
|
|
/// Servizio per navigare le aste pubbliche di Bidoo senza autenticazione
|
|
/// Permette di esplorare le categorie e visualizzare le aste disponibili
|
|
/// </summary>
|
|
public class BidooBrowserService
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly List<BidooCategoryInfo> _cachedCategories = new();
|
|
private DateTime _categoriesCachedAt = DateTime.MinValue;
|
|
private readonly TimeSpan _categoryCacheExpiry = TimeSpan.FromMinutes(30);
|
|
|
|
public BidooBrowserService()
|
|
{
|
|
var handler = new HttpClientHandler
|
|
{
|
|
UseCookies = false,
|
|
AutomaticDecompression = System.Net.DecompressionMethods.All
|
|
};
|
|
|
|
_httpClient = new HttpClient(handler)
|
|
{
|
|
Timeout = TimeSpan.FromSeconds(15)
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aggiunge headers browser-like per evitare blocchi
|
|
/// </summary>
|
|
private void AddBrowserHeaders(HttpRequestMessage request, string? referer = null)
|
|
{
|
|
request.Headers.Add("User-Agent",
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36");
|
|
request.Headers.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8");
|
|
request.Headers.Add("Accept-Language", "it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7");
|
|
request.Headers.Add("Accept-Encoding", "gzip, deflate, br");
|
|
request.Headers.Add("Cache-Control", "no-cache");
|
|
request.Headers.Add("Pragma", "no-cache");
|
|
|
|
if (!string.IsNullOrEmpty(referer))
|
|
{
|
|
request.Headers.Add("Referer", referer);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene la lista delle categorie disponibili (con cache)
|
|
/// </summary>
|
|
public async Task<List<BidooCategoryInfo>> GetCategoriesAsync(bool forceRefresh = false, CancellationToken cancellationToken = default)
|
|
{
|
|
// Controlla cache
|
|
if (!forceRefresh && _cachedCategories.Count > 0 && DateTime.UtcNow - _categoriesCachedAt < _categoryCacheExpiry)
|
|
{
|
|
return _cachedCategories.ToList();
|
|
}
|
|
|
|
var categories = new List<BidooCategoryInfo>();
|
|
|
|
try
|
|
{
|
|
var request = new HttpRequestMessage(HttpMethod.Get, "https://it.bidoo.com/");
|
|
AddBrowserHeaders(request);
|
|
|
|
var response = await _httpClient.SendAsync(request, cancellationToken);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var html = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
|
|
// Aggiungi categorie speciali prima
|
|
categories.Add(new BidooCategoryInfo { TabId = 3, TagId = 0, DisplayName = "Tutte le aste", Slug = "", IsSpecialCategory = true, Icon = "bi-grid-3x3-gap" });
|
|
categories.Add(new BidooCategoryInfo { TabId = 1, TagId = 0, DisplayName = "Aste di Puntate", Slug = "", IsSpecialCategory = true, Icon = "bi-coin" });
|
|
categories.Add(new BidooCategoryInfo { TabId = 5, TagId = 0, DisplayName = "Aste Manuali", Slug = "", IsSpecialCategory = true, Icon = "bi-hand-index" });
|
|
|
|
// Parse categorie dal CategoryMenu
|
|
// Pattern: javascript:selectBids(4, true, false, 6); con data-tag="6" e testo "Buoni"
|
|
var categoryPattern = new Regex(
|
|
@"<a\s+href=""\s*javascript:selectBids\(4,\s*true,\s*false,\s*(\d+)\);\s*""\s+data-tab=""4""\s+data-slug=""([^""]*)""\s+data-tag=""(\d+)""><span[^>]*>([^<]+)</span></a>",
|
|
RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
|
|
|
var matches = categoryPattern.Matches(html);
|
|
foreach (Match match in matches)
|
|
{
|
|
if (match.Success && match.Groups.Count >= 5)
|
|
{
|
|
int.TryParse(match.Groups[1].Value, out int tagId1);
|
|
var slug = match.Groups[2].Value.Trim();
|
|
int.TryParse(match.Groups[3].Value, out int tagId2);
|
|
var name = match.Groups[4].Value.Trim();
|
|
|
|
// Usa tagId1 o tagId2 (dovrebbero essere uguali)
|
|
var tagId = tagId1 > 0 ? tagId1 : tagId2;
|
|
|
|
if (tagId > 0 && !string.IsNullOrWhiteSpace(name))
|
|
{
|
|
categories.Add(new BidooCategoryInfo
|
|
{
|
|
TabId = 4,
|
|
TagId = tagId,
|
|
Slug = slug,
|
|
DisplayName = name,
|
|
IsSpecialCategory = false
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Se non abbiamo trovato categorie dal parsing, usa lista predefinita
|
|
if (categories.Count <= 3)
|
|
{
|
|
categories.AddRange(GetDefaultCategories());
|
|
}
|
|
|
|
// Aggiorna cache
|
|
_cachedCategories.Clear();
|
|
_cachedCategories.AddRange(categories);
|
|
_categoriesCachedAt = DateTime.UtcNow;
|
|
|
|
Console.WriteLine($"[BidooBrowser] Caricate {categories.Count} categorie");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[BidooBrowser] Errore caricamento categorie: {ex.Message}");
|
|
|
|
// Fallback a categorie predefinite
|
|
if (_cachedCategories.Count == 0)
|
|
{
|
|
categories.AddRange(GetDefaultCategories());
|
|
_cachedCategories.AddRange(categories);
|
|
}
|
|
else
|
|
{
|
|
return _cachedCategories.ToList();
|
|
}
|
|
}
|
|
|
|
return categories;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Categorie predefinite come fallback
|
|
/// </summary>
|
|
private static List<BidooCategoryInfo> GetDefaultCategories()
|
|
{
|
|
return new List<BidooCategoryInfo>
|
|
{
|
|
new() { TabId = 4, TagId = 6, DisplayName = "Buoni", Slug = "buoni" },
|
|
new() { TabId = 4, TagId = 5, DisplayName = "Smartphone", Slug = "smartphone" },
|
|
new() { TabId = 4, TagId = 7, DisplayName = "Apple", Slug = "apple" },
|
|
new() { TabId = 4, TagId = 13, DisplayName = "Bellezza", Slug = "bellezza" },
|
|
new() { TabId = 4, TagId = 8, DisplayName = "Cucina", Slug = "cucina" },
|
|
new() { TabId = 4, TagId = 18, DisplayName = "Casa & Giardino", Slug = "casa_e_giardino" },
|
|
new() { TabId = 4, TagId = 11, DisplayName = "Elettrodomestici", Slug = "elettrodomestici" },
|
|
new() { TabId = 4, TagId = 9, DisplayName = "Videogame", Slug = "videogame" },
|
|
new() { TabId = 4, TagId = 41, DisplayName = "Giocattoli", Slug = "giocattoli" },
|
|
new() { TabId = 4, TagId = 14, DisplayName = "Tablet e PC", Slug = "tablet-e-pc" },
|
|
new() { TabId = 4, TagId = 20, DisplayName = "Hobby", Slug = "hobby" },
|
|
new() { TabId = 4, TagId = 22, DisplayName = "Smartwatch", Slug = "smartwatch" },
|
|
new() { TabId = 4, TagId = 37, DisplayName = "Animali Domestici", Slug = "animali_domestici" },
|
|
new() { TabId = 4, TagId = 12, DisplayName = "Moda", Slug = "moda" },
|
|
new() { TabId = 4, TagId = 10, DisplayName = "Smart TV", Slug = "smart-tv" },
|
|
new() { TabId = 4, TagId = 21, DisplayName = "Fai da Te", Slug = "fai_da_te" },
|
|
new() { TabId = 4, TagId = 26, DisplayName = "Luxury", Slug = "luxury" },
|
|
new() { TabId = 4, TagId = 19, DisplayName = "Cuffie e Audio", Slug = "cuffie-e-audio" },
|
|
new() { TabId = 4, TagId = 23, DisplayName = "Back to school", Slug = "back-to-school" },
|
|
new() { TabId = 4, TagId = 38, DisplayName = "Prima Infanzia", Slug = "prima-infanzia" }
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene le aste di una categoria specifica
|
|
/// Bidoo usa un sistema AJAX per caricare le aste dinamicamente
|
|
/// </summary>
|
|
public async Task<List<BidooBrowserAuction>> GetAuctionsAsync(
|
|
BidooCategoryInfo category,
|
|
int page = 0,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var auctions = new List<BidooBrowserAuction>();
|
|
|
|
try
|
|
{
|
|
// Bidoo carica le aste tramite chiamata AJAX a index.php con parametri POST-like in query string
|
|
// Il pattern è: index.php?selectBids=1&tab=X&tag=Y&offset=Z
|
|
string url;
|
|
|
|
if (category.IsSpecialCategory)
|
|
{
|
|
// Categorie speciali: BIDS (1), ALL (3), MANUAL (5)
|
|
var tabValue = category.TabId;
|
|
url = $"https://it.bidoo.com/index.php?selectBids=1&tab={tabValue}&tag=0&offset={page * 20}";
|
|
}
|
|
else
|
|
{
|
|
// Categorie normali: tab=4 + tag specifico
|
|
url = $"https://it.bidoo.com/index.php?selectBids=1&tab=4&tag={category.TagId}&offset={page * 20}";
|
|
}
|
|
|
|
Console.WriteLine($"[BidooBrowser] Fetching category '{category.DisplayName}': {url}");
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
|
AddBrowserHeaders(request, "https://it.bidoo.com/");
|
|
request.Headers.Add("X-Requested-With", "XMLHttpRequest");
|
|
|
|
var response = await _httpClient.SendAsync(request, cancellationToken);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var html = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
|
|
// Parse aste dall'HTML (fragment AJAX)
|
|
auctions = ParseAuctionsFromHtml(html);
|
|
|
|
Console.WriteLine($"[BidooBrowser] Trovate {auctions.Count} aste nella categoria {category.DisplayName}");
|
|
|
|
// ?? DEBUG: Verifica quante aste hanno IsCreditAuction = true
|
|
if (category.IsSpecialCategory && category.TabId == 1)
|
|
{
|
|
var creditCount = auctions.Count(a => a.IsCreditAuction);
|
|
Console.WriteLine($"[BidooBrowser] DEBUG Aste di Puntate: {creditCount}/{auctions.Count} hanno IsCreditAuction=true");
|
|
|
|
// Log primi 3 nomi per debug
|
|
foreach (var a in auctions.Take(3))
|
|
{
|
|
Console.WriteLine($"[BidooBrowser] - {a.Name} (ID: {a.AuctionId}, IsCreditAuction: {a.IsCreditAuction})");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[BidooBrowser] Errore caricamento aste: {ex.Message}");
|
|
}
|
|
|
|
return auctions;
|
|
}
|
|
|
|
private static string GetTabName(int tabId)
|
|
{
|
|
return tabId switch
|
|
{
|
|
1 => "BIDS",
|
|
2 => "FAV",
|
|
3 => "ALL",
|
|
5 => "MANUAL",
|
|
_ => "ALL"
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parsa le aste dall'HTML della pagina
|
|
/// </summary>
|
|
private List<BidooBrowserAuction> ParseAuctionsFromHtml(string html)
|
|
{
|
|
var auctions = new List<BidooBrowserAuction>();
|
|
|
|
try
|
|
{
|
|
// Pattern per estrarre i div delle aste
|
|
// <div id="divAsta85584421" class="..." data-id="85584421" data-url="27_Puntate_85584421" data-freq="8" ...>
|
|
var auctionDivPattern = new Regex(
|
|
@"<div\s+id=""divAsta(\d+)""[^>]*" +
|
|
@"data-id=""(\d+)""[^>]*" +
|
|
@"data-url=""([^""]+)""[^>]*" +
|
|
@"data-freq=""(\d+)""[^>]*" +
|
|
@"(?:data-credit=""(\d+)"")?[^>]*" +
|
|
@"(?:data-credit-value=""(\d+)"")?[^>]*" +
|
|
@"(?:data-id-product=""(\d+)"")?",
|
|
RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
|
|
|
// Pattern alternativo più semplice per catturare attributi
|
|
var simplePattern = new Regex(
|
|
@"<div[^>]+id=""divAsta(\d+)""[^>]*>",
|
|
RegexOptions.IgnoreCase);
|
|
|
|
var divMatches = simplePattern.Matches(html);
|
|
|
|
foreach (Match divMatch in divMatches)
|
|
{
|
|
if (!divMatch.Success) continue;
|
|
|
|
var auctionId = divMatch.Groups[1].Value;
|
|
|
|
// Trova il blocco completo dell'asta
|
|
var startIndex = divMatch.Index;
|
|
var endPattern = @"<!--/ \.bid -->";
|
|
var endIndex = html.IndexOf(endPattern, startIndex);
|
|
if (endIndex < 0) endIndex = html.IndexOf("</div><!--", startIndex + 1000);
|
|
if (endIndex < 0) continue;
|
|
|
|
var auctionHtml = html.Substring(startIndex, Math.Min(endIndex - startIndex + 100, html.Length - startIndex));
|
|
|
|
var auction = ParseSingleAuction(auctionId, auctionHtml);
|
|
if (auction != null)
|
|
{
|
|
auctions.Add(auction);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[BidooBrowser] Errore parsing HTML: {ex.Message}");
|
|
}
|
|
|
|
return auctions;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parsa una singola asta dal suo blocco HTML
|
|
/// </summary>
|
|
private BidooBrowserAuction? ParseSingleAuction(string auctionId, string html)
|
|
{
|
|
try
|
|
{
|
|
var auction = new BidooBrowserAuction { AuctionId = auctionId };
|
|
|
|
// Estrai data-url
|
|
var urlMatch = Regex.Match(html, @"data-url=""([^""]+)""");
|
|
if (urlMatch.Success)
|
|
{
|
|
auction.Url = $"https://it.bidoo.com/auction.php?a={urlMatch.Groups[1].Value}";
|
|
}
|
|
|
|
// Estrai data-freq
|
|
var freqMatch = Regex.Match(html, @"data-freq=""(\d+)""");
|
|
if (freqMatch.Success && int.TryParse(freqMatch.Groups[1].Value, out int freq))
|
|
{
|
|
auction.TimerFrequency = freq;
|
|
}
|
|
|
|
// Estrai data-credit e data-credit-value
|
|
var creditMatch = Regex.Match(html, @"data-credit=""(\d+)""");
|
|
if (creditMatch.Success && creditMatch.Groups[1].Value == "1")
|
|
{
|
|
auction.IsCreditAuction = true;
|
|
}
|
|
|
|
var creditValueMatch = Regex.Match(html, @"data-credit-value=""(\d+)""");
|
|
if (creditValueMatch.Success && int.TryParse(creditValueMatch.Groups[1].Value, out int creditVal))
|
|
{
|
|
auction.CreditValue = creditVal;
|
|
}
|
|
|
|
// Estrai data-id-product
|
|
var productMatch = Regex.Match(html, @"data-id-product=""(\d+)""");
|
|
if (productMatch.Success && int.TryParse(productMatch.Groups[1].Value, out int productId))
|
|
{
|
|
auction.ProductId = productId;
|
|
}
|
|
|
|
// Estrai immagine
|
|
var imgMatch = Regex.Match(html, @"<img[^>]+class=""img_small[^""]*""[^>]+src=""([^""]+)""");
|
|
if (imgMatch.Success)
|
|
{
|
|
auction.ImageUrl = imgMatch.Groups[1].Value;
|
|
}
|
|
else
|
|
{
|
|
// Pattern alternativo
|
|
imgMatch = Regex.Match(html, @"src=""(https://[^""]+/products/[^""]+)""");
|
|
if (imgMatch.Success)
|
|
{
|
|
auction.ImageUrl = imgMatch.Groups[1].Value;
|
|
}
|
|
}
|
|
|
|
// Estrai nome prodotto
|
|
var nameMatch = Regex.Match(html, @"<a[^>]+class=""name[^""]*""[^>]*>([^<]+)</a>", RegexOptions.IgnoreCase);
|
|
if (nameMatch.Success)
|
|
{
|
|
auction.Name = System.Net.WebUtility.HtmlDecode(nameMatch.Groups[1].Value.Trim());
|
|
}
|
|
|
|
// Estrai prezzo compralo subito
|
|
var buyNowMatch = Regex.Match(html, @"buy-rapid-now[^>]*>[^<]*<i[^>]*></i>\s*([0-9,\.]+)\s*€", RegexOptions.IgnoreCase);
|
|
if (buyNowMatch.Success)
|
|
{
|
|
var priceStr = buyNowMatch.Groups[1].Value.Replace(",", ".").Trim();
|
|
if (decimal.TryParse(priceStr, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out decimal buyNow))
|
|
{
|
|
auction.BuyNowPrice = buyNow;
|
|
}
|
|
}
|
|
|
|
// Controlla se è manuale (bi-noauto)
|
|
auction.IsManualOnly = html.Contains("bi-noauto", StringComparison.OrdinalIgnoreCase);
|
|
|
|
// Prezzo e bidder verranno aggiornati dalla chiamata a data.php
|
|
auction.CurrentPrice = 0.01m;
|
|
auction.LastBidder = "";
|
|
auction.RemainingSeconds = auction.TimerFrequency;
|
|
|
|
return auction;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[BidooBrowser] Errore parsing asta {auctionId}: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aggiorna lo stato delle aste usando data.php con LISTID (polling multiplo)
|
|
/// Formato chiamata: data.php?LISTID=id1,id2,id3&chk=timestamp
|
|
/// Formato risposta: timestamp*(id;status;expiry;price;bidder;timer;countdown#id2;...)
|
|
/// </summary>
|
|
public async Task UpdateAuctionStatesAsync(List<BidooBrowserAuction> auctions, CancellationToken cancellationToken = default)
|
|
{
|
|
if (auctions.Count == 0) return;
|
|
|
|
try
|
|
{
|
|
// Costruisci la lista di ID per il polling (formato LISTID)
|
|
var auctionIds = string.Join(",", auctions.Select(a => a.AuctionId));
|
|
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
var url = $"https://it.bidoo.com/data.php?LISTID={auctionIds}&chk={timestamp}";
|
|
|
|
Console.WriteLine($"[BidooBrowser] Polling {auctions.Count} aste...");
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
|
AddBrowserHeaders(request, "https://it.bidoo.com/");
|
|
request.Headers.Add("X-Requested-With", "XMLHttpRequest");
|
|
|
|
var response = await _httpClient.SendAsync(request, cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
Console.WriteLine($"[BidooBrowser] Polling fallito: {response.StatusCode}");
|
|
return;
|
|
}
|
|
|
|
var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
|
|
// Parse risposta formato LISTID
|
|
ParseListIdResponse(responseText, auctions);
|
|
|
|
foreach (var auction in auctions)
|
|
{
|
|
auction.LastUpdated = DateTime.UtcNow;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[BidooBrowser] Errore aggiornamento stati: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parsa la risposta di data.php formato LISTID
|
|
/// Formato: serverTimestamp*(id;status;expiry;price;;#id2;status2;...)
|
|
/// Esempio: 1769073106*(85559629;ON;1769082240;1;;#85559630;ON;1769082240;1;;)
|
|
/// Il timestamp del server viene usato come riferimento per calcolare il tempo rimanente
|
|
/// </summary>
|
|
private void ParseListIdResponse(string response, List<BidooBrowserAuction> auctions)
|
|
{
|
|
try
|
|
{
|
|
// Trova inizio dati dopo timestamp*
|
|
var starIndex = response.IndexOf('*');
|
|
if (starIndex == -1)
|
|
{
|
|
Console.WriteLine("[BidooBrowser] Risposta non valida: manca '*'");
|
|
return;
|
|
}
|
|
|
|
// Estrai il timestamp del server (prima di *)
|
|
var serverTimestampStr = response.Substring(0, starIndex);
|
|
long serverTimestamp = 0;
|
|
long.TryParse(serverTimestampStr, out serverTimestamp);
|
|
|
|
var mainData = response.Substring(starIndex + 1);
|
|
|
|
// Rimuovi parentesi se presenti
|
|
if (mainData.StartsWith("(") && mainData.EndsWith(")"))
|
|
{
|
|
mainData = mainData.Substring(1, mainData.Length - 2);
|
|
}
|
|
|
|
// Split per ogni asta (separatore #)
|
|
var auctionEntries = mainData.Split('#', StringSplitOptions.RemoveEmptyEntries);
|
|
int updatedCount = 0;
|
|
|
|
foreach (var entry in auctionEntries)
|
|
{
|
|
// Formato: id;status;expiry;price;; (bidder e timer possono essere vuoti)
|
|
var fields = entry.Split(';');
|
|
if (fields.Length < 4) continue;
|
|
|
|
var id = fields[0].Trim();
|
|
var status = fields[1].Trim(); // ON/OFF
|
|
var expiryStr = fields[2].Trim(); // timestamp scadenza (stesso formato del server)
|
|
var priceStr = fields[3].Trim(); // prezzo (centesimi)
|
|
var bidder = fields.Length > 4 ? fields[4].Trim() : ""; // ultimo bidder (può essere vuoto)
|
|
|
|
var auction = auctions.FirstOrDefault(a => a.AuctionId == id);
|
|
if (auction == null) continue;
|
|
|
|
// Aggiorna prezzo (è in centesimi, convertire in euro)
|
|
if (int.TryParse(priceStr, out int priceCents))
|
|
{
|
|
auction.CurrentPrice = priceCents / 100m;
|
|
}
|
|
|
|
// Aggiorna bidder solo se non vuoto
|
|
if (!string.IsNullOrEmpty(bidder))
|
|
{
|
|
auction.LastBidder = bidder;
|
|
}
|
|
|
|
// Calcola tempo rimanente usando il timestamp del server come riferimento
|
|
if (long.TryParse(expiryStr, out long expiryTimestamp) && serverTimestamp > 0)
|
|
{
|
|
// Il tempo rimanente è: expiry - serverTime (entrambi nello stesso formato)
|
|
var remainingSeconds = expiryTimestamp - serverTimestamp;
|
|
auction.RemainingSeconds = remainingSeconds > 0 ? (int)remainingSeconds : 0;
|
|
}
|
|
else if (status == "ON")
|
|
{
|
|
// Se non riusciamo a calcolare, usa il timer frequency come fallback
|
|
if (auction.RemainingSeconds <= 0)
|
|
{
|
|
auction.RemainingSeconds = auction.TimerFrequency;
|
|
}
|
|
}
|
|
|
|
// Status: ON = attiva in countdown, OFF = terminata/in pausa
|
|
auction.IsActive = status == "ON";
|
|
auction.IsSold = status != "ON" && auction.RemainingSeconds <= 0;
|
|
|
|
updatedCount++;
|
|
}
|
|
|
|
Console.WriteLine($"[BidooBrowser] Aggiornate {updatedCount} aste su {auctionEntries.Length}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[BidooBrowser] Errore parsing LISTID response: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converte countdown string in secondi
|
|
/// Formati: "7m", "1h 16m", "00:08", vuoto (usa timer frequency)
|
|
/// </summary>
|
|
private int ParseCountdown(string countdown, int defaultSeconds)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(countdown))
|
|
{
|
|
return defaultSeconds;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Formato ore e minuti: "1h 16m"
|
|
var hourMatch = Regex.Match(countdown, @"(\d+)h");
|
|
var minMatch = Regex.Match(countdown, @"(\d+)m");
|
|
|
|
int totalSeconds = 0;
|
|
|
|
if (hourMatch.Success && int.TryParse(hourMatch.Groups[1].Value, out int hours))
|
|
{
|
|
totalSeconds += hours * 3600;
|
|
}
|
|
|
|
if (minMatch.Success && int.TryParse(minMatch.Groups[1].Value, out int mins))
|
|
{
|
|
totalSeconds += mins * 60;
|
|
}
|
|
|
|
if (totalSeconds > 0)
|
|
{
|
|
return totalSeconds;
|
|
}
|
|
|
|
// Formato "00:08" (mm:ss o ss)
|
|
if (countdown.Contains(":"))
|
|
{
|
|
var parts = countdown.Split(':');
|
|
if (parts.Length == 2 &&
|
|
int.TryParse(parts[0], out int p1) &&
|
|
int.TryParse(parts[1], out int p2))
|
|
{
|
|
return p1 * 60 + p2;
|
|
}
|
|
}
|
|
|
|
// Solo numero = secondi
|
|
if (int.TryParse(countdown, out int secs))
|
|
{
|
|
return secs;
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
return defaultSeconds;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Carica nuove aste usando get_auction_updates.php (simula scrolling infinito)
|
|
/// Questa API restituisce aste che non sono ancora state caricate
|
|
/// </summary>
|
|
public async Task<List<BidooBrowserAuction>> GetMoreAuctionsAsync(
|
|
BidooCategoryInfo category,
|
|
List<string> existingAuctionIds,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var newAuctions = new List<BidooBrowserAuction>();
|
|
|
|
try
|
|
{
|
|
var existingIdsSet = existingAuctionIds.ToHashSet();
|
|
|
|
// Prepara la chiamata POST a get_auction_updates.php
|
|
var url = "https://it.bidoo.com/get_auction_updates.php";
|
|
|
|
// Costruisci il body della richiesta
|
|
var viewIds = string.Join(",", existingAuctionIds);
|
|
var tabValue = category.IsSpecialCategory ? category.TabId : 4;
|
|
var tagValue = category.IsSpecialCategory ? 0 : category.TagId;
|
|
|
|
var formContent = new FormUrlEncodedContent(new[]
|
|
{
|
|
new KeyValuePair<string, string>("prefetch", "true"),
|
|
new KeyValuePair<string, string>("view", viewIds),
|
|
new KeyValuePair<string, string>("tab", tabValue.ToString()),
|
|
new KeyValuePair<string, string>("tag", tagValue.ToString())
|
|
});
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Post, url)
|
|
{
|
|
Content = formContent
|
|
};
|
|
|
|
AddBrowserHeaders(request, "https://it.bidoo.com/");
|
|
request.Headers.Add("X-Requested-With", "XMLHttpRequest");
|
|
|
|
Console.WriteLine($"[BidooBrowser] Fetching more auctions with {existingAuctionIds.Count} existing IDs...");
|
|
|
|
var response = await _httpClient.SendAsync(request, cancellationToken);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
|
|
// Parse la risposta JSON
|
|
// Formato: {"gc":[],"int":[],"list":[id1,id2,...],"items":["<html>","<html>",...]}
|
|
newAuctions = ParseGetAuctionUpdatesResponse(responseText, existingIdsSet);
|
|
|
|
Console.WriteLine($"[BidooBrowser] Trovate {newAuctions.Count} nuove aste");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[BidooBrowser] Errore caricamento nuove aste: {ex.Message}");
|
|
}
|
|
|
|
return newAuctions;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parsa la risposta di get_auction_updates.php
|
|
/// </summary>
|
|
private List<BidooBrowserAuction> ParseGetAuctionUpdatesResponse(string json, HashSet<string> existingIds)
|
|
{
|
|
var auctions = new List<BidooBrowserAuction>();
|
|
|
|
try
|
|
{
|
|
// Parse JSON manuale per estrarre items[]
|
|
// Cerchiamo "items":["...","..."]
|
|
var itemsMatch = Regex.Match(json, @"""items"":\s*\[(.*?)\](?=,""|\})", RegexOptions.Singleline);
|
|
if (!itemsMatch.Success)
|
|
{
|
|
Console.WriteLine("[BidooBrowser] Nessun items trovato nella risposta");
|
|
return auctions;
|
|
}
|
|
|
|
var itemsContent = itemsMatch.Groups[1].Value;
|
|
|
|
// Gli items sono stringhe HTML escaped, dobbiamo parsarle
|
|
// Ogni item è una stringa JSON che contiene HTML
|
|
var htmlPattern = new Regex(@"""((?:[^""\\]|\\.)*?)""", RegexOptions.Singleline);
|
|
var htmlMatches = htmlPattern.Matches(itemsContent);
|
|
|
|
foreach (Match htmlMatch in htmlMatches)
|
|
{
|
|
if (!htmlMatch.Success) continue;
|
|
|
|
// Unescape la stringa JSON
|
|
var escapedHtml = htmlMatch.Groups[1].Value;
|
|
var html = UnescapeJsonString(escapedHtml);
|
|
|
|
// Estrai l'ID dell'asta
|
|
var idMatch = Regex.Match(html, @"id=""divAsta(\d+)""");
|
|
if (!idMatch.Success) continue;
|
|
|
|
var auctionId = idMatch.Groups[1].Value;
|
|
|
|
// Salta se già esiste
|
|
if (existingIds.Contains(auctionId)) continue;
|
|
|
|
// Parsa l'asta dall'HTML
|
|
var auction = ParseSingleAuction(auctionId, html);
|
|
if (auction != null)
|
|
{
|
|
auctions.Add(auction);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[BidooBrowser] Errore parsing get_auction_updates response: {ex.Message}");
|
|
}
|
|
|
|
return auctions;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unescape di una stringa JSON
|
|
/// </summary>
|
|
private static string UnescapeJsonString(string escaped)
|
|
{
|
|
return escaped
|
|
.Replace("\\/", "/")
|
|
.Replace("\\n", "\n")
|
|
.Replace("\\r", "\r")
|
|
.Replace("\\t", "\t")
|
|
.Replace("\\\"", "\"")
|
|
.Replace("\\\\", "\\");
|
|
}
|
|
}
|
|
}
|