Reading Lists & More (#564)

* Added continous reading to the book reader. Clicking on the max pages to right of progress bar will now go to last page.

* Forgot a file for continous book reading

* Fixed up some code regarding transitioning between chapters. Arrows now show to represent a chapter transition.

* Laid the foundation for reading lists

* All foundation is laid out. Actions are wired in the UI. Backend repository is setup. Redid the migration to have ReadingList track modification so we can order them for the user.

* Updated add modal to have basic skeleton

* Hooked up ability to fetch reading lists from backend

* Made a huge performance improvement to GetChapterIdsForSeriesAsync() by reducing a JOIN and an iteration loop. Improvement went from 2 seconds -> 200 ms.

* Implemented the ability to add all chapters in a series to a reading list.

* Fixed issue with adding new items to reading list not being in a logical order. Lots of work on getting all the information around the reading list view. Added some foreign keys back to chapter so delete should clean up after itself.

* Added ability to open directly the series

* Reading List Items now have progress attached

* Hooked up list deletion and added a case where if doesn't exist on load, then redirect to library.

* Lots of changes. Introduced a dashboard component for the main app. This will sit on libraries route for now and will have 3 tabs to show different sections.

Moved libraries reel down to bottom as people are more likely to access recently added or in progress than explore their whole library.

Note: Bundles are messed up, they need to be reoptimized and routes need to be updated.

* Added pagination to the reading lists api and implemented a page to show all lists

* Cleaned up old code from all-collections component so now it only handles all collections and doesn't have the old code for an individual collection

* Hooked in actions and navigation on reading lists

* When the user re-arranges items, they are now persisted

* Implemented remove read, but performance is pretty poor. Needs to be optimized.

* Lots of API fixes for adding items to a series, returning items, etc. Committing before fixing incorrect fetches of items for a readingListId.

* Rewrote the joins for GetReadingListItemDtosByIdAsync() to not return extra records.

* Remove bug marker now that it is fixed

* Refactor update-by-series to move more of the code to a re-usable function for update-by-volume/chapter APIs

* Implemented the ability to add via series, volume or chapter.

* Added OPDS support for reading lists. This included adding VolumeId to the ReadingListDto.

* Fixed a bug with deleting items

* After we create a library inform user that a scan has started

* Added some extra help information for users on directory picker, since linux users were getting confused.

* Setup for the reading functionality

* Fixed an issue where opening the edit series modal and pressing save without doing anything would empty collection tags. Would happen often when editing cover images.

* Fixed get-next-chapter for reading list. Refactored all methods to use the new GetUserIdByUsernameAsync(), which is much faster and uses less memory.

* Hooked in prev chapter for continuous reading with reading list

* Hooked up the read code for manga reader and book reader to have list id passed

* Manga reader now functions completely with reading lists

* Implemented reading list and incognito mode into book reader

* Refactored some common reading code into reader service

* Added support for "Series -  - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz" format that can occur with FMD2.

* Implemented continuous reading with a reading list between different readers. This incurs a 3x performance hit on the book info api.

* style changes. Don't emit an event if position of draggable item hasn't changed

* Styling and added the edit reading list flow.

* Cleaned up some extra spaces when actionables isn't shown. Lots of cleanup for promoted lists.

* Refactored some filter code to a common service

* Added an RBS check in getting Items for a given user.

* Code smells

* More smells
This commit is contained in:
Joseph Milazzo 2021-09-08 10:03:27 -07:00 committed by GitHub
parent d65e49926a
commit cf7a9aa71e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
117 changed files with 7050 additions and 305 deletions

View file

@ -2,6 +2,8 @@
using System.Linq;
using System.Threading.Tasks;
using API.DTOs;
using API.DTOs.Reader;
using API.Entities.Enums;
using API.Extensions;
using API.Interfaces;
using API.Interfaces.Services;
@ -31,12 +33,31 @@ namespace API.Controllers
}
[HttpGet("{chapterId}/book-info")]
public async Task<ActionResult<string>> GetBookInfo(int chapterId)
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
{
// PERF: Write this in one DB call - This does not meet NFR
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
var volume = await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(chapter.VolumeId);
if (volume == null) return BadRequest("Could not find Volume");
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
if (series == null) return BadRequest("Series could not be found");
return book.Title;
var bookTitle = string.Empty;
if (series.Format == MangaFormat.Epub)
{
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
bookTitle = book.Title;
}
return new BookInfoDto()
{
BookTitle = bookTitle,
VolumeId = chapter.VolumeId,
SeriesFormat = series.Format,
SeriesId = series.Id,
LibraryId = series.LibraryId,
};
}
[HttpGet("{chapterId}/book-resources")]

View file

@ -225,11 +225,11 @@ namespace API.Controllers
[HttpGet("search")]
public async Task<ActionResult<IEnumerable<SearchResultDto>>> Search(string queryString)
{
queryString = queryString.Replace(@"%", "");
queryString = queryString.Trim().Replace(@"%", "");
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
// Get libraries user has access to
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).ToList();
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");

View file

@ -93,6 +93,19 @@ namespace API.Controllers
}
});
feed.Entries.Add(new FeedEntry()
{
Id = "readingList",
Title = "Reading Lists",
Content = new FeedEntryContent()
{
Text = "Browse by Reading Lists"
},
Links = new List<FeedLink>()
{
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/reading-list"),
}
});
feed.Entries.Add(new FeedEntry()
{
Id = "allLibraries",
Title = "All Libraries",
@ -190,6 +203,7 @@ namespace API.Controllers
return CreateXmlResult(SerializeXml(feed));
}
[HttpGet("{apiKey}/collections/{collectionId}")]
[Produces("application/xml")]
public async Task<IActionResult> GetCollection(int collectionId, string apiKey, [FromQuery] int pageNumber = 0)
@ -230,6 +244,76 @@ namespace API.Controllers
}
return CreateXmlResult(SerializeXml(feed));
}
[HttpGet("{apiKey}/reading-list")]
[Produces("application/xml")]
public async Task<IActionResult> GetReadingLists(string apiKey, [FromQuery] int pageNumber = 0)
{
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
var user = await GetUser(apiKey);
var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(user.Id, true, new UserParams()
{
PageNumber = pageNumber
});
var feed = CreateFeed("All Reading Lists", $"{apiKey}/reading-list", apiKey);
foreach (var readingListDto in readingLists)
{
feed.Entries.Add(new FeedEntry()
{
Id = readingListDto.Id.ToString(),
Title = readingListDto.Title,
Summary = readingListDto.Summary,
Links = new List<FeedLink>()
{
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/reading-list/{readingListDto.Id}"),
}
});
}
return CreateXmlResult(SerializeXml(feed));
}
[HttpGet("{apiKey}/reading-list/{readingListId}")]
[Produces("application/xml")]
public async Task<IActionResult> GetReadingListItems(int readingListId, string apiKey)
{
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
var user = await GetUser(apiKey);
var userWithLists = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(user.UserName);
var readingList = userWithLists.ReadingLists.SingleOrDefault(t => t.Id == readingListId);
if (readingList == null)
{
return BadRequest("Reading list does not exist or you don't have access");
}
var feed = CreateFeed(readingList.Title + " Reading List", $"{apiKey}/reading-list/{readingListId}", apiKey);
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, user.Id);
foreach (var item in items)
{
feed.Entries.Add(new FeedEntry()
{
Id = item.ChapterId.ToString(),
Title = "Chapter " + item.ChapterNumber,
Links = new List<FeedLink>()
{
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{item.SeriesId}/volume/{item.VolumeId}/chapter/{item.ChapterId}"),
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={item.ChapterId}")
}
});
}
return CreateXmlResult(SerializeXml(feed));
}

View file

@ -49,7 +49,7 @@ namespace API.Controllers
[HttpGet("image")]
public async Task<ActionResult> GetImage(int chapterId, int page)
{
if (page < 0) return BadRequest("Page cannot be less than 0");
if (page < 0) page = 0;
var chapter = await _cacheService.Ensure(chapterId);
if (chapter == null) return BadRequest("There was an issue finding image file for reading");
@ -76,20 +76,21 @@ namespace API.Controllers
/// <summary>
/// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.
/// </summary>
/// <param name="seriesId"></param>
/// <param name="seriesId">Not used</param>
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("chapter-info")]
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int seriesId, int chapterId)
{
// PERF: Write this in one DB call
// PERF: Write this in one DB call - This does not meet NFR
var chapter = await _cacheService.Ensure(chapterId);
if (chapter == null) return BadRequest("Could not find Chapter");
var volume = await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(chapter.VolumeId);
if (volume == null) return BadRequest("Could not find Volume");
var mangaFile = (await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId)).First();
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
if (series == null) return BadRequest("Series could not be found");
return Ok(new ChapterInfoDto()
{
@ -97,7 +98,10 @@ namespace API.Controllers
VolumeNumber = volume.Number + string.Empty,
VolumeId = volume.Id,
FileName = Path.GetFileName(mangaFile.FilePath),
SeriesName = series?.Name,
SeriesName = series.Name,
SeriesFormat = series.Format,
SeriesId = series.Id,
LibraryId = series.LibraryId,
IsSpecial = chapter.IsSpecial,
Pages = chapter.Pages,
});
@ -526,8 +530,8 @@ namespace API.Controllers
[HttpGet("next-chapter")]
public async Task<ActionResult<int>> GetNextChapter(int seriesId, int volumeId, int currentChapterId)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var volumes = await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, user.Id);
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var volumes = await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, userId);
var currentVolume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
var currentChapter = await _unitOfWork.VolumeRepository.GetChapterAsync(currentChapterId);
if (currentVolume.Number == 0)
@ -592,8 +596,8 @@ namespace API.Controllers
[HttpGet("prev-chapter")]
public async Task<ActionResult<int>> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var volumes = await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, user.Id);
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var volumes = await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, userId);
var currentVolume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
var currentChapter = await _unitOfWork.VolumeRepository.GetChapterAsync(currentChapterId);

View file

@ -0,0 +1,404 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Comparators;
using API.DTOs.ReadingLists;
using API.Entities;
using API.Extensions;
using API.Helpers;
using API.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace API.Controllers
{
public class ReadingListController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
public ReadingListController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetList(int readingListId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, userId));
}
/// <summary>
/// Returns reading lists (paginated) for a given user.
/// </summary>
/// <param name="includePromoted">Defaults to true</param>
/// <returns></returns>
[HttpPost("lists")]
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams, [FromQuery] bool includePromoted = true)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, includePromoted,
userParams);
Response.AddPaginationHeader(items.CurrentPage, items.PageSize, items.TotalCount, items.TotalPages);
return Ok(items);
}
/// <summary>
/// Fetches all reading list items for a given list including rich metadata around series, volume, chapters, and progress
/// </summary>
/// <remarks>This call is expensive</remarks>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpGet("items")]
public async Task<ActionResult<IEnumerable<ReadingListItemDto>>> GetListForUser(int readingListId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
return Ok(await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList()));
}
/// <summary>
/// Updates an items position
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update-position")]
public async Task<ActionResult> UpdateListItemPosition(UpdateReadingListPosition dto)
{
// Make sure UI buffers events
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList();
var item = items.Find(r => r.Id == dto.ReadingListItemId);
items.Remove(item);
items.Insert(dto.ToPosition, item);
for (var i = 0; i < items.Count; i++)
{
items[i].Order = i;
}
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
{
return Ok("Updated");
}
return BadRequest("Couldn't update position");
}
[HttpPost("delete-item")]
public async Task<ActionResult> DeleteListItem(UpdateReadingListPosition dto)
{
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList();
var item = items.Find(r => r.Id == dto.ReadingListItemId);
items.Remove(item);
for (var i = 0; i < items.Count; i++)
{
items[i].Order = i;
}
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
{
return Ok("Updated");
}
return BadRequest("Couldn't delete item");
}
/// <summary>
/// Removes all entries that are fully read from the reading list
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpPost("remove-read")]
public async Task<ActionResult> DeleteReadFromList([FromQuery] int readingListId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
items = await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList());
// Collect all Ids to remove
var itemIdsToRemove = items.Where(item => item.PagesRead == item.PagesTotal).Select(item => item.Id);
try
{
var listItems =
(await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).Where(r =>
itemIdsToRemove.Contains(r.Id));
_unitOfWork.ReadingListRepository.BulkRemove(listItems);
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
else
{
return Ok("Nothing to remove");
}
}
catch
{
await _unitOfWork.RollbackAsync();
}
return BadRequest("Could not remove read items");
}
/// <summary>
/// Deletes a reading list
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpDelete]
public async Task<ActionResult> DeleteList([FromQuery] int readingListId)
{
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
var readingList = user.ReadingLists.SingleOrDefault(r => r.Id == readingListId);
if (readingList == null)
{
return BadRequest("User is not associated with this reading list");
}
user.ReadingLists.Remove(readingList);
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
{
return Ok("Deleted");
}
return BadRequest("There was an issue deleting reading list");
}
/// <summary>
/// Creates a new List with a unique title. Returns the new ReadingList back
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("create")]
public async Task<ActionResult<ReadingListDto>> CreateList(CreateReadingListDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
// When creating, we need to make sure Title is unique
var hasExisting = user.ReadingLists.Any(l => l.Title.Equals(dto.Title));
if (hasExisting)
{
return BadRequest("A list of this name already exists");
}
user.ReadingLists.Add(new ReadingList()
{
Promoted = false,
Title = dto.Title,
Summary = string.Empty
});
if (!_unitOfWork.HasChanges()) return BadRequest("There was a problem creating list");
await _unitOfWork.CommitAsync();
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(dto.Title));
}
[HttpPost("update")]
public async Task<ActionResult> UpdateList(UpdateReadingListDto dto)
{
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
if (readingList == null) return BadRequest("List does not exist");
if (!string.IsNullOrEmpty(dto.Title))
{
readingList.Title = dto.Title; // Should I check if this is unique?
}
if (!string.IsNullOrEmpty(dto.Title))
{
readingList.Summary = dto.Summary;
}
readingList.Promoted = dto.Promoted;
_unitOfWork.ReadingListRepository.Update(readingList);
if (await _unitOfWork.CommitAsync())
{
return Ok("Updated");
}
return BadRequest("Could not update reading list");
}
[HttpPost("update-by-series")]
public async Task<ActionResult> UpdateListBySeries(UpdateReadingListBySeriesDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
var chapterIdsForSeries =
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new [] {dto.SeriesId});
// If there are adds, tell tracking this has been modified
if (await AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList))
{
_unitOfWork.ReadingListRepository.Update(readingList);
}
try
{
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
}
catch
{
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
}
[HttpPost("update-by-volume")]
public async Task<ActionResult> UpdateListByVolume(UpdateReadingListByVolumeDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
var chapterIdsForVolume =
(await _unitOfWork.VolumeRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList();
// If there are adds, tell tracking this has been modified
if (await AddChaptersToReadingList(dto.SeriesId, chapterIdsForVolume, readingList))
{
_unitOfWork.ReadingListRepository.Update(readingList);
}
try
{
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
}
catch
{
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
}
[HttpPost("update-by-chapter")]
public async Task<ActionResult> UpdateListByChapter(UpdateReadingListByChapterDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
// If there are adds, tell tracking this has been modified
if (await AddChaptersToReadingList(dto.SeriesId, new List<int>() { dto.ChapterId }, readingList))
{
_unitOfWork.ReadingListRepository.Update(readingList);
}
try
{
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
}
catch
{
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
}
/// <summary>
/// Adds a list of Chapters as reading list items to the passed reading list.
/// </summary>
/// <param name="seriesId"></param>
/// <param name="chapterIds"></param>
/// <param name="readingList"></param>
/// <returns>True if new chapters were added</returns>
private async Task<bool> AddChaptersToReadingList(int seriesId, IList<int> chapterIds,
ReadingList readingList)
{
readingList.Items ??= new List<ReadingListItem>();
var lastOrder = 0;
if (readingList.Items.Any())
{
lastOrder = readingList.Items.DefaultIfEmpty().Max(rli => rli.Order);
}
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds))
.OrderBy(c => int.Parse(c.Volume.Name))
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
var index = lastOrder + 1;
foreach (var chapter in chaptersForSeries)
{
if (existingChapterExists.Contains(chapter.Id)) continue;
readingList.Items.Add(new ReadingListItem()
{
Order = index,
ChapterId = chapter.Id,
SeriesId = seriesId,
VolumeId = chapter.VolumeId
});
index += 1;
}
return index > lastOrder + 1;
}
/// <summary>
/// Returns the next chapter within the reading list
/// </summary>
/// <param name="currentChapterId"></param>
/// <param name="readingListId"></param>
/// <returns>Chapter Id for next item, -1 if nothing exists</returns>
[HttpGet("next-chapter")]
public async Task<ActionResult<int>> GetNextChapter(int currentChapterId, int readingListId)
{
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
if (readingListItem == null) return BadRequest("Id does not exist");
var index = items.IndexOf(readingListItem) + 1;
if (items.Count > index)
{
return items[index].ChapterId;
}
return Ok(-1);
}
/// <summary>
/// Returns the prev chapter within the reading list
/// </summary>
/// <param name="currentChapterId"></param>
/// <param name="readingListId"></param>
/// <returns>Chapter Id for next item, -1 if nothing exists</returns>
[HttpGet("prev-chapter")]
public async Task<ActionResult<int>> GetPrevChapter(int currentChapterId, int readingListId)
{
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
if (readingListItem == null) return BadRequest("Id does not exist");
var index = items.IndexOf(readingListItem) - 1;
if (0 <= index)
{
return items[index].ChapterId;
}
return Ok(-1);
}
}
}

View file

@ -32,14 +32,14 @@ namespace API.Controllers
[HttpPost]
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams, [FromBody] FilterDto filterDto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var series =
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id, userParams, filterDto);
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto);
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series for library");
await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series);
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
@ -55,10 +55,10 @@ namespace API.Controllers
[HttpGet("{seriesId}")]
public async Task<ActionResult<SeriesDto>> GetSeries(int seriesId)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
try
{
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, user.Id));
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId));
}
catch (Exception e)
{
@ -95,15 +95,15 @@ namespace API.Controllers
[HttpGet("volumes")]
public async Task<ActionResult<IEnumerable<VolumeDto>>> GetVolumes(int seriesId)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, user.Id));
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, userId));
}
[HttpGet("volume")]
public async Task<ActionResult<VolumeDto>> GetVolume(int volumeId)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(volumeId, user.Id));
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(volumeId, userId));
}
[HttpGet("chapter")]
@ -182,14 +182,14 @@ namespace API.Controllers
[HttpPost("recently-added")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var series =
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, user.Id, userParams, filterDto);
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, userId, userParams, filterDto);
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series");
await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series);
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
@ -200,8 +200,8 @@ namespace API.Controllers
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetInProgress(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{
// NOTE: This has to be done manually like this due to the DistinctBy requirement
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var results = await _unitOfWork.SeriesRepository.GetInProgress(user.Id, libraryId, userParams, filterDto);
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var results = await _unitOfWork.SeriesRepository.GetInProgress(userId, libraryId, userParams, filterDto);
var listResults = results.DistinctBy(s => s.Name).Skip((userParams.PageNumber - 1) * userParams.PageSize)
.Take(userParams.PageSize).ToList();
@ -316,14 +316,14 @@ namespace API.Controllers
[HttpGet("series-by-collection")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetSeriesByCollectionTag(int collectionId, [FromQuery] UserParams userParams)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var series =
await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, user.Id, userParams);
await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams);
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series for collection");
await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series);
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
@ -339,8 +339,8 @@ namespace API.Controllers
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesById(SeriesByIdsDto dto)
{
if (dto.SeriesIds == null) return BadRequest("Must pass seriesIds");
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, user.Id));
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, userId));
}

View file

@ -103,7 +103,7 @@ namespace API.Controllers
}
else
{
_taskScheduler.ScheduleStatsTasks();
await _taskScheduler.ScheduleStatsTasks();
}
}
}

View file

@ -18,7 +18,7 @@ namespace API.Controllers
{
_unitOfWork = unitOfWork;
}
[Authorize(Policy = "RequireAdminRole")]
[HttpDelete("delete-user")]
public async Task<ActionResult> DeleteUser(string username)
@ -30,7 +30,7 @@ namespace API.Controllers
return BadRequest("Could not delete the user.");
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet]
public async Task<ActionResult<IEnumerable<MemberDto>>> GetUsers()
@ -42,8 +42,8 @@ namespace API.Controllers
public async Task<ActionResult<bool>> HasReadingProgress(int libraryId)
{
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, user.Id));
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId));
}
[HttpGet("has-library-access")]
@ -77,8 +77,8 @@ namespace API.Controllers
{
return Ok(preferencesDto);
}
return BadRequest("There was an issue saving preferences.");
}
}
}
}

View file

@ -0,0 +1,13 @@
using API.Entities.Enums;
namespace API.DTOs.Reader
{
public class BookInfoDto
{
public string BookTitle { get; set; }
public int SeriesId { get; set; }
public int VolumeId { get; set; }
public MangaFormat SeriesFormat { get; set; }
public int LibraryId { get; set; }
}
}

View file

@ -1,16 +1,21 @@
namespace API.DTOs.Reader
using API.Entities.Enums;
namespace API.DTOs.Reader
{
public class ChapterInfoDto
{
public string ChapterNumber { get; set; }
public string VolumeNumber { get; set; }
public int VolumeId { get; set; }
public string SeriesName { get; set; }
public MangaFormat SeriesFormat { get; set; }
public int SeriesId { get; set; }
public int LibraryId { get; set; }
public string ChapterTitle { get; set; } = "";
public int Pages { get; set; }
public string FileName { get; set; }
public bool IsSpecial { get; set; }
}
}
}

View file

@ -0,0 +1,7 @@
namespace API.DTOs.ReadingLists
{
public class CreateReadingListDto
{
public string Title { get; init; }
}
}

View file

@ -0,0 +1,13 @@
namespace API.DTOs.ReadingLists
{
public class ReadingListDto
{
public int Id { get; init; }
public string Title { get; set; }
public string Summary { get; set; }
/// <summary>
/// Reading lists that are promoted are only done by admins
/// </summary>
public bool Promoted { get; set; }
}
}

View file

@ -0,0 +1,25 @@
using API.Entities.Enums;
namespace API.DTOs.ReadingLists
{
public class ReadingListItemDto
{
public int Id { get; init; }
public int Order { get; init; }
public int ChapterId { get; init; }
public int SeriesId { get; init; }
public string SeriesName { get; set; }
public MangaFormat SeriesFormat { get; set; }
public int PagesRead { get; set; }
public int PagesTotal { get; set; }
public string ChapterNumber { get; set; }
public string VolumeNumber { get; set; }
public int VolumeId { get; set; }
public int LibraryId { get; set; }
public string Title { get; set; }
/// <summary>
/// Used internally only
/// </summary>
public int ReadingListId { get; set; }
}
}

View file

@ -0,0 +1,9 @@
namespace API.DTOs.ReadingLists
{
public class UpdateReadingListByChapterDto
{
public int ChapterId { get; init; }
public int SeriesId { get; init; }
public int ReadingListId { get; init; }
}
}

View file

@ -0,0 +1,8 @@
namespace API.DTOs.ReadingLists
{
public class UpdateReadingListBySeriesDto
{
public int SeriesId { get; init; }
public int ReadingListId { get; init; }
}
}

View file

@ -0,0 +1,9 @@
namespace API.DTOs.ReadingLists
{
public class UpdateReadingListByVolumeDto
{
public int VolumeId { get; init; }
public int SeriesId { get; init; }
public int ReadingListId { get; init; }
}
}

View file

@ -0,0 +1,10 @@
namespace API.DTOs.ReadingLists
{
public class UpdateReadingListDto
{
public int ReadingListId { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public bool Promoted { get; set; }
}
}

View file

@ -0,0 +1,10 @@
namespace API.DTOs.ReadingLists
{
public class UpdateReadingListPosition
{
public int ReadingListId { get; set; }
public int ReadingListItemId { get; set; }
public int FromPosition { get; set; }
public int ToPosition { get; set; }
}
}

View file

@ -14,6 +14,7 @@ namespace API.DTOs
public DateTime LastModified { get; set; }
public DateTime Created { get; set; }
public bool IsSpecial { get; set; }
public int SeriesId { get; set; }
public ICollection<ChapterDto> Chapters { get; set; }
}
}
}

View file

@ -1,7 +0,0 @@
namespace API.Data
{
public class BookmarkRepository
{
}
}

View file

@ -35,6 +35,9 @@ namespace API.Data
public DbSet<SeriesMetadata> SeriesMetadata { get; set; }
public DbSet<CollectionTag> CollectionTag { get; set; }
public DbSet<AppUserBookmark> AppUserBookmark { get; set; }
public DbSet<ReadingList> ReadingList { get; set; }
public DbSet<ReadingListItem> ReadingListItem { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,84 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace API.Data.Migrations
{
public partial class ReadingLists : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ReadingList",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Title = table.Column<string>(type: "TEXT", nullable: true),
Summary = table.Column<string>(type: "TEXT", nullable: true),
Promoted = table.Column<bool>(type: "INTEGER", nullable: false),
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
LastModified = table.Column<DateTime>(type: "TEXT", nullable: false),
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ReadingList", x => x.Id);
table.ForeignKey(
name: "FK_ReadingList_AspNetUsers_AppUserId",
column: x => x.AppUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ReadingListItem",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
LibraryId = table.Column<int>(type: "INTEGER", nullable: false),
SeriesId = table.Column<int>(type: "INTEGER", nullable: false),
VolumeId = table.Column<int>(type: "INTEGER", nullable: false),
ChapterId = table.Column<int>(type: "INTEGER", nullable: false),
Order = table.Column<int>(type: "INTEGER", nullable: false),
ReadingListId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ReadingListItem", x => x.Id);
table.ForeignKey(
name: "FK_ReadingListItem_ReadingList_ReadingListId",
column: x => x.ReadingListId,
principalTable: "ReadingList",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_ReadingList_AppUserId",
table: "ReadingList",
column: "AppUserId");
migrationBuilder.CreateIndex(
name: "IX_ReadingListItem_ReadingListId",
table: "ReadingListItem",
column: "ReadingListId");
migrationBuilder.CreateIndex(
name: "IX_ReadingListItem_SeriesId_VolumeId_ChapterId_LibraryId",
table: "ReadingListItem",
columns: new[] { "SeriesId", "VolumeId", "ChapterId", "LibraryId" },
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ReadingListItem");
migrationBuilder.DropTable(
name: "ReadingList");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace API.Data.Migrations
{
public partial class ReadingListsAdditions : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ReadingListItem_ReadingList_ReadingListId",
table: "ReadingListItem");
migrationBuilder.AlterColumn<int>(
name: "ReadingListId",
table: "ReadingListItem",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AddForeignKey(
name: "FK_ReadingListItem_ReadingList_ReadingListId",
table: "ReadingListItem",
column: "ReadingListId",
principalTable: "ReadingList",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ReadingListItem_ReadingList_ReadingListId",
table: "ReadingListItem");
migrationBuilder.AlterColumn<int>(
name: "ReadingListId",
table: "ReadingListItem",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AddForeignKey(
name: "FK_ReadingListItem_ReadingList_ReadingListId",
table: "ReadingListItem",
column: "ReadingListId",
principalTable: "ReadingList",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,67 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace API.Data.Migrations
{
public partial class ReadingListsExtraRealationships : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_ReadingListItem_ChapterId",
table: "ReadingListItem",
column: "ChapterId");
migrationBuilder.CreateIndex(
name: "IX_ReadingListItem_VolumeId",
table: "ReadingListItem",
column: "VolumeId");
migrationBuilder.AddForeignKey(
name: "FK_ReadingListItem_Chapter_ChapterId",
table: "ReadingListItem",
column: "ChapterId",
principalTable: "Chapter",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ReadingListItem_Series_SeriesId",
table: "ReadingListItem",
column: "SeriesId",
principalTable: "Series",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ReadingListItem_Volume_VolumeId",
table: "ReadingListItem",
column: "VolumeId",
principalTable: "Volume",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ReadingListItem_Chapter_ChapterId",
table: "ReadingListItem");
migrationBuilder.DropForeignKey(
name: "FK_ReadingListItem_Series_SeriesId",
table: "ReadingListItem");
migrationBuilder.DropForeignKey(
name: "FK_ReadingListItem_Volume_VolumeId",
table: "ReadingListItem");
migrationBuilder.DropIndex(
name: "IX_ReadingListItem_ChapterId",
table: "ReadingListItem");
migrationBuilder.DropIndex(
name: "IX_ReadingListItem_VolumeId",
table: "ReadingListItem");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,43 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace API.Data.Migrations
{
public partial class ReadingListsChanges : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_ReadingListItem_SeriesId_VolumeId_ChapterId_LibraryId",
table: "ReadingListItem");
migrationBuilder.DropColumn(
name: "LibraryId",
table: "ReadingListItem");
migrationBuilder.CreateIndex(
name: "IX_ReadingListItem_SeriesId",
table: "ReadingListItem",
column: "SeriesId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_ReadingListItem_SeriesId",
table: "ReadingListItem");
migrationBuilder.AddColumn<int>(
name: "LibraryId",
table: "ReadingListItem",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_ReadingListItem_SeriesId_VolumeId_ChapterId_LibraryId",
table: "ReadingListItem",
columns: new[] { "SeriesId", "VolumeId", "ChapterId", "LibraryId" },
unique: true);
}
}
}

View file

@ -440,6 +440,71 @@ namespace API.Data.Migrations
b.ToTable("MangaFile");
});
modelBuilder.Entity("API.Entities.ReadingList", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<bool>("Promoted")
.HasColumnType("INTEGER");
b.Property<string>("Summary")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.ToTable("ReadingList");
});
modelBuilder.Entity("API.Entities.ReadingListItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("Order")
.HasColumnType("INTEGER");
b.Property<int>("ReadingListId")
.HasColumnType("INTEGER");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("VolumeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChapterId");
b.HasIndex("ReadingListId");
b.HasIndex("SeriesId");
b.HasIndex("VolumeId");
b.ToTable("ReadingListItem");
});
modelBuilder.Entity("API.Entities.Series", b =>
{
b.Property<int>("Id")
@ -780,6 +845,52 @@ namespace API.Data.Migrations
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Entities.ReadingList", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithMany("ReadingLists")
.HasForeignKey("AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AppUser");
});
modelBuilder.Entity("API.Entities.ReadingListItem", b =>
{
b.HasOne("API.Entities.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.ReadingList", "ReadingList")
.WithMany("Items")
.HasForeignKey("ReadingListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.Series", "Series")
.WithMany()
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.Volume", "Volume")
.WithMany()
.HasForeignKey("VolumeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
b.Navigation("ReadingList");
b.Navigation("Series");
b.Navigation("Volume");
});
modelBuilder.Entity("API.Entities.Series", b =>
{
b.HasOne("API.Entities.Library", "Library")
@ -892,6 +1003,8 @@ namespace API.Data.Migrations
b.Navigation("Ratings");
b.Navigation("ReadingLists");
b.Navigation("UserPreferences");
b.Navigation("UserRoles");
@ -909,6 +1022,11 @@ namespace API.Data.Migrations
b.Navigation("Series");
});
modelBuilder.Entity("API.Entities.ReadingList", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("API.Entities.Series", b =>
{
b.Navigation("Metadata");

View file

@ -2,9 +2,10 @@
using System.Threading.Tasks;
using API.Entities.Enums;
using API.Interfaces;
using API.Interfaces.Repositories;
using Microsoft.EntityFrameworkCore;
namespace API.Data
namespace API.Data.Repositories
{
public class AppUserProgressRepository : IAppUserProgressRepository
{
@ -25,7 +26,7 @@ namespace API.Data
var rowsToRemove = await _context.AppUserProgresses
.Where(progress => !chapterIds.Contains(progress.ChapterId))
.ToListAsync();
_context.RemoveRange(rowsToRemove);
return await _context.SaveChangesAsync() > 0 ? rowsToRemove.Count : 0;
}
@ -45,7 +46,7 @@ namespace API.Data
.ToListAsync();
if (seriesIds.Count == 0) return false;
return await _context.Series
.Include(s => s.Library)
.Where(s => seriesIds.Contains(s.Id) && s.Library.Type == libraryType)
@ -53,4 +54,4 @@ namespace API.Data
.AnyAsync();
}
}
}
}

View file

@ -1,8 +1,11 @@
using API.Entities;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Interfaces.Repositories;
using Microsoft.EntityFrameworkCore;
namespace API.Data
namespace API.Data.Repositories
{
public class ChapterRepository : IChapterRepository
{
@ -18,6 +21,14 @@ namespace API.Data
_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();
}
// TODO: Move over Chapter based queries here
}
}

View file

@ -4,11 +4,12 @@ using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
using API.Interfaces;
using API.Interfaces.Repositories;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data
namespace API.Data.Repositories
{
public class CollectionTagRepository : ICollectionTagRepository
{

View file

@ -3,9 +3,10 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Interfaces;
using API.Interfaces.Repositories;
using Microsoft.EntityFrameworkCore;
namespace API.Data
namespace API.Data.Repositories
{
public class FileRepository : IFileRepository
{

View file

@ -5,11 +5,12 @@ using API.DTOs;
using API.Entities;
using API.Entities.Enums;
using API.Interfaces;
using API.Interfaces.Repositories;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data
namespace API.Data.Repositories
{
public class LibraryRepository : ILibraryRepository
{

View file

@ -0,0 +1,178 @@
using System.Collections.Generic;
using System.Linq;
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
{
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 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 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);
}
public async Task<ReadingList> GetReadingListByIdAsync(int readingListId)
{
return await _context.ReadingList
.Where(r => r.Id == readingListId)
.Include(r => r.Items)
.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();
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,
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)
{
var progressItem = items.SingleOrDefault(i => i.ChapterId == progress.ChapterId && i.ReadingListId == readingListId);
if (progressItem == null) continue;
progressItem.PagesRead = progress.PagesRead;
}
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

@ -8,11 +8,12 @@ using API.Entities;
using API.Extensions;
using API.Helpers;
using API.Interfaces;
using API.Interfaces.Repositories;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data
namespace API.Data.Repositories
{
public class SeriesRepository : ISeriesRepository
{
@ -221,21 +222,17 @@ namespace API.Data
public async Task<int[]> GetChapterIdsForSeriesAsync(int[] seriesIds)
{
var series = await _context.Series
.Where(s => seriesIds.Contains(s.Id))
.Include(s => s.Volumes)
.ThenInclude(v => v.Chapters)
var volumes = await _context.Volume
.Where(v => seriesIds.Contains(v.SeriesId))
.Include(v => v.Chapters)
.ToListAsync();
IList<int> chapterIds = new List<int>();
foreach (var s in series)
foreach (var v in volumes)
{
foreach (var v in s.Volumes)
foreach (var c in v.Chapters)
{
foreach (var c in v.Chapters)
{
chapterIds.Add(c.Id);
}
chapterIds.Add(c.Id);
}
}

View file

@ -5,10 +5,11 @@ using API.DTOs;
using API.Entities;
using API.Entities.Enums;
using API.Interfaces;
using API.Interfaces.Repositories;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
namespace API.Data
namespace API.Data.Repositories
{
public class SettingsRepository : ISettingsRepository
{
@ -45,4 +46,4 @@ namespace API.Data
return await _context.ServerSetting.ToListAsync();
}
}
}
}

View file

@ -5,12 +5,13 @@ using API.Constants;
using API.DTOs;
using API.Entities;
using API.Interfaces;
using API.Interfaces.Repositories;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace API.Data
namespace API.Data.Repositories
{
public class UserRepository : IUserRepository
{
@ -54,10 +55,36 @@ namespace API.Data
}
/// <summary>
/// Gets an AppUser by id. Returns back Progress information.
/// 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);
}
/// <summary>
/// Gets an AppUser by id. Returns back Progress information.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<AppUser> GetUserByIdAsync(int id)
{
return await _context.Users

View file

@ -4,11 +4,12 @@ using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
using API.Interfaces;
using API.Interfaces.Repositories;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data
namespace API.Data.Repositories
{
public class VolumeRepository : IVolumeRepository
{

View file

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using API.Data.Repositories;
using API.Entities;
using API.Interfaces;
using API.Interfaces.Repositories;
@ -32,6 +33,7 @@ namespace API.Data
public ICollectionTagRepository CollectionTagRepository => new CollectionTagRepository(_context, _mapper);
public IFileRepository FileRepository => new FileRepository(_context);
public IChapterRepository ChapterRepository => new ChapterRepository(_context);
public IReadingListRepository ReadingListRepository => new ReadingListRepository(_context, _mapper);
/// <summary>
/// Commits changes to the DB. Completes the open transaction.
@ -39,7 +41,6 @@ namespace API.Data
/// <returns></returns>
public bool Commit()
{
return _context.SaveChanges() > 0;
}
/// <summary>

View file

@ -18,6 +18,10 @@ namespace API.Entities
public AppUserPreferences UserPreferences { get; set; }
public ICollection<AppUserBookmark> Bookmarks { get; set; }
/// <summary>
/// Reading lists associated with this user
/// </summary>
public ICollection<ReadingList> ReadingLists { get; set; }
/// <summary>
/// An API Key to interact with external services, like OPDS
/// </summary>
public string ApiKey { get; set; }

View file

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using API.Entities.Interfaces;
namespace API.Entities
{
/// <summary>
/// This is a collection of <see cref="ReadingListItem"/> which represent individual chapters and an order.
/// </summary>
public class ReadingList : IEntityDate
{
public int Id { get; init; }
public string Title { get; set; }
public string Summary { get; set; }
/// <summary>
/// Reading lists that are promoted are only done by admins
/// </summary>
public bool Promoted { get; set; }
public ICollection<ReadingListItem> Items { get; set; }
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
// Relationships
public int AppUserId { get; set; }
public AppUser AppUser { get; set; }
}
}

View file

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
namespace API.Entities
{
//[Index(nameof(SeriesId), nameof(VolumeId), nameof(ChapterId), IsUnique = true)]
public class ReadingListItem
{
public int Id { get; init; }
public int SeriesId { get; set; }
public int VolumeId { get; set; }
public int ChapterId { get; set; }
/// <summary>
/// Order of the chapter within a Reading List
/// </summary>
public int Order { get; set; }
// Relationship
public ReadingList ReadingList { get; set; }
public int ReadingListId { get; set; }
// Idea, keep these for easy join statements
public Series Series { get; set; }
public Volume Volume { get; set; }
public Chapter Chapter { get; set; }
}
}

View file

@ -14,7 +14,6 @@ namespace API.Extensions
public static bool HasFileBeenModifiedSince(this FileInfo fileInfo, DateTime comparison)
{
return DateTime.Compare(fileInfo.LastWriteTime, comparison) > 0;
//return fileInfo?.LastWriteTime > comparison;
}
}
}

View file

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using API.DTOs;
using API.DTOs.ReadingLists;
using API.Entities;
using API.Helpers.Converters;
using AutoMapper;
@ -31,6 +32,9 @@ namespace API.Helpers
CreateMap<AppUserBookmark, BookmarkDto>();
CreateMap<ReadingList, ReadingListDto>();
CreateMap<ReadingListItem, ReadingListItemDto>();
CreateMap<Series, SearchResultDto>()
.ForMember(dest => dest.SeriesId,
opt => opt.MapFrom(src => src.Id))

View file

@ -14,6 +14,7 @@ namespace API.Interfaces
ICollectionTagRepository CollectionTagRepository { get; }
IFileRepository FileRepository { get; }
IChapterRepository ChapterRepository { get; }
IReadingListRepository ReadingListRepository { get; }
bool Commit();
Task<bool> CommitAsync();
bool HasChanges();

View file

@ -1,11 +1,11 @@
using System.Threading.Tasks;
using API.Entities.Enums;
namespace API.Interfaces
namespace API.Interfaces.Repositories
{
public interface IAppUserProgressRepository
{
Task<int> CleanupAbandonedChapters();
Task<bool> UserHasProgress(LibraryType libraryType, int userId);
}
}
}

View file

@ -1,9 +1,13 @@
using API.Entities;
using System.Collections.Generic;
using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
namespace API.Interfaces.Repositories
{
public interface IChapterRepository
{
void Update(Chapter chapter);
Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds);
}
}

View file

@ -3,7 +3,7 @@ using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
namespace API.Interfaces
namespace API.Interfaces.Repositories
{
public interface ICollectionTagRepository
{

View file

@ -1,10 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace API.Interfaces
namespace API.Interfaces.Repositories
{
public interface IFileRepository
{
Task<IEnumerable<string>> GetFileExtensions();
}
}
}

View file

@ -4,7 +4,7 @@ using API.DTOs;
using API.Entities;
using API.Entities.Enums;
namespace API.Interfaces
namespace API.Interfaces.Repositories
{
public interface ILibraryRepository
{

View file

@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using API.DTOs.ReadingLists;
using API.Entities;
using API.Helpers;
namespace API.Interfaces.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(string title);
Task<IEnumerable<ReadingListItem>> GetReadingListItemsByIdAsync(int readingListId);
void Remove(ReadingListItem item);
void BulkRemove(IEnumerable<ReadingListItem> items);
void Update(ReadingList list);
}
}

View file

@ -5,7 +5,7 @@ using API.DTOs.Filtering;
using API.Entities;
using API.Helpers;
namespace API.Interfaces
namespace API.Interfaces.Repositories
{
public interface ISeriesRepository
{

View file

@ -4,7 +4,7 @@ using API.DTOs;
using API.Entities;
using API.Entities.Enums;
namespace API.Interfaces
namespace API.Interfaces.Repositories
{
public interface ISettingsRepository
{
@ -12,6 +12,6 @@ namespace API.Interfaces
Task<ServerSettingDto> GetSettingsDtoAsync();
Task<ServerSetting> GetSettingAsync(ServerSettingKey key);
Task<IEnumerable<ServerSetting>> GetSettingsAsync();
}
}
}

View file

@ -3,7 +3,7 @@ using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
namespace API.Interfaces
namespace API.Interfaces.Repositories
{
public interface IUserRepository
{
@ -11,6 +11,8 @@ namespace API.Interfaces
void Update(AppUserPreferences preferences);
public void Delete(AppUser user);
Task<AppUser> GetUserByUsernameAsync(string username);
Task<int> GetUserIdByUsernameAsync(string username);
Task<AppUser> GetUserWithReadingListsByUsernameAsync(string username);
Task<AppUser> GetUserByIdAsync(int id);
Task<IEnumerable<MemberDto>> GetMembersAsync();
Task<IEnumerable<AppUser>> GetAdminUsersAsync();

View file

@ -3,7 +3,7 @@ using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
namespace API.Interfaces
namespace API.Interfaces.Repositories
{
public interface IVolumeRepository
{

View file

@ -129,9 +129,14 @@ namespace API.Parser
RegexTimeout),
// [dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz
new Regex(
@"(?<Series>.*) (\b|_|-)(vol)\.?",
@"(?<Series>.*) (\b|_|-)(vol)\.?(\s|-|_)?\d+",
RegexOptions.IgnoreCase | RegexOptions.Compiled,
RegexTimeout),
// [xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans]
new Regex(
@"(?<Series>.*) (\b|_|-)(vol)(ume)",
RegexOptions.IgnoreCase | RegexOptions.Compiled,
RegexTimeout),
//Knights of Sidonia c000 (S2 LE BD Omake - BLAME!) [Habanero Scans]
new Regex(
@"(?<Series>.*)(\bc\d+\b)",

View file

@ -161,6 +161,7 @@ namespace API.Services
/// <summary>
/// Not an external call. Only public so that we can call this for a Task
/// </summary>
// ReSharper disable once MemberCanBePrivate.Global
public async Task CheckForUpdate()
{
var update = await _versionUpdaterService.CheckForUpdate();