using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Comparators; using API.Data; using API.DTOs.ReadingLists; using API.Entities; using API.Extensions; using API.Helpers; using API.SignalR; using Microsoft.AspNetCore.Mvc; namespace API.Controllers { public class ReadingListController : BaseApiController { private readonly IUnitOfWork _unitOfWork; private readonly IEventHub _eventHub; private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst(); public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub) { _unitOfWork = unitOfWork; _eventHub = eventHub; } /// /// 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. /// /// Defaults to true /// [HttpPost("lists")] public async Task>> 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); } [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(await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList())); } /// /// Updates an items position /// /// /// [HttpPost("update-position")] public async Task 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"); } /// /// Deletes a list item from the list. Will reorder all item positions afterwards /// /// /// [HttpPost("delete-item")] public async Task DeleteListItem(UpdateReadingListPosition dto) { var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId); readingList.Items = readingList.Items.Where(r => r.Id != dto.ReadingListItemId).ToList(); var index = 0; foreach (var readingListItem in readingList.Items) { readingListItem.Order = index; index++; } if (!_unitOfWork.HasChanges()) return Ok(); if (await _unitOfWork.CommitAsync()) { 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 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()) return Ok("Nothing to remove"); await _unitOfWork.CommitAsync(); return Ok("Updated"); } catch { await _unitOfWork.RollbackAsync(); } return BadRequest("Could not remove read items"); } /// /// Deletes a reading list /// /// /// [HttpDelete] public async Task DeleteList([FromQuery] int readingListId) { var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername()); var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); var readingList = user.ReadingLists.SingleOrDefault(r => r.Id == readingListId); if (readingList == null && !isAdmin) { return BadRequest("User is not associated with this reading list"); } readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId); user.ReadingLists.Remove(readingList); if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync()) { return Ok("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.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(DbFactory.ReadingList(dto.Title, string.Empty, false)); if (!_unitOfWork.HasChanges()) return BadRequest("There was a problem creating list"); await _unitOfWork.CommitAsync(); return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(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"); if (!string.IsNullOrEmpty(dto.Title)) { readingList.Title = dto.Title; // Should I check if this is unique? readingList.NormalizedTitle = Parser.Parser.Normalize(readingList.Title); } if (!string.IsNullOrEmpty(dto.Title)) { readingList.Summary = dto.Summary; } 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 _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"); } /// /// 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 _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 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 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 _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 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 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 _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.ChapterRepository.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 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() { 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"); } /// /// Adds a list of Chapters as reading list items to the passed reading list. /// /// /// /// /// True if new chapters were added private async Task AddChaptersToReadingList(int seriesId, IList chapterIds, ReadingList readingList) { readingList.Items ??= new List(); 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 => float.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(DbFactory.ReadingListItem(index, seriesId, chapter.VolumeId, chapter.Id)); index += 1; } return index > lastOrder + 1; } /// /// 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); } } }