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:
367632
Mimante/Examples/it.bidoo.com - Schede.har
Normal file
367632
Mimante/Examples/it.bidoo.com - Schede.har
Normal file
File diff suppressed because one or more lines are too long
106
Mimante/Models/BidooBrowserAuction.cs
Normal file
106
Mimante/Models/BidooBrowserAuction.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Mimante/Models/BidooCategoryInfo.cs
Normal file
40
Mimante/Models/BidooCategoryInfo.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
450
Mimante/Pages/AuctionBrowser.razor
Normal file
450
Mimante/Pages/AuctionBrowser.razor
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -244,6 +244,7 @@ builder.Services.AddSingleton(htmlCacheService);
|
|||||||
builder.Services.AddSingleton(sp => new SessionService(auctionMonitor.GetApiClient()));
|
builder.Services.AddSingleton(sp => new SessionService(auctionMonitor.GetApiClient()));
|
||||||
builder.Services.AddSingleton<DatabaseService>();
|
builder.Services.AddSingleton<DatabaseService>();
|
||||||
builder.Services.AddSingleton<ApplicationStateService>();
|
builder.Services.AddSingleton<ApplicationStateService>();
|
||||||
|
builder.Services.AddSingleton<BidooBrowserService>();
|
||||||
builder.Services.AddScoped<StatsService>(sp =>
|
builder.Services.AddScoped<StatsService>(sp =>
|
||||||
{
|
{
|
||||||
var db = sp.GetRequiredService<DatabaseService>();
|
var db = sp.GetRequiredService<DatabaseService>();
|
||||||
|
|||||||
582
Mimante/Services/BidooBrowserService.cs
Normal file
582
Mimante/Services/BidooBrowserService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,11 @@
|
|||||||
<span>Monitor Aste</span>
|
<span>Monitor Aste</span>
|
||||||
</NavLink>
|
</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">
|
<NavLink class="nav-menu-item" href="freebids">
|
||||||
<i class="bi bi-gift"></i>
|
<i class="bi bi-gift"></i>
|
||||||
<span>Puntate Gratuite</span>
|
<span>Puntate Gratuite</span>
|
||||||
|
|||||||
@@ -226,3 +226,235 @@
|
|||||||
background: var(--bg-hover) !important;
|
background: var(--bg-hover) !important;
|
||||||
color: var(--text-primary) !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); }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user