Added additional voice work update logic.

This commit is contained in:
2025-10-11 17:28:39 -04:00
parent db0c3349a2
commit 278b6df650
38 changed files with 56745 additions and 64 deletions

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Common;
namespace JSMR.Infrastructure.Common.Languages;
public interface ILanguageIdentifier
{
Language GetLanguage(string text);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
using JSMR.Application.Common;
using NTextCat;
using System.Reflection;
namespace JSMR.Infrastructure.Common.Languages;
public class LanguageIdentifier : ILanguageIdentifier
{
private readonly string[] _languages =
[
"eng",
"jpn",
"kor",
"zho"
];
private readonly RankedLanguageIdentifier _identifier;
public LanguageIdentifier()
{
RankedLanguageIdentifierFactory factory = new();
using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("JSMR.Infrastructure.Languages.Language.xml");
_identifier = factory.Load(stream);
}
public Language GetLanguage(string text)
{
var rankedLanguages = _identifier.Identify(text).Where(x => _languages.Contains(x.Item1.Iso639_3));
var identifiedLanguage = rankedLanguages.OrderBy(x => x.Item2).FirstOrDefault();
if (identifiedLanguage == null)
return Language.Unknown;
return identifiedLanguage.Item1.Iso639_3 switch
{
"jpn" => Language.Japanese,
"eng" => Language.English,
"kor" => Language.Korean,
"zho" => Language.ChineseTraditional,// Or ChineseSimplified?
_ => Language.Unknown,
};
}
}

View File

@@ -1,6 +1,9 @@
namespace JSMR.Infrastructure.Common.SupportedLanguages;
using JSMR.Application.Common;
namespace JSMR.Infrastructure.Common.SupportedLanguages;
public class AlingualLanguage : ISupportedLanguage
{
public Language Language => Language.Unknown;
public string Code => "NM";
}

View File

@@ -1,6 +1,9 @@
namespace JSMR.Infrastructure.Common.SupportedLanguages;
using JSMR.Application.Common;
namespace JSMR.Infrastructure.Common.SupportedLanguages;
public class ChineseLanguage : ISupportedLanguage
{
public Language Language => Language.ChineseTraditional; // ???
public string Code => "CHI";
}

View File

@@ -1,6 +1,9 @@
namespace JSMR.Infrastructure.Common.SupportedLanguages;
using JSMR.Application.Common;
namespace JSMR.Infrastructure.Common.SupportedLanguages;
public class DLSiteOfficialTranslationLanguage : ISupportedLanguage
{
public string Code => "DOT";
public Language Language => Language.Unknown;
}

View File

@@ -1,6 +1,9 @@
namespace JSMR.Infrastructure.Common.SupportedLanguages;
using JSMR.Application.Common;
namespace JSMR.Infrastructure.Common.SupportedLanguages;
public class EnglishLanguage : ISupportedLanguage
{
public Language Language => Language.English;
public string Code => "ENG";
}

View File

@@ -1,6 +0,0 @@
namespace JSMR.Infrastructure.Common.SupportedLanguages;
public interface ISupportedLanguage
{
string Code { get; }
}

View File

@@ -1,6 +1,9 @@
namespace JSMR.Infrastructure.Common.SupportedLanguages;
using JSMR.Application.Common;
namespace JSMR.Infrastructure.Common.SupportedLanguages;
public class JapaneseLanguage : ISupportedLanguage
{
public Language Language => Language.Japanese;
public string Code => "JPN";
}

View File

@@ -1,6 +1,9 @@
namespace JSMR.Infrastructure.Common.SupportedLanguages;
using JSMR.Application.Common;
namespace JSMR.Infrastructure.Common.SupportedLanguages;
public class KoreanLanguage : ISupportedLanguage
{
public Language Language => Language.Korean;
public string Code => "KO_KR";
}

View File

@@ -1,6 +1,9 @@
namespace JSMR.Infrastructure.Common.SupportedLanguages;
using JSMR.Application.Common;
namespace JSMR.Infrastructure.Common.SupportedLanguages;
public class SimplifiedChineseLanguage : ISupportedLanguage
{
public Language Language => Language.ChineseSimplified;
public string Code => "CHI_HANS";
}

View File

@@ -1,6 +1,9 @@
namespace JSMR.Infrastructure.Common.SupportedLanguages;
using JSMR.Application.Common;
namespace JSMR.Infrastructure.Common.SupportedLanguages;
public class TraditionalChineseLanguage : ISupportedLanguage
{
public Language Language => Language.ChineseTraditional;
public string Code => "CHI_HANT";
}

View File

@@ -0,0 +1,6 @@
namespace JSMR.Infrastructure.Common.Time;
public class Clock : IClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}

View File

@@ -0,0 +1,6 @@
namespace JSMR.Infrastructure.Common.Time;
public interface IClock
{
DateTimeOffset UtcNow { get; }
}

View File

@@ -0,0 +1,69 @@
namespace JSMR.Infrastructure.Common.Time;
public interface ITimeProvider
{
DateTimeOffset Now();
DateTimeOffset Local(int year, int month, int day, int hour);
DateTimeOffset Local(DateTimeOffset offset);
}
public abstract class TimeProvider : ITimeProvider
{
protected abstract string Id { get; }
protected abstract string[] TimeZoneIds { get; }
private readonly IClock _clock;
private readonly TimeZoneInfo _timeZone;
public TimeProvider(IClock clock)
{
_clock = clock;
_timeZone = ResolveTimeZone();
}
private TimeZoneInfo ResolveTimeZone()
{
foreach (string timeZoneId in TimeZoneIds)
{
if (TimeZoneInfo.TryFindSystemTimeZoneById(timeZoneId, out TimeZoneInfo? timeZoneInfo))
return timeZoneInfo;
}
throw new TimeZoneNotFoundException($"Unable to resolve time zone for: {Id} ({string.Join(" / ", TimeZoneIds)})");
}
public DateTimeOffset Now() => TimeZoneInfo.ConvertTime(_clock.UtcNow, _timeZone);
public DateTimeOffset Local(DateTimeOffset offset) => TimeZoneInfo.ConvertTime(offset, _timeZone);
public DateTimeOffset Local(int year, int month, int day, int hour)
{
DateTime local = new(year, month, day, hour, 0, 0, DateTimeKind.Unspecified);
TimeSpan offset = _timeZone.GetUtcOffset(local);
return new DateTimeOffset(local, offset);
}
public DateTimeOffset CurrentScanAnchor()
{
DateTimeOffset now = Now();
DateTimeOffset midnight = Local(now.Year, now.Month, now.Day, 0);
DateTimeOffset fourPm = Local(now.Year, now.Month, now.Day, 16);
return now >= fourPm ? fourPm : midnight;
}
public DateTimeOffset PreviousScanAnchor(DateTimeOffset scanAnchorTokyo)
{
// Normalize to Tokyo (no-op if already)
var a = TimeZoneInfo.ConvertTime(scanAnchorTokyo, _timeZone);
return a.Hour == 16
? Local(a.Year, a.Month, a.Day, 0)
: Local(a.AddDays(-1).Year, a.AddDays(-1).Month, a.AddDays(-1).Day, 16);
}
}
public class TokyoTimeProvider(IClock clock) : TimeProvider(clock)
{
protected override string Id => "Tokyo Standard Time";
protected override string[] TimeZoneIds => ["Tokyo Standard Time", "Asia/Tokyo"];
}

View File

@@ -5,7 +5,6 @@ using JSMR.Application.Common;
using JSMR.Application.Common.Caching;
using JSMR.Application.Creators.Ports;
using JSMR.Application.Creators.Queries.Search.Ports;
using JSMR.Application.Integrations.Ports;
using JSMR.Application.Scanning.Ports;
using JSMR.Application.Tags.Ports;
using JSMR.Application.Tags.Queries.Search.Ports;
@@ -13,12 +12,14 @@ using JSMR.Application.VoiceWorks.Ports;
using JSMR.Application.VoiceWorks.Queries.Search;
using JSMR.Infrastructure.Caching;
using JSMR.Infrastructure.Caching.Adapters;
using JSMR.Infrastructure.Common.Languages;
using JSMR.Infrastructure.Common.SupportedLanguages;
using JSMR.Infrastructure.Common.Time;
using JSMR.Infrastructure.Data.Repositories.Circles;
using JSMR.Infrastructure.Data.Repositories.Creators;
using JSMR.Infrastructure.Data.Repositories.Tags;
using JSMR.Infrastructure.Data.Repositories.VoiceWorks;
using JSMR.Infrastructure.Http;
using JSMR.Infrastructure.Integrations.DLSite;
using JSMR.Infrastructure.Scanning;
using Microsoft.Extensions.DependencyInjection;
@@ -38,6 +39,12 @@ public static class InfrastructureServiceCollectionExtensions
services.AddKeyedScoped<IVoiceWorksScanner, JapaneseVoiceWorksScanner>(Locale.Japanese);
services.AddKeyedScoped<IVoiceWorksScanner, EnglishVoiceWorksScanner>(Locale.English);
services.AddKeyedScoped<ISupportedLanguage, JapaneseLanguage>(Locale.Japanese);
services.AddKeyedScoped<ISupportedLanguage, EnglishLanguage>(Locale.English);
services.AddKeyedScoped<ISupportedLanguage, SimplifiedChineseLanguage>(Locale.ChineseSimplified);
services.AddKeyedScoped<ISupportedLanguage, TraditionalChineseLanguage>(Locale.ChineseTraditional);
services.AddKeyedScoped<ISupportedLanguage, KoreanLanguage>(Locale.Korean);
services.AddScoped<ITagSearchProvider, TagSearchProvider>();
services.AddScoped<ITagWriter, TagWriter>();
@@ -55,6 +62,11 @@ public static class InfrastructureServiceCollectionExtensions
services.AddScoped<IHttpService, HttpService>();
services.AddScoped<IHtmlLoader, HtmlLoader>();
services.AddSingleton<ILanguageIdentifier, LanguageIdentifier>();
services.AddSingleton<IClock, Clock>();
services.AddSingleton<ITimeProvider, TokyoTimeProvider>();
return services;
}

View File

@@ -7,6 +7,7 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
{
public DbSet<VoiceWork> VoiceWorks { get; set; }
public DbSet<EnglishVoiceWork> EnglishVoiceWorks { get; set; }
public DbSet<VoiceWorkSupportedLanguage> VoiceWorkSupportedLanguages { get; set; }
public DbSet<VoiceWorkLocalization> VoiceWorkLocalizations { get; set; }
public DbSet<Circle> Circles { get; set; }
public DbSet<Tag> Tags { get; set; }

View File

@@ -12,7 +12,7 @@ public sealed class VoiceWorkCreatorConfiguration : IEntityTypeConfiguration<Voi
builder.HasKey(x => new { x.VoiceWorkId, x.CreatorId });
builder.HasOne(x => x.VoiceWork)
.WithMany(v => v.VoiceWorkCreators)
.WithMany(v => v.Creators)
.HasForeignKey(x => x.VoiceWorkId)
.OnDelete(DeleteBehavior.Cascade);

View File

@@ -0,0 +1,24 @@
using JSMR.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace JSMR.Infrastructure.Data.Configuration;
public sealed class VoiceWorkSupportedLanguageConfiguration : IEntityTypeConfiguration<VoiceWorkSupportedLanguage>
{
public void Configure(EntityTypeBuilder<VoiceWorkSupportedLanguage> builder)
{
builder.ToTable("voice_work_supported_languages");
builder.HasKey(x => x.VoiceWorkSupportedLanguageId);
builder.Property(x => x.Language).IsRequired().HasMaxLength(10);
builder.HasOne(x => x.VoiceWork)
.WithMany(v => v.SupportedLanguages)
.HasForeignKey(x => x.VoiceWorkId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasIndex(x => new { x.VoiceWorkId, x.Language }).IsUnique();
builder.HasIndex(x => x.VoiceWorkId);
}
}

View File

@@ -12,7 +12,7 @@ public sealed class VoiceWorkTagConfiguration : IEntityTypeConfiguration<VoiceWo
builder.HasKey(x => new { x.VoiceWorkId, x.TagId });
builder.HasOne(x => x.VoiceWork)
.WithMany(v => v.VoiceWorkTags)
.WithMany(v => v.Tags)
.HasForeignKey(x => x.VoiceWorkId)
.OnDelete(DeleteBehavior.Cascade);

View File

@@ -11,9 +11,9 @@ public class VoiceWorkSearchUpdater(AppDbContext dbContext) : IVoiceWorkSearchUp
{
List<VoiceWork> batch = await dbContext.VoiceWorks
.Include(vw => vw.Circle)
.Include(vw => vw.VoiceWorkTags)
.Include(vw => vw.Tags)
.ThenInclude(vwt => vwt.Tag)
.Include(vw => vw.VoiceWorkCreators)
.Include(vw => vw.Creators)
.ThenInclude(vwc => vwc.Creator)
.Include(vw => vw.EnglishVoiceWorks)
.Where(vw => voiceWorkIds.Contains(vw.VoiceWorkId))
@@ -71,7 +71,7 @@ public class VoiceWorkSearchUpdater(AppDbContext dbContext) : IVoiceWorkSearchUp
AppendRaw(sb, voiceWork.Description);
AppendRaw(sb, voiceWork.Circle?.Name);
foreach (var tag in voiceWork.VoiceWorkTags.Select(vwt => vwt.Tag))
foreach (var tag in voiceWork.Tags.Select(vwt => vwt.Tag))
{
if (tag is null)
continue;
@@ -86,7 +86,7 @@ public class VoiceWorkSearchUpdater(AppDbContext dbContext) : IVoiceWorkSearchUp
AppendRaw(sb, englishTag?.Name);
}
foreach (var creator in voiceWork.VoiceWorkCreators.Select(vwc => vwc.Creator))
foreach (var creator in voiceWork.Creators.Select(vwc => vwc.Creator))
{
if (creator is null)
continue;

View File

@@ -3,6 +3,8 @@
namespace JSMR.Infrastructure.Data.Repositories.VoiceWorks;
public record VoiceWorkUpsertContext(
DateTimeOffset CurrentScanAnchor,
DateTimeOffset PreviousScanAnchor,
Dictionary<string, Circle> Circles,
Dictionary<string, VoiceWork> VoiceWorks,
Dictionary<string, Tag> Tags,

View File

@@ -1,12 +1,14 @@
using JSMR.Application.Scanning.Contracts;
using JSMR.Application.Common;
using JSMR.Application.Scanning.Contracts;
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
using JSMR.Application.VoiceWorks.Ports;
using JSMR.Domain.Entities;
using JSMR.Infrastructure.Common.Time;
using Microsoft.EntityFrameworkCore;
namespace JSMR.Infrastructure.Data.Repositories.VoiceWorks;
public class VoiceWorkWriter(AppDbContext dbContext) : IVoiceWorkWriter
public class VoiceWorkWriter(AppDbContext dbContext, ITimeProvider timeProvider) : IVoiceWorkWriter
{
public async Task<int[]> UpsertAsync(IReadOnlyCollection<VoiceWorkIngest> ingests, CancellationToken cancellationToken)
{
@@ -29,14 +31,21 @@ public class VoiceWorkWriter(AppDbContext dbContext) : IVoiceWorkWriter
string[] tagNames = [.. ingests.SelectMany(i => i.Tags).Distinct()];
string[] creatorNames = [.. ingests.SelectMany(i => i.Creators).Distinct()];
DateTimeOffset currentScanAnchor = GetCurrentScanAnchor();
DateTimeOffset previousScanAnchor = PreviousScanAnchor(currentScanAnchor);
VoiceWorkUpsertContext upsertContext = new(
CurrentScanAnchor: currentScanAnchor,
PreviousScanAnchor: previousScanAnchor,
Circles: await dbContext.Circles
.Where(c => makerIds.Contains(c.MakerId))
.ToDictionaryAsync(c => c.MakerId, cancellationToken),
VoiceWorks: await dbContext.VoiceWorks
.Where(v => productIds.Contains(v.ProductId))
.Include(v => v.VoiceWorkCreators)
.Include(v => v.VoiceWorkTags)
.Include(v => v.Creators)
.Include(v => v.Tags)
.Include(v => v.Localizations)
.Include(v => v.SupportedLanguages)
.ToDictionaryAsync(v => v.ProductId, cancellationToken),
Tags: await dbContext.Tags
.Where(t => tagNames.Contains(t.Name))
@@ -49,6 +58,25 @@ public class VoiceWorkWriter(AppDbContext dbContext) : IVoiceWorkWriter
return upsertContext;
}
private DateTimeOffset GetCurrentScanAnchor()
{
DateTimeOffset now = timeProvider.Now();
DateTimeOffset midnight = timeProvider.Local(now.Year, now.Month, now.Day, 0);
DateTimeOffset fourPm = timeProvider.Local(now.Year, now.Month, now.Day, 16);
return now >= fourPm ? fourPm : midnight;
}
private DateTimeOffset PreviousScanAnchor(DateTimeOffset scanAnchorTokyo)
{
// Normalize to Tokyo (no-op if already)
var a = timeProvider.Local(scanAnchorTokyo);
return a.Hour == 16
? timeProvider.Local(a.Year, a.Month, a.Day, 0)
: timeProvider.Local(a.AddDays(-1).Year, a.AddDays(-1).Month, a.AddDays(-1).Day, 16);
}
private void Upsert(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
{
UpsertCircle(ingest, upsertContext);
@@ -57,6 +85,7 @@ public class VoiceWorkWriter(AppDbContext dbContext) : IVoiceWorkWriter
UpsertVoiceWorkTags(ingest, upsertContext);
UpsertCreators(ingest, upsertContext);
UpsertVoiceWorkCreators(ingest, upsertContext);
UpsertVoiceWorkSupportedLanguages(ingest, upsertContext);
}
private void UpsertCircle(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
@@ -85,23 +114,40 @@ public class VoiceWorkWriter(AppDbContext dbContext) : IVoiceWorkWriter
private void UpsertVoiceWork(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
{
VoiceWork voiceWork = GetOrAddVoiceWork(ingest, upsertContext);
bool isAdded = dbContext.Entry(voiceWork).State == EntityState.Added;
bool isWithinCurrentScanAnchor = voiceWork.LastScannedDate == upsertContext.CurrentScanAnchor;
bool isNewOnSale = voiceWork.SalesDate is null && ingest.SalesDate is not null;
bool isNew = isAdded || isWithinCurrentScanAnchor || isNewOnSale;
voiceWork.Circle = upsertContext.Circles[ingest.MakerId];
voiceWork.ProductName = ingest.Title;
voiceWork.Description = ingest.Description;
voiceWork.HasImage = ingest.HasImage;
voiceWork.Rating = (int)ingest.AgeRating;
voiceWork.Downloads = ingest.Downloads;
voiceWork.WishlistCount = ingest.WishlistCount;
voiceWork.HasTrial = ingest.HasTrial;
voiceWork.HasChobit = ingest.HasDLPlay;
voiceWork.StarRating = ingest.StarRating;
voiceWork.Votes = ingest.Votes;
voiceWork.IsValid = true;
//var est = i.EstimatedDate?.ToDateTime(new TimeOnly(0, 0));
//if (vw2.ExpectedDate != est) { vw2.ExpectedDate = est; changed = true; }
//var sales = i.SalesDate?.ToDateTime(new TimeOnly(0, 0));
//if (vw2.SalesDate != sales) { vw2.SalesDate = sales; changed = true; }
if (ingest.SalesDate.HasValue)
{
voiceWork.SalesDate = ingest.SalesDate.Value.ToDateTime(new TimeOnly(0, 0));
voiceWork.ExpectedDate = null;
voiceWork.PlannedReleaseDate = null;
voiceWork.Status = isNew ? (byte)VoiceWorkStatus.NewRelease : (byte)VoiceWorkStatus.Available;
}
else
{
voiceWork.SalesDate = null;
voiceWork.ExpectedDate = ingest.ExpectedDate?.ToDateTime(new TimeOnly(0, 0));
voiceWork.PlannedReleaseDate = ingest.RegistrationDate > upsertContext.CurrentScanAnchor ? ingest.RegistrationDate : null;
voiceWork.Status = isNew ? (byte)VoiceWorkStatus.NewAndUpcoming : (byte)VoiceWorkStatus.Upcoming;
}
}
private VoiceWork GetOrAddVoiceWork(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
@@ -144,17 +190,17 @@ public class VoiceWorkWriter(AppDbContext dbContext) : IVoiceWorkWriter
return tag;
}
private void UpsertCreators(VoiceWorkIngest ingest, VoiceWorkUpsertContext something)
private void UpsertCreators(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
{
foreach (string creatorName in ingest.Creators)
{
GetOrAddCreator(creatorName, something);
GetOrAddCreator(creatorName, upsertContext);
}
}
private Creator GetOrAddCreator(string creatorName, VoiceWorkUpsertContext something)
private Creator GetOrAddCreator(string creatorName, VoiceWorkUpsertContext upsertContext)
{
if (!something.Creators.TryGetValue(creatorName, out Creator? creator))
if (!upsertContext.Creators.TryGetValue(creatorName, out Creator? creator))
{
creator = new Creator
{
@@ -162,22 +208,22 @@ public class VoiceWorkWriter(AppDbContext dbContext) : IVoiceWorkWriter
};
dbContext.Creators.Add(creator);
something.Creators[creatorName] = creator;
upsertContext.Creators[creatorName] = creator;
}
return creator;
}
private void UpsertVoiceWorkTags(VoiceWorkIngest ingest, VoiceWorkUpsertContext something)
private void UpsertVoiceWorkTags(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
{
VoiceWork voiceWork = something.VoiceWorks[ingest.ProductId];
Dictionary<int, VoiceWorkTag> existingTagLinks = voiceWork.VoiceWorkTags.ToDictionary(x => x.TagId);
VoiceWork voiceWork = upsertContext.VoiceWorks[ingest.ProductId];
Dictionary<int, VoiceWorkTag> existingTagLinks = voiceWork.Tags.ToDictionary(x => x.TagId);
int position = 1;
foreach (string tagName in ingest.Tags)
{
Tag tag = something.Tags[tagName];
Tag tag = upsertContext.Tags[tagName];
if (!existingTagLinks.TryGetValue(tag.TagId, out VoiceWorkTag? voiceWorkTag))
{
@@ -195,16 +241,16 @@ public class VoiceWorkWriter(AppDbContext dbContext) : IVoiceWorkWriter
}
}
private void UpsertVoiceWorkCreators(VoiceWorkIngest ingest, VoiceWorkUpsertContext something)
private void UpsertVoiceWorkCreators(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
{
VoiceWork voiceWork = something.VoiceWorks[ingest.ProductId];
Dictionary<int, VoiceWorkCreator> existingCreatorLinks = voiceWork.VoiceWorkCreators.ToDictionary(x => x.CreatorId);
VoiceWork voiceWork = upsertContext.VoiceWorks[ingest.ProductId];
Dictionary<int, VoiceWorkCreator> existingCreatorLinks = voiceWork.Creators.ToDictionary(x => x.CreatorId);
int position = 1;
foreach (string creatorName in ingest.Creators)
{
Creator creator = something.Creators[creatorName];
Creator creator = upsertContext.Creators[creatorName];
if (!existingCreatorLinks.TryGetValue(creator.CreatorId, out VoiceWorkCreator? voiceWorkCreator))
{
@@ -222,6 +268,26 @@ public class VoiceWorkWriter(AppDbContext dbContext) : IVoiceWorkWriter
}
}
private void UpsertVoiceWorkSupportedLanguages(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
{
VoiceWork voiceWork = upsertContext.VoiceWorks[ingest.ProductId];
Dictionary<string, VoiceWorkSupportedLanguage> existingLanguageLinks = voiceWork.SupportedLanguages.ToDictionary(x => x.Language);
foreach (ISupportedLanguage supportedLanguage in ingest.SupportedLanguages)
{
if (!existingLanguageLinks.TryGetValue(supportedLanguage.Code, out VoiceWorkSupportedLanguage? voiceWorkSupportedLanguage))
{
voiceWorkSupportedLanguage = new VoiceWorkSupportedLanguage
{
VoiceWork = voiceWork,
Language = supportedLanguage.Code
};
dbContext.VoiceWorkSupportedLanguages.Add(voiceWorkSupportedLanguage);
}
}
}
public async Task<SetVoiceWorkFavoriteResponse> SetFavoriteAsync(SetVoiceWorkFavoriteRequest request, CancellationToken cancellationToken)
{
VoiceWork voiceWork = await GetVoiceWorkAsync(request.VoiceWorkId, cancellationToken);

View File

@@ -1,5 +1,6 @@
using JSMR.Application.Common;
using JSMR.Application.Integrations.DLSite.Models;
using JSMR.Infrastructure.Common.SupportedLanguages;
using JSMR.Infrastructure.Integrations.DLSite.Models;
namespace JSMR.Infrastructure.Integrations.DLSite.Mapping;
@@ -23,9 +24,25 @@ public static class DLSiteToDomainMapper
("CHI_HANS", Language.ChineseSimplified)
];
private static readonly (string Code, ISupportedLanguage Lang)[] SupportedLanguageFlags2 =
[
("JPN", new JapaneseLanguage()),
("ENG", new EnglishLanguage()),
("CHI", new ChineseLanguage()),
("CHI_HANT", new TraditionalChineseLanguage()),
("CHI_HANS", new SimplifiedChineseLanguage())
];
private static readonly Dictionary<string, Language> TranslationLanguageMap =
SupportedLanguageFlags.ToDictionary(x => x.Code, x => x.Lang, StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<int, AgeRating> AgeRatingMap = new()
{
{ 1, AgeRating.AllAges },
{ 2, AgeRating.R15 },
{ 3, AgeRating.R18 }
};
public static VoiceWorkDetailCollection Map(ProductInfoCollection? productInfoCollection)
{
VoiceWorkDetailCollection result = [];
@@ -63,7 +80,8 @@ public static class DLSiteToDomainMapper
AI = MapAIGeneration(optionsSet),
HasTrial = optionsSet.Contains(OptTrial),
HasDLPlay = optionsSet.Contains(OptDLPlay),
HasReviews = optionsSet.Contains(OptReviews)
HasReviews = optionsSet.Contains(OptReviews),
AgeRating = MapAgeRating(productInfo)
};
}
@@ -105,11 +123,11 @@ public static class DLSiteToDomainMapper
};
}
private static Language[] MapSupportedLanguages(HashSet<string> options)
private static ISupportedLanguage[] MapSupportedLanguages(HashSet<string> options)
{
List<Language> languages = [];
List<ISupportedLanguage> languages = [];
foreach (var (code, language) in SupportedLanguageFlags)
foreach (var (code, language) in SupportedLanguageFlags2)
{
if (options.Contains(code) && !languages.Contains(language))
languages.Add(language);
@@ -128,4 +146,12 @@ public static class DLSiteToDomainMapper
return AIGeneration.None;
}
private static AgeRating MapAgeRating(ProductInfo productInfo)
{
if (AgeRatingMap.TryGetValue(productInfo.AgeCategory, out AgeRating ageRating))
return ageRating;
return AgeRating.R18;
}
}

View File

@@ -7,12 +7,21 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.12.3" />
<None Remove="Common\Languages\Language.xml" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Common\Languages\Language.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.9" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="NTextCat" Version="0.3.65" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
</ItemGroup>

View File

@@ -1,4 +1,5 @@
using JSMR.Infrastructure.Common.Locales;
using JSMR.Application.Common;
using JSMR.Infrastructure.Common.Locales;
using JSMR.Infrastructure.Common.SupportedLanguages;
namespace JSMR.Infrastructure.Scanning;

View File

@@ -1,4 +1,5 @@
using JSMR.Infrastructure.Common.Locales;
using JSMR.Application.Common;
using JSMR.Infrastructure.Common.Locales;
using JSMR.Infrastructure.Common.SupportedLanguages;
using JSMR.Infrastructure.Http;
using System.Globalization;

View File

@@ -1,4 +1,5 @@
using JSMR.Infrastructure.Common.Locales;
using JSMR.Application.Common;
using JSMR.Infrastructure.Common.Locales;
using JSMR.Infrastructure.Common.SupportedLanguages;
using JSMR.Infrastructure.Http;
using System.Text.RegularExpressions;

View File

@@ -1,4 +1,5 @@
using HtmlAgilityPack;
using JSMR.Application.Common;
using JSMR.Application.Scanning.Contracts;
using JSMR.Application.Scanning.Ports;
using JSMR.Infrastructure.Common.Locales;
@@ -7,6 +8,7 @@ using JSMR.Infrastructure.Http;
using JSMR.Infrastructure.Scanning.Models;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace JSMR.Infrastructure.Scanning;
@@ -90,7 +92,8 @@ public abstract class VoiceWorksScanner(IHtmlLoader htmlLoader) : IVoiceWorksSca
ImageUrl = imageUrl,
Type = imageUrl.Contains("ana/doujin") ? DLSiteWorkType.Announced : DLSiteWorkType.Released,
StarRating = rating?.Score,
Votes = rating?.Votes
Votes = rating?.Votes,
AgeRating = GetAgeRating(node.GenreNodes)
};
if (node.ExpectedDateNode != null)
@@ -111,6 +114,19 @@ public abstract class VoiceWorksScanner(IHtmlLoader htmlLoader) : IVoiceWorksSca
return work;
}
private static AgeRating GetAgeRating(List<HtmlNode> genreNodes)
{
List<string> genres = ScannerUtilities.GetStringListFromNodes(genreNodes);
if (genres.Contains("全年齢"))
return AgeRating.AllAges;
if (genres.Contains("R-15"))
return AgeRating.R15;
return AgeRating.R18;
}
private static ScannedRating? GetScannedRating(HtmlNode starRatingNode)
{
if (starRatingNode == null)