using BlueLaminate.Cli; using BlueLaminate.Cli.Logging; using BlueLaminate.EFCore.Data; 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. var forceOption = new Option("--force") { Description = "Ignore the once-a-month throttle and sync now." }; var dryRunOption = new Option("--dry-run") { Description = "Load and print the skins without writing to the database." }; 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, }; syncSkins.SetAction((parseResult, ct) => SyncSkinsAsync( parseResult.GetValue(forceOption), parseResult.GetValue(dryRunOption), loggerFactory, ct)); var root = new RootCommand("BlueLaminate CLI — Counter-Strike skin tracker tools.") { syncSkins, }; return await root.Parse(args).InvokeAsync(); // 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 SyncSkinsAsync( bool force, bool dryRun, ILoggerFactory loggerFactory, CancellationToken ct) { var logger = loggerFactory.CreateLogger("BlueLaminate.Cli.SyncSkins"); var client = new SkinCatalogClient(CreateHttpClient()); if (dryRun) { 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 service = new SkinSyncService(db, client, loggerFactory.CreateLogger()); var result = await service.SyncAsync(force, ct); if (result.Skipped) { Console.WriteLine( $"Skipped: skins were last synced {result.LastRanAt:u}. " + "Next run allowed one month later — pass --force to override."); } else { Console.WriteLine( $"Synced {result.Loaded} skins: {result.Inserted} inserted, " + $"{result.Updated} updated, " + $"{result.Loaded - result.Inserted - result.Updated} unchanged " + $"({result.WeaponsCreated} weapons, {result.CollectionsCreated} collections created)."); } return 0; } static HttpClient CreateHttpClient() { var http = new HttpClient(); http.Timeout = TimeSpan.FromMinutes(2); http.DefaultRequestHeaders.UserAgent.ParseAdd("BlueLaminate.Cli"); return http; }