Logging Enhancements (#1521)
* Recreated Kavita Logging with Serilog instead of Default. This needs to be move out of the appsettings now, to allow auto updater to patch. * Refactored the code to be completely configured via Code rather than appsettings.json. This is a required step for Auto Updating. * Added in the ability to send logs directly to the UI only for users on the log route. Stopping implementation as Alerts page will handle the rest of the implementation. * Fixed up the backup service to not rely on Config from appsettings.json * Tweaked the Logging levels available * Moved everything over to File-scoped namespaces * Moved everything over to File-scoped namespaces * Code cleanup, removed an old migration and changed so debug logging doesn't print sensitive db data * Removed dead code
This commit is contained in:
parent
9f715cc35f
commit
d1a14f7e68
212 changed files with 16599 additions and 16834 deletions
File diff suppressed because it is too large
Load diff
|
@ -4,27 +4,26 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace API.Controllers;
|
||||
|
||||
public class AdminController : BaseApiController
|
||||
{
|
||||
public class AdminController : BaseApiController
|
||||
private readonly UserManager<AppUser> _userManager;
|
||||
|
||||
public AdminController(UserManager<AppUser> userManager)
|
||||
{
|
||||
private readonly UserManager<AppUser> _userManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public AdminController(UserManager<AppUser> userManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an admin exists on the system. This is essentially a check to validate if the system has been setup.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("exists")]
|
||||
public async Task<ActionResult<bool>> AdminExists()
|
||||
{
|
||||
var users = await _userManager.GetUsersInRoleAsync("Admin");
|
||||
return users.Count > 0;
|
||||
}
|
||||
/// <summary>
|
||||
/// Checks if an admin exists on the system. This is essentially a check to validate if the system has been setup.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("exists")]
|
||||
public async Task<ActionResult<bool>> AdminExists()
|
||||
{
|
||||
var users = await _userManager.GetUsersInRoleAsync("Admin");
|
||||
return users.Count > 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class BaseApiController : ControllerBase
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class BaseApiController : ControllerBase
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,151 +13,150 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using VersOne.Epub;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace API.Controllers;
|
||||
|
||||
public class BookController : BaseApiController
|
||||
{
|
||||
public class BookController : BaseApiController
|
||||
private readonly IBookService _bookService;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ICacheService _cacheService;
|
||||
|
||||
public BookController(IBookService bookService,
|
||||
IUnitOfWork unitOfWork, ICacheService cacheService)
|
||||
{
|
||||
private readonly IBookService _bookService;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ICacheService _cacheService;
|
||||
|
||||
public BookController(IBookService bookService,
|
||||
IUnitOfWork unitOfWork, ICacheService cacheService)
|
||||
{
|
||||
_bookService = bookService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_cacheService = cacheService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves information for the PDF and Epub reader
|
||||
/// </summary>
|
||||
/// <remarks>This only applies to Epub or PDF files</remarks>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{chapterId}/book-info")]
|
||||
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
|
||||
{
|
||||
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||
var bookTitle = string.Empty;
|
||||
switch (dto.SeriesFormat)
|
||||
{
|
||||
case MangaFormat.Epub:
|
||||
{
|
||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions);
|
||||
bookTitle = book.Title;
|
||||
break;
|
||||
}
|
||||
case MangaFormat.Pdf:
|
||||
{
|
||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
if (string.IsNullOrEmpty(bookTitle))
|
||||
{
|
||||
// Override with filename
|
||||
bookTitle = Path.GetFileNameWithoutExtension(mangaFile.FilePath);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case MangaFormat.Image:
|
||||
break;
|
||||
case MangaFormat.Archive:
|
||||
break;
|
||||
case MangaFormat.Unknown:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
return Ok(new BookInfoDto()
|
||||
{
|
||||
ChapterNumber = dto.ChapterNumber,
|
||||
VolumeNumber = dto.VolumeNumber,
|
||||
VolumeId = dto.VolumeId,
|
||||
BookTitle = bookTitle,
|
||||
SeriesName = dto.SeriesName,
|
||||
SeriesFormat = dto.SeriesFormat,
|
||||
SeriesId = dto.SeriesId,
|
||||
LibraryId = dto.LibraryId,
|
||||
IsSpecial = dto.IsSpecial,
|
||||
Pages = dto.Pages,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is an entry point to fetch resources from within an epub chapter/book.
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{chapterId}/book-resources")]
|
||||
[ResponseCache(Duration = 60 * 1, Location = ResponseCacheLocation.Client, NoStore = false)]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> GetBookPageResources(int chapterId, [FromQuery] string file)
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter is not valid");
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
|
||||
|
||||
var key = BookService.CleanContentKeys(file);
|
||||
if (!book.Content.AllFiles.ContainsKey(key)) return BadRequest("File was not found in book");
|
||||
|
||||
var bookFile = book.Content.AllFiles[key];
|
||||
var content = await bookFile.ReadContentAsBytesAsync();
|
||||
|
||||
var contentType = BookService.GetContentType(bookFile.ContentType);
|
||||
return File(content, contentType, $"{chapterId}-{file}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will return a list of mappings from ID -> page num. ID will be the xhtml key and page num will be the reading order
|
||||
/// this is used to rewrite anchors in the book text so that we always load properly in our reader.
|
||||
/// </summary>
|
||||
/// <remarks>This is essentially building the table of contents</remarks>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{chapterId}/chapters")]
|
||||
public async Task<ActionResult<ICollection<BookChapterItem>>> GetBookChapters(int chapterId)
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter is not valid");
|
||||
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
try
|
||||
{
|
||||
return Ok(await _bookService.GenerateTableOfContents(chapter));
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This returns a single page within the epub book. All html will be rewritten to be scoped within our reader,
|
||||
/// all css is scoped, etc.
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="page"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{chapterId}/book-page")]
|
||||
public async Task<ActionResult<string>> GetBookPage(int chapterId, [FromQuery] int page)
|
||||
{
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
var path = _cacheService.GetCachedFile(chapter);
|
||||
|
||||
var baseUrl = "//" + Request.Host + Request.PathBase + "/api/";
|
||||
|
||||
try
|
||||
{
|
||||
return Ok(await _bookService.GetBookPage(page, chapterId, path, baseUrl));
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
_bookService = bookService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_cacheService = cacheService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves information for the PDF and Epub reader
|
||||
/// </summary>
|
||||
/// <remarks>This only applies to Epub or PDF files</remarks>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{chapterId}/book-info")]
|
||||
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
|
||||
{
|
||||
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||
var bookTitle = string.Empty;
|
||||
switch (dto.SeriesFormat)
|
||||
{
|
||||
case MangaFormat.Epub:
|
||||
{
|
||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions);
|
||||
bookTitle = book.Title;
|
||||
break;
|
||||
}
|
||||
case MangaFormat.Pdf:
|
||||
{
|
||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
if (string.IsNullOrEmpty(bookTitle))
|
||||
{
|
||||
// Override with filename
|
||||
bookTitle = Path.GetFileNameWithoutExtension(mangaFile.FilePath);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case MangaFormat.Image:
|
||||
break;
|
||||
case MangaFormat.Archive:
|
||||
break;
|
||||
case MangaFormat.Unknown:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
return Ok(new BookInfoDto()
|
||||
{
|
||||
ChapterNumber = dto.ChapterNumber,
|
||||
VolumeNumber = dto.VolumeNumber,
|
||||
VolumeId = dto.VolumeId,
|
||||
BookTitle = bookTitle,
|
||||
SeriesName = dto.SeriesName,
|
||||
SeriesFormat = dto.SeriesFormat,
|
||||
SeriesId = dto.SeriesId,
|
||||
LibraryId = dto.LibraryId,
|
||||
IsSpecial = dto.IsSpecial,
|
||||
Pages = dto.Pages,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is an entry point to fetch resources from within an epub chapter/book.
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{chapterId}/book-resources")]
|
||||
[ResponseCache(Duration = 60 * 1, Location = ResponseCacheLocation.Client, NoStore = false)]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> GetBookPageResources(int chapterId, [FromQuery] string file)
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter is not valid");
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
|
||||
|
||||
var key = BookService.CleanContentKeys(file);
|
||||
if (!book.Content.AllFiles.ContainsKey(key)) return BadRequest("File was not found in book");
|
||||
|
||||
var bookFile = book.Content.AllFiles[key];
|
||||
var content = await bookFile.ReadContentAsBytesAsync();
|
||||
|
||||
var contentType = BookService.GetContentType(bookFile.ContentType);
|
||||
return File(content, contentType, $"{chapterId}-{file}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will return a list of mappings from ID -> page num. ID will be the xhtml key and page num will be the reading order
|
||||
/// this is used to rewrite anchors in the book text so that we always load properly in our reader.
|
||||
/// </summary>
|
||||
/// <remarks>This is essentially building the table of contents</remarks>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{chapterId}/chapters")]
|
||||
public async Task<ActionResult<ICollection<BookChapterItem>>> GetBookChapters(int chapterId)
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter is not valid");
|
||||
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
try
|
||||
{
|
||||
return Ok(await _bookService.GenerateTableOfContents(chapter));
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This returns a single page within the epub book. All html will be rewritten to be scoped within our reader,
|
||||
/// all css is scoped, etc.
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="page"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{chapterId}/book-page")]
|
||||
public async Task<ActionResult<string>> GetBookPage(int chapterId, [FromQuery] int page)
|
||||
{
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
var path = _cacheService.GetCachedFile(chapter);
|
||||
|
||||
var baseUrl = "//" + Request.Host + Request.PathBase + "/api/";
|
||||
|
||||
try
|
||||
{
|
||||
return Ok(await _bookService.GetBookPage(page, chapterId, path, baseUrl));
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,182 +11,181 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// APIs for Collections
|
||||
/// </summary>
|
||||
public class CollectionController : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// APIs for Collections
|
||||
/// </summary>
|
||||
public class CollectionController : BaseApiController
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _eventHub;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CollectionController(IUnitOfWork unitOfWork, IEventHub eventHub)
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _eventHub;
|
||||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CollectionController(IUnitOfWork unitOfWork, IEventHub eventHub)
|
||||
/// <summary>
|
||||
/// Return a list of all collection tags on the server
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<IEnumerable<CollectionTagDto>> GetAllTags()
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
if (isAdmin)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
||||
}
|
||||
return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a list of all collection tags on the server
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<IEnumerable<CollectionTagDto>> GetAllTags()
|
||||
/// <summary>
|
||||
/// Searches against the collection tags on the DB and returns matches that meet the search criteria.
|
||||
/// <remarks>Search strings will be cleaned of certain fields, like %</remarks>
|
||||
/// </summary>
|
||||
/// <param name="queryString">Search term</param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("search")]
|
||||
public async Task<IEnumerable<CollectionTagDto>> SearchTags(string queryString)
|
||||
{
|
||||
queryString ??= "";
|
||||
queryString = queryString.Replace(@"%", string.Empty);
|
||||
if (queryString.Length == 0) return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
||||
|
||||
return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing tag with a new title, promotion status, and summary.
|
||||
/// <remarks>UI does not contain controls to update title</remarks>
|
||||
/// </summary>
|
||||
/// <param name="updatedTag"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> UpdateTagPromotion(CollectionTagDto updatedTag)
|
||||
{
|
||||
var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updatedTag.Id);
|
||||
if (existingTag == null) return BadRequest("This tag does not exist");
|
||||
|
||||
existingTag.Promoted = updatedTag.Promoted;
|
||||
existingTag.Title = updatedTag.Title.Trim();
|
||||
existingTag.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(updatedTag.Title).ToUpper();
|
||||
existingTag.Summary = updatedTag.Summary.Trim();
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
if (isAdmin)
|
||||
{
|
||||
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
||||
}
|
||||
return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches against the collection tags on the DB and returns matches that meet the search criteria.
|
||||
/// <remarks>Search strings will be cleaned of certain fields, like %</remarks>
|
||||
/// </summary>
|
||||
/// <param name="queryString">Search term</param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("search")]
|
||||
public async Task<IEnumerable<CollectionTagDto>> SearchTags(string queryString)
|
||||
{
|
||||
queryString ??= "";
|
||||
queryString = queryString.Replace(@"%", string.Empty);
|
||||
if (queryString.Length == 0) return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
||||
|
||||
return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing tag with a new title, promotion status, and summary.
|
||||
/// <remarks>UI does not contain controls to update title</remarks>
|
||||
/// </summary>
|
||||
/// <param name="updatedTag"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> UpdateTagPromotion(CollectionTagDto updatedTag)
|
||||
{
|
||||
var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updatedTag.Id);
|
||||
if (existingTag == null) return BadRequest("This tag does not exist");
|
||||
|
||||
existingTag.Promoted = updatedTag.Promoted;
|
||||
existingTag.Title = updatedTag.Title.Trim();
|
||||
existingTag.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(updatedTag.Title).ToUpper();
|
||||
existingTag.Summary = updatedTag.Summary.Trim();
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok("Tag updated successfully");
|
||||
}
|
||||
}
|
||||
else
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok("Tag updated successfully");
|
||||
}
|
||||
|
||||
return BadRequest("Something went wrong, please try again");
|
||||
}
|
||||
else
|
||||
{
|
||||
return Ok("Tag updated successfully");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a collection tag onto multiple Series. If tag id is 0, this will create a new tag.
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("update-for-series")]
|
||||
public async Task<ActionResult> AddToMultipleSeries(CollectionTagBulkAddDto dto)
|
||||
return BadRequest("Something went wrong, please try again");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a collection tag onto multiple Series. If tag id is 0, this will create a new tag.
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("update-for-series")]
|
||||
public async Task<ActionResult> AddToMultipleSeries(CollectionTagBulkAddDto dto)
|
||||
{
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(dto.CollectionTagId);
|
||||
if (tag == null)
|
||||
{
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(dto.CollectionTagId);
|
||||
if (tag == null)
|
||||
tag = DbFactory.CollectionTag(0, dto.CollectionTagTitle, String.Empty, false);
|
||||
_unitOfWork.CollectionTagRepository.Add(tag);
|
||||
}
|
||||
|
||||
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIdsAsync(dto.SeriesIds);
|
||||
foreach (var metadata in seriesMetadatas)
|
||||
{
|
||||
if (!metadata.CollectionTags.Any(t => t.Title.Equals(tag.Title, StringComparison.InvariantCulture)))
|
||||
{
|
||||
tag = DbFactory.CollectionTag(0, dto.CollectionTagTitle, String.Empty, false);
|
||||
_unitOfWork.CollectionTagRepository.Add(tag);
|
||||
metadata.CollectionTags.Add(tag);
|
||||
_unitOfWork.SeriesMetadataRepository.Update(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
return BadRequest("There was an issue updating series with collection tag");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a given tag, update the summary if summary has changed and remove a set of series from the tag.
|
||||
/// </summary>
|
||||
/// <param name="updateSeriesForTagDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("update-series")]
|
||||
public async Task<ActionResult> UpdateSeriesForTag(UpdateSeriesForTagDto updateSeriesForTagDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(updateSeriesForTagDto.Tag.Id);
|
||||
if (tag == null) return BadRequest("Not a valid Tag");
|
||||
tag.SeriesMetadatas ??= new List<SeriesMetadata>();
|
||||
|
||||
// Check if Tag has updated (Summary)
|
||||
if (tag.Summary == null || !tag.Summary.Equals(updateSeriesForTagDto.Tag.Summary))
|
||||
{
|
||||
tag.Summary = updateSeriesForTagDto.Tag.Summary;
|
||||
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||
}
|
||||
|
||||
tag.CoverImageLocked = updateSeriesForTagDto.Tag.CoverImageLocked;
|
||||
|
||||
if (!updateSeriesForTagDto.Tag.CoverImageLocked)
|
||||
{
|
||||
tag.CoverImageLocked = false;
|
||||
tag.CoverImage = string.Empty;
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
|
||||
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||
}
|
||||
|
||||
foreach (var seriesIdToRemove in updateSeriesForTagDto.SeriesIdsToRemove)
|
||||
{
|
||||
tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove));
|
||||
}
|
||||
|
||||
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIdsAsync(dto.SeriesIds);
|
||||
foreach (var metadata in seriesMetadatas)
|
||||
if (tag.SeriesMetadatas.Count == 0)
|
||||
{
|
||||
if (!metadata.CollectionTags.Any(t => t.Title.Equals(tag.Title, StringComparison.InvariantCulture)))
|
||||
{
|
||||
metadata.CollectionTags.Add(tag);
|
||||
_unitOfWork.SeriesMetadataRepository.Update(metadata);
|
||||
}
|
||||
_unitOfWork.CollectionTagRepository.Remove(tag);
|
||||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
if (!_unitOfWork.HasChanges()) return Ok("No updates");
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok();
|
||||
return Ok("Tag updated");
|
||||
}
|
||||
return BadRequest("There was an issue updating series with collection tag");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a given tag, update the summary if summary has changed and remove a set of series from the tag.
|
||||
/// </summary>
|
||||
/// <param name="updateSeriesForTagDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("update-series")]
|
||||
public async Task<ActionResult> UpdateSeriesForTag(UpdateSeriesForTagDto updateSeriesForTagDto)
|
||||
catch (Exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(updateSeriesForTagDto.Tag.Id);
|
||||
if (tag == null) return BadRequest("Not a valid Tag");
|
||||
tag.SeriesMetadatas ??= new List<SeriesMetadata>();
|
||||
|
||||
// Check if Tag has updated (Summary)
|
||||
if (tag.Summary == null || !tag.Summary.Equals(updateSeriesForTagDto.Tag.Summary))
|
||||
{
|
||||
tag.Summary = updateSeriesForTagDto.Tag.Summary;
|
||||
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||
}
|
||||
|
||||
tag.CoverImageLocked = updateSeriesForTagDto.Tag.CoverImageLocked;
|
||||
|
||||
if (!updateSeriesForTagDto.Tag.CoverImageLocked)
|
||||
{
|
||||
tag.CoverImageLocked = false;
|
||||
tag.CoverImage = string.Empty;
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
|
||||
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||
}
|
||||
|
||||
foreach (var seriesIdToRemove in updateSeriesForTagDto.SeriesIdsToRemove)
|
||||
{
|
||||
tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove));
|
||||
}
|
||||
|
||||
|
||||
if (tag.SeriesMetadatas.Count == 0)
|
||||
{
|
||||
_unitOfWork.CollectionTagRepository.Remove(tag);
|
||||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok("No updates");
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok("Tag updated");
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
|
||||
return BadRequest("Something went wrong. Please try again.");
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
|
||||
return BadRequest("Something went wrong. Please try again.");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,210 +16,209 @@ using Microsoft.AspNetCore.Identity;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// All APIs related to downloading entities from the system. Requires Download Role or Admin Role.
|
||||
/// </summary>
|
||||
[Authorize(Policy="RequireDownloadRole")]
|
||||
public class DownloadController : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// All APIs related to downloading entities from the system. Requires Download Role or Admin Role.
|
||||
/// </summary>
|
||||
[Authorize(Policy="RequireDownloadRole")]
|
||||
public class DownloadController : BaseApiController
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IArchiveService _archiveService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IDownloadService _downloadService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILogger<DownloadController> _logger;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IAccountService _accountService;
|
||||
private const string DefaultContentType = "application/octet-stream";
|
||||
|
||||
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService,
|
||||
IDownloadService downloadService, IEventHub eventHub, ILogger<DownloadController> logger, IBookmarkService bookmarkService,
|
||||
IAccountService accountService)
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IArchiveService _archiveService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IDownloadService _downloadService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILogger<DownloadController> _logger;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IAccountService _accountService;
|
||||
private const string DefaultContentType = "application/octet-stream";
|
||||
|
||||
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService,
|
||||
IDownloadService downloadService, IEventHub eventHub, ILogger<DownloadController> logger, IBookmarkService bookmarkService,
|
||||
IAccountService accountService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_archiveService = archiveService;
|
||||
_directoryService = directoryService;
|
||||
_downloadService = downloadService;
|
||||
_eventHub = eventHub;
|
||||
_logger = logger;
|
||||
_bookmarkService = bookmarkService;
|
||||
_accountService = accountService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a given volume, return the size in bytes
|
||||
/// </summary>
|
||||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("volume-size")]
|
||||
public async Task<ActionResult<long>> GetVolumeSize(int volumeId)
|
||||
{
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
|
||||
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a given chapter, return the size in bytes
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter-size")]
|
||||
public async Task<ActionResult<long>> GetChapterSize(int chapterId)
|
||||
{
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a series, return the size in bytes
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("series-size")]
|
||||
public async Task<ActionResult<long>> GetSeriesSize(int seriesId)
|
||||
{
|
||||
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
||||
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Downloads all chapters within a volume. If the chapters are multiple zips, they will all be zipped up.
|
||||
/// </summary>
|
||||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy="RequireDownloadRole")]
|
||||
[HttpGet("volume")]
|
||||
public async Task<ActionResult> DownloadVolume(int volumeId)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
try
|
||||
{
|
||||
return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series.Name} - Volume {volume.Number}.zip");
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> HasDownloadPermission()
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
return await _accountService.HasDownloadPermission(user);
|
||||
}
|
||||
|
||||
private ActionResult GetFirstFileDownload(IEnumerable<MangaFile> files)
|
||||
{
|
||||
var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files);
|
||||
return PhysicalFile(zipFile, contentType, fileDownloadName, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the zip for a single chapter. If the chapter contains multiple files, they will be zipped.
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter")]
|
||||
public async Task<ActionResult> DownloadChapter(int chapterId)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
try
|
||||
{
|
||||
return await DownloadFiles(files, $"download_{User.GetUsername()}_c{chapterId}", $"{series.Name} - Chapter {chapter.Number}.zip");
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ActionResult> DownloadFiles(ICollection<MangaFile> files, string tempFolder, string downloadName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
||||
Path.GetFileNameWithoutExtension(downloadName), 0F, "started"));
|
||||
if (files.Count == 1)
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
||||
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
|
||||
return GetFirstFileDownload(files);
|
||||
}
|
||||
|
||||
var filePath = _archiveService.CreateZipForDownload(files.Select(c => c.FilePath), tempFolder);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
||||
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
|
||||
return PhysicalFile(filePath, DefaultContentType, downloadName, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an exception when trying to download files");
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
||||
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("series")]
|
||||
public async Task<ActionResult> DownloadSeries(int seriesId)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||
try
|
||||
{
|
||||
return await DownloadFiles(files, $"download_{User.GetUsername()}_s{seriesId}", $"{series.Name}.zip");
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads all bookmarks in a zip for
|
||||
/// </summary>
|
||||
/// <param name="downloadBookmarkDto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("bookmarks")]
|
||||
public async Task<ActionResult> DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest("Bookmarks cannot be empty");
|
||||
|
||||
// We know that all bookmarks will be for one single seriesId
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId);
|
||||
|
||||
var files = await _bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id));
|
||||
|
||||
var filename = $"{series.Name} - Bookmarks.zip";
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 0F));
|
||||
var seriesIds = string.Join("_", downloadBookmarkDto.Bookmarks.Select(b => b.SeriesId).Distinct());
|
||||
var filePath = _archiveService.CreateZipForDownload(files,
|
||||
$"download_{user.Id}_{seriesIds}_bookmarks");
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 1F));
|
||||
|
||||
|
||||
return PhysicalFile(filePath, DefaultContentType, filename, true);
|
||||
}
|
||||
|
||||
_unitOfWork = unitOfWork;
|
||||
_archiveService = archiveService;
|
||||
_directoryService = directoryService;
|
||||
_downloadService = downloadService;
|
||||
_eventHub = eventHub;
|
||||
_logger = logger;
|
||||
_bookmarkService = bookmarkService;
|
||||
_accountService = accountService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a given volume, return the size in bytes
|
||||
/// </summary>
|
||||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("volume-size")]
|
||||
public async Task<ActionResult<long>> GetVolumeSize(int volumeId)
|
||||
{
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
|
||||
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a given chapter, return the size in bytes
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter-size")]
|
||||
public async Task<ActionResult<long>> GetChapterSize(int chapterId)
|
||||
{
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a series, return the size in bytes
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("series-size")]
|
||||
public async Task<ActionResult<long>> GetSeriesSize(int seriesId)
|
||||
{
|
||||
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
||||
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Downloads all chapters within a volume. If the chapters are multiple zips, they will all be zipped up.
|
||||
/// </summary>
|
||||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy="RequireDownloadRole")]
|
||||
[HttpGet("volume")]
|
||||
public async Task<ActionResult> DownloadVolume(int volumeId)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
try
|
||||
{
|
||||
return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series.Name} - Volume {volume.Number}.zip");
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> HasDownloadPermission()
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
return await _accountService.HasDownloadPermission(user);
|
||||
}
|
||||
|
||||
private ActionResult GetFirstFileDownload(IEnumerable<MangaFile> files)
|
||||
{
|
||||
var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files);
|
||||
return PhysicalFile(zipFile, contentType, fileDownloadName, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the zip for a single chapter. If the chapter contains multiple files, they will be zipped.
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter")]
|
||||
public async Task<ActionResult> DownloadChapter(int chapterId)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
try
|
||||
{
|
||||
return await DownloadFiles(files, $"download_{User.GetUsername()}_c{chapterId}", $"{series.Name} - Chapter {chapter.Number}.zip");
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ActionResult> DownloadFiles(ICollection<MangaFile> files, string tempFolder, string downloadName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
||||
Path.GetFileNameWithoutExtension(downloadName), 0F, "started"));
|
||||
if (files.Count == 1)
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
||||
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
|
||||
return GetFirstFileDownload(files);
|
||||
}
|
||||
|
||||
var filePath = _archiveService.CreateZipForDownload(files.Select(c => c.FilePath), tempFolder);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
||||
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
|
||||
return PhysicalFile(filePath, DefaultContentType, downloadName, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an exception when trying to download files");
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
||||
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("series")]
|
||||
public async Task<ActionResult> DownloadSeries(int seriesId)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||
try
|
||||
{
|
||||
return await DownloadFiles(files, $"download_{User.GetUsername()}_s{seriesId}", $"{series.Name}.zip");
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads all bookmarks in a zip for
|
||||
/// </summary>
|
||||
/// <param name="downloadBookmarkDto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("bookmarks")]
|
||||
public async Task<ActionResult> DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest("Bookmarks cannot be empty");
|
||||
|
||||
// We know that all bookmarks will be for one single seriesId
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId);
|
||||
|
||||
var files = await _bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id));
|
||||
|
||||
var filename = $"{series.Name} - Bookmarks.zip";
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 0F));
|
||||
var seriesIds = string.Join("_", downloadBookmarkDto.Bookmarks.Select(b => b.SeriesId).Distinct());
|
||||
var filePath = _archiveService.CreateZipForDownload(files,
|
||||
$"download_{user.Id}_{seriesIds}_bookmarks");
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 1F));
|
||||
|
||||
|
||||
return PhysicalFile(filePath, DefaultContentType, filename, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,147 +7,146 @@ using API.Services;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for servicing up images stored in Kavita for entities
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
public class ImageController : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// Responsible for servicing up images stored in Kavita for entities
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
public class ImageController : BaseApiController
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService)
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns cover image for Chapter
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter-cover")]
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
public async Task<ActionResult> GetChapterCoverImage(int chapterId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
|
||||
|
||||
/// <summary>
|
||||
/// Returns cover image for Chapter
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter-cover")]
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
public async Task<ActionResult> GetChapterCoverImage(int chapterId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns cover image for Volume
|
||||
/// </summary>
|
||||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("volume-cover")]
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
public async Task<ActionResult> GetVolumeCoverImage(int volumeId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
|
||||
|
||||
/// <summary>
|
||||
/// Returns cover image for Volume
|
||||
/// </summary>
|
||||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("volume-cover")]
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
public async Task<ActionResult> GetVolumeCoverImage(int volumeId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns cover image for Series
|
||||
/// </summary>
|
||||
/// <param name="seriesId">Id of Series</param>
|
||||
/// <returns></returns>
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
[HttpGet("series-cover")]
|
||||
public async Task<ActionResult> GetSeriesCoverImage(int seriesId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
|
||||
|
||||
/// <summary>
|
||||
/// Returns cover image for Series
|
||||
/// </summary>
|
||||
/// <param name="seriesId">Id of Series</param>
|
||||
/// <returns></returns>
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
[HttpGet("series-cover")]
|
||||
public async Task<ActionResult> GetSeriesCoverImage(int seriesId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
|
||||
Response.AddCacheHeader(path);
|
||||
|
||||
Response.AddCacheHeader(path);
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns cover image for Collection Tag
|
||||
/// </summary>
|
||||
/// <param name="collectionTagId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("collection-cover")]
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
|
||||
|
||||
/// <summary>
|
||||
/// Returns cover image for Collection Tag
|
||||
/// </summary>
|
||||
/// <param name="collectionTagId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("collection-cover")]
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns cover image for a Reading List
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("readinglist-cover")]
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
public async Task<ActionResult> GetReadingListCoverImage(int readingListId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
|
||||
|
||||
/// <summary>
|
||||
/// Returns cover image for a Reading List
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("readinglist-cover")]
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
public async Task<ActionResult> GetReadingListCoverImage(int readingListId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns image for a given bookmark page
|
||||
/// </summary>
|
||||
/// <remarks>This request is served unauthenticated, but user must be passed via api key to validate</remarks>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="pageNum">Starts at 0</param>
|
||||
/// <param name="apiKey">API Key for user. Needed to authenticate request</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("bookmark")]
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
public async Task<ActionResult> GetBookmarkImage(int chapterId, int pageNum, string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
var bookmark = await _unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, userId);
|
||||
if (bookmark == null) return BadRequest("Bookmark does not exist");
|
||||
|
||||
/// <summary>
|
||||
/// Returns image for a given bookmark page
|
||||
/// </summary>
|
||||
/// <remarks>This request is served unauthenticated, but user must be passed via api key to validate</remarks>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="pageNum">Starts at 0</param>
|
||||
/// <param name="apiKey">API Key for user. Needed to authenticate request</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("bookmark")]
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
public async Task<ActionResult> GetBookmarkImage(int chapterId, int pageNum, string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
var bookmark = await _unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, userId);
|
||||
if (bookmark == null) return BadRequest("Bookmark does not exist");
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
var file = new FileInfo(Path.Join(bookmarkDirectory, bookmark.FileName));
|
||||
var format = Path.GetExtension(file.FullName).Replace(".", "");
|
||||
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
var file = new FileInfo(Path.Join(bookmarkDirectory, bookmark.FileName));
|
||||
var format = Path.GetExtension(file.FullName).Replace(".", "");
|
||||
return PhysicalFile(file.FullName, "image/" + format, Path.GetFileName(file.FullName));
|
||||
}
|
||||
|
||||
return PhysicalFile(file.FullName, "image/" + format, Path.GetFileName(file.FullName));
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns a temp coverupload image
|
||||
/// </summary>
|
||||
/// <param name="filename">Filename of file. This is used with upload/upload-by-url</param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy="RequireAdminRole")]
|
||||
[HttpGet("cover-upload")]
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
public ActionResult GetCoverUploadImage(string filename)
|
||||
{
|
||||
if (filename.Contains("..")) return BadRequest("Invalid Filename");
|
||||
|
||||
/// <summary>
|
||||
/// Returns a temp coverupload image
|
||||
/// </summary>
|
||||
/// <param name="filename">Filename of file. This is used with upload/upload-by-url</param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy="RequireAdminRole")]
|
||||
[HttpGet("cover-upload")]
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
public ActionResult GetCoverUploadImage(string filename)
|
||||
{
|
||||
if (filename.Contains("..")) return BadRequest("Invalid Filename");
|
||||
var path = Path.Join(_directoryService.TempDirectory, filename);
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"File does not exist");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
|
||||
|
||||
var path = Path.Join(_directoryService.TempDirectory, filename);
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"File does not exist");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
|
||||
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,323 +22,322 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using TaskScheduler = API.Services.TaskScheduler;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace API.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class LibraryController : BaseApiController
|
||||
{
|
||||
[Authorize]
|
||||
public class LibraryController : BaseApiController
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly ILogger<LibraryController> _logger;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILibraryWatcher _libraryWatcher;
|
||||
|
||||
public LibraryController(IDirectoryService directoryService,
|
||||
ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler,
|
||||
IUnitOfWork unitOfWork, IEventHub eventHub, ILibraryWatcher libraryWatcher)
|
||||
{
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly ILogger<LibraryController> _logger;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILibraryWatcher _libraryWatcher;
|
||||
_directoryService = directoryService;
|
||||
_logger = logger;
|
||||
_mapper = mapper;
|
||||
_taskScheduler = taskScheduler;
|
||||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
_libraryWatcher = libraryWatcher;
|
||||
}
|
||||
|
||||
public LibraryController(IDirectoryService directoryService,
|
||||
ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler,
|
||||
IUnitOfWork unitOfWork, IEventHub eventHub, ILibraryWatcher libraryWatcher)
|
||||
/// <summary>
|
||||
/// Creates a new Library. Upon library creation, adds new library to all Admin accounts.
|
||||
/// </summary>
|
||||
/// <param name="createLibraryDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("create")]
|
||||
public async Task<ActionResult> AddLibrary(CreateLibraryDto createLibraryDto)
|
||||
{
|
||||
if (await _unitOfWork.LibraryRepository.LibraryExists(createLibraryDto.Name))
|
||||
{
|
||||
_directoryService = directoryService;
|
||||
_logger = logger;
|
||||
_mapper = mapper;
|
||||
_taskScheduler = taskScheduler;
|
||||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
_libraryWatcher = libraryWatcher;
|
||||
return BadRequest("Library name already exists. Please choose a unique name to the server.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Library. Upon library creation, adds new library to all Admin accounts.
|
||||
/// </summary>
|
||||
/// <param name="createLibraryDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("create")]
|
||||
public async Task<ActionResult> AddLibrary(CreateLibraryDto createLibraryDto)
|
||||
var library = new Library
|
||||
{
|
||||
if (await _unitOfWork.LibraryRepository.LibraryExists(createLibraryDto.Name))
|
||||
Name = createLibraryDto.Name,
|
||||
Type = createLibraryDto.Type,
|
||||
Folders = createLibraryDto.Folders.Select(x => new FolderPath {Path = x}).ToList()
|
||||
};
|
||||
|
||||
_unitOfWork.LibraryRepository.Add(library);
|
||||
|
||||
var admins = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).ToList();
|
||||
foreach (var admin in admins)
|
||||
{
|
||||
admin.Libraries ??= new List<Library>();
|
||||
admin.Libraries.Add(library);
|
||||
}
|
||||
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue. Please try again.");
|
||||
|
||||
_logger.LogInformation("Created a new library: {LibraryName}", library.Name);
|
||||
await _libraryWatcher.RestartWatching();
|
||||
_taskScheduler.ScanLibrary(library.Id);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
|
||||
MessageFactory.LibraryModifiedEvent(library.Id, "create"), false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of directories for a given path. If path is empty, returns root drives.
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("list")]
|
||||
public ActionResult<IEnumerable<DirectoryDto>> GetDirectories(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return Ok(Directory.GetLogicalDrives().Select(d => new DirectoryDto()
|
||||
{
|
||||
return BadRequest("Library name already exists. Please choose a unique name to the server.");
|
||||
Name = d,
|
||||
FullPath = d
|
||||
}));
|
||||
}
|
||||
|
||||
if (!Directory.Exists(path)) return BadRequest("This is not a valid path");
|
||||
|
||||
return Ok(_directoryService.ListDirectory(path));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries()
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosAsync());
|
||||
}
|
||||
|
||||
[HttpGet("jump-bar")]
|
||||
public async Task<ActionResult<IEnumerable<JumpKeyDto>>> GetJumpBar(int libraryId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, userId)) return BadRequest("User does not have access to library");
|
||||
|
||||
return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId));
|
||||
}
|
||||
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("grant-access")]
|
||||
public async Task<ActionResult<MemberDto>> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username);
|
||||
if (user == null) return BadRequest("Could not validate user");
|
||||
|
||||
var libraryString = string.Join(",", updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name));
|
||||
_logger.LogInformation("Granting user {UserName} access to: {Libraries}", updateLibraryForUserDto.Username, libraryString);
|
||||
|
||||
var allLibraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
|
||||
foreach (var library in allLibraries)
|
||||
{
|
||||
library.AppUsers ??= new List<AppUser>();
|
||||
var libraryContainsUser = library.AppUsers.Any(u => u.UserName == user.UserName);
|
||||
var libraryIsSelected = updateLibraryForUserDto.SelectedLibraries.Any(l => l.Id == library.Id);
|
||||
if (libraryContainsUser && !libraryIsSelected)
|
||||
{
|
||||
// Remove
|
||||
library.AppUsers.Remove(user);
|
||||
}
|
||||
else if (!libraryContainsUser && libraryIsSelected)
|
||||
{
|
||||
library.AppUsers.Add(user);
|
||||
}
|
||||
|
||||
var library = new Library
|
||||
{
|
||||
Name = createLibraryDto.Name,
|
||||
Type = createLibraryDto.Type,
|
||||
Folders = createLibraryDto.Folders.Select(x => new FolderPath {Path = x}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
_unitOfWork.LibraryRepository.Add(library);
|
||||
if (!_unitOfWork.HasChanges())
|
||||
{
|
||||
_logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username);
|
||||
return Ok(_mapper.Map<MemberDto>(user));
|
||||
}
|
||||
|
||||
var admins = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).ToList();
|
||||
foreach (var admin in admins)
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
_logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username);
|
||||
return Ok(_mapper.Map<MemberDto>(user));
|
||||
}
|
||||
|
||||
|
||||
return BadRequest("There was a critical issue. Please try again.");
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("scan")]
|
||||
public ActionResult Scan(int libraryId, bool force = false)
|
||||
{
|
||||
_taskScheduler.ScanLibrary(libraryId, force);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("refresh-metadata")]
|
||||
public ActionResult RefreshMetadata(int libraryId, bool force = true)
|
||||
{
|
||||
_taskScheduler.RefreshMetadata(libraryId, force);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("analyze")]
|
||||
public ActionResult Analyze(int libraryId)
|
||||
{
|
||||
_taskScheduler.AnalyzeFilesForLibrary(libraryId, true);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("libraries")]
|
||||
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibrariesForUser()
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a valid path, will invoke either a Scan Series or Scan Library. If the folder does not exist within Kavita, the request will be ignored
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("scan-folder")]
|
||||
public async Task<ActionResult> ScanFolder(ScanFolderDto dto)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(dto.ApiKey);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
// Validate user has Admin privileges
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
if (!isAdmin) return BadRequest("API key must belong to an admin");
|
||||
if (dto.FolderPath.Contains("..")) return BadRequest("Invalid Path");
|
||||
|
||||
dto.FolderPath = Services.Tasks.Scanner.Parser.Parser.NormalizePath(dto.FolderPath);
|
||||
|
||||
var libraryFolder = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync())
|
||||
.SelectMany(l => l.Folders)
|
||||
.Distinct()
|
||||
.Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath);
|
||||
|
||||
var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder,
|
||||
new List<string>() {dto.FolderPath});
|
||||
|
||||
_taskScheduler.ScanFolder(seriesFolder.Keys.Count == 1 ? seriesFolder.Keys.First() : dto.FolderPath);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpDelete("delete")]
|
||||
public async Task<ActionResult<bool>> DeleteLibrary(int libraryId)
|
||||
{
|
||||
var username = User.GetUsername();
|
||||
_logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, username);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId);
|
||||
var seriesIds = series.Select(x => x.Id).ToArray();
|
||||
var chapterIds =
|
||||
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(seriesIds);
|
||||
|
||||
try
|
||||
{
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
|
||||
if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId))
|
||||
{
|
||||
admin.Libraries ??= new List<Library>();
|
||||
admin.Libraries.Add(library);
|
||||
// TODO: Figure out how to cancel a job
|
||||
_logger.LogInformation("User is attempting to delete a library while a scan is in progress");
|
||||
return BadRequest(
|
||||
"You cannot delete a library while a scan is in progress. Please wait for scan to continue then try to delete");
|
||||
}
|
||||
_unitOfWork.LibraryRepository.Delete(library);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue. Please try again.");
|
||||
|
||||
_logger.LogInformation("Created a new library: {LibraryName}", library.Name);
|
||||
await _libraryWatcher.RestartWatching();
|
||||
_taskScheduler.ScanLibrary(library.Id);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
|
||||
MessageFactory.LibraryModifiedEvent(library.Id, "create"), false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of directories for a given path. If path is empty, returns root drives.
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("list")]
|
||||
public ActionResult<IEnumerable<DirectoryDto>> GetDirectories(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
if (chapterIds.Any())
|
||||
{
|
||||
return Ok(Directory.GetLogicalDrives().Select(d => new DirectoryDto()
|
||||
{
|
||||
Name = d,
|
||||
FullPath = d
|
||||
}));
|
||||
}
|
||||
|
||||
if (!Directory.Exists(path)) return BadRequest("This is not a valid path");
|
||||
|
||||
return Ok(_directoryService.ListDirectory(path));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries()
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosAsync());
|
||||
}
|
||||
|
||||
[HttpGet("jump-bar")]
|
||||
public async Task<ActionResult<IEnumerable<JumpKeyDto>>> GetJumpBar(int libraryId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, userId)) return BadRequest("User does not have access to library");
|
||||
|
||||
return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId));
|
||||
}
|
||||
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("grant-access")]
|
||||
public async Task<ActionResult<MemberDto>> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username);
|
||||
if (user == null) return BadRequest("Could not validate user");
|
||||
|
||||
var libraryString = string.Join(",", updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name));
|
||||
_logger.LogInformation("Granting user {UserName} access to: {Libraries}", updateLibraryForUserDto.Username, libraryString);
|
||||
|
||||
var allLibraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
|
||||
foreach (var library in allLibraries)
|
||||
{
|
||||
library.AppUsers ??= new List<AppUser>();
|
||||
var libraryContainsUser = library.AppUsers.Any(u => u.UserName == user.UserName);
|
||||
var libraryIsSelected = updateLibraryForUserDto.SelectedLibraries.Any(l => l.Id == library.Id);
|
||||
if (libraryContainsUser && !libraryIsSelected)
|
||||
{
|
||||
// Remove
|
||||
library.AppUsers.Remove(user);
|
||||
}
|
||||
else if (!libraryContainsUser && libraryIsSelected)
|
||||
{
|
||||
library.AppUsers.Add(user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges())
|
||||
{
|
||||
_logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username);
|
||||
return Ok(_mapper.Map<MemberDto>(user));
|
||||
}
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
_logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username);
|
||||
return Ok(_mapper.Map<MemberDto>(user));
|
||||
}
|
||||
|
||||
|
||||
return BadRequest("There was a critical issue. Please try again.");
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("scan")]
|
||||
public ActionResult Scan(int libraryId, bool force = false)
|
||||
{
|
||||
_taskScheduler.ScanLibrary(libraryId, force);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("refresh-metadata")]
|
||||
public ActionResult RefreshMetadata(int libraryId, bool force = true)
|
||||
{
|
||||
_taskScheduler.RefreshMetadata(libraryId, force);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("analyze")]
|
||||
public ActionResult Analyze(int libraryId)
|
||||
{
|
||||
_taskScheduler.AnalyzeFilesForLibrary(libraryId, true);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("libraries")]
|
||||
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibrariesForUser()
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a valid path, will invoke either a Scan Series or Scan Library. If the folder does not exist within Kavita, the request will be ignored
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("scan-folder")]
|
||||
public async Task<ActionResult> ScanFolder(ScanFolderDto dto)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(dto.ApiKey);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
// Validate user has Admin privileges
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
if (!isAdmin) return BadRequest("API key must belong to an admin");
|
||||
if (dto.FolderPath.Contains("..")) return BadRequest("Invalid Path");
|
||||
|
||||
dto.FolderPath = Services.Tasks.Scanner.Parser.Parser.NormalizePath(dto.FolderPath);
|
||||
|
||||
var libraryFolder = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync())
|
||||
.SelectMany(l => l.Folders)
|
||||
.Distinct()
|
||||
.Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath);
|
||||
|
||||
var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder,
|
||||
new List<string>() {dto.FolderPath});
|
||||
|
||||
_taskScheduler.ScanFolder(seriesFolder.Keys.Count == 1 ? seriesFolder.Keys.First() : dto.FolderPath);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpDelete("delete")]
|
||||
public async Task<ActionResult<bool>> DeleteLibrary(int libraryId)
|
||||
{
|
||||
var username = User.GetUsername();
|
||||
_logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, username);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId);
|
||||
var seriesIds = series.Select(x => x.Id).ToArray();
|
||||
var chapterIds =
|
||||
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(seriesIds);
|
||||
|
||||
try
|
||||
{
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
|
||||
if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId))
|
||||
{
|
||||
// TODO: Figure out how to cancel a job
|
||||
_logger.LogInformation("User is attempting to delete a library while a scan is in progress");
|
||||
return BadRequest(
|
||||
"You cannot delete a library while a scan is in progress. Please wait for scan to continue then try to delete");
|
||||
}
|
||||
_unitOfWork.LibraryRepository.Delete(library);
|
||||
await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters();
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
if (chapterIds.Any())
|
||||
{
|
||||
await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters();
|
||||
await _unitOfWork.CommitAsync();
|
||||
_taskScheduler.CleanupChapters(chapterIds);
|
||||
}
|
||||
|
||||
await _libraryWatcher.RestartWatching();
|
||||
|
||||
foreach (var seriesId in seriesIds)
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved,
|
||||
MessageFactory.SeriesRemovedEvent(seriesId, string.Empty, libraryId), false);
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
|
||||
MessageFactory.LibraryModifiedEvent(libraryId, "delete"), false);
|
||||
return Ok(true);
|
||||
_taskScheduler.CleanupChapters(chapterIds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
await _libraryWatcher.RestartWatching();
|
||||
|
||||
foreach (var seriesId in seriesIds)
|
||||
{
|
||||
_logger.LogError(ex, "There was a critical error trying to delete the library");
|
||||
await _unitOfWork.RollbackAsync();
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing Library with new name, folders, and/or type.
|
||||
/// </summary>
|
||||
/// <remarks>Any folder or type change will invoke a scan.</remarks>
|
||||
/// <param name="libraryForUserDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto libraryForUserDto)
|
||||
{
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id, LibraryIncludes.Folders);
|
||||
|
||||
var originalFolders = library.Folders.Select(x => x.Path).ToList();
|
||||
|
||||
library.Name = libraryForUserDto.Name;
|
||||
library.Folders = libraryForUserDto.Folders.Select(s => new FolderPath() {Path = s}).ToList();
|
||||
|
||||
var typeUpdate = library.Type != libraryForUserDto.Type;
|
||||
library.Type = libraryForUserDto.Type;
|
||||
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the library.");
|
||||
if (originalFolders.Count != libraryForUserDto.Folders.Count() || typeUpdate)
|
||||
{
|
||||
await _libraryWatcher.RestartWatching();
|
||||
_taskScheduler.ScanLibrary(library.Id);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved,
|
||||
MessageFactory.SeriesRemovedEvent(seriesId, string.Empty, libraryId), false);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
|
||||
MessageFactory.LibraryModifiedEvent(libraryId, "delete"), false);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
public async Task<ActionResult<SearchResultGroupDto>> Search(string queryString)
|
||||
catch (Exception ex)
|
||||
{
|
||||
queryString = Uri.UnescapeDataString(queryString).Trim().Replace(@"%", string.Empty).Replace(":", string.Empty);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
// Get libraries user has access to
|
||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
|
||||
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.SearchSeries(user.Id, isAdmin, libraries.Select(l => l.Id).ToArray(), queryString);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
[HttpGet("type")]
|
||||
public async Task<ActionResult<LibraryType>> GetLibraryType(int libraryId)
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId));
|
||||
_logger.LogError(ex, "There was a critical error trying to delete the library");
|
||||
await _unitOfWork.RollbackAsync();
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing Library with new name, folders, and/or type.
|
||||
/// </summary>
|
||||
/// <remarks>Any folder or type change will invoke a scan.</remarks>
|
||||
/// <param name="libraryForUserDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto libraryForUserDto)
|
||||
{
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id, LibraryIncludes.Folders);
|
||||
|
||||
var originalFolders = library.Folders.Select(x => x.Path).ToList();
|
||||
|
||||
library.Name = libraryForUserDto.Name;
|
||||
library.Folders = libraryForUserDto.Folders.Select(s => new FolderPath() {Path = s}).ToList();
|
||||
|
||||
var typeUpdate = library.Type != libraryForUserDto.Type;
|
||||
library.Type = libraryForUserDto.Type;
|
||||
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the library.");
|
||||
if (originalFolders.Count != libraryForUserDto.Folders.Count() || typeUpdate)
|
||||
{
|
||||
await _libraryWatcher.RestartWatching();
|
||||
_taskScheduler.ScanLibrary(library.Id);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
public async Task<ActionResult<SearchResultGroupDto>> Search(string queryString)
|
||||
{
|
||||
queryString = Uri.UnescapeDataString(queryString).Trim().Replace(@"%", string.Empty).Replace(":", string.Empty);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
// Get libraries user has access to
|
||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
|
||||
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.SearchSeries(user.Id, isAdmin, libraries.Select(l => l.Id).ToArray(), queryString);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
[HttpGet("type")]
|
||||
public async Task<ActionResult<LibraryType>> GetLibraryType(int libraryId)
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,44 +7,43 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace API.Controllers;
|
||||
|
||||
public class PluginController : BaseApiController
|
||||
{
|
||||
public class PluginController : BaseApiController
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ITokenService _tokenService;
|
||||
private readonly ILogger<PluginController> _logger;
|
||||
|
||||
public PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger<PluginController> logger)
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ITokenService _tokenService;
|
||||
private readonly ILogger<PluginController> _logger;
|
||||
_unitOfWork = unitOfWork;
|
||||
_tokenService = tokenService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger<PluginController> logger)
|
||||
/// <summary>
|
||||
/// Authenticate with the Server given an apiKey. This will log you in by returning the user object and the JWT token.
|
||||
/// </summary>
|
||||
/// <remarks>This API is not fully built out and may require more information in later releases</remarks>
|
||||
/// <param name="apiKey">API key which will be used to authenticate and return a valid user token back</param>
|
||||
/// <param name="pluginName">Name of the Plugin</param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("authenticate")]
|
||||
public async Task<ActionResult<UserDto>> Authenticate([Required] string apiKey, [Required] string pluginName)
|
||||
{
|
||||
// NOTE: In order to log information about plugins, we need some Plugin Description information for each request
|
||||
// Should log into access table so we can tell the user
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId <= 0) return Unauthorized();
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
_logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName, user.UserName, userId);
|
||||
return new UserDto
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_tokenService = tokenService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticate with the Server given an apiKey. This will log you in by returning the user object and the JWT token.
|
||||
/// </summary>
|
||||
/// <remarks>This API is not fully built out and may require more information in later releases</remarks>
|
||||
/// <param name="apiKey">API key which will be used to authenticate and return a valid user token back</param>
|
||||
/// <param name="pluginName">Name of the Plugin</param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("authenticate")]
|
||||
public async Task<ActionResult<UserDto>> Authenticate([Required] string apiKey, [Required] string pluginName)
|
||||
{
|
||||
// NOTE: In order to log information about plugins, we need some Plugin Description information for each request
|
||||
// Should log into access table so we can tell the user
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId <= 0) return Unauthorized();
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
_logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName, user.UserName, userId);
|
||||
return new UserDto
|
||||
{
|
||||
Username = user.UserName,
|
||||
Token = await _tokenService.CreateToken(user),
|
||||
ApiKey = user.ApiKey,
|
||||
};
|
||||
}
|
||||
Username = user.UserName,
|
||||
Token = await _tokenService.CreateToken(user),
|
||||
ApiKey = user.ApiKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -13,483 +13,482 @@ using API.SignalR;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace API.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class ReadingListController : BaseApiController
|
||||
{
|
||||
[Authorize]
|
||||
public class ReadingListController : BaseApiController
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IReadingListService _readingListService;
|
||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||
|
||||
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub, IReadingListService readingListService)
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IReadingListService _readingListService;
|
||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
_readingListService = readingListService;
|
||||
}
|
||||
|
||||
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub, IReadingListService readingListService)
|
||||
/// <summary>
|
||||
/// Fetches a single Reading List
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[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>
|
||||
/// Returns all Reading Lists the user has access to that have a series within it.
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("lists-for-series")]
|
||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForSeries(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(userId, seriesId, true);
|
||||
|
||||
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(items);
|
||||
}
|
||||
|
||||
|
||||
/// <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 user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
_readingListService = readingListService;
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches a single Reading List
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetList(int readingListId)
|
||||
if (await _readingListService.UpdateReadingListItemPosition(dto)) return Ok("Updated");
|
||||
|
||||
|
||||
return BadRequest("Couldn't update position");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a list item from the list. Will reorder all item positions afterwards
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("delete-item")]
|
||||
public async Task<ActionResult> DeleteListItem(UpdateReadingListPosition dto)
|
||||
{
|
||||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, userId));
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
}
|
||||
|
||||
/// <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)
|
||||
if (await _readingListService.DeleteReadingListItem(dto))
|
||||
{
|
||||
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);
|
||||
return Ok("Updated");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Reading Lists the user has access to that have a series within it.
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("lists-for-series")]
|
||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForSeries(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(userId, seriesId, true);
|
||||
return BadRequest("Couldn't delete item");
|
||||
}
|
||||
|
||||
return Ok(items);
|
||||
/// <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 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");
|
||||
}
|
||||
|
||||
/// <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)
|
||||
if (await _readingListService.RemoveFullyReadItems(readingListId, user))
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
|
||||
return Ok(items);
|
||||
return Ok("Updated");
|
||||
}
|
||||
|
||||
return BadRequest("Could not remove read items");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an items position
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-position")]
|
||||
public async Task<ActionResult> UpdateListItemPosition(UpdateReadingListPosition dto)
|
||||
/// <summary>
|
||||
/// Deletes a reading list
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpDelete]
|
||||
public async Task<ActionResult> DeleteList([FromQuery] int readingListId)
|
||||
{
|
||||
var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
// 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");
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a list item from the list. Will reorder all item positions afterwards
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("delete-item")]
|
||||
public async Task<ActionResult> DeleteListItem(UpdateReadingListPosition dto)
|
||||
if (await _readingListService.DeleteReadingList(readingListId, user)) return Ok("List was 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.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)
|
||||
{
|
||||
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");
|
||||
return BadRequest("A list of this name already exists");
|
||||
}
|
||||
|
||||
/// <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 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the properties (title, summary) of a reading list
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[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");
|
||||
|
||||
var user = await _readingListService.UserHasReadingListAccess(readingList.Id, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
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");
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a reading list
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpDelete]
|
||||
public async Task<ActionResult> DeleteList([FromQuery] int readingListId)
|
||||
if (!string.IsNullOrEmpty(dto.Title))
|
||||
{
|
||||
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");
|
||||
readingList.Title = dto.Title; // Should I check if this is unique?
|
||||
readingList.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(readingList.Title);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(dto.Title))
|
||||
{
|
||||
readingList.Summary = dto.Summary;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
readingList.Promoted = dto.Promoted;
|
||||
|
||||
readingList.CoverImageLocked = dto.CoverImageLocked;
|
||||
|
||||
if (!dto.CoverImageLocked)
|
||||
{
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the properties (title, summary) of a reading list
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[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");
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.Title))
|
||||
{
|
||||
readingList.Title = dto.Title; // Should I check if this is unique?
|
||||
readingList.NormalizedTitle = Services.Tasks.Scanner.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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
readingList.CoverImageLocked = false;
|
||||
readingList.CoverImage = string.Empty;
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false);
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
|
||||
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok("Updated");
|
||||
}
|
||||
return BadRequest("Could not update reading list");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all chapters from a Series to a reading list
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-by-series")]
|
||||
public async Task<ActionResult> 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");
|
||||
}
|
||||
return BadRequest("Could not update reading list");
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all chapters from a Series to a reading list
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-by-series")]
|
||||
public async Task<ActionResult> UpdateListBySeries(UpdateReadingListBySeriesDto dto)
|
||||
return Ok("Nothing to do");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds all chapters from a list of volumes and chapters to a reading list
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-by-multiple")]
|
||||
public async Task<ActionResult> UpdateListByMultiple(UpdateReadingListByMultipleDto dto)
|
||||
{
|
||||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
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())
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
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});
|
||||
return Ok("Nothing to do");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all chapters from a list of series to a reading list
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-by-multiple-series")]
|
||||
public async Task<ActionResult> 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(dto.SeriesId, chapterIdsForSeries, readingList))
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds all chapters from a list of volumes and chapters to a reading list
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-by-multiple")]
|
||||
public async Task<ActionResult> UpdateListByMultiple(UpdateReadingListByMultipleDto dto)
|
||||
try
|
||||
{
|
||||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all chapters from a list of series to a reading list
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-by-multiple-series")]
|
||||
public async Task<ActionResult> UpdateListByMultipleSeries(UpdateReadingListByMultipleSeriesDto dto)
|
||||
catch
|
||||
{
|
||||
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");
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
[HttpPost("update-by-volume")]
|
||||
public async Task<ActionResult> UpdateListByVolume(UpdateReadingListByVolumeDto dto)
|
||||
return Ok("Nothing to do");
|
||||
}
|
||||
|
||||
[HttpPost("update-by-volume")]
|
||||
public async Task<ActionResult> UpdateListByVolume(UpdateReadingListByVolumeDto dto)
|
||||
{
|
||||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
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");
|
||||
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");
|
||||
|
||||
[HttpPost("update-by-chapter")]
|
||||
public async Task<ActionResult> UpdateListByChapter(UpdateReadingListByChapterDto dto)
|
||||
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))
|
||||
{
|
||||
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<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");
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <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)
|
||||
try
|
||||
{
|
||||
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)
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
return items[index].ChapterId;
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
}
|
||||
|
||||
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)
|
||||
catch
|
||||
{
|
||||
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);
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
}
|
||||
|
||||
[HttpPost("update-by-chapter")]
|
||||
public async Task<ActionResult> 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<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>
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,480 +19,479 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace API.Controllers;
|
||||
|
||||
public class SeriesController : BaseApiController
|
||||
{
|
||||
public class SeriesController : BaseApiController
|
||||
private readonly ILogger<SeriesController> _logger;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ISeriesService _seriesService;
|
||||
|
||||
|
||||
public SeriesController(ILogger<SeriesController> logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, ISeriesService seriesService)
|
||||
{
|
||||
private readonly ILogger<SeriesController> _logger;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ISeriesService _seriesService;
|
||||
_logger = logger;
|
||||
_taskScheduler = taskScheduler;
|
||||
_unitOfWork = unitOfWork;
|
||||
_seriesService = seriesService;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams, [FromBody] FilterDto filterDto)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto);
|
||||
|
||||
public SeriesController(ILogger<SeriesController> logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, ISeriesService seriesService)
|
||||
// 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(userId, 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:int}")]
|
||||
public async Task<ActionResult<SeriesDto>> GetSeries(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
try
|
||||
{
|
||||
_logger = logger;
|
||||
_taskScheduler = taskScheduler;
|
||||
_unitOfWork = unitOfWork;
|
||||
_seriesService = seriesService;
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "There was an issue fetching {SeriesId}", seriesId);
|
||||
throw new KavitaException("This series does not exist");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams, [FromBody] FilterDto filterDto)
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpDelete("{seriesId}")]
|
||||
public async Task<ActionResult<bool>> DeleteSeries(int seriesId)
|
||||
{
|
||||
var username = User.GetUsername();
|
||||
_logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", seriesId, username);
|
||||
|
||||
return Ok(await _seriesService.DeleteMultipleSeries(new[] {seriesId}));
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("delete-multiple")]
|
||||
public async Task<ActionResult> DeleteMultipleSeries(DeleteSeriesDto dto)
|
||||
{
|
||||
var username = User.GetUsername();
|
||||
_logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", dto.SeriesIds, username);
|
||||
|
||||
if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok();
|
||||
|
||||
return BadRequest("There was an issue deleting the series requested");
|
||||
}
|
||||
|
||||
/// <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 userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId));
|
||||
}
|
||||
|
||||
[HttpGet("volume")]
|
||||
public async Task<ActionResult<VolumeDto>> GetVolume(int volumeId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId));
|
||||
}
|
||||
|
||||
[HttpGet("chapter")]
|
||||
public async Task<ActionResult<ChapterDto>> GetChapter(int chapterId)
|
||||
{
|
||||
return Ok(await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId));
|
||||
}
|
||||
|
||||
[HttpGet("chapter-metadata")]
|
||||
public async Task<ActionResult<ChapterDto>> GetChapterMetadata(int chapterId)
|
||||
{
|
||||
return Ok(await _unitOfWork.ChapterRepository.GetChapterMetadataDtoAsync(chapterId));
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("update-rating")]
|
||||
public async Task<ActionResult> UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Ratings);
|
||||
if (!await _seriesService.UpdateRating(user, updateSeriesRatingDto)) 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");
|
||||
|
||||
var seriesExists =
|
||||
await _unitOfWork.SeriesRepository.DoesSeriesNameExistInLibrary(updateSeries.Name.Trim(), series.LibraryId,
|
||||
series.Format);
|
||||
if (series.Name != updateSeries.Name && seriesExists)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var series =
|
||||
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(userId, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
||||
return Ok(series);
|
||||
return BadRequest("A series already exists in this library with this name. Series Names must be unique to a library.");
|
||||
}
|
||||
|
||||
/// <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:int}")]
|
||||
public async Task<ActionResult<SeriesDto>> GetSeries(int seriesId)
|
||||
series.Name = updateSeries.Name.Trim();
|
||||
series.NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name);
|
||||
if (!string.IsNullOrEmpty(updateSeries.SortName.Trim()))
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
try
|
||||
series.SortName = updateSeries.SortName.Trim();
|
||||
}
|
||||
|
||||
series.LocalizedName = updateSeries.LocalizedName.Trim();
|
||||
series.NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.LocalizedName);
|
||||
|
||||
series.NameLocked = updateSeries.NameLocked;
|
||||
series.SortNameLocked = updateSeries.SortNameLocked;
|
||||
series.LocalizedNameLocked = updateSeries.LocalizedNameLocked;
|
||||
|
||||
|
||||
var needsRefreshMetadata = false;
|
||||
// This is when you hit Reset
|
||||
if (series.CoverImageLocked && !updateSeries.CoverImageLocked)
|
||||
{
|
||||
// Trigger a refresh when we are moving from a locked image to a non-locked
|
||||
needsRefreshMetadata = true;
|
||||
series.CoverImage = string.Empty;
|
||||
series.CoverImageLocked = updateSeries.CoverImageLocked;
|
||||
}
|
||||
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
if (needsRefreshMetadata)
|
||||
{
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId));
|
||||
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.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();
|
||||
_logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", seriesId, username);
|
||||
|
||||
return Ok(await _seriesService.DeleteMultipleSeries(new[] {seriesId}));
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("delete-multiple")]
|
||||
public async Task<ActionResult> DeleteMultipleSeries(DeleteSeriesDto dto)
|
||||
{
|
||||
var username = User.GetUsername();
|
||||
_logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", dto.SeriesIds, username);
|
||||
|
||||
if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok();
|
||||
|
||||
return BadRequest("There was an issue deleting the series requested");
|
||||
}
|
||||
|
||||
/// <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 userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId));
|
||||
}
|
||||
|
||||
[HttpGet("volume")]
|
||||
public async Task<ActionResult<VolumeDto>> GetVolume(int volumeId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId));
|
||||
}
|
||||
|
||||
[HttpGet("chapter")]
|
||||
public async Task<ActionResult<ChapterDto>> GetChapter(int chapterId)
|
||||
{
|
||||
return Ok(await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId));
|
||||
}
|
||||
|
||||
[HttpGet("chapter-metadata")]
|
||||
public async Task<ActionResult<ChapterDto>> GetChapterMetadata(int chapterId)
|
||||
{
|
||||
return Ok(await _unitOfWork.ChapterRepository.GetChapterMetadataDtoAsync(chapterId));
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("update-rating")]
|
||||
public async Task<ActionResult> UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Ratings);
|
||||
if (!await _seriesService.UpdateRating(user, updateSeriesRatingDto)) return BadRequest("There was a critical error.");
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)
|
||||
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 userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var series =
|
||||
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(userId, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
[HttpPost("recently-updated-series")]
|
||||
public async Task<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChapters()
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId));
|
||||
}
|
||||
|
||||
[HttpPost("all")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeries(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var series =
|
||||
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");
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches series that are on deck aka have progress on them.
|
||||
/// </summary>
|
||||
/// <param name="filterDto"></param>
|
||||
/// <param name="userParams"></param>
|
||||
/// <param name="libraryId">Default of 0 meaning all libraries</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("on-deck")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetOnDeck(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, filterDto);
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, pagedList);
|
||||
|
||||
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
||||
|
||||
return Ok(pagedList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a Cover Image Generation task
|
||||
/// </summary>
|
||||
/// <param name="refreshSeriesDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("refresh-metadata")]
|
||||
public ActionResult RefreshSeriesMetadata(RefreshSeriesDto refreshSeriesDto)
|
||||
{
|
||||
_taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scan a series and force each file to be updated. This should be invoked via the User, hence why we force.
|
||||
/// </summary>
|
||||
/// <param name="refreshSeriesDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("scan")]
|
||||
public ActionResult ScanSeries(RefreshSeriesDto refreshSeriesDto)
|
||||
{
|
||||
_taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run a file analysis on the series.
|
||||
/// </summary>
|
||||
/// <param name="refreshSeriesDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("analyze")]
|
||||
public ActionResult AnalyzeSeries(RefreshSeriesDto refreshSeriesDto)
|
||||
{
|
||||
_taskScheduler.AnalyzeFilesForSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns metadata for a given series
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("metadata")]
|
||||
public async Task<ActionResult<SeriesMetadataDto>> GetSeriesMetadata(int seriesId)
|
||||
{
|
||||
var metadata = await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId);
|
||||
return Ok(metadata);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update series metadata
|
||||
/// </summary>
|
||||
/// <param name="updateSeriesMetadataDto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("metadata")]
|
||||
public async Task<ActionResult> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto)
|
||||
{
|
||||
if (await _seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto))
|
||||
{
|
||||
_logger.LogInformation("{UserName} is updating Series {SeriesName}", User.GetUsername(), updateSeries.Name);
|
||||
return Ok("Successfully updated");
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(updateSeries.Id);
|
||||
return BadRequest("Could not update metadata");
|
||||
}
|
||||
|
||||
if (series == null) return BadRequest("Series does not exist");
|
||||
/// <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 userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams);
|
||||
|
||||
var seriesExists =
|
||||
await _unitOfWork.SeriesRepository.DoesSeriesNameExistInLibrary(updateSeries.Name.Trim(), series.LibraryId,
|
||||
series.Format);
|
||||
if (series.Name != updateSeries.Name && seriesExists)
|
||||
// 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(userId, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches Series for a set of Ids. This will check User for permission access and filter out any Ids that don't exist or
|
||||
/// the user does not have access to.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("series-by-ids")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesById(SeriesByIdsDto dto)
|
||||
{
|
||||
if (dto.SeriesIds == null) return BadRequest("Must pass seriesIds");
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, userId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the age rating for the <see cref="AgeRating"/> enum value
|
||||
/// </summary>
|
||||
/// <param name="ageRating"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("age-rating")]
|
||||
public ActionResult<string> GetAgeRating(int ageRating)
|
||||
{
|
||||
var val = (AgeRating) ageRating;
|
||||
|
||||
return Ok(val.ToDescription());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a special DTO for Series Detail page.
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>Do not rely on this API externally. May change without hesitation. </remarks>
|
||||
[HttpGet("series-detail")]
|
||||
public async Task<ActionResult<SeriesDetailDto>> GetSeriesDetailBreakdown(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return await _seriesService.GetSeriesDetail(seriesId, userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the series for the MangaFile id. If the user does not have access (shouldn't happen by the UI),
|
||||
/// then null is returned
|
||||
/// </summary>
|
||||
/// <param name="mangaFileId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("series-for-mangafile")]
|
||||
public async Task<ActionResult<SeriesDto>> GetSeriesForMangaFile(int mangaFileId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, userId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the series for the Chapter id. If the user does not have access (shouldn't happen by the UI),
|
||||
/// then null is returned
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("series-for-chapter")]
|
||||
public async Task<ActionResult<SeriesDto>> GetSeriesForChapter(int chapterId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, userId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the related series for a given series
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="relation">Type of Relationship to pull back</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("related")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRelatedSeries(int seriesId, RelationKind relation)
|
||||
{
|
||||
// Send back a custom DTO with each type or maybe sorted in some way
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(userId, seriesId, relation));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all related series against the passed series Id
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("all-related")]
|
||||
public async Task<ActionResult<RelatedSeriesDto>> GetAllRelatedSeries(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Update the relations attached to the Series. Does not generate associated Sequel/Prequel pairs on target series.
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy="RequireAdminRole")]
|
||||
[HttpPost("update-related")]
|
||||
public async Task<ActionResult> UpdateRelatedSeries(UpdateRelatedSeriesDto dto)
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Related);
|
||||
|
||||
UpdateRelationForKind(dto.Adaptations, series.Relations.Where(r => r.RelationKind == RelationKind.Adaptation).ToList(), series, RelationKind.Adaptation);
|
||||
UpdateRelationForKind(dto.Characters, series.Relations.Where(r => r.RelationKind == RelationKind.Character).ToList(), series, RelationKind.Character);
|
||||
UpdateRelationForKind(dto.Contains, series.Relations.Where(r => r.RelationKind == RelationKind.Contains).ToList(), series, RelationKind.Contains);
|
||||
UpdateRelationForKind(dto.Others, series.Relations.Where(r => r.RelationKind == RelationKind.Other).ToList(), series, RelationKind.Other);
|
||||
UpdateRelationForKind(dto.SideStories, series.Relations.Where(r => r.RelationKind == RelationKind.SideStory).ToList(), series, RelationKind.SideStory);
|
||||
UpdateRelationForKind(dto.SpinOffs, series.Relations.Where(r => r.RelationKind == RelationKind.SpinOff).ToList(), series, RelationKind.SpinOff);
|
||||
UpdateRelationForKind(dto.AlternativeSettings, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeSetting).ToList(), series, RelationKind.AlternativeSetting);
|
||||
UpdateRelationForKind(dto.AlternativeVersions, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeVersion).ToList(), series, RelationKind.AlternativeVersion);
|
||||
UpdateRelationForKind(dto.Doujinshis, series.Relations.Where(r => r.RelationKind == RelationKind.Doujinshi).ToList(), series, RelationKind.Doujinshi);
|
||||
UpdateRelationForKind(dto.Prequels, series.Relations.Where(r => r.RelationKind == RelationKind.Prequel).ToList(), series, RelationKind.Prequel);
|
||||
UpdateRelationForKind(dto.Sequels, series.Relations.Where(r => r.RelationKind == RelationKind.Sequel).ToList(), series, RelationKind.Sequel);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
if (await _unitOfWork.CommitAsync()) return Ok();
|
||||
|
||||
|
||||
return BadRequest("There was an issue updating relationships");
|
||||
}
|
||||
|
||||
// TODO: Move this to a Service and Unit Test it
|
||||
private void UpdateRelationForKind(ICollection<int> dtoTargetSeriesIds, IEnumerable<SeriesRelation> adaptations, Series series, RelationKind kind)
|
||||
{
|
||||
foreach (var adaptation in adaptations.Where(adaptation => !dtoTargetSeriesIds.Contains(adaptation.TargetSeriesId)))
|
||||
{
|
||||
// If the seriesId isn't in dto, it means we've removed or reclassified
|
||||
series.Relations.Remove(adaptation);
|
||||
}
|
||||
|
||||
// At this point, we only have things to add
|
||||
foreach (var targetSeriesId in dtoTargetSeriesIds)
|
||||
{
|
||||
// This ensures we don't allow any duplicates to be added
|
||||
if (series.Relations.SingleOrDefault(r =>
|
||||
r.RelationKind == kind && r.TargetSeriesId == targetSeriesId) !=
|
||||
null) continue;
|
||||
|
||||
series.Relations.Add(new SeriesRelation()
|
||||
{
|
||||
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.NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name);
|
||||
if (!string.IsNullOrEmpty(updateSeries.SortName.Trim()))
|
||||
{
|
||||
series.SortName = updateSeries.SortName.Trim();
|
||||
}
|
||||
|
||||
series.LocalizedName = updateSeries.LocalizedName.Trim();
|
||||
series.NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.LocalizedName);
|
||||
|
||||
series.NameLocked = updateSeries.NameLocked;
|
||||
series.SortNameLocked = updateSeries.SortNameLocked;
|
||||
series.LocalizedNameLocked = updateSeries.LocalizedNameLocked;
|
||||
|
||||
|
||||
var needsRefreshMetadata = false;
|
||||
// This is when you hit Reset
|
||||
if (series.CoverImageLocked && !updateSeries.CoverImageLocked)
|
||||
{
|
||||
// Trigger a refresh when we are moving from a locked image to a non-locked
|
||||
needsRefreshMetadata = true;
|
||||
series.CoverImage = string.Empty;
|
||||
series.CoverImageLocked = updateSeries.CoverImageLocked;
|
||||
}
|
||||
|
||||
Series = series,
|
||||
SeriesId = series.Id,
|
||||
TargetSeriesId = targetSeriesId,
|
||||
RelationKind = kind
|
||||
});
|
||||
_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 userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var series =
|
||||
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(userId, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
[HttpPost("recently-updated-series")]
|
||||
public async Task<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChapters()
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId));
|
||||
}
|
||||
|
||||
[HttpPost("all")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeries(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var series =
|
||||
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");
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches series that are on deck aka have progress on them.
|
||||
/// </summary>
|
||||
/// <param name="filterDto"></param>
|
||||
/// <param name="userParams"></param>
|
||||
/// <param name="libraryId">Default of 0 meaning all libraries</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("on-deck")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetOnDeck(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, filterDto);
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, pagedList);
|
||||
|
||||
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
||||
|
||||
return Ok(pagedList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a Cover Image Generation task
|
||||
/// </summary>
|
||||
/// <param name="refreshSeriesDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("refresh-metadata")]
|
||||
public ActionResult RefreshSeriesMetadata(RefreshSeriesDto refreshSeriesDto)
|
||||
{
|
||||
_taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scan a series and force each file to be updated. This should be invoked via the User, hence why we force.
|
||||
/// </summary>
|
||||
/// <param name="refreshSeriesDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("scan")]
|
||||
public ActionResult ScanSeries(RefreshSeriesDto refreshSeriesDto)
|
||||
{
|
||||
_taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run a file analysis on the series.
|
||||
/// </summary>
|
||||
/// <param name="refreshSeriesDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("analyze")]
|
||||
public ActionResult AnalyzeSeries(RefreshSeriesDto refreshSeriesDto)
|
||||
{
|
||||
_taskScheduler.AnalyzeFilesForSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns metadata for a given series
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("metadata")]
|
||||
public async Task<ActionResult<SeriesMetadataDto>> GetSeriesMetadata(int seriesId)
|
||||
{
|
||||
var metadata = await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId);
|
||||
return Ok(metadata);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update series metadata
|
||||
/// </summary>
|
||||
/// <param name="updateSeriesMetadataDto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("metadata")]
|
||||
public async Task<ActionResult> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto)
|
||||
{
|
||||
if (await _seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto))
|
||||
{
|
||||
return Ok("Successfully updated");
|
||||
}
|
||||
|
||||
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 userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var series =
|
||||
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(userId, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches Series for a set of Ids. This will check User for permission access and filter out any Ids that don't exist or
|
||||
/// the user does not have access to.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("series-by-ids")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesById(SeriesByIdsDto dto)
|
||||
{
|
||||
if (dto.SeriesIds == null) return BadRequest("Must pass seriesIds");
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, userId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the age rating for the <see cref="AgeRating"/> enum value
|
||||
/// </summary>
|
||||
/// <param name="ageRating"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("age-rating")]
|
||||
public ActionResult<string> GetAgeRating(int ageRating)
|
||||
{
|
||||
var val = (AgeRating) ageRating;
|
||||
|
||||
return Ok(val.ToDescription());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a special DTO for Series Detail page.
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>Do not rely on this API externally. May change without hesitation. </remarks>
|
||||
[HttpGet("series-detail")]
|
||||
public async Task<ActionResult<SeriesDetailDto>> GetSeriesDetailBreakdown(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return await _seriesService.GetSeriesDetail(seriesId, userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the series for the MangaFile id. If the user does not have access (shouldn't happen by the UI),
|
||||
/// then null is returned
|
||||
/// </summary>
|
||||
/// <param name="mangaFileId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("series-for-mangafile")]
|
||||
public async Task<ActionResult<SeriesDto>> GetSeriesForMangaFile(int mangaFileId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, userId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the series for the Chapter id. If the user does not have access (shouldn't happen by the UI),
|
||||
/// then null is returned
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("series-for-chapter")]
|
||||
public async Task<ActionResult<SeriesDto>> GetSeriesForChapter(int chapterId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, userId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the related series for a given series
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="relation">Type of Relationship to pull back</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("related")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRelatedSeries(int seriesId, RelationKind relation)
|
||||
{
|
||||
// Send back a custom DTO with each type or maybe sorted in some way
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(userId, seriesId, relation));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all related series against the passed series Id
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("all-related")]
|
||||
public async Task<ActionResult<RelatedSeriesDto>> GetAllRelatedSeries(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Update the relations attached to the Series. Does not generate associated Sequel/Prequel pairs on target series.
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy="RequireAdminRole")]
|
||||
[HttpPost("update-related")]
|
||||
public async Task<ActionResult> UpdateRelatedSeries(UpdateRelatedSeriesDto dto)
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Related);
|
||||
|
||||
UpdateRelationForKind(dto.Adaptations, series.Relations.Where(r => r.RelationKind == RelationKind.Adaptation).ToList(), series, RelationKind.Adaptation);
|
||||
UpdateRelationForKind(dto.Characters, series.Relations.Where(r => r.RelationKind == RelationKind.Character).ToList(), series, RelationKind.Character);
|
||||
UpdateRelationForKind(dto.Contains, series.Relations.Where(r => r.RelationKind == RelationKind.Contains).ToList(), series, RelationKind.Contains);
|
||||
UpdateRelationForKind(dto.Others, series.Relations.Where(r => r.RelationKind == RelationKind.Other).ToList(), series, RelationKind.Other);
|
||||
UpdateRelationForKind(dto.SideStories, series.Relations.Where(r => r.RelationKind == RelationKind.SideStory).ToList(), series, RelationKind.SideStory);
|
||||
UpdateRelationForKind(dto.SpinOffs, series.Relations.Where(r => r.RelationKind == RelationKind.SpinOff).ToList(), series, RelationKind.SpinOff);
|
||||
UpdateRelationForKind(dto.AlternativeSettings, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeSetting).ToList(), series, RelationKind.AlternativeSetting);
|
||||
UpdateRelationForKind(dto.AlternativeVersions, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeVersion).ToList(), series, RelationKind.AlternativeVersion);
|
||||
UpdateRelationForKind(dto.Doujinshis, series.Relations.Where(r => r.RelationKind == RelationKind.Doujinshi).ToList(), series, RelationKind.Doujinshi);
|
||||
UpdateRelationForKind(dto.Prequels, series.Relations.Where(r => r.RelationKind == RelationKind.Prequel).ToList(), series, RelationKind.Prequel);
|
||||
UpdateRelationForKind(dto.Sequels, series.Relations.Where(r => r.RelationKind == RelationKind.Sequel).ToList(), series, RelationKind.Sequel);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
if (await _unitOfWork.CommitAsync()) return Ok();
|
||||
|
||||
|
||||
return BadRequest("There was an issue updating relationships");
|
||||
}
|
||||
|
||||
// TODO: Move this to a Service and Unit Test it
|
||||
private void UpdateRelationForKind(ICollection<int> dtoTargetSeriesIds, IEnumerable<SeriesRelation> adaptations, Series series, RelationKind kind)
|
||||
{
|
||||
foreach (var adaptation in adaptations.Where(adaptation => !dtoTargetSeriesIds.Contains(adaptation.TargetSeriesId)))
|
||||
{
|
||||
// If the seriesId isn't in dto, it means we've removed or reclassified
|
||||
series.Relations.Remove(adaptation);
|
||||
}
|
||||
|
||||
// At this point, we only have things to add
|
||||
foreach (var targetSeriesId in dtoTargetSeriesIds)
|
||||
{
|
||||
// This ensures we don't allow any duplicates to be added
|
||||
if (series.Relations.SingleOrDefault(r =>
|
||||
r.RelationKind == kind && r.TargetSeriesId == targetSeriesId) !=
|
||||
null) continue;
|
||||
|
||||
series.Relations.Add(new SeriesRelation()
|
||||
{
|
||||
Series = series,
|
||||
SeriesId = series.Id,
|
||||
TargetSeriesId = targetSeriesId,
|
||||
RelationKind = kind
|
||||
});
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ using API.DTOs.Jobs;
|
|||
using API.DTOs.Stats;
|
||||
using API.DTOs.Update;
|
||||
using API.Extensions;
|
||||
using API.Logging;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using Hangfire;
|
||||
|
@ -20,143 +21,141 @@ using Microsoft.Extensions.Hosting;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using TaskScheduler = System.Threading.Tasks.TaskScheduler;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace API.Controllers;
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
public class ServerController : BaseApiController
|
||||
{
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
public class ServerController : BaseApiController
|
||||
private readonly IHostApplicationLifetime _applicationLifetime;
|
||||
private readonly ILogger<ServerController> _logger;
|
||||
private readonly IBackupService _backupService;
|
||||
private readonly IArchiveService _archiveService;
|
||||
private readonly IVersionUpdaterService _versionUpdaterService;
|
||||
private readonly IStatsService _statsService;
|
||||
private readonly ICleanupService _cleanupService;
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
|
||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger,
|
||||
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
||||
ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService)
|
||||
{
|
||||
private readonly IHostApplicationLifetime _applicationLifetime;
|
||||
private readonly ILogger<ServerController> _logger;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly IBackupService _backupService;
|
||||
private readonly IArchiveService _archiveService;
|
||||
private readonly IVersionUpdaterService _versionUpdaterService;
|
||||
private readonly IStatsService _statsService;
|
||||
private readonly ICleanupService _cleanupService;
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
_applicationLifetime = applicationLifetime;
|
||||
_logger = logger;
|
||||
_backupService = backupService;
|
||||
_archiveService = archiveService;
|
||||
_versionUpdaterService = versionUpdaterService;
|
||||
_statsService = statsService;
|
||||
_cleanupService = cleanupService;
|
||||
_emailService = emailService;
|
||||
_bookmarkService = bookmarkService;
|
||||
}
|
||||
|
||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
|
||||
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
||||
ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService)
|
||||
/// <summary>
|
||||
/// Attempts to Restart the server. Does not work, will shutdown the instance.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("restart")]
|
||||
public ActionResult RestartServer()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is restarting server from admin dashboard", User.GetUsername());
|
||||
|
||||
_applicationLifetime.StopApplication();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs an ad-hoc cleanup of Cache
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("clear-cache")]
|
||||
public ActionResult ClearCache()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is clearing cache of server from admin dashboard", User.GetUsername());
|
||||
_cleanupService.CleanupCacheDirectory();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs an ad-hoc backup of the Database
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("backup-db")]
|
||||
public ActionResult BackupDatabase()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is backing up database of server from admin dashboard", User.GetUsername());
|
||||
RecurringJob.Trigger("backup");
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns non-sensitive information about the current system
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("server-info")]
|
||||
public async Task<ActionResult<ServerInfoDto>> GetVersion()
|
||||
{
|
||||
return Ok(await _statsService.GetServerInfo());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the scheduling of the convert bookmarks job. Only one job will run at a time.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("convert-bookmarks")]
|
||||
public ActionResult ScheduleConvertBookmarks()
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _bookmarkService.ConvertAllBookmarkToWebP());
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("logs")]
|
||||
public ActionResult GetLogs()
|
||||
{
|
||||
var files = _backupService.GetLogFiles();
|
||||
try
|
||||
{
|
||||
_applicationLifetime = applicationLifetime;
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_backupService = backupService;
|
||||
_archiveService = archiveService;
|
||||
_versionUpdaterService = versionUpdaterService;
|
||||
_statsService = statsService;
|
||||
_cleanupService = cleanupService;
|
||||
_emailService = emailService;
|
||||
_bookmarkService = bookmarkService;
|
||||
var zipPath = _archiveService.CreateZipForDownload(files, "logs");
|
||||
return PhysicalFile(zipPath, "application/zip", Path.GetFileName(zipPath), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to Restart the server. Does not work, will shutdown the instance.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("restart")]
|
||||
public ActionResult RestartServer()
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
_logger.LogInformation("{UserName} is restarting server from admin dashboard", User.GetUsername());
|
||||
|
||||
_applicationLifetime.StopApplication();
|
||||
return Ok();
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs an ad-hoc cleanup of Cache
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("clear-cache")]
|
||||
public ActionResult ClearCache()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is clearing cache of server from admin dashboard", User.GetUsername());
|
||||
_cleanupService.CleanupCacheDirectory();
|
||||
/// <summary>
|
||||
/// Checks for updates, if no updates that are > current version installed, returns null
|
||||
/// </summary>
|
||||
[HttpGet("check-update")]
|
||||
public async Task<ActionResult<UpdateNotificationDto>> CheckForUpdates()
|
||||
{
|
||||
return Ok(await _versionUpdaterService.CheckForUpdate());
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
[HttpGet("changelog")]
|
||||
public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog()
|
||||
{
|
||||
return Ok(await _versionUpdaterService.GetAllReleases());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs an ad-hoc backup of the Database
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("backup-db")]
|
||||
public ActionResult BackupDatabase()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is backing up database of server from admin dashboard", User.GetUsername());
|
||||
RecurringJob.Trigger("backup");
|
||||
return Ok();
|
||||
}
|
||||
/// <summary>
|
||||
/// Is this server accessible to the outside net
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("accessible")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<bool>> IsServerAccessible()
|
||||
{
|
||||
return await _emailService.CheckIfAccessible(Request.Host.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns non-sensitive information about the current system
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("server-info")]
|
||||
public async Task<ActionResult<ServerInfoDto>> GetVersion()
|
||||
{
|
||||
return Ok(await _statsService.GetServerInfo());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the scheduling of the convert bookmarks job. Only one job will run at a time.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("convert-bookmarks")]
|
||||
public ActionResult ScheduleConvertBookmarks()
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _bookmarkService.ConvertAllBookmarkToWebP());
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("logs")]
|
||||
public ActionResult GetLogs()
|
||||
{
|
||||
var files = _backupService.GetLogFiles(_config.GetMaxRollingFiles(), _config.GetLoggingFileName());
|
||||
try
|
||||
{
|
||||
var zipPath = _archiveService.CreateZipForDownload(files, "logs");
|
||||
return PhysicalFile(zipPath, "application/zip", Path.GetFileName(zipPath), true);
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks for updates, if no updates that are > current version installed, returns null
|
||||
/// </summary>
|
||||
[HttpGet("check-update")]
|
||||
public async Task<ActionResult<UpdateNotificationDto>> CheckForUpdates()
|
||||
{
|
||||
return Ok(await _versionUpdaterService.CheckForUpdate());
|
||||
}
|
||||
|
||||
[HttpGet("changelog")]
|
||||
public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog()
|
||||
{
|
||||
return Ok(await _versionUpdaterService.GetAllReleases());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is this server accessible to the outside net
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("accessible")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<bool>> IsServerAccessible()
|
||||
{
|
||||
return await _emailService.CheckIfAccessible(Request.Host.ToString());
|
||||
}
|
||||
|
||||
[HttpGet("jobs")]
|
||||
public ActionResult<IEnumerable<JobDto>> GetJobs()
|
||||
{
|
||||
var recurringJobs = Hangfire.JobStorage.Current.GetConnection().GetRecurringJobs().Select(
|
||||
dto =>
|
||||
[HttpGet("jobs")]
|
||||
public ActionResult<IEnumerable<JobDto>> GetJobs()
|
||||
{
|
||||
var recurringJobs = JobStorage.Current.GetConnection().GetRecurringJobs().Select(
|
||||
dto =>
|
||||
new JobDto() {
|
||||
Id = dto.Id,
|
||||
Title = dto.Id.Replace('-', ' '),
|
||||
|
@ -165,10 +164,9 @@ namespace API.Controllers
|
|||
LastExecution = dto.LastExecution,
|
||||
});
|
||||
|
||||
// For now, let's just do something simple
|
||||
//var enqueuedJobs = JobStorage.Current.GetMonitoringApi().EnqueuedJobs("default", 0, int.MaxValue);
|
||||
return Ok(recurringJobs);
|
||||
// For now, let's just do something simple
|
||||
//var enqueuedJobs = JobStorage.Current.GetMonitoringApi().EnqueuedJobs("default", 0, int.MaxValue);
|
||||
return Ok(recurringJobs);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ using API.DTOs.Settings;
|
|||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Converters;
|
||||
using API.Logging;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using AutoMapper;
|
||||
|
@ -20,285 +21,284 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace API.Controllers;
|
||||
|
||||
public class SettingsController : BaseApiController
|
||||
{
|
||||
public class SettingsController : BaseApiController
|
||||
private readonly ILogger<SettingsController> _logger;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly ILibraryWatcher _libraryWatcher;
|
||||
|
||||
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler,
|
||||
IDirectoryService directoryService, IMapper mapper, IEmailService emailService, ILibraryWatcher libraryWatcher)
|
||||
{
|
||||
private readonly ILogger<SettingsController> _logger;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly ILibraryWatcher _libraryWatcher;
|
||||
_logger = logger;
|
||||
_unitOfWork = unitOfWork;
|
||||
_taskScheduler = taskScheduler;
|
||||
_directoryService = directoryService;
|
||||
_mapper = mapper;
|
||||
_emailService = emailService;
|
||||
_libraryWatcher = libraryWatcher;
|
||||
}
|
||||
|
||||
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler,
|
||||
IDirectoryService directoryService, IMapper mapper, IEmailService emailService, ILibraryWatcher libraryWatcher)
|
||||
[AllowAnonymous]
|
||||
[HttpGet("base-url")]
|
||||
public async Task<ActionResult<string>> GetBaseUrl()
|
||||
{
|
||||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
return Ok(settingsDto.BaseUrl);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
||||
{
|
||||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
return Ok(settingsDto);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("reset")]
|
||||
public async Task<ActionResult<ServerSettingDto>> ResetSettings()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is resetting Server Settings", User.GetUsername());
|
||||
|
||||
return await UpdateSettings(_mapper.Map<ServerSettingDto>(Seed.DefaultSettings));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the email service url
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("reset-email-url")]
|
||||
public async Task<ActionResult<ServerSettingDto>> ResetEmailServiceUrlSettings()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is resetting Email Service Url Setting", User.GetUsername());
|
||||
var emailSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl);
|
||||
emailSetting.Value = EmailService.DefaultApiUrl;
|
||||
_unitOfWork.SettingsRepository.Update(emailSetting);
|
||||
|
||||
if (!await _unitOfWork.CommitAsync())
|
||||
{
|
||||
_logger = logger;
|
||||
_unitOfWork = unitOfWork;
|
||||
_taskScheduler = taskScheduler;
|
||||
_directoryService = directoryService;
|
||||
_mapper = mapper;
|
||||
_emailService = emailService;
|
||||
_libraryWatcher = libraryWatcher;
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("base-url")]
|
||||
public async Task<ActionResult<string>> GetBaseUrl()
|
||||
return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync());
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("test-email-url")]
|
||||
public async Task<ActionResult<EmailTestResultDto>> TestEmailServiceUrl(TestEmailDto dto)
|
||||
{
|
||||
return Ok(await _emailService.TestConnectivity(dto.Url));
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
|
||||
{
|
||||
_logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername());
|
||||
|
||||
// We do not allow CacheDirectory changes, so we will ignore.
|
||||
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
||||
var updateBookmarks = false;
|
||||
var originalBookmarkDirectory = _directoryService.BookmarkDirectory;
|
||||
|
||||
var bookmarkDirectory = updateSettingsDto.BookmarksDirectory;
|
||||
if (!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks") &&
|
||||
!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks/"))
|
||||
{
|
||||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
return Ok(settingsDto.BaseUrl);
|
||||
bookmarkDirectory = _directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks");
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
||||
if (string.IsNullOrEmpty(updateSettingsDto.BookmarksDirectory))
|
||||
{
|
||||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
return Ok(settingsDto);
|
||||
bookmarkDirectory = _directoryService.BookmarkDirectory;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("reset")]
|
||||
public async Task<ActionResult<ServerSettingDto>> ResetSettings()
|
||||
foreach (var setting in currentSettings)
|
||||
{
|
||||
_logger.LogInformation("{UserName} is resetting Server Settings", User.GetUsername());
|
||||
|
||||
return await UpdateSettings(_mapper.Map<ServerSettingDto>(Seed.DefaultSettings));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the email service url
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("reset-email-url")]
|
||||
public async Task<ActionResult<ServerSettingDto>> ResetEmailServiceUrlSettings()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is resetting Email Service Url Setting", User.GetUsername());
|
||||
var emailSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl);
|
||||
emailSetting.Value = EmailService.DefaultApiUrl;
|
||||
_unitOfWork.SettingsRepository.Update(emailSetting);
|
||||
|
||||
if (!await _unitOfWork.CommitAsync())
|
||||
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
setting.Value = updateSettingsDto.TaskBackup;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync());
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("test-email-url")]
|
||||
public async Task<ActionResult<EmailTestResultDto>> TestEmailServiceUrl(TestEmailDto dto)
|
||||
{
|
||||
return Ok(await _emailService.TestConnectivity(dto.Url));
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
|
||||
{
|
||||
_logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername());
|
||||
|
||||
// We do not allow CacheDirectory changes, so we will ignore.
|
||||
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
||||
var updateBookmarks = false;
|
||||
var originalBookmarkDirectory = _directoryService.BookmarkDirectory;
|
||||
|
||||
var bookmarkDirectory = updateSettingsDto.BookmarksDirectory;
|
||||
if (!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks") &&
|
||||
!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks/"))
|
||||
if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value)
|
||||
{
|
||||
bookmarkDirectory = _directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks");
|
||||
setting.Value = updateSettingsDto.TaskScan;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(updateSettingsDto.BookmarksDirectory))
|
||||
if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value)
|
||||
{
|
||||
bookmarkDirectory = _directoryService.BookmarkDirectory;
|
||||
setting.Value = updateSettingsDto.Port + string.Empty;
|
||||
// Port is managed in appSetting.json
|
||||
Configuration.Port = updateSettingsDto.Port;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
foreach (var setting in currentSettings)
|
||||
if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value)
|
||||
{
|
||||
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.TaskBackup;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.TaskScan;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.Port + string.Empty;
|
||||
// Port is managed in appSetting.json
|
||||
Configuration.Port = updateSettingsDto.Port;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value)
|
||||
{
|
||||
var path = !updateSettingsDto.BaseUrl.StartsWith("/")
|
||||
? $"/{updateSettingsDto.BaseUrl}"
|
||||
: updateSettingsDto.BaseUrl;
|
||||
path = !path.EndsWith("/")
|
||||
? $"{path}/"
|
||||
: path;
|
||||
setting.Value = path;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
|
||||
Configuration.LogLevel = updateSettingsDto.LoggingLevel;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EnableOpds && updateSettingsDto.EnableOpds + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.EnableOpds + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.ConvertBookmarkToWebP && updateSettingsDto.ConvertBookmarkToWebP + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.ConvertBookmarkToWebP + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
|
||||
if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value)
|
||||
{
|
||||
// Validate new directory can be used
|
||||
if (!await _directoryService.CheckWriteAccess(bookmarkDirectory))
|
||||
{
|
||||
return BadRequest("Bookmark Directory does not have correct permissions for Kavita to use");
|
||||
}
|
||||
|
||||
originalBookmarkDirectory = setting.Value;
|
||||
// Normalize the path deliminators. Just to look nice in DB, no functionality
|
||||
setting.Value = _directoryService.FileSystem.Path.GetFullPath(bookmarkDirectory);
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
updateBookmarks = true;
|
||||
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
if (!updateSettingsDto.AllowStatCollection)
|
||||
{
|
||||
_taskScheduler.CancelStatsTasks();
|
||||
}
|
||||
else
|
||||
{
|
||||
await _taskScheduler.ScheduleStatsTasks();
|
||||
}
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EnableSwaggerUi && updateSettingsDto.EnableSwaggerUi + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.EnableSwaggerUi + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.TotalBackups && updateSettingsDto.TotalBackups + string.Empty != setting.Value)
|
||||
{
|
||||
if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1)
|
||||
{
|
||||
return BadRequest("Total Backups must be between 1 and 30");
|
||||
}
|
||||
setting.Value = updateSettingsDto.TotalBackups + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
|
||||
FlurlHttp.ConfigureClient(setting.Value, cli =>
|
||||
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
|
||||
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EnableFolderWatching && updateSettingsDto.EnableFolderWatching + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.EnableFolderWatching + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
|
||||
if (updateSettingsDto.EnableFolderWatching)
|
||||
{
|
||||
await _libraryWatcher.StartWatching();
|
||||
}
|
||||
else
|
||||
{
|
||||
_libraryWatcher.StopWatching();
|
||||
}
|
||||
}
|
||||
var path = !updateSettingsDto.BaseUrl.StartsWith("/")
|
||||
? $"/{updateSettingsDto.BaseUrl}"
|
||||
: updateSettingsDto.BaseUrl;
|
||||
path = !path.EndsWith("/")
|
||||
? $"{path}/"
|
||||
: path;
|
||||
setting.Value = path;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok(updateSettingsDto);
|
||||
|
||||
try
|
||||
if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
if (updateBookmarks)
|
||||
{
|
||||
_directoryService.ExistOrCreate(bookmarkDirectory);
|
||||
_directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory);
|
||||
_directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory);
|
||||
}
|
||||
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
|
||||
LogLevelOptions.SwitchLogLevel(updateSettingsDto.LoggingLevel);
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
if (setting.Key == ServerSettingKey.EnableOpds && updateSettingsDto.EnableOpds + string.Empty != setting.Value)
|
||||
{
|
||||
_logger.LogError(ex, "There was an exception when updating server settings");
|
||||
await _unitOfWork.RollbackAsync();
|
||||
return BadRequest("There was a critical issue. Please try again.");
|
||||
setting.Value = updateSettingsDto.EnableOpds + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.ConvertBookmarkToWebP && updateSettingsDto.ConvertBookmarkToWebP + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.ConvertBookmarkToWebP + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
|
||||
_logger.LogInformation("Server Settings updated");
|
||||
await _taskScheduler.ScheduleTasks();
|
||||
return Ok(updateSettingsDto);
|
||||
if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value)
|
||||
{
|
||||
// Validate new directory can be used
|
||||
if (!await _directoryService.CheckWriteAccess(bookmarkDirectory))
|
||||
{
|
||||
return BadRequest("Bookmark Directory does not have correct permissions for Kavita to use");
|
||||
}
|
||||
|
||||
originalBookmarkDirectory = setting.Value;
|
||||
// Normalize the path deliminators. Just to look nice in DB, no functionality
|
||||
setting.Value = _directoryService.FileSystem.Path.GetFullPath(bookmarkDirectory);
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
updateBookmarks = true;
|
||||
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
if (!updateSettingsDto.AllowStatCollection)
|
||||
{
|
||||
_taskScheduler.CancelStatsTasks();
|
||||
}
|
||||
else
|
||||
{
|
||||
await _taskScheduler.ScheduleStatsTasks();
|
||||
}
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EnableSwaggerUi && updateSettingsDto.EnableSwaggerUi + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.EnableSwaggerUi + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.TotalBackups && updateSettingsDto.TotalBackups + string.Empty != setting.Value)
|
||||
{
|
||||
if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1)
|
||||
{
|
||||
return BadRequest("Total Backups must be between 1 and 30");
|
||||
}
|
||||
setting.Value = updateSettingsDto.TotalBackups + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
|
||||
FlurlHttp.ConfigureClient(setting.Value, cli =>
|
||||
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
|
||||
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EnableFolderWatching && updateSettingsDto.EnableFolderWatching + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.EnableFolderWatching + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
|
||||
if (updateSettingsDto.EnableFolderWatching)
|
||||
{
|
||||
await _libraryWatcher.StartWatching();
|
||||
}
|
||||
else
|
||||
{
|
||||
_libraryWatcher.StopWatching();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("task-frequencies")]
|
||||
public ActionResult<IEnumerable<string>> GetTaskFrequencies()
|
||||
if (!_unitOfWork.HasChanges()) return Ok(updateSettingsDto);
|
||||
|
||||
try
|
||||
{
|
||||
return Ok(CronConverter.Options);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
if (updateBookmarks)
|
||||
{
|
||||
_directoryService.ExistOrCreate(bookmarkDirectory);
|
||||
_directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory);
|
||||
_directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an exception when updating server settings");
|
||||
await _unitOfWork.RollbackAsync();
|
||||
return BadRequest("There was a critical issue. Please try again.");
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("library-types")]
|
||||
public ActionResult<IEnumerable<string>> GetLibraryTypes()
|
||||
{
|
||||
return Ok(Enum.GetValues<LibraryType>().Select(t => t.ToDescription()));
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("log-levels")]
|
||||
public ActionResult<IEnumerable<string>> GetLogLevels()
|
||||
{
|
||||
return Ok(new [] {"Trace", "Debug", "Information", "Warning", "Critical"});
|
||||
}
|
||||
_logger.LogInformation("Server Settings updated");
|
||||
await _taskScheduler.ScheduleTasks();
|
||||
return Ok(updateSettingsDto);
|
||||
}
|
||||
|
||||
[HttpGet("opds-enabled")]
|
||||
public async Task<ActionResult<bool>> GetOpdsEnabled()
|
||||
{
|
||||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
return Ok(settingsDto.EnableOpds);
|
||||
}
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("task-frequencies")]
|
||||
public ActionResult<IEnumerable<string>> GetTaskFrequencies()
|
||||
{
|
||||
return Ok(CronConverter.Options);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("library-types")]
|
||||
public ActionResult<IEnumerable<string>> GetLibraryTypes()
|
||||
{
|
||||
return Ok(Enum.GetValues<LibraryType>().Select(t => t.ToDescription()));
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("log-levels")]
|
||||
public ActionResult<IEnumerable<string>> GetLogLevels()
|
||||
{
|
||||
return Ok(new [] {"Trace", "Debug", "Information", "Warning", "Critical"});
|
||||
}
|
||||
|
||||
[HttpGet("opds-enabled")]
|
||||
public async Task<ActionResult<bool>> GetOpdsEnabled()
|
||||
{
|
||||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
return Ok(settingsDto.EnableOpds);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,298 +12,297 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using NetVips;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
public class UploadController : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
public class UploadController : BaseApiController
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly ILogger<UploadController> _logger;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IEventHub _eventHub;
|
||||
|
||||
/// <inheritdoc />
|
||||
public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILogger<UploadController> logger,
|
||||
ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub)
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly ILogger<UploadController> _logger;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IEventHub _eventHub;
|
||||
_unitOfWork = unitOfWork;
|
||||
_imageService = imageService;
|
||||
_logger = logger;
|
||||
_taskScheduler = taskScheduler;
|
||||
_directoryService = directoryService;
|
||||
_eventHub = eventHub;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILogger<UploadController> logger,
|
||||
ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub)
|
||||
/// <summary>
|
||||
/// This stores a file (image) in temp directory for use in a cover image replacement flow.
|
||||
/// This is automatically cleaned up.
|
||||
/// </summary>
|
||||
/// <param name="dto">Escaped url to download from</param>
|
||||
/// <returns>filename</returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("upload-by-url")]
|
||||
public async Task<ActionResult<string>> GetImageFromFile(UploadUrlDto dto)
|
||||
{
|
||||
var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace("/", "_").Replace(":", "_");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", "");
|
||||
try
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_imageService = imageService;
|
||||
_logger = logger;
|
||||
_taskScheduler = taskScheduler;
|
||||
_directoryService = directoryService;
|
||||
_eventHub = eventHub;
|
||||
var path = await dto.Url
|
||||
.DownloadFileAsync(_directoryService.TempDirectory, $"coverupload_{dateString}.{format}");
|
||||
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
|
||||
return BadRequest($"Could not download file");
|
||||
|
||||
if (!await _imageService.IsImage(path)) return BadRequest("Url does not return a valid image");
|
||||
|
||||
return $"coverupload_{dateString}.{format}";
|
||||
}
|
||||
catch (FlurlHttpException ex)
|
||||
{
|
||||
// Unauthorized
|
||||
if (ex.StatusCode == 401)
|
||||
return BadRequest("The server requires authentication to load the url externally");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This stores a file (image) in temp directory for use in a cover image replacement flow.
|
||||
/// This is automatically cleaned up.
|
||||
/// </summary>
|
||||
/// <param name="dto">Escaped url to download from</param>
|
||||
/// <returns>filename</returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("upload-by-url")]
|
||||
public async Task<ActionResult<string>> GetImageFromFile(UploadUrlDto dto)
|
||||
return BadRequest("Unable to download image, please use another url or upload by file");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces series cover image and locks it with a base64 encoded image
|
||||
/// </summary>
|
||||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[HttpPost("series")]
|
||||
public async Task<ActionResult> UploadSeriesCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
{
|
||||
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
|
||||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace("/", "_").Replace(":", "_");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", "");
|
||||
try
|
||||
{
|
||||
var path = await dto.Url
|
||||
.DownloadFileAsync(_directoryService.TempDirectory, $"coverupload_{dateString}.{format}");
|
||||
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
|
||||
return BadRequest($"Could not download file");
|
||||
|
||||
if (!await _imageService.IsImage(path)) return BadRequest("Url does not return a valid image");
|
||||
|
||||
return $"coverupload_{dateString}.{format}";
|
||||
}
|
||||
catch (FlurlHttpException ex)
|
||||
{
|
||||
// Unauthorized
|
||||
if (ex.StatusCode == 401)
|
||||
return BadRequest("The server requires authentication to load the url externally");
|
||||
}
|
||||
|
||||
return BadRequest("Unable to download image, please use another url or upload by file");
|
||||
return BadRequest("You must pass a url to use");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces series cover image and locks it with a base64 encoded image
|
||||
/// </summary>
|
||||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[HttpPost("series")]
|
||||
public async Task<ActionResult> UploadSeriesCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
try
|
||||
{
|
||||
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
|
||||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, ImageService.GetSeriesFormat(uploadFileDto.Id));
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
return BadRequest("You must pass a url to use");
|
||||
series.CoverImage = filePath;
|
||||
series.CoverImageLocked = true;
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
}
|
||||
|
||||
try
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, ImageService.GetSeriesFormat(uploadFileDto.Id));
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
series.CoverImage = filePath;
|
||||
series.CoverImageLocked = true;
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
}
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false);
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "There was an issue uploading cover image for Series {Id}", uploadFileDto.Id);
|
||||
await _unitOfWork.RollbackAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false);
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Series");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "There was an issue uploading cover image for Series {Id}", uploadFileDto.Id);
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces collection tag cover image and locks it with a base64 encoded image
|
||||
/// </summary>
|
||||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[HttpPost("collection")]
|
||||
public async Task<ActionResult> UploadCollectionCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
return BadRequest("Unable to save cover image to Series");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces collection tag cover image and locks it with a base64 encoded image
|
||||
/// </summary>
|
||||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[HttpPost("collection")]
|
||||
public async Task<ActionResult> UploadCollectionCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
{
|
||||
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
|
||||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
|
||||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
return BadRequest("You must pass a url to use");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}");
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(uploadFileDto.Id);
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
tag.CoverImage = filePath;
|
||||
tag.CoverImageLocked = true;
|
||||
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||
}
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "There was an issue uploading cover image for Collection Tag {Id}", uploadFileDto.Id);
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Collection Tag");
|
||||
return BadRequest("You must pass a url to use");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces reading list cover image and locks it with a base64 encoded image
|
||||
/// </summary>
|
||||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[HttpPost("reading-list")]
|
||||
public async Task<ActionResult> UploadReadingListCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
try
|
||||
{
|
||||
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
|
||||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}");
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(uploadFileDto.Id);
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
return BadRequest("You must pass a url to use");
|
||||
tag.CoverImage = filePath;
|
||||
tag.CoverImageLocked = true;
|
||||
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||
}
|
||||
|
||||
try
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id);
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
readingList.CoverImage = filePath;
|
||||
readingList.CoverImageLocked = true;
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "There was an issue uploading cover image for Reading List {Id}", uploadFileDto.Id);
|
||||
await _unitOfWork.RollbackAsync();
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Reading List");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "There was an issue uploading cover image for Collection Tag {Id}", uploadFileDto.Id);
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
|
||||
/// </summary>
|
||||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[HttpPost("chapter")]
|
||||
public async Task<ActionResult> UploadChapterCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
return BadRequest("Unable to save cover image to Collection Tag");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces reading list cover image and locks it with a base64 encoded image
|
||||
/// </summary>
|
||||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[HttpPost("reading-list")]
|
||||
public async Task<ActionResult> UploadReadingListCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
{
|
||||
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
|
||||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
|
||||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
return BadRequest("You must pass a url to use");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
chapter.CoverImage = filePath;
|
||||
chapter.CoverImageLocked = true;
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId);
|
||||
volume.CoverImage = chapter.CoverImage;
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
}
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(chapter.VolumeId, MessageFactoryEntityTypes.Volume), false);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "There was an issue uploading cover image for Chapter {Id}", uploadFileDto.Id);
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Chapter");
|
||||
return BadRequest("You must pass a url to use");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
|
||||
/// </summary>
|
||||
/// <param name="uploadFileDto">Does not use Url property</param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("reset-chapter-lock")]
|
||||
public async Task<ActionResult> ResetChapterLock(UploadFileDto uploadFileDto)
|
||||
try
|
||||
{
|
||||
try
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id);
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
var originalFile = chapter.CoverImage;
|
||||
chapter.CoverImage = string.Empty;
|
||||
chapter.CoverImageLocked = false;
|
||||
readingList.CoverImage = filePath;
|
||||
readingList.CoverImageLocked = true;
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "There was an issue uploading cover image for Reading List {Id}", uploadFileDto.Id);
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Reading List");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
|
||||
/// </summary>
|
||||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[HttpPost("chapter")]
|
||||
public async Task<ActionResult> UploadChapterCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
{
|
||||
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
|
||||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
return BadRequest("You must pass a url to use");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
chapter.CoverImage = filePath;
|
||||
chapter.CoverImageLocked = true;
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId);
|
||||
volume.CoverImage = chapter.CoverImage;
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
System.IO.File.Delete(originalFile);
|
||||
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
_logger.LogError(e, "There was an issue resetting cover lock for Chapter {Id}", uploadFileDto.Id);
|
||||
await _unitOfWork.RollbackAsync();
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(chapter.VolumeId, MessageFactoryEntityTypes.Volume), false);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to resetting cover lock for Chapter");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "There was an issue uploading cover image for Chapter {Id}", uploadFileDto.Id);
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Chapter");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
|
||||
/// </summary>
|
||||
/// <param name="uploadFileDto">Does not use Url property</param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("reset-chapter-lock")]
|
||||
public async Task<ActionResult> ResetChapterLock(UploadFileDto uploadFileDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
var originalFile = chapter.CoverImage;
|
||||
chapter.CoverImage = string.Empty;
|
||||
chapter.CoverImageLocked = false;
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId);
|
||||
volume.CoverImage = chapter.CoverImage;
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
System.IO.File.Delete(originalFile);
|
||||
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "There was an issue resetting cover lock for Chapter {Id}", uploadFileDto.Id);
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to resetting cover lock for Chapter");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,112 +13,111 @@ using AutoMapper;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace API.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class UsersController : BaseApiController
|
||||
{
|
||||
[Authorize]
|
||||
public class UsersController : BaseApiController
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IEventHub _eventHub;
|
||||
|
||||
public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub)
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IEventHub _eventHub;
|
||||
_unitOfWork = unitOfWork;
|
||||
_mapper = mapper;
|
||||
_eventHub = eventHub;
|
||||
}
|
||||
|
||||
public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub)
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpDelete("delete-user")]
|
||||
public async Task<ActionResult> DeleteUser(string username)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username);
|
||||
_unitOfWork.UserRepository.Delete(user);
|
||||
|
||||
if (await _unitOfWork.CommitAsync()) return Ok();
|
||||
|
||||
return BadRequest("Could not delete the user.");
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<MemberDto>>> GetUsers()
|
||||
{
|
||||
return Ok(await _unitOfWork.UserRepository.GetEmailConfirmedMemberDtosAsync());
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("pending")]
|
||||
public async Task<ActionResult<IEnumerable<MemberDto>>> GetPendingUsers()
|
||||
{
|
||||
return Ok(await _unitOfWork.UserRepository.GetPendingMemberDtosAsync());
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("has-reading-progress")]
|
||||
public async Task<ActionResult<bool>> HasReadingProgress(int libraryId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId));
|
||||
}
|
||||
|
||||
[HttpGet("has-library-access")]
|
||||
public async Task<ActionResult<bool>> HasLibraryAccess(int libraryId)
|
||||
{
|
||||
var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername());
|
||||
return Ok(libs.Any(x => x.Id == libraryId));
|
||||
}
|
||||
|
||||
[HttpPost("update-preferences")]
|
||||
public async Task<ActionResult<UserPreferencesDto>> UpdatePreferences(UserPreferencesDto preferencesDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(),
|
||||
AppUserIncludes.UserPreferences);
|
||||
var existingPreferences = user.UserPreferences;
|
||||
|
||||
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
|
||||
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
|
||||
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
|
||||
existingPreferences.AutoCloseMenu = preferencesDto.AutoCloseMenu;
|
||||
existingPreferences.ShowScreenHints = preferencesDto.ShowScreenHints;
|
||||
existingPreferences.ReaderMode = preferencesDto.ReaderMode;
|
||||
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
|
||||
existingPreferences.BackgroundColor = string.IsNullOrEmpty(preferencesDto.BackgroundColor) ? "#000000" : preferencesDto.BackgroundColor;
|
||||
existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin;
|
||||
existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing;
|
||||
existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily;
|
||||
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
|
||||
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
|
||||
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
|
||||
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
||||
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
|
||||
existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode;
|
||||
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
|
||||
existingPreferences.GlobalPageLayoutMode = preferencesDto.GlobalPageLayoutMode;
|
||||
existingPreferences.BlurUnreadSummaries = preferencesDto.BlurUnreadSummaries;
|
||||
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
|
||||
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
|
||||
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
|
||||
|
||||
_unitOfWork.UserRepository.Update(existingPreferences);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_mapper = mapper;
|
||||
_eventHub = eventHub;
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
|
||||
return Ok(preferencesDto);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpDelete("delete-user")]
|
||||
public async Task<ActionResult> DeleteUser(string username)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username);
|
||||
_unitOfWork.UserRepository.Delete(user);
|
||||
return BadRequest("There was an issue saving preferences.");
|
||||
}
|
||||
|
||||
if (await _unitOfWork.CommitAsync()) return Ok();
|
||||
[HttpGet("get-preferences")]
|
||||
public async Task<ActionResult<UserPreferencesDto>> GetPreferences()
|
||||
{
|
||||
return _mapper.Map<UserPreferencesDto>(
|
||||
await _unitOfWork.UserRepository.GetPreferencesAsync(User.GetUsername()));
|
||||
|
||||
return BadRequest("Could not delete the user.");
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<MemberDto>>> GetUsers()
|
||||
{
|
||||
return Ok(await _unitOfWork.UserRepository.GetEmailConfirmedMemberDtosAsync());
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("pending")]
|
||||
public async Task<ActionResult<IEnumerable<MemberDto>>> GetPendingUsers()
|
||||
{
|
||||
return Ok(await _unitOfWork.UserRepository.GetPendingMemberDtosAsync());
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("has-reading-progress")]
|
||||
public async Task<ActionResult<bool>> HasReadingProgress(int libraryId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId));
|
||||
}
|
||||
|
||||
[HttpGet("has-library-access")]
|
||||
public async Task<ActionResult<bool>> HasLibraryAccess(int libraryId)
|
||||
{
|
||||
var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername());
|
||||
return Ok(libs.Any(x => x.Id == libraryId));
|
||||
}
|
||||
|
||||
[HttpPost("update-preferences")]
|
||||
public async Task<ActionResult<UserPreferencesDto>> UpdatePreferences(UserPreferencesDto preferencesDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(),
|
||||
AppUserIncludes.UserPreferences);
|
||||
var existingPreferences = user.UserPreferences;
|
||||
|
||||
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
|
||||
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
|
||||
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
|
||||
existingPreferences.AutoCloseMenu = preferencesDto.AutoCloseMenu;
|
||||
existingPreferences.ShowScreenHints = preferencesDto.ShowScreenHints;
|
||||
existingPreferences.ReaderMode = preferencesDto.ReaderMode;
|
||||
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
|
||||
existingPreferences.BackgroundColor = string.IsNullOrEmpty(preferencesDto.BackgroundColor) ? "#000000" : preferencesDto.BackgroundColor;
|
||||
existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin;
|
||||
existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing;
|
||||
existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily;
|
||||
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
|
||||
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
|
||||
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
|
||||
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
||||
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
|
||||
existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode;
|
||||
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
|
||||
existingPreferences.GlobalPageLayoutMode = preferencesDto.GlobalPageLayoutMode;
|
||||
existingPreferences.BlurUnreadSummaries = preferencesDto.BlurUnreadSummaries;
|
||||
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
|
||||
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
|
||||
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
|
||||
|
||||
_unitOfWork.UserRepository.Update(existingPreferences);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
|
||||
return Ok(preferencesDto);
|
||||
}
|
||||
|
||||
return BadRequest("There was an issue saving preferences.");
|
||||
}
|
||||
|
||||
[HttpGet("get-preferences")]
|
||||
public async Task<ActionResult<UserPreferencesDto>> GetPreferences()
|
||||
{
|
||||
return _mapper.Map<UserPreferencesDto>(
|
||||
await _unitOfWork.UserRepository.GetPreferencesAsync(User.GetUsername()));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue