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();