From 0a85555f383e7103a47e9fdb3a36a3c87fd0488c Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Wed, 17 Mar 2021 14:29:50 -0500 Subject: [PATCH] Temp commit to record attempts. Stream APIs are implemented and working. Added some new test cases based on deployed Kavita server testing. --- API.Tests/ParserTest.cs | 7 +- API/API.csproj | 5 + API/Comparators/ChapterSortComparer.cs | 21 +++- API/Controllers/SeriesController.cs | 9 ++ API/DTOs/InProgressChapterDto.cs | 23 +++++ API/DTOs/SeriesDto.cs | 2 + API/Data/SeriesRepository.cs | 97 ++++++------------- API/Data/UnitOfWork.cs | 10 +- API/Data/VolumeRepository.cs | 97 ++++++++++++++++++- .../ApplicationServiceExtensions.cs | 9 +- API/Extensions/EnumerableExtensions.cs | 21 ++++ API/Helpers/AutoMapperProfiles.cs | 1 + API/Interfaces/IVolumeRepository.cs | 1 + API/Program.cs | 4 +- API/Services/Tasks/ScannerService.cs | 2 +- API/Startup.cs | 28 +++++- 16 files changed, 247 insertions(+), 90 deletions(-) create mode 100644 API/DTOs/InProgressChapterDto.cs create mode 100644 API/Extensions/EnumerableExtensions.cs diff --git a/API.Tests/ParserTest.cs b/API.Tests/ParserTest.cs index 6c1b5cfd5..0d13f7628 100644 --- a/API.Tests/ParserTest.cs +++ b/API.Tests/ParserTest.cs @@ -49,6 +49,7 @@ namespace API.Tests [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", "0")] [InlineData("VanDread-v01-c001[MD].zip", "1")] [InlineData("Ichiban_Ushiro_no_Daimaou_v04_ch27_[VISCANS].zip", "4")] + [InlineData("Mob Psycho 100 v02 (2019) (Digital) (Shizu).cbz", "2")] public void ParseVolumeTest(string filename, string expected) { Assert.Equal(expected, ParseVolume(filename)); @@ -100,6 +101,9 @@ namespace API.Tests [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 09", "Kedouin Makoto - Corpse Party Musume")] [InlineData("Goblin Slayer Side Story - Year One 025.5", "Goblin Slayer Side Story - Year One")] [InlineData("Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire)", "Goblin Slayer - Brand New Day")] + [InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", "Yumekui-Merry")] + [InlineData("Yumekui-Merry DKThiasScanlations Chapter51v2", "Yumekui-Merry")] + [InlineData("Yumekui-Merry_DKThiasScanlations&RenzokuseiScans_Chapter61", "Yumekui-Merry")] public void ParseSeriesTest(string filename, string expected) { Assert.Equal(expected, ParseSeries(filename)); @@ -130,6 +134,7 @@ namespace API.Tests [InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")] [InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", "11")] [InlineData("Yumekui-Merry DKThiasScanlations Chapter51v2", "51")] + [InlineData("Yumekui-Merry_DKThiasScanlations&RenzokuseiScans_Chapter61", "61")] [InlineData("Goblin Slayer Side Story - Year One 017.5", "17.5")] [InlineData("Beelzebub_53[KSH].zip", "53")] [InlineData("Black Bullet - v4 c20.5 [batoto]", "20.5")] @@ -139,7 +144,7 @@ namespace API.Tests [InlineData("Vol 1", "0")] [InlineData("VanDread-v01-c001[MD].zip", "1")] [InlineData("Goblin Slayer Side Story - Year One 025.5", "25.5")] - //[InlineData("[Tempus Edax Rerum] Epigraph of the Closed Curve - Chapter 6.zip", "6")] + [InlineData("Mob Psycho 100 v02 (2019) (Digital) (Shizu).cbz", "0")] public void ParseChaptersTest(string filename, string expected) { Assert.Equal(expected, ParseChapter(filename)); diff --git a/API/API.csproj b/API/API.csproj index 008455438..0f6d90fb6 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -6,12 +6,17 @@ true + + false + + + diff --git a/API/Comparators/ChapterSortComparer.cs b/API/Comparators/ChapterSortComparer.cs index 725622bec..1798afe7e 100644 --- a/API/Comparators/ChapterSortComparer.cs +++ b/API/Comparators/ChapterSortComparer.cs @@ -2,15 +2,26 @@ namespace API.Comparators { - public class ChapterSortComparer : IComparer + public class ChapterSortComparer : IComparer { - public int Compare(int x, int y) + // public int Compare(int x, int y) + // { + // if (x == 0 && y == 0) return 0; + // // if x is 0, it comes second + // if (x == 0) return 1; + // // if y is 0, it comes second + // if (y == 0) return -1; + // + // return x.CompareTo(y); + // } + + public int Compare(float x, float y) { - if (x == 0 && y == 0) return 0; + if (x == 0.0 && y == 0.0) return 0; // if x is 0, it comes second - if (x == 0) return 1; + if (x == 0.0) return 1; // if y is 0, it comes second - if (y == 0) return -1; + if (y == 0.0) return -1; return x.CompareTo(y); } diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 611fff59b..c1e4c9b73 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -140,6 +140,15 @@ namespace API.Controllers return Ok(await _unitOfWork.SeriesRepository.GetInProgress(user.Id, libraryId, limit)); } + [HttpGet("continue-reading")] + public async Task>> GetContinueReading(int libraryId = 0, int limit = 20) + { + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + return Ok(await _unitOfWork.VolumeRepository.GetContinueReading(user.Id, libraryId, limit)); + } + + + } } \ No newline at end of file diff --git a/API/DTOs/InProgressChapterDto.cs b/API/DTOs/InProgressChapterDto.cs new file mode 100644 index 000000000..f0a0096ef --- /dev/null +++ b/API/DTOs/InProgressChapterDto.cs @@ -0,0 +1,23 @@ +namespace API.DTOs +{ + public class InProgressChapterDto + { + public int Id { get; init; } + /// + /// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2". + /// + public string Range { get; init; } + /// + /// Smallest number of the Range. + /// + public string Number { get; init; } + /// + /// Total number of pages in all MangaFiles + /// + public int Pages { get; init; } + public int SeriesId { get; init; } + public int LibraryId { get; init; } + public string SeriesName { get; init; } + + } +} \ No newline at end of file diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index 593870309..b3057baac 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -21,5 +21,7 @@ /// Review from logged in user. Calculated at API-time. /// public string UserReview { get; set; } + + public int LibraryId { get; set; } } } \ No newline at end of file diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index 29e3aaadb..8c1949edc 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -280,87 +280,52 @@ namespace API.Data } /// - /// Returns a list of Series that were added within 2 weeks. + /// Returns a list of Series that were added, ordered by Created desc /// /// Library to restrict to, if 0, will apply to all libraries - /// + /// How many series to pick. /// public async Task> GetRecentlyAdded(int libraryId, int limit) { - // TODO: Remove 2 week condition - var twoWeeksAgo = DateTime.Today.Subtract(TimeSpan.FromDays(14)); - _logger.LogDebug("2 weeks from today is: {Date}", twoWeeksAgo); return await _context.Series - .Where(s => s.Created > twoWeeksAgo && (libraryId <= 0 || s.LibraryId == libraryId)) + .Where(s => (libraryId <= 0 || s.LibraryId == libraryId)) .Take(limit) - .OrderBy(s => s.Created) + .OrderByDescending(s => s.Created) .AsNoTracking() .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); - } + /// + /// + /// + /// + /// + /// + /// public async Task> GetInProgress(int userId, int libraryId, int limit) { - //&& (libraryId <= 0 || s.Series.LibraryId == libraryId) - var twoWeeksAgo = DateTime.Today.Subtract(TimeSpan.FromDays(14)); - _logger.LogInformation("GetInProgress"); - _logger.LogDebug("2 weeks from today is: {Date}", twoWeeksAgo); - // var series = await _context.Series - // .Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) => new - // { - // Series = s, - // Progress = progress - // }) - // .DefaultIfEmpty() - // .Where(s => s.Series.Created > twoWeeksAgo - // && s.Progress.AppUserId == userId - // && s.Progress.PagesRead > s.Series.Pages) - // .Take(limit) - // .OrderBy(s => s.Series.Created) - // .AsNoTracking() - // .Select(s => s.Series) - // .ProjectTo(_mapper.ConfigurationProvider) - // .ToListAsync(); + //var twoWeeksAgo = DateTime.Today.Subtract(TimeSpan.FromDays(14)); // TODO: Think about moving this to a setting var series = await _context.Series - .Where(s => s.Created > twoWeeksAgo) // && (libraryId <= 0 || s.LibraryId == libraryId) - .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - - await AddSeriesModifiers(userId, series); - - return series.Where(s => s.PagesRead > 0).Take(limit).ToList(); - } - - - public async Task> GetSeriesStream(int userId) - { - // Testing out In Progress to figure out how to design generalized solution - var userProgress = await _context.AppUserProgresses - .Where(p => p.AppUserId == userId && p.PagesRead > 0) - .AsNoTracking() - .ToListAsync(); - if (!userProgress.Any()) return new SeriesDto[] {}; - - var seriesIds = userProgress.Select(p => p.SeriesId).ToList(); - /* - *select P.*, S.Name, S.Pages from AppUserProgresses AS P - LEFT join Series as "S" on s.Id = P.SeriesId - where AppUserId = 1 AND P.PagesRead > 0 AND P.PagesRead < S.Pages - * - */ - - - - - // var series = await _context.Series - // .Where(s => seriesIds.Contains(s.Id) && s.Pages) // I need a join - - - - - return new SeriesDto[] {}; + .Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) => new + { + Series = s, + progress.PagesRead, + progress.AppUserId, + progress.LastModified + }) + .Where(s => s.AppUserId == userId + && s.PagesRead > 0 + && s.PagesRead < s.Series.Pages + && (libraryId <= 0 || s.Series.LibraryId == libraryId) ) + .Take(limit) + .OrderByDescending(s => s.LastModified) + .AsNoTracking() + .Select(s => s.Series) + .Distinct() + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + return series; } } } \ No newline at end of file diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index 4b8ffac81..aa3c9ab5f 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -12,21 +12,21 @@ namespace API.Data private readonly DataContext _context; private readonly IMapper _mapper; private readonly UserManager _userManager; - private readonly ILogger _seriesLogger; + private readonly ILogger _logger; - public UnitOfWork(DataContext context, IMapper mapper, UserManager userManager, ILogger seriesLogger) + public UnitOfWork(DataContext context, IMapper mapper, UserManager userManager, ILogger logger) { _context = context; _mapper = mapper; _userManager = userManager; - _seriesLogger = seriesLogger; + _logger = logger; } - public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper, _seriesLogger); + public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper, _logger); public IUserRepository UserRepository => new UserRepository(_context, _userManager); public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper); - public IVolumeRepository VolumeRepository => new VolumeRepository(_context, _mapper); + public IVolumeRepository VolumeRepository => new VolumeRepository(_context, _mapper, _logger); public ISettingsRepository SettingsRepository => new SettingsRepository(_context, _mapper); diff --git a/API/Data/VolumeRepository.cs b/API/Data/VolumeRepository.cs index 6b9e541ea..4461c5dfb 100644 --- a/API/Data/VolumeRepository.cs +++ b/API/Data/VolumeRepository.cs @@ -1,12 +1,15 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using API.Comparators; using API.DTOs; using API.Entities; +using API.Extensions; using API.Interfaces; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace API.Data { @@ -14,11 +17,13 @@ namespace API.Data { private readonly DataContext _context; private readonly IMapper _mapper; + private readonly ILogger _logger; - public VolumeRepository(DataContext context, IMapper mapper) + public VolumeRepository(DataContext context, IMapper mapper, ILogger logger) { _context = context; _mapper = mapper; + _logger = logger; } public void Update(Volume volume) @@ -84,5 +89,95 @@ namespace API.Data .AsNoTracking() .ToListAsync(); } + + /// + /// Gets the first (ordered) volume/chapter in a series where the user has progress on it. Only completed volumes/chapters, next entity shouldn't + /// have any read progress on it. + /// + /// + /// + /// + /// + public async Task> GetContinueReading(int userId, int libraryId, int limit) + { + _logger.LogInformation("Get Continue Reading"); + var progress = await _context.Chapter + .Join(_context.AppUserProgresses, chapter => chapter.Id, progress => progress.ChapterId, + (chapter, progress) => + new + { + Chapter = chapter, + Progress = progress + }) + .Join(_context.Series, arg => arg.Progress.SeriesId, series => series.Id, (arg, series) => + new + { + arg.Chapter, + arg.Progress, + Series = series + }) + .AsNoTracking() + .Where(arg => arg.Progress.AppUserId == userId + && arg.Progress.PagesRead < arg.Chapter.Pages) + .OrderByDescending(d => d.Progress.LastModified) + .Take(limit) + .ToListAsync(); + + return progress + .OrderBy(c => float.Parse(c.Chapter.Number), new ChapterSortComparer()) + .DistinctBy(p => p.Series.Id) + .Select(arg => new InProgressChapterDto() + { + Id = arg.Chapter.Id, + Number = arg.Chapter.Number, + Range = arg.Chapter.Range, + SeriesId = arg.Progress.SeriesId, + SeriesName = arg.Series.Name, + LibraryId = arg.Series.LibraryId, + Pages = arg.Chapter.Pages, + }); + + // var chapters = await _context.Chapter + // .Join(_context.AppUserProgresses, chapter => chapter.Id, progress => progress.ChapterId, (chapter, progress) => + // new + // { + // Chapter = chapter, + // Progress = progress + // }) + // .Where(arg => arg.Progress.AppUserId == userId && arg.Progress.PagesRead < arg.Chapter.Pages) + // .Join(_context.Series, arg => arg.Progress.SeriesId, series => series.Id, (arg, series) => + // new + // { + // arg.Chapter, + // arg.Progress, + // Series = series + // }) + // .AsNoTracking() + // //.OrderBy(s => s.Chapter.Number) + // .GroupBy(p => p.Series.Id) + // .Select(g => g.FirstOrDefault()) + // .Select(arg => new InProgressChapterDto() + // { + // Id = arg.Chapter.Id, + // Number = arg.Chapter.Number, + // Range = arg.Chapter.Range, + // SeriesId = arg.Progress.SeriesId, + // SeriesName = arg.Series.Name, + // LibraryId = arg.Series.LibraryId, + // Pages = arg.Chapter.Pages, + // }) + // + // //.OrderBy(c => float.Parse(c.Number)) //can't convert to SQL + // + // .ToListAsync(); + // + // + // return chapters; + + + // return chapters + // .OrderBy(c => float.Parse(c.Number), new ChapterSortComparer()) + // .DistinctBy(c => c.SeriesName); + } } } \ No newline at end of file diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index f8b10a442..6c5d4ec34 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -7,6 +7,7 @@ using API.Services.Tasks; using AutoMapper; using Hangfire; using Hangfire.LiteDB; +using Hangfire.MemoryStorage; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -45,15 +46,7 @@ namespace API.Extensions var loggingSection = config.GetSection("Logging"); loggingBuilder.AddFile(loggingSection); }); - - services.AddHangfire(configuration => configuration - .UseSimpleAssemblyNameTypeSerializer() - .UseRecommendedSerializerSettings() - .UseLiteDbStorage()); - // Add the processing server as IHostedService - services.AddHangfireServer(); - return services; } diff --git a/API/Extensions/EnumerableExtensions.cs b/API/Extensions/EnumerableExtensions.cs new file mode 100644 index 000000000..b8293436e --- /dev/null +++ b/API/Extensions/EnumerableExtensions.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace API.Extensions +{ + public static class EnumerableExtensions + { + public static IEnumerable DistinctBy + (this IEnumerable source, Func keySelector) + { + var seenKeys = new HashSet(); + foreach (var element in source) + { + if (seenKeys.Add(keySelector(element))) + { + yield return element; + } + } + } + } +} \ No newline at end of file diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index f5d670b59..85b1aaf9a 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -20,6 +20,7 @@ namespace API.Helpers CreateMap(); CreateMap(); + //CreateMap(); CreateMap(); diff --git a/API/Interfaces/IVolumeRepository.cs b/API/Interfaces/IVolumeRepository.cs index faf18abb8..bec554fde 100644 --- a/API/Interfaces/IVolumeRepository.cs +++ b/API/Interfaces/IVolumeRepository.cs @@ -13,5 +13,6 @@ namespace API.Interfaces Task> GetFilesForChapter(int chapterId); Task> GetChaptersAsync(int volumeId); Task GetChapterCoverImageAsync(int chapterId); + Task> GetContinueReading(int userId, int libraryId, int limit); } } \ No newline at end of file diff --git a/API/Program.cs b/API/Program.cs index c38736407..e3d8cae60 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -43,9 +43,9 @@ namespace API logger.LogError(ex, "An error occurred during migration"); } - // Load all tasks from DI (TODO: This is not working) + // Load all tasks from DI (TODO: This is not working - WarmupServicesStartupTask is Null) var startupTasks = host.Services.GetServices(); - + // Execute all the tasks foreach (var startupTask in startupTasks) { diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 61842eac8..e63144113 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -66,7 +66,7 @@ namespace API.Services.Tasks //[DisableConcurrentExecution(5)] [AutomaticRetry(Attempts = 0, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public void ScanLibrary(int libraryId, bool forceUpdate) + public async void ScanLibrary(int libraryId, bool forceUpdate) { _forceUpdate = forceUpdate; var sw = Stopwatch.StartNew(); diff --git a/API/Startup.cs b/API/Startup.cs index 55b7f6f9e..2174e5ed2 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -6,6 +6,8 @@ using API.Extensions; using API.Middleware; using API.Services; using Hangfire; +using Hangfire.LiteDB; +using Hangfire.MemoryStorage; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -25,10 +27,12 @@ namespace API public class Startup { private readonly IConfiguration _config; + private readonly IWebHostEnvironment _env; - public Startup(IConfiguration config) + public Startup(IConfiguration config, IWebHostEnvironment env) { _config = config; + _env = env; } // This method gets called by the runtime. Use this method to add services to the container. @@ -63,6 +67,24 @@ namespace API services.AddResponseCaching(); + if (_env.IsDevelopment()) + { + services.AddHangfire(configuration => configuration + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UseMemoryStorage()); + } + else + { + services.AddHangfire(configuration => configuration + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UseLiteDbStorage()); + } + + // Add the processing server as IHostedService + services.AddHangfireServer(); + // services // .AddStartupTask() @@ -127,6 +149,10 @@ namespace API }); applicationLifetime.ApplicationStopping.Register(OnShutdown); + applicationLifetime.ApplicationStarted.Register(() => + { + Console.WriteLine("Kavita - v0.3"); + }); } private void OnShutdown()