Added inital job entity and services. Added released works API integration.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
using JSMR.Application.Integrations.Chobit.Models;
|
using JSMR.Application.Integrations.Chobit.Models;
|
||||||
|
|
||||||
namespace JSMR.Application.Integrations.Ports;
|
namespace JSMR.Application.Integrations.Chobit.Ports;
|
||||||
|
|
||||||
public interface IChobitClient
|
public interface IChobitClient
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using JSMR.Application.Integrations.Cien.Models;
|
using JSMR.Application.Integrations.Cien.Models;
|
||||||
|
|
||||||
namespace JSMR.Application.Integrations.Ports;
|
namespace JSMR.Application.Integrations.Cien.Ports;
|
||||||
|
|
||||||
public interface ICienClient
|
public interface ICienClient
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
|
||||||
|
public record ReleasedWork
|
||||||
|
{
|
||||||
|
public required string ProductId { get; init; }
|
||||||
|
public required string Title { get; init; }
|
||||||
|
public required string MaskedTitle { get; init; }
|
||||||
|
public required string Description { get; init; }
|
||||||
|
public required string MaskedDescription { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using JSMR.Application.Enums;
|
||||||
|
|
||||||
|
namespace JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
|
||||||
|
public record ReleasedWorksRequest(
|
||||||
|
Locale Locale,
|
||||||
|
DateOnly Date,
|
||||||
|
int Period
|
||||||
|
);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
|
||||||
|
public class ReleasedWorksCollection : Dictionary<string, ReleasedWork>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
10
JSMR.Application/Integrations/DLSite/Ports/IDLSiteClient.cs
Normal file
10
JSMR.Application/Integrations/DLSite/Ports/IDLSiteClient.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using JSMR.Application.Integrations.DLSite.Models;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
|
||||||
|
namespace JSMR.Application.Integrations.DLSite.Ports;
|
||||||
|
|
||||||
|
public interface IDLSiteClient
|
||||||
|
{
|
||||||
|
Task<VoiceWorkDetailCollection> GetVoiceWorkDetailsAsync(string[] productIds, CancellationToken cancellationToken = default);
|
||||||
|
Task<ReleasedWorksCollection> GetReleasedWorksAsync(ReleasedWorksRequest request, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using JSMR.Application.Integrations.DLSite.Models;
|
|
||||||
|
|
||||||
namespace JSMR.Application.Integrations.Ports;
|
|
||||||
|
|
||||||
public interface IDLSiteClient
|
|
||||||
{
|
|
||||||
Task<VoiceWorkDetailCollection> GetVoiceWorkDetailsAsync(string[] productIds, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
10
JSMR.Application/Jobs/IJobProgressWriter.cs
Normal file
10
JSMR.Application/Jobs/IJobProgressWriter.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace JSMR.Application.Jobs;
|
||||||
|
|
||||||
|
public interface IJobProgressWriter
|
||||||
|
{
|
||||||
|
Task SetStepAsync(int jobId, string step, CancellationToken cancellationToken);
|
||||||
|
Task SetProgressAsync(int jobId, int? current, int? total, CancellationToken cancellationToken);
|
||||||
|
Task SetHeartbeatAsync(int jobId, CancellationToken cancellationToken);
|
||||||
|
Task CompleteAsync(int jobId, string? summary, CancellationToken cancellationToken);
|
||||||
|
Task FailAsync(int jobId, string error, CancellationToken cancellationTokenct);
|
||||||
|
}
|
||||||
15
JSMR.Application/Jobs/IJobRepository.cs
Normal file
15
JSMR.Application/Jobs/IJobRepository.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using JSMR.Domain.Entities;
|
||||||
|
|
||||||
|
namespace JSMR.Application.Jobs;
|
||||||
|
|
||||||
|
public interface IJobRepository
|
||||||
|
{
|
||||||
|
Task<Job> AddAsync(Job job, CancellationToken cancellationToken);
|
||||||
|
Task<Job?> GetByIdAsync(int id, CancellationToken cancellationToken);
|
||||||
|
Task<IReadOnlyList<Job>> GetRecentAsync(int take, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<bool> AnyRunningAsync(CancellationToken cancellationToken);
|
||||||
|
Task<Job?> TryClaimNextQueuedAsync(string workerName, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task SaveChangesAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
using JSMR.Application.Scanning.Contracts;
|
||||||
|
|
||||||
|
namespace JSMR.Application.Scanning.Ports;
|
||||||
|
|
||||||
|
public interface IReleasedWorksProvider
|
||||||
|
{
|
||||||
|
Task<ReleasedWorksCollection> GetReleasedWorksAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using JSMR.Application.Common.Caching;
|
using JSMR.Application.Common.Caching;
|
||||||
using JSMR.Application.Integrations.Chobit.Models;
|
using JSMR.Application.Integrations.Chobit.Models;
|
||||||
|
using JSMR.Application.Integrations.Chobit.Ports;
|
||||||
using JSMR.Application.Integrations.DLSite.Models;
|
using JSMR.Application.Integrations.DLSite.Models;
|
||||||
using JSMR.Application.Integrations.Ports;
|
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Ports;
|
||||||
using JSMR.Application.Scanning.Contracts;
|
using JSMR.Application.Scanning.Contracts;
|
||||||
using JSMR.Application.Scanning.Ports;
|
using JSMR.Application.Scanning.Ports;
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@ public sealed class ScanVoiceWorksHandler(
|
|||||||
IDLSiteClient dlsiteClient,
|
IDLSiteClient dlsiteClient,
|
||||||
IChobitClient chobitClient,
|
IChobitClient chobitClient,
|
||||||
ISpamCircleCache spamCircleCache,
|
ISpamCircleCache spamCircleCache,
|
||||||
|
IReleasedWorksProvider releasedWorksProvider,
|
||||||
IVoiceWorkSearchUpdater searchUpdater)
|
IVoiceWorkSearchUpdater searchUpdater)
|
||||||
{
|
{
|
||||||
public async Task<ScanVoiceWorksResponse> HandleAsync(ScanVoiceWorksRequest request, CancellationToken cancellationToken)
|
public async Task<ScanVoiceWorksResponse> HandleAsync(ScanVoiceWorksRequest request, CancellationToken cancellationToken)
|
||||||
@@ -47,11 +50,14 @@ public sealed class ScanVoiceWorksHandler(
|
|||||||
string[] productIds = [.. scanResult.Works.Where(x => !string.IsNullOrWhiteSpace(x.ProductId)).Select(x => x.ProductId!)];
|
string[] productIds = [.. scanResult.Works.Where(x => !string.IsNullOrWhiteSpace(x.ProductId)).Select(x => x.ProductId!)];
|
||||||
VoiceWorkDetailCollection voiceWorkDetails = await dlsiteClient.GetVoiceWorkDetailsAsync(productIds, cancellationToken);
|
VoiceWorkDetailCollection voiceWorkDetails = await dlsiteClient.GetVoiceWorkDetailsAsync(productIds, cancellationToken);
|
||||||
ChobitResultCollection chobitResults = await chobitClient.GetSampleInfoAsync(productIds, cancellationToken);
|
ChobitResultCollection chobitResults = await chobitClient.GetSampleInfoAsync(productIds, cancellationToken);
|
||||||
|
ReleasedWorksCollection releasedWorkCollection = await releasedWorksProvider.GetReleasedWorksAsync(scanResult, cancellationToken);
|
||||||
|
|
||||||
VoiceWorkIngest[] ingests = [.. scanResult.Works.Select(work =>
|
VoiceWorkIngest[] ingests = [.. scanResult.Works.Select(work =>
|
||||||
{
|
{
|
||||||
voiceWorkDetails.TryGetValue(work.ProductId!, out VoiceWorkDetails? value);
|
voiceWorkDetails.TryGetValue(work.ProductId!, out VoiceWorkDetails? value);
|
||||||
chobitResults.TryGetValue(work.ProductId, out ChobitResult? chobit);
|
chobitResults.TryGetValue(work.ProductId, out ChobitResult? chobit);
|
||||||
|
releasedWorkCollection.TryGetValue(work.ProductId, out ReleasedWork? releasedWork);
|
||||||
|
|
||||||
return VoiceWorkIngest.From(work, value, chobit);
|
return VoiceWorkIngest.From(work, value, chobit);
|
||||||
})];
|
})];
|
||||||
|
|
||||||
|
|||||||
33
JSMR.Domain/Entities/Job.cs
Normal file
33
JSMR.Domain/Entities/Job.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using JSMR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace JSMR.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class Job
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public string Code { get; set; } = null!;
|
||||||
|
public JobStatus Status { get; set; }
|
||||||
|
|
||||||
|
public string? RequestedByUserId { get; set; }
|
||||||
|
public string RequestedSource { get; set; } = "Manual";
|
||||||
|
|
||||||
|
public string? ParametersJson { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedUtc { get; set; }
|
||||||
|
public DateTime? StartedUtc { get; set; }
|
||||||
|
public DateTime? CompletedUtc { get; set; }
|
||||||
|
public DateTime? HeartbeatUtc { get; set; }
|
||||||
|
|
||||||
|
public string? WorkerName { get; set; }
|
||||||
|
public string? CurrentStep { get; set; }
|
||||||
|
|
||||||
|
public int? ProgressCurrent { get; set; }
|
||||||
|
public int? ProgressTotal { get; set; }
|
||||||
|
|
||||||
|
public string? ResultSummary { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
|
||||||
|
public bool CancellationRequested { get; set; }
|
||||||
|
public int AttemptCount { get; set; }
|
||||||
|
}
|
||||||
10
JSMR.Domain/Enums/JobStatus.cs
Normal file
10
JSMR.Domain/Enums/JobStatus.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace JSMR.Domain.Enums;
|
||||||
|
|
||||||
|
public enum JobStatus
|
||||||
|
{
|
||||||
|
Queued = 0,
|
||||||
|
Running = 1,
|
||||||
|
Succeeded = 2,
|
||||||
|
Failed = 3,
|
||||||
|
Cancelled = 4
|
||||||
|
}
|
||||||
@@ -5,7 +5,9 @@ using JSMR.Application.Common.Caching;
|
|||||||
using JSMR.Application.Creators.Ports;
|
using JSMR.Application.Creators.Ports;
|
||||||
using JSMR.Application.Creators.Queries.Search.Ports;
|
using JSMR.Application.Creators.Queries.Search.Ports;
|
||||||
using JSMR.Application.Enums;
|
using JSMR.Application.Enums;
|
||||||
using JSMR.Application.Integrations.Ports;
|
using JSMR.Application.Integrations.Chobit.Ports;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Ports;
|
||||||
|
using JSMR.Application.Jobs;
|
||||||
using JSMR.Application.Scanning.Ports;
|
using JSMR.Application.Scanning.Ports;
|
||||||
using JSMR.Application.Tags.Ports;
|
using JSMR.Application.Tags.Ports;
|
||||||
using JSMR.Application.Tags.Queries.Search.Ports;
|
using JSMR.Application.Tags.Queries.Search.Ports;
|
||||||
@@ -19,6 +21,7 @@ using JSMR.Infrastructure.Common.SupportedLanguages;
|
|||||||
using JSMR.Infrastructure.Common.Time;
|
using JSMR.Infrastructure.Common.Time;
|
||||||
using JSMR.Infrastructure.Data.Repositories.Circles;
|
using JSMR.Infrastructure.Data.Repositories.Circles;
|
||||||
using JSMR.Infrastructure.Data.Repositories.Creators;
|
using JSMR.Infrastructure.Data.Repositories.Creators;
|
||||||
|
using JSMR.Infrastructure.Data.Repositories.Jobs;
|
||||||
using JSMR.Infrastructure.Data.Repositories.Tags;
|
using JSMR.Infrastructure.Data.Repositories.Tags;
|
||||||
using JSMR.Infrastructure.Data.Repositories.Users;
|
using JSMR.Infrastructure.Data.Repositories.Users;
|
||||||
using JSMR.Infrastructure.Data.Repositories.VoiceWorks;
|
using JSMR.Infrastructure.Data.Repositories.VoiceWorks;
|
||||||
@@ -50,6 +53,8 @@ public static class InfrastructureServiceCollectionExtensions
|
|||||||
services.AddKeyedScoped<IVoiceWorksScanner, EnglishVoiceWorksScanner>(Locale.English);
|
services.AddKeyedScoped<IVoiceWorksScanner, EnglishVoiceWorksScanner>(Locale.English);
|
||||||
services.AddScoped<IVoiceWorkScannerRepository, VoiceWorkScannerRepository>();
|
services.AddScoped<IVoiceWorkScannerRepository, VoiceWorkScannerRepository>();
|
||||||
|
|
||||||
|
services.AddScoped<IReleasedWorksProvider, ReleasedWorksProvider>();
|
||||||
|
|
||||||
services.AddKeyedScoped<IVoiceWorkUpdater, VoiceWorkUpdater>(Locale.Japanese);
|
services.AddKeyedScoped<IVoiceWorkUpdater, VoiceWorkUpdater>(Locale.Japanese);
|
||||||
services.AddKeyedScoped<IVoiceWorkUpdater, EnglishVoiceWorkUpdater>(Locale.English);
|
services.AddKeyedScoped<IVoiceWorkUpdater, EnglishVoiceWorkUpdater>(Locale.English);
|
||||||
services.AddScoped<IVoiceWorkUpdaterRepository, VoiceWorkUpdaterRepository>();
|
services.AddScoped<IVoiceWorkUpdaterRepository, VoiceWorkUpdaterRepository>();
|
||||||
@@ -68,6 +73,9 @@ public static class InfrastructureServiceCollectionExtensions
|
|||||||
services.AddScoped<ICreatorSearchProvider, CreatorSearchProvider>();
|
services.AddScoped<ICreatorSearchProvider, CreatorSearchProvider>();
|
||||||
services.AddScoped<ICreatorWriter, CreatorWriter>();
|
services.AddScoped<ICreatorWriter, CreatorWriter>();
|
||||||
|
|
||||||
|
services.AddScoped<IJobRepository, JobRepository>();
|
||||||
|
services.AddScoped<IJobProgressWriter, JobProgressWriter>();
|
||||||
|
|
||||||
services.AddSingleton<ICache, MemoryCacheAdapter>();
|
services.AddSingleton<ICache, MemoryCacheAdapter>();
|
||||||
services.AddSingleton<ISpamCircleCache, SpamCircleCache>();
|
services.AddSingleton<ISpamCircleCache, SpamCircleCache>();
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
|||||||
public DbSet<Series> Series { get; set; }
|
public DbSet<Series> Series { get; set; }
|
||||||
public DbSet<VoiceWorkSearch> VoiceWorkSearches { get; set; }
|
public DbSet<VoiceWorkSearch> VoiceWorkSearches { get; set; }
|
||||||
public DbSet<User> Users { get; set; }
|
public DbSet<User> Users { get; set; }
|
||||||
|
public DbSet<Job> Jobs { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
|||||||
44
JSMR.Infrastructure/Data/Configuration/JobConfiguration.cs
Normal file
44
JSMR.Infrastructure/Data/Configuration/JobConfiguration.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using JSMR.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Data.Configuration;
|
||||||
|
|
||||||
|
public sealed class JobConfiguration : IEntityTypeConfiguration<Job>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Job> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("Jobs");
|
||||||
|
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
|
||||||
|
builder.Property(x => x.Code)
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(x => x.RequestedByUserId)
|
||||||
|
.HasMaxLength(100);
|
||||||
|
|
||||||
|
builder.Property(x => x.RequestedSource)
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(x => x.WorkerName)
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
builder.Property(x => x.CurrentStep)
|
||||||
|
.HasMaxLength(500);
|
||||||
|
|
||||||
|
builder.Property(x => x.ResultSummary)
|
||||||
|
.HasMaxLength(2000);
|
||||||
|
|
||||||
|
builder.Property(x => x.Error)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
builder.Property(x => x.ParametersJson)
|
||||||
|
.HasColumnType("LONGTEXT");
|
||||||
|
|
||||||
|
builder.HasIndex(x => new { x.Status, x.CreatedUtc });
|
||||||
|
builder.HasIndex(x => x.Code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using JSMR.Application.Jobs;
|
||||||
|
using JSMR.Domain.Entities;
|
||||||
|
using JSMR.Domain.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Data.Repositories.Jobs;
|
||||||
|
|
||||||
|
public sealed class JobProgressWriter(AppDbContext dbContext) : IJobProgressWriter
|
||||||
|
{
|
||||||
|
public async Task SetStepAsync(int jobId, string step, CancellationToken canellationToken)
|
||||||
|
{
|
||||||
|
Job? job = await dbContext.Jobs.FirstOrDefaultAsync(x => x.Id == jobId, canellationToken);
|
||||||
|
|
||||||
|
if (job is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
job.CurrentStep = step;
|
||||||
|
job.HeartbeatUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(canellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetProgressAsync(int jobId, int? current, int? total, CancellationToken canellationToken)
|
||||||
|
{
|
||||||
|
Job? job = await dbContext.Jobs.FirstOrDefaultAsync(x => x.Id == jobId, canellationToken);
|
||||||
|
|
||||||
|
if (job is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
job.ProgressCurrent = current;
|
||||||
|
job.ProgressTotal = total;
|
||||||
|
job.HeartbeatUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(canellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetHeartbeatAsync(int jobId, CancellationToken canellationToken)
|
||||||
|
{
|
||||||
|
Job? job = await dbContext.Jobs.FirstOrDefaultAsync(x => x.Id == jobId, canellationToken);
|
||||||
|
|
||||||
|
if (job is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
job.HeartbeatUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(canellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CompleteAsync(int jobId, string? summary, CancellationToken canellationToken)
|
||||||
|
{
|
||||||
|
Job? job = await dbContext.Jobs.FirstOrDefaultAsync(x => x.Id == jobId, canellationToken);
|
||||||
|
|
||||||
|
if (job is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
job.Status = JobStatus.Succeeded;
|
||||||
|
job.CompletedUtc = DateTime.UtcNow;
|
||||||
|
job.HeartbeatUtc = DateTime.UtcNow;
|
||||||
|
job.ResultSummary = summary;
|
||||||
|
job.CurrentStep = "Completed";
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(canellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task FailAsync(int jobId, string error, CancellationToken canellationToken)
|
||||||
|
{
|
||||||
|
Job? job = await dbContext.Jobs.FirstOrDefaultAsync(x => x.Id == jobId, canellationToken);
|
||||||
|
|
||||||
|
if (job is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
job.Status = JobStatus.Failed;
|
||||||
|
job.CompletedUtc = DateTime.UtcNow;
|
||||||
|
job.HeartbeatUtc = DateTime.UtcNow;
|
||||||
|
job.Error = error;
|
||||||
|
job.CurrentStep = "Failed";
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(canellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
JSMR.Infrastructure/Data/Repositories/Jobs/JobRepository.cs
Normal file
52
JSMR.Infrastructure/Data/Repositories/Jobs/JobRepository.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using JSMR.Application.Jobs;
|
||||||
|
using JSMR.Domain.Entities;
|
||||||
|
using JSMR.Domain.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Data.Repositories.Jobs;
|
||||||
|
|
||||||
|
public sealed class JobRepository(AppDbContext dbContext) : IJobRepository
|
||||||
|
{
|
||||||
|
public async Task<Job> AddAsync(Job job, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await dbContext.Jobs.AddAsync(job, cancellationToken);
|
||||||
|
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Job?> GetByIdAsync(int id, CancellationToken cancellationToken)
|
||||||
|
=> dbContext.Jobs.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Job>> GetRecentAsync(int take, CancellationToken cancellationToken)
|
||||||
|
=> await dbContext.Jobs
|
||||||
|
.OrderByDescending(x => x.CreatedUtc)
|
||||||
|
.Take(take)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
public Task<bool> AnyRunningAsync(CancellationToken cancellationToken)
|
||||||
|
=> dbContext.Jobs.AnyAsync(x => x.Status == JobStatus.Running, cancellationToken);
|
||||||
|
|
||||||
|
public async Task<Job?> TryClaimNextQueuedAsync(string workerName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Job? next = await dbContext.Jobs
|
||||||
|
.Where(x => x.Status == JobStatus.Queued)
|
||||||
|
.OrderBy(x => x.CreatedUtc)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (next is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
next.Status = JobStatus.Running;
|
||||||
|
next.StartedUtc = DateTime.UtcNow;
|
||||||
|
next.HeartbeatUtc = DateTime.UtcNow;
|
||||||
|
next.WorkerName = workerName;
|
||||||
|
next.AttemptCount += 1;
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SaveChangesAsync(CancellationToken ct)
|
||||||
|
=> dbContext.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
48
JSMR.Infrastructure/Globalization/LocaleMap.cs
Normal file
48
JSMR.Infrastructure/Globalization/LocaleMap.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using JSMR.Application.Enums;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Globalization;
|
||||||
|
|
||||||
|
internal class LocaleMapper
|
||||||
|
{
|
||||||
|
// TODO: Deprecate
|
||||||
|
public static readonly IReadOnlyDictionary<Locale, (string Abbreviation, string Code)> Map =
|
||||||
|
new Dictionary<Locale, (string, string)>
|
||||||
|
{
|
||||||
|
{ Locale.Japanese, ("jp", "ja_JP") },
|
||||||
|
{ Locale.English, ("en", "en_US") },
|
||||||
|
{ Locale.ChineseSimplified, ("zh-cn", "zh_CN") },
|
||||||
|
{ Locale.ChineseTraditional, ("zh-tw", "zh_TW") },
|
||||||
|
{ Locale.Korean, ("ko", "ko_KR") },
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public static string ToDLSiteLocale(Locale locale) => locale switch
|
||||||
|
{
|
||||||
|
Locale.Japanese => "ja-jp",
|
||||||
|
Locale.English => "en-us",
|
||||||
|
Locale.ChineseSimplified => "zh-cn",
|
||||||
|
Locale.ChineseTraditional => "zh-tw",
|
||||||
|
Locale.Korean => "ko-kr",
|
||||||
|
_ => throw new NotSupportedException($"Locale '{locale}' is not supported.")
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string ToDLSiteApiLocale(Locale locale) => locale switch
|
||||||
|
{
|
||||||
|
Locale.Japanese => "ja_JP",
|
||||||
|
Locale.English => "en_US",
|
||||||
|
Locale.ChineseSimplified => "zh_CN",
|
||||||
|
Locale.ChineseTraditional => "zh_TW",
|
||||||
|
Locale.Korean => "ko_KR",
|
||||||
|
_ => throw new NotSupportedException($"Locale '{locale}' is not supported.")
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string ToAbbreviation(Locale locale) => locale switch
|
||||||
|
{
|
||||||
|
Locale.Japanese => "jp",
|
||||||
|
Locale.English => "en",
|
||||||
|
Locale.ChineseSimplified => "zh-cn",
|
||||||
|
Locale.ChineseTraditional => "zh-tw",
|
||||||
|
Locale.Korean => "ko",
|
||||||
|
_ => throw new NotSupportedException($"Locale '{locale}' is not supported.")
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using JSMR.Application.Integrations.Chobit.Models;
|
using JSMR.Application.Integrations.Chobit.Models;
|
||||||
using JSMR.Application.Integrations.Ports;
|
using JSMR.Application.Integrations.Chobit.Ports;
|
||||||
using JSMR.Infrastructure.Http;
|
using JSMR.Infrastructure.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
using JSMR.Application.Integrations.DLSite.Models;
|
using JSMR.Application.Integrations.DLSite.Models;
|
||||||
using JSMR.Application.Integrations.Ports;
|
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Ports;
|
||||||
|
using JSMR.Infrastructure.Globalization;
|
||||||
using JSMR.Infrastructure.Http;
|
using JSMR.Infrastructure.Http;
|
||||||
using JSMR.Infrastructure.Integrations.DLSite.Mapping;
|
using JSMR.Infrastructure.Integrations.DLSite.Mapping;
|
||||||
using JSMR.Infrastructure.Integrations.DLSite.Models;
|
using JSMR.Infrastructure.Integrations.DLSite.Models;
|
||||||
|
using JSMR.Infrastructure.Integrations.DLSite.Models.NewWorks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace JSMR.Infrastructure.Integrations.DLSite;
|
namespace JSMR.Infrastructure.Integrations.DLSite;
|
||||||
@@ -23,4 +26,23 @@ public class DLSiteClient(HttpClient http, ILogger<DLSiteClient> logger) : ApiCl
|
|||||||
|
|
||||||
return DLSiteToDomainMapper.Map(productInfoCollection);
|
return DLSiteToDomainMapper.Map(productInfoCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ReleasedWorksCollection> GetReleasedWorksAsync(ReleasedWorksRequest request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
string locale = LocaleMapper.ToDLSiteLocale(request.Locale);
|
||||||
|
string date = request.Date.ToString("yyyy-MM-dd");
|
||||||
|
string url = $"maniax/new/work/api?locale={locale}&date={date}&period={request.Period}";
|
||||||
|
|
||||||
|
NewWorksApiResponse response = await GetJsonAsync<NewWorksApiResponse>(url, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
if (response.Meta.Code != 200)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"DLsite returned code {response.Meta.Code}. " +
|
||||||
|
$"ErrorType: {response.Meta.ErrorType}. " +
|
||||||
|
$"ErrorMessage: {response.Meta.ErrorMessage}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return DLSiteReleasedWorksMapper.Map(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using JSMR.Application.Integrations.Ports;
|
//using JSMR.Application.Integrations.Ports;
|
||||||
using JSMR.Infrastructure.Integrations.DLSite.Serialization;
|
//using JSMR.Infrastructure.Integrations.DLSite.Serialization;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
//using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace JSMR.Infrastructure.Integrations.DLSite;
|
//namespace JSMR.Infrastructure.Integrations.DLSite;
|
||||||
|
|
||||||
public static class DLSiteClientRegistration
|
public static class DLSiteClientRegistration
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
using JSMR.Infrastructure.Integrations.DLSite.Models.NewWorks;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Integrations.DLSite.Mapping;
|
||||||
|
|
||||||
|
public static class DLSiteReleasedWorksMapper
|
||||||
|
{
|
||||||
|
public static ReleasedWorksCollection Map(NewWorksApiResponse response)
|
||||||
|
{
|
||||||
|
ReleasedWorksCollection result = [];
|
||||||
|
|
||||||
|
if (response.Data.Products.Length == 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
foreach (NewWorksApiProduct product in response.Data.Products)
|
||||||
|
{
|
||||||
|
result.Add(product.Id, Map(product));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReleasedWork Map(NewWorksApiProduct product)
|
||||||
|
{
|
||||||
|
return new ReleasedWork
|
||||||
|
{
|
||||||
|
ProductId = product.Id,
|
||||||
|
Title = product.Name ?? string.Empty,
|
||||||
|
MaskedTitle = product.NameMasked ?? string.Empty,
|
||||||
|
Description = product.Description ?? string.Empty,
|
||||||
|
MaskedDescription = product.DescriptionMasked ?? string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Integrations.DLSite.Models.NewWorks;
|
||||||
|
|
||||||
|
public record NewWorksApiData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("products")]
|
||||||
|
public required NewWorksApiProduct[] Products { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Integrations.DLSite.Models.NewWorks;
|
||||||
|
|
||||||
|
public record NewWorksApiMeta
|
||||||
|
{
|
||||||
|
[JsonPropertyName("code")]
|
||||||
|
public int Code { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("errorMessage")]
|
||||||
|
public string ErrorMessage { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("errorType")]
|
||||||
|
public string ErrorType { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Integrations.DLSite.Models.NewWorks;
|
||||||
|
|
||||||
|
public record NewWorksApiProduct
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public required string Id { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string? Name { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("nameMasked")]
|
||||||
|
public string? NameMasked { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("description")]
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("descriptionMasked")]
|
||||||
|
public string? DescriptionMasked { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Integrations.DLSite.Models.NewWorks;
|
||||||
|
|
||||||
|
public record NewWorksApiResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("meta")]
|
||||||
|
public required NewWorksApiMeta Meta { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public required NewWorksApiData Data { get; init; }
|
||||||
|
}
|
||||||
@@ -35,4 +35,8 @@
|
|||||||
<ProjectReference Include="..\JSMR.Domain\JSMR.Domain.csproj" />
|
<ProjectReference Include="..\JSMR.Domain\JSMR.Domain.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Jobs\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using JSMR.Application.Enums;
|
using JSMR.Application.Enums;
|
||||||
using JSMR.Domain.ValueObjects;
|
using JSMR.Domain.ValueObjects;
|
||||||
|
using JSMR.Infrastructure.Globalization;
|
||||||
|
|
||||||
namespace JSMR.Infrastructure.Scanning;
|
namespace JSMR.Infrastructure.Scanning;
|
||||||
|
|
||||||
@@ -90,7 +91,10 @@ public sealed class DLSiteSearchFilterBuilder
|
|||||||
|
|
||||||
public string BuildSearchQuery(int pageNumber, int pageSize)
|
public string BuildSearchQuery(int pageNumber, int pageSize)
|
||||||
{
|
{
|
||||||
var (localeAbbreviation, localeCode) = LocaleMap.Map[_locale];
|
//string localeAbbreviation = LocaleMapper.ToAbbreviation(_locale);
|
||||||
|
//string localeCode = LocaleMapper.ToDLSiteLocale(_locale);
|
||||||
|
|
||||||
|
var (localeAbbreviation, localeCode) = LocaleMapper.Map[_locale];
|
||||||
|
|
||||||
using (var writer = new StringWriter())
|
using (var writer = new StringWriter())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
using JSMR.Application.Enums;
|
|
||||||
|
|
||||||
namespace JSMR.Infrastructure.Scanning;
|
|
||||||
|
|
||||||
internal class LocaleMap
|
|
||||||
{
|
|
||||||
public static readonly IReadOnlyDictionary<Locale, (string Abbreviation, string Code)> Map =
|
|
||||||
new Dictionary<Locale, (string, string)>
|
|
||||||
{
|
|
||||||
{ Locale.Japanese, ("jp", "ja_JP") },
|
|
||||||
{ Locale.English, ("en", "en_US") },
|
|
||||||
{ Locale.ChineseSimplified, ("zh-cn", "zh_CN") },
|
|
||||||
{ Locale.ChineseTraditional, ("zh-tw", "zh_TW") },
|
|
||||||
{ Locale.Korean, ("ko", "ko_KR") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
39
JSMR.Infrastructure/Scanning/ReleasedWorksProvider.cs
Normal file
39
JSMR.Infrastructure/Scanning/ReleasedWorksProvider.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using JSMR.Application.Enums;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Ports;
|
||||||
|
using JSMR.Application.Scanning.Contracts;
|
||||||
|
using JSMR.Application.Scanning.Ports;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Scanning;
|
||||||
|
|
||||||
|
public class ReleasedWorksProvider(IDLSiteClient dlsiteClient) : IReleasedWorksProvider
|
||||||
|
{
|
||||||
|
public async Task<ReleasedWorksCollection> GetReleasedWorksAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
DateOnly[] salesDates =
|
||||||
|
[
|
||||||
|
.. scanResult.Works
|
||||||
|
.Where(x => x.SalesDate.HasValue)
|
||||||
|
.Select(x => x.SalesDate!.Value)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (salesDates.Length == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
DateOnly minDate = salesDates.Min();
|
||||||
|
DateOnly maxDate = salesDates.Max();
|
||||||
|
|
||||||
|
DateOnly requestDate = minDate.AddDays(-1);
|
||||||
|
DateOnly requestEndDate = maxDate.AddDays(1);
|
||||||
|
|
||||||
|
int period = (requestEndDate.DayNumber - requestDate.DayNumber) + 1;
|
||||||
|
|
||||||
|
ReleasedWorksRequest releasedWorksRequest = new(
|
||||||
|
Locale: Locale.English,
|
||||||
|
Date: requestDate,
|
||||||
|
Period: period
|
||||||
|
);
|
||||||
|
|
||||||
|
return await dlsiteClient.GetReleasedWorksAsync(releasedWorksRequest, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,60 @@
|
|||||||
namespace JSMR.Tests.Http;
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
|
namespace JSMR.Tests.Http;
|
||||||
|
|
||||||
internal sealed class FakeHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> handler) : HttpMessageHandler
|
internal sealed class FakeHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> handler) : HttpMessageHandler
|
||||||
{
|
{
|
||||||
|
public List<HttpRequestMessage> Requests { get; } = [];
|
||||||
|
|
||||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
Requests.Add(request);
|
||||||
|
|
||||||
return Task.FromResult(handler(request));
|
return Task.FromResult(handler(request));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class HttpRequestMessageAssertions
|
||||||
|
{
|
||||||
|
public static void ShouldHavePath(this HttpRequestMessage request, string expectedPath)
|
||||||
|
{
|
||||||
|
request.RequestUri.ShouldNotBeNull();
|
||||||
|
request.RequestUri!.AbsolutePath.ShouldBe(expectedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ShouldHaveQueryParam(this HttpRequestMessage request, string key, string expectedValue)
|
||||||
|
{
|
||||||
|
request.RequestUri.ShouldNotBeNull();
|
||||||
|
|
||||||
|
var query = QueryHelpers.ParseQuery(request.RequestUri!.Query);
|
||||||
|
query[key].ToString().ShouldBe(expectedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//internal sealed class FakeHttpMessageHandler : HttpMessageHandler
|
||||||
|
//{
|
||||||
|
// private readonly Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> _handler;
|
||||||
|
|
||||||
|
// public List<HttpRequestMessage> Requests { get; } = [];
|
||||||
|
|
||||||
|
// public FakeHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> handler)
|
||||||
|
// {
|
||||||
|
// _handler = handler;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public FakeHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> handler)
|
||||||
|
// : this((request, _) => handler(request))
|
||||||
|
// {
|
||||||
|
// }
|
||||||
|
|
||||||
|
// protected override Task<HttpResponseMessage> SendAsync(
|
||||||
|
// HttpRequestMessage request,
|
||||||
|
// CancellationToken cancellationToken)
|
||||||
|
// {
|
||||||
|
// Requests.Add(request);
|
||||||
|
|
||||||
|
// HttpResponseMessage response = _handler(request, cancellationToken);
|
||||||
|
// return Task.FromResult(response);
|
||||||
|
// }
|
||||||
|
//}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
using JSMR.Application.Integrations.DLSite.Models;
|
using JSMR.Application.Enums;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Models;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
using JSMR.Domain.Enums;
|
using JSMR.Domain.Enums;
|
||||||
using JSMR.Domain.ValueObjects;
|
using JSMR.Domain.ValueObjects;
|
||||||
using JSMR.Infrastructure.Integrations.DLSite;
|
using JSMR.Infrastructure.Integrations.DLSite;
|
||||||
using JSMR.Infrastructure.Integrations.DLSite.Mapping;
|
using JSMR.Infrastructure.Integrations.DLSite.Mapping;
|
||||||
using JSMR.Infrastructure.Integrations.DLSite.Models;
|
using JSMR.Infrastructure.Integrations.DLSite.Models;
|
||||||
|
using JSMR.Infrastructure.Integrations.DLSite.Models.NewWorks;
|
||||||
using JSMR.Tests.Http;
|
using JSMR.Tests.Http;
|
||||||
using JSMR.Tests.Utilities;
|
using JSMR.Tests.Utilities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -21,11 +24,11 @@ public class DLSiteClientTests
|
|||||||
return await ResourceHelper.ReadAsync($"JSMR.Tests.Integrations.DLSite.{resourceName}");
|
return await ResourceHelper.ReadAsync($"JSMR.Tests.Integrations.DLSite.{resourceName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<DLSiteClient> GetDLSiteClientThatReturns(string resourceName)
|
private static async Task<(DLSiteClient Client, FakeHttpMessageHandler Handler)> GetDLSiteClientThatReturns(string resourceName)
|
||||||
{
|
{
|
||||||
string content = await ReadJsonResourceAsync(resourceName);
|
string content = await ReadJsonResourceAsync(resourceName);
|
||||||
|
|
||||||
FakeHttpMessageHandler handler = new(request =>
|
FakeHttpMessageHandler handler = new(_ =>
|
||||||
{
|
{
|
||||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
{
|
{
|
||||||
@@ -41,13 +44,27 @@ public class DLSiteClientTests
|
|||||||
var logger = Substitute.For<ILogger<DLSiteClient>>();
|
var logger = Substitute.For<ILogger<DLSiteClient>>();
|
||||||
var client = new DLSiteClient(httpClient, logger);
|
var client = new DLSiteClient(httpClient, logger);
|
||||||
|
|
||||||
return client;
|
return (client, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetVoiceWorkDetailsAsync_Should_Call_Expected_Request()
|
||||||
|
{
|
||||||
|
var (client, handler) = await GetDLSiteClientThatReturns("Product-Info.json");
|
||||||
|
await client.GetVoiceWorkDetailsAsync(["RJ01230163", "RJ01536422"], CancellationToken.None);
|
||||||
|
|
||||||
|
handler.Requests.Count.ShouldBe(1);
|
||||||
|
|
||||||
|
HttpRequestMessage httpRequest = handler.Requests[0];
|
||||||
|
httpRequest.Method.ShouldBe(HttpMethod.Get);
|
||||||
|
httpRequest.ShouldHavePath("/maniax/product/info/ajax");
|
||||||
|
httpRequest.ShouldHaveQueryParam("product_id", "RJ01230163,RJ01536422");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Deserialize_Product_Info_Collection()
|
public async Task Deserialize_Product_Info_Collection()
|
||||||
{
|
{
|
||||||
var client = await GetDLSiteClientThatReturns("Product-Info.json");
|
var (client, handler) = await GetDLSiteClientThatReturns("Product-Info.json");
|
||||||
var result = await client.GetVoiceWorkDetailsAsync(["RJ01230163", "RJ01536422"], CancellationToken.None);
|
var result = await client.GetVoiceWorkDetailsAsync(["RJ01230163", "RJ01536422"], CancellationToken.None);
|
||||||
|
|
||||||
result.Count.ShouldBe(2);
|
result.Count.ShouldBe(2);
|
||||||
@@ -144,4 +161,78 @@ public class DLSiteClientTests
|
|||||||
secondVoiceWorkDetails.Translation.Language.ShouldBe(Language.English);
|
secondVoiceWorkDetails.Translation.Language.ShouldBe(Language.English);
|
||||||
secondVoiceWorkDetails.Translation.Kind.ShouldBe(TranslationKind.Official | TranslationKind.Recommended);
|
secondVoiceWorkDetails.Translation.Kind.ShouldBe(TranslationKind.Official | TranslationKind.Recommended);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetReleasedWorksAsync_Should_Call_Expected_Request()
|
||||||
|
{
|
||||||
|
var (client, handler) = await GetDLSiteClientThatReturns("Released-Works.json");
|
||||||
|
|
||||||
|
ReleasedWorksRequest request = new(Locale.English, new(2025, 1, 1), 1);
|
||||||
|
|
||||||
|
await client.GetReleasedWorksAsync(request, CancellationToken.None);
|
||||||
|
|
||||||
|
handler.Requests.Count.ShouldBe(1);
|
||||||
|
|
||||||
|
HttpRequestMessage httpRequest = handler.Requests[0];
|
||||||
|
httpRequest.Method.ShouldBe(HttpMethod.Get);
|
||||||
|
httpRequest.ShouldHavePath("/maniax/new/work/api");
|
||||||
|
httpRequest.ShouldHaveQueryParam("locale", "en-us");
|
||||||
|
httpRequest.ShouldHaveQueryParam("date", "2025-01-01");
|
||||||
|
httpRequest.ShouldHaveQueryParam("period", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Map_Basic_Released_Work_Collection()
|
||||||
|
{
|
||||||
|
NewWorksApiResponse response = new()
|
||||||
|
{
|
||||||
|
Meta = new()
|
||||||
|
{
|
||||||
|
Code = 200
|
||||||
|
},
|
||||||
|
Data = new()
|
||||||
|
{
|
||||||
|
Products =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = "RG1",
|
||||||
|
Name = "The Title",
|
||||||
|
NameMasked = "Masked Title",
|
||||||
|
Description = "The description",
|
||||||
|
DescriptionMasked = "The masked description"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ReleasedWorksCollection collection = DLSiteReleasedWorksMapper.Map(response);
|
||||||
|
collection.Count.ShouldBe(1);
|
||||||
|
collection.ShouldContainKey("RG1");
|
||||||
|
|
||||||
|
ReleasedWork releasedWork = collection["RG1"];
|
||||||
|
releasedWork.ProductId.ShouldBe("RG1");
|
||||||
|
releasedWork.Title.ShouldBe("The Title");
|
||||||
|
releasedWork.MaskedTitle.ShouldBe("Masked Title");
|
||||||
|
releasedWork.Description.ShouldBe("The description");
|
||||||
|
releasedWork.MaskedDescription.ShouldBe("The masked description");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Deserialize_Released_Work_Collection()
|
||||||
|
{
|
||||||
|
var (client, handler) = await GetDLSiteClientThatReturns("Released-Works.json");
|
||||||
|
var request = new ReleasedWorksRequest(Locale.English, new(2025, 1, 1), 1);
|
||||||
|
var result = await client.GetReleasedWorksAsync(request, CancellationToken.None);
|
||||||
|
|
||||||
|
result.Count.ShouldBe(13);
|
||||||
|
|
||||||
|
result.ShouldContainKey("RJ01588345");
|
||||||
|
|
||||||
|
ReleasedWork releasedWork = result["RJ01588345"];
|
||||||
|
releasedWork.Title.ShouldBe("魔都一贅肉のスゴイデブ");
|
||||||
|
releasedWork.MaskedTitle.ShouldBe("魔都一贅肉のスゴイデブ");
|
||||||
|
releasedWork.Description.ShouldBe("圧巻の超連続・激太りご褒美パラダイス、ここに!全24ページ描き下ろし!太→激太への肥満化シークエンスが11作収録!餌付け、自己肥育、お風呂、縛り…などなど多種多様の太り方でご褒美を堪能せよ!");
|
||||||
|
releasedWork.MaskedDescription.ShouldBe("圧巻の超連続・激太りご褒美パラダイス、ここに!全24ページ描き下ろし!太→激太への肥満化シークエンスが11作収録!餌付け、自己肥育、お風呂、縛り…などなど多種多様の太り方でご褒美を堪能せよ!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
4922
JSMR.Tests/Integrations/DLSite/Released-Works.json
Normal file
4922
JSMR.Tests/Integrations/DLSite/Released-Works.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@
|
|||||||
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result-No-Data.jsonp" />
|
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result-No-Data.jsonp" />
|
||||||
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result-Collection.jsonp" />
|
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result-Collection.jsonp" />
|
||||||
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result.jsonp" />
|
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result.jsonp" />
|
||||||
|
<EmbeddedResource Include="Integrations\DLSite\Released-Works.json" />
|
||||||
<EmbeddedResource Include="Integrations\DLSite\Product-Info.json" />
|
<EmbeddedResource Include="Integrations\DLSite\Product-Info.json" />
|
||||||
<EmbeddedResource Include="Scanning\English-Page-Updated.html" />
|
<EmbeddedResource Include="Scanning\English-Page-Updated.html" />
|
||||||
<EmbeddedResource Include="Scanning\Japanese-Page-Updated.html" />
|
<EmbeddedResource Include="Scanning\Japanese-Page-Updated.html" />
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.5" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using JSMR.Application.Integrations.Chobit.Models;
|
using JSMR.Application.Integrations.Chobit.Models;
|
||||||
using JSMR.Application.Integrations.DLSite.Models;
|
using JSMR.Application.Integrations.DLSite.Models;
|
||||||
using JSMR.Application.Integrations.Ports;
|
using JSMR.Application.Integrations.DLSite.Ports;
|
||||||
using JSMR.Application.Scanning.Contracts;
|
using JSMR.Application.Scanning.Contracts;
|
||||||
using JSMR.Application.Scanning.Ports;
|
using JSMR.Application.Scanning.Ports;
|
||||||
using JSMR.Infrastructure.Http;
|
using JSMR.Infrastructure.Http;
|
||||||
|
|||||||
Reference in New Issue
Block a user