Merged develop in

This commit is contained in:
Joseph Milazzo 2025-04-26 16:17:05 -05:00
commit d12a79892f
1443 changed files with 215765 additions and 44113 deletions

View file

@ -20,9 +20,10 @@ using API.Services.Tasks.Scanner;
using API.SignalR;
using AutoMapper;
using EasyCaching.Core;
using Hangfire;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.Logging;
using TaskScheduler = API.Services.TaskScheduler;
@ -79,10 +80,10 @@ public class LibraryController : BaseApiController
.WithFolders(dto.Folders.Select(x => new FolderPath {Path = x}).Distinct().ToList())
.WithFolderWatching(dto.FolderWatching)
.WithIncludeInDashboard(dto.IncludeInDashboard)
.WithIncludeInRecommended(dto.IncludeInRecommended)
.WithManageCollections(dto.ManageCollections)
.WithManageReadingLists(dto.ManageReadingLists)
.WIthAllowScrobbling(dto.AllowScrobbling)
.WithAllowScrobbling(dto.AllowScrobbling)
.WithAllowMetadataMatching(dto.AllowMetadataMatching)
.Build();
library.LibraryFileTypes = dto.FileGroupTypes
@ -134,13 +135,19 @@ public class LibraryController : BaseApiController
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library"));
await _libraryWatcher.RestartWatching();
_taskScheduler.ScanLibrary(library.Id);
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
if (library.FolderWatching)
{
await _libraryWatcher.RestartWatching();
}
BackgroundJob.Enqueue(() => _taskScheduler.ScanLibrary(library.Id, false));
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
MessageFactory.LibraryModifiedEvent(library.Id, "create"), false);
await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate,
MessageFactory.SideNavUpdateEvent(User.GetUserId()), false);
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
return Ok();
}
@ -167,11 +174,35 @@ public class LibraryController : BaseApiController
return Ok(_directoryService.ListDirectory(path));
}
/// <summary>
/// Return a specific library
/// </summary>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpGet]
public async Task<ActionResult<LibraryDto?>> GetLibrary(int libraryId)
{
var username = User.GetUsername();
if (string.IsNullOrEmpty(username)) return Unauthorized();
var cacheKey = CacheKey + username;
var result = await _libraryCacheProvider.GetAsync<IEnumerable<LibraryDto>>(cacheKey);
if (result.HasValue)
{
return Ok(result.Value.FirstOrDefault(l => l.Id == libraryId));
}
var ret = _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username).ToList();
await _libraryCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromHours(24));
return Ok(ret.Find(l => l.Id == libraryId));
}
/// <summary>
/// Return all libraries in the Server
/// </summary>
/// <returns></returns>
[HttpGet]
[HttpGet("libraries")]
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries()
{
var username = User.GetUsername();
@ -183,7 +214,6 @@ public class LibraryController : BaseApiController
var ret = _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username);
await _libraryCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromHours(24));
_logger.LogDebug("Caching libraries for {Key}", cacheKey);
return Ok(ret);
}
@ -268,7 +298,23 @@ public class LibraryController : BaseApiController
public async Task<ActionResult> Scan(int libraryId, bool force = false)
{
if (libraryId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "libraryId"));
_taskScheduler.ScanLibrary(libraryId, force);
await _taskScheduler.ScanLibrary(libraryId, force);
return Ok();
}
/// <summary>
/// Enqueues a bunch of library scans
/// </summary>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("scan-multiple")]
public async Task<ActionResult> ScanMultiple(BulkActionDto dto)
{
foreach (var libraryId in dto.Ids)
{
await _taskScheduler.ScanLibrary(libraryId, dto.Force ?? false);
}
return Ok();
}
@ -287,17 +333,63 @@ public class LibraryController : BaseApiController
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("refresh-metadata")]
public ActionResult RefreshMetadata(int libraryId, bool force = true)
public ActionResult RefreshMetadata(int libraryId, bool force = true, bool forceColorscape = true)
{
_taskScheduler.RefreshMetadata(libraryId, force);
_taskScheduler.RefreshMetadata(libraryId, force, forceColorscape);
return Ok();
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("analyze")]
public ActionResult Analyze(int libraryId)
[HttpPost("refresh-metadata-multiple")]
public ActionResult RefreshMetadataMultiple(BulkActionDto dto, bool forceColorscape = true)
{
_taskScheduler.AnalyzeFilesForLibrary(libraryId, true);
foreach (var libraryId in dto.Ids)
{
_taskScheduler.RefreshMetadata(libraryId, dto.Force ?? false, forceColorscape);
}
return Ok();
}
/// <summary>
/// Copy the library settings (adv tab + optional type) to a set of other libraries.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("copy-settings-from")]
public async Task<ActionResult> CopySettingsFromLibraryToLibraries(CopySettingsFromLibraryDto dto)
{
var sourceLibrary = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.SourceLibraryId, LibraryIncludes.ExcludePatterns | LibraryIncludes.FileTypes);
if (sourceLibrary == null) return BadRequest("SourceLibraryId must exist");
var libraries = await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.TargetLibraryIds, LibraryIncludes.ExcludePatterns | LibraryIncludes.FileTypes | LibraryIncludes.Folders);
foreach (var targetLibrary in libraries)
{
UpdateLibrarySettings(new UpdateLibraryDto()
{
Folders = targetLibrary.Folders.Select(s => s.Path),
Name = targetLibrary.Name,
Id = targetLibrary.Id,
Type = sourceLibrary.Type,
AllowScrobbling = sourceLibrary.AllowScrobbling,
ExcludePatterns = sourceLibrary.LibraryExcludePatterns.Select(p => p.Pattern).ToList(),
FolderWatching = sourceLibrary.FolderWatching,
ManageCollections = sourceLibrary.ManageCollections,
FileGroupTypes = sourceLibrary.LibraryFileTypes.Select(t => t.FileTypeGroup).ToList(),
IncludeInDashboard = sourceLibrary.IncludeInDashboard,
IncludeInSearch = sourceLibrary.IncludeInSearch,
ManageReadingLists = sourceLibrary.ManageReadingLists
}, targetLibrary, dto.IncludeType);
}
await _unitOfWork.CommitAsync();
if (sourceLibrary.FolderWatching)
{
BackgroundJob.Enqueue(() => _libraryWatcher.RestartWatching());
}
return Ok();
}
@ -327,20 +419,65 @@ public class LibraryController : BaseApiController
.Distinct()
.Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath);
var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder,
new List<string>() {dto.FolderPath});
var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder, [dto.FolderPath]);
_taskScheduler.ScanFolder(seriesFolder.Keys.Count == 1 ? seriesFolder.Keys.First() : dto.FolderPath);
return Ok();
}
/// <summary>
/// Deletes the library and all series within it.
/// </summary>
/// <remarks>This does not touch any files</remarks>
/// <param name="libraryId"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpDelete("delete")]
public async Task<ActionResult<bool>> DeleteLibrary(int libraryId)
{
_logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, User.GetUsername());
try
{
return Ok(await DeleteLibrary(libraryId, User.GetUserId()));
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
/// <summary>
/// Deletes multiple libraries and all series within it.
/// </summary>
/// <remarks>This does not touch any files</remarks>
/// <param name="libraryIds"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpDelete("delete-multiple")]
public async Task<ActionResult<bool>> DeleteMultipleLibraries([FromQuery] List<int> libraryIds)
{
var username = User.GetUsername();
_logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, username);
_logger.LogInformation("Libraries {LibraryIds} are being deleted by {UserName}", libraryIds, username);
foreach (var libraryId in libraryIds)
{
try
{
await DeleteLibrary(libraryId, User.GetUserId());
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
return Ok();
}
private async Task<bool> DeleteLibrary(int libraryId, int userId)
{
var series = await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId);
var seriesIds = series.Select(x => x.Id).ToArray();
var chapterIds =
@ -351,16 +488,19 @@ public class LibraryController : BaseApiController
if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId))
{
_logger.LogInformation("User is attempting to delete a library while a scan is in progress");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "delete-library-while-scan"));
throw new KavitaException(await _localizationService.Translate(userId, "delete-library-while-scan"));
}
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist"));
if (library == null)
{
throw new KavitaException(await _localizationService.Translate(userId, "library-doesnt-exist"));
}
// Due to a bad schema that I can't figure out how to fix, we need to erase all RelatedSeries before we delete the library
// Aka SeriesRelation has an invalid foreign key
foreach (var s in await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(library.Id,
SeriesIncludes.Related))
foreach (var s in await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(library.Id, SeriesIncludes.Related))
{
s.Relations = new List<SeriesRelation>();
_unitOfWork.SeriesRepository.Update(s);
@ -377,7 +517,7 @@ public class LibraryController : BaseApiController
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate,
MessageFactory.SideNavUpdateEvent(User.GetUserId()), false);
MessageFactory.SideNavUpdateEvent(userId), false);
if (chapterIds.Any())
{
@ -386,7 +526,7 @@ public class LibraryController : BaseApiController
_taskScheduler.CleanupChapters(chapterIds);
}
await _libraryWatcher.RestartWatching();
BackgroundJob.Enqueue(() => _libraryWatcher.RestartWatching());
foreach (var seriesId in seriesIds)
{
@ -396,13 +536,13 @@ public class LibraryController : BaseApiController
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
MessageFactory.LibraryModifiedEvent(libraryId, "delete"), false);
return Ok(true);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "There was a critical issue. Please try again");
await _unitOfWork.RollbackAsync();
return Ok(false);
return false;
}
}
@ -444,14 +584,46 @@ public class LibraryController : BaseApiController
var typeUpdate = library.Type != dto.Type;
var folderWatchingUpdate = library.FolderWatching != dto.FolderWatching;
library.Type = dto.Type;
UpdateLibrarySettings(dto, library);
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(userId, "generic-library-update"));
if (folderWatchingUpdate || originalFoldersCount != dto.Folders.Count() || typeUpdate)
{
BackgroundJob.Enqueue(() => _libraryWatcher.RestartWatching());
}
if (originalFoldersCount != dto.Folders.Count() || typeUpdate)
{
await _taskScheduler.ScanLibrary(library.Id);
}
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
MessageFactory.LibraryModifiedEvent(library.Id, "update"), false);
await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate,
MessageFactory.SideNavUpdateEvent(userId), false);
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
return Ok();
}
private void UpdateLibrarySettings(UpdateLibraryDto dto, Library library, bool updateType = true)
{
if (updateType)
{
library.Type = dto.Type;
}
library.FolderWatching = dto.FolderWatching;
library.IncludeInDashboard = dto.IncludeInDashboard;
library.IncludeInRecommended = dto.IncludeInRecommended;
library.IncludeInSearch = dto.IncludeInSearch;
library.ManageCollections = dto.ManageCollections;
library.ManageReadingLists = dto.ManageReadingLists;
library.AllowScrobbling = dto.AllowScrobbling;
library.AllowMetadataMatching = dto.AllowMetadataMatching;
library.LibraryFileTypes = dto.FileGroupTypes
.Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id})
.Distinct()
@ -463,7 +635,7 @@ public class LibraryController : BaseApiController
.ToList();
// Override Scrobbling for Comic libraries since there are no providers to scrobble to
if (library.Type == LibraryType.Comic)
if (library.Type is LibraryType.Comic or LibraryType.ComicVine)
{
_logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name.Replace(Environment.NewLine, string.Empty));
library.AllowScrobbling = false;
@ -471,28 +643,6 @@ public class LibraryController : BaseApiController
_unitOfWork.LibraryRepository.Update(library);
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(userId, "generic-library-update"));
if (originalFoldersCount != dto.Folders.Count() || typeUpdate)
{
await _libraryWatcher.RestartWatching();
_taskScheduler.ScanLibrary(library.Id);
}
if (folderWatchingUpdate)
{
await _libraryWatcher.RestartWatching();
}
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
MessageFactory.LibraryModifiedEvent(library.Id, "update"), false);
await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate,
MessageFactory.SideNavUpdateEvent(userId), false);
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
return Ok();
}
/// <summary>