Added additional voice work update logic.
This commit is contained in:
7
JSMR.Application/Common/ISupportedLanguage.cs
Normal file
7
JSMR.Application/Common/ISupportedLanguage.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace JSMR.Application.Common;
|
||||
|
||||
public interface ISupportedLanguage
|
||||
{
|
||||
string Code { get; }
|
||||
Language Language { get; }
|
||||
}
|
||||
@@ -6,10 +6,11 @@ public class VoiceWorkDetails
|
||||
{
|
||||
public VoiceWorkSeries? Series { get; init; }
|
||||
public VoiceWorkTranslation? Translation { get; init; }
|
||||
public AgeRating AgeRating { get; init; }
|
||||
public int WishlistCount { get; init; }
|
||||
public int DownloadCount { get; init; }
|
||||
public DateTime? RegistrationDate { get; init; }
|
||||
public Language[] SupportedLanguages { get; init; } = [];
|
||||
public ISupportedLanguage[] SupportedLanguages { get; init; } = [];
|
||||
public AIGeneration AI { get; init; }
|
||||
public bool HasTrial { get; init; }
|
||||
public bool HasDLPlay { get; init; }
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace JSMR.Application.Scanning.Contracts;
|
||||
using JSMR.Application.Common;
|
||||
|
||||
namespace JSMR.Application.Scanning.Contracts;
|
||||
|
||||
public class DLSiteWork
|
||||
{
|
||||
@@ -19,6 +21,7 @@ public class DLSiteWork
|
||||
public bool HasTrial { get; set;}
|
||||
public ICollection<string> Tags { get; set; } = [];
|
||||
public ICollection<string> Creators { get; set; } = [];
|
||||
public string? ImageUrl { get; set; }
|
||||
public string? SmallImageUrl { get; set; }
|
||||
public required string ImageUrl { get; set; }
|
||||
public required string SmallImageUrl { get; set; }
|
||||
public AgeRating AgeRating { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using JSMR.Application.Integrations.DLSite.Models;
|
||||
using JSMR.Application.Common;
|
||||
using JSMR.Application.Integrations.DLSite.Models;
|
||||
|
||||
namespace JSMR.Application.Scanning.Contracts;
|
||||
|
||||
@@ -17,5 +18,11 @@ public record VoiceWorkIngest(DLSiteWork Work, VoiceWorkDetails? Details)
|
||||
public bool HasDLPlay { get; init; } = Details?.HasDLPlay ?? false;
|
||||
public byte? StarRating { get; init; } = Work.StarRating;
|
||||
public int? Votes { get; init; } = Work.Votes;
|
||||
public AgeRating AgeRating { get; init; } = Details?.AgeRating ?? Work.AgeRating;
|
||||
public bool HasImage { get; init; } = Work.ImageUrl.Contains("no_img") == false;
|
||||
public ICollection<ISupportedLanguage> SupportedLanguages { get; init; } = Details?.SupportedLanguages ?? [];
|
||||
public DateOnly? ExpectedDate { get; init; } = Work.ExpectedDate;
|
||||
public DateOnly? SalesDate { get; init; } = Work.SalesDate;
|
||||
public DateTime? RegistrationDate { get; init; } = Details?.RegistrationDate;
|
||||
// TODO: Other properties
|
||||
}
|
||||
@@ -20,7 +20,7 @@ public class VoiceWork
|
||||
public bool? IsValid { get; set; }
|
||||
public DateTime? LastScannedDate { get; set; }
|
||||
public byte Status { get; set; }
|
||||
public byte SubtitleLanguage { get; set; }
|
||||
public byte SubtitleLanguage { get; set; } // TO BE DEPRECATED
|
||||
public bool HasChobit { get; set; }
|
||||
public bool IsPurchased { get; set; }
|
||||
public int? WishlistCount { get; set; }
|
||||
@@ -36,8 +36,9 @@ public class VoiceWork
|
||||
//public int? VoiceWorkSearchId { get; set; }
|
||||
//public VoiceWorkSearch? VoiceWorkSearch { get; set; }
|
||||
|
||||
public virtual ICollection<VoiceWorkTag> VoiceWorkTags { get; set; } = [];
|
||||
public virtual ICollection<VoiceWorkCreator> VoiceWorkCreators { get; set; } = [];
|
||||
public virtual ICollection<VoiceWorkTag> Tags { get; set; } = [];
|
||||
public virtual ICollection<VoiceWorkCreator> Creators { get; set; } = [];
|
||||
public virtual ICollection<EnglishVoiceWork> EnglishVoiceWorks { get; set; } = [];
|
||||
public virtual ICollection<VoiceWorkSupportedLanguage> SupportedLanguages { get; set; } = [];
|
||||
public virtual ICollection<VoiceWorkLocalization> Localizations { get; set; } = [];
|
||||
}
|
||||
10
JSMR.Domain/Entities/VoiceWorkSupportedLanguage.cs
Normal file
10
JSMR.Domain/Entities/VoiceWorkSupportedLanguage.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace JSMR.Domain.Entities;
|
||||
|
||||
public sealed class VoiceWorkSupportedLanguage
|
||||
{
|
||||
public int VoiceWorkSupportedLanguageId { get; set; }
|
||||
public int VoiceWorkId { get; set; }
|
||||
public string Language { get; set; } = null!;
|
||||
|
||||
public VoiceWork VoiceWork { get; set; } = null!;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
201
JSMR.Tests/Fixtures/VoiceWorkUpsertFixture.cs
Normal file
201
JSMR.Tests/Fixtures/VoiceWorkUpsertFixture.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
using JSMR.Application.Common;
|
||||
using JSMR.Application.Integrations.DLSite.Models;
|
||||
using JSMR.Application.Scanning.Contracts;
|
||||
using JSMR.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JSMR.Tests.Fixtures;
|
||||
|
||||
public class VoiceWorkUpsertFixture : MariaDbFixture
|
||||
{
|
||||
protected override async Task OnInitializedAsync(AppDbContext context)
|
||||
{
|
||||
await SeedAsync(context);
|
||||
}
|
||||
|
||||
private static async Task SeedAsync(AppDbContext context)
|
||||
{
|
||||
if (await context.VoiceWorks.AnyAsync())
|
||||
return;
|
||||
|
||||
context.Circles.AddRange(
|
||||
new() { CircleId = 1, Name = "Good Dreams", MakerId = "RG00001" },
|
||||
new() { CircleId = 2, Name = "Sweet Dreams", Favorite = true, MakerId = "RG00002" },
|
||||
new() { CircleId = 3, Name = "Nightmare Fuel", Blacklisted = true, MakerId = "RG00003" }
|
||||
);
|
||||
|
||||
context.VoiceWorks.AddRange(
|
||||
new() { VoiceWorkId = 1, CircleId = 1, ProductId = "RJ0000001", ProductName = "Today Sounds", Description = "An average product.", Status = (byte)VoiceWorkStatus.Available, SalesDate = new(2025, 1, 1), Downloads = 500, WishlistCount = 750, StarRating = 35 },
|
||||
new() { VoiceWorkId = 2, CircleId = 2, ProductId = "RJ0000002", ProductName = "Super Comfy ASMR", Description = "An amazing product!", Status = (byte)VoiceWorkStatus.NewRelease, SalesDate = new(2025, 1, 3), Downloads = 5000, WishlistCount = 12000, StarRating = 50, Favorite = true },
|
||||
new() { VoiceWorkId = 3, CircleId = 3, ProductId = "RJ0000003", ProductName = "Low Effort", Description = "A bad product.", Status = (byte)VoiceWorkStatus.Available, SalesDate = new(2025, 1, 2), Downloads = 50, WishlistCount = 100, StarRating = 20 },
|
||||
new() { VoiceWorkId = 4, CircleId = 1, ProductId = "RJ0000004", ProductName = "Tomorrow Sounds", Description = "A average upcoming product.", Status = (byte)VoiceWorkStatus.Upcoming, ExpectedDate = new(2025, 1, 1), WishlistCount = 300 },
|
||||
new() { VoiceWorkId = 5, CircleId = 2, ProductId = "RJ0000005", ProductName = "Super Comfy ASMR+", Description = "All your favorite sounds, plus more!", Status = (byte)VoiceWorkStatus.NewAndUpcoming, ExpectedDate = new(2025, 1, 11), WishlistCount = 10000 }
|
||||
);
|
||||
|
||||
context.Tags.AddRange(
|
||||
new() { TagId = 1, Name = "ASMR" },
|
||||
new() { TagId = 2, Name = "OL" },
|
||||
new() { TagId = 3, Name = "ほのぼの" },
|
||||
new() { TagId = 4, Name = "エルフ/妖精" },
|
||||
new() { TagId = 5, Name = "ツンデレ", Favorite = true },
|
||||
new() { TagId = 6, Name = "オールハッピー" },
|
||||
new() { TagId = 7, Name = "ギャル" },
|
||||
new() { TagId = 8, Name = "メイド" },
|
||||
new() { TagId = 9, Name = "ノンフィクション/体験談", Blacklisted = true }
|
||||
);
|
||||
|
||||
context.EnglishTags.AddRange(
|
||||
new() { EnglishTagId = 1, TagId = 1, Name = "ASMR" },
|
||||
new() { EnglishTagId = 2, TagId = 2, Name = "Office Lady" },
|
||||
new() { EnglishTagId = 3, TagId = 3, Name = "Heartwarming" },
|
||||
new() { EnglishTagId = 4, TagId = 4, Name = "Elf / Fairy" },
|
||||
new() { EnglishTagId = 5, TagId = 5, Name = "Tsundere" },
|
||||
new() { EnglishTagId = 6, TagId = 6, Name = "All Happy" },
|
||||
new() { EnglishTagId = 7, TagId = 7, Name = "Gal" },
|
||||
new() { EnglishTagId = 8, TagId = 8, Name = "Maid" },
|
||||
new() { EnglishTagId = 9, TagId = 9, Name = "Non-Fiction / Narrative" }
|
||||
);
|
||||
|
||||
context.VoiceWorkTags.AddRange(
|
||||
new() { VoiceWorkId = 1, TagId = 1 }, // ASMR
|
||||
new() { VoiceWorkId = 1, TagId = 2 }, // Office Lady
|
||||
|
||||
new() { VoiceWorkId = 2, TagId = 1 }, // ASMR
|
||||
new() { VoiceWorkId = 2, TagId = 3 }, // Heartwarming
|
||||
new() { VoiceWorkId = 2, TagId = 4 }, // Elf / Fairy
|
||||
new() { VoiceWorkId = 2, TagId = 5 }, // Tsundere
|
||||
new() { VoiceWorkId = 2, TagId = 6 }, // All Happy
|
||||
new() { VoiceWorkId = 2, TagId = 7 }, // Gal
|
||||
new() { VoiceWorkId = 2, TagId = 8 }, // Maid
|
||||
|
||||
new() { VoiceWorkId = 3, TagId = 5 }, // Tsundere
|
||||
new() { VoiceWorkId = 3, TagId = 9 } // Non-Fiction / Narrative
|
||||
//new() { VoiceWorkId = 3, TagId = 1 },
|
||||
//new() { VoiceWorkId = 3, TagId = 1 },
|
||||
//new() { VoiceWorkId = 3, TagId = 1 },
|
||||
//new() { VoiceWorkId = 3, TagId = 1 },
|
||||
//new() { VoiceWorkId = 3, TagId = 1 },
|
||||
|
||||
//new() { VoiceWorkId = 4, TagId = 1 },
|
||||
//new() { VoiceWorkId = 4, TagId = 1 },
|
||||
//new() { VoiceWorkId = 4, TagId = 1 },
|
||||
//new() { VoiceWorkId = 4, TagId = 1 },
|
||||
//new() { VoiceWorkId = 4, TagId = 1 },
|
||||
//new() { VoiceWorkId = 4, TagId = 1 },
|
||||
//new() { VoiceWorkId = 4, TagId = 1 },
|
||||
|
||||
//new() { VoiceWorkId = 5, TagId = 5 } // Tsundere
|
||||
//new() { VoiceWorkId = 5, TagId = 1 },
|
||||
//new() { VoiceWorkId = 5, TagId = 1 },
|
||||
//new() { VoiceWorkId = 5, TagId = 1 },
|
||||
//new() { VoiceWorkId = 5, TagId = 1 },
|
||||
//new() { VoiceWorkId = 5, TagId = 1 },
|
||||
//new() { VoiceWorkId = 5, TagId = 1 }
|
||||
);
|
||||
|
||||
context.Creators.AddRange(
|
||||
new() { CreatorId = 1, Name = "陽向葵ゅか", Favorite = true },
|
||||
new() { CreatorId = 2, Name = "秋野かえで" },
|
||||
new() { CreatorId = 3, Name = "柚木つばめ" },
|
||||
new() { CreatorId = 4, Name = "逢坂成美" },
|
||||
new() { CreatorId = 5, Name = "山田じぇみ子", Blacklisted = true }
|
||||
);
|
||||
|
||||
context.VoiceWorkCreators.AddRange(
|
||||
new() { VoiceWorkId = 1, CreatorId = 2 }, // 秋野かえで
|
||||
|
||||
new() { VoiceWorkId = 2, CreatorId = 1 }, // 陽向葵ゅか
|
||||
|
||||
new() { VoiceWorkId = 3, CreatorId = 5 }, // 山田じぇみ子
|
||||
new() { VoiceWorkId = 3, CreatorId = 1 }, // 陽向葵ゅか
|
||||
|
||||
new() { VoiceWorkId = 4, CreatorId = 3 }, // 柚木つばめ
|
||||
|
||||
new() { VoiceWorkId = 5, CreatorId = 1 }, // 陽向葵ゅか
|
||||
new() { VoiceWorkId = 5, CreatorId = 4 } // 逢坂成美
|
||||
);
|
||||
|
||||
// <Product Id> <Maker Id> <Circle Name> <Product Name> <Product Description> <Tags> <Creators>
|
||||
context.VoiceWorkSearches.AddRange(
|
||||
new() { VoiceWorkId = 1, SearchText = "RJ0000001 RG00001 Good Dreams Today Sounds An average product. ASMR Office Lady" },
|
||||
new() { VoiceWorkId = 2, SearchText = "RJ0000002 RG00002 Sweet Dreams Super Comfy ASMR An amazing product! ASMR Heartwarming Elf / Fairy Tsundere All Happy Gal Maid" },
|
||||
new() { VoiceWorkId = 3, SearchText = "RJ0000003 RG00003 Nightmare Fuel Low Effort A bad product." },
|
||||
new() { VoiceWorkId = 4, SearchText = "RJ0000004 RG00001 Good Dreams Tomorrow Sounds A average upcoming product." },
|
||||
new() { VoiceWorkId = 5, SearchText = "RJ0000005 RG00002 Sweet Dreams Super Comfy ASMR+ All your favorite sounds, plus more!" }
|
||||
);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public VoiceWorkIngest[] GetFirstRoundIngests()
|
||||
{
|
||||
DLSiteWork work1 = new()
|
||||
{
|
||||
ProductId = "001",
|
||||
ProductName = "fdsfs",
|
||||
MakerId = "RG00001",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
};
|
||||
|
||||
VoiceWorkDetails details1 = new()
|
||||
{
|
||||
AgeRating = AgeRating.R18,
|
||||
};
|
||||
|
||||
VoiceWorkIngest[] ingests =
|
||||
[
|
||||
new(work1, details1)
|
||||
{
|
||||
MakerId = "RG00001",
|
||||
MakerName = "Good Dreams",
|
||||
ProductId = "RJ0000001",
|
||||
Title = "Today Sounds",
|
||||
Description = "An average product.",
|
||||
Tags = ["ASMR", "Office Lady"],
|
||||
AgeRating = AgeRating.R15,
|
||||
SalesDate = new DateOnly(2025, 1, 1),
|
||||
ExpectedDate = null,
|
||||
WishlistCount = 750,
|
||||
Downloads = 500,
|
||||
HasTrial = true,
|
||||
HasDLPlay = true
|
||||
},
|
||||
new(work1, details1)
|
||||
{
|
||||
MakerId = "RG00002",
|
||||
MakerName = "Sweet Dreams",
|
||||
ProductId = "RJ0000002",
|
||||
Title = "Super Comfy ASMR",
|
||||
Description = "An amazing product!",
|
||||
AgeRating = AgeRating.AllAges,
|
||||
Tags = ["ASMR", "Heartwarming", "Elf / Fairy", "Tsundere", "All Happy", "Gal", "Maid"],
|
||||
SalesDate = new DateOnly(2025, 3, 1),
|
||||
ExpectedDate = null,
|
||||
WishlistCount = 12000,
|
||||
Downloads = 5000,
|
||||
HasTrial = true,
|
||||
HasDLPlay = true
|
||||
},
|
||||
new(work1, details1)
|
||||
{
|
||||
MakerId = "RG00003",
|
||||
MakerName = "Nightmare Fuel",
|
||||
ProductId = "RJ0000003",
|
||||
Title = "Low Effort",
|
||||
Description = "A bad product.",
|
||||
Tags = ["Tsundere", "Non-Fiction / Narrative"],
|
||||
AgeRating = AgeRating.R18,
|
||||
SalesDate = new DateOnly(2025, 1, 1),
|
||||
ExpectedDate = null,
|
||||
WishlistCount = 100,
|
||||
Downloads = 50,
|
||||
HasTrial = true,
|
||||
HasDLPlay = false
|
||||
}
|
||||
];
|
||||
|
||||
return ingests;
|
||||
}
|
||||
}
|
||||
47
JSMR.Tests/Integration/VoiceWorkUpsertTests.cs
Normal file
47
JSMR.Tests/Integration/VoiceWorkUpsertTests.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using JSMR.Application.Integrations.DLSite.Models;
|
||||
using JSMR.Application.Scanning.Contracts;
|
||||
using JSMR.Infrastructure.Common.Time;
|
||||
using JSMR.Infrastructure.Data;
|
||||
using JSMR.Infrastructure.Data.Repositories.VoiceWorks;
|
||||
using JSMR.Tests.Fixtures;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JSMR.Tests.Integration;
|
||||
|
||||
public class VoiceWorkUpsertTests(VoiceWorkUpsertFixture fixture) : IClassFixture<VoiceWorkUpsertFixture>
|
||||
{
|
||||
[Fact]
|
||||
public async Task Filter_None()
|
||||
{
|
||||
await using AppDbContext context = fixture.CreateDbContext();
|
||||
|
||||
ITimeProvider timeProvider = Substitute.For<ITimeProvider>();
|
||||
timeProvider.Now().Returns(new DateTimeOffset(2025, 10, 1, 0, 0, 0, 0, TimeSpan.FromSeconds(0)));
|
||||
|
||||
DLSiteWork work1 = new()
|
||||
{
|
||||
ProductId = "001",
|
||||
ProductName = "fdsfs",
|
||||
MakerId = "RG00001",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
};
|
||||
|
||||
VoiceWorkDetails details1 = new()
|
||||
{
|
||||
AgeRating = Application.Common.AgeRating.R18,
|
||||
};
|
||||
|
||||
VoiceWorkIngest[] ingests =
|
||||
[
|
||||
new(work1, details1)
|
||||
];
|
||||
|
||||
VoiceWorkWriter writer = new(context, timeProvider);
|
||||
await writer.UpsertAsync(ingests, CancellationToken.None);
|
||||
|
||||
context.VoiceWorks.Count().ShouldBe(2);
|
||||
}
|
||||
}
|
||||
@@ -22,14 +22,14 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
<PackageReference Include="Testcontainers" Version="4.7.0" />
|
||||
<PackageReference Include="Testcontainers.MariaDb" Version="4.7.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
Reference in New Issue
Block a user