9bb4f85802
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
103 lines
4.3 KiB
C#
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('/'));
|
|
}
|
|
}
|