Temp commit to record attempts. Stream APIs are implemented and working. Added some new test cases based on deployed Kavita server testing.

This commit is contained in:
Joseph Milazzo 2021-03-17 14:29:50 -05:00
parent 55cd0c5fe5
commit 0a85555f38
16 changed files with 247 additions and 90 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));
@ -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));

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

@ -140,6 +140,15 @@ namespace API.Controllers
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

@ -280,87 +280,52 @@ namespace API.Data
}
/// <summary>
/// Returns a list of Series that were added within 2 weeks.
/// 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"></param>
/// <param name="limit">How many series to pick.</param>
/// <returns></returns>
public async Task<IEnumerable<SeriesDto>> 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<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)
{
//&& (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<SeriesDto>(_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<SeriesDto>(_mapper.ConfigurationProvider)
.ToListAsync();
await AddSeriesModifiers(userId, series);
return series.Where(s => s.PagesRead > 0).Take(limit).ToList();
}
public async Task<IEnumerable<SeriesDto>> 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<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,95 @@ 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 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);
}
}
}

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

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

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

@ -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<WarmupServicesStartupTask>();
// Execute all the tasks
foreach (var startupTask in startupTasks)
{

View File

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

View File

@ -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<WarmupServicesStartupTask>()
@ -127,6 +149,10 @@ namespace API
});
applicationLifetime.ApplicationStopping.Register(OnShutdown);
applicationLifetime.ApplicationStarted.Register(() =>
{
Console.WriteLine("Kavita - v0.3");
});
}
private void OnShutdown()