From 0dd11e635114dfa8260186d38ba944461261aa32 Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Sun, 29 Mar 2026 21:24:04 -0400 Subject: [PATCH] Added English localization logic during the regular scan process. --- .../Scanning/Contracts/VoiceWorkIngest.cs | 1 + .../Contracts/VoiceWorkLocalizationIngest.cs | 10 ++ .../Scanning/Ports/IVoiceWorkIngestBuilder.cs | 8 ++ .../Scanning/ScanVoiceWorksHandler.cs | 23 +---- ...frastructureServiceCollectionExtensions.cs | 1 + .../Ingestion/VoiceWorkUpdater.cs | 37 ++++++++ .../Scanning/ReleasedWorksProvider.cs | 2 +- .../Scanning/VoiceWorkIngestBuilder.cs | 94 +++++++++++++++++++ JSMR.Tests/JSMR.Tests.csproj | 2 +- JSMR.UI.Blazor/JSMR.UI.Blazor.csproj | 6 +- JSMR.Worker/Properties/launchSettings.json | 8 ++ 11 files changed, 166 insertions(+), 26 deletions(-) create mode 100644 JSMR.Application/Scanning/Contracts/VoiceWorkLocalizationIngest.cs create mode 100644 JSMR.Application/Scanning/Ports/IVoiceWorkIngestBuilder.cs create mode 100644 JSMR.Infrastructure/Scanning/VoiceWorkIngestBuilder.cs diff --git a/JSMR.Application/Scanning/Contracts/VoiceWorkIngest.cs b/JSMR.Application/Scanning/Contracts/VoiceWorkIngest.cs index d7ae30c..206f34f 100644 --- a/JSMR.Application/Scanning/Contracts/VoiceWorkIngest.cs +++ b/JSMR.Application/Scanning/Contracts/VoiceWorkIngest.cs @@ -29,6 +29,7 @@ public sealed record VoiceWorkIngest public AIGeneration AI { get; init; } public VoiceWorkSeries? Series { get; init; } public VoiceWorkTranslation? Translation { get; init; } + public VoiceWorkLocalizationIngest[] Localizations { get; init; } = []; public static VoiceWorkIngest From(DLSiteWork work, VoiceWorkDetails? details, ChobitResult? chobit) { diff --git a/JSMR.Application/Scanning/Contracts/VoiceWorkLocalizationIngest.cs b/JSMR.Application/Scanning/Contracts/VoiceWorkLocalizationIngest.cs new file mode 100644 index 0000000..3e7c39d --- /dev/null +++ b/JSMR.Application/Scanning/Contracts/VoiceWorkLocalizationIngest.cs @@ -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; } +} \ No newline at end of file diff --git a/JSMR.Application/Scanning/Ports/IVoiceWorkIngestBuilder.cs b/JSMR.Application/Scanning/Ports/IVoiceWorkIngestBuilder.cs new file mode 100644 index 0000000..e94d2c9 --- /dev/null +++ b/JSMR.Application/Scanning/Ports/IVoiceWorkIngestBuilder.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Scanning.Contracts; + +namespace JSMR.Application.Scanning.Ports; + +public interface IVoiceWorkIngestBuilder +{ + Task BuildAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs b/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs index ff46032..e29d860 100644 --- a/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs +++ b/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs @@ -1,9 +1,4 @@ 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.Ports; @@ -12,10 +7,8 @@ namespace JSMR.Application.Scanning; public sealed class ScanVoiceWorksHandler( IVoiceWorkScannerRepository scannerRepository, IVoiceWorkUpdaterRepository updaterRepository, - IDLSiteClient dlsiteClient, - IChobitClient chobitClient, ISpamCircleCache spamCircleCache, - IReleasedWorksProvider releasedWorksProvider, + IVoiceWorkIngestBuilder ingestBuilder, IVoiceWorkSearchUpdater searchUpdater) { public async Task 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!)]; - 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); - })]; + VoiceWorkIngest[] ingests = await ingestBuilder.BuildAsync(scanResult, cancellationToken); VoiceWorkUpsertResult[] upsertResults = await updater.UpsertAsync(ingests, cancellationToken); int[] voiceWorkIds = [.. upsertResults.Where(x => x.VoiceWorkId.HasValue).Select(x => x.VoiceWorkId!.Value)]; diff --git a/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs b/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs index 9fa1c56..535505b 100644 --- a/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs +++ b/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs @@ -54,6 +54,7 @@ public static class InfrastructureServiceCollectionExtensions services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddKeyedScoped(Locale.Japanese); services.AddKeyedScoped(Locale.English); diff --git a/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs b/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs index dfcd9bf..d5a9218 100644 --- a/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs +++ b/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs @@ -72,6 +72,7 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider .AsSplitQuery() .Include(v => v.Creators) .Include(v => v.Tags) + .Include(v => v.EnglishVoiceWorks) .Include(v => v.Localizations) .Include(v => v.SupportedLanguages) .ToDictionaryAsync(v => v.ProductId, cancellationToken), @@ -161,6 +162,7 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider UpsertVoiceWorkCreators(ingest, upsertContext); UpsertVoiceWorkSupportedLanguages(ingest, upsertContext); UpsertSeries(ingest, upsertContext); + UpsertVoiceWorkLocalizations(ingest, upsertContext); return dbContext.Entry(voiceWork).State switch { @@ -473,4 +475,39 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider 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; + } } \ No newline at end of file diff --git a/JSMR.Infrastructure/Scanning/ReleasedWorksProvider.cs b/JSMR.Infrastructure/Scanning/ReleasedWorksProvider.cs index 04fff10..661f871 100644 --- a/JSMR.Infrastructure/Scanning/ReleasedWorksProvider.cs +++ b/JSMR.Infrastructure/Scanning/ReleasedWorksProvider.cs @@ -30,7 +30,7 @@ public class ReleasedWorksProvider(IDLSiteClient dlsiteClient) : IReleasedWorksP ReleasedWorksRequest releasedWorksRequest = new( Locale: Locale.English, - Date: requestDate, + Date: requestEndDate, Period: period ); diff --git a/JSMR.Infrastructure/Scanning/VoiceWorkIngestBuilder.cs b/JSMR.Infrastructure/Scanning/VoiceWorkIngestBuilder.cs new file mode 100644 index 0000000..5fc6d98 --- /dev/null +++ b/JSMR.Infrastructure/Scanning/VoiceWorkIngestBuilder.cs @@ -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 BuildAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken) + { + string[] productIds = [.. scanResult.Works.Where(x => !string.IsNullOrWhiteSpace(x.ProductId)).Select(x => x.ProductId!)]; + + Task detailsTask = dlsiteClient.GetVoiceWorkDetailsAsync(productIds, cancellationToken); + Task chobitTask = chobitClient.GetSampleInfoAsync(productIds, cancellationToken); + Task releasedTask = releasedWorksProvider.GetReleasedWorksAsync(scanResult, cancellationToken); + + await Task.WhenAll(detailsTask, chobitTask, releasedTask); + + VoiceWorkDetailCollection voiceWorkDetails = await detailsTask; + ChobitResultCollection chobitResults = await chobitTask; + ReleasedWorksCollection releasedWorkCollection = await releasedTask; + + List 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]; + } +} \ No newline at end of file diff --git a/JSMR.Tests/JSMR.Tests.csproj b/JSMR.Tests/JSMR.Tests.csproj index 781f131..fd443a0 100644 --- a/JSMR.Tests/JSMR.Tests.csproj +++ b/JSMR.Tests/JSMR.Tests.csproj @@ -26,7 +26,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/JSMR.UI.Blazor/JSMR.UI.Blazor.csproj b/JSMR.UI.Blazor/JSMR.UI.Blazor.csproj index 54b11ed..ad8e631 100644 --- a/JSMR.UI.Blazor/JSMR.UI.Blazor.csproj +++ b/JSMR.UI.Blazor/JSMR.UI.Blazor.csproj @@ -10,14 +10,14 @@ - + - + - + diff --git a/JSMR.Worker/Properties/launchSettings.json b/JSMR.Worker/Properties/launchSettings.json index 40bab03..a20e84c 100644 --- a/JSMR.Worker/Properties/launchSettings.json +++ b/JSMR.Worker/Properties/launchSettings.json @@ -16,6 +16,14 @@ }, "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)": { "commandName": "Project", "commandLineArgs": "scan --locale English --start 1 --end 3 --pageSize 100",