using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.DTOs; using API.DTOs.ReadingLists; using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Extensions.QueryExtensions; using API.Helpers; using API.Services; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; [Flags] public enum ReadingListIncludes { None = 1, Items = 2, ItemChapter = 4, } public interface IReadingListRepository { Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true); Task GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None); Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId); Task GetReadingListDtoByIdAsync(int readingListId, int userId); Task> AddReadingProgressModifiers(int userId, IList items); Task GetReadingListDtoByTitleAsync(int userId, string title); Task> GetReadingListItemsByIdAsync(int readingListId); Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, bool includePromoted); void Remove(ReadingListItem item); void Add(ReadingList list); void BulkRemove(IEnumerable items); void Update(ReadingList list); Task Count(); Task GetCoverImageAsync(int readingListId); Task> GetRandomCoverImagesAsync(int readingListId); Task> GetAllCoverImagesAsync(); Task ReadingListExists(string name); IEnumerable GetReadingListCharactersAsync(int readingListId); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); Task RemoveReadingListsWithoutSeries(); Task GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items); } public class ReadingListRepository : IReadingListRepository { private readonly DataContext _context; private readonly IMapper _mapper; public ReadingListRepository(DataContext context, IMapper mapper) { _context = context; _mapper = mapper; } public void Update(ReadingList list) { _context.Entry(list).State = EntityState.Modified; } public void Add(ReadingList list) { _context.Add(list); } public async Task Count() { return await _context.ReadingList.CountAsync(); } public async Task GetCoverImageAsync(int readingListId) { return await _context.ReadingList .Where(c => c.Id == readingListId) .Select(c => c.CoverImage) .SingleOrDefaultAsync(); } public async Task> GetAllCoverImagesAsync() { return (await _context.ReadingList .Select(t => t.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) .ToListAsync())!; } public async Task> GetRandomCoverImagesAsync(int readingListId) { var random = new Random(); var data = await _context.ReadingList .Where(r => r.Id == readingListId) .SelectMany(r => r.Items.Select(ri => ri.Chapter.CoverImage)) .Where(t => !string.IsNullOrEmpty(t)) .ToListAsync(); if (data.Count < 4) return new List(); return data .OrderBy(_ => random.Next()) .Take(4) .ToList(); } public async Task ReadingListExists(string name) { var normalized = name.ToNormalized(); return await _context.ReadingList .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized)); } public IEnumerable GetReadingListCharactersAsync(int readingListId) { return _context.ReadingListItem .Where(item => item.ReadingListId == readingListId) .SelectMany(item => item.Chapter.People.Where(p => p.Role == PersonRole.Character)) .OrderBy(p => p.NormalizedName) .Distinct() .ProjectTo(_mapper.ConfigurationProvider) .AsEnumerable(); } public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) { var extension = encodeFormat.GetExtension(); return await _context.ReadingList .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) .ToListAsync(); } public async Task RemoveReadingListsWithoutSeries() { var listsToDelete = await _context.ReadingList .Include(c => c.Items) .Where(c => c.Items.Count == 0) .AsSplitQuery() .ToListAsync(); _context.RemoveRange(listsToDelete); return await _context.SaveChangesAsync(); } public async Task GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items) { var normalized = name.ToNormalized(); return await _context.ReadingList .Includes(includes) .FirstOrDefaultAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId); } public void Remove(ReadingListItem item) { _context.ReadingListItem.Remove(item); } public void BulkRemove(IEnumerable items) { _context.ReadingListItem.RemoveRange(items); } public async Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true) { var userAgeRating = (await _context.AppUser.SingleAsync(u => u.Id == userId)).AgeRestriction; var query = _context.ReadingList .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) .Where(l => l.AgeRating >= userAgeRating); query = sortByLastModified ? query.OrderByDescending(l => l.LastModified) : query.OrderBy(l => l.NormalizedTitle); var finalQuery = query.ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking(); return await PagedList.CreateAsync(finalQuery, userParams.PageNumber, userParams.PageSize); } public async Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, bool includePromoted) { var query = _context.ReadingList .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) .Where(l => l.Items.Any(i => i.SeriesId == seriesId)) .AsSplitQuery() .OrderBy(l => l.Title) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking(); return await query.ToListAsync(); } public async Task GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None) { return await _context.ReadingList .Where(r => r.Id == readingListId) .Includes(includes) .Include(r => r.Items.OrderBy(item => item.Order)) .AsSplitQuery() .SingleOrDefaultAsync(); } public async Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId) { var userLibraries = _context.Library .Include(l => l.AppUsers) .Where(library => library.AppUsers.Any(user => user.Id == userId)) .AsSplitQuery() .AsNoTracking() .Select(library => library.Id) .ToList(); var items = await _context.ReadingListItem .Where(s => s.ReadingListId == readingListId) .Join(_context.Chapter, s => s.ChapterId, chapter => chapter.Id, (data, chapter) => new { TotalPages = chapter.Pages, ChapterNumber = chapter.Range, chapter.ReleaseDate, ReadingListItem = data, ChapterTitleName = chapter.TitleName, FileSize = chapter.Files.Sum(f => f.Bytes) }) .Join(_context.Volume, s => s.ReadingListItem.VolumeId, volume => volume.Id, (data, volume) => new { data.ReadingListItem, data.TotalPages, data.ChapterNumber, data.ReleaseDate, data.ChapterTitleName, data.FileSize, VolumeId = volume.Id, VolumeNumber = volume.Name, }) .Join(_context.Series, s => s.ReadingListItem.SeriesId, series => series.Id, (data, s) => new { SeriesName = s.Name, SeriesFormat = s.Format, s.LibraryId, data.ReadingListItem, data.TotalPages, data.ChapterNumber, data.VolumeNumber, data.VolumeId, data.ReleaseDate, data.ChapterTitleName, data.FileSize, LibraryName = _context.Library.Where(l => l.Id == s.LibraryId).Select(l => l.Name).Single(), LibraryType = _context.Library.Where(l => l.Id == s.LibraryId).Select(l => l.Type).Single() }) .Select(data => new ReadingListItemDto() { Id = data.ReadingListItem.Id, ChapterId = data.ReadingListItem.ChapterId, Order = data.ReadingListItem.Order, SeriesId = data.ReadingListItem.SeriesId, SeriesName = data.SeriesName, SeriesFormat = data.SeriesFormat, PagesTotal = data.TotalPages, ChapterNumber = data.ChapterNumber, VolumeNumber = data.VolumeNumber, LibraryId = data.LibraryId, VolumeId = data.VolumeId, ReadingListId = data.ReadingListItem.ReadingListId, ReleaseDate = data.ReleaseDate, LibraryType = data.LibraryType, ChapterTitleName = data.ChapterTitleName, LibraryName = data.LibraryName, FileSize = data.FileSize }) .Where(o => userLibraries.Contains(o.LibraryId)) .OrderBy(rli => rli.Order) .AsSplitQuery() .AsNoTracking() .ToListAsync(); foreach (var item in items) { item.Title = ReadingListService.FormatTitle(item); } // Attach progress information var fetchedChapterIds = items.Select(i => i.ChapterId); var progresses = await _context.AppUserProgresses .Where(p => fetchedChapterIds.Contains(p.ChapterId)) .AsNoTracking() .ToListAsync(); foreach (var progress in progresses) { var progressItem = items.SingleOrDefault(i => i.ChapterId == progress.ChapterId && i.ReadingListId == readingListId); if (progressItem == null) continue; progressItem.PagesRead = progress.PagesRead; progressItem.LastReadingProgressUtc = progress.LastModifiedUtc; } return items; } public async Task GetReadingListDtoByIdAsync(int readingListId, int userId) { return await _context.ReadingList .Where(r => r.Id == readingListId && (r.AppUserId == userId || r.Promoted)) .ProjectTo(_mapper.ConfigurationProvider) .SingleOrDefaultAsync(); } public async Task> AddReadingProgressModifiers(int userId, IList items) { var chapterIds = items.Select(i => i.ChapterId).Distinct(); var userProgress = await _context.AppUserProgresses .Where(p => p.AppUserId == userId && chapterIds.Contains(p.ChapterId)) .AsNoTracking() .ToListAsync(); foreach (var item in items) { var progress = userProgress.Where(p => p.ChapterId == item.ChapterId).ToList(); if (progress.Count == 0) continue; item.PagesRead = progress.Sum(p => p.PagesRead); item.LastReadingProgressUtc = progress.Max(p => p.LastModifiedUtc); } return items; } public async Task GetReadingListDtoByTitleAsync(int userId, string title) { return await _context.ReadingList .Where(r => r.Title.Equals(title) && r.AppUserId == userId) .ProjectTo(_mapper.ConfigurationProvider) .SingleOrDefaultAsync(); } public async Task> GetReadingListItemsByIdAsync(int readingListId) { return await _context.ReadingListItem .Where(r => r.ReadingListId == readingListId) .OrderBy(r => r.Order) .ToListAsync(); } }