CVR Manager 1.0.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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('/'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user