mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
* Implemented a basic version of enhanced search where we can return multiple types of entities in one go. Current unoptimized version is twice as expensive as normal search, but under NFR. Currently 200ms max. * Worked in some basic code for grouped typeahead search component. Keyboard navigation is working. * Most of the code is in place for the typeahead. Needs css work and some accessibility work. * Hooked up filtering into all-series. Added debouncing on search, clear input field now works. Some optimizations related to memory cleanup * Added ability to define a custom placeholder * Hooked in noResults template and logic * Fixed a duplicate issue in Collection tag searching and commented out old code. OPDS still needs some updates. * Don't trigger inputChanged when reopening/clicking on input. * Added Reading list to OPDS search * Added a new image component so all the images can be lazyloaded without logic duplication * Added a maxWidth/Height on the image component * Search css update * cursor fixes * card changes - fixing border radius on cards - adding bottom card color * Expose intenral state of if the search component has focus * Adjusted the accessibility to not use complex keys and just use tab instead since this is a search, not a typeahead * Cleaned up dead code, removed angular-ng-complete library as it's no longer used. * Fixes for mobile search * Merged code * Fixed a bad merge and some nav bar styling * Cleaned up the focus code for nav bar * Removed focusIndex and just use hover state. Fixed clicking on items * fixing overlay overlap issue Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
252 lines
9.7 KiB
C#
252 lines
9.7 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 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;
|
|
|
|
public LibraryController(IDirectoryService directoryService,
|
|
ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler,
|
|
IUnitOfWork unitOfWork)
|
|
{
|
|
_directoryService = directoryService;
|
|
_logger = logger;
|
|
_mapper = mapper;
|
|
_taskScheduler = taskScheduler;
|
|
_unitOfWork = unitOfWork;
|
|
}
|
|
|
|
/// <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);
|
|
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);
|
|
}
|
|
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);
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|