using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; namespace VideoDownloader.Mobile; /// 데스크톱 원격 서버 API 클라이언트(M1). 토큰은 QR(#t=)/수동 입력에서 획득. public sealed class ApiClient { public static readonly ApiClient Instance = new(); private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(8) }; public string? BaseUrl { get; private set; } public string? Token { get; private set; } public string? LastError { get; private set; } public bool IsConfigured => BaseUrl is { Length: > 0 } && Token is { Length: > 0 }; public event Action? ConnectionChanged; /// QR 문자열(http://ip:port/#t=TOKEN) 또는 수동 입력(host:port + token) 구성. public bool Configure(string? qrOrUrl, string? manualToken = null) { try { if (!string.IsNullOrWhiteSpace(manualToken)) { var host = qrOrUrl!.Trim().TrimEnd('/'); if (!host.StartsWith("http", StringComparison.OrdinalIgnoreCase)) host = "http://" + host; BaseUrl = host; Token = manualToken.Trim(); } else { var u = new Uri(qrOrUrl!.Trim()); var frag = u.Fragment.TrimStart('#'); var token = System.Web.HttpUtility.ParseQueryString(frag)["t"]; if (string.IsNullOrEmpty(token)) return false; BaseUrl = $"http://{u.Authority}"; Token = token; } _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token); Preferences.Set("vd_base", BaseUrl); Preferences.Set("vd_token", Token); ConnectionChanged?.Invoke(); return true; } catch { return false; } } public void Restore() { var b = Preferences.Get("vd_base", ""); var t = Preferences.Get("vd_token", ""); if (b.Length > 0 && t.Length > 0) { BaseUrl = b; Token = t; _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", t); } } public void Forget() { BaseUrl = null; Token = null; Preferences.Remove("vd_base"); Preferences.Remove("vd_token"); ConnectionChanged?.Invoke(); } public async Task> GetQueueAsync() { try { LastError = null; var r = await _http.GetAsync($"{BaseUrl}/api/queue"); if (!r.IsSuccessStatusCode) { LastError = $"HTTP {(int)r.StatusCode}"; return new(); } return await r.Content.ReadFromJsonAsync>() ?? new(); } catch (Exception e) { LastError = e.Message; return new(); } } public async Task AddAsync(string url) { try { var r = await _http.PostAsJsonAsync($"{BaseUrl}/api/queue", new { url }); return r.IsSuccessStatusCode; } catch { return false; } } public Task ControlAsync(string id, string action) => PostAsync($"/api/queue/{id}/{action}"); public async Task PairAsync() { try { var r = await _http.PostAsJsonAsync($"{BaseUrl}/api/pair", new { token = Token, device = "Android" }); return r.IsSuccessStatusCode; } catch { return false; } } public async Task> GetLibraryAsync() { try { var r = await _http.GetAsync($"{BaseUrl}/api/library"); if (!r.IsSuccessStatusCode) return new(); return await r.Content.ReadFromJsonAsync>() ?? new(); } catch { return new(); } } public string StreamUrl(LibraryItem item) => $"{BaseUrl}/stream/{item.Id}?t={Token}"; private Task PostAsync(string path) { try { var t = _http.PostAsync($"{BaseUrl}{path}", null); return Continue(t); } catch { return Task.FromResult(false); } static async Task Continue(Task t) { try { return (await t).IsSuccessStatusCode; } catch { return false; } } } private async Task GetAsync(string path) where T : class { try { var r = await _http.GetAsync($"{BaseUrl}{path}"); return r.IsSuccessStatusCode ? await r.Content.ReadFromJsonAsync() : null; } catch { return null; } } } public sealed record QueueItem(string Id, string Title, string Url, string Status, double Percent); public sealed record LibraryItem(string Id, string FileName, long SizeBytes, DateTime Modified);