Kavita/API/Data/Repositories/ReadingListRepository.cs
Joe Milazzo 7616eb5b0f
UTC Dates + CDisplayEx API Enhancements (#1781)
* Introduced a new claim on the Token to get UserId as well as Username, thus allowing for many places of reduced DB calls. All users will need to reauthenticate.

Introduced UTC Dates throughout the application, they are not exposed in all DTOs, that will come later when we fully switch over. For now, Utc dates will be updated along side timezone specific dates.

Refactored get-progress/progress api to be 50% faster by reducing how much data is loaded from the query.

* Speed up the following apis:
collection/search, download/bookmarks, reader/bookmark-info, recommended/quick-reads, recommended/quick-catchup-reads, recommended/highly-rated, recommended/more-in, recommended/rediscover, want-to-read/

* Added a migration to sync all dates with their new UTC counterpart.

* Added LastReadingProgressUtc onto ChapterDto for some browsing apis, but not all.

Added LastReadingProgressUtc to reading list items.

Refactored the migration to run raw SQL which is much faster.

* Added LastReadingProgressUtc onto ChapterDto for some browsing apis, but not all.

Added LastReadingProgressUtc to reading list items.

Refactored the migration to run raw SQL which is much faster.

* Fixed the unit tests

* Fixed an issue with auto mapper which was causing progress page number to not get sent to UI

* series/volume has chapter last reading progress

* Added filesize and library name on reading list item dto for CDisplayEx.

* Some minor code cleanup

* Forgot to fill a field
2023-02-11 04:01:24 -08:00

278 lines
10 KiB
C#

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.DTOs.ReadingLists;
using API.Entities;
using API.Entities.Enums;
using API.Helpers;
using API.Services;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
public interface 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(int userId, string title);
Task<IEnumerable<ReadingListItem>> GetReadingListItemsByIdAsync(int readingListId);
Task<IEnumerable<ReadingListDto>> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId,
bool includePromoted);
void Remove(ReadingListItem item);
void Add(ReadingList list);
void BulkRemove(IEnumerable<ReadingListItem> items);
void Update(ReadingList list);
Task<int> Count();
Task<string> GetCoverImageAsync(int readingListId);
Task<IList<string>> GetAllCoverImagesAsync();
Task<bool> ReadingListExists(string name);
}
public class ReadingListRepository : IReadingListRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public ReadingListRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public void Update(ReadingList list)
{
_context.Entry(list).State = EntityState.Modified;
}
public void Add(ReadingList list)
{
_context.Add(list);
}
public async Task<int> Count()
{
return await _context.ReadingList.CountAsync();
}
public async Task<string> GetCoverImageAsync(int readingListId)
{
return await _context.ReadingList
.Where(c => c.Id == readingListId)
.Select(c => c.CoverImage)
.AsNoTracking()
.SingleOrDefaultAsync();
}
public async Task<IList<string>> GetAllCoverImagesAsync()
{
return await _context.ReadingList
.Select(t => t.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.AsNoTracking()
.ToListAsync();
}
public async Task<bool> ReadingListExists(string name)
{
var normalized = Services.Tasks.Scanner.Parser.Parser.Normalize(name);
return await _context.ReadingList
.AnyAsync(x => x.NormalizedTitle.Equals(normalized));
}
public void Remove(ReadingListItem item)
{
_context.ReadingListItem.Remove(item);
}
public void BulkRemove(IEnumerable<ReadingListItem> items)
{
_context.ReadingListItem.RemoveRange(items);
}
public async Task<PagedList<ReadingListDto>> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams)
{
var userAgeRating = (await _context.AppUser.SingleAsync(u => u.Id == userId)).AgeRestriction;
var query = _context.ReadingList
.Where(l => l.AppUserId == userId || (includePromoted && l.Promoted ))
.Where(l => l.AgeRating >= userAgeRating)
.OrderBy(l => l.LastModified)
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
.AsNoTracking();
return await PagedList<ReadingListDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
}
public async Task<IEnumerable<ReadingListDto>> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, bool includePromoted)
{
var query = _context.ReadingList
.Where(l => l.AppUserId == userId || (includePromoted && l.Promoted ))
.Where(l => l.Items.Any(i => i.SeriesId == seriesId))
.AsSplitQuery()
.OrderBy(l => l.Title)
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
.AsNoTracking();
return await query.ToListAsync();
}
public async Task<ReadingList> GetReadingListByIdAsync(int readingListId)
{
return await _context.ReadingList
.Where(r => r.Id == readingListId)
.Include(r => r.Items.OrderBy(item => item.Order))
.AsSplitQuery()
.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))
.AsSplitQuery()
.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,
chapter.ReleaseDate,
ReadingListItem = data,
ChapterTitleName = chapter.TitleName,
FileSize = chapter.Files.Sum(f => f.Bytes)
})
.Join(_context.Volume, s => s.ReadingListItem.VolumeId, volume => volume.Id, (data, volume) => new
{
data.ReadingListItem,
data.TotalPages,
data.ChapterNumber,
data.ReleaseDate,
data.ChapterTitleName,
data.FileSize,
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,
data.VolumeNumber,
data.VolumeId,
data.ReleaseDate,
data.ChapterTitleName,
data.FileSize,
LibraryName = _context.Library.Where(l => l.Id == s.LibraryId).Select(l => l.Name).Single(),
LibraryType = _context.Library.Where(l => l.Id == s.LibraryId).Select(l => l.Type).Single()
})
.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,
ReleaseDate = data.ReleaseDate,
LibraryType = data.LibraryType,
ChapterTitleName = data.ChapterTitleName,
LibraryName = data.LibraryName,
FileSize = data.FileSize
})
.Where(o => userLibraries.Contains(o.LibraryId))
.OrderBy(rli => rli.Order)
.AsSplitQuery()
.AsNoTracking()
.ToListAsync();
foreach (var item in items)
{
item.Title = ReadingListService.FormatTitle(item);
}
// 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)
{
var progressItem = items.SingleOrDefault(i => i.ChapterId == progress.ChapterId && i.ReadingListId == readingListId);
if (progressItem == null) continue;
progressItem.PagesRead = progress.PagesRead;
progressItem.LastReadingProgressUtc = progress.LastModifiedUtc;
}
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();
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).ToList();
if (progress.Count == 0) continue;
item.PagesRead = progress.Sum(p => p.PagesRead);
item.LastReadingProgressUtc = progress.Max(p => p.LastModifiedUtc);
}
return items;
}
public async Task<ReadingListDto> GetReadingListDtoByTitleAsync(int userId, string title)
{
return await _context.ReadingList
.Where(r => r.Title.Equals(title) && r.AppUserId == userId)
.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();
}
}