Aggiornamento live aste, azioni rapide e scroll infinito

- Aggiornamento automatico degli stati delle aste ogni 500ms, rimosso il bottone manuale "Aggiorna Prezzi"
- Aggiunti pulsanti per copiare il link e aprire l'asta in nuova scheda
- Possibilità di rimuovere aste dal monitor direttamente dalla lista
- Caricamento aste ottimizzato: scroll infinito senza duplicati tramite nuova API get_auction_updates.php
- Migliorato il parsing dei dati e la precisione del countdown usando il timestamp del server
- Refactoring vari per migliorare la reattività e l'esperienza utente
This commit is contained in:
2026-01-22 11:43:59 +01:00
parent 865bfa2752
commit 2833cd0487
3 changed files with 28732 additions and 62 deletions

File diff suppressed because one or more lines are too long

View File

@@ -4,6 +4,7 @@
@using AutoBidder.Services
@inject BidooBrowserService BrowserService
@inject ApplicationStateService AppState
@inject IJSRuntime JSRuntime
@implements IDisposable
<PageTitle>Esplora Aste - AutoBidder</PageTitle>
@@ -24,13 +25,6 @@
<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>
@@ -195,10 +189,22 @@
<!-- Actions -->
<div class="auction-actions">
<div class="d-flex gap-1 mb-1">
<button class="btn btn-outline-secondary btn-sm flex-grow-1"
@onclick="() => CopyAuctionLink(auction)"
title="Copia link">
<i class="bi bi-clipboard"></i>
</button>
<button class="btn btn-outline-secondary btn-sm flex-grow-1"
@onclick="() => OpenAuctionInNewTab(auction)"
title="Apri in nuova scheda">
<i class="bi bi-box-arrow-up-right"></i>
</button>
</div>
@if (auction.IsMonitored)
{
<button class="btn btn-success btn-sm w-100" disabled>
<i class="bi bi-check-lg me-1"></i>Monitorata
<button class="btn btn-warning btn-sm w-100" @onclick="() => RemoveFromMonitor(auction)">
<i class="bi bi-dash-lg me-1"></i>Togli dal Monitor
</button>
}
else
@@ -240,12 +246,12 @@
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;
private bool isUpdatingInBackground = false;
protected override async Task OnInitializedAsync()
{
@@ -256,14 +262,14 @@
await LoadAuctions();
}
// Auto-update states every 5 seconds
// Auto-update states every 500ms for real-time price updates
stateUpdateTimer = new System.Threading.Timer(async _ =>
{
if (auctions.Count > 0 && !isUpdatingStates)
if (auctions.Count > 0 && !isUpdatingInBackground)
{
await UpdateAuctionStatesBackground();
}
}, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
}, null, TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(500));
}
private async Task LoadCategories()
@@ -332,18 +338,20 @@
private async Task LoadMoreAuctions()
{
if (categories.Count == 0 || selectedCategoryIndex < 0)
if (categories.Count == 0 || selectedCategoryIndex < 0 || auctions.Count == 0)
return;
isLoadingMore = true;
currentPage++;
cts?.Cancel();
cts = new CancellationTokenSource();
try
{
var category = categories[selectedCategoryIndex];
var newAuctions = await BrowserService.GetAuctionsAsync(category, currentPage, cts.Token);
var existingIds = auctions.Select(a => a.AuctionId).ToList();
// Usa GetMoreAuctionsAsync che evita duplicati
var newAuctions = await BrowserService.GetMoreAuctionsAsync(category, existingIds, cts.Token);
if (newAuctions.Count == 0)
{
@@ -353,13 +361,14 @@
{
auctions.AddRange(newAuctions);
UpdateMonitoredStatus();
// Aggiorna stati delle nuove aste
await BrowserService.UpdateAuctionStatesAsync(newAuctions, cts.Token);
}
}
catch (Exception ex)
{
Console.WriteLine($"[Browser] Error loading more auctions: {ex.Message}");
currentPage--; // Rollback
}
finally
{
@@ -368,25 +377,11 @@
}
}
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()
{
if (isUpdatingInBackground) return;
isUpdatingInBackground = true;
try
{
await BrowserService.UpdateAuctionStatesAsync(auctions);
@@ -397,6 +392,10 @@
{
// Ignore background errors
}
finally
{
isUpdatingInBackground = false;
}
}
private async Task RefreshAll()
@@ -441,6 +440,48 @@
StateHasChanged();
}
private void RemoveFromMonitor(BidooBrowserAuction browserAuction)
{
if (!browserAuction.IsMonitored) return;
// Trova l'asta nel monitor
var auctionToRemove = AppState.Auctions.FirstOrDefault(a => a.AuctionId == browserAuction.AuctionId);
if (auctionToRemove != null)
{
AppState.RemoveAuction(auctionToRemove);
browserAuction.IsMonitored = false;
// Save to disk
AutoBidder.Utilities.PersistenceManager.SaveAuctions(AppState.Auctions.ToList());
}
StateHasChanged();
}
private async Task CopyAuctionLink(BidooBrowserAuction auction)
{
try
{
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", auction.Url);
}
catch (Exception ex)
{
Console.WriteLine($"[Browser] Error copying link: {ex.Message}");
}
}
private async Task OpenAuctionInNewTab(BidooBrowserAuction auction)
{
try
{
await JSRuntime.InvokeVoidAsync("window.open", auction.Url, "_blank");
}
catch (Exception ex)
{
Console.WriteLine($"[Browser] Error opening link: {ex.Message}");
}
}
public void Dispose()
{
stateUpdateTimer?.Dispose();

View File

@@ -440,8 +440,9 @@ namespace AutoBidder.Services
/// <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)
/// Formato: serverTimestamp*(id;status;expiry;price;;#id2;status2;...)
/// Esempio: 1769073106*(85559629;ON;1769082240;1;;#85559630;ON;1769082240;1;;)
/// Il timestamp del server viene usato come riferimento per calcolare il tempo rimanente
/// </summary>
private void ParseListIdResponse(string response, List<BidooBrowserAuction> auctions)
{
@@ -455,6 +456,11 @@ namespace AutoBidder.Services
return;
}
// Estrai il timestamp del server (prima di *)
var serverTimestampStr = response.Substring(0, starIndex);
long serverTimestamp = 0;
long.TryParse(serverTimestampStr, out serverTimestamp);
var mainData = response.Substring(starIndex + 1);
// Rimuovi parentesi se presenti
@@ -465,20 +471,19 @@ namespace AutoBidder.Services
// Split per ogni asta (separatore #)
var auctionEntries = mainData.Split('#', StringSplitOptions.RemoveEmptyEntries);
int updatedCount = 0;
foreach (var entry in auctionEntries)
{
// Formato: id;status;expiry;price;bidder;timer;countdown
// Formato: id;status;expiry;price;; (bidder e timer possono essere vuoti)
var fields = entry.Split(';');
if (fields.Length < 5) continue;
if (fields.Length < 4) 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 expiryStr = fields[2].Trim(); // timestamp scadenza (stesso formato del server)
var priceStr = fields[3].Trim(); // prezzo (centesimi)
var bidder = fields.Length > 4 ? fields[4].Trim() : ""; // ultimo bidder (può essere vuoto)
var auction = auctions.FirstOrDefault(a => a.AuctionId == id);
if (auction == null) continue;
@@ -489,32 +494,36 @@ namespace AutoBidder.Services
auction.CurrentPrice = priceCents / 100m;
}
// Aggiorna bidder
auction.LastBidder = bidder;
// Aggiorna timer frequency
if (int.TryParse(timer, out int timerFreq) && timerFreq > 0)
// Aggiorna bidder solo se non vuoto
if (!string.IsNullOrEmpty(bidder))
{
auction.TimerFrequency = timerFreq;
auction.LastBidder = bidder;
}
// Parse countdown per calcolare secondi rimanenti
auction.RemainingSeconds = ParseCountdown(countdown, auction.TimerFrequency);
// Calcola tempo rimanente usando il timestamp del server come riferimento
if (long.TryParse(expiryStr, out long expiryTimestamp) && serverTimestamp > 0)
{
// Il tempo rimanente è: expiry - serverTime (entrambi nello stesso formato)
var remainingSeconds = expiryTimestamp - serverTimestamp;
auction.RemainingSeconds = remainingSeconds > 0 ? (int)remainingSeconds : 0;
}
else if (status == "ON")
{
// Se non riusciamo a calcolare, usa il timer frequency come fallback
if (auction.RemainingSeconds <= 0)
{
auction.RemainingSeconds = auction.TimerFrequency;
}
}
// Status: ON = attiva, OFF = in countdown
auction.IsActive = true;
auction.IsSold = false;
// Status: ON = attiva in countdown, OFF = terminata/in pausa
auction.IsActive = status == "ON";
auction.IsSold = status != "ON" && auction.RemainingSeconds <= 0;
// 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;
}
updatedCount++;
}
Console.WriteLine($"[BidooBrowser] Aggiornate {auctionEntries.Length} aste");
Console.WriteLine($"[BidooBrowser] Aggiornate {updatedCount} aste su {auctionEntries.Length}");
}
catch (Exception ex)
{
@@ -578,5 +587,137 @@ namespace AutoBidder.Services
return defaultSeconds;
}
/// <summary>
/// Carica nuove aste usando get_auction_updates.php (simula scrolling infinito)
/// Questa API restituisce aste che non sono ancora state caricate
/// </summary>
public async Task<List<BidooBrowserAuction>> GetMoreAuctionsAsync(
BidooCategoryInfo category,
List<string> existingAuctionIds,
CancellationToken cancellationToken = default)
{
var newAuctions = new List<BidooBrowserAuction>();
try
{
var existingIdsSet = existingAuctionIds.ToHashSet();
// Prepara la chiamata POST a get_auction_updates.php
var url = "https://it.bidoo.com/get_auction_updates.php";
// Costruisci il body della richiesta
var viewIds = string.Join(",", existingAuctionIds);
var tabValue = category.IsSpecialCategory ? category.TabId : 4;
var tagValue = category.IsSpecialCategory ? 0 : category.TagId;
var formContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("prefetch", "true"),
new KeyValuePair<string, string>("view", viewIds),
new KeyValuePair<string, string>("tab", tabValue.ToString()),
new KeyValuePair<string, string>("tag", tagValue.ToString())
});
var request = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = formContent
};
AddBrowserHeaders(request, "https://it.bidoo.com/");
request.Headers.Add("X-Requested-With", "XMLHttpRequest");
Console.WriteLine($"[BidooBrowser] Fetching more auctions with {existingAuctionIds.Count} existing IDs...");
var response = await _httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
// Parse la risposta JSON
// Formato: {"gc":[],"int":[],"list":[id1,id2,...],"items":["<html>","<html>",...]}
newAuctions = ParseGetAuctionUpdatesResponse(responseText, existingIdsSet);
Console.WriteLine($"[BidooBrowser] Trovate {newAuctions.Count} nuove aste");
}
catch (Exception ex)
{
Console.WriteLine($"[BidooBrowser] Errore caricamento nuove aste: {ex.Message}");
}
return newAuctions;
}
/// <summary>
/// Parsa la risposta di get_auction_updates.php
/// </summary>
private List<BidooBrowserAuction> ParseGetAuctionUpdatesResponse(string json, HashSet<string> existingIds)
{
var auctions = new List<BidooBrowserAuction>();
try
{
// Parse JSON manuale per estrarre items[]
// Cerchiamo "items":["...","..."]
var itemsMatch = Regex.Match(json, @"""items"":\s*\[(.*?)\](?=,""|\})", RegexOptions.Singleline);
if (!itemsMatch.Success)
{
Console.WriteLine("[BidooBrowser] Nessun items trovato nella risposta");
return auctions;
}
var itemsContent = itemsMatch.Groups[1].Value;
// Gli items sono stringhe HTML escaped, dobbiamo parsarle
// Ogni item è una stringa JSON che contiene HTML
var htmlPattern = new Regex(@"""((?:[^""\\]|\\.)*?)""", RegexOptions.Singleline);
var htmlMatches = htmlPattern.Matches(itemsContent);
foreach (Match htmlMatch in htmlMatches)
{
if (!htmlMatch.Success) continue;
// Unescape la stringa JSON
var escapedHtml = htmlMatch.Groups[1].Value;
var html = UnescapeJsonString(escapedHtml);
// Estrai l'ID dell'asta
var idMatch = Regex.Match(html, @"id=""divAsta(\d+)""");
if (!idMatch.Success) continue;
var auctionId = idMatch.Groups[1].Value;
// Salta se già esiste
if (existingIds.Contains(auctionId)) continue;
// Parsa l'asta dall'HTML
var auction = ParseSingleAuction(auctionId, html);
if (auction != null)
{
auctions.Add(auction);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[BidooBrowser] Errore parsing get_auction_updates response: {ex.Message}");
}
return auctions;
}
/// <summary>
/// Unescape di una stringa JSON
/// </summary>
private static string UnescapeJsonString(string escaped)
{
return escaped
.Replace("\\/", "/")
.Replace("\\n", "\n")
.Replace("\\r", "\r")
.Replace("\\t", "\t")
.Replace("\\\"", "\"")
.Replace("\\\\", "\\");
}
}
}