237 lines
8.7 KiB
C#
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;
|
|
}
|
|
}
|