Change to static skin catalog population

This commit is contained in:
bob
2026-05-29 18:36:17 -05:00
parent 6f3c0175cd
commit b51f1d9f5f
26 changed files with 3063 additions and 370 deletions

View File

@@ -1,8 +1,25 @@
using System.CommandLine;
using BlueLaminate.Cli;
using BlueLaminate.Cli.Logging;
using BlueLaminate.EFCore.Data;
using BlueLaminate.Scraper.Weapons;
using BlueLaminate.Scraper.Wiki;
using BlueLaminate.Scraper.Skins;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Resources;
using System.CommandLine;
// OpenTelemetry logging through a compact console sink that prints one
// "{utc timestamp} {message}" line per record. Swapping in an OTLP exporter
// later is a change here. Disposed at process exit so buffered records flush.
using var loggerFactory = LoggerFactory.Create(logging =>
{
logging.AddOpenTelemetry(otel =>
{
otel.SetResourceBuilder(
ResourceBuilder.CreateDefault().AddService("BlueLaminate.Cli"));
otel.IncludeFormattedMessage = true;
otel.AddProcessor(new SimpleLogRecordExportProcessor(new CompactConsoleLogExporter()));
});
});
// Entry point: System.CommandLine builds the command tree, parsing, and help.
// New features are added as additional commands here as they're implemented.
@@ -12,56 +29,73 @@ var forceOption = new Option<bool>("--force")
};
var dryRunOption = new Option<bool>("--dry-run")
{
Description = "Scrape and print the weapons without writing to the database."
Description = "Load and print the skins without writing to the database."
};
var syncWeapons = new Command(
"sync-weapons",
"Scrape the CS2 weapon catalogue from the wiki and upsert it (throttled to once a month).")
var syncSkins = new Command(
"sync-skins",
"Load the CS2 skin catalogue from the CSGO-API dataset and upsert it (throttled to once a month).")
{
forceOption,
dryRunOption,
};
syncWeapons.SetAction((parseResult, ct) =>
SyncWeaponsAsync(parseResult.GetValue(forceOption), parseResult.GetValue(dryRunOption), ct));
syncSkins.SetAction((parseResult, ct) =>
SyncSkinsAsync(
parseResult.GetValue(forceOption),
parseResult.GetValue(dryRunOption),
loggerFactory,
ct));
var root = new RootCommand("BlueLaminate CLI — Counter-Strike skin tracker tools.")
{
syncWeapons,
syncSkins,
};
return await root.Parse(args).InvokeAsync();
// Fetch the CS2 weapon catalogue from the wiki and upsert it. Throttled to once
// a month unless --force is passed; --dry-run scrapes and prints without a DB.
static async Task<int> SyncWeaponsAsync(bool force, bool dryRun, CancellationToken ct)
// Load the CS2 skin catalogue from the CSGO-API dataset and upsert it. Weapons
// and collections are derived from the skins themselves. Throttled to once a
// month unless --force; --dry-run loads and prints without a DB.
static async Task<int> SyncSkinsAsync(
bool force, bool dryRun, ILoggerFactory loggerFactory, CancellationToken ct)
{
var scraper = new WeaponWikiScraper(new WikiPageFetcher(CreateHttpClient()));
var logger = loggerFactory.CreateLogger("BlueLaminate.Cli.SyncSkins");
var client = new SkinCatalogClient(CreateHttpClient());
if (dryRun)
{
var weapons = await scraper.ScrapeAsync(ct);
Console.WriteLine($"Scraped {weapons.Count} weapons (dry run, nothing written):");
foreach (var w in weapons)
Console.WriteLine($" {w.Name,-20} {w.Type,-16} {w.Team}");
logger.LogInformation("Loading skin catalogue (dry run — nothing will be written).");
var skins = await client.FetchAsync(ct);
logger.LogInformation("Loaded {Count} skins.", skins.Count);
Console.WriteLine($"Loaded {skins.Count} skins (dry run, nothing written):");
foreach (var s in skins)
{
var tags = (s.StatTrakAvailable ? " ST" : "") + (s.SouvenirAvailable ? " SV" : "");
var range = s.FloatMin is not null ? $"{s.FloatMin:0.00}-{s.FloatMax:0.00}" : "—";
var sources = s.Sources.Count > 0 ? string.Join(", ", s.Sources.Select(x => x.Name)) : "—";
Console.WriteLine(
$" {s.WeaponName,-16} {s.Name,-24} {s.Rarity,-14} {range,-10} {sources}{tags}");
}
return 0;
}
using var db = new SkinTrackerDbContextFactory().CreateDbContext([]);
var result = await new WeaponSyncService(db, scraper).SyncAsync(force, ct);
var service = new SkinSyncService(db, client, loggerFactory.CreateLogger<SkinSyncService>());
var result = await service.SyncAsync(force, ct);
if (result.Skipped)
{
Console.WriteLine(
$"Skipped: weapons were last synced {result.LastRanAt:u}. "
$"Skipped: skins were last synced {result.LastRanAt:u}. "
+ "Next run allowed one month later — pass --force to override.");
}
else
{
Console.WriteLine(
$"Synced {result.Scraped} weapons: {result.Inserted} inserted, "
$"Synced {result.Loaded} skins: {result.Inserted} inserted, "
+ $"{result.Updated} updated, "
+ $"{result.Scraped - result.Inserted - result.Updated} unchanged.");
+ $"{result.Loaded - result.Inserted - result.Updated} unchanged "
+ $"({result.WeaponsCreated} weapons, {result.CollectionsCreated} collections created).");
}
return 0;
@@ -70,10 +104,7 @@ static async Task<int> SyncWeaponsAsync(bool force, bool dryRun, CancellationTok
static HttpClient CreateHttpClient()
{
var http = new HttpClient();
// The wiki is fronted by Cloudflare; a browser-like User-Agent is accepted
// on the MediaWiki API endpoint the scraper uses.
http.DefaultRequestHeaders.UserAgent.ParseAdd(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36");
http.Timeout = TimeSpan.FromMinutes(2);
http.DefaultRequestHeaders.UserAgent.ParseAdd("BlueLaminate.Cli");
return http;
}