Added additional voice work update logic.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
using JSMR.Application.Common;
|
||||
|
||||
namespace JSMR.Infrastructure.Common.Languages;
|
||||
|
||||
public interface ILanguageIdentifier
|
||||
{
|
||||
Language GetLanguage(string text);
|
||||
}
|
||||
56094
JSMR.Infrastructure/Common/Languages/Language.xml
Normal file
56094
JSMR.Infrastructure/Common/Languages/Language.xml
Normal file
File diff suppressed because it is too large
Load Diff
44
JSMR.Infrastructure/Common/Languages/LanguageIdentifier.cs
Normal file
44
JSMR.Infrastructure/Common/Languages/LanguageIdentifier.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace JSMR.Infrastructure.Common.SupportedLanguages;
|
||||
|
||||
public interface ISupportedLanguage
|
||||
{
|
||||
string Code { get; }
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
6
JSMR.Infrastructure/Common/Time/Clock.cs
Normal file
6
JSMR.Infrastructure/Common/Time/Clock.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace JSMR.Infrastructure.Common.Time;
|
||||
|
||||
public class Clock : IClock
|
||||
{
|
||||
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
|
||||
}
|
||||
6
JSMR.Infrastructure/Common/Time/IClock.cs
Normal file
6
JSMR.Infrastructure/Common/Time/IClock.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace JSMR.Infrastructure.Common.Time;
|
||||
|
||||
public interface IClock
|
||||
{
|
||||
DateTimeOffset UtcNow { get; }
|
||||
}
|
||||
69
JSMR.Infrastructure/Common/Time/TimeProvider.cs
Normal file
69
JSMR.Infrastructure/Common/Time/TimeProvider.cs
Normal 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"];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user