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 CleanupAbandonedChapters(); Task UserHasProgress(LibraryType libraryType, int userId); Task GetUserProgressAsync(int chapterId, int userId); Task HasAnyProgressOnSeriesAsync(int seriesId, int userId); /// /// This is built exclusively for /// /// Task GetAnyProgress(); Task> GetUserProgressForSeriesAsync(int seriesId, int userId); Task> GetAllProgress(); Task GetLatestProgress(); Task GetUserProgressDtoAsync(int chapterId, int userId); Task AnyUserProgressForSeriesAsync(int seriesId, int userId); Task GetHighestFullyReadChapterForSeries(int seriesId, int userId); Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId); Task GetLatestProgressForSeries(int seriesId, int userId); Task 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; } /// /// This will remove any entries that have chapterIds that no longer exists. This will execute the save as well. /// public async Task 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; } /// /// Checks if user has any progress against a library of passed type /// /// /// /// public async Task 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 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 GetAnyProgress() { return await _context.AppUserProgresses.FirstOrDefaultAsync(); } #nullable disable /// /// This will return any user progress. This filters out progress rows that have no pages read. /// /// /// /// public async Task> GetUserProgressForSeriesAsync(int seriesId, int userId) { return await _context.AppUserProgresses .Where(p => p.SeriesId == seriesId && p.AppUserId == userId && p.PagesRead > 0) .ToListAsync(); } public async Task> GetAllProgress() { return await _context.AppUserProgresses.ToListAsync(); } /// /// Returns the latest progress in UTC /// /// public async Task GetLatestProgress() { return await _context.AppUserProgresses .Select(d => d.LastModifiedUtc) .OrderByDescending(d => d) .FirstOrDefaultAsync(); } public async Task GetUserProgressDtoAsync(int chapterId, int userId) { return await _context.AppUserProgresses .Where(p => p.AppUserId == userId && p.ChapterId == chapterId) .ProjectTo(_mapper.ConfigurationProvider) .FirstOrDefaultAsync(); } public async Task AnyUserProgressForSeriesAsync(int seriesId, int userId) { return await _context.AppUserProgresses .Where(p => p.SeriesId == seriesId && p.AppUserId == userId && p.PagesRead > 0) .AnyAsync(); } public async Task 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 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 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 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 GetUserProgressAsync(int chapterId, int userId) { return await _context.AppUserProgresses .Where(p => p.ChapterId == chapterId && p.AppUserId == userId) .FirstOrDefaultAsync(); } #nullable disable }