using System.Net.Http.Headers; using System.Text.Json; using System.Text.RegularExpressions; using CVRManager.Models; namespace CVRManager.Services; public record CvrmgMod( int Id, string Name, string[] Aliases, string Category, string ModVersion, string GameVersion, string LoaderVersion, string Type, string Author, string Description, string DownloadLink, string SourceLink, string Hash, int ApprovalStatus, DateTime Updated) { public string StatusText => ApprovalStatus switch { 1 => "approved", 2 => "outdated", 3 => "obsolete", _ => "unknown" }; public string ModType => Type.Equals("Plugin", StringComparison.OrdinalIgnoreCase) ? "Plugins" : "Mods"; } public class CvrmgService { const string ListUrl = "https://api.cvrmg.com/v1/mods"; readonly HttpClient http = new(); List? cache; DateTime fetched; public CvrmgService() { http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("CVRManager", "1.0")); } public static string Normalize(string s) => Regex.Replace(s ?? "", "[^a-z0-9]", "", RegexOptions.IgnoreCase).ToLowerInvariant(); public async Task> GetAllAsync(CancellationToken ct, bool force = false) { if (!force && cache != null && DateTime.UtcNow - fetched < TimeSpan.FromMinutes(10)) return cache; var path = Path.Combine(Paths.Root, "sources", "cvrmg.json"); string? json = null; if (!force && File.Exists(path) && DateTime.UtcNow - File.GetLastWriteTimeUtc(path) < TimeSpan.FromHours(1)) json = await File.ReadAllTextAsync(path, ct); if (json == null) { try { using var resp = await http.GetAsync(ListUrl, ct); if (!resp.IsSuccessStatusCode) throw new InvalidOperationException($"CVRMG returned {(int)resp.StatusCode}."); json = await resp.Content.ReadAsStringAsync(ct); Directory.CreateDirectory(Path.GetDirectoryName(path)!); await File.WriteAllTextAsync(path, json, ct); } catch (Exception e) when (e is not OperationCanceledException && File.Exists(path)) { json = await File.ReadAllTextAsync(path, ct); } } using var doc = JsonDocument.Parse(json); var list = new List(); foreach (var m in doc.RootElement.EnumerateArray()) { if (!m.TryGetProperty("versions", out var versions) || versions.ValueKind != JsonValueKind.Array) continue; JsonElement? best = null; var bestNo = int.MinValue; foreach (var v in versions.EnumerateArray()) { var no = v.TryGetProperty("_version", out var n) && n.ValueKind == JsonValueKind.Number ? n.GetInt32() : 0; if (no >= bestNo) { best = v; bestNo = no; } } if (best is not { } ver) continue; string S(JsonElement e, string k) => e.TryGetProperty(k, out var p) && p.ValueKind == JsonValueKind.String ? p.GetString() ?? "" : ""; var aliases = m.TryGetProperty("aliases", out var al) && al.ValueKind == JsonValueKind.Array ? al.EnumerateArray().Where(a => a.ValueKind == JsonValueKind.String).Select(a => a.GetString()!).ToArray() : Array.Empty(); DateTime.TryParse(S(ver, "updateDate"), null, System.Globalization.DateTimeStyles.AdjustToUniversal, out var updated); list.Add(new CvrmgMod( m.GetProperty("_id").GetInt32(), S(ver, "name"), aliases, S(m, "category"), S(ver, "modVersion"), S(ver, "gameVersion"), S(ver, "loaderVersion").TrimStart('v'), S(ver, "modType"), S(ver, "author"), S(ver, "description"), S(ver, "downloadLink"), S(ver, "sourceLink"), S(ver, "hash"), ver.TryGetProperty("approvalStatus", out var st) && st.ValueKind == JsonValueKind.Number ? st.GetInt32() : 0, updated)); } cache = list; fetched = DateTime.UtcNow; return list; } public static ReleaseInfo ToRelease(CvrmgMod m, string fileName) => new() { Tag = m.ModVersion, Published = m.Updated, GameVersion = m.GameVersion, LoaderVersion = m.LoaderVersion, Assets = { new AssetInfo { Name = fileName, Url = m.DownloadLink, Hash = m.Hash } }, }; public static void ApplyMeta(ModDef def, CvrmgMod m) { def.CvrmgId = m.Id; if (def.Author.Length == 0) def.Author = m.Author; def.Description = m.Description; def.LatestVersion = m.ModVersion; def.Category = m.Category; def.Status = m.StatusText; if (def.SourceLink.Length == 0) def.SourceLink = m.SourceLink; } }