diff --git a/Epub/KoeBook.Epub/Contracts/Services/IScrapingClientService.cs b/Epub/KoeBook.Epub/Contracts/Services/IScrapingClientService.cs index 44cff57..c2a0b81 100644 --- a/Epub/KoeBook.Epub/Contracts/Services/IScrapingClientService.cs +++ b/Epub/KoeBook.Epub/Contracts/Services/IScrapingClientService.cs @@ -1,10 +1,18 @@ -namespace KoeBook.Epub.Contracts.Services; +using System.Net.Http.Headers; + +namespace KoeBook.Epub.Contracts.Services; public interface IScrapingClientService { /// /// スクレイピングでGETする用 - /// APIは不要 + /// APIを叩く際は不要 + /// + Task GetAsStringAsync(string url, CancellationToken ct); + + /// + /// スクレイピングでGETする用 + /// APIを叩く際は不要 /// - ValueTask GetAsync(string url, CancellationToken ct); + Task GetAsStreamAsync(string url, Stream destination, CancellationToken ct); } diff --git a/Epub/KoeBook.Epub/Services/ScrapingClientService.cs b/Epub/KoeBook.Epub/Services/ScrapingClientService.cs index 9a0ab20..1367980 100644 --- a/Epub/KoeBook.Epub/Services/ScrapingClientService.cs +++ b/Epub/KoeBook.Epub/Services/ScrapingClientService.cs @@ -1,22 +1,103 @@ -using KoeBook.Epub.Contracts.Services; +using System.Net.Http.Headers; +using KoeBook.Epub.Contracts.Services; namespace KoeBook.Epub.Services; -public sealed class ScrapingClientService(IHttpClientFactory httpClientFactory, TimeProvider timeProvider) : IScrapingClientService, IDisposable +public sealed class ScrapingClientService : IScrapingClientService, IDisposable { - private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; - private readonly PeriodicTimer _periodicTimer = new(TimeSpan.FromSeconds(10), timeProvider); + private readonly IHttpClientFactory _httpClientFactory; + private readonly PeriodicTimer _periodicTimer; + private readonly Queue> _actionQueue = []; + private bool _workerActivated; + + public ScrapingClientService(IHttpClientFactory httpClientFactory, TimeProvider timeProvider) + { + _httpClientFactory = httpClientFactory; + _periodicTimer = new(TimeSpan.FromSeconds(10), timeProvider); + + Worker(); + } public void Dispose() { _periodicTimer.Dispose(); } - public async ValueTask GetAsync(string url, CancellationToken ct) + private async void Worker() + { + lock (_actionQueue) + { + _workerActivated = true; + } + + while (await _periodicTimer.WaitForNextTickAsync().ConfigureAwait(false) && _actionQueue.Count > 0) + { + if (_actionQueue.TryDequeue(out var action)) + { + await action(_httpClientFactory.CreateClient()).ConfigureAwait(false); + } + } + + lock (_actionQueue) + { + _workerActivated = false; + } + } + + public Task GetAsStringAsync(string url, CancellationToken ct) { - await _periodicTimer.WaitForNextTickAsync(ct).ConfigureAwait(false); + var taskCompletion = new TaskCompletionSource(); + _actionQueue.Enqueue(async httpClient => + { + if (ct.IsCancellationRequested) + taskCompletion.SetCanceled(ct); + + try + { + var response = await httpClient.GetAsync(url, ct).ConfigureAwait(false); + taskCompletion.SetResult(await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false)); + } + catch (Exception ex) + { + taskCompletion.SetException(ex); + } + }); + + lock (_actionQueue) + { + if (!_workerActivated) + Worker(); + } + + return taskCompletion.Task; + } + + public Task GetAsStreamAsync(string url, Stream destination, CancellationToken ct) + { + var taskCompletion = new TaskCompletionSource(); + _actionQueue.Enqueue(async httpClient => + { + if (ct.IsCancellationRequested) + taskCompletion.SetCanceled(ct); + + try + { + var response = await httpClient.GetAsync(url, ct).ConfigureAwait(false); + await response.Content.CopyToAsync(destination, ct).ConfigureAwait(false); + taskCompletion.SetResult(response.Content.Headers.ContentDisposition); + } + catch (Exception ex) + { + taskCompletion.SetException(ex); + } + }); + + lock (_actionQueue) + { + if (!_workerActivated) + Worker(); + } - var httpClient = _httpClientFactory.CreateClient(); - return await httpClient.GetAsync(url, ct).ConfigureAwait(false); + return taskCompletion.Task; } }