using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Comparators; using API.Data; using API.Data.Repositories; using API.DTOs.ReadingLists; using API.Entities; using API.Extensions; using API.Helpers; using API.Services; using API.SignalR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace API.Controllers; [Authorize] public class ReadingListController : BaseApiController { private readonly IUnitOfWork _unitOfWork; private readonly IEventHub _eventHub; private readonly IReadingListService _readingListService; public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub, IReadingListService readingListService) { _unitOfWork = unitOfWork; _eventHub = eventHub; _readingListService = readingListService; } /// /// Fetches a single Reading List /// /// /// [HttpGet] public async Task>> GetList(int readingListId) { var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, userId)); } /// /// Returns reading lists (paginated) for a given user. /// /// Include Promoted Reading Lists along with user's Reading Lists. Defaults to true /// Pagination parameters /// [HttpPost("lists")] public async Task>> GetListsForUser([FromQuery] UserParams userParams, 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); } /// /// Returns all Reading Lists the user has access to that have a series within it. /// /// /// [HttpGet("lists-for-series")] public async Task>> GetListsForSeries(int seriesId) { var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(userId, seriesId, true); return Ok(items); } /// /// Fetches all reading list items for a given list including rich metadata around series, volume, chapters, and progress /// /// This call is expensive /// /// [HttpGet("items")] public async Task>> GetListForUser(int readingListId) { var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId); return Ok(items); } /// /// Updates an items position /// /// /// [HttpPost("update-position")] public async Task UpdateListItemPosition(UpdateReadingListPosition dto) { // Make sure UI buffers events var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername()); if (user == null) { return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); } if (await _readingListService.UpdateReadingListItemPosition(dto)) return Ok("Updated"); return BadRequest("Couldn't update position"); } /// /// Deletes a list item from the list. Will reorder all item positions afterwards /// /// /// [HttpPost("delete-item")] public async Task DeleteListItem(UpdateReadingListPosition dto) { var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername()); if (user == null) { return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); } if (await _readingListService.DeleteReadingListItem(dto)) { return Ok("Updated"); } return BadRequest("Couldn't delete item"); } /// /// Removes all entries that are fully read from the reading list /// /// /// [HttpPost("remove-read")] public async Task DeleteReadFromList([FromQuery] int readingListId) { var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername()); if (user == null) { return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); } if (await _readingListService.RemoveFullyReadItems(readingListId, user)) { return Ok("Updated"); } return BadRequest("Could not remove read items"); } /// /// Deletes a reading list /// /// /// [HttpDelete] public async Task DeleteList([FromQuery] int readingListId) { var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername()); if (user == null) { return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); } if (await _readingListService.DeleteReadingList(readingListId, user)) return Ok("List was deleted"); return BadRequest("There was an issue deleting reading list"); } /// /// Creates a new List with a unique title. Returns the new ReadingList back /// /// /// [HttpPost("create")] public async Task> CreateList(CreateReadingListDto dto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.ReadingListsWithItems); // 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"); } var readingList = DbFactory.ReadingList(dto.Title, string.Empty, false); user.ReadingLists.Add(readingList); if (!_unitOfWork.HasChanges()) return BadRequest("There was a problem creating list"); await _unitOfWork.CommitAsync(); return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(user.Id, dto.Title)); } /// /// Update the properties (title, summary) of a reading list /// /// /// [HttpPost("update")] public async Task UpdateList(UpdateReadingListDto dto) { var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId); if (readingList == null) return BadRequest("List does not exist"); var user = await _readingListService.UserHasReadingListAccess(readingList.Id, User.GetUsername()); if (user == null) { return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); } dto.Title = dto.Title.Trim(); if (!string.IsNullOrEmpty(dto.Title)) { readingList.Summary = dto.Summary; if (!readingList.Title.Equals(dto.Title)) { var hasExisting = user.ReadingLists.Any(l => l.Title.Equals(dto.Title)); if (hasExisting) { return BadRequest("A list of this name already exists"); } readingList.Title = dto.Title; readingList.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(readingList.Title); } } readingList.Promoted = dto.Promoted; readingList.CoverImageLocked = dto.CoverImageLocked; if (!dto.CoverImageLocked) { readingList.CoverImageLocked = false; readingList.CoverImage = string.Empty; await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false); _unitOfWork.ReadingListRepository.Update(readingList); } _unitOfWork.ReadingListRepository.Update(readingList); if (await _unitOfWork.CommitAsync()) { return Ok("Updated"); } return BadRequest("Could not update reading list"); } /// /// Adds all chapters from a Series to a reading list /// /// /// [HttpPost("update-by-series")] public async Task UpdateListBySeries(UpdateReadingListBySeriesDto dto) { var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername()); if (user == null) { return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); } 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 _readingListService.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"); } /// /// Adds all chapters from a list of volumes and chapters to a reading list /// /// /// [HttpPost("update-by-multiple")] public async Task UpdateListByMultiple(UpdateReadingListByMultipleDto dto) { var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername()); if (user == null) { return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); if (readingList == null) return BadRequest("Reading List does not exist"); var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); foreach (var chapterId in dto.ChapterIds) { chapterIds.Add(chapterId); } // If there are adds, tell tracking this has been modified if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIds, readingList)) { _unitOfWork.ReadingListRepository.Update(readingList); } try { if (_unitOfWork.HasChanges()) { await _unitOfWork.CommitAsync(); return Ok("Updated"); } } catch { await _unitOfWork.RollbackAsync(); } return Ok("Nothing to do"); } /// /// Adds all chapters from a list of series to a reading list /// /// /// [HttpPost("update-by-multiple-series")] public async Task UpdateListByMultipleSeries(UpdateReadingListByMultipleSeriesDto dto) { var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername()); if (user == null) { return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); if (readingList == null) return BadRequest("Reading List does not exist"); var ids = await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(dto.SeriesIds.ToArray()); foreach (var seriesId in ids.Keys) { // If there are adds, tell tracking this has been modified if (await _readingListService.AddChaptersToReadingList(seriesId, ids[seriesId], 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 UpdateListByVolume(UpdateReadingListByVolumeDto dto) { var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername()); if (user == null) { return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); if (readingList == null) return BadRequest("Reading List does not exist"); var chapterIdsForVolume = (await _unitOfWork.ChapterRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList(); // If there are adds, tell tracking this has been modified if (await _readingListService.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 UpdateListByChapter(UpdateReadingListByChapterDto dto) { var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername()); if (user == null) { return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); } 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 _readingListService.AddChaptersToReadingList(dto.SeriesId, new List() { 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"); } /// /// Returns the next chapter within the reading list /// /// /// /// Chapter Id for next item, -1 if nothing exists [HttpGet("next-chapter")] public async Task> 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); } /// /// Returns the prev chapter within the reading list /// /// /// /// Chapter Id for next item, -1 if nothing exists [HttpGet("prev-chapter")] public async Task> 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); } }