Added Chobit integration. Updated tests.
This commit is contained in:
@@ -24,6 +24,7 @@ using JSMR.Infrastructure.Data.Repositories.Users;
|
||||
using JSMR.Infrastructure.Data.Repositories.VoiceWorks;
|
||||
using JSMR.Infrastructure.Http;
|
||||
using JSMR.Infrastructure.Ingestion;
|
||||
using JSMR.Infrastructure.Integrations.Chobit;
|
||||
using JSMR.Infrastructure.Integrations.DLSite;
|
||||
using JSMR.Infrastructure.Scanning;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -76,6 +77,9 @@ public static class InfrastructureServiceCollectionExtensions
|
||||
services.AddSingleton<ITimeProvider, TokyoTimeProvider>();
|
||||
|
||||
services.AddHttpServices();
|
||||
services.AddNewHttpServices();
|
||||
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -113,7 +117,50 @@ public static class InfrastructureServiceCollectionExtensions
|
||||
// Register DLSiteClient as a normal scoped service
|
||||
services.AddScoped<IDLSiteClient, DLSiteClient>();
|
||||
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddNewHttpServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddHttpClient<IDLSiteClient, DLSiteClient>((sp, http) =>
|
||||
{
|
||||
http.BaseAddress = new Uri("https://www.dlsite.com/");
|
||||
http.DefaultRequestHeaders.UserAgent.ParseAdd("JSMR/1.0");
|
||||
http.Timeout = TimeSpan.FromSeconds(15);
|
||||
})
|
||||
.AddResilienceHandler("dlsite", builder =>
|
||||
{
|
||||
builder.AddRetry(new HttpRetryStrategyOptions
|
||||
{
|
||||
MaxRetryAttempts = 3,
|
||||
UseJitter = true,
|
||||
Delay = TimeSpan.FromMilliseconds(200),
|
||||
BackoffType = DelayBackoffType.Exponential,
|
||||
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
|
||||
.Handle<HttpRequestException>()
|
||||
.HandleResult(r => (int)r.StatusCode >= 500 || (int)r.StatusCode == 429)
|
||||
});
|
||||
});
|
||||
|
||||
services.AddHttpClient<IChobitClient, ChobitClient>((sp, http) =>
|
||||
{
|
||||
http.BaseAddress = new Uri("https://chobit.cc/");
|
||||
http.DefaultRequestHeaders.UserAgent.ParseAdd("JSMR/1.0");
|
||||
http.Timeout = TimeSpan.FromSeconds(15);
|
||||
})
|
||||
.AddResilienceHandler("chobit", builder =>
|
||||
{
|
||||
builder.AddRetry(new HttpRetryStrategyOptions
|
||||
{
|
||||
MaxRetryAttempts = 3,
|
||||
UseJitter = true,
|
||||
Delay = TimeSpan.FromMilliseconds(200),
|
||||
BackoffType = DelayBackoffType.Exponential,
|
||||
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
|
||||
.Handle<HttpRequestException>()
|
||||
.HandleResult(r => (int)r.StatusCode >= 500 || (int)r.StatusCode == 429)
|
||||
});
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -1,91 +1,121 @@
|
||||
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(IHttpService http, ILogger logger, JsonSerializerOptions? json = null)
|
||||
public abstract class ApiClient(HttpClient http, ILogger logger, JsonSerializerOptions? json = null)
|
||||
{
|
||||
protected async Task<T> GetJsonAsync<T>(string url, CancellationToken cancellationToken = default)
|
||||
protected async Task<TResponse> GetJsonAsync<TResponse>(
|
||||
string url,
|
||||
Action<HttpRequestHeaders>? configureHeaders = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
HttpStringResponse response = await http.GetAsync(url, cancellationToken);
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, url);
|
||||
configureHeaders?.Invoke(request.Headers);
|
||||
|
||||
if (response.Content is null)
|
||||
throw new Exception("No content to deserialize");
|
||||
LogRequest(request);
|
||||
|
||||
return JsonSerializer.Deserialize<T>(response.Content, json)
|
||||
?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(T).Name} from {url}.");
|
||||
using HttpResponseMessage response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
await EnsureSuccess(response).ConfigureAwait(false);
|
||||
|
||||
Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return await JsonSerializer.DeserializeAsync<TResponse>(stream, json, cancellationToken).ConfigureAwait(false)
|
||||
?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}.");
|
||||
}
|
||||
|
||||
//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);
|
||||
protected async Task<TResponse> GetJsonpAsync<TResponse>(
|
||||
string url,
|
||||
Action<HttpRequestHeaders>? configureHeaders = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, url);
|
||||
configureHeaders?.Invoke(request.Headers);
|
||||
|
||||
// LogRequest(req);
|
||||
LogRequest(request);
|
||||
|
||||
// using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
|
||||
// await EnsureSuccess(res).ConfigureAwait(false);
|
||||
using HttpResponseMessage response = await http.SendAsync(
|
||||
request,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// var stream = await res.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
|
||||
await EnsureSuccess(response).ConfigureAwait(false);
|
||||
|
||||
// var model = await JsonSerializer.DeserializeAsync<T>(stream, json, ct).ConfigureAwait(false)
|
||||
// ?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(T).Name} from {url}.");
|
||||
string body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// return model;
|
||||
//}
|
||||
string jsonBody = ExtractJsonFromJsonp(body);
|
||||
|
||||
//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);
|
||||
return JsonSerializer.Deserialize<TResponse>(jsonBody, json)
|
||||
?? throw new InvalidOperationException($"Failed to deserialize JSONP payload to {typeof(TResponse).Name} from {url}.");
|
||||
}
|
||||
|
||||
// LogRequest(req);
|
||||
private static string ExtractJsonFromJsonp(string body)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
throw new InvalidOperationException("Response body was empty.");
|
||||
|
||||
// using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
|
||||
// await EnsureSuccess(res).ConfigureAwait(false);
|
||||
body = body.Trim();
|
||||
|
||||
// var stream = await res.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
|
||||
int firstParen = body.IndexOf('(');
|
||||
int lastParen = body.LastIndexOf(')');
|
||||
|
||||
// var model = await JsonSerializer.DeserializeAsync<TResponse>(stream, json, ct).ConfigureAwait(false)
|
||||
// ?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}.");
|
||||
if (firstParen < 0 || lastParen <= firstParen)
|
||||
throw new InvalidOperationException("Response was not valid JSONP.");
|
||||
|
||||
// return model;
|
||||
//}
|
||||
return body[(firstParen + 1)..lastParen].Trim();
|
||||
}
|
||||
|
||||
//protected virtual void LogRequest(HttpRequestMessage req)
|
||||
// => logger.LogDebug("HTTP {Method} {Uri}", req.Method, req.RequestUri);
|
||||
protected async Task<TResponse> PostJsonAsync<TRequest, TResponse>(
|
||||
string url,
|
||||
TRequest payload,
|
||||
Action<HttpRequestHeaders>? configureHeaders = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
StringContent content = new(JsonSerializer.Serialize(payload, json), Encoding.UTF8, "application/json");
|
||||
|
||||
//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));
|
||||
using HttpRequestMessage request = new(HttpMethod.Post, url)
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
|
||||
//protected static string Truncate(string s, int max) => s.Length <= max ? s : s[..max] + "…";
|
||||
configureHeaders?.Invoke(request.Headers);
|
||||
|
||||
//protected async Task EnsureSuccess(HttpResponseMessage res)
|
||||
//{
|
||||
// if (res.IsSuccessStatusCode) return;
|
||||
LogRequest(request);
|
||||
|
||||
// string body;
|
||||
// try { body = await res.Content.ReadAsStringAsync().ConfigureAwait(false); }
|
||||
// catch { body = "<unable to read body>"; }
|
||||
using HttpResponseMessage response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
await EnsureSuccess(response).ConfigureAwait(false);
|
||||
|
||||
// LogFailure(res, body);
|
||||
Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 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 await JsonSerializer.DeserializeAsync<TResponse>(stream, json, cancellationToken).ConfigureAwait(false)
|
||||
?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}.");
|
||||
}
|
||||
|
||||
protected virtual void LogRequest(HttpRequestMessage request)
|
||||
=> logger.LogDebug("HTTP {Method} {Uri}", request.Method, request.RequestUri);
|
||||
|
||||
protected virtual void LogFailure(HttpResponseMessage response, string body)
|
||||
=> logger.LogWarning("HTTP {Status} for {Uri}. Body: {Body}", (int)response.StatusCode, response.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);
|
||||
}
|
||||
}
|
||||
@@ -206,7 +206,7 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
|
||||
voiceWork.Downloads = ingest.Downloads;
|
||||
voiceWork.WishlistCount = ingest.WishlistCount;
|
||||
voiceWork.HasTrial = ingest.HasTrial;
|
||||
voiceWork.HasChobit = ingest.HasDLPlay;
|
||||
voiceWork.HasChobit = ingest.HasChobit;
|
||||
voiceWork.StarRating = ingest.StarRating;
|
||||
voiceWork.Votes = ingest.Votes;
|
||||
voiceWork.OriginalProductId = ingest.Translation?.OriginalProductId;
|
||||
|
||||
@@ -5,11 +5,17 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JSMR.Infrastructure.Integrations.Chobit;
|
||||
|
||||
public class ChobitClient(IHttpService http, ILogger logger) : ApiClient(http, logger), IChobitClient
|
||||
public class ChobitClient(HttpClient http, ILogger<ChobitClient> logger) : ApiClient(http, logger), IChobitClient
|
||||
{
|
||||
public Task<ChobitWorkResult> GetSampleInfoAsync(string[] productIds, CancellationToken cancellationToken = default)
|
||||
public Task<ChobitResult> GetSampleInfoAsync(string productId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var url = $"api/v2/dlsite/embed?workno_list=${string.Join(",", productIds)}";
|
||||
return GetJsonAsync<ChobitWorkResult>(url, cancellationToken);
|
||||
var url = $"api/v1/dlsite/embed?workno={productId}";
|
||||
return GetJsonpAsync<ChobitResult>(url, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public Task<ChobitResultCollection> GetSampleInfoAsync(string[] productIds, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var url = $"api/v2/dlsite/embed?workno_list={string.Join(",", productIds)}";
|
||||
return GetJsonpAsync<ChobitResultCollection>(url, cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -7,21 +7,19 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JSMR.Infrastructure.Integrations.DLSite;
|
||||
|
||||
public class DLSiteClient(IHttpService http, ILogger<DLSiteClient> logger) : ApiClient(http, logger), IDLSiteClient
|
||||
public class DLSiteClient(HttpClient http, ILogger<DLSiteClient> logger) : ApiClient(http, logger), IDLSiteClient
|
||||
{
|
||||
public async Task<VoiceWorkDetailCollection> GetVoiceWorkDetailsAsync(string[] productIds, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (productIds.Length == 0)
|
||||
return [];
|
||||
|
||||
string productIdCollection = string.Join(",", productIds.Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(productIdCollection))
|
||||
string[] validProductIds = [.. productIds.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()];
|
||||
|
||||
if (validProductIds.Length == 0)
|
||||
return [];
|
||||
|
||||
string productIdCollection = string.Join(",", validProductIds);
|
||||
string url = $"maniax/product/info/ajax?product_id={productIdCollection}&cdn_cache_min=1";
|
||||
|
||||
var productInfoCollection = await GetJsonAsync<ProductInfoCollection>(url, cancellationToken);
|
||||
ProductInfoCollection productInfoCollection = await GetJsonAsync<ProductInfoCollection>(url, cancellationToken: cancellationToken);
|
||||
|
||||
return DLSiteToDomainMapper.Map(productInfoCollection);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user