using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using AutoMapper; using Kavita.API.Repositories; using Kavita.Database.Extensions; using Kavita.Models.DTOs; using Kavita.Models.Entities; using Kavita.Models.Entities.Enums; using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; namespace Kavita.Database.Repositories; public class VolumeRepository(DataContext context, IMapper mapper) : IVolumeRepository { public void Add(Volume volume) { context.Volume.Add(volume); } public void Update(Volume volume) { context.Entry(volume).State = EntityState.Modified; } public void Remove(Volume volume) { context.Volume.Remove(volume); } public void Remove(IList volumes) { context.Volume.RemoveRange(volumes); } /// /// Returns a list of non-tracked files for a given volume. /// /// /// /// public async Task> GetFilesForVolume(int volumeId, CancellationToken ct = default) { return await context.Chapter .Where(c => volumeId == c.VolumeId) .Include(c => c.Files) .SelectMany(c => c.Files) .AsSplitQuery() .AsNoTracking() .ToListAsync(ct); } /// /// Returns the cover image file for the given volume /// /// /// /// public async Task GetVolumeCoverImageAsync(int volumeId, CancellationToken ct = default) { return await context.Volume .Where(v => v.Id == volumeId) .Select(v => v.CoverImage) .SingleOrDefaultAsync(ct); } /// /// Returns all chapter Ids belonging to a list of Volume Ids /// /// /// /// public async Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds, CancellationToken ct = default) { return await context.Chapter .Where(c => volumeIds.Contains(c.VolumeId)) .Select(c => c.Id) .ToListAsync(ct); } /// /// Returns all volumes that contain a seriesId in a passed array. /// /// /// Include chapter entities /// /// public async Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false, CancellationToken ct = default) { var query = context.Volume .Where(v => seriesIds.Contains(v.SeriesId)); if (includeChapters) { query = query .Includes(VolumeIncludes.Chapters) .AsSplitQuery(); } var volumes = await query.ToListAsync(ct); foreach (var volume in volumes) { volume.Chapters = volume.Chapters.OrderBy(c => c.SortOrder).ToList(); } return volumes; } /// /// Returns an individual Volume including Chapters and Files and Reading Progress for a given volumeId /// /// /// /// /// public async Task GetVolumeDtoAsync(int volumeId, int userId, CancellationToken ct = default) { return await context.Volume .Where(vol => vol.Id == volumeId) .Includes(VolumeIncludes.Chapters | VolumeIncludes.Files) .AsSplitQuery() .OrderBy(v => v.MinNumber) .ProjectToWithProgress(mapper, userId) .FirstOrDefaultAsync(vol => vol.Id == volumeId, ct); } /// /// Returns the full Volumes including Chapters and Files for a given series /// /// /// /// public async Task> GetVolumes(int seriesId, CancellationToken ct = default) { return await context.Volume .Where(vol => vol.SeriesId == seriesId) .Includes(VolumeIncludes.Chapters | VolumeIncludes.Files) .AsSplitQuery() .OrderBy(vol => vol.MinNumber) .ToListAsync(ct); } public async Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None, CancellationToken ct = default) { return await context.Volume .Where(vol => volumeIds.Contains(vol.Id)) .Includes(includes) .AsSplitQuery() .OrderBy(vol => vol.MinNumber) .ToListAsync(ct); } /// /// Returns a single volume with Chapter and Files /// /// /// /// /// public async Task GetVolumeByIdAsync(int volumeId, VolumeIncludes includes = VolumeIncludes.Files, CancellationToken ct = default) { return await context.Volume .Includes(includes) .AsSplitQuery() .SingleOrDefaultAsync(vol => vol.Id == volumeId, ct); } /// /// Returns all volumes for a given series with progress information attached. Includes all Chapters as well. /// /// /// /// /// /// public async Task> GetVolumesDtoAsync(int seriesId, int userId, VolumeIncludes includes = VolumeIncludes.Chapters, CancellationToken ct = default) { return await context.Volume .Where(vol => vol.SeriesId == seriesId) .Includes(includes) .OrderBy(volume => volume.MinNumber) .ProjectToWithProgress(mapper, userId) .AsSplitQuery() .ToListAsync(ct); } public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, CancellationToken ct = default) { var extension = encodeFormat.GetExtension(); return await context.Volume .Includes(VolumeIncludes.Chapters) .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) .AsSplitQuery() .ToListAsync(ct); } /// /// Returns cover images for locked chapters /// /// /// public async Task> GetCoverImagesForLockedVolumesAsync(CancellationToken ct = default) { return (await context.Volume .Where(c => c.CoverImageLocked) .Select(c => c.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) .ToListAsync(ct))!; } public async Task GetFilesizeForVolumeAsync(int volumeId, CancellationToken ct = default) { return await context.Chapter .Where(c => volumeId == c.VolumeId) .Include(c => c.Files) .SelectMany(c => c.Files) .SumAsync(f => f.Bytes, cancellationToken: ct); } public async Task> GetFilesizeForVolumesAsync(IList volumeIds, CancellationToken ct = default) { return await volumeIds.BatchToDictionaryAsync(50, batch => context.Chapter .Where(c => batch.Contains(c.VolumeId)) .GroupBy(c => c.VolumeId) .Select(g => new { VolumeId = g.Key, TotalBytes = g.SelectMany(c => c.Files).Sum(f => f.Bytes) }) .ToDictionaryAsync(x => x.VolumeId, x => x.TotalBytes, cancellationToken: ct)); } }