using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Data.Scanner; using API.DTOs; using API.DTOs.CollectionTags; using API.DTOs.Filtering; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Extensions; using API.Helpers; using API.Interfaces.Repositories; using API.Services.Tasks; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories { public class SeriesRepository : ISeriesRepository { private readonly DataContext _context; private readonly IMapper _mapper; public SeriesRepository(DataContext context, IMapper mapper) { _context = context; _mapper = mapper; } public void Attach(Series series) { _context.Series.Attach(series); } public void Update(Series series) { _context.Entry(series).State = EntityState.Modified; } public void Remove(Series series) { _context.Series.Remove(series); } public void Remove(IEnumerable series) { _context.Series.RemoveRange(series); } /// /// Returns if a series name and format exists already in a library /// /// Name of series /// Format of series /// public async Task DoesSeriesNameExistInLibrary(string name, MangaFormat format) { var libraries = _context.Series .AsNoTracking() .Where(x => x.Name.Equals(name) && x.Format == format) .Select(s => s.LibraryId); return await _context.Series .AsNoTracking() .Where(s => libraries.Contains(s.LibraryId) && s.Name.Equals(name) && s.Format == format) .CountAsync() > 1; } public async Task> GetSeriesForLibraryIdAsync(int libraryId) { return await _context.Series .Where(s => s.LibraryId == libraryId) .OrderBy(s => s.SortName) .ToListAsync(); } /// /// Used for to /// /// /// public async Task> GetFullSeriesForLibraryIdAsync(int libraryId, UserParams userParams) { var query = _context.Series .Where(s => s.LibraryId == libraryId) .Include(s => s.Metadata) .ThenInclude(m => m.People) .Include(s => s.Metadata) .ThenInclude(m => m.Genres) .Include(s => s.Volumes) .ThenInclude(v => v.Chapters) .ThenInclude(cm => cm.People) .Include(s => s.Volumes) .ThenInclude(v => v.Chapters) .ThenInclude(c => c.Files) .AsSplitQuery() .OrderBy(s => s.SortName); return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } /// /// This is a heavy call. Returns all entities down to Files and Library and Series Metadata. /// /// /// public async Task GetFullSeriesForSeriesIdAsync(int seriesId) { return await _context.Series .Where(s => s.Id == seriesId) .Include(s => s.Metadata) .ThenInclude(m => m.People) .Include(s => s.Metadata) .ThenInclude(m => m.Genres) .Include(s => s.Library) .Include(s => s.Volumes) .ThenInclude(v => v.Chapters) .ThenInclude(cm => cm.People) .Include(s => s.Volumes) .ThenInclude(v => v.Chapters) .ThenInclude(c => c.Files) .AsSplitQuery() .SingleOrDefaultAsync(); } public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter) { var formats = filter.GetSqlFilter(); var query = _context.Series .Where(s => s.LibraryId == libraryId && formats.Contains(s.Format)) .OrderBy(s => s.SortName) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking(); return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } public async Task> SearchSeries(int[] libraryIds, string searchQuery) { return await _context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%") || EF.Functions.Like(s.OriginalName, $"%{searchQuery}%") || EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%")) .Include(s => s.Library) .OrderBy(s => s.SortName) .AsNoTracking() .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } public async Task GetSeriesDtoByIdAsync(int seriesId, int userId) { var series = await _context.Series.Where(x => x.Id == seriesId) .ProjectTo(_mapper.ConfigurationProvider) .SingleAsync(); var seriesList = new List() {series}; await AddSeriesModifiers(userId, seriesList); return seriesList[0]; } public async Task DeleteSeriesAsync(int seriesId) { var series = await _context.Series.Where(s => s.Id == seriesId).SingleOrDefaultAsync(); _context.Series.Remove(series); return await _context.SaveChangesAsync() > 0; } /// /// Returns Volumes, Metadata, and Collection Tags /// /// /// public async Task GetSeriesByIdAsync(int seriesId) { return await _context.Series .Include(s => s.Volumes) .Include(s => s.Metadata) .ThenInclude(m => m.CollectionTags) .Include(s => s.Metadata) .ThenInclude(m => m.Genres) .Include(s => s.Metadata) .ThenInclude(m => m.People) .Where(s => s.Id == seriesId) .SingleOrDefaultAsync(); } /// /// Returns Volumes, Metadata, and Collection Tags /// /// /// public async Task> GetSeriesByIdsAsync(IList seriesIds) { return await _context.Series .Include(s => s.Volumes) .Include(s => s.Metadata) .ThenInclude(m => m.CollectionTags) .Where(s => seriesIds.Contains(s.Id)) .ToListAsync(); } public async Task GetChapterIdsForSeriesAsync(int[] seriesIds) { var volumes = await _context.Volume .Where(v => seriesIds.Contains(v.SeriesId)) .Include(v => v.Chapters) .ToListAsync(); IList chapterIds = new List(); foreach (var v in volumes) { foreach (var c in v.Chapters) { chapterIds.Add(c.Id); } } return chapterIds.ToArray(); } /// /// This returns a dictonary mapping seriesId -> list of chapters back for each series id passed /// /// /// public async Task>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds) { var volumes = await _context.Volume .Where(v => seriesIds.Contains(v.SeriesId)) .Include(v => v.Chapters) .ToListAsync(); var seriesChapters = new Dictionary>(); foreach (var v in volumes) { foreach (var c in v.Chapters) { if (!seriesChapters.ContainsKey(v.SeriesId)) { var list = new List(); seriesChapters.Add(v.SeriesId, list); } seriesChapters[v.SeriesId].Add(c.Id); } } return seriesChapters; } public async Task AddSeriesModifiers(int userId, List series) { var userProgress = await _context.AppUserProgresses .Where(p => p.AppUserId == userId && series.Select(s => s.Id).Contains(p.SeriesId)) .ToListAsync(); var userRatings = await _context.AppUserRating .Where(r => r.AppUserId == userId && series.Select(s => s.Id).Contains(r.SeriesId)) .ToListAsync(); foreach (var s in series) { s.PagesRead = userProgress.Where(p => p.SeriesId == s.Id).Sum(p => p.PagesRead); var rating = userRatings.SingleOrDefault(r => r.SeriesId == s.Id); if (rating == null) continue; s.UserRating = rating.Rating; s.UserReview = rating.Review; } } public async Task GetSeriesCoverImageAsync(int seriesId) { return await _context.Series .Where(s => s.Id == seriesId) .Select(s => s.CoverImage) .AsNoTracking() .SingleOrDefaultAsync(); } /// /// Returns a list of Series that were added, ordered by Created desc /// /// /// Library to restrict to, if 0, will apply to all libraries /// Contains pagination information /// Optional filter on query /// public async Task> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter) { var formats = filter.GetSqlFilter(); if (libraryId == 0) { var userLibraries = _context.Library .Include(l => l.AppUsers) .Where(library => library.AppUsers.Any(user => user.Id == userId)) .AsNoTracking() .Select(library => library.Id) .ToList(); var allQuery = _context.Series .Where(s => userLibraries.Contains(s.LibraryId) && formats.Contains(s.Format)) .OrderByDescending(s => s.Created) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking(); return await PagedList.CreateAsync(allQuery, userParams.PageNumber, userParams.PageSize); } var query = _context.Series .Where(s => s.LibraryId == libraryId && formats.Contains(s.Format)) .OrderByDescending(s => s.Created) .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() .AsNoTracking(); return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } /// /// Returns Series that the user has some partial progress on. Sorts based on activity. Sort first by User progress, but if a series /// has been updated recently, bump it to the front. /// /// /// Library to restrict to, if 0, will apply to all libraries /// Pagination information /// Optional (default null) filter on query /// public async Task> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter) { var formats = filter.GetSqlFilter(); IList userLibraries; if (libraryId == 0) { userLibraries = _context.Library .Include(l => l.AppUsers) .Where(library => library.AppUsers.Any(user => user.Id == userId)) .AsNoTracking() .Select(library => library.Id) .ToList(); } else { userLibraries = new List() {libraryId}; } var series = _context.Series .Where(s => formats.Contains(s.Format) && userLibraries.Contains(s.LibraryId)) .Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) => new { Series = s, PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId).Sum(s1 => s1.PagesRead), progress.AppUserId, LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id && p.AppUserId == userId).Max(p => p.LastModified) }) .AsNoTracking(); var retSeries = series.Where(s => s.AppUserId == userId && s.PagesRead > 0 && s.PagesRead < s.Series.Pages) .OrderByDescending(s => s.LastModified) .ThenByDescending(s => s.Series.LastModified) .Select(s => s.Series) .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() .AsNoTracking(); // Pagination does not work for this query as when we pull the data back, we get multiple rows of the same series. See controller for pagination code return await retSeries.ToListAsync(); } public async Task GetSeriesMetadata(int seriesId) { var metadataDto = await _context.SeriesMetadata .Where(metadata => metadata.SeriesId == seriesId) .Include(m => m.Genres) .AsNoTracking() .ProjectTo(_mapper.ConfigurationProvider) .SingleOrDefaultAsync(); if (metadataDto != null) { metadataDto.Tags = await _context.CollectionTag .Include(t => t.SeriesMetadatas) .Where(t => t.SeriesMetadatas.Select(s => s.SeriesId).Contains(seriesId)) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking() .ToListAsync(); } return metadataDto; } public async Task> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams) { var userLibraries = _context.Library .Include(l => l.AppUsers) .Where(library => library.AppUsers.Any(user => user.Id == userId)) .AsNoTracking() .Select(library => library.Id) .ToList(); var query = _context.CollectionTag .Where(s => s.Id == collectionId) .Include(c => c.SeriesMetadatas) .ThenInclude(m => m.Series) .SelectMany(c => c.SeriesMetadatas.Select(sm => sm.Series).Where(s => userLibraries.Contains(s.LibraryId))) .OrderBy(s => s.LibraryId) .ThenBy(s => s.SortName) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking(); return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } public async Task> GetFilesForSeries(int seriesId) { return await _context.Volume .Where(v => v.SeriesId == seriesId) .Include(v => v.Chapters) .ThenInclude(c => c.Files) .SelectMany(v => v.Chapters.SelectMany(c => c.Files)) .AsNoTracking() .ToListAsync(); } public async Task> GetSeriesDtoForIdsAsync(IEnumerable seriesIds, int userId) { var allowedLibraries = _context.Library .Include(l => l.AppUsers) .Where(library => library.AppUsers.Any(x => x.Id == userId)) .Select(l => l.Id); return await _context.Series .Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId)) .OrderBy(s => s.SortName) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking() .AsSplitQuery() .ToListAsync(); } public async Task> GetAllCoverImagesAsync() { return await _context.Series .Select(s => s.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) .AsNoTracking() .ToListAsync(); } public async Task> GetLockedCoverImagesAsync() { return await _context.Series .Where(s => s.CoverImageLocked && !string.IsNullOrEmpty(s.CoverImage)) .Select(s => s.CoverImage) .AsNoTracking() .ToListAsync(); } /// /// Returns the number of series for a given library (or all libraries if libraryId is 0) /// /// Defaults to 0, library to restrict count to /// private async Task GetSeriesCount(int libraryId = 0) { if (libraryId > 0) { return await _context.Series .Where(s => s.LibraryId == libraryId) .CountAsync(); } return await _context.Series.CountAsync(); } /// /// Returns the number of series that should be processed in parallel to optimize speed and memory. Minimum of 50 /// /// Defaults to 0 meaning no library /// private async Task> GetChunkSize(int libraryId = 0) { var totalSeries = await GetSeriesCount(libraryId); return new Tuple(totalSeries, 50); } public async Task GetChunkInfo(int libraryId = 0) { var (totalSeries, chunkSize) = await GetChunkSize(libraryId); if (totalSeries == 0) return new Chunk() { TotalChunks = 0, TotalSize = 0, ChunkSize = 0 }; var totalChunks = Math.Max((int) Math.Ceiling((totalSeries * 1.0) / chunkSize), 1); return new Chunk() { TotalSize = totalSeries, ChunkSize = chunkSize, TotalChunks = totalChunks }; } public async Task> GetSeriesMetadataForIdsAsync(IEnumerable seriesIds) { return await _context.SeriesMetadata .Where(sm => seriesIds.Contains(sm.SeriesId)) .Include(sm => sm.CollectionTags) .ToListAsync(); } } }