Aggiunta pagina "Esplora Aste" per browser pubblico

Introdotta la funzionalità di esplorazione delle aste pubbliche di Bidoo senza login, accessibile dal menu principale.
Aggiunti nuovi modelli (`BidooBrowserAuction`, `BidooCategoryInfo`) e servizio (`BidooBrowserService`) per scraping e polling delle aste e categorie.
Creata la pagina Blazor `AuctionBrowser.razor` con griglia responsive, badge, filtri per categoria, caricamento incrementale e aggiornamento automatico degli stati.
Aggiornati i servizi in `Program.cs` e aggiunti nuovi stili CSS per la UI moderna.
Le aste possono essere aggiunte rapidamente al monitor personale. Parsing robusto e fallback su categorie predefinite in caso di errori.
This commit is contained in:
2026-01-22 00:08:16 +01:00
parent 70ed8f0a61
commit 865bfa2752
8 changed files with 369048 additions and 0 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,106 @@
using System;
namespace AutoBidder.Models
{
/// <summary>
/// Rappresenta un'asta visualizzata nel browser delle aste
/// Contiene informazioni base per la visualizzazione nella griglia
/// </summary>
public class BidooBrowserAuction
{
/// <summary>
/// ID univoco dell'asta
/// </summary>
public string AuctionId { get; set; } = "";
/// <summary>
/// URL completo dell'asta
/// </summary>
public string Url { get; set; } = "";
/// <summary>
/// Nome/titolo del prodotto
/// </summary>
public string Name { get; set; } = "";
/// <summary>
/// URL dell'immagine del prodotto
/// </summary>
public string ImageUrl { get; set; } = "";
/// <summary>
/// Prezzo attuale dell'asta in euro
/// </summary>
public decimal CurrentPrice { get; set; }
/// <summary>
/// Username dell'ultimo bidder
/// </summary>
public string LastBidder { get; set; } = "";
/// <summary>
/// Tempo rimanente in secondi
/// </summary>
public int RemainingSeconds { get; set; }
/// <summary>
/// Timer formattato (es: "00:08")
/// </summary>
public string TimerDisplay => $"{RemainingSeconds / 60:00}:{RemainingSeconds % 60:00}";
/// <summary>
/// Frequenza timer dell'asta (in secondi)
/// </summary>
public int TimerFrequency { get; set; } = 8;
/// <summary>
/// Prezzo "Compralo Subito"
/// </summary>
public decimal BuyNowPrice { get; set; }
/// <summary>
/// Indica se l'asta è già stata aggiunta al monitor
/// </summary>
public bool IsMonitored { get; set; }
/// <summary>
/// Indica se l'asta è attiva (non chiusa)
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// Indica se l'asta è venduta
/// </summary>
public bool IsSold { get; set; }
/// <summary>
/// Indica se l'asta richiede solo puntate manuali (no autobid)
/// </summary>
public bool IsManualOnly { get; set; }
/// <summary>
/// Indica se è un'asta turbo (timer < 10 sec)
/// </summary>
public bool IsTurbo => TimerFrequency <= 8;
/// <summary>
/// ID del prodotto
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// Indica se l'asta è un'asta di puntate/crediti
/// </summary>
public bool IsCreditAuction { get; set; }
/// <summary>
/// Valore crediti se è un'asta di puntate
/// </summary>
public int CreditValue { get; set; }
/// <summary>
/// Timestamp ultimo aggiornamento stato
/// </summary>
public DateTime LastUpdated { get; set; } = DateTime.UtcNow;
}
}

View File

@@ -0,0 +1,40 @@
namespace AutoBidder.Models
{
/// <summary>
/// Rappresenta una categoria/scheda di aste su Bidoo
/// </summary>
public class BidooCategoryInfo
{
/// <summary>
/// ID del tab (es: 1, 2, 3, 4, 5)
/// </summary>
public int TabId { get; set; }
/// <summary>
/// ID del tag per le categorie specifiche (es: 6=Buoni, 5=Smartphone)
/// </summary>
public int TagId { get; set; }
/// <summary>
/// Slug della categoria (es: "buoni", "smartphone")
/// </summary>
public string Slug { get; set; } = "";
/// <summary>
/// Nome visualizzato della categoria
/// </summary>
public string DisplayName { get; set; } = "";
/// <summary>
/// Indica se questa categoria è una categoria speciale (preferite, tutte, puntate, manuali)
/// </summary>
public bool IsSpecialCategory { get; set; }
/// <summary>
/// Icona da mostrare (opzionale)
/// </summary>
public string? Icon { get; set; }
public override string ToString() => DisplayName;
}
}

View File

@@ -0,0 +1,450 @@
@page "/browser"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@using AutoBidder.Models
@using AutoBidder.Services
@inject BidooBrowserService BrowserService
@inject ApplicationStateService AppState
@implements IDisposable
<PageTitle>Esplora Aste - AutoBidder</PageTitle>
<div class="browser-container animate-fade-in p-4">
<!-- Header -->
<div class="d-flex align-items-center justify-content-between mb-4 flex-wrap gap-3">
<div class="d-flex align-items-center animate-fade-in-down">
<i class="bi bi-search text-primary me-3" style="font-size: 2rem;"></i>
<div>
<h2 class="mb-0 fw-bold">Esplora Aste</h2>
<small class="text-muted">Naviga le aste pubbliche di Bidoo senza login</small>
</div>
</div>
<div class="d-flex gap-2 flex-wrap">
<button class="btn btn-outline-secondary" @onclick="RefreshAll" disabled="@isLoading">
<i class="bi @(isLoading ? "bi-arrow-clockwise spin" : "bi-arrow-clockwise")"></i>
Aggiorna
</button>
@if (auctions.Count > 0)
{
<button class="btn btn-outline-primary" @onclick="UpdateAuctionStates" disabled="@isUpdatingStates">
<i class="bi @(isUpdatingStates ? "bi-broadcast spin" : "bi-broadcast")"></i>
Aggiorna Prezzi
</button>
}
</div>
</div>
<!-- Category Selector -->
<div class="card mb-4 shadow-sm">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-6">
<label class="form-label fw-semibold">
<i class="bi bi-tag me-2"></i>Categoria
</label>
<select class="form-select form-select-lg" @bind="selectedCategoryIndex" @bind:after="OnCategoryChanged">
@if (categories.Count == 0)
{
<option value="-1">Caricamento categorie...</option>
}
else
{
@for (int i = 0; i < categories.Count; i++)
{
<option value="@i">
@if (!string.IsNullOrEmpty(categories[i].Icon))
{
@categories[i].DisplayName
}
else
{
@categories[i].DisplayName
}
</option>
}
}
</select>
</div>
<div class="col-md-3">
<div class="stats-mini">
<span class="text-muted">Aste caricate:</span>
<span class="fw-bold text-primary ms-2">@auctions.Count</span>
</div>
</div>
<div class="col-md-3">
<div class="stats-mini">
<span class="text-muted">Monitorate:</span>
<span class="fw-bold text-success ms-2">@auctions.Count(a => a.IsMonitored)</span>
</div>
</div>
</div>
</div>
</div>
<!-- Loading State -->
@if (isLoading)
{
<div class="text-center py-5 animate-fade-in">
<div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;">
<span class="visually-hidden">Caricamento...</span>
</div>
<p class="text-muted">Caricamento aste in corso...</p>
</div>
}
else if (errorMessage != null)
{
<div class="alert alert-warning d-flex align-items-center animate-scale-in">
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
<div>
<strong>Attenzione</strong><br />
@errorMessage
</div>
</div>
}
else if (auctions.Count == 0)
{
<div class="text-center py-5 animate-fade-in">
<i class="bi bi-inbox text-muted" style="font-size: 4rem;"></i>
<p class="text-muted mt-3">Nessuna asta trovata in questa categoria</p>
<button class="btn btn-primary" @onclick="LoadAuctions">
<i class="bi bi-arrow-clockwise me-2"></i>Ricarica
</button>
</div>
}
else
{
<!-- Auctions Grid -->
<div class="auction-grid animate-fade-in">
@foreach (var auction in auctions)
{
<div class="auction-card @(auction.IsMonitored ? "monitored" : "") @(auction.IsSold ? "sold" : "")">
<!-- Image -->
<div class="auction-image">
@if (!string.IsNullOrEmpty(auction.ImageUrl))
{
<img src="@auction.ImageUrl" alt="@auction.Name" loading="lazy" />
}
else
{
<div class="placeholder-image">
<i class="bi bi-image"></i>
</div>
}
<!-- Badges -->
<div class="auction-badges">
@if (auction.IsCreditAuction)
{
<span class="badge bg-warning text-dark">
<i class="bi bi-coin"></i> @auction.CreditValue
</span>
}
@if (auction.IsManualOnly)
{
<span class="badge bg-info">
<i class="bi bi-hand-index"></i> Manuale
</span>
}
@if (auction.IsTurbo)
{
<span class="badge bg-danger">
<i class="bi bi-lightning"></i> @auction.TimerFrequency s
</span>
}
</div>
@if (auction.IsSold)
{
<div class="sold-overlay">
<span>VENDUTO</span>
</div>
}
@if (auction.IsMonitored)
{
<div class="monitored-badge">
<i class="bi bi-check-circle-fill"></i>
</div>
}
</div>
<!-- Info -->
<div class="auction-info">
<h6 class="auction-name" title="@auction.Name">@auction.Name</h6>
<div class="auction-price">
<span class="current-price">@auction.CurrentPrice.ToString("0.00") €</span>
@if (auction.BuyNowPrice > 0)
{
<span class="buynow-price text-muted">
<small>Compra: @auction.BuyNowPrice.ToString("0.00") €</small>
</span>
}
</div>
<div class="auction-bidder">
<i class="bi bi-person-fill text-muted me-1"></i>
<span>@(string.IsNullOrEmpty(auction.LastBidder) ? "—" : auction.LastBidder)</span>
</div>
<div class="auction-timer @(auction.RemainingSeconds <= 3 ? "urgent" : "")">
<i class="bi bi-clock me-1"></i>
@auction.TimerDisplay
</div>
</div>
<!-- Actions -->
<div class="auction-actions">
@if (auction.IsMonitored)
{
<button class="btn btn-success btn-sm w-100" disabled>
<i class="bi bi-check-lg me-1"></i>Monitorata
</button>
}
else
{
<button class="btn btn-primary btn-sm w-100" @onclick="() => AddToMonitor(auction)">
<i class="bi bi-plus-lg me-1"></i>Aggiungi al Monitor
</button>
}
</div>
</div>
}
</div>
<!-- Load More -->
@if (canLoadMore)
{
<div class="text-center mt-4">
<button class="btn btn-outline-primary btn-lg" @onclick="LoadMoreAuctions" disabled="@isLoadingMore">
@if (isLoadingMore)
{
<span class="spinner-border spinner-border-sm me-2"></span>
}
else
{
<i class="bi bi-plus-circle me-2"></i>
}
Carica Altre Aste
</button>
</div>
}
}
</div>
@code {
private List<BidooCategoryInfo> categories = new();
private List<BidooBrowserAuction> auctions = new();
private int selectedCategoryIndex = 0;
private int currentPage = 0;
private bool isLoading = false;
private bool isLoadingMore = false;
private bool isUpdatingStates = false;
private bool canLoadMore = true;
private string? errorMessage = null;
private System.Threading.Timer? stateUpdateTimer;
private CancellationTokenSource? cts;
protected override async Task OnInitializedAsync()
{
await LoadCategories();
if (categories.Count > 0)
{
await LoadAuctions();
}
// Auto-update states every 5 seconds
stateUpdateTimer = new System.Threading.Timer(async _ =>
{
if (auctions.Count > 0 && !isUpdatingStates)
{
await UpdateAuctionStatesBackground();
}
}, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
}
private async Task LoadCategories()
{
try
{
categories = await BrowserService.GetCategoriesAsync();
}
catch (Exception ex)
{
Console.WriteLine($"[Browser] Error loading categories: {ex.Message}");
errorMessage = "Errore nel caricamento delle categorie";
}
}
private async Task OnCategoryChanged()
{
currentPage = 0;
canLoadMore = true;
auctions.Clear();
await LoadAuctions();
}
private async Task LoadAuctions()
{
if (categories.Count == 0 || selectedCategoryIndex < 0 || selectedCategoryIndex >= categories.Count)
return;
isLoading = true;
errorMessage = null;
cts?.Cancel();
cts = new CancellationTokenSource();
try
{
var category = categories[selectedCategoryIndex];
var newAuctions = await BrowserService.GetAuctionsAsync(category, currentPage, cts.Token);
auctions = newAuctions;
canLoadMore = newAuctions.Count >= 20; // Assume pagination at 20
// Mark already monitored auctions
UpdateMonitoredStatus();
// Get initial states
if (auctions.Count > 0)
{
await BrowserService.UpdateAuctionStatesAsync(auctions, cts.Token);
}
}
catch (OperationCanceledException)
{
// Ignore cancellation
}
catch (Exception ex)
{
Console.WriteLine($"[Browser] Error loading auctions: {ex.Message}");
errorMessage = "Errore nel caricamento delle aste";
}
finally
{
isLoading = false;
StateHasChanged();
}
}
private async Task LoadMoreAuctions()
{
if (categories.Count == 0 || selectedCategoryIndex < 0)
return;
isLoadingMore = true;
currentPage++;
cts?.Cancel();
cts = new CancellationTokenSource();
try
{
var category = categories[selectedCategoryIndex];
var newAuctions = await BrowserService.GetAuctionsAsync(category, currentPage, cts.Token);
if (newAuctions.Count == 0)
{
canLoadMore = false;
}
else
{
auctions.AddRange(newAuctions);
UpdateMonitoredStatus();
await BrowserService.UpdateAuctionStatesAsync(newAuctions, cts.Token);
}
}
catch (Exception ex)
{
Console.WriteLine($"[Browser] Error loading more auctions: {ex.Message}");
currentPage--; // Rollback
}
finally
{
isLoadingMore = false;
StateHasChanged();
}
}
private async Task UpdateAuctionStates()
{
if (auctions.Count == 0) return;
isUpdatingStates = true;
try
{
await BrowserService.UpdateAuctionStatesAsync(auctions);
UpdateMonitoredStatus();
}
finally
{
isUpdatingStates = false;
StateHasChanged();
}
}
private async Task UpdateAuctionStatesBackground()
{
try
{
await BrowserService.UpdateAuctionStatesAsync(auctions);
UpdateMonitoredStatus();
await InvokeAsync(StateHasChanged);
}
catch
{
// Ignore background errors
}
}
private async Task RefreshAll()
{
await LoadCategories();
currentPage = 0;
canLoadMore = true;
auctions.Clear();
await LoadAuctions();
}
private void UpdateMonitoredStatus()
{
var monitoredIds = AppState.Auctions.Select(a => a.AuctionId).ToHashSet();
foreach (var auction in auctions)
{
auction.IsMonitored = monitoredIds.Contains(auction.AuctionId);
}
}
private void AddToMonitor(BidooBrowserAuction browserAuction)
{
if (browserAuction.IsMonitored) return;
var auctionInfo = new AuctionInfo
{
AuctionId = browserAuction.AuctionId,
Name = browserAuction.Name,
OriginalUrl = browserAuction.Url,
BuyNowPrice = (double)browserAuction.BuyNowPrice,
IsActive = true,
IsPaused = true, // Start paused
AddedAt = DateTime.UtcNow
};
AppState.AddAuction(auctionInfo);
browserAuction.IsMonitored = true;
// Save to disk
AutoBidder.Utilities.PersistenceManager.SaveAuctions(AppState.Auctions.ToList());
StateHasChanged();
}
public void Dispose()
{
stateUpdateTimer?.Dispose();
cts?.Cancel();
cts?.Dispose();
}
}

View File

@@ -244,6 +244,7 @@ builder.Services.AddSingleton(htmlCacheService);
builder.Services.AddSingleton(sp => new SessionService(auctionMonitor.GetApiClient()));
builder.Services.AddSingleton<DatabaseService>();
builder.Services.AddSingleton<ApplicationStateService>();
builder.Services.AddSingleton<BidooBrowserService>();
builder.Services.AddScoped<StatsService>(sp =>
{
var db = sp.GetRequiredService<DatabaseService>();

View File

@@ -0,0 +1,582 @@
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}");
}
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: timestamp*(id;status;expiry;price;bidder;timer;countdown#id2;status2;...)
/// Esempio: 1769032850*(85583891;OFF;1769019191;62;sederafo30;3;7m#85582947;OFF;1769023093;680;pandaka;3;1h 16m)
/// </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;
}
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);
foreach (var entry in auctionEntries)
{
// Formato: id;status;expiry;price;bidder;timer;countdown
var fields = entry.Split(';');
if (fields.Length < 5) continue;
var id = fields[0].Trim();
var status = fields[1].Trim(); // ON/OFF
var expiry = fields[2].Trim(); // timestamp scadenza
var priceStr = fields[3].Trim(); // prezzo in centesimi
var bidder = fields[4].Trim(); // ultimo bidder
var timer = fields.Length > 5 ? fields[5].Trim() : ""; // frequenza timer
var countdown = fields.Length > 6 ? fields[6].Trim() : ""; // tempo rimanente (es: "7m", "1h 16m")
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
auction.LastBidder = bidder;
// Aggiorna timer frequency
if (int.TryParse(timer, out int timerFreq) && timerFreq > 0)
{
auction.TimerFrequency = timerFreq;
}
// Parse countdown per calcolare secondi rimanenti
auction.RemainingSeconds = ParseCountdown(countdown, auction.TimerFrequency);
// Status: ON = attiva, OFF = in countdown
auction.IsActive = true;
auction.IsSold = false;
// Se countdown contiene "Ha Vinto" o simile, è venduta
if (countdown.Contains("Vinto", StringComparison.OrdinalIgnoreCase) ||
countdown.Contains("Chiusa", StringComparison.OrdinalIgnoreCase))
{
auction.IsSold = true;
auction.IsActive = false;
}
}
Console.WriteLine($"[BidooBrowser] Aggiornate {auctionEntries.Length} aste");
}
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;
}
}
}

View File

@@ -19,6 +19,11 @@
<span>Monitor Aste</span>
</NavLink>
<NavLink class="nav-menu-item" href="browser">
<i class="bi bi-search"></i>
<span>Esplora Aste</span>
</NavLink>
<NavLink class="nav-menu-item" href="freebids">
<i class="bi bi-gift"></i>
<span>Puntate Gratuite</span>

View File

@@ -226,3 +226,235 @@
background: var(--bg-hover) !important;
color: var(--text-primary) !important;
}
/* === AUCTION BROWSER STYLES === */
.browser-container {
max-width: 1400px;
margin: 0 auto;
}
.stats-mini {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
background: var(--bg-secondary);
border-radius: var(--radius-md);
font-size: 0.9rem;
}
/* Auction Grid */
.auction-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
@media (min-width: 768px) {
.auction-grid {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
}
@media (min-width: 1200px) {
.auction-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
}
/* Auction Card */
.auction-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
overflow: hidden;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
}
.auction-card:hover {
border-color: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.auction-card.monitored {
border-color: rgba(34, 197, 94, 0.4);
background: linear-gradient(135deg, var(--bg-card) 0%, rgba(34, 197, 94, 0.05) 100%);
}
.auction-card.sold {
opacity: 0.6;
}
/* Auction Image */
.auction-image {
position: relative;
width: 100%;
aspect-ratio: 4/3;
background: var(--bg-secondary);
overflow: hidden;
}
.auction-image img {
width: 100%;
height: 100%;
object-fit: contain;
background: white;
}
.auction-image .placeholder-image {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: var(--text-muted);
opacity: 0.3;
}
.auction-badges {
position: absolute;
top: 0.5rem;
left: 0.5rem;
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.auction-badges .badge {
font-size: 0.7rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
}
.sold-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
}
.sold-overlay span {
background: var(--danger);
color: white;
padding: 0.5rem 1rem;
border-radius: var(--radius-md);
font-weight: 700;
font-size: 0.9rem;
transform: rotate(-15deg);
}
.monitored-badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
width: 28px;
height: 28px;
background: var(--success);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1rem;
box-shadow: var(--shadow-sm);
}
/* Auction Info */
.auction-info {
padding: 0.75rem;
flex: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.auction-name {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
min-height: 2.2em;
}
.auction-price {
display: flex;
flex-direction: column;
margin-top: 0.25rem;
}
.auction-price .current-price {
font-size: 1.1rem;
font-weight: 700;
color: var(--success);
}
.auction-price .buynow-price {
font-size: 0.75rem;
}
.auction-bidder {
font-size: 0.8rem;
color: var(--text-muted);
display: flex;
align-items: center;
}
.auction-bidder span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
}
.auction-timer {
font-size: 0.85rem;
font-weight: 600;
color: var(--info);
display: flex;
align-items: center;
}
.auction-timer.urgent {
color: var(--danger);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* Auction Actions */
.auction-actions {
padding: 0.5rem 0.75rem 0.75rem;
border-top: 1px solid var(--border-subtle);
}
.auction-actions .btn {
font-size: 0.8rem;
padding: 0.4rem 0.75rem;
border-radius: var(--radius-sm);
}
/* Spin animation for loading */
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}