Brings up the pull-model scraper: the .NET C2 hands skin+wear jobs to Python nodriver workers that scrape cs.money and post results back, plus the supporting Core/EFCore data model, migrations, and docker-compose orchestration. IPRoyal proxying lets workers scale horizontally with a distinct residential exit IP each: every worker process mints its own sticky session at startup, and an in-process forwarding proxy injects the gateway auth so Chromium talks only to an auth-free localhost endpoint (zero CDP). On a Cloudflare challenge a worker rotates to a fresh session/IP and re-warms. Verified end-to-end against live IPRoyal: distinct US residential exits per worker and IP rotation on demand. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
75 lines
2.8 KiB
C#
75 lines
2.8 KiB
C#
using BlueLaminate.Core.Listings;
|
|
using BlueLaminate.Scraper.CsFloat;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using System.CommandLine;
|
|
|
|
namespace BlueLaminate.Cli.Commands;
|
|
|
|
/// <summary>
|
|
/// <c>sweep-catalog</c>: catalogue-driven sweep querying each catalogue skin's
|
|
/// listings by def_index+paint_index. Presentation over
|
|
/// <see cref="ListingSweepService.SweepCatalogAsync"/>.
|
|
/// </summary>
|
|
internal static class SweepCatalogCommand
|
|
{
|
|
public static Command Build(IHost host)
|
|
{
|
|
var command = new Command(
|
|
"sweep-catalog",
|
|
"Catalogue-driven sweep: query each catalogue skin's listings by def_index+paint_index, "
|
|
+ "split by wear band (min_float/max_float), so only weapons are fetched (no "
|
|
+ "stickers/cases/agents) and each wear band is an independent checkpoint. Each band is "
|
|
+ "paged to completion, so Removed-tracking is accurate. Runs continuously (looping the "
|
|
+ "catalogue, never-swept/stalest bands first) until Ctrl+C; paces off rate-limit headers. "
|
|
+ "Reads CSFLOAT_API_KEY.");
|
|
|
|
command.SetAction((parseResult, ct) => RunAsync(host, ct));
|
|
|
|
return command;
|
|
}
|
|
|
|
private static async Task<int> RunAsync(IHost host, CancellationToken ct)
|
|
{
|
|
using var scope = host.Services.CreateScope();
|
|
CsFloatListingsClient? client = null;
|
|
|
|
try
|
|
{
|
|
var service = scope.ServiceProvider.GetRequiredService<ListingSweepService>();
|
|
client = scope.ServiceProvider.GetRequiredService<CsFloatListingsClient>();
|
|
|
|
Console.WriteLine("Catalogue sweep (weapons only). Running until Ctrl+C…");
|
|
|
|
var r = await service.SweepCatalogAsync(ct: ct);
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine($"Catalogue sweep {r.StoppedReason}:");
|
|
Console.WriteLine($" Wear-bands : {r.SkinsCovered}");
|
|
Console.WriteLine($" Pages fetched : {r.Pages}");
|
|
Console.WriteLine($" Listings seen : {r.Seen}");
|
|
Console.WriteLine($" Inserted : {r.Inserted}");
|
|
Console.WriteLine($" Updated : {r.Updated}");
|
|
Console.WriteLine($" Removed : {r.Removed}");
|
|
Console.WriteLine();
|
|
Console.WriteLine(client.LastRateLimit.ToString());
|
|
return 0;
|
|
}
|
|
catch (CsFloatApiException ex)
|
|
{
|
|
Console.Error.WriteLine(ex.Message);
|
|
if (client is not null)
|
|
{
|
|
Console.Error.WriteLine(client.LastRateLimit.ToString());
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.Error.WriteLine($"Catalogue sweep failed: {ex.Message}");
|
|
return 1;
|
|
}
|
|
}
|
|
}
|