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>
47 lines
2.2 KiB
C#
47 lines
2.2 KiB
C#
using BlueLaminate.EFCore.Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace BlueLaminate.Core.CsMoney;
|
|
|
|
/// <summary>One marketplace's current presence for a skin or a physical item.</summary>
|
|
/// <param name="Marketplace">"csfloat", "csmoney", …</param>
|
|
/// <param name="ActiveCount">Active listings on this market.</param>
|
|
/// <param name="MinPrice">Cheapest active listing (the comparable price).</param>
|
|
/// <param name="MaxPrice">Dearest active listing.</param>
|
|
/// <param name="LastSeenAt">When this market was last observed to have it.</param>
|
|
public sealed record MarketPresence(
|
|
string Marketplace, int ActiveCount, decimal MinPrice, decimal MaxPrice, DateTimeOffset LastSeenAt);
|
|
|
|
/// <summary>
|
|
/// Answers "where is this listed?" over the cross-market <c>market_listings</c> view.
|
|
/// Per physical item (<see cref="ForInstanceAsync"/>) for the exact-copy / arbitrage /
|
|
/// dupe view, or per catalogue skin (<see cref="ForSkinAsync"/>) for "which markets
|
|
/// carry this skin, and cheapest where".
|
|
/// </summary>
|
|
public sealed class MarketPresenceService
|
|
{
|
|
private const string Active = "Active";
|
|
|
|
private readonly SkinTrackerDbContext _db;
|
|
|
|
public MarketPresenceService(SkinTrackerDbContext db) => _db = db;
|
|
|
|
/// <summary>Markets currently listing this exact physical copy.</summary>
|
|
public Task<List<MarketPresence>> ForInstanceAsync(int skinInstanceId, CancellationToken ct = default) =>
|
|
_db.MarketListings
|
|
.Where(m => m.SkinInstanceId == skinInstanceId && m.Status == Active)
|
|
.GroupBy(m => m.Marketplace)
|
|
.Select(g => new MarketPresence(
|
|
g.Key, g.Count(), g.Min(x => x.Price), g.Max(x => x.Price), g.Max(x => x.LastSeenAt)))
|
|
.ToListAsync(ct);
|
|
|
|
/// <summary>Markets currently listing this skin (any wear), cheapest per market.</summary>
|
|
public Task<List<MarketPresence>> ForSkinAsync(int skinId, CancellationToken ct = default) =>
|
|
_db.MarketListings
|
|
.Where(m => m.SkinId == skinId && m.Status == Active)
|
|
.GroupBy(m => m.Marketplace)
|
|
.Select(g => new MarketPresence(
|
|
g.Key, g.Count(), g.Min(x => x.Price), g.Max(x => x.Price), g.Max(x => x.LastSeenAt)))
|
|
.ToListAsync(ct);
|
|
}
|