Aggiunta calcolo valore prodotto e miglioramenti UI

Implementato il calcolo del valore reale dei prodotti in asta,
includendo il prezzo "Compra Subito", spese di spedizione e
risparmio stimato. Aggiunta una nuova sezione "Info Prodotto"
nella UI per visualizzare i dettagli estratti e i calcoli.

- **AuctionMonitorControl.xaml**: Aggiunta sezione fissa per
  mostrare informazioni prodotto e calcolo valore.
- **AuctionMonitorControl.xaml.cs**: Gestiti eventi per il
  caricamento e aggiornamento delle informazioni prodotto.
- **MainWindow**: Integrati handler per il calcolo e refresh
  delle informazioni prodotto.
- **AuctionInfo.cs**: Aggiunte proprietà per gestire prezzo
  "Compra Subito", spese di spedizione e limiti di vincita.
- **ProductValueCalculator.cs**: Nuova utility per calcolare
  il valore del prodotto e parsare informazioni dall'HTML.
- **AuctionViewModel.cs**: Binding per visualizzare risparmio,
  costo totale e convenienza nella UI.
- **Documentazione**: Aggiornata con dettagli sull'algoritmo
  di calcolo e layout UI.

Fix:
- Risolto problema di encoding UTF-8 per emoji nella UI.
- Migliorato parsing HTML per prezzi e limiti di vincita.

TODO:
- Testare parsing su più aste e gestire edge cases.
- Implementare caricamento automatico delle informazioni.
This commit is contained in:
2025-11-21 16:55:21 +01:00
parent f124f2e4e8
commit ee67bedc31
171 changed files with 134519 additions and 241 deletions
@@ -0,0 +1,173 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using AutoBidder.Models;
using AutoBidder.Utilities;
namespace AutoBidder.Services
{
/// <summary>
/// Estensioni per AuctionMonitor per gestire il calcolo del valore del prodotto
/// </summary>
public static class AuctionMonitorProductValueExtensions
{
/// <summary>
/// Inizializza le informazioni del prodotto per un'asta
/// Scarica la pagina HTML e estrae prezzo "Compra Subito", limite vincita, ecc.
/// </summary>
/// <param name="apiClient">Client API per scaricare la pagina</param>
/// <param name="auctionInfo">Asta da inizializzare</param>
/// <param name="token">Token di cancellazione</param>
/// <returns>True se almeno alcune informazioni sono state estratte</returns>
public static async Task<bool> InitializeProductInfoAsync(
BidooApiClient apiClient,
AuctionInfo auctionInfo,
CancellationToken token = default)
{
try
{
// Costruisci URL della pagina dell'asta
string auctionUrl = auctionInfo.OriginalUrl;
if (string.IsNullOrWhiteSpace(auctionUrl))
{
// Fallback: costruisci URL standard
auctionUrl = $"https://it.bidoo.com/auction.php?a={auctionInfo.AuctionId}";
}
auctionInfo.AddLog("[PRODUCT INFO] Scaricamento informazioni prodotto...");
// Scarica HTML della pagina (riutilizza il metodo di polling se necessario)
var html = await DownloadAuctionPageAsync(apiClient, auctionUrl, token);
if (string.IsNullOrWhiteSpace(html))
{
auctionInfo.AddLog("[PRODUCT INFO] WARN: Nessun HTML ricevuto");
return false;
}
// Estrai informazioni dal HTML
bool extracted = ProductValueCalculator.ExtractProductInfo(html, auctionInfo);
if (extracted)
{
var log = "[PRODUCT INFO] Estratte:";
if (auctionInfo.BuyNowPrice.HasValue)
log += $" Compra Subito={auctionInfo.BuyNowPrice.Value:F2}€";
if (auctionInfo.HasWinLimit)
log += $" Limite={auctionInfo.WinLimitDescription}";
auctionInfo.AddLog(log);
}
else
{
auctionInfo.AddLog("[PRODUCT INFO] WARN: Nessuna informazione trovata nell'HTML");
}
return extracted;
}
catch (Exception ex)
{
auctionInfo.AddLog($"[PRODUCT INFO ERROR] {ex.Message}");
return false;
}
}
/// <summary>
/// Aggiorna il valore calcolato del prodotto basandosi sullo stato corrente
/// </summary>
/// <param name="auctionInfo">Asta da aggiornare</param>
/// <param name="state">Stato corrente dell'asta</param>
/// <param name="logValue">Se true, logga il valore nel log dell'asta</param>
public static void UpdateProductValue(AuctionInfo auctionInfo, AuctionState state, bool logValue = false)
{
try
{
// Calcola numero totale puntate basato sul prezzo (ogni puntata = +0.01€)
int totalBids = (int)Math.Round(state.Price * 100);
// Calcola valore
var value = ProductValueCalculator.Calculate(auctionInfo, state.Price, totalBids);
// Salva nel model
auctionInfo.CalculatedValue = value;
// Log opzionale (utile solo per eventi significativi, non ad ogni polling)
if (logValue)
{
var message = ProductValueCalculator.FormatValueMessage(value);
auctionInfo.AddLog($"[VALUE] {message}");
}
}
catch (Exception ex)
{
auctionInfo.AddLog($"[VALUE ERROR] {ex.Message}");
}
}
/// <summary>
/// Verifica se conviene ancora puntare basandosi sul valore del prodotto
/// </summary>
/// <param name="auctionInfo">Asta da verificare</param>
/// <param name="minSavingsPercentage">Percentuale minima di risparmio per considerare conveniente (default: 0)</param>
/// <returns>True se conviene puntare, False se si sta spendendo troppo</returns>
public static bool IsStillWorthBidding(AuctionInfo auctionInfo, double minSavingsPercentage = 0)
{
var value = auctionInfo.CalculatedValue;
// Se non abbiamo calcolato il valore, assumiamo sia conveniente
if (value == null)
return true;
// Se non c'è un prezzo "Compra Subito" di riferimento, non possiamo decidere
if (!value.BuyNowPrice.HasValue)
return true;
// Verifica risparmio
if (!value.Savings.HasValue)
return true;
// Controlla se il risparmio è abbastanza
if (value.SavingsPercentage.HasValue)
{
return value.SavingsPercentage.Value >= minSavingsPercentage;
}
return value.Savings.Value >= 0;
}
/// <summary>
/// Scarica la pagina HTML dell'asta
/// </summary>
private static async Task<string> DownloadAuctionPageAsync(
BidooApiClient apiClient,
string url,
CancellationToken token)
{
try
{
// Usa HttpClient del BidooApiClient se disponibile
// Altrimenti crea un client temporaneo
using var httpClient = new System.Net.Http.HttpClient();
// Copia headers dal client API se disponibile (per autenticazione)
var session = apiClient.GetSession();
if (!string.IsNullOrWhiteSpace(session.AuthToken))
{
httpClient.DefaultRequestHeaders.Add("Cookie", $"auth_token={session.AuthToken}");
}
httpClient.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
var response = await httpClient.GetAsync(url, token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch
{
return string.Empty;
}
}
}
}
+12 -62
View File
@@ -495,20 +495,20 @@ namespace AutoBidder.Services
result.Success = true;
var parts = responseText.Split('|');
// Log della risposta completa per debugging
Log($"[BID PARSE] Risposta completa: {responseText}", auctionId);
Log($"[BID PARSE] Numero totale campi: {parts.Length}", auctionId);
// Formato risposta Bidoo: 9 campi separati da |
// Campo 1 (indice 0): "ok"
// Campo 2 (indice 1): Puntate residue totali
// Campo 3 (indice 2): ?
// Campo 4 (indice 3): ?
if (parts.Length > 1 && int.TryParse(parts[1], out var remaining))
{
result.RemainingBids = remaining;
_session.RemainingBids = remaining;
}
// Campo 5 (indice 4): Puntate usate su questa asta
// Campo 6-9 (indice 5-8): Altri dati
if (parts.Length > 4 && int.TryParse(parts[4], out var usedOnAuction))
{
result.BidsUsedOnThisAuction = usedOnAuction;
}
// Prezzo - presumibilmente nel campo 3 o nei campi successivi
// Cerchiamo un campo numerico ragionevole per il prezzo
for (int i = 1; i < Math.Min(parts.Length, 6); i++)
{
if (i == 1 || i == 4) continue; // Skip remaining bids e bids used
@@ -516,62 +516,12 @@ namespace AutoBidder.Services
if (double.TryParse(parts[i], out var priceIndex) && priceIndex > 0)
{
result.NewPrice = priceIndex * 0.01;
Log($"[BID PARSE] Campo {i+1} - Prezzo parsato: €{result.NewPrice:F2}", auctionId);
break;
}
}
// Campo 2 (indice 1): Puntate residue totali
if (parts.Length > 1)
{
Log($"[BID PARSE] Campo 2 (indice 1) - Remaining bids: '{parts[1]}'", auctionId);
if (int.TryParse(parts[1], out var remaining))
{
result.RemainingBids = remaining;
_session.RemainingBids = remaining;
Log($"[BID SUCCESS] ✅ Puntate residue totali: {remaining}", auctionId);
}
else
{
Log($"[BID PARSE WARN] ⚠️ Impossibile parsare campo 2 come int: '{parts[1]}'", auctionId);
}
}
else
{
Log($"[BID PARSE ERROR] ❌ Risposta non ha campo 2 (remaining bids)", auctionId);
}
// Campo 5 (indice 4): Puntate usate su questa asta
if (parts.Length > 4)
{
Log($"[BID PARSE] Campo 5 (indice 4) - Bids used on auction: '{parts[4]}'", auctionId);
if (int.TryParse(parts[4], out var usedOnAuction))
{
result.BidsUsedOnThisAuction = usedOnAuction;
Log($"[BID SUCCESS] ✅ Puntate usate su questa asta: {usedOnAuction}", auctionId);
}
else
{
Log($"[BID PARSE WARN] ⚠️ Impossibile parsare campo 5 come int: '{parts[4]}'", auctionId);
}
}
else
{
Log($"[BID PARSE ERROR] ❌ Risposta non ha campo 5 (bids used on auction) - Campi totali: {parts.Length}", auctionId);
}
// Log tutti i campi per debugging completo
Log($"[BID PARSE DEBUG] Tutti i campi della risposta:", auctionId);
for (int i = 0; i < parts.Length; i++)
{
Log($" Campo {i+1} (indice {i}): '{parts[i]}'", auctionId);
}
// Messaggio finale
if (!result.RemainingBids.HasValue && !result.BidsUsedOnThisAuction.HasValue)
{
Log("[BID SUCCESS] ⚠️ Puntata piazzata ma dati contatori non disponibili nella risposta", auctionId);
}
// Log solo successo
Log($"[BID SUCCESS] Puntata piazzata", auctionId);
}
else if (responseText.StartsWith("error", StringComparison.OrdinalIgnoreCase) || responseText.StartsWith("no|", StringComparison.OrdinalIgnoreCase))
{