Kavita/API/Data/Repositories/AppUserProgressRepository.cs
Joe Milazzo a0a6da9c60
Personal Table of Contents (#2148)
* Fixed a bad default setting for token key

* Changed the payment link to support Google Pay

* Fixed duplicate events occurring on newly added series from a scan.

Fixed the version update code from not firing and made it check every 4-6 hours (random per user per restart)

* Check for new releases on startup as well.

Added Personal Table of Contents (called Bookmarks on epub and pdf reader). The idea is that sometimes you want to bookmark certain parts of pages to get back to quickly later. This mechanism will allow you to do that without having to edit the underlying ToC.

* Added a button to update modal to show how to update for those unaware.

* Darkened the link text within tables to be more visible.

* Update link for how to update now is dynamic for docker users

* Refactored to send proper star/end dates for scrobble read events for upcoming changes in the API.

Added GoogleBooks Rating UI code if I go forward with API changes.

* When Scrobbling, send when the first and last progress for the series was.

Added OpenLibrary icon for upcoming enhancements for Kavita+.

Changed the Update checker to execute at start.

* Fixed backups not saving favicons in the correct place

* Refactored the layout code for Personal ToC

* More bugfixes around toc

* Box alignment

* Fixed up closing the overlay when bookmark mode is active

* Fixed up closing the overlay when bookmark mode is active

---------

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
2023-07-21 15:29:35 -07:00

209 lines
8.0 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Data.ManualMigrations;
using API.DTOs;
using API.Entities;
using API.Entities.Enums;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface IAppUserProgressRepository
{
void Update(AppUserProgress userProgress);
Task<int> CleanupAbandonedChapters();
Task<bool> UserHasProgress(LibraryType libraryType, int userId);
Task<AppUserProgress?> GetUserProgressAsync(int chapterId, int userId);
Task<bool> HasAnyProgressOnSeriesAsync(int seriesId, int userId);
/// <summary>
/// This is built exclusively for <see cref="MigrateUserProgressLibraryId"/>
/// </summary>
/// <returns></returns>
Task<AppUserProgress?> GetAnyProgress();
Task<IEnumerable<AppUserProgress>> GetUserProgressForSeriesAsync(int seriesId, int userId);
Task<IEnumerable<AppUserProgress>> GetAllProgress();
Task<DateTime> GetLatestProgress();
Task<ProgressDto> GetUserProgressDtoAsync(int chapterId, int userId);
Task<bool> AnyUserProgressForSeriesAsync(int seriesId, int userId);
Task<int> GetHighestFullyReadChapterForSeries(int seriesId, int userId);
Task<int> GetHighestFullyReadVolumeForSeries(int seriesId, int userId);
Task<DateTime?> GetLatestProgressForSeries(int seriesId, int userId);
Task<DateTime?> GetFirstProgressForSeries(int seriesId, int userId);
}
#nullable disable
public class AppUserProgressRepository : IAppUserProgressRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public AppUserProgressRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public void Update(AppUserProgress userProgress)
{
_context.Entry(userProgress).State = EntityState.Modified;
}
/// <summary>
/// This will remove any entries that have chapterIds that no longer exists. This will execute the save as well.
/// </summary>
public async Task<int> CleanupAbandonedChapters()
{
var chapterIds = _context.Chapter.Select(c => c.Id);
var rowsToRemove = await _context.AppUserProgresses
.Where(progress => !chapterIds.Contains(progress.ChapterId))
.ToListAsync();
var rowsToRemoveBookmarks = await _context.AppUserBookmark
.Where(progress => !chapterIds.Contains(progress.ChapterId))
.ToListAsync();
var rowsToRemoveReadingLists = await _context.ReadingListItem
.Where(item => !chapterIds.Contains(item.ChapterId))
.ToListAsync();
_context.RemoveRange(rowsToRemove);
_context.RemoveRange(rowsToRemoveBookmarks);
_context.RemoveRange(rowsToRemoveReadingLists);
return await _context.SaveChangesAsync() > 0 ? rowsToRemove.Count : 0;
}
/// <summary>
/// Checks if user has any progress against a library of passed type
/// </summary>
/// <param name="libraryType"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<bool> UserHasProgress(LibraryType libraryType, int userId)
{
var seriesIds = await _context.AppUserProgresses
.Where(aup => aup.PagesRead > 0 && aup.AppUserId == userId)
.AsNoTracking()
.Select(aup => aup.SeriesId)
.ToListAsync();
if (seriesIds.Count == 0) return false;
return await _context.Series
.Include(s => s.Library)
.Where(s => seriesIds.Contains(s.Id) && s.Library.Type == libraryType)
.AsNoTracking()
.AnyAsync();
}
public async Task<bool> HasAnyProgressOnSeriesAsync(int seriesId, int userId)
{
return await _context.AppUserProgresses
.AnyAsync(aup => aup.PagesRead > 0 && aup.AppUserId == userId && aup.SeriesId == seriesId);
}
#nullable enable
public async Task<AppUserProgress?> GetAnyProgress()
{
return await _context.AppUserProgresses.FirstOrDefaultAsync();
}
#nullable disable
/// <summary>
/// This will return any user progress. This filters out progress rows that have no pages read.
/// </summary>
/// <param name="seriesId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<IEnumerable<AppUserProgress>> GetUserProgressForSeriesAsync(int seriesId, int userId)
{
return await _context.AppUserProgresses
.Where(p => p.SeriesId == seriesId && p.AppUserId == userId && p.PagesRead > 0)
.ToListAsync();
}
public async Task<IEnumerable<AppUserProgress>> GetAllProgress()
{
return await _context.AppUserProgresses.ToListAsync();
}
/// <summary>
/// Returns the latest progress in UTC
/// </summary>
/// <returns></returns>
public async Task<DateTime> GetLatestProgress()
{
return await _context.AppUserProgresses
.Select(d => d.LastModifiedUtc)
.OrderByDescending(d => d)
.FirstOrDefaultAsync();
}
public async Task<ProgressDto> GetUserProgressDtoAsync(int chapterId, int userId)
{
return await _context.AppUserProgresses
.Where(p => p.AppUserId == userId && p.ChapterId == chapterId)
.ProjectTo<ProgressDto>(_mapper.ConfigurationProvider)
.FirstOrDefaultAsync();
}
public async Task<bool> AnyUserProgressForSeriesAsync(int seriesId, int userId)
{
return await _context.AppUserProgresses
.Where(p => p.SeriesId == seriesId && p.AppUserId == userId && p.PagesRead > 0)
.AnyAsync();
}
public async Task<int> GetHighestFullyReadChapterForSeries(int seriesId, int userId)
{
var list = await _context.AppUserProgresses
.Join(_context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id,
(appUserProgresses, chapter) => new {appUserProgresses, chapter})
.Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId &&
p.appUserProgresses.PagesRead >= p.chapter.Pages)
.Select(p => p.chapter.Number)
.ToListAsync();
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(float.Parse(d)));
}
public async Task<int> GetHighestFullyReadVolumeForSeries(int seriesId, int userId)
{
var list = await _context.AppUserProgresses
.Join(_context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id,
(appUserProgresses, chapter) => new {appUserProgresses, chapter})
.Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId &&
p.appUserProgresses.PagesRead >= p.chapter.Pages)
.Select(p => p.chapter.Volume.Number)
.ToListAsync();
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max();
}
public async Task<DateTime?> GetLatestProgressForSeries(int seriesId, int userId)
{
var list = await _context.AppUserProgresses.Where(p => p.AppUserId == userId && p.SeriesId == seriesId)
.Select(p => p.LastModifiedUtc)
.ToListAsync();
return list.Count == 0 ? null : list.DefaultIfEmpty().Max();
}
public async Task<DateTime?> GetFirstProgressForSeries(int seriesId, int userId)
{
var list = await _context.AppUserProgresses.Where(p => p.AppUserId == userId && p.SeriesId == seriesId)
.Select(p => p.LastModifiedUtc)
.ToListAsync();
return list.Count == 0 ? null : list.DefaultIfEmpty().Min();
}
#nullable enable
public async Task<AppUserProgress?> GetUserProgressAsync(int chapterId, int userId)
{
return await _context.AppUserProgresses
.Where(p => p.ChapterId == chapterId && p.AppUserId == userId)
.FirstOrDefaultAsync();
}
#nullable disable
}