using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using API.Data.ManualMigrations; using API.DTOs; using API.Entities; using API.Entities.Enums; using API.Services.Tasks.Scanner.Parser; 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); Task UpdateAllProgressThatAreMoreThanChapterPages(); } #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.Range) .ToListAsync(); return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(Parser.MaxNumberFromRange(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.MaxNumber) .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(); } public async Task UpdateAllProgressThatAreMoreThanChapterPages() { var updates = _context.AppUserProgresses .Join(_context.Chapter, progress => progress.ChapterId, chapter => chapter.Id, (progress, chapter) => new { Progress = progress, Chapter = chapter }) .Where(joinResult => joinResult.Progress.PagesRead > joinResult.Chapter.Pages) .Select(result => new { ProgressId = result.Progress.Id, NewPagesRead = Math.Min(result.Progress.PagesRead, result.Chapter.Pages) }) .AsEnumerable(); // Need to run this Raw because DataContext will update LastModified on the entity which breaks ordering for progress var sqlBuilder = new StringBuilder(); foreach (var update in updates) { sqlBuilder.Append($"UPDATE AppUserProgresses SET PagesRead = {update.NewPagesRead} WHERE Id = {update.ProgressId};"); } // Execute the batch SQL var batchSql = sqlBuilder.ToString(); await _context.Database.ExecuteSqlRawAsync(batchSql); } #nullable enable public async Task GetUserProgressAsync(int chapterId, int userId) { return await _context.AppUserProgresses .Where(p => p.ChapterId == chapterId && p.AppUserId == userId) .FirstOrDefaultAsync(); } }