using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; using API.DTOs; using API.DTOs.Filtering; using API.DTOs.JumpBar; using API.DTOs.Metadata; using API.Entities; using API.Entities.Enums; using AutoMapper; using AutoMapper.QueryableExtensions; using Kavita.Common.Extensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; [Flags] public enum LibraryIncludes { None = 1, Series = 2, AppUser = 4, Folders = 8, // Ratings = 16 } public interface ILibraryRepository { void Add(Library library); void Update(Library library); void Delete(Library library); Task> GetLibraryDtosAsync(); Task LibraryExists(string libraryName); Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes); Task GetFullLibraryForIdAsync(int libraryId); Task GetFullLibraryForIdAsync(int libraryId, int seriesId); Task> GetLibraryDtosForUsernameAsync(string userName); Task> GetLibrariesAsync(); Task DeleteLibrary(int libraryId); Task> GetLibrariesForUserIdAsync(int userId); Task GetLibraryTypeAsync(int libraryId); Task> GetLibraryForIdsAsync(IList libraryIds); Task GetTotalFiles(); IEnumerable GetJumpBarAsync(int libraryId); Task> GetAllAgeRatingsDtosForLibrariesAsync(List libraryIds); Task> GetAllLanguagesForLibrariesAsync(List libraryIds); IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds); } public class LibraryRepository : ILibraryRepository { private readonly DataContext _context; private readonly IMapper _mapper; public LibraryRepository(DataContext context, IMapper mapper) { _context = context; _mapper = mapper; } public void Add(Library library) { _context.Library.Add(library); } public void Update(Library library) { _context.Entry(library).State = EntityState.Modified; } public void Delete(Library library) { _context.Library.Remove(library); } public async Task> GetLibraryDtosForUsernameAsync(string userName) { return await _context.Library .Include(l => l.AppUsers) .Where(library => library.AppUsers.Any(x => x.UserName == userName)) .OrderBy(l => l.Name) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking() .AsSingleQuery() .ToListAsync(); } public async Task> GetLibrariesAsync() { return await _context.Library .Include(l => l.AppUsers) .ToListAsync(); } public async Task DeleteLibrary(int libraryId) { var library = await GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders | LibraryIncludes.Series); _context.Library.Remove(library); return await _context.SaveChangesAsync() > 0; } public async Task> GetLibrariesForUserIdAsync(int userId) { return await _context.Library .Include(l => l.AppUsers) .Where(l => l.AppUsers.Select(ap => ap.Id).Contains(userId)) .AsNoTracking() .ToListAsync(); } public async Task GetLibraryTypeAsync(int libraryId) { return await _context.Library .Where(l => l.Id == libraryId) .AsNoTracking() .Select(l => l.Type) .SingleAsync(); } public async Task> GetLibraryForIdsAsync(IList libraryIds) { return await _context.Library .Where(x => libraryIds.Contains(x.Id)) .ToListAsync(); } public async Task GetTotalFiles() { return await _context.MangaFile.CountAsync(); } public IEnumerable GetJumpBarAsync(int libraryId) { var seriesSortCharacters = _context.Series.Where(s => s.LibraryId == libraryId) .Select(s => s.SortName.ToUpper()) .OrderBy(s => s) .AsEnumerable() .Select(s => s[0]); // Map the title to the number of entities var firstCharacterMap = new Dictionary(); foreach (var sortChar in seriesSortCharacters) { var c = sortChar; var isAlpha = char.IsLetter(sortChar); if (!isAlpha) c = '#'; if (!firstCharacterMap.ContainsKey(c)) { firstCharacterMap[c] = 0; } firstCharacterMap[c] += 1; } return firstCharacterMap.Keys.Select(k => new JumpKeyDto() { Key = k + string.Empty, Size = firstCharacterMap[k], Title = k + string.Empty }); } public async Task> GetLibraryDtosAsync() { return await _context.Library .Include(f => f.Folders) .OrderBy(l => l.Name) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking() .ToListAsync(); } public async Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes) { var query = _context.Library .Where(x => x.Id == libraryId); query = AddIncludesToQuery(query, includes); return await query.SingleAsync(); } private static IQueryable AddIncludesToQuery(IQueryable query, LibraryIncludes includeFlags) { if (includeFlags.HasFlag(LibraryIncludes.Folders)) { query = query.Include(l => l.Folders); } if (includeFlags.HasFlag(LibraryIncludes.Series)) { query = query.Include(l => l.Series); } if (includeFlags.HasFlag(LibraryIncludes.AppUser)) { query = query.Include(l => l.AppUsers); } return query; } /// /// This returns a Library with all it's Series -> Volumes -> Chapters. This is expensive. Should only be called when needed. /// /// /// public async Task GetFullLibraryForIdAsync(int libraryId) { return await _context.Library .Where(x => x.Id == libraryId) .Include(f => f.Folders) .Include(l => l.Series) .ThenInclude(s => s.Metadata) .Include(l => l.Series) .ThenInclude(s => s.Volumes) .ThenInclude(v => v.Chapters) .ThenInclude(c => c.Files) .AsSplitQuery() .SingleAsync(); } /// /// This is a heavy call, pulls all entities for a Library, except this version only grabs for one series id /// /// /// /// public async Task GetFullLibraryForIdAsync(int libraryId, int seriesId) { return await _context.Library .Where(x => x.Id == libraryId) .Include(f => f.Folders) .Include(l => l.Series.Where(s => s.Id == seriesId)) .ThenInclude(s => s.Metadata) .Include(l => l.Series.Where(s => s.Id == seriesId)) .ThenInclude(s => s.Volumes) .ThenInclude(v => v.Chapters) .ThenInclude(c => c.Files) .AsSplitQuery() .SingleAsync(); } public async Task LibraryExists(string libraryName) { return await _context.Library .AsNoTracking() .AnyAsync(x => x.Name == libraryName); } public async Task> GetLibrariesForUserAsync(AppUser user) { return await _context.Library .Where(library => library.AppUsers.Contains(user)) .Include(l => l.Folders) .AsNoTracking() .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } public async Task> GetAllAgeRatingsDtosForLibrariesAsync(List libraryIds) { return await _context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Select(s => s.Metadata.AgeRating) .Distinct() .Select(s => new AgeRatingDto() { Value = s, Title = s.ToDescription() }) .ToListAsync(); } public async Task> GetAllLanguagesForLibrariesAsync(List libraryIds) { var ret = await _context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Select(s => s.Metadata.Language) .AsNoTracking() .Distinct() .ToListAsync(); return ret .Where(s => !string.IsNullOrEmpty(s)) .Select(s => new LanguageDto() { Title = CultureInfo.GetCultureInfo(s).DisplayName, IsoCode = s }) .OrderBy(s => s.Title) .ToList(); } public IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds) { return _context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Select(s => s.Metadata.PublicationStatus) .Distinct() .AsEnumerable() .Select(s => new PublicationStatusDto() { Value = s, Title = s.ToDescription() }) .OrderBy(s => s.Title); } }