-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathUpdateService.cs
More file actions
301 lines (254 loc) · 11.1 KB
/
UpdateService.cs
File metadata and controls
301 lines (254 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
namespace TaskbarInfo
{
public sealed class UpdateService
{
private const string RepoOwner = "yixing233";
private const string RepoName = "LyricsX";
private const string DefaultVersion = "1.0.0";
private static readonly HttpClient HttpClient = CreateHttpClient();
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
public static string RepositoryUrl => $"https://github.com/{RepoOwner}/{RepoName}";
public static string ReleasesUrl => $"{RepositoryUrl}/releases";
public static string CurrentVersionDisplay => $"v{ToDisplayVersion(GetCurrentVersion())}";
public async Task<UpdateCheckResult> CheckForUpdatesAsync(CancellationToken cancellationToken = default)
{
var currentVersion = GetCurrentVersion();
using var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.github.com/repos/{RepoOwner}/{RepoName}/releases/latest");
using var response = await HttpClient.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return UpdateCheckResult.NoRelease(currentVersion);
}
if (!response.IsSuccessStatusCode)
{
var fallbackResult = await TryCheckFromLatestReleasePageAsync(currentVersion, cancellationToken);
if (fallbackResult != null)
{
return fallbackResult;
}
return UpdateCheckResult.Failed(
currentVersion,
$"GitHub 返回 {(int)response.StatusCode} {response.ReasonPhrase}");
}
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
var release = await JsonSerializer.DeserializeAsync<GitHubReleaseResponse>(stream, JsonOptions, cancellationToken);
if (release == null)
{
return UpdateCheckResult.Failed(currentVersion, "未能解析 GitHub Release 响应。");
}
var latestVersion = TryParseVersion(release.TagName) ?? TryParseVersion(release.Name);
if (latestVersion == null)
{
return UpdateCheckResult.Failed(currentVersion, "最新 Release 的标签不是可比较的版本号。请使用类似 v1.2.3 的标签。");
}
var releasePageUrl = string.IsNullOrWhiteSpace(release.HtmlUrl) ? ReleasesUrl : release.HtmlUrl!;
var downloadUrl = SelectBestAssetUrl(release.Assets) ?? releasePageUrl;
var releaseTitle = string.IsNullOrWhiteSpace(release.Name) ? release.TagName ?? latestVersion.ToString() : release.Name!;
return UpdateCheckResult.SuccessResult(
currentVersion,
latestVersion,
releaseTitle,
release.TagName ?? releaseTitle,
release.Body ?? string.Empty,
releasePageUrl,
downloadUrl,
release.PublishedAt);
}
private static async Task<UpdateCheckResult?> TryCheckFromLatestReleasePageAsync(Version currentVersion, CancellationToken cancellationToken)
{
using var response = await HttpClient.GetAsync($"{RepositoryUrl}/releases/latest", cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return UpdateCheckResult.NoRelease(currentVersion);
}
if (!response.IsSuccessStatusCode)
{
return null;
}
var finalUri = response.RequestMessage?.RequestUri?.AbsoluteUri;
if (string.IsNullOrWhiteSpace(finalUri) || !finalUri.Contains("/releases/tag/", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var tag = Uri.UnescapeDataString(finalUri.Split('/').Last());
var latestVersion = TryParseVersion(tag);
if (latestVersion == null)
{
return null;
}
return UpdateCheckResult.SuccessResult(
currentVersion,
latestVersion,
tag,
tag,
string.Empty,
finalUri,
finalUri,
null);
}
private static HttpClient CreateHttpClient()
{
var client = new HttpClient();
var version = ToDisplayVersion(GetCurrentVersion());
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LyricsX", version));
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
client.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28");
return client;
}
private static Version GetCurrentVersion()
{
var assembly = Assembly.GetExecutingAssembly();
var informational = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
return TryParseVersion(informational)
?? NormalizeVersion(assembly.GetName().Version)
?? new Version(DefaultVersion);
}
private static Version? TryParseVersion(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var cleaned = value.Trim();
if (cleaned.StartsWith('v') || cleaned.StartsWith('V'))
{
cleaned = cleaned[1..];
}
var suffixIndex = cleaned.IndexOfAny(['-', '+', ' ']);
if (suffixIndex >= 0)
{
cleaned = cleaned[..suffixIndex];
}
if (!Version.TryParse(cleaned, out var version))
{
return null;
}
return NormalizeVersion(version);
}
private static Version? NormalizeVersion(Version? version)
{
if (version == null)
{
return null;
}
var build = version.Build >= 0 ? version.Build : 0;
return new Version(version.Major, version.Minor, build);
}
private static string ToDisplayVersion(Version version)
{
return $"{version.Major}.{version.Minor}.{version.Build}";
}
private static string? SelectBestAssetUrl(IReadOnlyList<GitHubReleaseAssetResponse>? assets)
{
if (assets == null || assets.Count == 0)
{
return null;
}
var preferred = assets.FirstOrDefault(asset =>
!string.IsNullOrWhiteSpace(asset.BrowserDownloadUrl) &&
(asset.Name?.EndsWith(".msi", StringComparison.OrdinalIgnoreCase) == true
|| asset.Name?.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) == true
|| asset.Name?.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) == true));
return preferred?.BrowserDownloadUrl
?? assets.FirstOrDefault(asset => !string.IsNullOrWhiteSpace(asset.BrowserDownloadUrl))?.BrowserDownloadUrl;
}
private sealed class GitHubReleaseResponse
{
[JsonPropertyName("tag_name")]
public string? TagName { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("body")]
public string? Body { get; set; }
[JsonPropertyName("html_url")]
public string? HtmlUrl { get; set; }
[JsonPropertyName("published_at")]
public DateTimeOffset? PublishedAt { get; set; }
[JsonPropertyName("assets")]
public List<GitHubReleaseAssetResponse>? Assets { get; set; }
}
private sealed class GitHubReleaseAssetResponse
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("browser_download_url")]
public string? BrowserDownloadUrl { get; set; }
}
}
public sealed class UpdateCheckResult
{
private UpdateCheckResult()
{
}
public bool Success { get; private init; }
public bool HasUpdate { get; private init; }
public bool NoReleasePublished { get; private init; }
public Version CurrentVersion { get; private init; } = new Version(1, 0, 0);
public Version? LatestVersion { get; private init; }
public string CurrentVersionDisplay => $"v{CurrentVersion.Major}.{CurrentVersion.Minor}.{CurrentVersion.Build}";
public string LatestVersionDisplay => LatestVersion == null ? "未知" : $"v{LatestVersion.Major}.{LatestVersion.Minor}.{LatestVersion.Build}";
public string ReleaseName { get; private init; } = string.Empty;
public string ReleaseTag { get; private init; } = string.Empty;
public string ReleaseNotes { get; private init; } = string.Empty;
public string ReleasePageUrl { get; private init; } = UpdateService.ReleasesUrl;
public string DownloadUrl { get; private init; } = UpdateService.ReleasesUrl;
public DateTimeOffset? PublishedAt { get; private init; }
public string? ErrorMessage { get; private init; }
public static UpdateCheckResult SuccessResult(
Version currentVersion,
Version latestVersion,
string releaseName,
string releaseTag,
string releaseNotes,
string releasePageUrl,
string downloadUrl,
DateTimeOffset? publishedAt)
{
return new UpdateCheckResult
{
Success = true,
HasUpdate = latestVersion > currentVersion,
CurrentVersion = currentVersion,
LatestVersion = latestVersion,
ReleaseName = releaseName,
ReleaseTag = releaseTag,
ReleaseNotes = releaseNotes,
ReleasePageUrl = releasePageUrl,
DownloadUrl = downloadUrl,
PublishedAt = publishedAt
};
}
public static UpdateCheckResult NoRelease(Version currentVersion)
{
return new UpdateCheckResult
{
Success = true,
NoReleasePublished = true,
CurrentVersion = currentVersion
};
}
public static UpdateCheckResult Failed(Version currentVersion, string errorMessage)
{
return new UpdateCheckResult
{
Success = false,
CurrentVersion = currentVersion,
ErrorMessage = errorMessage
};
}
}
}