using BlueLaminate.EFCore.Data; using BlueLaminate.EFCore.Entities; using BlueLaminate.Scraper.Weapons; using Microsoft.EntityFrameworkCore; namespace BlueLaminate.Cli; /// True when the monthly throttle suppressed the run. /// When the previous successful run happened, if any. public sealed record WeaponSyncResult( bool Skipped, DateTimeOffset? LastRanAt, int Scraped, int Inserted, int Updated); /// /// Fetches the CS2 weapon catalogue and upserts it into the database. The /// catalogue changes rarely, so a run is throttled to at most once a month /// unless explicitly forced. /// public sealed class WeaponSyncService { public const string Source = "weapons"; private readonly SkinTrackerDbContext _db; private readonly WeaponWikiScraper _scraper; public WeaponSyncService(SkinTrackerDbContext db, WeaponWikiScraper scraper) { _db = db; _scraper = scraper; } public async Task SyncAsync(bool force = false, CancellationToken ct = default) { var now = DateTimeOffset.UtcNow; var lastRanAt = await _db.ScrapeRuns .Where(r => r.Source == Source) .OrderByDescending(r => r.RanAt) .Select(r => (DateTimeOffset?)r.RanAt) .FirstOrDefaultAsync(ct); if (!force && lastRanAt is { } last && last.AddMonths(1) > now) return new WeaponSyncResult(Skipped: true, last, Scraped: 0, Inserted: 0, Updated: 0); var scraped = await _scraper.ScrapeAsync(ct); var existing = await _db.Weapons.ToDictionaryAsync(w => w.Name, ct); var inserted = 0; var updated = 0; foreach (var s in scraped) { if (existing.TryGetValue(s.Name, out var weapon)) { if (weapon.Type != s.Type || weapon.Team != s.Team) { weapon.Type = s.Type; weapon.Team = s.Team; updated++; } } else { _db.Weapons.Add(new Weapon { Name = s.Name, Type = s.Type, Team = s.Team }); inserted++; } } _db.ScrapeRuns.Add(new ScrapeRun { Source = Source, RanAt = now, ItemCount = scraped.Count }); await _db.SaveChangesAsync(ct); return new WeaponSyncResult(Skipped: false, lastRanAt, scraped.Count, inserted, updated); } }