Files
CVRManager/Services/GitHubService.cs
T
Exterrata 9bb4f85802 CVR Manager 1.0.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 02:46:35 -04:00

103 lines
4.3 KiB
C#

using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.RegularExpressions;
using CVRManager.Models;
namespace CVRManager.Services;
public class GitHubService
{
readonly HttpClient http = new();
readonly Func<string> token;
public GitHubService(Func<string> token)
{
this.token = token;
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("CVRManager", "1.0"));
http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
}
public static string? ParseRepo(string input)
{
input = input.Trim();
var m = Regex.Match(input, @"^(?:https?://github\.com/)?([\w.-]+)/([\w.-]+?)(?:\.git)?(?:/.*)?$");
return m.Success ? $"{m.Groups[1].Value}/{m.Groups[2].Value}" : null;
}
HttpRequestMessage Request(string url)
{
var req = new HttpRequestMessage(HttpMethod.Get, url);
var t = token();
if (!string.IsNullOrWhiteSpace(t)) req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", t);
return req;
}
public async Task<List<ReleaseInfo>> GetReleasesAsync(string repo, CancellationToken ct)
{
using var resp = await http.SendAsync(Request($"https://api.github.com/repos/{repo}/releases?per_page=40"), ct);
if (!resp.IsSuccessStatusCode)
{
var hint = (int)resp.StatusCode is 403 or 429 ? " (rate limited: add a GitHub token in Settings)" : "";
throw new InvalidOperationException($"GitHub returned {(int)resp.StatusCode} for {repo}{hint}.");
}
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync(ct));
var list = new List<ReleaseInfo>();
foreach (var r in doc.RootElement.EnumerateArray())
{
if (r.GetProperty("draft").GetBoolean()) continue;
var rel = new ReleaseInfo
{
Tag = r.GetProperty("tag_name").GetString() ?? "",
Prerelease = r.GetProperty("prerelease").GetBoolean(),
Published = r.TryGetProperty("published_at", out var p) && p.ValueKind == JsonValueKind.String ? p.GetDateTime() : default,
};
foreach (var a in r.GetProperty("assets").EnumerateArray())
rel.Assets.Add(new AssetInfo
{
Name = a.GetProperty("name").GetString() ?? "",
Url = a.GetProperty("browser_download_url").GetString() ?? "",
Size = a.GetProperty("size").GetInt64(),
});
list.Add(rel);
}
return list;
}
public async Task<ReleaseInfo?> GetMelonLoaderNightlyAsync(CancellationToken ct)
{
using var resp = await http.SendAsync(Request(
"https://api.github.com/repos/LavaGang/MelonLoader/actions/workflows/build.yml/runs?branch=master&status=success&per_page=1"), ct);
if (!resp.IsSuccessStatusCode) return null;
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync(ct));
var runs = doc.RootElement.GetProperty("workflow_runs");
if (runs.GetArrayLength() == 0) return null;
var run = runs[0];
var sha = run.GetProperty("head_sha").GetString() ?? "";
var id = run.GetProperty("id").GetInt64();
var rel = new ReleaseInfo
{
Tag = $"nightly-{sha[..Math.Min(7, sha.Length)]}",
Prerelease = true,
Published = run.GetProperty("created_at").GetDateTime(),
};
rel.Assets.Add(new AssetInfo
{
Name = "MelonLoader.x64.zip",
Url = $"https://nightly.link/LavaGang/MelonLoader/actions/runs/{id}/MelonLoader.Windows.x64.CI.Release.zip",
});
return rel;
}
public async Task<string> DownloadAsync(string url, string path, CancellationToken ct)
{
using var resp = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct);
resp.EnsureSuccessStatusCode();
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await using var src = await resp.Content.ReadAsStreamAsync(ct);
await using var dst = File.Create(path);
await src.CopyToAsync(dst, ct);
var last = resp.RequestMessage?.RequestUri?.Segments.LastOrDefault() ?? "";
return Uri.UnescapeDataString(last.TrimEnd('/'));
}
}