.NET 6 Coding Patterns + Unit Tests (#823)

* Refactored all files to have Interfaces within the same file. Started moving over to file-scoped namespaces.

* Refactored common methods for getting underlying file's cover, pages, and extracting into 1 interface.

* More refactoring around removing dependence on explicit filetype testing for getting information.

* Code is buildable, tests are broken. Huge refactor (not completed) which makes most of DirectoryService testable with a mock filesystem (and thus the services that utilize it).

* Finished porting DirectoryService to use mocked filesystem implementation.

* Added a null check

* Added a null check

* Finished all unit tests for DirectoryService.

* Some misc cleanup on the code

* Fixed up some bugs from refactoring scan loop.

* Implemented CleanupService testing and refactored more of DirectoryService to be non-static.

Fixed a bug where cover file cleanup wasn't properly finding files due to a regex bug.

* Fixed an issue in CleanupBackup() where we weren't properly selecting database files older than 30 days. Finished CleanupService Tests.

* Refactored Flatten and RemoveNonImages to directory service to allow CacheService to be testable.

* Finally have CacheService tested. Rewrote GetCachedPagePath() to be much more straightforward & performant.

* Updated DefaultParserTests.cs to contain all existing tests and follow new test layout format.

* All tests fixed up
This commit is contained in:
Joseph Milazzo 2021-12-05 10:58:53 -06:00 committed by GitHub
parent bf1876ff44
commit bbe8f800f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
115 changed files with 6734 additions and 5370 deletions

View file

@ -2,78 +2,84 @@
using System.Threading.Tasks;
using API.Entities;
using API.Entities.Enums;
using API.Interfaces.Repositories;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories
namespace API.Data.Repositories;
public interface IAppUserProgressRepository
{
public class AppUserProgressRepository : IAppUserProgressRepository
void Update(AppUserProgress userProgress);
Task<int> CleanupAbandonedChapters();
Task<bool> UserHasProgress(LibraryType libraryType, int userId);
Task<AppUserProgress> GetUserProgressAsync(int chapterId, int userId);
}
public class AppUserProgressRepository : IAppUserProgressRepository
{
private readonly DataContext _context;
public AppUserProgressRepository(DataContext context)
{
private readonly DataContext _context;
_context = context;
}
public AppUserProgressRepository(DataContext context)
{
_context = context;
}
public void Update(AppUserProgress userProgress)
{
_context.Entry(userProgress).State = EntityState.Modified;
}
public void Update(AppUserProgress userProgress)
{
_context.Entry(userProgress).State = EntityState.Modified;
}
/// <summary>
/// This will remove any entries that have chapterIds that no longer exists. This will execute the save as well.
/// </summary>
public async Task<int> CleanupAbandonedChapters()
{
var chapterIds = _context.Chapter.Select(c => c.Id);
/// <summary>
/// This will remove any entries that have chapterIds that no longer exists. This will execute the save as well.
/// </summary>
public async Task<int> CleanupAbandonedChapters()
{
var chapterIds = _context.Chapter.Select(c => c.Id);
var rowsToRemove = await _context.AppUserProgresses
.Where(progress => !chapterIds.Contains(progress.ChapterId))
.ToListAsync();
var rowsToRemove = await _context.AppUserProgresses
.Where(progress => !chapterIds.Contains(progress.ChapterId))
.ToListAsync();
var rowsToRemoveBookmarks = await _context.AppUserBookmark
.Where(progress => !chapterIds.Contains(progress.ChapterId))
.ToListAsync();
var rowsToRemoveBookmarks = await _context.AppUserBookmark
.Where(progress => !chapterIds.Contains(progress.ChapterId))
.ToListAsync();
var rowsToRemoveReadingLists = await _context.ReadingListItem
.Where(item => !chapterIds.Contains(item.ChapterId))
.ToListAsync();
var rowsToRemoveReadingLists = await _context.ReadingListItem
.Where(item => !chapterIds.Contains(item.ChapterId))
.ToListAsync();
_context.RemoveRange(rowsToRemove);
_context.RemoveRange(rowsToRemoveBookmarks);
_context.RemoveRange(rowsToRemoveReadingLists);
return await _context.SaveChangesAsync() > 0 ? rowsToRemove.Count : 0;
}
_context.RemoveRange(rowsToRemove);
_context.RemoveRange(rowsToRemoveBookmarks);
_context.RemoveRange(rowsToRemoveReadingLists);
return await _context.SaveChangesAsync() > 0 ? rowsToRemove.Count : 0;
}
/// <summary>
/// Checks if user has any progress against a library of passed type
/// </summary>
/// <param name="libraryType"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<bool> UserHasProgress(LibraryType libraryType, int userId)
{
var seriesIds = await _context.AppUserProgresses
.Where(aup => aup.PagesRead > 0 && aup.AppUserId == userId)
.AsNoTracking()
.Select(aup => aup.SeriesId)
.ToListAsync();
/// <summary>
/// Checks if user has any progress against a library of passed type
/// </summary>
/// <param name="libraryType"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<bool> UserHasProgress(LibraryType libraryType, int userId)
{
var seriesIds = await _context.AppUserProgresses
.Where(aup => aup.PagesRead > 0 && aup.AppUserId == userId)
.AsNoTracking()
.Select(aup => aup.SeriesId)
.ToListAsync();
if (seriesIds.Count == 0) return false;
if (seriesIds.Count == 0) return false;
return await _context.Series
.Include(s => s.Library)
.Where(s => seriesIds.Contains(s.Id) && s.Library.Type == libraryType)
.AsNoTracking()
.AnyAsync();
}
return await _context.Series
.Include(s => s.Library)
.Where(s => seriesIds.Contains(s.Id) && s.Library.Type == libraryType)
.AsNoTracking()
.AnyAsync();
}
public async Task<AppUserProgress> GetUserProgressAsync(int chapterId, int userId)
{
return await _context.AppUserProgresses
.Where(p => p.ChapterId == chapterId && p.AppUserId == userId)
.FirstOrDefaultAsync();
}
public async Task<AppUserProgress> GetUserProgressAsync(int chapterId, int userId)
{
return await _context.AppUserProgresses
.Where(p => p.ChapterId == chapterId && p.AppUserId == userId)
.FirstOrDefaultAsync();
}
}

View file

@ -1,195 +1,207 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.DTOs;
using API.DTOs.Reader;
using API.Entities;
using API.Interfaces.Repositories;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories
namespace API.Data.Repositories;
public interface IChapterRepository
{
public class ChapterRepository : IChapterRepository
void Update(Chapter chapter);
Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds);
Task<IChapterInfoDto> GetChapterInfoDtoAsync(int chapterId);
Task<int> GetChapterTotalPagesAsync(int chapterId);
Task<Chapter> GetChapterAsync(int chapterId);
Task<ChapterDto> GetChapterDtoAsync(int chapterId);
Task<IList<MangaFile>> GetFilesForChapterAsync(int chapterId);
Task<IList<Chapter>> GetChaptersAsync(int volumeId);
Task<IList<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds);
Task<string> GetChapterCoverImageAsync(int chapterId);
Task<IList<string>> GetAllCoverImagesAsync();
Task<IEnumerable<string>> GetCoverImagesForLockedChaptersAsync();
}
public class ChapterRepository : IChapterRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public ChapterRepository(DataContext context, IMapper mapper)
{
private readonly DataContext _context;
private readonly IMapper _mapper;
_context = context;
_mapper = mapper;
}
public ChapterRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public void Update(Chapter chapter)
{
_context.Entry(chapter).State = EntityState.Modified;
}
public void Update(Chapter chapter)
{
_context.Entry(chapter).State = EntityState.Modified;
}
public async Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds)
{
return await _context.Chapter
.Where(c => chapterIds.Contains(c.Id))
.Include(c => c.Volume)
.ToListAsync();
}
public async Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds)
{
return await _context.Chapter
.Where(c => chapterIds.Contains(c.Id))
.Include(c => c.Volume)
.ToListAsync();
}
/// <summary>
/// Populates a partial IChapterInfoDto
/// </summary>
/// <returns></returns>
public async Task<IChapterInfoDto> 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.Number,
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
})
.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
})
.AsNoTracking()
.SingleOrDefaultAsync();
/// <summary>
/// Populates a partial IChapterInfoDto
/// </summary>
/// <returns></returns>
public async Task<IChapterInfoDto> 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.Number,
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
})
.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
})
.AsNoTracking()
.SingleOrDefaultAsync();
return chapterInfo;
}
return chapterInfo;
}
public Task<int> GetChapterTotalPagesAsync(int chapterId)
{
return _context.Chapter
.Where(c => c.Id == chapterId)
.Select(c => c.Pages)
.SingleOrDefaultAsync();
}
public async Task<ChapterDto> GetChapterDtoAsync(int chapterId)
{
var chapter = await _context.Chapter
.Include(c => c.Files)
.ProjectTo<ChapterDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.SingleOrDefaultAsync(c => c.Id == chapterId);
public Task<int> GetChapterTotalPagesAsync(int chapterId)
{
return _context.Chapter
.Where(c => c.Id == chapterId)
.Select(c => c.Pages)
.SingleOrDefaultAsync();
}
public async Task<ChapterDto> GetChapterDtoAsync(int chapterId)
{
var chapter = await _context.Chapter
.Include(c => c.Files)
.ProjectTo<ChapterDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.SingleOrDefaultAsync(c => c.Id == chapterId);
return chapter;
}
return chapter;
}
/// <summary>
/// Returns non-tracked files for a given chapterId
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
public async Task<IList<MangaFile>> GetFilesForChapterAsync(int chapterId)
{
return await _context.MangaFile
.Where(c => chapterId == c.ChapterId)
.AsNoTracking()
.ToListAsync();
}
/// <summary>
/// Returns non-tracked files for a given chapterId
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
public async Task<IList<MangaFile>> GetFilesForChapterAsync(int chapterId)
{
return await _context.MangaFile
.Where(c => chapterId == c.ChapterId)
.AsNoTracking()
.ToListAsync();
}
/// <summary>
/// Returns a Chapter for an Id. Includes linked <see cref="MangaFile"/>s.
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
public async Task<Chapter> GetChapterAsync(int chapterId)
{
return await _context.Chapter
.Include(c => c.Files)
.SingleOrDefaultAsync(c => c.Id == chapterId);
}
/// <summary>
/// Returns a Chapter for an Id. Includes linked <see cref="MangaFile"/>s.
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
public async Task<Chapter> GetChapterAsync(int chapterId)
{
return await _context.Chapter
.Include(c => c.Files)
.SingleOrDefaultAsync(c => c.Id == chapterId);
}
/// <summary>
/// Returns Chapters for a volume id.
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
public async Task<IList<Chapter>> GetChaptersAsync(int volumeId)
{
return await _context.Chapter
.Where(c => c.VolumeId == volumeId)
.ToListAsync();
}
/// <summary>
/// Returns Chapters for a volume id.
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
public async Task<IList<Chapter>> GetChaptersAsync(int volumeId)
{
return await _context.Chapter
.Where(c => c.VolumeId == volumeId)
.ToListAsync();
}
/// <summary>
/// Returns the cover image for a chapter id.
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
public async Task<string> GetChapterCoverImageAsync(int chapterId)
{
/// <summary>
/// Returns the cover image for a chapter id.
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
public async Task<string> GetChapterCoverImageAsync(int chapterId)
{
return await _context.Chapter
.Where(c => c.Id == chapterId)
.Select(c => c.CoverImage)
.AsNoTracking()
.SingleOrDefaultAsync();
}
return await _context.Chapter
.Where(c => c.Id == chapterId)
.Select(c => c.CoverImage)
.AsNoTracking()
.SingleOrDefaultAsync();
}
public async Task<IList<string>> GetAllCoverImagesAsync()
{
return await _context.Chapter
.Select(c => c.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.AsNoTracking()
.ToListAsync();
}
public async Task<IList<string>> GetAllCoverImagesAsync()
{
return await _context.Chapter
.Select(c => c.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.AsNoTracking()
.ToListAsync();
}
/// <summary>
/// Returns cover images for locked chapters
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<string>> GetCoverImagesForLockedChaptersAsync()
{
return await _context.Chapter
.Where(c => c.CoverImageLocked)
.Select(c => c.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.AsNoTracking()
.ToListAsync();
}
/// <summary>
/// Returns cover images for locked chapters
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<string>> GetCoverImagesForLockedChaptersAsync()
{
return await _context.Chapter
.Where(c => c.CoverImageLocked)
.Select(c => c.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.AsNoTracking()
.ToListAsync();
}
/// <summary>
/// Returns non-tracked files for a set of <paramref name="chapterIds"/>
/// </summary>
/// <param name="chapterIds">List of chapter Ids</param>
/// <returns></returns>
public async Task<IList<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds)
{
return await _context.MangaFile
.Where(c => chapterIds.Contains(c.ChapterId))
.AsNoTracking()
.ToListAsync();
}
/// <summary>
/// Returns non-tracked files for a set of <paramref name="chapterIds"/>
/// </summary>
/// <param name="chapterIds">List of chapter Ids</param>
/// <returns></returns>
public async Task<IList<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds)
{
return await _context.MangaFile
.Where(c => chapterIds.Contains(c.ChapterId))
.AsNoTracking()
.ToListAsync();
}
}

View file

@ -3,123 +3,136 @@ using System.Linq;
using System.Threading.Tasks;
using API.DTOs.CollectionTags;
using API.Entities;
using API.Interfaces.Repositories;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories
namespace API.Data.Repositories;
public interface ICollectionTagRepository
{
public class CollectionTagRepository : ICollectionTagRepository
void Add(CollectionTag tag);
void Remove(CollectionTag tag);
Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync();
Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery);
Task<string> GetCoverImageAsync(int collectionTagId);
Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync();
Task<CollectionTag> GetTagAsync(int tagId);
Task<CollectionTag> GetFullTagAsync(int tagId);
void Update(CollectionTag tag);
Task<int> RemoveTagsWithoutSeries();
Task<IEnumerable<CollectionTag>> GetAllTagsAsync();
Task<IList<string>> GetAllCoverImagesAsync();
}
public class CollectionTagRepository : ICollectionTagRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public CollectionTagRepository(DataContext context, IMapper mapper)
{
private readonly DataContext _context;
private readonly IMapper _mapper;
_context = context;
_mapper = mapper;
}
public CollectionTagRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public void Add(CollectionTag tag)
{
_context.CollectionTag.Add(tag);
}
public void Add(CollectionTag tag)
{
_context.CollectionTag.Add(tag);
}
public void Remove(CollectionTag tag)
{
_context.CollectionTag.Remove(tag);
}
public void Remove(CollectionTag tag)
{
_context.CollectionTag.Remove(tag);
}
public void Update(CollectionTag tag)
{
_context.Entry(tag).State = EntityState.Modified;
}
public void Update(CollectionTag tag)
{
_context.Entry(tag).State = EntityState.Modified;
}
/// <summary>
/// Removes any collection tags without any series
/// </summary>
public async Task<int> RemoveTagsWithoutSeries()
{
var tagsToDelete = await _context.CollectionTag
.Include(c => c.SeriesMetadatas)
.Where(c => c.SeriesMetadatas.Count == 0)
.ToListAsync();
_context.RemoveRange(tagsToDelete);
/// <summary>
/// Removes any collection tags without any series
/// </summary>
public async Task<int> RemoveTagsWithoutSeries()
{
var tagsToDelete = await _context.CollectionTag
.Include(c => c.SeriesMetadatas)
.Where(c => c.SeriesMetadatas.Count == 0)
.ToListAsync();
_context.RemoveRange(tagsToDelete);
return await _context.SaveChangesAsync();
}
return await _context.SaveChangesAsync();
}
public async Task<IEnumerable<CollectionTag>> GetAllTagsAsync()
{
return await _context.CollectionTag
.OrderBy(c => c.NormalizedTitle)
.ToListAsync();
}
public async Task<IEnumerable<CollectionTag>> GetAllTagsAsync()
{
return await _context.CollectionTag
.OrderBy(c => c.NormalizedTitle)
.ToListAsync();
}
public async Task<IList<string>> GetAllCoverImagesAsync()
{
return await _context.CollectionTag
.Select(t => t.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.AsNoTracking()
.ToListAsync();
}
public async Task<IList<string>> GetAllCoverImagesAsync()
{
return await _context.CollectionTag
.Select(t => t.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.AsNoTracking()
.ToListAsync();
}
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
{
return await _context.CollectionTag
.Select(c => c)
.OrderBy(c => c.NormalizedTitle)
.AsNoTracking()
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
{
return await _context.CollectionTag
.Select(c => c)
.OrderBy(c => c.NormalizedTitle)
.AsNoTracking()
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync()
{
return await _context.CollectionTag
.Where(c => c.Promoted)
.OrderBy(c => c.NormalizedTitle)
.AsNoTracking()
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync()
{
return await _context.CollectionTag
.Where(c => c.Promoted)
.OrderBy(c => c.NormalizedTitle)
.AsNoTracking()
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<CollectionTag> GetTagAsync(int tagId)
{
return await _context.CollectionTag
.Where(c => c.Id == tagId)
.SingleOrDefaultAsync();
}
public async Task<CollectionTag> GetTagAsync(int tagId)
{
return await _context.CollectionTag
.Where(c => c.Id == tagId)
.SingleOrDefaultAsync();
}
public async Task<CollectionTag> GetFullTagAsync(int tagId)
{
return await _context.CollectionTag
.Where(c => c.Id == tagId)
.Include(c => c.SeriesMetadatas)
.SingleOrDefaultAsync();
}
public async Task<CollectionTag> GetFullTagAsync(int tagId)
{
return await _context.CollectionTag
.Where(c => c.Id == tagId)
.Include(c => c.SeriesMetadatas)
.SingleOrDefaultAsync();
}
public async Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery)
{
return await _context.CollectionTag
.Where(s => EF.Functions.Like(s.Title, $"%{searchQuery}%")
|| EF.Functions.Like(s.NormalizedTitle, $"%{searchQuery}%"))
.OrderBy(s => s.Title)
.AsNoTracking()
.OrderBy(c => c.NormalizedTitle)
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery)
{
return await _context.CollectionTag
.Where(s => EF.Functions.Like(s.Title, $"%{searchQuery}%")
|| EF.Functions.Like(s.NormalizedTitle, $"%{searchQuery}%"))
.OrderBy(s => s.Title)
.AsNoTracking()
.OrderBy(c => c.NormalizedTitle)
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<string> GetCoverImageAsync(int collectionTagId)
{
return await _context.CollectionTag
.Where(c => c.Id == collectionTagId)
.Select(c => c.CoverImage)
.AsNoTracking()
.SingleOrDefaultAsync();
}
public async Task<string> GetCoverImageAsync(int collectionTagId)
{
return await _context.CollectionTag
.Where(c => c.Id == collectionTagId)
.Select(c => c.CoverImage)
.AsNoTracking()
.SingleOrDefaultAsync();
}
}

View file

@ -2,12 +2,20 @@
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Interfaces.Repositories;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
public interface IGenreRepository
{
void Attach(Genre genre);
void Remove(Genre genre);
Task<Genre> FindByNameAsync(string genreName);
Task<IList<Genre>> GetAllGenres();
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
}
public class GenreRepository : IGenreRepository
{
private readonly DataContext _context;
@ -51,6 +59,6 @@ public class GenreRepository : IGenreRepository
public async Task<IList<Genre>> GetAllGenres()
{
return await _context.Genre.ToListAsync();;
return await _context.Genre.ToListAsync();
}
}

View file

@ -5,194 +5,208 @@ using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
using API.Entities.Enums;
using API.Interfaces.Repositories;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories
namespace API.Data.Repositories;
[Flags]
public enum LibraryIncludes
{
[Flags]
public enum LibraryIncludes
{
None = 1,
Series = 2,
AppUser = 4,
Folders = 8,
// Ratings = 16
}
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<IEnumerable<LibraryDto>> 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<LibraryDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.AsSingleQuery()
.ToListAsync();
}
public async Task<IEnumerable<Library>> GetLibrariesAsync()
{
return await _context.Library
.Include(l => l.AppUsers)
.ToListAsync();
}
public async Task<bool> DeleteLibrary(int libraryId)
{
var library = await GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders | LibraryIncludes.Series);
_context.Library.Remove(library);
return await _context.SaveChangesAsync() > 0;
}
public async Task<IEnumerable<Library>> 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<LibraryType> GetLibraryTypeAsync(int libraryId)
{
return await _context.Library
.Where(l => l.Id == libraryId)
.AsNoTracking()
.Select(l => l.Type)
.SingleAsync();
}
public async Task<IEnumerable<LibraryDto>> GetLibraryDtosAsync()
{
return await _context.Library
.Include(f => f.Folders)
.OrderBy(l => l.Name)
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.ToListAsync();
}
public async Task<Library> 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<Library> AddIncludesToQuery(IQueryable<Library> 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;
}
/// <summary>
/// This returns a Library with all it's Series -> Volumes -> Chapters. This is expensive. Should only be called when needed.
/// </summary>
/// <param name="libraryId"></param>
/// <returns></returns>
public async Task<Library> 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();
}
/// <summary>
/// This is a heavy call, pulls all entities for a Library, except this version only grabs for one series id
/// </summary>
/// <param name="libraryId"></param>
/// <param name="seriesId"></param>
/// <returns></returns>
public async Task<Library> 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<bool> LibraryExists(string libraryName)
{
return await _context.Library
.AsNoTracking()
.AnyAsync(x => x.Name == libraryName);
}
public async Task<IEnumerable<LibraryDto>> GetLibrariesForUserAsync(AppUser user)
{
return await _context.Library
.Where(library => library.AppUsers.Contains(user))
.Include(l => l.Folders)
.AsNoTracking()
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
}
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<IEnumerable<LibraryDto>> GetLibraryDtosAsync();
Task<bool> LibraryExists(string libraryName);
Task<Library> GetLibraryForIdAsync(int libraryId, LibraryIncludes includes);
Task<Library> GetFullLibraryForIdAsync(int libraryId);
Task<Library> GetFullLibraryForIdAsync(int libraryId, int seriesId);
Task<IEnumerable<LibraryDto>> GetLibraryDtosForUsernameAsync(string userName);
Task<IEnumerable<Library>> GetLibrariesAsync();
Task<bool> DeleteLibrary(int libraryId);
Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId);
Task<LibraryType> GetLibraryTypeAsync(int libraryId);
}
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<IEnumerable<LibraryDto>> 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<LibraryDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.AsSingleQuery()
.ToListAsync();
}
public async Task<IEnumerable<Library>> GetLibrariesAsync()
{
return await _context.Library
.Include(l => l.AppUsers)
.ToListAsync();
}
public async Task<bool> DeleteLibrary(int libraryId)
{
var library = await GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders | LibraryIncludes.Series);
_context.Library.Remove(library);
return await _context.SaveChangesAsync() > 0;
}
public async Task<IEnumerable<Library>> 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<LibraryType> GetLibraryTypeAsync(int libraryId)
{
return await _context.Library
.Where(l => l.Id == libraryId)
.AsNoTracking()
.Select(l => l.Type)
.SingleAsync();
}
public async Task<IEnumerable<LibraryDto>> GetLibraryDtosAsync()
{
return await _context.Library
.Include(f => f.Folders)
.OrderBy(l => l.Name)
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.ToListAsync();
}
public async Task<Library> 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<Library> AddIncludesToQuery(IQueryable<Library> 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;
}
/// <summary>
/// This returns a Library with all it's Series -> Volumes -> Chapters. This is expensive. Should only be called when needed.
/// </summary>
/// <param name="libraryId"></param>
/// <returns></returns>
public async Task<Library> 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();
}
/// <summary>
/// This is a heavy call, pulls all entities for a Library, except this version only grabs for one series id
/// </summary>
/// <param name="libraryId"></param>
/// <param name="seriesId"></param>
/// <returns></returns>
public async Task<Library> 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<bool> LibraryExists(string libraryName)
{
return await _context.Library
.AsNoTracking()
.AnyAsync(x => x.Name == libraryName);
}
public async Task<IEnumerable<LibraryDto>> GetLibrariesForUserAsync(AppUser user)
{
return await _context.Library
.Where(library => library.AppUsers.Contains(user))
.Include(l => l.Folders)
.AsNoTracking()
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
}

View file

@ -2,59 +2,65 @@
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Interfaces.Repositories;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories
namespace API.Data.Repositories;
public interface IPersonRepository
{
public class PersonRepository : IPersonRepository
void Attach(Person person);
void Remove(Person person);
Task<IList<Person>> GetAllPeople();
Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false);
}
public class PersonRepository : IPersonRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public PersonRepository(DataContext context, IMapper mapper)
{
private readonly DataContext _context;
private readonly IMapper _mapper;
_context = context;
_mapper = mapper;
}
public PersonRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public void Attach(Person person)
{
_context.Person.Attach(person);
}
public void Attach(Person person)
{
_context.Person.Attach(person);
}
public void Remove(Person person)
{
_context.Person.Remove(person);
}
public void Remove(Person person)
{
_context.Person.Remove(person);
}
public async Task<Person> FindByNameAsync(string name)
{
var normalizedName = Parser.Parser.Normalize(name);
return await _context.Person
.Where(p => normalizedName.Equals(p.NormalizedName))
.SingleOrDefaultAsync();
}
public async Task<Person> FindByNameAsync(string name)
{
var normalizedName = Parser.Parser.Normalize(name);
return await _context.Person
.Where(p => normalizedName.Equals(p.NormalizedName))
.SingleOrDefaultAsync();
}
public async Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false)
{
var peopleWithNoConnections = await _context.Person
.Include(p => p.SeriesMetadatas)
.Include(p => p.ChapterMetadatas)
.Where(p => p.SeriesMetadatas.Count == 0 && p.ChapterMetadatas.Count == 0)
.ToListAsync();
public async Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false)
{
var peopleWithNoConnections = await _context.Person
.Include(p => p.SeriesMetadatas)
.Include(p => p.ChapterMetadatas)
.Where(p => p.SeriesMetadatas.Count == 0 && p.ChapterMetadatas.Count == 0)
.ToListAsync();
_context.Person.RemoveRange(peopleWithNoConnections);
_context.Person.RemoveRange(peopleWithNoConnections);
await _context.SaveChangesAsync();
}
await _context.SaveChangesAsync();
}
public async Task<IList<Person>> GetAllPeople()
{
return await _context.Person
.ToListAsync();
}
public async Task<IList<Person>> GetAllPeople()
{
return await _context.Person
.ToListAsync();
}
}

View file

@ -4,175 +4,187 @@ using System.Threading.Tasks;
using API.DTOs.ReadingLists;
using API.Entities;
using API.Helpers;
using API.Interfaces.Repositories;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories
namespace API.Data.Repositories;
public interface IReadingListRepository
{
public class ReadingListRepository : IReadingListRepository
Task<PagedList<ReadingListDto>> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams);
Task<ReadingList> GetReadingListByIdAsync(int readingListId);
Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId);
Task<ReadingListDto> GetReadingListDtoByIdAsync(int readingListId, int userId);
Task<IEnumerable<ReadingListItemDto>> AddReadingProgressModifiers(int userId, IList<ReadingListItemDto> items);
Task<ReadingListDto> GetReadingListDtoByTitleAsync(string title);
Task<IEnumerable<ReadingListItem>> GetReadingListItemsByIdAsync(int readingListId);
void Remove(ReadingListItem item);
void BulkRemove(IEnumerable<ReadingListItem> items);
void Update(ReadingList list);
}
public class ReadingListRepository : IReadingListRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public ReadingListRepository(DataContext context, IMapper mapper)
{
private readonly DataContext _context;
private readonly IMapper _mapper;
_context = context;
_mapper = mapper;
}
public ReadingListRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public void Update(ReadingList list)
{
_context.Entry(list).State = EntityState.Modified;
}
public void Update(ReadingList list)
{
_context.Entry(list).State = EntityState.Modified;
}
public void Remove(ReadingListItem item)
{
_context.ReadingListItem.Remove(item);
}
public void Remove(ReadingListItem item)
{
_context.ReadingListItem.Remove(item);
}
public void BulkRemove(IEnumerable<ReadingListItem> items)
{
_context.ReadingListItem.RemoveRange(items);
}
public void BulkRemove(IEnumerable<ReadingListItem> items)
{
_context.ReadingListItem.RemoveRange(items);
}
public async Task<PagedList<ReadingListDto>> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams)
{
var query = _context.ReadingList
.Where(l => l.AppUserId == userId || (includePromoted && l.Promoted ))
.OrderBy(l => l.LastModified)
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
.AsNoTracking();
public async Task<PagedList<ReadingListDto>> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams)
{
var query = _context.ReadingList
.Where(l => l.AppUserId == userId || (includePromoted && l.Promoted ))
.OrderBy(l => l.LastModified)
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
.AsNoTracking();
return await PagedList<ReadingListDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
}
return await PagedList<ReadingListDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
}
public async Task<ReadingList> GetReadingListByIdAsync(int readingListId)
{
return await _context.ReadingList
.Where(r => r.Id == readingListId)
.Include(r => r.Items.OrderBy(item => item.Order))
.SingleOrDefaultAsync();
}
public async Task<ReadingList> GetReadingListByIdAsync(int readingListId)
{
return await _context.ReadingList
.Where(r => r.Id == readingListId)
.Include(r => r.Items.OrderBy(item => item.Order))
.SingleOrDefaultAsync();
}
public async Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId)
{
var userLibraries = _context.Library
.Include(l => l.AppUsers)
.Where(library => library.AppUsers.Any(user => user.Id == userId))
.AsNoTracking()
.Select(library => library.Id)
.ToList();
public async Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId)
{
var userLibraries = _context.Library
.Include(l => l.AppUsers)
.Where(library => library.AppUsers.Any(user => user.Id == userId))
.AsNoTracking()
.Select(library => library.Id)
.ToList();
var items = await _context.ReadingListItem
.Where(s => s.ReadingListId == readingListId)
.Join(_context.Chapter, s => s.ChapterId, chapter => chapter.Id, (data, chapter) => new
{
TotalPages = chapter.Pages,
ChapterNumber = chapter.Range,
readingListItem = data
})
.Join(_context.Volume, s => s.readingListItem.VolumeId, volume => volume.Id, (data, volume) => new
var items = await _context.ReadingListItem
.Where(s => s.ReadingListId == readingListId)
.Join(_context.Chapter, s => s.ChapterId, chapter => chapter.Id, (data, chapter) => new
{
TotalPages = chapter.Pages,
ChapterNumber = chapter.Range,
readingListItem = data
})
.Join(_context.Volume, s => s.readingListItem.VolumeId, volume => volume.Id, (data, volume) => new
{
data.readingListItem,
data.TotalPages,
data.ChapterNumber,
VolumeId = volume.Id,
VolumeNumber = volume.Name,
})
.Join(_context.Series, s => s.readingListItem.SeriesId, series => series.Id,
(data, s) => new
{
SeriesName = s.Name,
SeriesFormat = s.Format,
s.LibraryId,
data.readingListItem,
data.TotalPages,
data.ChapterNumber,
VolumeId = volume.Id,
VolumeNumber = volume.Name,
data.VolumeNumber,
data.VolumeId
})
.Join(_context.Series, s => s.readingListItem.SeriesId, series => series.Id,
(data, s) => new
{
SeriesName = s.Name,
SeriesFormat = s.Format,
s.LibraryId,
data.readingListItem,
data.TotalPages,
data.ChapterNumber,
data.VolumeNumber,
data.VolumeId
})
.Select(data => new ReadingListItemDto()
{
Id = data.readingListItem.Id,
ChapterId = data.readingListItem.ChapterId,
Order = data.readingListItem.Order,
SeriesId = data.readingListItem.SeriesId,
SeriesName = data.SeriesName,
SeriesFormat = data.SeriesFormat,
PagesTotal = data.TotalPages,
ChapterNumber = data.ChapterNumber,
VolumeNumber = data.VolumeNumber,
LibraryId = data.LibraryId,
VolumeId = data.VolumeId,
ReadingListId = data.readingListItem.ReadingListId
})
.Where(o => userLibraries.Contains(o.LibraryId))
.OrderBy(rli => rli.Order)
.AsNoTracking()
.ToListAsync();
// Attach progress information
var fetchedChapterIds = items.Select(i => i.ChapterId);
var progresses = await _context.AppUserProgresses
.Where(p => fetchedChapterIds.Contains(p.ChapterId))
.AsNoTracking()
.ToListAsync();
foreach (var progress in progresses)
.Select(data => new ReadingListItemDto()
{
var progressItem = items.SingleOrDefault(i => i.ChapterId == progress.ChapterId && i.ReadingListId == readingListId);
if (progressItem == null) continue;
Id = data.readingListItem.Id,
ChapterId = data.readingListItem.ChapterId,
Order = data.readingListItem.Order,
SeriesId = data.readingListItem.SeriesId,
SeriesName = data.SeriesName,
SeriesFormat = data.SeriesFormat,
PagesTotal = data.TotalPages,
ChapterNumber = data.ChapterNumber,
VolumeNumber = data.VolumeNumber,
LibraryId = data.LibraryId,
VolumeId = data.VolumeId,
ReadingListId = data.readingListItem.ReadingListId
})
.Where(o => userLibraries.Contains(o.LibraryId))
.OrderBy(rli => rli.Order)
.AsNoTracking()
.ToListAsync();
progressItem.PagesRead = progress.PagesRead;
}
// Attach progress information
var fetchedChapterIds = items.Select(i => i.ChapterId);
var progresses = await _context.AppUserProgresses
.Where(p => fetchedChapterIds.Contains(p.ChapterId))
.AsNoTracking()
.ToListAsync();
return items;
}
public async Task<ReadingListDto> GetReadingListDtoByIdAsync(int readingListId, int userId)
foreach (var progress in progresses)
{
return await _context.ReadingList
.Where(r => r.Id == readingListId && (r.AppUserId == userId || r.Promoted))
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
.SingleOrDefaultAsync();
var progressItem = items.SingleOrDefault(i => i.ChapterId == progress.ChapterId && i.ReadingListId == readingListId);
if (progressItem == null) continue;
progressItem.PagesRead = progress.PagesRead;
}
public async Task<IEnumerable<ReadingListItemDto>> AddReadingProgressModifiers(int userId, IList<ReadingListItemDto> items)
{
var chapterIds = items.Select(i => i.ChapterId).Distinct().ToList();
var userProgress = await _context.AppUserProgresses
.Where(p => p.AppUserId == userId && chapterIds.Contains(p.ChapterId))
.AsNoTracking()
.ToListAsync();
foreach (var item in items)
{
var progress = userProgress.Where(p => p.ChapterId == item.ChapterId);
item.PagesRead = progress.Sum(p => p.PagesRead);
}
return items;
}
public async Task<ReadingListDto> GetReadingListDtoByTitleAsync(string title)
{
return await _context.ReadingList
.Where(r => r.Title.Equals(title))
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
.SingleOrDefaultAsync();
}
public async Task<IEnumerable<ReadingListItem>> GetReadingListItemsByIdAsync(int readingListId)
{
return await _context.ReadingListItem
.Where(r => r.ReadingListId == readingListId)
.OrderBy(r => r.Order)
.ToListAsync();
}
return items;
}
public async Task<ReadingListDto> GetReadingListDtoByIdAsync(int readingListId, int userId)
{
return await _context.ReadingList
.Where(r => r.Id == readingListId && (r.AppUserId == userId || r.Promoted))
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
.SingleOrDefaultAsync();
}
public async Task<IEnumerable<ReadingListItemDto>> AddReadingProgressModifiers(int userId, IList<ReadingListItemDto> items)
{
var chapterIds = items.Select(i => i.ChapterId).Distinct().ToList();
var userProgress = await _context.AppUserProgresses
.Where(p => p.AppUserId == userId && chapterIds.Contains(p.ChapterId))
.AsNoTracking()
.ToListAsync();
foreach (var item in items)
{
var progress = userProgress.Where(p => p.ChapterId == item.ChapterId);
item.PagesRead = progress.Sum(p => p.PagesRead);
}
return items;
}
public async Task<ReadingListDto> GetReadingListDtoByTitleAsync(string title)
{
return await _context.ReadingList
.Where(r => r.Title.Equals(title))
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
.SingleOrDefaultAsync();
}
public async Task<IEnumerable<ReadingListItem>> GetReadingListItemsByIdAsync(int readingListId)
{
return await _context.ReadingListItem
.Where(r => r.ReadingListId == readingListId)
.OrderBy(r => r.Order)
.ToListAsync();
}
}

View file

@ -1,21 +1,23 @@
using API.Entities;
using API.Entities.Metadata;
using API.Interfaces.Repositories;
using API.Entities.Metadata;
namespace API.Data.Repositories
namespace API.Data.Repositories;
public interface ISeriesMetadataRepository
{
public class SeriesMetadataRepository : ISeriesMetadataRepository
void Update(SeriesMetadata seriesMetadata);
}
public class SeriesMetadataRepository : ISeriesMetadataRepository
{
private readonly DataContext _context;
public SeriesMetadataRepository(DataContext context)
{
private readonly DataContext _context;
_context = context;
}
public SeriesMetadataRepository(DataContext context)
{
_context = context;
}
public void Update(SeriesMetadata seriesMetadata)
{
_context.SeriesMetadata.Update(seriesMetadata);
}
public void Update(SeriesMetadata seriesMetadata)
{
_context.SeriesMetadata.Update(seriesMetadata);
}
}

File diff suppressed because it is too large Load diff

View file

@ -4,45 +4,50 @@ using System.Threading.Tasks;
using API.DTOs.Settings;
using API.Entities;
using API.Entities.Enums;
using API.Interfaces.Repositories;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories
namespace API.Data.Repositories;
public interface ISettingsRepository
{
public class SettingsRepository : ISettingsRepository
void Update(ServerSetting settings);
Task<ServerSettingDto> GetSettingsDtoAsync();
Task<ServerSetting> GetSettingAsync(ServerSettingKey key);
Task<IEnumerable<ServerSetting>> GetSettingsAsync();
}
public class SettingsRepository : ISettingsRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public SettingsRepository(DataContext context, IMapper mapper)
{
private readonly DataContext _context;
private readonly IMapper _mapper;
_context = context;
_mapper = mapper;
}
public SettingsRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public void Update(ServerSetting settings)
{
_context.Entry(settings).State = EntityState.Modified;
}
public void Update(ServerSetting settings)
{
_context.Entry(settings).State = EntityState.Modified;
}
public async Task<ServerSettingDto> GetSettingsDtoAsync()
{
var settings = await _context.ServerSetting
.Select(x => x)
.AsNoTracking()
.ToListAsync();
return _mapper.Map<ServerSettingDto>(settings);
}
public async Task<ServerSettingDto> GetSettingsDtoAsync()
{
var settings = await _context.ServerSetting
.Select(x => x)
.AsNoTracking()
.ToListAsync();
return _mapper.Map<ServerSettingDto>(settings);
}
public Task<ServerSetting> GetSettingAsync(ServerSettingKey key)
{
return _context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key);
}
public Task<ServerSetting> GetSettingAsync(ServerSettingKey key)
{
return _context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key);
}
public async Task<IEnumerable<ServerSetting>> GetSettingsAsync()
{
return await _context.ServerSetting.ToListAsync();
}
public async Task<IEnumerable<ServerSetting>> GetSettingsAsync()
{
return await _context.ServerSetting.ToListAsync();
}
}

View file

@ -6,254 +6,276 @@ using API.Constants;
using API.DTOs;
using API.DTOs.Reader;
using API.Entities;
using API.Interfaces.Repositories;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories
namespace API.Data.Repositories;
[Flags]
public enum AppUserIncludes
{
[Flags]
public enum AppUserIncludes
None = 1,
Progress = 2,
Bookmarks = 4,
ReadingLists = 8,
Ratings = 16
}
public interface IUserRepository
{
void Update(AppUser user);
void Update(AppUserPreferences preferences);
void Update(AppUserBookmark bookmark);
public void Delete(AppUser user);
Task<IEnumerable<MemberDto>> GetMembersAsync();
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
Task<IEnumerable<AppUser>> GetNonAdminUsersAsync();
Task<bool> IsUserAdmin(AppUser user);
Task<AppUserRating> GetUserRating(int seriesId, int userId);
Task<AppUserPreferences> GetPreferencesAsync(string username);
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId);
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForChapter(int userId, int chapterId);
Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId);
Task<AppUserBookmark> GetBookmarkForPage(int page, int chapterId, int userId);
Task<int> GetUserIdByApiKeyAsync(string apiKey);
Task<AppUser> GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None);
Task<AppUser> GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None);
Task<int> GetUserIdByUsernameAsync(string username);
Task<AppUser> GetUserWithReadingListsByUsernameAsync(string username);
}
public class UserRepository : IUserRepository
{
private readonly DataContext _context;
private readonly UserManager<AppUser> _userManager;
private readonly IMapper _mapper;
public UserRepository(DataContext context, UserManager<AppUser> userManager, IMapper mapper)
{
None = 1,
Progress = 2,
Bookmarks = 4,
ReadingLists = 8,
Ratings = 16
_context = context;
_userManager = userManager;
_mapper = mapper;
}
public class UserRepository : IUserRepository
public void Update(AppUser user)
{
private readonly DataContext _context;
private readonly UserManager<AppUser> _userManager;
private readonly IMapper _mapper;
_context.Entry(user).State = EntityState.Modified;
}
public UserRepository(DataContext context, UserManager<AppUser> userManager, IMapper mapper)
public void Update(AppUserPreferences preferences)
{
_context.Entry(preferences).State = EntityState.Modified;
}
public void Update(AppUserBookmark bookmark)
{
_context.Entry(bookmark).State = EntityState.Modified;
}
public void Delete(AppUser user)
{
_context.AppUser.Remove(user);
}
/// <summary>
/// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags.
/// </summary>
/// <param name="username"></param>
/// <param name="includeFlags">Includes() you want. Pass multiple with flag1 | flag2 </param>
/// <returns></returns>
public async Task<AppUser> GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None)
{
var query = _context.Users
.Where(x => x.UserName == username);
query = AddIncludesToQuery(query, includeFlags);
return await query.SingleOrDefaultAsync();
}
/// <summary>
/// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags.
/// </summary>
/// <param name="userId"></param>
/// <param name="includeFlags">Includes() you want. Pass multiple with flag1 | flag2 </param>
/// <returns></returns>
public async Task<AppUser> GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None)
{
var query = _context.Users
.Where(x => x.Id == userId);
query = AddIncludesToQuery(query, includeFlags);
return await query.SingleOrDefaultAsync();
}
public async Task<AppUserBookmark> GetBookmarkForPage(int page, int chapterId, int userId)
{
return await _context.AppUserBookmark
.Where(b => b.Page == page && b.ChapterId == chapterId && b.AppUserId == userId)
.SingleOrDefaultAsync();
}
private static IQueryable<AppUser> AddIncludesToQuery(IQueryable<AppUser> query, AppUserIncludes includeFlags)
{
if (includeFlags.HasFlag(AppUserIncludes.Bookmarks))
{
_context = context;
_userManager = userManager;
_mapper = mapper;
query = query.Include(u => u.Bookmarks);
}
public void Update(AppUser user)
if (includeFlags.HasFlag(AppUserIncludes.Progress))
{
_context.Entry(user).State = EntityState.Modified;
query = query.Include(u => u.Progresses);
}
public void Update(AppUserPreferences preferences)
if (includeFlags.HasFlag(AppUserIncludes.ReadingLists))
{
_context.Entry(preferences).State = EntityState.Modified;
query = query.Include(u => u.ReadingLists);
}
public void Update(AppUserBookmark bookmark)
if (includeFlags.HasFlag(AppUserIncludes.Ratings))
{
_context.Entry(bookmark).State = EntityState.Modified;
query = query.Include(u => u.Ratings);
}
public void Delete(AppUser user)
{
_context.AppUser.Remove(user);
}
return query;
}
/// <summary>
/// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags.
/// </summary>
/// <param name="username"></param>
/// <param name="includeFlags">Includes() you want. Pass multiple with flag1 | flag2 </param>
/// <returns></returns>
public async Task<AppUser> GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None)
{
var query = _context.Users
.Where(x => x.UserName == username);
query = AddIncludesToQuery(query, includeFlags);
/// <summary>
/// This fetches the Id for a user. Use whenever you just need an ID.
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
public async Task<int> GetUserIdByUsernameAsync(string username)
{
return await _context.Users
.Where(x => x.UserName == username)
.Select(u => u.Id)
.SingleOrDefaultAsync();
}
return await query.SingleOrDefaultAsync();
}
/// <summary>
/// Gets an AppUser by username. Returns back Reading List and their Items.
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
public async Task<AppUser> GetUserWithReadingListsByUsernameAsync(string username)
{
return await _context.Users
.Include(u => u.ReadingLists)
.ThenInclude(l => l.Items)
.SingleOrDefaultAsync(x => x.UserName == username);
}
/// <summary>
/// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags.
/// </summary>
/// <param name="userId"></param>
/// <param name="includeFlags">Includes() you want. Pass multiple with flag1 | flag2 </param>
/// <returns></returns>
public async Task<AppUser> GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None)
{
var query = _context.Users
.Where(x => x.Id == userId);
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
{
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
}
query = AddIncludesToQuery(query, includeFlags);
public async Task<IEnumerable<AppUser>> GetNonAdminUsersAsync()
{
return await _userManager.GetUsersInRoleAsync(PolicyConstants.PlebRole);
}
return await query.SingleOrDefaultAsync();
}
public async Task<bool> IsUserAdmin(AppUser user)
{
return await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
}
public async Task<AppUserBookmark> GetBookmarkForPage(int page, int chapterId, int userId)
{
return await _context.AppUserBookmark
.Where(b => b.Page == page && b.ChapterId == chapterId && b.AppUserId == userId)
.SingleOrDefaultAsync();
}
public async Task<AppUserRating> GetUserRating(int seriesId, int userId)
{
return await _context.AppUserRating.Where(r => r.SeriesId == seriesId && r.AppUserId == userId)
.SingleOrDefaultAsync();
}
private static IQueryable<AppUser> AddIncludesToQuery(IQueryable<AppUser> query, AppUserIncludes includeFlags)
{
if (includeFlags.HasFlag(AppUserIncludes.Bookmarks))
public async Task<AppUserPreferences> GetPreferencesAsync(string username)
{
return await _context.AppUserPreferences
.Include(p => p.AppUser)
.SingleOrDefaultAsync(p => p.AppUser.UserName == username);
}
public async Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId)
{
return await _context.AppUserBookmark
.Where(x => x.AppUserId == userId && x.SeriesId == seriesId)
.OrderBy(x => x.Page)
.AsNoTracking()
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId)
{
return await _context.AppUserBookmark
.Where(x => x.AppUserId == userId && x.VolumeId == volumeId)
.OrderBy(x => x.Page)
.AsNoTracking()
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForChapter(int userId, int chapterId)
{
return await _context.AppUserBookmark
.Where(x => x.AppUserId == userId && x.ChapterId == chapterId)
.OrderBy(x => x.Page)
.AsNoTracking()
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId)
{
return await _context.AppUserBookmark
.Where(x => x.AppUserId == userId)
.OrderBy(x => x.Page)
.AsNoTracking()
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
/// <summary>
/// Fetches the UserId by API Key. This does not include any extra information
/// </summary>
/// <param name="apiKey"></param>
/// <returns></returns>
public async Task<int> GetUserIdByApiKeyAsync(string apiKey)
{
return await _context.AppUser
.Where(u => u.ApiKey.Equals(apiKey))
.Select(u => u.Id)
.SingleOrDefaultAsync();
}
public async Task<IEnumerable<MemberDto>> GetMembersAsync()
{
return await _context.Users
.Include(x => x.Libraries)
.Include(r => r.UserRoles)
.ThenInclude(r => r.Role)
.OrderBy(u => u.UserName)
.Select(u => new MemberDto
{
query = query.Include(u => u.Bookmarks);
}
if (includeFlags.HasFlag(AppUserIncludes.Progress))
{
query = query.Include(u => u.Progresses);
}
if (includeFlags.HasFlag(AppUserIncludes.ReadingLists))
{
query = query.Include(u => u.ReadingLists);
}
if (includeFlags.HasFlag(AppUserIncludes.Ratings))
{
query = query.Include(u => u.Ratings);
}
return query;
}
/// <summary>
/// This fetches the Id for a user. Use whenever you just need an ID.
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
public async Task<int> GetUserIdByUsernameAsync(string username)
{
return await _context.Users
.Where(x => x.UserName == username)
.Select(u => u.Id)
.SingleOrDefaultAsync();
}
/// <summary>
/// Gets an AppUser by username. Returns back Reading List and their Items.
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
public async Task<AppUser> GetUserWithReadingListsByUsernameAsync(string username)
{
return await _context.Users
.Include(u => u.ReadingLists)
.ThenInclude(l => l.Items)
.SingleOrDefaultAsync(x => x.UserName == username);
}
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
{
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
}
public async Task<IEnumerable<AppUser>> GetNonAdminUsersAsync()
{
return await _userManager.GetUsersInRoleAsync(PolicyConstants.PlebRole);
}
public async Task<bool> IsUserAdmin(AppUser user)
{
return await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
}
public async Task<AppUserRating> GetUserRating(int seriesId, int userId)
{
return await _context.AppUserRating.Where(r => r.SeriesId == seriesId && r.AppUserId == userId)
.SingleOrDefaultAsync();
}
public async Task<AppUserPreferences> GetPreferencesAsync(string username)
{
return await _context.AppUserPreferences
.Include(p => p.AppUser)
.SingleOrDefaultAsync(p => p.AppUser.UserName == username);
}
public async Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId)
{
return await _context.AppUserBookmark
.Where(x => x.AppUserId == userId && x.SeriesId == seriesId)
.OrderBy(x => x.Page)
.AsNoTracking()
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId)
{
return await _context.AppUserBookmark
.Where(x => x.AppUserId == userId && x.VolumeId == volumeId)
.OrderBy(x => x.Page)
.AsNoTracking()
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForChapter(int userId, int chapterId)
{
return await _context.AppUserBookmark
.Where(x => x.AppUserId == userId && x.ChapterId == chapterId)
.OrderBy(x => x.Page)
.AsNoTracking()
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId)
{
return await _context.AppUserBookmark
.Where(x => x.AppUserId == userId)
.OrderBy(x => x.Page)
.AsNoTracking()
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
/// <summary>
/// Fetches the UserId by API Key. This does not include any extra information
/// </summary>
/// <param name="apiKey"></param>
/// <returns></returns>
public async Task<int> GetUserIdByApiKeyAsync(string apiKey)
{
return await _context.AppUser
.Where(u => u.ApiKey.Equals(apiKey))
.Select(u => u.Id)
.SingleOrDefaultAsync();
}
public async Task<IEnumerable<MemberDto>> GetMembersAsync()
{
return await _context.Users
.Include(x => x.Libraries)
.Include(r => r.UserRoles)
.ThenInclude(r => r.Role)
.OrderBy(u => u.UserName)
.Select(u => new MemberDto
Id = u.Id,
Username = u.UserName,
Created = u.Created,
LastActive = u.LastActive,
Roles = u.UserRoles.Select(r => r.Role.Name).ToList(),
Libraries = u.Libraries.Select(l => new LibraryDto
{
Id = u.Id,
Username = u.UserName,
Created = u.Created,
LastActive = u.LastActive,
Roles = u.UserRoles.Select(r => r.Role.Name).ToList(),
Libraries = u.Libraries.Select(l => new LibraryDto
{
Name = l.Name,
Type = l.Type,
LastScanned = l.LastScanned,
Folders = l.Folders.Select(x => x.Path).ToList()
}).ToList()
})
.AsNoTracking()
.ToListAsync();
}
Name = l.Name,
Type = l.Type,
LastScanned = l.LastScanned,
Folders = l.Folders.Select(x => x.Path).ToList()
}).ToList()
})
.AsNoTracking()
.ToListAsync();
}
}

View file

@ -4,206 +4,220 @@ using System.Threading.Tasks;
using API.Comparators;
using API.DTOs;
using API.Entities;
using API.Interfaces.Repositories;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories
namespace API.Data.Repositories;
public interface IVolumeRepository
{
public class VolumeRepository : IVolumeRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public VolumeRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
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);
}
/// <summary>
/// Returns a list of non-tracked files for a given volume.
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
public async Task<IList<MangaFile>> GetFilesForVolume(int volumeId)
{
return await _context.Chapter
.Where(c => volumeId == c.VolumeId)
.Include(c => c.Files)
.SelectMany(c => c.Files)
.AsNoTracking()
.ToListAsync();
}
/// <summary>
/// Returns the cover image file for the given volume
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
public async Task<string> GetVolumeCoverImageAsync(int volumeId)
{
return await _context.Volume
.Where(v => v.Id == volumeId)
.Select(v => v.CoverImage)
.AsNoTracking()
.SingleOrDefaultAsync();
}
/// <summary>
/// Returns all chapter Ids belonging to a list of Volume Ids
/// </summary>
/// <param name="volumeIds"></param>
/// <returns></returns>
public async Task<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds)
{
return await _context.Chapter
.Where(c => volumeIds.Contains(c.VolumeId))
.Select(c => c.Id)
.ToListAsync();
}
/// <summary>
/// Returns all volumes that contain a seriesId in passed array.
/// </summary>
/// <param name="seriesIds"></param>
/// <returns></returns>
public async Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false)
{
var query = _context.Volume
.Where(v => seriesIds.Contains(v.SeriesId));
if (includeChapters)
{
query = query.Include(v => v.Chapters);
}
return await query.ToListAsync();
}
/// <summary>
/// Returns an individual Volume including Chapters and Files and Reading Progress for a given volumeId
/// </summary>
/// <param name="volumeId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<VolumeDto> GetVolumeDtoAsync(int volumeId, int userId)
{
var volume = await _context.Volume
.Where(vol => vol.Id == volumeId)
.Include(vol => vol.Chapters)
.ThenInclude(c => c.Files)
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
.SingleAsync(vol => vol.Id == volumeId);
var volumeList = new List<VolumeDto>() {volume};
await AddVolumeModifiers(userId, volumeList);
return volumeList[0];
}
/// <summary>
/// Returns the full Volumes including Chapters and Files for a given series
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
public async Task<IEnumerable<Volume>> GetVolumes(int seriesId)
{
return await _context.Volume
.Where(vol => vol.SeriesId == seriesId)
.Include(vol => vol.Chapters)
.ThenInclude(c => c.Files)
.OrderBy(vol => vol.Number)
.ToListAsync();
}
/// <summary>
/// Returns a single volume with Chapter and Files
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
public async Task<Volume> GetVolumeAsync(int volumeId)
{
return await _context.Volume
.Include(vol => vol.Chapters)
.ThenInclude(c => c.Files)
.SingleOrDefaultAsync(vol => vol.Id == volumeId);
}
/// <summary>
/// Returns all volumes for a given series with progress information attached. Includes all Chapters as well.
/// </summary>
/// <param name="seriesId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<IEnumerable<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId)
{
var volumes = await _context.Volume
.Where(vol => vol.SeriesId == seriesId)
.Include(vol => vol.Chapters)
.ThenInclude(c => c.People) // TODO: Measure cost of this
.OrderBy(volume => volume.Number)
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.ToListAsync();
await AddVolumeModifiers(userId, volumes);
SortSpecialChapters(volumes);
return volumes;
}
public async Task<Volume> GetVolumeByIdAsync(int volumeId)
{
return await _context.Volume.SingleOrDefaultAsync(x => x.Id == volumeId);
}
private static void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
{
var sorter = new NaturalSortComparer();
foreach (var v in volumes.Where(vDto => vDto.Number == 0))
{
v.Chapters = v.Chapters.OrderBy(x => x.Range, sorter).ToList();
}
}
private async Task AddVolumeModifiers(int userId, IReadOnlyCollection<VolumeDto> volumes)
{
var volIds = volumes.Select(s => s.Id);
var userProgress = await _context.AppUserProgresses
.Where(p => p.AppUserId == userId && volIds.Contains(p.VolumeId))
.AsNoTracking()
.ToListAsync();
foreach (var v in volumes)
{
foreach (var c in v.Chapters)
{
c.PagesRead = userProgress.Where(p => p.ChapterId == c.Id).Sum(p => p.PagesRead);
}
v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead);
}
}
}
void Add(Volume volume);
void Update(Volume volume);
void Remove(Volume volume);
Task<IList<MangaFile>> GetFilesForVolume(int volumeId);
Task<string> GetVolumeCoverImageAsync(int volumeId);
Task<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds);
Task<IEnumerable<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId);
Task<Volume> GetVolumeAsync(int volumeId);
Task<VolumeDto> GetVolumeDtoAsync(int volumeId, int userId);
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false);
Task<IEnumerable<Volume>> GetVolumes(int seriesId);
Task<Volume> GetVolumeByIdAsync(int volumeId);
}
public class VolumeRepository : IVolumeRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public VolumeRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
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);
}
/// <summary>
/// Returns a list of non-tracked files for a given volume.
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
public async Task<IList<MangaFile>> GetFilesForVolume(int volumeId)
{
return await _context.Chapter
.Where(c => volumeId == c.VolumeId)
.Include(c => c.Files)
.SelectMany(c => c.Files)
.AsNoTracking()
.ToListAsync();
}
/// <summary>
/// Returns the cover image file for the given volume
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
public async Task<string> GetVolumeCoverImageAsync(int volumeId)
{
return await _context.Volume
.Where(v => v.Id == volumeId)
.Select(v => v.CoverImage)
.AsNoTracking()
.SingleOrDefaultAsync();
}
/// <summary>
/// Returns all chapter Ids belonging to a list of Volume Ids
/// </summary>
/// <param name="volumeIds"></param>
/// <returns></returns>
public async Task<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds)
{
return await _context.Chapter
.Where(c => volumeIds.Contains(c.VolumeId))
.Select(c => c.Id)
.ToListAsync();
}
/// <summary>
/// Returns all volumes that contain a seriesId in passed array.
/// </summary>
/// <param name="seriesIds"></param>
/// <param name="includeChapters">Include chapter entities</param>
/// <returns></returns>
public async Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false)
{
var query = _context.Volume
.Where(v => seriesIds.Contains(v.SeriesId));
if (includeChapters)
{
query = query.Include(v => v.Chapters);
}
return await query.ToListAsync();
}
/// <summary>
/// Returns an individual Volume including Chapters and Files and Reading Progress for a given volumeId
/// </summary>
/// <param name="volumeId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<VolumeDto> GetVolumeDtoAsync(int volumeId, int userId)
{
var volume = await _context.Volume
.Where(vol => vol.Id == volumeId)
.Include(vol => vol.Chapters)
.ThenInclude(c => c.Files)
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
.SingleAsync(vol => vol.Id == volumeId);
var volumeList = new List<VolumeDto>() {volume};
await AddVolumeModifiers(userId, volumeList);
return volumeList[0];
}
/// <summary>
/// Returns the full Volumes including Chapters and Files for a given series
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
public async Task<IEnumerable<Volume>> GetVolumes(int seriesId)
{
return await _context.Volume
.Where(vol => vol.SeriesId == seriesId)
.Include(vol => vol.Chapters)
.ThenInclude(c => c.Files)
.OrderBy(vol => vol.Number)
.ToListAsync();
}
/// <summary>
/// Returns a single volume with Chapter and Files
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
public async Task<Volume> GetVolumeAsync(int volumeId)
{
return await _context.Volume
.Include(vol => vol.Chapters)
.ThenInclude(c => c.Files)
.SingleOrDefaultAsync(vol => vol.Id == volumeId);
}
/// <summary>
/// Returns all volumes for a given series with progress information attached. Includes all Chapters as well.
/// </summary>
/// <param name="seriesId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<IEnumerable<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId)
{
var volumes = await _context.Volume
.Where(vol => vol.SeriesId == seriesId)
.Include(vol => vol.Chapters)
.ThenInclude(c => c.People)
.OrderBy(volume => volume.Number)
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.ToListAsync();
await AddVolumeModifiers(userId, volumes);
SortSpecialChapters(volumes);
return volumes;
}
public async Task<Volume> GetVolumeByIdAsync(int volumeId)
{
return await _context.Volume.SingleOrDefaultAsync(x => x.Id == volumeId);
}
private static void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
{
var sorter = new NaturalSortComparer();
foreach (var v in volumes.Where(vDto => vDto.Number == 0))
{
v.Chapters = v.Chapters.OrderBy(x => x.Range, sorter).ToList();
}
}
private async Task AddVolumeModifiers(int userId, IReadOnlyCollection<VolumeDto> volumes)
{
var volIds = volumes.Select(s => s.Id);
var userProgress = await _context.AppUserProgresses
.Where(p => p.AppUserId == userId && volIds.Contains(p.VolumeId))
.AsNoTracking()
.ToListAsync();
foreach (var v in volumes)
{
foreach (var c in v.Chapters)
{
c.PagesRead = userProgress.Where(p => p.ChapterId == c.Id).Sum(p => p.PagesRead);
}
v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead);
}
}
}