Merge pull request #87 from Kareadita/feature/streams

Streams!
This commit is contained in:
Joseph Milazzo 2021-03-17 18:18:15 -05:00 committed by GitHub
commit 4052306c97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 382 additions and 86 deletions

View File

@ -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)
{

View File

@ -6,12 +6,17 @@
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.0" />
<PackageReference Include="Hangfire" Version="1.7.18" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.18" />
<PackageReference Include="Hangfire.LiteDB" Version="0.4.0" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.1" NoWarn="NU1605" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.1" NoWarn="NU1605" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.1" />

View File

@ -2,15 +2,26 @@
namespace API.Comparators
{
public class ChapterSortComparer : IComparer<int>
public class ChapterSortComparer : IComparer<float>
{
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);
}

View File

@ -126,5 +126,29 @@ namespace API.Controllers
return BadRequest("There was an error with updating the series");
}
[HttpGet("recently-added")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded(int libraryId = 0, int limit = 20)
{
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, limit));
}
[HttpGet("in-progress")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> 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<ActionResult<IEnumerable<SeriesDto>>> 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));
}
}
}

View File

@ -0,0 +1,23 @@
namespace API.DTOs
{
public class InProgressChapterDto
{
public int Id { get; init; }
/// <summary>
/// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2".
/// </summary>
public string Range { get; init; }
/// <summary>
/// Smallest number of the Range.
/// </summary>
public string Number { get; init; }
/// <summary>
/// Total number of pages in all MangaFiles
/// </summary>
public int Pages { get; init; }
public int SeriesId { get; init; }
public int LibraryId { get; init; }
public string SeriesName { get; init; }
}
}

View File

@ -21,5 +21,7 @@
/// Review from logged in user. Calculated at API-time.
/// </summary>
public string UserReview { get; set; }
public int LibraryId { get; set; }
}
}

View File

@ -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<LibraryDto>(_mapper.ConfigurationProvider).ToListAsync();
}
}
}

View File

@ -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");
}
}
}

View File

@ -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<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<int>("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");

View File

@ -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<DateTime>(
name: "Created",
table: "AppUserProgresses",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
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");
}
}
}

View File

@ -159,6 +159,12 @@ namespace API.Data.Migrations
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<int>("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");

View File

@ -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);
}
}
/// <summary>
/// Returns a list of Series that were added, ordered by Created desc
/// </summary>
/// <param name="libraryId">Library to restrict to, if 0, will apply to all libraries</param>
/// <param name="limit">How many series to pick.</param>
/// <returns></returns>
public async Task<IEnumerable<SeriesDto>> GetRecentlyAdded(int libraryId, int limit)
{
return await _context.Series
.Where(s => (libraryId <= 0 || s.LibraryId == libraryId))
.Take(limit)
.OrderByDescending(s => s.Created)
.AsNoTracking()
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
/// <summary>
///
/// </summary>
/// <param name="userId"></param>
/// <param name="libraryId"></param>
/// <param name="limit"></param>
/// <returns></returns>
public async Task<IEnumerable<SeriesDto>> 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<SeriesDto>(_mapper.ConfigurationProvider)
.ToListAsync();
return series;
}
}
}

View File

@ -12,21 +12,21 @@ namespace API.Data
private readonly DataContext _context;
private readonly IMapper _mapper;
private readonly UserManager<AppUser> _userManager;
private readonly ILogger<UnitOfWork> _seriesLogger;
private readonly ILogger<UnitOfWork> _logger;
public UnitOfWork(DataContext context, IMapper mapper, UserManager<AppUser> userManager, ILogger<UnitOfWork> seriesLogger)
public UnitOfWork(DataContext context, IMapper mapper, UserManager<AppUser> userManager, ILogger<UnitOfWork> 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);

View File

@ -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();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="userId"></param>
/// <param name="libraryId"></param>
/// <param name="limit"></param>
/// <returns></returns>
public async Task<IEnumerable<InProgressChapterDto>> 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,
});
}
}
}

View File

@ -1,20 +1,25 @@

using System;
using API.Entities.Interfaces;
namespace API.Entities
{
/// <summary>
/// Represents the progress a single user has on a given Volume. Progress is realistically tracked against the Volume's chapters.
/// </summary>
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; }
}
}

View File

@ -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?
/// <summary>
/// Sum of all Volume page counts
/// </summary>

View File

@ -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;
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
namespace API.Extensions
{
public static class EnumerableExtensions
{
public static IEnumerable<TSource> DistinctBy<TSource, TKey>
(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
var seenKeys = new HashSet<TKey>();
foreach (var element in source)
{
if (seenKeys.Add(keySelector(element)))
{
yield return element;
}
}
}
}
}

View File

@ -0,0 +1,12 @@
using API.Interfaces.Services;
using Microsoft.Extensions.DependencyInjection;
namespace API.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddStartupTask<T>(this IServiceCollection services)
where T : class, IStartupTask
=> services.AddTransient<IStartupTask, T>();
}
}

View File

@ -20,7 +20,7 @@ namespace API.Helpers
CreateMap<Chapter, ChapterDto>();
CreateMap<Series, SeriesDto>();
CreateMap<AppUserPreferences, UserPreferencesDto>();
CreateMap<Series, SearchResultDto>()

View File

@ -55,5 +55,7 @@ namespace API.Interfaces
Task<byte[]> GetVolumeCoverImageAsync(int volumeId);
Task<byte[]> GetSeriesCoverImageAsync(int seriesId);
Task<IEnumerable<SeriesDto>> GetInProgress(int userId, int libraryId, int limit);
Task<IEnumerable<SeriesDto>> GetRecentlyAdded(int libraryId, int limit);
}
}

View File

@ -13,5 +13,6 @@ namespace API.Interfaces
Task<IList<MangaFile>> GetFilesForChapter(int chapterId);
Task<IList<Chapter>> GetChaptersAsync(int volumeId);
Task<byte[]> GetChapterCoverImageAsync(int chapterId);
Task<IEnumerable<InProgressChapterDto>> GetContinueReading(int userId, int libraryId, int limit);
}
}

View File

@ -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

View File

@ -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+\.(?<Chapter>\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)(?<Series>.*) (?<Chapter>\d+(?:.\d+|-\d+)?)(?: \(\d{4}\))?",

View File

@ -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<Program>>();
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<WarmupServicesStartupTask>();
foreach (var startupTask in startupTasks)
{
await startupTask.ExecuteAsync();
}
await host.RunAsync();
}

View File

@ -52,6 +52,7 @@ namespace API.Services
return chapter;
}
public void Cleanup()
{
_logger.LogInformation("Performing cleanup of Cache directory");

View File

@ -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<TaskScheduler> 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)

View File

@ -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);

View File

@ -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");

View File

@ -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)
{

View File

@ -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;

View File

@ -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<WarmupServicesStartupTask>()
.TryAddSingleton(services);
// Add the processing server as IHostedService
services.AddHangfireServer();
//services.AddStartupTask<WarmupServicesStartupTask>(services).
services.AddTransient<IStartupTask, WarmupServicesStartupTask>().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<ExceptionMiddleware>();
@ -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);
}
}
}