using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.DTOs; using API.DTOs.Metadata; using API.DTOs.Reader; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Extensions; using API.Extensions.QueryExtensions; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; #nullable enable [Flags] public enum ChapterIncludes { None = 1, Volumes = 2, Files = 4, People = 8, Genres = 16, Tags = 32, ExternalReviews = 1 << 6, ExternalRatings = 1 << 7 } public interface IChapterRepository { void Update(Chapter chapter); void Remove(Chapter chapter); void Remove(IList chapters); Task> GetChaptersByIdsAsync(IList chapterIds, ChapterIncludes includes = ChapterIncludes.None); Task GetChapterInfoDtoAsync(int chapterId); Task GetChapterTotalPagesAsync(int chapterId); Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); Task GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); Task> GetFilesForChapterAsync(int chapterId); Task> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None); Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds); Task GetChapterCoverImageAsync(int chapterId); Task> GetAllCoverImagesAsync(); Task> GetAllChaptersWithCoversInDifferentEncoding(EncodeFormat format); Task> GetCoverImagesForLockedChaptersAsync(); Task AddChapterModifiers(int userId, ChapterDto chapter); IEnumerable GetChaptersForSeries(int seriesId); Task> GetAllChaptersForSeries(int seriesId); Task GetAverageUserRating(int chapterId, int userId); Task> GetExternalChapterReviewDtos(int chapterId); Task> GetExternalChapterReview(int chapterId); Task> GetExternalChapterRatingDtos(int chapterId); Task> GetExternalChapterRatings(int chapterId); } public class ChapterRepository : IChapterRepository { private readonly DataContext _context; private readonly IMapper _mapper; public ChapterRepository(DataContext context, IMapper mapper) { _context = context; _mapper = mapper; } public void Update(Chapter chapter) { _context.Entry(chapter).State = EntityState.Modified; } public void Remove(Chapter chapter) { _context.Chapter.Remove(chapter); } public void Remove(IList chapters) { _context.Chapter.RemoveRange(chapters); } public async Task> GetChaptersByIdsAsync(IList chapterIds, ChapterIncludes includes = ChapterIncludes.None) { return await _context.Chapter .Where(c => chapterIds.Contains(c.Id)) .Includes(includes) .AsSplitQuery() .ToListAsync(); } /// /// Populates a partial IChapterInfoDto /// /// public async Task GetChapterInfoDtoAsync(int chapterId) { var chapterInfo = await _context.Chapter .Where(c => c.Id == chapterId) .Join(_context.Volume, c => c.VolumeId, v => v.Id, (chapter, volume) => new { ChapterNumber = chapter.MinNumber, VolumeNumber = volume.Name, VolumeId = volume.Id, chapter.IsSpecial, chapter.TitleName, volume.SeriesId, chapter.Pages, }) .Join(_context.Series, data => data.SeriesId, series => series.Id, (data, series) => new { data.ChapterNumber, data.VolumeNumber, data.VolumeId, data.IsSpecial, data.SeriesId, data.Pages, data.TitleName, SeriesFormat = series.Format, SeriesName = series.Name, series.LibraryId, LibraryType = series.Library.Type }) .Select(data => new ChapterInfoDto() { ChapterNumber = data.ChapterNumber + string.Empty, // TODO: Fix this VolumeNumber = data.VolumeNumber + string.Empty, // TODO: Fix this VolumeId = data.VolumeId, IsSpecial = data.IsSpecial, SeriesId = data.SeriesId, SeriesFormat = data.SeriesFormat, SeriesName = data.SeriesName, LibraryId = data.LibraryId, Pages = data.Pages, ChapterTitle = data.TitleName, LibraryType = data.LibraryType }) .AsNoTracking() .AsSplitQuery() .SingleOrDefaultAsync(); return chapterInfo; } public Task GetChapterTotalPagesAsync(int chapterId) { return _context.Chapter .Where(c => c.Id == chapterId) .Select(c => c.Pages) .FirstOrDefaultAsync(); } public async Task GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) { var chapter = await _context.Chapter .Includes(includes) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking() .AsSplitQuery() .FirstOrDefaultAsync(c => c.Id == chapterId); return chapter; } public async Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) { var chapter = await _context.Chapter .Includes(includes) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking() .AsSplitQuery() .SingleOrDefaultAsync(c => c.Id == chapterId); return chapter; } /// /// Returns non-tracked files for a given chapterId /// /// /// public async Task> GetFilesForChapterAsync(int chapterId) { return await _context.MangaFile .Where(c => chapterId == c.ChapterId) .AsNoTracking() .ToListAsync(); } /// /// Returns a Chapter for an Id. Includes linked s. /// /// /// /// public async Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) { return await _context.Chapter .Includes(includes) .OrderBy(c => c.SortOrder) .FirstOrDefaultAsync(c => c.Id == chapterId); } /// /// Returns Chapters for a volume id. /// /// /// public async Task> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None) { return await _context.Chapter .Where(c => c.VolumeId == volumeId) .Includes(includes) .OrderBy(c => c.SortOrder) .ToListAsync(); } /// /// Returns the cover image for a chapter id. /// /// /// public async Task GetChapterCoverImageAsync(int chapterId) { return await _context.Chapter .Where(c => c.Id == chapterId) .Select(c => c.CoverImage) .SingleOrDefaultAsync(); } public async Task> GetAllCoverImagesAsync() { return (await _context.Chapter .Select(c => c.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) .ToListAsync())!; } public async Task> GetAllChaptersWithCoversInDifferentEncoding(EncodeFormat format) { var extension = format.GetExtension(); return await _context.Chapter .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) .ToListAsync(); } /// /// Returns cover images for locked chapters /// /// public async Task> GetCoverImagesForLockedChaptersAsync() { return (await _context.Chapter .Where(c => c.CoverImageLocked) .Select(c => c.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) .ToListAsync())!; } /// /// Returns non-tracked files for a set of /// /// List of chapter Ids /// public async Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds) { return await _context.MangaFile .Where(c => chapterIds.Contains(c.ChapterId)) .AsNoTracking() .ToListAsync(); } public async Task AddChapterModifiers(int userId, ChapterDto chapter) { var progress = await _context.AppUserProgresses.Where(x => x.AppUserId == userId && x.ChapterId == chapter.Id) .AsNoTracking() .FirstOrDefaultAsync(); if (progress != null) { chapter.PagesRead = progress.PagesRead ; chapter.LastReadingProgressUtc = progress.LastModifiedUtc; chapter.LastReadingProgress = progress.LastModified; } else { chapter.PagesRead = 0; chapter.LastReadingProgressUtc = DateTime.MinValue; chapter.LastReadingProgress = DateTime.MinValue; } return chapter; } /// /// Includes Volumes /// /// /// public IEnumerable GetChaptersForSeries(int seriesId) { return _context.Chapter .Where(c => c.Volume.SeriesId == seriesId) .OrderBy(c => c.SortOrder) .Include(c => c.Volume) .AsEnumerable(); } public async Task> GetAllChaptersForSeries(int seriesId) { return await _context.Chapter .Where(c => c.Volume.SeriesId == seriesId) .OrderBy(c => c.SortOrder) .Include(c => c.Volume) .Include(c => c.People) .ThenInclude(cp => cp.Person) .ToListAsync(); } public async Task GetAverageUserRating(int chapterId, int userId) { // If there is 0 or 1 rating and that rating is you, return 0 back var countOfRatingsThatAreUser = await _context.AppUserChapterRating .Where(r => r.ChapterId == chapterId && r.HasBeenRated) .CountAsync(u => u.AppUserId == userId); if (countOfRatingsThatAreUser == 1) { return 0; } var avg = (await _context.AppUserChapterRating .Where(r => r.ChapterId == chapterId && r.HasBeenRated) .AverageAsync(r => (int?) r.Rating)); return avg.HasValue ? (int) (avg.Value * 20) : 0; } public async Task> GetExternalChapterReviewDtos(int chapterId) { return await _context.Chapter .Where(c => c.Id == chapterId) .SelectMany(c => c.ExternalReviews) // Don't use ProjectTo, it fails to map int to float (??) .Select(r => _mapper.Map(r)) .ToListAsync(); } public async Task> GetExternalChapterReview(int chapterId) { return await _context.Chapter .Where(c => c.Id == chapterId) .SelectMany(c => c.ExternalReviews) .ToListAsync(); } public async Task> GetExternalChapterRatingDtos(int chapterId) { return await _context.Chapter .Where(c => c.Id == chapterId) .SelectMany(c => c.ExternalRatings) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } public async Task> GetExternalChapterRatings(int chapterId) { return await _context.Chapter .Where(c => c.Id == chapterId) .SelectMany(c => c.ExternalRatings) .ToListAsync(); } }