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