using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Data.Misc; using API.DTOs.Person; 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; #nullable enable [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); Task> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId, 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); Task ReadingListExistsForUser(string name, int userId); IEnumerable GetReadingListPeopleAsync(int readingListId, PersonRole role); Task GetReadingListAllPeopleAsync(int readingListId); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); Task RemoveReadingListsWithoutSeries(); Task GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items); Task> GetReadingListsByIds(IList ids, ReadingListIncludes includes = ReadingListIncludes.Items); Task> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items); Task GetReadingListInfoAsync(int readingListId); } 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) .FirstOrDefaultAsync(); } 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(); 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 async Task ReadingListExistsForUser(string name, int userId) { var normalized = name.ToNormalized(); return await _context.ReadingList .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId); } public IEnumerable GetReadingListPeopleAsync(int readingListId, PersonRole role) { return _context.ReadingListItem .Where(item => item.ReadingListId == readingListId) .SelectMany(item => item.Chapter.People) .Where(p => p.Role == role) .OrderBy(p => p.Person.NormalizedName) .Select(p => p.Person) .Distinct() .ProjectTo(_mapper.ConfigurationProvider) .AsEnumerable(); } public async Task GetReadingListAllPeopleAsync(int readingListId) { var allPeople = await _context.ReadingListItem .Where(item => item.ReadingListId == readingListId) .SelectMany(item => item.Chapter.People) .OrderBy(p => p.Person.NormalizedName) .Select(p => new { Role = p.Role, Person = _mapper.Map(p.Person) }) .Distinct() .ToListAsync(); // Create the ReadingListCast object var cast = new ReadingListCast(); // Group people by role and populate the appropriate collections foreach (var personGroup in allPeople.GroupBy(p => p.Role)) { var people = personGroup.Select(pg => pg.Person).ToList(); switch (personGroup.Key) { case PersonRole.Writer: cast.Writers = people; break; case PersonRole.CoverArtist: cast.CoverArtists = people; break; case PersonRole.Publisher: cast.Publishers = people; break; case PersonRole.Character: cast.Characters = people; break; case PersonRole.Penciller: cast.Pencillers = people; break; case PersonRole.Inker: cast.Inkers = people; break; case PersonRole.Imprint: cast.Imprints = people; break; case PersonRole.Colorist: cast.Colorists = people; break; case PersonRole.Letterer: cast.Letterers = people; break; case PersonRole.Editor: cast.Editors = people; break; case PersonRole.Translator: cast.Translators = people; break; case PersonRole.Team: cast.Teams = people; break; case PersonRole.Location: cast.Locations = people; break; case PersonRole.Other: break; } } return cast; } 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 async Task> GetReadingListsByIds(IList ids, ReadingListIncludes includes = ReadingListIncludes.Items) { return await _context.ReadingList .Where(c => ids.Contains(c.Id)) .Includes(includes) .AsSplitQuery() .ToListAsync(); } public async Task> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items) { return await _context.ReadingList .Where(rl => rl.Items.Any(rli => rli.SeriesId == seriesId)) .Includes(includes) .AsSplitQuery() .ToListAsync(); } /// /// Returns a Partial ReadingListInfoDto. The HourEstimate needs to be calculated outside the repo /// /// /// public async Task GetReadingListInfoAsync(int readingListId) { // Get sum of these across all ReadingListItems: long wordCount, int pageCount, bool isEpub (assume false if any ReadingListeItem.Series.Format is non-epub) var readingList = await _context.ReadingList .Where(rl => rl.Id == readingListId) .Include(rl => rl.Items) .ThenInclude(item => item.Series) .Include(rl => rl.Items) .ThenInclude(item => item.Volume) .Include(rl => rl.Items) .ThenInclude(item => item.Chapter) .Select(rl => new ReadingListInfoDto() { WordCount = rl.Items.Sum(item => item.Chapter.WordCount), Pages = rl.Items.Sum(item => item.Chapter.Pages), IsAllEpub = rl.Items.All(item => item.Series.Format == MangaFormat.Epub), }) .FirstOrDefaultAsync(); return readingList; } 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 user = await _context.AppUser.FirstAsync(u => u.Id == userId); var query = _context.ReadingList .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) .RestrictAgainstAgeRestriction(user.GetAgeRestriction()); 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 user = await _context.AppUser.FirstAsync(u => u.Id == userId); var query = _context.ReadingList .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) .RestrictAgainstAgeRestriction(user.GetAgeRestriction()) .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> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId, bool includePromoted) { var user = await _context.AppUser.FirstAsync(u => u.Id == userId); var query = _context.ReadingList .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) .RestrictAgainstAgeRestriction(user.GetAgeRestriction()) .Where(l => l.Items.Any(i => i.ChapterId == chapterId)) .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.GetUserLibraries(userId); 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), chapter.Summary, chapter.IsSpecial }) .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, data.Summary, data.IsSpecial, 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, data.Summary, data.IsSpecial, 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, Summary = data.Summary, IsSpecial = data.IsSpecial }) .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) { var user = await _context.AppUser.FirstAsync(u => u.Id == userId); return await _context.ReadingList .Where(r => r.Id == readingListId && (r.AppUserId == userId || r.Promoted)) .RestrictAgainstAgeRestriction(user.GetAgeRestriction()) .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(); } }