releases/VideoDownloader.Core/Update/UpdateChecker.cs
Yun Chan 02995f4153 MEDIABROWSER 개명·업데이트 채널 전환·아이콘 tofu 근본 수정 (BACKLOG 13차) — ①개명: installer.iss AppName/출력 MediaBrowser-Setup-*(AppId·exe·데이터 경로 유지로 업그레이드 호환), 워크플로 산출물명(mediabrowser-*.tar.gz, MediaBrowser-<ver>-arm64.apk), 창 타이틀·모바일 표시명·obtainium.json·README. ②UpdateChecker: apiBase 주입(기본=자체 Forgejo /api/v1, GitHub 는 apiBase 로 호환), osx 자산 tar.gz 허용(기존 dmg 만 → mac 영원히 자산 없음 결함 수정), AppConfig 기본 UpdateRepo=Video-Downloader/releases+UpdateApiBase — 테스트 4건. ③아이콘 tofu(사용자 보고 "절대 안고쳐지네") 근본 원인: Shared.xaml 암시적 TextBlock 스타일의 FontFamily=UiFont 강제가 버튼 ContentPresenter 자동 TextBlock 의 IconFont 상속을 덮어 모든 PUA 글리프가 notdef — 폰트 설치/글리프 존재/HW·SW 렌더/RTB/DPI/런타임 8·10/자체포함·공유 전부 배제 후 실창 픽셀 비교로 격리, 콘텐츠 TextBlock 의 실폰트가 Pretendard 체인임을 테스트로 실증. 수정: 암시 스타일 FontFamily 제거(창 루트 상속으로 통일, PairingWindow/PlayerWindow 는 SetResourceReference), PUA+텍스트 혼합 콘텐츠 7곳+코드 1곳 정리, 순수 글리프 2곳 IconFont 명시. 회귀 방어: IconGlyphXamlTests(정적 스캔)·IconContentFontTests(실창 폰트 단정)·IconGlyphRenderTests(픽셀 비교) — 462/462 GREEN, 수정 전후 실창 캡처로 글리프 렌더 확인
2026-08-18 07:13:18 +09:00

91 lines
4.3 KiB
C#

using System.Text.Json;
namespace VideoDownloader.Core.Update;
/// <summary>릴리스의 설치 자산 하나.</summary>
public sealed record UpdateAsset(string Name, string DownloadUrl);
/// <summary>업데이트 확인 결과. LatestVersion 은 태그의 v 접두를 뗀 정규형.</summary>
public sealed record UpdateCheckResult(bool IsNewer, string CurrentVersion, string LatestVersion, string ReleaseUrl, UpdateAsset? Asset);
/// <summary>
/// DEPLOYMENT_PLAN §6 자동 업데이트 v1 — Releases latest 태그 폴링(무의존·전 OS 공통).
/// 기본 채널은 자체 Forgejo 공개 릴리스 저장소(§5-bis)이고, apiBase 로 GitHub 등 다른 채널도 쓸 수 있다
/// (Forgejo 와 GitHub 의 releases/latest 응답 형태가 동일). 설치는 사람 관문 — URL 만 돌려준다.
/// </summary>
public sealed class UpdateChecker
{
/// <summary>기본 업데이트 채널 API 베이스 — Forgejo 는 /api/v1 접두 포함.</summary>
public const string DefaultApiBase = "https://git.chanpaca.net/api/v1";
private readonly HttpClient _http;
public UpdateChecker(HttpMessageHandler? handler = null)
{
_http = handler == null ? new HttpClient() : new HttpClient(handler);
_http.DefaultRequestHeaders.UserAgent.ParseAdd("VideoDownloader-UpdateCheck"); // GitHub API 는 UA 필수
_http.Timeout = TimeSpan.FromSeconds(15);
}
/// <summary>"v1.2.3"/"1.2.3.0" 을 관대하게 비교. 어느 쪽이든 파싱 불가면 false(갱신 아님).</summary>
public static bool IsNewer(string current, string latest) =>
TryParse(current, out var cur) && TryParse(latest, out var lat) && lat > cur;
private static bool TryParse(string s, out Version version)
{
s = s.Trim().TrimStart('v', 'V');
if (!s.Contains('.')) s += ".0"; // Version.TryParse 는 최소 2파트 요구
return Version.TryParse(s, out version!);
}
/// <summary>RID(win-x64 등)에 맞는 설치 자산 선택. "x64" 판정에서 arm64 를 먼저 배제한다.</summary>
public static UpdateAsset? SelectAsset(IEnumerable<UpdateAsset> assets, string rid)
{
var parts = rid.Split('-');
var os = parts[0];
var arch = parts[^1];
foreach (var asset in assets)
{
var name = asset.Name.ToLowerInvariant();
var isArm = name.Contains("arm64");
if (arch == "arm64" ? !isArm : isArm || !name.Contains(arch)) continue;
var osMatch = os switch
{
"win" => name.EndsWith(".exe") || name.EndsWith(".msi"),
"linux" => name.EndsWith(".tar.gz") || name.EndsWith(".appimage") || name.EndsWith(".deb"),
"osx" => name.EndsWith(".dmg") || name.EndsWith(".tar.gz"), // 프리뷰는 tar.gz 배포(§5-bis), 정식 서명판은 dmg
_ => false,
};
if (osMatch) return asset;
}
return null;
}
/// <summary>
/// repo("owner/repo")의 latest 릴리스를 조회한다. API 실패는 HttpRequestException 으로 드러난다.
/// apiBase 미지정이면 기본 채널(자체 Forgejo). GitHub 는 "https://api.github.com" 을 넘긴다.
/// </summary>
public async Task<UpdateCheckResult> CheckAsync(string repo, string currentVersion, string rid, string? apiBase = null, CancellationToken ct = default)
{
using var res = await _http.GetAsync($"{(apiBase ?? DefaultApiBase).TrimEnd('/')}/repos/{repo}/releases/latest", ct);
res.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync(ct));
var root = doc.RootElement;
var tag = root.GetProperty("tag_name").GetString() ?? "";
var releaseUrl = root.TryGetProperty("html_url", out var h) ? h.GetString() ?? "" : "";
var assets = new List<UpdateAsset>();
if (root.TryGetProperty("assets", out var arr) && arr.ValueKind == JsonValueKind.Array)
{
foreach (var a in arr.EnumerateArray())
{
assets.Add(new UpdateAsset(
a.GetProperty("name").GetString() ?? "",
a.GetProperty("browser_download_url").GetString() ?? ""));
}
}
return new UpdateCheckResult(
IsNewer(currentVersion, tag), currentVersion, tag.TrimStart('v', 'V'), releaseUrl, SelectAsset(assets, rid));
}
}