diff --git a/JSMR.Application/Scanning/Contracts/DLSiteWork.cs b/JSMR.Application/Scanning/Contracts/DLSiteWork.cs index a390772..a7459ce 100644 --- a/JSMR.Application/Scanning/Contracts/DLSiteWork.cs +++ b/JSMR.Application/Scanning/Contracts/DLSiteWork.cs @@ -4,11 +4,8 @@ namespace JSMR.Application.Scanning.Contracts; public class DLSiteWork { - //public DLSiteWorkType Type { get; set; } - //public DLSiteWorkCategory Category { get; set; } public required string ProductName { get; set; } public required string ProductId { get; set; } - //public DateOnly? AnnouncedDate { get; set; } public DateOnly? ExpectedDate { get; set; } public DateOnly? SalesDate { get; set; } public int Downloads { get; set; } diff --git a/JSMR.Infrastructure/Scanning/ReleasedWorksProvider.cs b/JSMR.Infrastructure/Scanning/ReleasedWorksProvider.cs index 661f871..6a42ed6 100644 --- a/JSMR.Infrastructure/Scanning/ReleasedWorksProvider.cs +++ b/JSMR.Infrastructure/Scanning/ReleasedWorksProvider.cs @@ -8,6 +8,8 @@ namespace JSMR.Infrastructure.Scanning; public class ReleasedWorksProvider(IDLSiteClient dlsiteClient) : IReleasedWorksProvider { + private const int MaxPeriodDays = 60; + public async Task GetReleasedWorksAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken) { DateOnly[] salesDates = @@ -20,20 +22,72 @@ public class ReleasedWorksProvider(IDLSiteClient dlsiteClient) : IReleasedWorksP if (salesDates.Length == 0) return []; + HashSet productIds = [.. scanResult.Works.Select(x => x.ProductId)]; + DateOnly minDate = salesDates.Min(); DateOnly maxDate = salesDates.Max(); - DateOnly requestDate = minDate.AddDays(-1); - DateOnly requestEndDate = maxDate.AddDays(1); + ReleasedWorksCollection collection = []; - int period = (requestEndDate.DayNumber - requestDate.DayNumber) + 1; + DateOnly chunkStart = minDate; - ReleasedWorksRequest releasedWorksRequest = new( - Locale: Locale.English, - Date: requestEndDate, - Period: period - ); + while (chunkStart <= maxDate) + { + int endDayNumber = Math.Min(chunkStart.DayNumber + MaxPeriodDays - 1, maxDate.DayNumber); + DateOnly chunkEnd = DateOnly.FromDayNumber(endDayNumber); - return await dlsiteClient.GetReleasedWorksAsync(releasedWorksRequest, cancellationToken); + int period = chunkEnd.DayNumber - chunkStart.DayNumber + 1; + + ReleasedWorksRequest request = new( + Locale: Locale.English, + Date: chunkEnd, + Period: period); + + ReleasedWorksCollection chunk = await dlsiteClient.GetReleasedWorksAsync(request, cancellationToken); + + foreach (string productId in chunk.Keys) + { + if (productIds.Contains(productId) == false) + continue; + + if (collection.ContainsKey(productId)) + continue; + + collection.Add(productId, chunk[productId]); + } + + chunkStart = chunkEnd.AddDays(1); + } + + return collection; } + + //public async Task GetReleasedWorksAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken) + //{ + // DateOnly[] salesDates = + // [ + // .. scanResult.Works + // .Where(x => x.SalesDate.HasValue) + // .Select(x => x.SalesDate!.Value) + // ]; + + // if (salesDates.Length == 0) + // return []; + + // DateOnly minDate = salesDates.Min(); + // DateOnly maxDate = salesDates.Max(); + + // DateOnly requestDate = minDate.AddDays(-1); + // DateOnly requestEndDate = maxDate.AddDays(1); + + // int period = (requestEndDate.DayNumber - requestDate.DayNumber) + 1; + + // ReleasedWorksRequest releasedWorksRequest = new( + // Locale: Locale.English, + // Date: requestEndDate, + // Period: period + // ); + + // return await dlsiteClient.GetReleasedWorksAsync(releasedWorksRequest, cancellationToken); + //} } \ No newline at end of file diff --git a/JSMR.Tests/Unit/ReleasedWorksProviderTests.cs b/JSMR.Tests/Unit/ReleasedWorksProviderTests.cs new file mode 100644 index 0000000..fb09a86 --- /dev/null +++ b/JSMR.Tests/Unit/ReleasedWorksProviderTests.cs @@ -0,0 +1,288 @@ +using JSMR.Application.Enums; +using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks; +using JSMR.Application.Integrations.DLSite.Ports; +using JSMR.Application.Scanning.Contracts; +using JSMR.Infrastructure.Scanning; +using NSubstitute; +using Shouldly; + +namespace JSMR.Tests.Unit; + +public class ReleasedWorksProviderTests +{ + private readonly IDLSiteClient _dlsiteClient = Substitute.For(); + + [Fact] + public async Task GetReleasedWorksAsync_WhenNoSalesDates_ReturnsEmptyAndDoesNotCallClient() + { + VoiceWorkScanResult scanResult = new( + Works: + [ + new DLSiteWork + { + ProductId = "RJ001", + SalesDate = null, + ProductName = "", + MakerId = "", + Maker = "", + ImageUrl = "", + SmallImageUrl = "" + } + ], + EndOfResults: false + ); + + ReleasedWorksProvider provider = new(_dlsiteClient); + + ReleasedWorksCollection result = await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken); + + result.ShouldBeEmpty(); + + await _dlsiteClient.DidNotReceiveWithAnyArgs().GetReleasedWorksAsync(default!, TestContext.Current.CancellationToken); + } + + [Fact] + public async Task GetReleasedWorksAsync_WhenRangeIsUnder60Days_CallsClientOnce() + { + VoiceWorkScanResult scanResult = new( + Works: + [ + new DLSiteWork + { + ProductId = "RJ001", + SalesDate = new DateOnly(2024, 1, 10), + ProductName = "", + MakerId = "", + Maker = "", + ImageUrl = "", + SmallImageUrl = "" + }, + new DLSiteWork + { + ProductId = "RJ002", + SalesDate = new DateOnly(2024, 1, 20), + ProductName = "", + MakerId = "", + Maker = "", + ImageUrl = "", + SmallImageUrl = "" + } + ], + EndOfResults: false + ); + + ReleasedWorksCollection apiResult = new() + { + ["RJ001"] = new ReleasedWork + { + ProductId = "RJ001", + Title = "English title 1", + Description = "Description", + MaskedTitle = "English title 1", + MaskedDescription = "Description", + }, + ["RJ002"] = new ReleasedWork + { + ProductId = "RJ002", + Title = "English title 2", + Description = "Description", + MaskedTitle = "English title 2", + MaskedDescription = "Description", + } + }; + + _dlsiteClient + .GetReleasedWorksAsync(Arg.Any(), Arg.Any()) + .Returns(apiResult); + + ReleasedWorksProvider provider = new(_dlsiteClient); + + ReleasedWorksCollection result = + await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken); + + result.Keys.ShouldBe(["RJ001", "RJ002"], ignoreOrder: true); + + await _dlsiteClient.Received(1).GetReleasedWorksAsync( + Arg.Is(x => + x.Locale == Locale.English && + x.Date == new DateOnly(2024, 1, 20) && + x.Period == 11), + Arg.Any()); + } + + [Fact] + public async Task GetReleasedWorksAsync_WhenRangeExceeds60Days_SplitsIntoMultipleRequests() + { + VoiceWorkScanResult scanResult = new( + Works: + [ + new DLSiteWork + { + ProductId = "RJ001", + SalesDate = new DateOnly(2024, 1, 1), + ProductName = "", + MakerId = "", + Maker = "", + ImageUrl = "", + SmallImageUrl = "" + }, + new DLSiteWork + { + ProductId = "RJ002", + SalesDate = new DateOnly(2024, 3, 5), + ProductName = "", + MakerId = "", + Maker = "", + ImageUrl = "", + SmallImageUrl = "" + } + ], + EndOfResults: false + ); + + _dlsiteClient + .GetReleasedWorksAsync(Arg.Any(), Arg.Any()) + .Returns([]); + + ReleasedWorksProvider provider = new(_dlsiteClient); + + await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken); + + await _dlsiteClient.Received(1).GetReleasedWorksAsync( + Arg.Is(x => + x.Date == new DateOnly(2024, 2, 29) && + x.Period == 60), + Arg.Any()); + + await _dlsiteClient.Received(1).GetReleasedWorksAsync( + Arg.Is(x => + x.Date == new DateOnly(2024, 3, 5) && + x.Period == 5), + Arg.Any()); + + await _dlsiteClient.Received(2).GetReleasedWorksAsync( + Arg.Any(), + Arg.Any()); + } + + [Fact] + public async Task GetReleasedWorksAsync_FiltersOutProductsNotInScanResult() + { + VoiceWorkScanResult scanResult = new( + Works: + [ + new DLSiteWork + { + ProductId = "RJ001", + SalesDate = new DateOnly(2024, 1, 10), + ProductName = "", + MakerId = "", + Maker = "", + ImageUrl = "", + SmallImageUrl = "" + } + ], + EndOfResults: false + ); + + ReleasedWorksCollection apiResult = new() + { + ["RJ001"] = new ReleasedWork + { + ProductId = "RJ001", + Title = "Keep me", + Description = "Description", + MaskedTitle = "Keep me", + MaskedDescription = "Description", + }, + ["RJ999"] = new ReleasedWork + { + ProductId = "RJ999", + Title = "Ignore me", + Description = "Description", + MaskedTitle = "Ignore me", + MaskedDescription = "Description", + }, + }; + + _dlsiteClient + .GetReleasedWorksAsync(Arg.Any(), Arg.Any()) + .Returns(apiResult); + + ReleasedWorksProvider provider = new(_dlsiteClient); + + ReleasedWorksCollection result = + await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken); + + result.Keys.ShouldBe(["RJ001"]); + } + + [Fact] + public async Task GetReleasedWorksAsync_WhenSameProductReturnedTwice_KeepsFirstResult() + { + VoiceWorkScanResult scanResult = new( + Works: + [ + new DLSiteWork + { + ProductId = "RJ001", + SalesDate = new DateOnly(2024, 1, 1), + ProductName = "", + MakerId = "", + Maker = "", + ImageUrl = "", + SmallImageUrl = "" + }, + new DLSiteWork + { + ProductId = "RJ002", + SalesDate = new DateOnly(2024, 3, 5), + ProductName = "", + MakerId = "", + Maker = "", + ImageUrl = "", + SmallImageUrl = "" + } + ], + EndOfResults: false + ); + + _dlsiteClient + .GetReleasedWorksAsync( + Arg.Is(x => x.Period == 60), + Arg.Any()) + .Returns(new ReleasedWorksCollection + { + ["RJ001"] = new ReleasedWork + { + ProductId = "RJ001", + Title = "First", + Description = "Description", + MaskedTitle = "First", + MaskedDescription = "Description", + } + }); + + _dlsiteClient + .GetReleasedWorksAsync( + Arg.Is(x => x.Period == 5), + Arg.Any()) + .Returns(new ReleasedWorksCollection + { + ["RJ001"] = new ReleasedWork + { + ProductId = "RJ001", + Title = "Second", + Description = "Description", + MaskedTitle = "Second", + MaskedDescription = "Description", + } + }); + + ReleasedWorksProvider provider = new(_dlsiteClient); + + ReleasedWorksCollection result = await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken); + + result["RJ001"].Title.ShouldBe("First"); + } +} \ No newline at end of file