mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
* Added some documentation. Removed Require Admin Role from Search Tags. Added Summary to be updated on UpdateTag. * Added Swagger xml doc generation to beef up the documentation. Started adding xml comments to the APIs. This is a needed, slow task for upcoming Plugins system. * Implemented the ability to upload a custom series image to override the existing cover image. Refactored some code out to use ImageService and added more documentation * When a page cache fails, delete cache directory so user can try to reload. * Implemented the ability to lock a series cover image such that after user uploads something, it wont get refreshed by Kavita. * Implemented the ability to reset cover image for series by unlocking * Kick off a series refresh after a cover is unlocked. * Ability to press enter to load a url * Ability to reset selection * Cleaned up cover chooser such that reset is nicer, errors inform user to use file upload, series edit modal now doesn't use scrollable body. Mobile tweaks. CoverImageLocked is now sent to the UI. * More css changes to look better * When no bookmarks, don't show both markups * Fixed issues where images wouldn't refresh after cover image was changed. * Implemented the ability to change the cover images for collection tags. * Added property and API for chapter cover image update * Added UI code to prepare for updating cover image for chapters. need to rearrange components * Moved a ton of code around to separate card related screens into their own module. * Implemented the ability to update a chapter/volume cover image * Refactored action for volume to say edit to reflect modal action * Fixed issue where after editing chapter cover image, the underlying card wouldn't update * Fixed an issue where we were passing volumeId to the reset chapter lock. Changed some logic in volume cover image generation. * Automatically apply when you hit reset cover image
330 lines
14 KiB
C#
330 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using API.Data;
|
|
using API.DTOs;
|
|
using API.DTOs.Filtering;
|
|
using API.Entities;
|
|
using API.Extensions;
|
|
using API.Helpers;
|
|
using API.Interfaces;
|
|
using Kavita.Common;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace API.Controllers
|
|
{
|
|
public class SeriesController : BaseApiController
|
|
{
|
|
private readonly ILogger<SeriesController> _logger;
|
|
private readonly ITaskScheduler _taskScheduler;
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
|
|
public SeriesController(ILogger<SeriesController> logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork)
|
|
{
|
|
_logger = logger;
|
|
_taskScheduler = taskScheduler;
|
|
_unitOfWork = unitOfWork;
|
|
}
|
|
|
|
[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 series =
|
|
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id, 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);
|
|
|
|
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
|
|
|
return Ok(series);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches a Series for a given Id
|
|
/// </summary>
|
|
/// <param name="seriesId">Series Id to fetch details for</param>
|
|
/// <returns></returns>
|
|
/// <exception cref="KavitaException">Throws an exception if the series Id does exist</exception>
|
|
[HttpGet("{seriesId}")]
|
|
public async Task<ActionResult<SeriesDto>> GetSeries(int seriesId)
|
|
{
|
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
|
try
|
|
{
|
|
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, user.Id));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_logger.LogError(e, "There was an issue fetching {SeriesId}", seriesId);
|
|
throw new KavitaException("This series does not exist");
|
|
}
|
|
|
|
}
|
|
|
|
[Authorize(Policy = "RequireAdminRole")]
|
|
[HttpDelete("{seriesId}")]
|
|
public async Task<ActionResult<bool>> DeleteSeries(int seriesId)
|
|
{
|
|
var username = User.GetUsername();
|
|
var chapterIds = (await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new []{seriesId}));
|
|
_logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", seriesId, username);
|
|
var result = await _unitOfWork.SeriesRepository.DeleteSeriesAsync(seriesId);
|
|
|
|
if (result)
|
|
{
|
|
_taskScheduler.CleanupChapters(chapterIds);
|
|
}
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns All volumes for a series with progress information and Chapters
|
|
/// </summary>
|
|
/// <param name="seriesId"></param>
|
|
/// <returns></returns>
|
|
[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));
|
|
}
|
|
|
|
[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));
|
|
}
|
|
|
|
[HttpGet("chapter")]
|
|
public async Task<ActionResult<VolumeDto>> GetChapter(int chapterId)
|
|
{
|
|
return Ok(await _unitOfWork.VolumeRepository.GetChapterDtoAsync(chapterId));
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpPost("update-rating")]
|
|
public async Task<ActionResult> UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto)
|
|
{
|
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
|
var userRating = await _unitOfWork.UserRepository.GetUserRating(updateSeriesRatingDto.SeriesId, user.Id) ??
|
|
new AppUserRating();
|
|
|
|
userRating.Rating = updateSeriesRatingDto.UserRating;
|
|
userRating.Review = updateSeriesRatingDto.UserReview;
|
|
userRating.SeriesId = updateSeriesRatingDto.SeriesId;
|
|
|
|
if (userRating.Id == 0)
|
|
{
|
|
user.Ratings ??= new List<AppUserRating>();
|
|
user.Ratings.Add(userRating);
|
|
}
|
|
|
|
_unitOfWork.UserRepository.Update(user);
|
|
|
|
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical error.");
|
|
|
|
return Ok();
|
|
}
|
|
|
|
[HttpPost("update")]
|
|
public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)
|
|
{
|
|
_logger.LogInformation("{UserName} is updating Series {SeriesName}", User.GetUsername(), updateSeries.Name);
|
|
|
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(updateSeries.Id);
|
|
|
|
if (series == null) return BadRequest("Series does not exist");
|
|
|
|
if (series.Name != updateSeries.Name && await _unitOfWork.SeriesRepository.DoesSeriesNameExistInLibrary(updateSeries.Name))
|
|
{
|
|
return BadRequest("A series already exists in this library with this name. Series Names must be unique to a library.");
|
|
}
|
|
series.Name = updateSeries.Name.Trim();
|
|
series.LocalizedName = updateSeries.LocalizedName.Trim();
|
|
series.SortName = updateSeries.SortName.Trim();
|
|
series.Summary = updateSeries.Summary.Trim();
|
|
|
|
var needsRefreshMetadata = false;
|
|
if (!updateSeries.CoverImageLocked)
|
|
{
|
|
series.CoverImageLocked = false;
|
|
needsRefreshMetadata = true;
|
|
}
|
|
|
|
_unitOfWork.SeriesRepository.Update(series);
|
|
|
|
if (await _unitOfWork.CommitAsync())
|
|
{
|
|
if (needsRefreshMetadata)
|
|
{
|
|
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id);
|
|
}
|
|
return Ok();
|
|
}
|
|
|
|
return BadRequest("There was an error with updating the series");
|
|
}
|
|
|
|
[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 series =
|
|
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, user.Id, 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);
|
|
|
|
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
|
|
|
return Ok(series);
|
|
}
|
|
|
|
[HttpPost("in-progress")]
|
|
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 listResults = results.DistinctBy(s => s.Name).Skip((userParams.PageNumber - 1) * userParams.PageSize)
|
|
.Take(userParams.PageSize).ToList();
|
|
var pagedList = new PagedList<SeriesDto>(listResults, listResults.Count, userParams.PageNumber, userParams.PageSize);
|
|
|
|
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
|
|
|
return Ok(pagedList);
|
|
}
|
|
|
|
[Authorize(Policy = "RequireAdminRole")]
|
|
[HttpPost("refresh-metadata")]
|
|
public ActionResult RefreshSeriesMetadata(RefreshSeriesDto refreshSeriesDto)
|
|
{
|
|
_taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId);
|
|
return Ok();
|
|
}
|
|
|
|
[Authorize(Policy = "RequireAdminRole")]
|
|
[HttpPost("scan")]
|
|
public ActionResult ScanSeries(RefreshSeriesDto refreshSeriesDto)
|
|
{
|
|
_taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId);
|
|
return Ok();
|
|
}
|
|
|
|
[HttpGet("metadata")]
|
|
public async Task<ActionResult<SeriesMetadataDto>> GetSeriesMetadata(int seriesId)
|
|
{
|
|
var metadata = await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId);
|
|
return Ok(metadata);
|
|
}
|
|
|
|
[HttpPost("metadata")]
|
|
public async Task<ActionResult> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto)
|
|
{
|
|
try
|
|
{
|
|
var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId;
|
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
|
if (series.Metadata == null)
|
|
{
|
|
series.Metadata = DbFactory.SeriesMetadata(updateSeriesMetadataDto.Tags
|
|
.Select(dto => DbFactory.CollectionTag(dto.Id, dto.Title, dto.Summary, dto.Promoted)).ToList());
|
|
}
|
|
else
|
|
{
|
|
series.Metadata.CollectionTags ??= new List<CollectionTag>();
|
|
var newTags = new List<CollectionTag>();
|
|
|
|
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
|
|
var existingTags = series.Metadata.CollectionTags.ToList();
|
|
foreach (var existing in existingTags)
|
|
{
|
|
if (updateSeriesMetadataDto.Tags.SingleOrDefault(t => t.Id == existing.Id) == null)
|
|
{
|
|
// Remove tag
|
|
series.Metadata.CollectionTags.Remove(existing);
|
|
}
|
|
}
|
|
|
|
// At this point, all tags that aren't in dto have been removed.
|
|
foreach (var tag in updateSeriesMetadataDto.Tags)
|
|
{
|
|
var existingTag = series.Metadata.CollectionTags.SingleOrDefault(t => t.Title == tag.Title);
|
|
if (existingTag != null)
|
|
{
|
|
// Update existingTag
|
|
existingTag.Promoted = tag.Promoted;
|
|
existingTag.Title = tag.Title;
|
|
existingTag.NormalizedTitle = Parser.Parser.Normalize(tag.Title).ToUpper();
|
|
}
|
|
else
|
|
{
|
|
// Add new tag
|
|
newTags.Add(DbFactory.CollectionTag(tag.Id, tag.Title, tag.Summary, tag.Promoted));
|
|
}
|
|
}
|
|
|
|
foreach (var tag in newTags)
|
|
{
|
|
series.Metadata.CollectionTags.Add(tag);
|
|
}
|
|
}
|
|
|
|
if (!_unitOfWork.HasChanges())
|
|
{
|
|
return Ok("No changes to save");
|
|
}
|
|
|
|
if (await _unitOfWork.CommitAsync())
|
|
{
|
|
return Ok("Successfully updated");
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
await _unitOfWork.RollbackAsync();
|
|
}
|
|
|
|
return BadRequest("Could not update metadata");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all Series grouped by the passed Collection Id with Pagination.
|
|
/// </summary>
|
|
/// <param name="collectionId">Collection Id to pull series from</param>
|
|
/// <param name="userParams">Pagination information</param>
|
|
/// <returns></returns>
|
|
[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 series =
|
|
await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, user.Id, 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);
|
|
|
|
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
|
|
|
return Ok(series);
|
|
}
|
|
|
|
|
|
}
|
|
}
|