Add Windows desktop version
This commit is contained in:
236
windows/StreamPlayer.Desktop/Services/UpdateService.cs
Normal file
236
windows/StreamPlayer.Desktop/Services/UpdateService.cs
Normal file
@@ -0,0 +1,236 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user