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:
28488
Mimante/Examples/it.bidoo.com - Scorrimento.har
Normal file
28488
Mimante/Examples/it.bidoo.com - Scorrimento.har
Normal file
File diff suppressed because one or more lines are too long
@@ -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();
|
||||
|
||||
@@ -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("\\\\", "\\");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user