diff --git a/Epub/KoeBook.Epub/Contracts/Services/IScrapingClientService.cs b/Epub/KoeBook.Epub/Contracts/Services/IScrapingClientService.cs new file mode 100644 index 0000000..c2a0b81 --- /dev/null +++ b/Epub/KoeBook.Epub/Contracts/Services/IScrapingClientService.cs @@ -0,0 +1,18 @@ +using System.Net.Http.Headers; + +namespace KoeBook.Epub.Contracts.Services; + +public interface IScrapingClientService +{ + /// + /// スクレイピングでGETする用 + /// APIを叩く際は不要 + /// + Task GetAsStringAsync(string url, CancellationToken ct); + + /// + /// スクレイピングでGETする用 + /// APIを叩く際は不要 + /// + Task GetAsStreamAsync(string url, Stream destination, CancellationToken ct); +} diff --git a/Epub/KoeBook.Epub/Services/ScrapingAozoraService.cs b/Epub/KoeBook.Epub/Services/ScrapingAozoraService.cs index 3516dd9..7ea75c7 100644 --- a/Epub/KoeBook.Epub/Services/ScrapingAozoraService.cs +++ b/Epub/KoeBook.Epub/Services/ScrapingAozoraService.cs @@ -5,13 +5,16 @@ using KoeBook.Core; using KoeBook.Epub.Contracts.Services; using KoeBook.Epub.Models; +using Microsoft.Extensions.DependencyInjection; using static KoeBook.Epub.Utility.ScrapingHelper; namespace KoeBook.Epub.Services { - public partial class ScrapingAozoraService : IScrapingService + public partial class ScrapingAozoraService([FromKeyedServices(nameof(ScrapingAozoraService))] IScrapingClientService scrapingClientService) : IScrapingService { + private readonly IScrapingClientService _scrapingClientService = scrapingClientService; + public bool IsMatchSite(Uri uri) { return uri.Host == "www.aozora.gr.jp"; diff --git a/Epub/KoeBook.Epub/Services/ScrapingClientService.cs b/Epub/KoeBook.Epub/Services/ScrapingClientService.cs new file mode 100644 index 0000000..164898e --- /dev/null +++ b/Epub/KoeBook.Epub/Services/ScrapingClientService.cs @@ -0,0 +1,116 @@ +using System.Net.Http.Headers; +using KoeBook.Epub.Contracts.Services; + +namespace KoeBook.Epub.Services; + +public sealed class ScrapingClientService : IScrapingClientService, IDisposable +{ + 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); + } + + public Task GetAsStringAsync(string url, CancellationToken ct) + { + var taskCompletion = new TaskCompletionSource(); + + lock (_actionQueue) + _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); + } + }); + + EnsureWorkerActivated(); + + return taskCompletion.Task; + } + + public Task GetAsStreamAsync(string url, Stream destination, CancellationToken ct) + { + var taskCompletion = new TaskCompletionSource(); + + lock (_actionQueue) + _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); + } + }); + + EnsureWorkerActivated(); + + return taskCompletion.Task; + } + + /// + /// が起動していない場合は起動します + /// + private void EnsureWorkerActivated() + { + bool activateWorker; + lock (_actionQueue) activateWorker = !_workerActivated; + + if (activateWorker) + Worker(); + } + + /// + /// のConsumer + /// 別スレッドでループさせるためにvoid + /// + private async void Worker() + { + lock (_actionQueue) + _workerActivated = true; + + try + { + while (await _periodicTimer.WaitForNextTickAsync().ConfigureAwait(false) && _actionQueue.Count > 0) + { + Func? action; + lock (_actionQueue) + if (!_actionQueue.TryDequeue(out action)) + continue; + + await action(_httpClientFactory.CreateClient()).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + } + finally + { + lock (_actionQueue) + _workerActivated = false; + } + } + + public void Dispose() + { + _periodicTimer.Dispose(); + } +} diff --git a/Epub/KoeBook.Epub/Services/ScrapingNaroService.cs b/Epub/KoeBook.Epub/Services/ScrapingNaroService.cs index c04694f..70dd1e4 100644 --- a/Epub/KoeBook.Epub/Services/ScrapingNaroService.cs +++ b/Epub/KoeBook.Epub/Services/ScrapingNaroService.cs @@ -6,13 +6,15 @@ using KoeBook.Core; using KoeBook.Epub.Contracts.Services; using KoeBook.Epub.Models; +using Microsoft.Extensions.DependencyInjection; using static KoeBook.Epub.Utility.ScrapingHelper; namespace KoeBook.Epub.Services { - public partial class ScrapingNaroService(IHttpClientFactory httpClientFactory) : IScrapingService + public partial class ScrapingNaroService(IHttpClientFactory httpClientFactory, [FromKeyedServices(nameof(ScrapingNaroService))] IScrapingClientService scrapingClientService) : IScrapingService { private readonly IHttpClientFactory _httpCliantFactory = httpClientFactory; + private readonly IScrapingClientService _scrapingClientService = scrapingClientService; public bool IsMatchSite(Uri uri) { diff --git a/KoeBook/App.xaml.cs b/KoeBook/App.xaml.cs index 0ed636f..10b0523 100644 --- a/KoeBook/App.xaml.cs +++ b/KoeBook/App.xaml.cs @@ -60,6 +60,9 @@ public App() .UseContentRoot(AppContext.BaseDirectory) .ConfigureServices((context, services) => { + // System + services.AddSingleton(TimeProvider.System); + // Default Activation Handler services.AddTransient, DefaultActivationHandler>(); @@ -99,7 +102,11 @@ public App() services.AddSingleton(); services.AddSingleton(); - services.AddSingleton() + // Epub Services + services + .AddKeyedSingleton(nameof(ScrapingAozoraService)) + .AddKeyedSingleton(nameof(ScrapingNaroService)) + .AddSingleton() .AddSingleton() .AddSingleton(); services.AddSingleton();