diff --git a/Beam.Dynamic/Binding.cs b/Beam.Dynamic/Binding.cs index a66624f..7f6647b 100644 --- a/Beam.Dynamic/Binding.cs +++ b/Beam.Dynamic/Binding.cs @@ -1,69 +1,19 @@  using aeqw89.DataKeys; using HtmlAgilityPack; +using System.Text.Json; using System.Text.Json.Serialization; namespace Beam.Dynamic { - public class Binding(DataKey key) : IKeyed { + public class Binding(DataKey key) : IBinding, IKeyed { public Binding(string key) : this(new DataKey(key)) { } public Binding() : this("") { } [JsonRequired] public DataKey Key { get; set; } = key; - [JsonRequired] - public BindingType Type { get; set; } - public string? ArrayDelimiters { get; set; } public string? XPath { get; set; } public string? CssPath { get; set; } public string? Text { get; set; } - private IDataProvider? Provider_; - public IDataProvider? Provider { - get => Provider_; - set { - if (value is null) - return; - if (value is not IDataProvider) - throw new InvalidOperationException(); - var constructor = value.GetType().GetConstructor([]); - if (!constructor?.IsPublic ?? true) - throw new InvalidOperationException(); - Provider_ = value; - } - } - - public HtmlNode? ResolveNode(HtmlDocument doc) { - if (XPath is not null) - return doc.DocumentNode.SelectSingleNode(XPath); - if (CssPath is not null) - return doc.DocumentNode.ThenByClasses(CssPath.Split('/')); - if (Provider is not null) - return Provider.GetNode(doc); - return null; - } - - public string ResolveString(HtmlDocument doc) { - if (XPath is not null) - return doc.DocumentNode.SelectSingleNode(XPath)?.InnerText ?? ""; - if (CssPath is not null) - return doc.DocumentNode.ThenByClasses(CssPath.Split('/'))?.InnerText ?? ""; - if (Provider is not null) - return Provider.Get(doc); - return ""; - } - - public string[] ResolveArray(HtmlDocument doc) { - if (Type is not BindingType.Array) - return []; - var str = ResolveString(doc); - return str.Split(ArrayDelimiters); - } - - public dynamic? Resolve(HtmlDocument doc) => Type switch { - BindingType.Single => ResolveString(doc), - BindingType.Array => ResolveArray(doc), - BindingType.UseProvider => Provider?.Get(doc), - _ => null - }; } } diff --git a/Beam.Dynamic/BindingType.cs b/Beam.Dynamic/BindingType.cs deleted file mode 100644 index d24edd5..0000000 --- a/Beam.Dynamic/BindingType.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Beam.Dynamic { - public enum BindingType { - Single, - Array, - UseProvider - } -} diff --git a/Beam.Dynamic/ContentsArrayDataProvider.cs b/Beam.Dynamic/ContentsArrayDataProvider.cs new file mode 100644 index 0000000..1d5af6d --- /dev/null +++ b/Beam.Dynamic/ContentsArrayDataProvider.cs @@ -0,0 +1,15 @@ +using HtmlAgilityPack; + +namespace Beam.Dynamic { + public class ContentsArrayDataProvider : ContentsDataProvider, IDataProvider { + public string[] ArrayDelimiters { get; set; } = [";"]; + + string[] IDataProvider.Get(HtmlDocument document) { + if (Content is null) + return []; + + return Content.Select(document)?.InnerText?.Split(ArrayDelimiters, StringSplitOptions.RemoveEmptyEntries) ?? []; + } + } + +} diff --git a/Beam.Dynamic/ContentsDataProvider.cs b/Beam.Dynamic/ContentsDataProvider.cs new file mode 100644 index 0000000..28a8d3e --- /dev/null +++ b/Beam.Dynamic/ContentsDataProvider.cs @@ -0,0 +1,20 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Beam.Dynamic { + public class ContentsDataProvider : IDataProvider { + public IBinding? Content { get; set; } + + public string Get(HtmlDocument document) { + if (Content is null) + return ""; + + return Content.Select(document)?.InnerText ?? ""; + } + } +} diff --git a/Beam.Dynamic/DataBindings.cs b/Beam.Dynamic/DataBindings.cs index 95ad480..193ffc3 100644 --- a/Beam.Dynamic/DataBindings.cs +++ b/Beam.Dynamic/DataBindings.cs @@ -2,21 +2,21 @@ namespace Beam.Dynamic { public record class DataBindings { - public Binding? Title { get; set; } - public Binding? Authors { get; set; } - public Binding? Description { get; set; } - public Binding? Content { get; set; } - public Binding? Language { get; set; } - public Binding? Tags { get; set; } + public IDataProvider? Title { get; set; } + public IDataProvider? Authors { get; set; } + public IDataProvider? Description { get; set; } + public IDataProvider? Content { get; set; } + public IDataProvider? Language { get; set; } + public IDataProvider? Tags { get; set; } public virtual ResolvedBindings Resolve(HtmlDocument doc) { return new ResolvedBindings() { - Title = Title?.Resolve(doc), - Authors = Authors?.Resolve(doc) ?? Array.Empty(), - Language = Language?.Resolve(doc) ?? Array.Empty(), - Content = Content?.Resolve(doc), - Description = Description?.Resolve(doc), - Tags = Tags?.Resolve(doc) ?? Array.Empty() + Title = Title?.Get(doc), + Authors = Authors?.Get(doc) ?? [], + Language = Language?.Get(doc), + Content = Content?.Get(doc), + Description = Description?.Get(doc), + Tags = Tags?.Get(doc) ?? [] }; } } diff --git a/Beam.Dynamic/DropDownDataProvider.cs b/Beam.Dynamic/DropDownDataProvider.cs new file mode 100644 index 0000000..6750758 --- /dev/null +++ b/Beam.Dynamic/DropDownDataProvider.cs @@ -0,0 +1,41 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Beam.Dynamic { + public class DropDownDataProvider + : IDataProvider, + IDataProvider, + IDataProvider { + public IBinding? Content { get; set; } + + public SourceLink[] Get(HtmlDocument document) { + if (Content is null) + return []; + var node = Content.Select(document); + if (node is null) + return []; + List links = []; + foreach (var child in node.ChildNodes.Where(x => x.Name == "option")) { + var childValue = child.GetAttributeValue("value", null); + if (!Uri.TryCreate(childValue, UriKind.Absolute, out _)) + continue; + links.Add(new SourceLink(childValue)); + } + + return links.ToArray(); + } + + string[] IDataProvider.Get(HtmlDocument document) { + return this.Get(document).Select(x => x.Link.AbsoluteUri).ToArray(); + } + + string IDataProvider.Get(HtmlDocument document) { + return JsonSerializer.Serialize(this.Get(document)); + } + } +} diff --git a/Beam.Dynamic/IBinding.cs b/Beam.Dynamic/IBinding.cs new file mode 100644 index 0000000..bf79832 --- /dev/null +++ b/Beam.Dynamic/IBinding.cs @@ -0,0 +1,24 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Beam.Dynamic { + [JsonDerivedType(typeof(Binding), "binding")] + public interface IBinding { + string? XPath { get; set; } + string? CssPath { get; set; } + + HtmlNode? Select(HtmlDocument doc) { + if (XPath is not null) + return doc.DocumentNode.SelectSingleNode(XPath); + if (CssPath is not null) + return doc.DocumentNode.ThenByClasses(CssPath.Split('/')); + return null; + } + } +} diff --git a/Beam.Dynamic/IDataProvider.cs b/Beam.Dynamic/IDataProvider.cs index 95e961e..d46edad 100644 --- a/Beam.Dynamic/IDataProvider.cs +++ b/Beam.Dynamic/IDataProvider.cs @@ -1,10 +1,14 @@ using HtmlAgilityPack; +using System.Text.Json.Serialization; namespace Beam.Dynamic { - [System.Text.Json.Serialization.JsonDerivedType(typeof(ParagraphedContentDataProvider), "paragraphed-data-provider")] - [System.Text.Json.Serialization.JsonDerivedType(typeof(ListContentDataProvider), "list-data-provider")] - public interface IDataProvider { - public string Get(HtmlDocument document); - public HtmlNode? GetNode(HtmlDocument document); + [JsonDerivedType(typeof(ParagraphedContentDataProvider), "paragraphed")] + [JsonDerivedType(typeof(ListContentDataProvider), "list")] + [JsonDerivedType(typeof(ContentsArrayDataProvider), "array")] + [JsonDerivedType(typeof(ContentsDataProvider), "single")] + [JsonDerivedType(typeof(DropDownDataProvider), "dropdown")] + public interface IDataProvider { + public T Get(HtmlDocument document); + //public HtmlNode? GetNode(HtmlDocument document); } } \ No newline at end of file diff --git a/Beam.Dynamic/ListContentDataProvider.cs b/Beam.Dynamic/ListContentDataProvider.cs index 9ecea32..0564ed0 100644 --- a/Beam.Dynamic/ListContentDataProvider.cs +++ b/Beam.Dynamic/ListContentDataProvider.cs @@ -2,14 +2,14 @@ using System.Text; namespace Beam.Dynamic { - public class ListContentDataProvider : IDataProvider { - public Binding? Content { get; set; } + public class ListContentDataProvider : IDataProvider { + public IBinding? Content { get; set; } public string Get(HtmlDocument document) { if (Content is null) return ""; - var node = Content.ResolveNode(document); + var node = Content.Select(document); if (node is null) return ""; @@ -23,9 +23,5 @@ namespace Beam.Dynamic { content.Append(node.ChildNodes.Last().InnerText.Trim()); return content.ToString(); } - - public HtmlNode? GetNode(HtmlDocument document) { - return Content?.ResolveNode(document); - } } } diff --git a/Beam.Dynamic/ParagraphedContentDataProvider.cs b/Beam.Dynamic/ParagraphedContentDataProvider.cs index 2262475..67e32b5 100644 --- a/Beam.Dynamic/ParagraphedContentDataProvider.cs +++ b/Beam.Dynamic/ParagraphedContentDataProvider.cs @@ -6,14 +6,14 @@ using System.Text; using System.Threading.Tasks; namespace Beam.Dynamic { - public class ParagraphedContentDataProvider : IDataProvider { - public Binding? Content { get; set; } + public class ParagraphedContentDataProvider : IDataProvider { + public IBinding? Content { get; set; } public string Get(HtmlDocument document) { if (Content is null) return ""; - var node = Content.ResolveNode(document); + var node = Content.Select(document); if (node is null) return ""; @@ -26,10 +26,5 @@ namespace Beam.Dynamic { return content.ToString(); } - - public HtmlNode? GetNode(HtmlDocument document) { - return Content?.ResolveNode(document); - } - } } diff --git a/Beam.Temporary.Cli/CommonTransformers.cs b/Beam.Temporary.Cli/CommonTransformers.cs index ceeb090..e67254a 100644 --- a/Beam.Temporary.Cli/CommonTransformers.cs +++ b/Beam.Temporary.Cli/CommonTransformers.cs @@ -11,10 +11,10 @@ namespace Beam.Temporary.Cli { public static class CommonTransformers { public static AsyncTransformer ArticleDataTransformer(DataBindings? binding) => (x) => { return Task.FromResult(new ArticleData() { - Authors = [OnlineCleaner.Clean(binding?.Authors?.Resolve(x) ?? "")], - Name = OnlineCleaner.Clean(binding?.Title?.ResolveString(x) ?? ""), - Categories = OnlineCleaner.Clean(binding?.Tags?.ResolveString(x) ?? "").Split(';') ?? [], - Description = OnlineCleaner.Clean(binding?.Description?.ResolveString(x) ?? "") + Authors = binding?.Authors?.Get(x)?.Select(OnlineCleaner.Clean)?.ToArray() ?? [], + Name = OnlineCleaner.Clean(binding?.Title?.Get(x) ?? ""), + Categories = binding?.Tags?.Get(x)?.Select(OnlineCleaner.Clean)?.ToArray() ?? [], + Description = OnlineCleaner.Clean(binding?.Description?.Get(x) ?? "") }); }; diff --git a/Beam.Temporary.Cli/NovelStatics.cs b/Beam.Temporary.Cli/NovelStatics.cs index f58163d..7ac3446 100644 --- a/Beam.Temporary.Cli/NovelStatics.cs +++ b/Beam.Temporary.Cli/NovelStatics.cs @@ -103,36 +103,34 @@ namespace Beam.Temporary.Cli { var binding_aux = new DataKey("aeqw89:bindings:wodushu_aux"); sdd.Bindings.Add(binding_agg, new() { - Title = new Binding() { - XPath = "/html/body/div[4]/div/div/div[2]/h1", - Type = BindingType.Single + Title = new ContentsDataProvider() { + Content = new Binding() { + XPath = "/html/body/div[4]/div/div/div[2]/h1" + } }, - Content = new Binding() { - Type = BindingType.UseProvider, - Provider = new ParagraphedContentDataProvider() { - Content = new Binding() { - XPath = "//*[@id=\"content\"]" - } + + Content = new ParagraphedContentDataProvider() { + Content = new Binding() { + XPath = "//*[@id=\"content\"]" } }, }); sdd.Bindings.Add(binding_aux, new() { - Title = new Binding() { - XPath = "/html/body/div[3]/div[1]/div/div/div[2]/div[1]/h1", - Type = BindingType.Single + Title = new ContentsDataProvider() { + Content = new Binding() { + XPath = "/html/body/div[3]/div[1]/div/div/div[2]/div[1]/h1" + } }, - Authors = new Binding() { - XPath = "/html/body/div[3]/div[1]/div/div/div[2]/div[1]/div/p[1]/a", - Type = BindingType.Single + Authors = new ContentsArrayDataProvider() { + Content = new Binding() { + XPath = "/html/body/div[3]/div[1]/div/div/div[2]/div[1]/div/p[1]/a" + } }, - Description = new Binding() { - Provider = new ParagraphedContentDataProvider() { - Content = new Binding() { - XPath = "/html/body/div[3]/div[1]/div/div/div[2]/div[2]" - } - }, - Type = BindingType.UseProvider + Description = new ParagraphedContentDataProvider() { + Content = new Binding() { + XPath = "/html/body/div[3]/div[1]/div/div/div[2]/div[2]" + } } }); } diff --git a/Beam/ApiCall.cs b/Beam/ApiCall.cs new file mode 100644 index 0000000..6b84128 --- /dev/null +++ b/Beam/ApiCall.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http.Json; +using System.Text; +using System.Threading.Tasks; + +namespace Beam { + public class ApiCall(HttpClient client, string uri, HttpMethod method, KeyValuePair[] headers, object? requestData, object? body, params HashSet successCodes) { + public HttpClient Client { get; } = client; + public object? RequestData { get; } = requestData; + public object? Body { get; } + public string Uri { get; } = uri; + public HttpMethod Method { get; } = method; + public KeyValuePair[] Headers { get; } = headers; + public HashSet SuccessCodes { get; } = successCodes; + + public async Task GetResponse(ILogger? logger, (int @try, int max)? tries = null, CancellationToken ct = default) { + logger?.LogInformation("Fetching '{}' with method '{}'", Uri, Method); + var request = new HttpRequestMessage(Method, Uri); + request.Content = body is null ? request.Content : JsonContent.Create(body); + foreach (var header in Headers) + request.Headers.Add(header.Key, header.Value); + var response = await Client.SendAsync(request, ct); + + if (tries is not null && tries?.@try < tries?.max && !SuccessCodes.Contains(response.StatusCode)) { + await Task.Delay((int)Math.Min(Math.Pow(2, tries.Value.@try), 60) * 1000, ct); + return await GetResponse(logger, (tries.Value.@try + 1, tries.Value.max), ct); + } + + return await ApiResponse.CreateAsync(response, logger, RequestData, ct); + } + + public static async Task Get(HttpClient client, string url, ILoggerFactory factory) + => await new ApiCall(client, url, HttpMethod.Get, [], null, null).GetResponse(factory.CreateLogger()); + } +} diff --git a/Beam/ApiCallBuilder.cs b/Beam/ApiCallBuilder.cs new file mode 100644 index 0000000..135a79a --- /dev/null +++ b/Beam/ApiCallBuilder.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Reflection.PortableExecutable; +using System.Text; +using System.Threading.Tasks; + +namespace Beam { + public class ApiCallBuilder(HttpClient client) { + HttpClient Client = client; + string Uri; + object? Data; + object? Body; + HttpMethod Method = HttpMethod.Get; + List>> Headers = []; + HashSet SuccessCodes = [HttpStatusCode.OK]; + + public ApiCallBuilder WithUri(string uri) { + Uri = uri; + return this; + } + + public ApiCallBuilder WithRequestData(object? data) { + Data = data; + return this; + } + + public ApiCallBuilder WithBody(object? data) { + Body = data; + return this; + } + + public ApiCallBuilder WithMethod(HttpMethod method) { + Method = method; + return this; + } + + public ApiCallBuilder WithHeaders(IEnumerable<(string Key, IEnumerable Value)> headers) { + Headers = headers.Select((x) => new KeyValuePair>(x.Key, x.Value.ToList())).ToList(); + return this; + } + + public ApiCallBuilder WithSuccessCodes(params HashSet codes) { + SuccessCodes = codes; + return this; + } + + public ApiCallBuilder WithSuccessCodes(params IEnumerable codes) { + SuccessCodes = codes.Cast().ToHashSet(); + return this; + } + + public ApiCallBuilder AddHeader(string key, string value) { + if (Headers.Any((x) => x.Key == key)) + Headers.FirstOrDefault((x) => x.Key == key).Value.Add(value); + else + Headers.Add(new KeyValuePair>(key, [value])); + return this; + } + + public ApiCall Build() { + if (Uri is null) + throw new InvalidOperationException(); + if (Method is null) + throw new InvalidOperationException(); + if (Headers is null) + Headers = []; + + return new ApiCall(Client, Uri, Method, Headers.Select((x) => new KeyValuePair(x.Key, x.Value.ToArray())).ToArray(), Data, Body, SuccessCodes); + } + } +} diff --git a/Beam/ApiCalls.cs b/Beam/ApiCalls.cs new file mode 100644 index 0000000..845cb0d --- /dev/null +++ b/Beam/ApiCalls.cs @@ -0,0 +1,48 @@ +// ApiCalls.cs +using System.Collections.Concurrent; +using System.Net; +using Microsoft.Extensions.Logging; + +namespace Beam { + /// + /// Executes a batch of s using either sequential or parallel strategy. + /// + public sealed class ApiCalls { + private readonly IReadOnlyList _calls; + private readonly int _maxDegree; + + internal ApiCalls(IReadOnlyList calls, int? maxDegree) { + _calls = calls ?? throw new ArgumentNullException(nameof(calls)); + _maxDegree = Math.Max(1, maxDegree ?? 1); + } + + /// + /// Runs every call and returns the ordered list of s. + /// + public async Task> ExecuteAsync( + ILogger? logger = null, + (int @try, int max)? tries = null, + CancellationToken ct = default) { + if (_maxDegree == 1) { + // sequential + var sequential = new List(_calls.Count); + foreach (var call in _calls) + sequential.Add(await call.GetResponse(logger, tries, ct)); + return sequential; + } + + // parallel + var bag = new ConcurrentBag<(int idx, ApiResponse res)>(); + await Parallel.ForEachAsync( + _calls.Select((c, i) => (call: c, idx: i)), + new ParallelOptions { MaxDegreeOfParallelism = _maxDegree, CancellationToken = ct }, + async (item, token) => { + var response = await item.call.GetResponse(logger, tries, token); + bag.Add((item.idx, response)); + }); + + // keep original ordering + return bag.OrderBy(x => x.idx).Select(x => x.res).ToList(); + } + } +} diff --git a/Beam/ApiCallsBuilder.cs b/Beam/ApiCallsBuilder.cs new file mode 100644 index 0000000..760a55b --- /dev/null +++ b/Beam/ApiCallsBuilder.cs @@ -0,0 +1,47 @@ +// ApiCallsBuilder.cs +using System.Net; + +namespace Beam { + /// + /// Fluent builder for . + /// + public sealed class ApiCallsBuilder { + private readonly List _calls = []; + private int? _parallelism = 1; // default = sequential + + public ApiCallsBuilder Add(ApiCall call) { + _calls.Add(call ?? throw new ArgumentNullException(nameof(call))); + return this; + } + + public ApiCallsBuilder AddRange(IEnumerable calls) { + _calls.AddRange(calls ?? throw new ArgumentNullException(nameof(calls))); + return this; + } + + /// Adds the same call times. + public ApiCallsBuilder Repeat(ApiCall prototype, int times) { + if (times < 1) throw new ArgumentOutOfRangeException(nameof(times)); + for (var i = 0; i < times; i++) + Add(prototype); + return this; + } + + /// Run with the specified degree of parallelism (≥ 1). + public ApiCallsBuilder UseParallel(int maxDegree) => SetDegree(Math.Max(1, maxDegree)); + + /// Run sequentially (same as UseParallel(1)). + public ApiCallsBuilder UseSequential() => SetDegree(1); + + private ApiCallsBuilder SetDegree(int degree) { + _parallelism = degree; + return this; + } + + public ApiCalls Build() { + if (_calls.Count == 0) + throw new InvalidOperationException("At least one ApiCall is required."); + return new ApiCalls(_calls, _parallelism); + } + } +} diff --git a/Beam/ApiResponse.cs b/Beam/ApiResponse.cs new file mode 100644 index 0000000..3fc2548 --- /dev/null +++ b/Beam/ApiResponse.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.Logging; +using System.Net; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; + +namespace Beam { + /// + /// Wrapper that lets the response body be read any number of times (even concurrently). + /// + public sealed class ApiResponse { + private readonly byte[] _buffer; + + private ApiResponse(HttpResponseMessage response, byte[] buffer, ILogger? logger, object? requestData = null) { + Response = response; + _buffer = buffer; + Logger = logger; + RequestData = requestData; + } + + public HttpResponseMessage Response { get; } + public object? RequestData { get; } + public ILogger? Logger { get; } + + /* ---------- creation ---------- */ + + public static async Task CreateAsync( + HttpResponseMessage response, + ILogger? logger = null, + object? requestData = null, + CancellationToken ct = default) { + if (response is null) throw new ArgumentNullException(nameof(response)); + + var buffer = response.Content is null + ? [] + : await response.Content.ReadAsByteArrayAsync(ct).ConfigureAwait(false); + + return new ApiResponse(response, buffer, logger, requestData); + } + + /* ---------- status helpers ---------- */ + + public bool Is404 => Response.StatusCode == HttpStatusCode.NotFound; + public bool Is403 => Response.StatusCode == HttpStatusCode.Forbidden; + public bool Is500 => Response.StatusCode == HttpStatusCode.InternalServerError; + public bool Is400 => Response.StatusCode == HttpStatusCode.BadRequest; + public bool Is200 => Response.IsSuccessStatusCode; + + public ApiResponse OnError(Action errorHandler) { + if (!Is200) errorHandler(Response.StatusCode); + return this; + } + + /* ---------- content helpers ---------- */ + + public Task AsSerializedObject(CancellationToken ct = default) { + if (!Is200) throw new InvalidOperationException(); + if (Response.Content?.Headers.ContentType?.MediaType != "application/json") + Logger?.LogWarning("Content-Type is not JSON, yet JSON deserialization was requested."); + + return Task.FromResult(JsonSerializer.Deserialize(_buffer)); + } + + public Task AsDynamicObject(T _, CancellationToken ct = default) + => AsSerializedObject(ct); + + public Task AsString(CancellationToken ct = default) { + if (!Is200) Logger?.LogWarning("Non-success response; attempting to read content."); + return Task.FromResult(Encoding.UTF8.GetString(_buffer)); + } + + public Task AsBinary(CancellationToken ct = default) { + if (!Is200) Logger?.LogWarning("Non-success response; attempting to read content."); + return Task.FromResult(_buffer); + } + + public Task AsStream(CancellationToken ct = default) { + if (!Is200) Logger?.LogWarning("Non-success response; attempting to read content."); + return Task.FromResult(new MemoryStream(_buffer, writable: false)); + } + } +} diff --git a/Beam/DownloadEnumerable.cs b/Beam/DownloadEnumerable.cs index 29ed543..2855e84 100644 --- a/Beam/DownloadEnumerable.cs +++ b/Beam/DownloadEnumerable.cs @@ -5,19 +5,13 @@ using System.Text; using System.Threading.Tasks; namespace Beam { - //public class DownloadEnumerable(IAsyncEnumerator> download) : IAsyncEnumerable> { - // public IAsyncEnumerator> Download { get; } = download; + public class DownloadEnumerable(IAsyncEnumerator> download) : IAsyncEnumerable> { + public IAsyncEnumerator> Download { get; } = download; - // public IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default) - // => Download; + public IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default) + => Download; - // public static DownloadEnumerable Empty() - // => new(Array.Empty>().ToAsyncEnumerable().GetAsyncEnumerator()); - //} - - public class DownloadEnumerable(IAsyncEnumerable> download) : IAsyncEnumerable> { - public IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default) { - return download.GetAsyncEnumerator(cancellationToken); - } + public static DownloadEnumerable Empty() + => new(Array.Empty>().ToAsyncEnumerable().GetAsyncEnumerator()); } }