Added English localization logic during the regular scan process.
All checks were successful
ci / build-test (push) Successful in 2m17s
ci / publish-image (push) Successful in 1m41s

This commit is contained in:
2026-03-29 21:24:04 -04:00
parent d9e421178f
commit 0dd11e6351
11 changed files with 166 additions and 26 deletions

View File

@@ -29,6 +29,7 @@ public sealed record VoiceWorkIngest
public AIGeneration AI { get; init; } public AIGeneration AI { get; init; }
public VoiceWorkSeries? Series { get; init; } public VoiceWorkSeries? Series { get; init; }
public VoiceWorkTranslation? Translation { get; init; } public VoiceWorkTranslation? Translation { get; init; }
public VoiceWorkLocalizationIngest[] Localizations { get; init; } = [];
public static VoiceWorkIngest From(DLSiteWork work, VoiceWorkDetails? details, ChobitResult? chobit) public static VoiceWorkIngest From(DLSiteWork work, VoiceWorkDetails? details, ChobitResult? chobit)
{ {

View File

@@ -0,0 +1,10 @@
using JSMR.Domain.Enums;
namespace JSMR.Application.Scanning.Contracts;
public sealed class VoiceWorkLocalizationIngest
{
public Language Language { get; init; }
public string? Title { get; init; }
public string? Description { get; init; }
}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Scanning.Contracts;
namespace JSMR.Application.Scanning.Ports;
public interface IVoiceWorkIngestBuilder
{
Task<VoiceWorkIngest[]> BuildAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken);
}

View File

@@ -1,9 +1,4 @@
using JSMR.Application.Common.Caching; using JSMR.Application.Common.Caching;
using JSMR.Application.Integrations.Chobit.Models;
using JSMR.Application.Integrations.Chobit.Ports;
using JSMR.Application.Integrations.DLSite.Models;
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;
@@ -12,10 +7,8 @@ namespace JSMR.Application.Scanning;
public sealed class ScanVoiceWorksHandler( public sealed class ScanVoiceWorksHandler(
IVoiceWorkScannerRepository scannerRepository, IVoiceWorkScannerRepository scannerRepository,
IVoiceWorkUpdaterRepository updaterRepository, IVoiceWorkUpdaterRepository updaterRepository,
IDLSiteClient dlsiteClient,
IChobitClient chobitClient,
ISpamCircleCache spamCircleCache, ISpamCircleCache spamCircleCache,
IReleasedWorksProvider releasedWorksProvider, IVoiceWorkIngestBuilder ingestBuilder,
IVoiceWorkSearchUpdater searchUpdater) IVoiceWorkSearchUpdater searchUpdater)
{ {
public async Task<ScanVoiceWorksResponse> HandleAsync(ScanVoiceWorksRequest request, CancellationToken cancellationToken) public async Task<ScanVoiceWorksResponse> HandleAsync(ScanVoiceWorksRequest request, CancellationToken cancellationToken)
@@ -47,19 +40,7 @@ public sealed class ScanVoiceWorksHandler(
); );
} }
string[] productIds = [.. scanResult.Works.Where(x => !string.IsNullOrWhiteSpace(x.ProductId)).Select(x => x.ProductId!)]; VoiceWorkIngest[] ingests = await ingestBuilder.BuildAsync(scanResult, cancellationToken);
VoiceWorkDetailCollection voiceWorkDetails = await dlsiteClient.GetVoiceWorkDetailsAsync(productIds, cancellationToken);
ChobitResultCollection chobitResults = await chobitClient.GetSampleInfoAsync(productIds, cancellationToken);
ReleasedWorksCollection releasedWorkCollection = await releasedWorksProvider.GetReleasedWorksAsync(scanResult, cancellationToken);
VoiceWorkIngest[] ingests = [.. scanResult.Works.Select(work =>
{
voiceWorkDetails.TryGetValue(work.ProductId!, out VoiceWorkDetails? value);
chobitResults.TryGetValue(work.ProductId, out ChobitResult? chobit);
releasedWorkCollection.TryGetValue(work.ProductId, out ReleasedWork? releasedWork);
return VoiceWorkIngest.From(work, value, chobit);
})];
VoiceWorkUpsertResult[] upsertResults = await updater.UpsertAsync(ingests, cancellationToken); VoiceWorkUpsertResult[] upsertResults = await updater.UpsertAsync(ingests, cancellationToken);
int[] voiceWorkIds = [.. upsertResults.Where(x => x.VoiceWorkId.HasValue).Select(x => x.VoiceWorkId!.Value)]; int[] voiceWorkIds = [.. upsertResults.Where(x => x.VoiceWorkId.HasValue).Select(x => x.VoiceWorkId!.Value)];

View File

@@ -54,6 +54,7 @@ public static class InfrastructureServiceCollectionExtensions
services.AddScoped<IVoiceWorkScannerRepository, VoiceWorkScannerRepository>(); services.AddScoped<IVoiceWorkScannerRepository, VoiceWorkScannerRepository>();
services.AddScoped<IReleasedWorksProvider, ReleasedWorksProvider>(); services.AddScoped<IReleasedWorksProvider, ReleasedWorksProvider>();
services.AddScoped<IVoiceWorkIngestBuilder, VoiceWorkIngestBuilder>();
services.AddKeyedScoped<IVoiceWorkUpdater, VoiceWorkUpdater>(Locale.Japanese); services.AddKeyedScoped<IVoiceWorkUpdater, VoiceWorkUpdater>(Locale.Japanese);
services.AddKeyedScoped<IVoiceWorkUpdater, EnglishVoiceWorkUpdater>(Locale.English); services.AddKeyedScoped<IVoiceWorkUpdater, EnglishVoiceWorkUpdater>(Locale.English);

View File

@@ -72,6 +72,7 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
.AsSplitQuery() .AsSplitQuery()
.Include(v => v.Creators) .Include(v => v.Creators)
.Include(v => v.Tags) .Include(v => v.Tags)
.Include(v => v.EnglishVoiceWorks)
.Include(v => v.Localizations) .Include(v => v.Localizations)
.Include(v => v.SupportedLanguages) .Include(v => v.SupportedLanguages)
.ToDictionaryAsync(v => v.ProductId, cancellationToken), .ToDictionaryAsync(v => v.ProductId, cancellationToken),
@@ -161,6 +162,7 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
UpsertVoiceWorkCreators(ingest, upsertContext); UpsertVoiceWorkCreators(ingest, upsertContext);
UpsertVoiceWorkSupportedLanguages(ingest, upsertContext); UpsertVoiceWorkSupportedLanguages(ingest, upsertContext);
UpsertSeries(ingest, upsertContext); UpsertSeries(ingest, upsertContext);
UpsertVoiceWorkLocalizations(ingest, upsertContext);
return dbContext.Entry(voiceWork).State switch return dbContext.Entry(voiceWork).State switch
{ {
@@ -473,4 +475,39 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
return series; return series;
} }
private void UpsertVoiceWorkLocalizations(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
{
// For now, just adding/updating English voice works
foreach (VoiceWorkLocalizationIngest localizationIngest in ingest.Localizations)
{
if (localizationIngest.Language is Language.English)
{
EnglishVoiceWork englishVoiceWork = GetOrAddEnglishVoiceWork(ingest, upsertContext);
englishVoiceWork.ProductName = localizationIngest.Title ?? string.Empty;
englishVoiceWork.Description = localizationIngest.Description ?? string.Empty;
englishVoiceWork.IsValid = true;
}
}
}
private EnglishVoiceWork GetOrAddEnglishVoiceWork(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
{
VoiceWork voiceWork = upsertContext.VoiceWorks[ingest.ProductId];
EnglishVoiceWork? englishVoiceWork = voiceWork.EnglishVoiceWorks.FirstOrDefault();
if (englishVoiceWork is null)
{
englishVoiceWork = new EnglishVoiceWork
{
VoiceWork = voiceWork,
ProductName = string.Empty,
Description = string.Empty
};
dbContext.EnglishVoiceWorks.Add(englishVoiceWork);
}
return englishVoiceWork;
}
} }

View File

@@ -30,7 +30,7 @@ public class ReleasedWorksProvider(IDLSiteClient dlsiteClient) : IReleasedWorksP
ReleasedWorksRequest releasedWorksRequest = new( ReleasedWorksRequest releasedWorksRequest = new(
Locale: Locale.English, Locale: Locale.English,
Date: requestDate, Date: requestEndDate,
Period: period Period: period
); );

View File

@@ -0,0 +1,94 @@
using JSMR.Application.Integrations.Chobit.Models;
using JSMR.Application.Integrations.Chobit.Ports;
using JSMR.Application.Integrations.DLSite.Models;
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
using JSMR.Application.Integrations.DLSite.Ports;
using JSMR.Application.Scanning.Contracts;
using JSMR.Application.Scanning.Ports;
using JSMR.Domain.Enums;
using JSMR.Infrastructure.Common.Languages;
namespace JSMR.Infrastructure.Scanning;
public class VoiceWorkIngestBuilder(
IDLSiteClient dlsiteClient,
IChobitClient chobitClient,
IReleasedWorksProvider releasedWorksProvider,
ILanguageIdentifier languageIdentifier) : IVoiceWorkIngestBuilder
{
public async Task<VoiceWorkIngest[]> BuildAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken)
{
string[] productIds = [.. scanResult.Works.Where(x => !string.IsNullOrWhiteSpace(x.ProductId)).Select(x => x.ProductId!)];
Task<VoiceWorkDetailCollection> detailsTask = dlsiteClient.GetVoiceWorkDetailsAsync(productIds, cancellationToken);
Task<ChobitResultCollection> chobitTask = chobitClient.GetSampleInfoAsync(productIds, cancellationToken);
Task<ReleasedWorksCollection> releasedTask = releasedWorksProvider.GetReleasedWorksAsync(scanResult, cancellationToken);
await Task.WhenAll(detailsTask, chobitTask, releasedTask);
VoiceWorkDetailCollection voiceWorkDetails = await detailsTask;
ChobitResultCollection chobitResults = await chobitTask;
ReleasedWorksCollection releasedWorkCollection = await releasedTask;
List<VoiceWorkIngest> ingests = [];
foreach (DLSiteWork work in scanResult.Works)
{
voiceWorkDetails.TryGetValue(work.ProductId!, out VoiceWorkDetails? details);
chobitResults.TryGetValue(work.ProductId, out ChobitResult? chobit);
releasedWorkCollection.TryGetValue(work.ProductId, out ReleasedWork? releasedWork);
VoiceWorkIngest ingest = new()
{
MakerId = work.MakerId,
MakerName = work.Maker,
ProductId = work.ProductId,
Title = details?.Title ?? work.ProductName,
Description = work.Description ?? string.Empty,
Tags = work.Tags,
Creators = work.Creators,
WishlistCount = details?.WishlistCount ?? 0,
Downloads = Math.Max(work.Downloads, details?.DownloadCount ?? 0),
HasTrial = work.HasTrial || (details?.HasTrial ?? false),
HasChobit = chobit?.Count > 0,
StarRating = work.StarRating,
Votes = work.Votes,
AgeRating = details?.AgeRating ?? work.AgeRating,
HasImage = !string.IsNullOrEmpty(work.ImageUrl) && !work.ImageUrl.Contains("no_img", StringComparison.OrdinalIgnoreCase),
SupportedLanguages = details?.SupportedLanguages ?? [],
ExpectedDate = work.ExpectedDate,
SalesDate = work.SalesDate,
RegistrationDate = details?.RegistrationDate,
AI = details?.AI ?? AIGeneration.None,
Series = details?.Series,
Translation = details?.Translation,
Localizations = GetLocalizationIngests(releasedWork)
};
ingests.Add(ingest);
}
return [.. ingests];
}
private VoiceWorkLocalizationIngest[] GetLocalizationIngests(ReleasedWork? releasedWork)
{
if (releasedWork is null)
return [];
Language titleLanguage = languageIdentifier.GetLanguage(releasedWork.Title);
Language descriptionLanguage = languageIdentifier.GetLanguage(releasedWork.Description);
if (titleLanguage is not Language.English && descriptionLanguage is not Language.English)
return [];
VoiceWorkLocalizationIngest localizationIngest = new()
{
Title = releasedWork.Title,
Description = releasedWork.Description,
Language = Language.English
};
return [localizationIngest];
}
}

View File

@@ -26,7 +26,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="coverlet.collector" Version="8.0.0"> <PackageReference Include="coverlet.collector" Version="8.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@@ -10,14 +10,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Bit.BlazorUI" Version="10.4.2" /> <PackageReference Include="Bit.BlazorUI" Version="10.4.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.5" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.5" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.9" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" /> <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageReference Include="MudBlazor" Version="9.1.0" /> <PackageReference Include="MudBlazor" Version="9.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Radzen.Blazor" Version="10.0.2" /> <PackageReference Include="Radzen.Blazor" Version="10.0.6" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -16,6 +16,14 @@
}, },
"workingDirectory": "" "workingDirectory": ""
}, },
"Scan (JP, 10 pages)": {
"commandName": "Project",
"commandLineArgs": "scan --locale Japanese --start 1 --end 10 --pageSize 100",
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
},
"workingDirectory": ""
},
"Scan (EN, 3 pages)": { "Scan (EN, 3 pages)": {
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "scan --locale English --start 1 --end 3 --pageSize 100", "commandLineArgs": "scan --locale English --start 1 --end 3 --pageSize 100",