Files
Operation-Blue-Laminate-v2/BlueLaminate/BlueLaminate.Scraper/Skins/SkinCatalogClient.cs

106 lines
3.4 KiB
C#

using System.Text.Json;
using System.Text.Json.Serialization;
namespace BlueLaminate.Scraper.Skins;
/// <summary>
/// Loads the CS2 skin catalogue from the ByMykel/CSGO-API static dataset
/// (skins.json) and maps it to <see cref="CatalogSkin"/> records. This replaces
/// the old HTML scraper: one JSON file carries every skin with its weapon,
/// category, rarity, wear range, and the collections/containers it comes from.
/// </summary>
public sealed class SkinCatalogClient
{
public const string DefaultUrl =
"https://raw.githubusercontent.com/ByMykel/CSGO-API/refs/heads/main/public/api/en/skins.json";
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
};
private readonly HttpClient _http;
private readonly string _url;
public SkinCatalogClient(HttpClient http, string? url = null)
{
_http = http;
_url = url ?? DefaultUrl;
}
public async Task<IReadOnlyList<CatalogSkin>> FetchAsync(CancellationToken ct = default)
{
await using var stream = await _http.GetStreamAsync(_url, ct);
var dtos = await JsonSerializer.DeserializeAsync<List<SkinDto>>(stream, Options, ct)
?? throw new InvalidOperationException("skins.json deserialized to null.");
return dtos.Select(Map).ToList();
}
private static CatalogSkin Map(SkinDto dto)
{
var sources = new List<CatalogSource>();
AddSources(sources, dto.Collections, "Collection");
AddSources(sources, dto.Crates, "Container");
return new CatalogSkin(
Id: dto.Id,
WeaponName: dto.Weapon?.Name ?? "Unknown",
Category: dto.Category?.Name ?? "Unknown",
Team: MapTeam(dto.Team?.Id),
// Knives with no finish carry a null pattern; "Vanilla" is the community term.
Name: dto.Pattern?.Name ?? "Vanilla",
Rarity: dto.Rarity?.Name ?? "Unknown",
Description: dto.Description,
ImageUrl: dto.Image,
StatTrakAvailable: dto.Stattrak,
SouvenirAvailable: dto.Souvenir,
FloatMin: dto.MinFloat,
FloatMax: dto.MaxFloat,
Sources: sources);
}
private static void AddSources(List<CatalogSource> into, List<NamedDto>? items, string type)
{
if (items is null)
return;
foreach (var item in items)
{
if (string.IsNullOrEmpty(item.Id) || string.IsNullOrEmpty(item.Name))
continue;
if (into.Any(s => s.Id == item.Id))
continue;
into.Add(new CatalogSource(item.Id, item.Name, type));
}
}
private static string MapTeam(string? teamId) => teamId switch
{
"terrorists" => "T",
"counter-terrorists" => "CT",
_ => "Both",
};
private sealed record SkinDto(
string Id,
string? Name,
string? Description,
NamedDto? Weapon,
NamedDto? Category,
NamedDto? Pattern,
decimal? MinFloat,
decimal? MaxFloat,
NamedDto? Rarity,
bool Stattrak,
bool Souvenir,
string? Image,
NamedDto? Team,
List<NamedDto>? Collections,
List<NamedDto>? Crates);
private sealed record NamedDto(string? Id, string? Name);
}