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 API.Extensions; using API.Extensions.QueryExtensions; using API.Services.Tasks.Scanner.Parser; 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, FileTypes = 16, ExcludePatterns = 32 } 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 = LibraryIncludes.None); IEnumerable GetLibraryDtosForUsernameAsync(string userName); Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None); Task> GetLibrariesForUserIdAsync(int userId); IEnumerable GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None); Task GetLibraryTypeAsync(int libraryId); Task> GetLibraryForIdsAsync(IEnumerable libraryIds, LibraryIncludes includes = LibraryIncludes.None); Task GetTotalFiles(); IEnumerable GetJumpBarAsync(int libraryId); Task> GetAllAgeRatingsDtosForLibrariesAsync(List libraryIds); Task> GetAllLanguagesForLibrariesAsync(List? libraryIds); IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds); Task DoAnySeriesFoldersMatch(IEnumerable folders); Task GetLibraryCoverImageAsync(int libraryId); Task> GetAllCoverImagesAsync(); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); Task GetAllowsScrobblingBySeriesId(int seriesId); } 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) { if (library == null) return; _context.Library.Remove(library); } public IEnumerable GetLibraryDtosForUsernameAsync(string userName) { return _context.Library .Include(l => l.AppUsers) .Include(l => l.LibraryFileTypes) .Include(l => l.LibraryExcludePatterns) .Where(library => library.AppUsers.Any(x => x.UserName!.Equals(userName))) .OrderBy(l => l.Name) .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() .AsEnumerable(); } /// /// Returns all libraries including their AppUsers + extra includes /// /// /// public async Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None) { return await _context.Library .Include(l => l.AppUsers) .Includes(includes) .ToListAsync(); } /// /// This does not track /// /// /// 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 IEnumerable GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None) { return _context.Library .IsRestricted(queryContext) .Where(l => l.AppUsers.Select(ap => ap.Id).Contains(userId)) .Select(l => l.Id) .AsEnumerable(); } public async Task GetLibraryTypeAsync(int libraryId) { return await _context.Library .Where(l => l.Id == libraryId) .AsNoTracking() .Select(l => l.Type) .FirstAsync(); } public async Task> GetLibraryForIdsAsync(IEnumerable libraryIds, LibraryIncludes includes = LibraryIncludes.None) { return await _context.Library .Where(x => libraryIds.Contains(x.Id)) .Includes(includes) .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 = '#'; firstCharacterMap.TryAdd(c, 0); firstCharacterMap[c] += 1; } return firstCharacterMap.Keys.Select(k => new JumpKeyDto() { Key = k + string.Empty, Size = firstCharacterMap[k], Title = k + string.Empty }); } /// /// Returns all Libraries with their Folders /// /// public async Task> GetLibraryDtosAsync() { return await _context.Library .Include(f => f.Folders) .Include(l => l.LibraryFileTypes) .OrderBy(l => l.Name) .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() .AsNoTracking() .ToListAsync(); } public async Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None) { var query = _context.Library .Where(x => x.Id == libraryId) .Includes(includes); return await query.SingleOrDefaultAsync(); } public async Task LibraryExists(string libraryName) { return await _context.Library .AsNoTracking() .AnyAsync(x => x.Name != null && x.Name.Equals(libraryName)); } public async Task> GetLibrariesForUserAsync(AppUser user) { return await _context.Library .Where(library => library.AppUsers.Contains(user)) .Include(l => l.Folders) .AsNoTracking() .AsSplitQuery() .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 .WhereIf(libraryIds is {Count: > 0} , s => libraryIds.Contains(s.LibraryId)) .Select(s => s.Metadata.Language) .AsSplitQuery() .AsNoTracking() .Distinct() .ToListAsync(); return ret .Where(s => !string.IsNullOrEmpty(s)) .DistinctBy(Parser.Normalize) .Select(GetCulture) .Where(s => s != null) .OrderBy(s => s.Title) .ToList(); } private static LanguageDto GetCulture(string s) { try { return new LanguageDto() { Title = CultureInfo.GetCultureInfo(s).DisplayName, IsoCode = s }; } catch (Exception) { // ignored } return new LanguageDto() { Title = s, IsoCode = s };; } public IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds) { return _context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .AsSplitQuery() .Select(s => s.Metadata.PublicationStatus) .Distinct() .AsEnumerable() .Select(s => new PublicationStatusDto() { Value = s, Title = s.ToDescription() }) .OrderBy(s => s.Title); } /// /// Checks if any series folders match the folders passed in /// /// /// public async Task DoAnySeriesFoldersMatch(IEnumerable folders) { var normalized = folders.Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath); return await _context.Series.AnyAsync(s => normalized.Contains(s.FolderPath)); } public Task GetLibraryCoverImageAsync(int libraryId) { return _context.Library .Where(l => l.Id == libraryId) .Select(l => l.CoverImage) .SingleOrDefaultAsync(); } public async Task> GetAllCoverImagesAsync() { return (await _context.ReadingList .Select(t => t.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) .ToListAsync())!; } public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) { var extension = encodeFormat.GetExtension(); return await _context.Library .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) .ToListAsync(); } public async Task GetAllowsScrobblingBySeriesId(int seriesId) { return await _context.Series.Where(s => s.Id == seriesId) .Select(s => s.Library.AllowScrobbling) .SingleOrDefaultAsync(); } }