mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
* Cleaned up some styles on the progress bar in book reader * Fixed up some phone-hidden classes and added titles around the codebase. Stat reporting on first run now takes into account that admin user wont exist. * Fixed manage library page not updating last scan time when a notification event comes in. * Integrated SeriesSort ComicInfo tag (somehow it got missed) * Some minor style changes and no results found for bookmarks on chapter detail modal * Fixed the labels in action bar on book reader so Prev/Next are in same place * Cleaned up some responsive styles around images and reduced custom classes in light of new display classes on collection detail and series detail pages * Fixed an issue with webkit browsers and book reader where the scroll to would fail as the document wasn't fully rendered. A 10ms delay seems to fix the issue. * Cleaned up some code and filtering for collections. Collection detail is missing filtering functionality somehow, disabled the button and will add in future release * Correctly validate and show a message when a user is not an admin or has change password role when going through forget password flow. * Fixed a bug on manage libraries where library last scan didn't work on first scan of a library, due to there being no updated series. * Fixed a rendering issue with text being focused on confirm email page textboxes. Fixed a bug where when deleting a theme that was default, Kavita didn't reset Dark as the default theme. * Cleaned up the naming and styles for side nav active item hover * Fixed event widget to have correct styling on eink and light * Tried to fix a rendering issue on side nav for light themes, but can't figure it out * On light more, ensure switches are green * Fixed a bug where opening a page with a preselected filter, the filter toggle button would require 2 clicks to collapse * Reverted the revert of On Deck. * Improved the upload by url experience by sending a custom fail error to UI when a url returns 401. * When deleting a library, emit a series removed event for each series removed so user's dashboards/screens update. * Fixed an api throwing an error due to text being sent back instead of json. * Fixed a refresh bug with refreshing pending invites after deleting an invite. Ensure we always refresh pending invites even if user cancel's from invite, as they might invite, then hit cancel, where invite is still active. * Fixed a bug where invited users with + in the email would fail due to validation, but UI wouldn't properly inform user.
266 lines
10 KiB
C#
266 lines
10 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using API.Data;
|
|
using API.Data.Repositories;
|
|
using API.DTOs;
|
|
using API.DTOs.Search;
|
|
using API.Entities;
|
|
using API.Entities.Enums;
|
|
using API.Extensions;
|
|
using API.Services;
|
|
using API.SignalR;
|
|
using AutoMapper;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace API.Controllers
|
|
{
|
|
[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;
|
|
|
|
public LibraryController(IDirectoryService directoryService,
|
|
ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler,
|
|
IUnitOfWork unitOfWork, IEventHub eventHub)
|
|
{
|
|
_directoryService = directoryService;
|
|
_logger = logger;
|
|
_mapper = mapper;
|
|
_taskScheduler = taskScheduler;
|
|
_unitOfWork = unitOfWork;
|
|
_eventHub = eventHub;
|
|
}
|
|
|
|
/// <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))
|
|
{
|
|
return BadRequest("Library name already exists. Please choose a unique name to the server.");
|
|
}
|
|
|
|
var library = new Library
|
|
{
|
|
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);
|
|
_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<string>> GetDirectories(string path)
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
{
|
|
return Ok(Directory.GetLogicalDrives());
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
[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)
|
|
{
|
|
_taskScheduler.ScanLibrary(libraryId);
|
|
return Ok();
|
|
}
|
|
|
|
[Authorize(Policy = "RequireAdminRole")]
|
|
[HttpPost("refresh-metadata")]
|
|
public ActionResult RefreshMetadata(int libraryId)
|
|
{
|
|
_taskScheduler.RefreshMetadata(libraryId);
|
|
return Ok();
|
|
}
|
|
|
|
[HttpGet("libraries")]
|
|
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibrariesForUser()
|
|
{
|
|
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()));
|
|
}
|
|
|
|
[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);
|
|
_unitOfWork.LibraryRepository.Delete(library);
|
|
await _unitOfWork.CommitAsync();
|
|
|
|
if (chapterIds.Any())
|
|
{
|
|
await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters();
|
|
await _unitOfWork.CommitAsync();
|
|
_taskScheduler.CleanupChapters(chapterIds);
|
|
}
|
|
|
|
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);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "There was a critical error trying to delete the library");
|
|
await _unitOfWork.RollbackAsync();
|
|
return Ok(false);
|
|
}
|
|
}
|
|
|
|
[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();
|
|
|
|
_unitOfWork.LibraryRepository.Update(library);
|
|
|
|
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the library.");
|
|
if (originalFolders.Count != libraryForUserDto.Folders.Count())
|
|
{
|
|
_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));
|
|
}
|
|
}
|
|
}
|