Files
app/windows/StreamPlayer.Desktop/Services/UpdateService.cs
2025-12-17 19:20:55 +00:00

237 lines
8.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using StreamPlayer.Desktop.Models;
namespace StreamPlayer.Desktop.Services;
public sealed class UpdateService
{
private static readonly HttpClient HttpClient = new(new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.All
})
{
Timeout = TimeSpan.FromSeconds(20)
};
public async Task<UpdateInfo?> CheckForUpdatesAsync(CancellationToken cancellationToken)
{
using var request = new HttpRequestMessage(HttpMethod.Get, AppVersion.LatestReleaseApi);
using var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
var root = document.RootElement;
string tagName = root.GetPropertyOrDefault("tag_name");
string versionName = DeriveVersionName(tagName, root.GetPropertyOrDefault("name"));
int versionCode = ParseVersionCode(versionName);
string releaseNotes = root.GetPropertyOrDefault("body");
string releasePageUrl = root.GetPropertyOrDefault("html_url");
string downloadUrl = string.Empty;
string downloadFileName = string.Empty;
long sizeBytes = 0;
int downloadCount = 0;
JsonElement assetsElement = default;
bool hasAssets = root.TryGetProperty("assets", out assetsElement) && assetsElement.ValueKind == JsonValueKind.Array;
if (hasAssets && (TryFindAsset(assetsElement, IsApkAsset, out var apkAsset) ||
TryGetFirstAsset(assetsElement, out apkAsset)))
{
downloadUrl = apkAsset.GetPropertyOrDefault("browser_download_url");
downloadFileName = apkAsset.GetPropertyOrDefault("name");
long.TryParse(apkAsset.GetPropertyOrDefault("size"), out sizeBytes);
int.TryParse(apkAsset.GetPropertyOrDefault("download_count"), out downloadCount);
}
var manifest = hasAssets
? await TryFetchManifestAsync(assetsElement, cancellationToken).ConfigureAwait(false)
: null;
if (manifest is not null)
{
versionCode = manifest.Value.GetPropertyOrDefaultInt("versionCode", versionCode);
var manifestVersionName = manifest.Value.GetPropertyOrDefault("versionName");
if (!string.IsNullOrWhiteSpace(manifestVersionName))
{
versionName = manifestVersionName;
}
int minSupported = manifest.Value.GetPropertyOrDefaultInt("minSupportedVersionCode", 0);
bool forceUpdate = manifest.Value.GetPropertyOrDefaultBool("forceUpdate", false);
string manifestUrl = manifest.Value.GetPropertyOrDefault("downloadUrl");
if (!string.IsNullOrWhiteSpace(manifestUrl))
{
downloadUrl = manifestUrl;
}
string manifestFileName = manifest.Value.GetPropertyOrDefault("fileName");
if (!string.IsNullOrWhiteSpace(manifestFileName))
{
downloadFileName = manifestFileName;
}
long manifestSize = manifest.Value.GetPropertyOrDefaultLong("sizeBytes", sizeBytes);
if (manifestSize > 0)
{
sizeBytes = manifestSize;
}
string manifestNotes = manifest.Value.GetPropertyOrDefault("notes");
if (!string.IsNullOrWhiteSpace(manifestNotes) && string.IsNullOrWhiteSpace(releaseNotes))
{
releaseNotes = manifestNotes;
}
return BuildInfo(versionCode, versionName, releaseNotes, downloadUrl, downloadFileName,
sizeBytes, downloadCount, releasePageUrl, minSupported, forceUpdate);
}
return BuildInfo(versionCode, versionName, releaseNotes, downloadUrl, downloadFileName,
sizeBytes, downloadCount, releasePageUrl, 0, false);
}
private static UpdateInfo? BuildInfo(
int versionCode,
string versionName,
string releaseNotes,
string downloadUrl,
string downloadFileName,
long sizeBytes,
int downloadCount,
string releasePageUrl,
int minSupported,
bool forceUpdate)
{
if (string.IsNullOrWhiteSpace(downloadUrl))
{
return null;
}
return new UpdateInfo(
versionCode,
versionName,
releaseNotes,
downloadUrl,
downloadFileName,
sizeBytes,
downloadCount,
releasePageUrl,
minSupported,
forceUpdate);
}
private static async Task<JsonElement?> TryFetchManifestAsync(JsonElement assets, CancellationToken cancellationToken)
{
foreach (var asset in assets.EnumerateArray())
{
string name = asset.GetPropertyOrDefault("name").ToLowerInvariant();
if (!name.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!name.Contains("update", StringComparison.OrdinalIgnoreCase) &&
!name.Contains("manifest", StringComparison.OrdinalIgnoreCase))
{
continue;
}
string url = asset.GetPropertyOrDefault("browser_download_url");
if (string.IsNullOrWhiteSpace(url))
{
continue;
}
try
{
using var manifestResponse = await HttpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
manifestResponse.EnsureSuccessStatusCode();
await using var stream = await manifestResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
return document.RootElement.Clone();
}
catch
{
// Try next manifest candidate.
}
}
return null;
}
private static bool TryFindAsset(JsonElement assets, Func<JsonElement, bool> predicate, out JsonElement asset)
{
if (assets.ValueKind != JsonValueKind.Array)
{
asset = default;
return false;
}
foreach (var candidate in assets.EnumerateArray())
{
if (predicate(candidate))
{
asset = candidate;
return true;
}
}
asset = default;
return false;
}
private static bool IsApkAsset(JsonElement asset)
{
string name = asset.GetPropertyOrDefault("name").ToLowerInvariant();
return name.EndsWith(".apk", StringComparison.OrdinalIgnoreCase) ||
name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase);
}
private static bool TryGetFirstAsset(JsonElement assets, out JsonElement asset)
{
if (assets.ValueKind == JsonValueKind.Array)
{
using var enumerator = assets.EnumerateArray().GetEnumerator();
if (enumerator.MoveNext())
{
asset = enumerator.Current;
return true;
}
}
asset = default;
return false;
}
private static string DeriveVersionName(string tagName, string fallback)
{
string baseName = string.IsNullOrWhiteSpace(tagName) ? fallback : tagName;
if (string.IsNullOrWhiteSpace(baseName))
{
return string.Empty;
}
return Regex.Replace(baseName, @"^[Vv]", string.Empty).Trim();
}
private static int ParseVersionCode(string versionName)
{
if (string.IsNullOrWhiteSpace(versionName))
{
return -1;
}
var parts = versionName.Split('.', StringSplitOptions.RemoveEmptyEntries);
int major = ParsePart(parts, 0);
int minor = ParsePart(parts, 1);
int patch = ParsePart(parts, 2);
return major * 10000 + minor * 100 + patch;
}
private static int ParsePart(IReadOnlyList<string> parts, int index)
{
if (index >= parts.Count)
{
return 0;
}
if (int.TryParse(Regex.Replace(parts[index], @"[^\d]", string.Empty), out int value))
{
return value;
}
return 0;
}
}