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.Entities; using API.Extensions; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; [Flags] public enum ChapterIncludes { None = 1, Volumes = 2, Files = 4, } public interface IChapterRepository { void Update(Chapter chapter); 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); Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds); Task GetChapterCoverImageAsync(int chapterId); Task> GetAllCoverImagesAsync(); Task> GetAllChaptersWithNonWebPCovers(); Task> GetCoverImagesForLockedChaptersAsync(); Task AddChapterModifiers(int userId, ChapterDto chapter); } 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 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.Range, 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, VolumeNumber = data.VolumeNumber + string.Empty, 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) .FirstOrDefaultAsync(c => c.Id == chapterId); } /// /// Returns Chapters for a volume id. /// /// /// public async Task> GetChaptersAsync(int volumeId) { return await _context.Chapter .Where(c => c.VolumeId == volumeId) .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) .AsNoTracking() .SingleOrDefaultAsync(); } public async Task> GetAllCoverImagesAsync() { return await _context.Chapter .Select(c => c.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) .AsNoTracking() .ToListAsync(); } public async Task> GetAllChaptersWithNonWebPCovers() { return await _context.Chapter .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(".webp")) .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)) .AsNoTracking() .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; } else { chapter.PagesRead = 0; chapter.LastReadingProgressUtc = DateTime.MinValue; } return chapter; } }