diff --git a/API.Tests/ParserTest.cs b/API.Tests/ParserTest.cs index 6c1b5cfd5..0190e6dc7 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)); @@ -130,6 +131,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 +141,6 @@ 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")] public void ParseChaptersTest(string filename, string expected) { Assert.Equal(expected, ParseChapter(filename)); @@ -301,10 +302,7 @@ namespace API.Tests Chapters = "6.5", Filename = "Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire).cbz", Format = MangaFormat.Archive, FullFilePath = filepath }); - - - - + foreach (var file in expected.Keys) { 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 8be412c07..c1e4c9b73 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -126,5 +126,29 @@ namespace API.Controllers return BadRequest("There was an error with updating the series"); } + + [HttpGet("recently-added")] + public async Task>> GetRecentlyAdded(int libraryId = 0, int limit = 20) + { + return Ok(await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, limit)); + } + + [HttpGet("in-progress")] + public async Task>> GetInProgress(int libraryId = 0, int limit = 20) + { + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + 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/LibraryRepository.cs b/API/Data/LibraryRepository.cs index 80dbbb553..c33c42281 100644 --- a/API/Data/LibraryRepository.cs +++ b/API/Data/LibraryRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -112,5 +113,7 @@ namespace API.Data .Include(l => l.Folders) .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); } + + } } \ No newline at end of file diff --git a/API/Data/Migrations/20210313001830_SearchIndex.cs b/API/Data/Migrations/20210313001830_SearchIndex.cs deleted file mode 100644 index 2272f73bf..000000000 --- a/API/Data/Migrations/20210313001830_SearchIndex.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace API.Data.Migrations -{ - public partial class SearchIndex : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateIndex( - name: "IX_Series_Name_NormalizedName_LocalizedName", - table: "Series", - columns: new[] { "Name", "NormalizedName", "LocalizedName" }, - unique: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_Series_Name_NormalizedName_LocalizedName", - table: "Series"); - } - } -} diff --git a/API/Data/Migrations/20210313001830_SearchIndex.Designer.cs b/API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs similarity index 98% rename from API/Data/Migrations/20210313001830_SearchIndex.Designer.cs rename to API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs index 5aff37c5d..a407ccc28 100644 --- a/API/Data/Migrations/20210313001830_SearchIndex.Designer.cs +++ b/API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs @@ -9,8 +9,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace API.Data.Migrations { [DbContext(typeof(DataContext))] - [Migration("20210313001830_SearchIndex")] - partial class SearchIndex + [Migration("20210315134028_SearchIndexAndProgressDates")] + partial class SearchIndexAndProgressDates { protected override void BuildTargetModel(ModelBuilder modelBuilder) { @@ -161,6 +161,12 @@ namespace API.Data.Migrations b.Property("ChapterId") .HasColumnType("INTEGER"); + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + b.Property("PagesRead") .HasColumnType("INTEGER"); @@ -367,7 +373,7 @@ namespace API.Data.Migrations b.HasIndex("LibraryId"); - b.HasIndex("Name", "NormalizedName", "LocalizedName") + b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId") .IsUnique(); b.ToTable("Series"); diff --git a/API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.cs b/API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.cs new file mode 100644 index 000000000..02dc1db2c --- /dev/null +++ b/API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class SearchIndexAndProgressDates : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Created", + table: "AppUserProgresses", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "AppUserProgresses", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.CreateIndex( + name: "IX_Series_Name_NormalizedName_LocalizedName_LibraryId", + table: "Series", + columns: new[] { "Name", "NormalizedName", "LocalizedName", "LibraryId" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Series_Name_NormalizedName_LocalizedName_LibraryId", + table: "Series"); + + migrationBuilder.DropColumn( + name: "Created", + table: "AppUserProgresses"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "AppUserProgresses"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 922966162..c0fcd6f87 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -159,6 +159,12 @@ namespace API.Data.Migrations b.Property("ChapterId") .HasColumnType("INTEGER"); + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + b.Property("PagesRead") .HasColumnType("INTEGER"); @@ -365,7 +371,7 @@ namespace API.Data.Migrations b.HasIndex("LibraryId"); - b.HasIndex("Name", "NormalizedName", "LocalizedName") + b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId") .IsUnique(); b.ToTable("Series"); diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index d1b5e3eb5..8c1949edc 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -9,6 +11,7 @@ using API.Interfaces; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Logging; namespace API.Data @@ -275,5 +278,54 @@ namespace API.Data v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead); } } + + /// + /// 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) + { + return await _context.Series + .Where(s => (libraryId <= 0 || s.LibraryId == libraryId)) + .Take(limit) + .OrderByDescending(s => s.Created) + .AsNoTracking() + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + } + + /// + /// + /// + /// + /// + /// + /// + public async Task> GetInProgress(int userId, int libraryId, int limit) + { + //var twoWeeksAgo = DateTime.Today.Subtract(TimeSpan.FromDays(14)); // TODO: Think about moving this to a setting + var series = await _context.Series + .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..35119efa8 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,53 @@ 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 chapters = await _context.Chapter + .Join(_context.AppUserProgresses, c => c.Id, p => p.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 chapters + .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, + }); + } } } \ No newline at end of file diff --git a/API/Entities/AppUserProgress.cs b/API/Entities/AppUserProgress.cs index be3953246..170a249ac 100644 --- a/API/Entities/AppUserProgress.cs +++ b/API/Entities/AppUserProgress.cs @@ -1,20 +1,25 @@  +using System; +using API.Entities.Interfaces; + namespace API.Entities { /// /// Represents the progress a single user has on a given Volume. Progress is realistically tracked against the Volume's chapters. /// - public class AppUserProgress + public class AppUserProgress : IEntityDate { public int Id { get; set; } public int PagesRead { get; set; } public int VolumeId { get; set; } public int SeriesId { get; set; } - public int ChapterId { get; set; } // Relationships public AppUser AppUser { get; set; } public int AppUserId { get; set; } + + public DateTime Created { get; set; } + public DateTime LastModified { get; set; } } } \ No newline at end of file diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index c3d5ba68e..6406e118f 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore; namespace API.Entities { - [Index(nameof(Name), nameof(NormalizedName), nameof(LocalizedName), IsUnique = true)] + [Index(nameof(Name), nameof(NormalizedName), nameof(LocalizedName), nameof(LibraryId), IsUnique = true)] public class Series : IEntityDate { public int Id { get; set; } @@ -36,7 +36,6 @@ namespace API.Entities public DateTime Created { get; set; } public DateTime LastModified { get; set; } public byte[] CoverImage { get; set; } - // NOTE: Do I want to store a thumbImage for search results? /// /// Sum of all Volume page counts /// 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/Extensions/ServiceCollectionExtensions.cs b/API/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..d3cae4191 --- /dev/null +++ b/API/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,12 @@ +using API.Interfaces.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace API.Extensions +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddStartupTask(this IServiceCollection services) + where T : class, IStartupTask + => services.AddTransient(); + } +} \ No newline at end of file diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index f5d670b59..328a27ade 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -20,7 +20,7 @@ namespace API.Helpers CreateMap(); CreateMap(); - + CreateMap(); CreateMap() diff --git a/API/Interfaces/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs index e9d950937..647469e69 100644 --- a/API/Interfaces/ISeriesRepository.cs +++ b/API/Interfaces/ISeriesRepository.cs @@ -55,5 +55,7 @@ namespace API.Interfaces Task GetVolumeCoverImageAsync(int volumeId); Task GetSeriesCoverImageAsync(int seriesId); + Task> GetInProgress(int userId, int libraryId, int limit); + Task> GetRecentlyAdded(int libraryId, int limit); } } \ No newline at end of file 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/Middleware/ExceptionMiddleware.cs b/API/Middleware/ExceptionMiddleware.cs index 5c168cf3d..8c76b8644 100644 --- a/API/Middleware/ExceptionMiddleware.cs +++ b/API/Middleware/ExceptionMiddleware.cs @@ -25,6 +25,7 @@ namespace API.Middleware public async Task InvokeAsync(HttpContext context) { + // BUG: I think Hangfire timeouts are triggering the middleware to hijack an API call try { await _next(context); // downstream middlewares or http call diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index 1e860ab28..27b6309c1 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -124,9 +124,10 @@ namespace API.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip new Regex( - @"v\d+\.(?\d+(?:.\d+|-\d+)?)", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Mob Psycho 100 + // Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz new Regex( @"^(?!Vol)(?.*) (?\d+(?:.\d+|-\d+)?)(?: \(\d{4}\))?", diff --git a/API/Program.cs b/API/Program.cs index 7d7628252..aa6c98e56 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -3,6 +3,8 @@ using System.Threading.Tasks; using API.Data; using API.Entities; using API.Interfaces; +using API.Interfaces.Services; +using API.Services; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -40,7 +42,13 @@ namespace API var logger = services.GetRequiredService < ILogger>(); logger.LogError(ex, "An error occurred during migration"); } - + + // Load all tasks from DI and initialize them (TODO: This is not working - WarmupServicesStartupTask is Null) + var startupTasks = host.Services.GetServices(); + foreach (var startupTask in startupTasks) + { + await startupTask.ExecuteAsync(); + } await host.RunAsync(); } diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index a5bdb4220..8e6ede340 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -52,6 +52,7 @@ namespace API.Services return chapter; } + public void Cleanup() { _logger.LogInformation("Performing cleanup of Cache directory"); diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 492ff6357..28241aaea 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -6,6 +6,8 @@ using API.Helpers.Converters; using API.Interfaces; using API.Interfaces.Services; using Hangfire; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace API.Services @@ -21,15 +23,15 @@ namespace API.Services private readonly ICleanupService _cleanupService; private readonly IDirectoryService _directoryService; - public BackgroundJobServer Client => new BackgroundJobServer(); - // new BackgroundJobServerOptions() - // { - // WorkerCount = 1 - // } + public static BackgroundJobServer Client => new BackgroundJobServer(new BackgroundJobServerOptions() + { + WorkerCount = 1 + }); + public TaskScheduler(ICacheService cacheService, ILogger logger, IScannerService scannerService, IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService, ICleanupService cleanupService, - IDirectoryService directoryService) + IDirectoryService directoryService, IWebHostEnvironment env) { _cacheService = cacheService; _logger = logger; @@ -40,15 +42,23 @@ namespace API.Services _cleanupService = cleanupService; _directoryService = directoryService; - //Hangfire.RecurringJob.RemoveIfExists(); - ScheduleTasks(); - //JobStorage.Current.GetMonitoringApi().EnqueuedJobs() - + if (!env.IsDevelopment()) + { + ScheduleTasks(); + } + else + { + RecurringJob.RemoveIfExists("scan-libraries"); + RecurringJob.RemoveIfExists("backup"); + RecurringJob.RemoveIfExists("cleanup"); + } + } public void ScheduleTasks() { _logger.LogInformation("Scheduling reoccurring tasks"); + string setting = null; setting = Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan)).Result.Value; if (setting != null) @@ -80,8 +90,7 @@ namespace API.Services _logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId); BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId, forceUpdate)); - //BackgroundJob.Enqueue(() => _cleanupService.Cleanup()); // When we do a scan, force cache to re-unpack in case page numbers change - RecurringJob.Trigger("cleanup"); // TODO: Alternate way to trigger jobs. Test this out and see if we should switch. + BackgroundJob.Enqueue(() => _cleanupService.Cleanup()); // When we do a scan, force cache to re-unpack in case page numbers change } public void CleanupChapters(int[] chapterIds) diff --git a/API/Services/Tasks/BackupService.cs b/API/Services/Tasks/BackupService.cs index 3a5685035..9642a0faa 100644 --- a/API/Services/Tasks/BackupService.cs +++ b/API/Services/Tasks/BackupService.cs @@ -3,11 +3,13 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; +using System.Threading; using System.Threading.Tasks; using API.Entities.Enums; using API.Extensions; using API.Interfaces; using API.Interfaces.Services; +using Hangfire; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -57,6 +59,7 @@ namespace API.Services.Tasks return files; } + [AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)] public void BackupDatabase() { _logger.LogInformation("Beginning backup of Database at {BackupTime}", DateTime.Now); diff --git a/API/Services/Tasks/CleanupService.cs b/API/Services/Tasks/CleanupService.cs index d7079b4a7..a33cf746f 100644 --- a/API/Services/Tasks/CleanupService.cs +++ b/API/Services/Tasks/CleanupService.cs @@ -1,5 +1,6 @@ using System.IO; using API.Interfaces.Services; +using Hangfire; using Microsoft.Extensions.Logging; namespace API.Services.Tasks @@ -20,6 +21,7 @@ namespace API.Services.Tasks _logger = logger; } + [AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)] public void Cleanup() { _logger.LogInformation("Cleaning temp directory"); diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 5587ad0c8..61842eac8 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -33,7 +33,8 @@ namespace API.Services.Tasks _metadataService = metadataService; } - [DisableConcurrentExecution(timeoutInSeconds: 120)] + //[DisableConcurrentExecution(timeoutInSeconds: 5)] + [AutomaticRetry(Attempts = 0, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Delete)] public void ScanLibraries() { var libraries = Task.Run(() => _unitOfWork.LibraryRepository.GetLibrariesAsync()).Result.ToList(); @@ -63,7 +64,7 @@ namespace API.Services.Tasks _scannedSeries = null; } - [DisableConcurrentExecution(5)] + //[DisableConcurrentExecution(5)] [AutomaticRetry(Attempts = 0, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Delete)] public void ScanLibrary(int libraryId, bool forceUpdate) { diff --git a/API/Services/WarmupServiceStartupTask.cs b/API/Services/WarmupServiceStartupTask.cs index fd9b1745b..36463451a 100644 --- a/API/Services/WarmupServiceStartupTask.cs +++ b/API/Services/WarmupServiceStartupTask.cs @@ -18,14 +18,13 @@ namespace API.Services _provider = provider; } - public Task ExecuteAsync(CancellationToken cancellationToken) + public Task ExecuteAsync(CancellationToken cancellationToken = default) { - using (var scope = _provider.CreateScope()) + using var scope = _provider.CreateScope(); + foreach (var singleton in GetServices(_services)) { - foreach (var singleton in GetServices(_services)) - { - scope.ServiceProvider.GetServices(singleton); - } + Console.WriteLine("DI preloading of " + singleton.FullName); + scope.ServiceProvider.GetServices(singleton); } return Task.CompletedTask; diff --git a/API/Startup.cs b/API/Startup.cs index 4e9319b37..c9a6a8eca 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -1,11 +1,13 @@ using System; using System.IO.Compression; using System.Linq; -using API.Data; using API.Extensions; +using API.Interfaces.Services; 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; @@ -16,7 +18,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; namespace API @@ -24,10 +25,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. @@ -62,15 +65,30 @@ namespace API services.AddResponseCaching(); + if (_env.IsDevelopment()) + { + services.AddHangfire(configuration => configuration + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UseMemoryStorage()); + } + else + { + services.AddHangfire(configuration => configuration + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UseLiteDbStorage()); + } - services - .AddStartupTask() - .TryAddSingleton(services); + // Add the processing server as IHostedService + services.AddHangfireServer(); + //services.AddStartupTask(services). + services.AddTransient().TryAddSingleton(services); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime) { app.UseMiddleware(); @@ -125,6 +143,20 @@ namespace API endpoints.MapHangfireDashboard(); endpoints.MapFallbackToController("Index", "Fallback"); }); + + applicationLifetime.ApplicationStopping.Register(OnShutdown); + applicationLifetime.ApplicationStarted.Register(() => + { + Console.WriteLine("Kavita - v0.3"); + }); + } + + private void OnShutdown() + { + Console.WriteLine("Server is shutting down. Going to dispose Hangfire"); + //this code is called when the application stops + TaskScheduler.Client.Dispose(); + System.Threading.Thread.Sleep(1000); } } }