Initial implementation of voice works scanning.

This commit is contained in:
2025-09-11 00:07:49 -04:00
parent f250276a99
commit 3c0a39b324
50 changed files with 1351 additions and 88 deletions

View File

@@ -1,79 +1,88 @@
using Microsoft.Extensions.Logging;
using System.IO;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace JSMR.Infrastructure.Http;
public abstract class ApiClient(HttpClient http, ILogger logger, JsonSerializerOptions? json = null)
public abstract class ApiClient(IHttpService http, ILogger logger, JsonSerializerOptions? json = null)
{
protected async Task<T> GetJsonAsync<T>(
string url,
Action<HttpRequestHeaders>? configureHeaders = null,
CancellationToken ct = default
)
protected async Task<T> GetJsonAsync<T>(string url, CancellationToken cancellationToken = default)
{
using var req = new HttpRequestMessage(HttpMethod.Get, url);
configureHeaders?.Invoke(req.Headers);
string response = await http.GetStringAsync(url, cancellationToken);
LogRequest(req);
using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
await EnsureSuccess(res).ConfigureAwait(false);
var stream = await res.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
var model = await JsonSerializer.DeserializeAsync<T>(stream, json, ct).ConfigureAwait(false)
return JsonSerializer.Deserialize<T>(response, json)
?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(T).Name} from {url}.");
return model;
}
protected async Task<TResponse> PostJsonAsync<TRequest, TResponse>(
string url,
TRequest payload,
Action<HttpRequestHeaders>? configureHeaders = null,
CancellationToken ct = default)
{
var content = new StringContent(JsonSerializer.Serialize(payload, json), Encoding.UTF8, "application/json");
using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };
configureHeaders?.Invoke(req.Headers);
//protected async Task<T> GetJsonAsync<T>(
// string url,
// Action<HttpRequestHeaders>? configureHeaders = null,
// CancellationToken ct = default
// )
//{
// using var req = new HttpRequestMessage(HttpMethod.Get, url);
// configureHeaders?.Invoke(req.Headers);
LogRequest(req);
// LogRequest(req);
using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
await EnsureSuccess(res).ConfigureAwait(false);
// using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
// await EnsureSuccess(res).ConfigureAwait(false);
var stream = await res.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
// var stream = await res.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
var model = await JsonSerializer.DeserializeAsync<TResponse>(stream, json, ct).ConfigureAwait(false)
?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}.");
return model;
}
// var model = await JsonSerializer.DeserializeAsync<T>(stream, json, ct).ConfigureAwait(false)
// ?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(T).Name} from {url}.");
protected virtual void LogRequest(HttpRequestMessage req)
=> logger.LogDebug("HTTP {Method} {Uri}", req.Method, req.RequestUri);
// return model;
//}
protected virtual void LogFailure(HttpResponseMessage res, string body)
=> logger.LogWarning("HTTP {Status} for {Uri}. Body: {Body}", (int)res.StatusCode, res.RequestMessage?.RequestUri, Truncate(body, 500));
//protected async Task<TResponse> PostJsonAsync<TRequest, TResponse>(
// string url,
// TRequest payload,
// Action<HttpRequestHeaders>? configureHeaders = null,
// CancellationToken ct = default)
//{
// var content = new StringContent(JsonSerializer.Serialize(payload, json), Encoding.UTF8, "application/json");
// using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };
// configureHeaders?.Invoke(req.Headers);
protected static string Truncate(string s, int max) => s.Length <= max ? s : s[..max] + "…";
// LogRequest(req);
protected async Task EnsureSuccess(HttpResponseMessage res)
{
if (res.IsSuccessStatusCode) return;
// using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
// await EnsureSuccess(res).ConfigureAwait(false);
string body;
try { body = await res.Content.ReadAsStringAsync().ConfigureAwait(false); }
catch { body = "<unable to read body>"; }
// var stream = await res.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
LogFailure(res, body);
// var model = await JsonSerializer.DeserializeAsync<TResponse>(stream, json, ct).ConfigureAwait(false)
// ?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}.");
// Throw a richer exception (you can customize per API)
throw new HttpRequestException(
$"Request to {res.RequestMessage?.RequestUri} failed: {(int)res.StatusCode} {res.ReasonPhrase}. Body: {Truncate(body, 1000)}",
null,
res.StatusCode);
}
// return model;
//}
//protected virtual void LogRequest(HttpRequestMessage req)
// => logger.LogDebug("HTTP {Method} {Uri}", req.Method, req.RequestUri);
//protected virtual void LogFailure(HttpResponseMessage res, string body)
// => logger.LogWarning("HTTP {Status} for {Uri}. Body: {Body}", (int)res.StatusCode, res.RequestMessage?.RequestUri, Truncate(body, 500));
//protected static string Truncate(string s, int max) => s.Length <= max ? s : s[..max] + "…";
//protected async Task EnsureSuccess(HttpResponseMessage res)
//{
// if (res.IsSuccessStatusCode) return;
// string body;
// try { body = await res.Content.ReadAsStringAsync().ConfigureAwait(false); }
// catch { body = "<unable to read body>"; }
// LogFailure(res, body);
// Throw a richer exception(you can customize per API)
// throw new HttpRequestException(
// $"Request to {res.RequestMessage?.RequestUri} failed: {(int)res.StatusCode} {res.ReasonPhrase}. Body: {Truncate(body, 1000)}",
// null,
// res.StatusCode);
//}
}

View File

@@ -0,0 +1,16 @@
using HtmlAgilityPack;
namespace JSMR.Infrastructure.Http;
public class HtmlLoader(IHttpService httpService) : IHtmlLoader
{
public async Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken)
{
string html = await httpService.GetStringAsync(url, cancellationToken);
HtmlDocument document = new();
document.LoadHtml(html);
return document;
}
}

View File

@@ -0,0 +1,24 @@
namespace JSMR.Infrastructure.Http;
public class HttpService(HttpClient httpClient) : IHttpService
{
public Task<string> GetStringAsync(string url, CancellationToken cancellationToken)
=> GetStringAsync(url, new Dictionary<string, string>(), cancellationToken);
public async Task<string> GetStringAsync(string url, IDictionary<string, string> headers, CancellationToken cancellationToken)
{
using HttpRequestMessage request = new(HttpMethod.Get, url);
foreach (KeyValuePair<string, string> header in headers)
{
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("JSMR/1.0");
using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,8 @@
using HtmlAgilityPack;
namespace JSMR.Infrastructure.Http;
public interface IHtmlLoader
{
Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,7 @@
namespace JSMR.Infrastructure.Http;
public interface IHttpService
{
Task<string> GetStringAsync(string url, CancellationToken cancellationToken);
Task<string> GetStringAsync(string url, IDictionary<string, string> headers, CancellationToken cancellationToken);
}