Library Settings Modal + New Library Settings (#1660)
* Bump loader-utils from 2.0.3 to 2.0.4 in /UI/Web Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Fixed want to read button on series detail not performing the correct action * Started the library settings. Added ability to update a cover image for a library. Updated backup db to also copy reading list (and now library) cover images. * Integrated Edit Library into new settings (not tested) and hooked up a wizard-like flow for new library. * Fixed a missing update event in backend when updating a library. * Disable Save when form invalid. Do inline validation on Library name when user types to ensure the name is valid. * Trim library names before you check anything * General code cleanup * Implemented advanced settings for library (include in dashboard, search, recommended) and ability to turn off folder watching for individual libraries. Refactored some code to streamline perf in some flows. * Removed old components replaced with new modal * Code smells Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
parent
48b15e564d
commit
73d77e6264
47 changed files with 2530 additions and 276 deletions
|
@ -247,7 +247,6 @@ public class AccountController : BaseApiController
|
|||
[HttpGet("roles")]
|
||||
public ActionResult<IList<string>> GetRoles()
|
||||
{
|
||||
// TODO: This should be moved to ServerController
|
||||
return typeof(PolicyConstants)
|
||||
.GetFields(BindingFlags.Public | BindingFlags.Static)
|
||||
.Where(f => f.FieldType == typeof(string))
|
||||
|
|
|
@ -41,6 +41,22 @@ public class ImageController : BaseApiController
|
|||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns cover image for Library
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("library-cover")]
|
||||
[ResponseCache(CacheProfileName = "Images")]
|
||||
public async Task<ActionResult> GetLibraryCoverImage(int libraryId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId));
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns cover image for Volume
|
||||
/// </summary>
|
||||
|
|
|
@ -295,35 +295,62 @@ public class LibraryController : BaseApiController
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the library name exists or not
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("name-exists")]
|
||||
public async Task<ActionResult<bool>> IsLibraryNameValid(string name)
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.LibraryExists(name.Trim()));
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto libraryForUserDto)
|
||||
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto dto)
|
||||
{
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id, LibraryIncludes.Folders);
|
||||
var newName = dto.Name.Trim();
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders);
|
||||
if (await _unitOfWork.LibraryRepository.LibraryExists(newName) && !library.Name.Equals(newName))
|
||||
return BadRequest("Library name already exists");
|
||||
|
||||
var originalFolders = library.Folders.Select(x => x.Path).ToList();
|
||||
|
||||
library.Name = libraryForUserDto.Name;
|
||||
library.Folders = libraryForUserDto.Folders.Select(s => new FolderPath() {Path = s}).ToList();
|
||||
library.Name = newName;
|
||||
library.Folders = dto.Folders.Select(s => new FolderPath() {Path = s}).ToList();
|
||||
|
||||
var typeUpdate = library.Type != libraryForUserDto.Type;
|
||||
library.Type = libraryForUserDto.Type;
|
||||
var typeUpdate = library.Type != dto.Type;
|
||||
var folderWatchingUpdate = library.FolderWatching != dto.FolderWatching;
|
||||
library.Type = dto.Type;
|
||||
library.FolderWatching = dto.FolderWatching;
|
||||
library.IncludeInDashboard = dto.IncludeInDashboard;
|
||||
library.IncludeInRecommended = dto.IncludeInRecommended;
|
||||
library.IncludeInSearch = dto.IncludeInSearch;
|
||||
|
||||
_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)
|
||||
if (originalFolders.Count != 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);
|
||||
|
||||
return Ok();
|
||||
|
||||
}
|
||||
|
|
|
@ -100,7 +100,6 @@ public class ReaderController : BaseApiController
|
|||
|
||||
try
|
||||
{
|
||||
// TODO: This code is very generic and repeated, see if we can refactor into a common method
|
||||
var path = _cacheService.GetCachedPagePath(chapter, page);
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}. Try refreshing to allow re-cache.");
|
||||
var format = Path.GetExtension(path).Replace(".", "");
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Search;
|
||||
using API.Extensions;
|
||||
|
@ -50,17 +51,16 @@ public class SearchController : BaseApiController
|
|||
[HttpGet("search")]
|
||||
public async Task<ActionResult<SearchResultGroupDto>> Search(string queryString)
|
||||
{
|
||||
queryString = Uri.UnescapeDataString(queryString).Trim().Replace(@"%", string.Empty).Replace(":", string.Empty);
|
||||
queryString = Services.Tasks.Scanner.Parser.Parser.CleanQuery(queryString);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
// Get libraries user has access to
|
||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
|
||||
var libraries = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(user.Id, QueryContext.Search).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");
|
||||
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);
|
||||
var series = await _unitOfWork.SeriesRepository.SearchSeries(user.Id, isAdmin,
|
||||
libraries, queryString);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
|
|
|
@ -179,8 +179,6 @@ public class ServerController : BaseApiController
|
|||
LastExecution = dto.LastExecution,
|
||||
});
|
||||
|
||||
// For now, let's just do something simple
|
||||
//var enqueuedJobs = JobStorage.Current.GetMonitoringApi().EnqueuedJobs("default", 0, int.MaxValue);
|
||||
return Ok(recurringJobs);
|
||||
|
||||
}
|
||||
|
|
|
@ -266,6 +266,63 @@ public class UploadController : BaseApiController
|
|||
return BadRequest("Unable to save cover image to Chapter");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces library cover image with a base64 encoded image. If empty string passed, will reset to null.
|
||||
/// </summary>
|
||||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[HttpPost("library")]
|
||||
public async Task<ActionResult> UploadLibraryCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
{
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(uploadFileDto.Id);
|
||||
if (library == null) return BadRequest("This library does not exist");
|
||||
|
||||
// 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))
|
||||
{
|
||||
library.CoverImage = null;
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(library.Id, MessageFactoryEntityTypes.Library), false);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetLibraryFormat(uploadFileDto.Id)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
library.CoverImage = filePath;
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
}
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(library.Id, MessageFactoryEntityTypes.Library), false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "There was an issue uploading cover image for Library {Id}", uploadFileDto.Id);
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Library");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
|
||||
/// </summary>
|
||||
|
|
|
@ -13,5 +13,25 @@ public class LibraryDto
|
|||
/// </summary>
|
||||
public DateTime LastScanned { get; init; }
|
||||
public LibraryType Type { get; init; }
|
||||
/// <summary>
|
||||
/// An optional Cover Image or null
|
||||
/// </summary>
|
||||
public string CoverImage { get; init; }
|
||||
/// <summary>
|
||||
/// If Folder Watching is enabled for this library
|
||||
/// </summary>
|
||||
public bool FolderWatching { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Include Library series on Dashboard Streams
|
||||
/// </summary>
|
||||
public bool IncludeInDashboard { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Include Library series on Recommended Streams
|
||||
/// </summary>
|
||||
public bool IncludeInRecommended { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Include library series in Search
|
||||
/// </summary>
|
||||
public bool IncludeInSearch { get; set; } = true;
|
||||
public ICollection<string> Folders { get; init; }
|
||||
}
|
||||
|
|
|
@ -9,4 +9,9 @@ public class UpdateLibraryDto
|
|||
public string Name { get; init; }
|
||||
public LibraryType Type { get; set; }
|
||||
public IEnumerable<string> Folders { get; init; }
|
||||
public bool FolderWatching { get; init; }
|
||||
public bool IncludeInDashboard { get; init; }
|
||||
public bool IncludeInRecommended { get; init; }
|
||||
public bool IncludeInSearch { get; init; }
|
||||
|
||||
}
|
||||
|
|
|
@ -89,6 +89,20 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.GlobalPageLayoutMode)
|
||||
.HasDefaultValue(PageLayoutMode.Cards);
|
||||
|
||||
|
||||
builder.Entity<Library>()
|
||||
.Property(b => b.FolderWatching)
|
||||
.HasDefaultValue(true);
|
||||
builder.Entity<Library>()
|
||||
.Property(b => b.IncludeInDashboard)
|
||||
.HasDefaultValue(true);
|
||||
builder.Entity<Library>()
|
||||
.Property(b => b.IncludeInRecommended)
|
||||
.HasDefaultValue(true);
|
||||
builder.Entity<Library>()
|
||||
.Property(b => b.IncludeInSearch)
|
||||
.HasDefaultValue(true);
|
||||
}
|
||||
|
||||
|
||||
|
|
1693
API/Data/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs
generated
Normal file
1693
API/Data/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,59 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class ExtendedLibrarySettings : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "FolderWatching",
|
||||
table: "Library",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IncludeInDashboard",
|
||||
table: "Library",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IncludeInRecommended",
|
||||
table: "Library",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IncludeInSearch",
|
||||
table: "Library",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "FolderWatching",
|
||||
table: "Library");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IncludeInDashboard",
|
||||
table: "Library");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IncludeInRecommended",
|
||||
table: "Library");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IncludeInSearch",
|
||||
table: "Library");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -545,6 +545,26 @@ namespace API.Data.Migrations
|
|||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("FolderWatching")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IncludeInDashboard")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IncludeInRecommended")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IncludeInSearch")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ using API.DTOs.JumpBar;
|
|||
using API.DTOs.Metadata;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Kavita.Common.Extensions;
|
||||
|
@ -38,7 +39,7 @@ public interface ILibraryRepository
|
|||
Task<IEnumerable<Library>> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None);
|
||||
Task<bool> DeleteLibrary(int libraryId);
|
||||
Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId);
|
||||
Task<IEnumerable<int>> GetLibraryIdsForUserIdAsync(int userId);
|
||||
IEnumerable<int> GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None);
|
||||
Task<LibraryType> GetLibraryTypeAsync(int libraryId);
|
||||
Task<IEnumerable<Library>> GetLibraryForIdsAsync(IEnumerable<int> libraryIds, LibraryIncludes includes = LibraryIncludes.None);
|
||||
Task<int> GetTotalFiles();
|
||||
|
@ -48,7 +49,8 @@ public interface ILibraryRepository
|
|||
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync();
|
||||
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
|
||||
Task<bool> DoAnySeriesFoldersMatch(IEnumerable<string> folders);
|
||||
Library GetLibraryByFolder(string folder);
|
||||
Task<string> GetLibraryCoverImageAsync(int libraryId);
|
||||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
}
|
||||
|
||||
public class LibraryRepository : ILibraryRepository
|
||||
|
@ -126,12 +128,13 @@ public class LibraryRepository : ILibraryRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<int>> GetLibraryIdsForUserIdAsync(int userId)
|
||||
public IEnumerable<int> GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None)
|
||||
{
|
||||
return await _context.Library
|
||||
return _context.Library
|
||||
.IsRestricted(queryContext)
|
||||
.Where(l => l.AppUsers.Select(ap => ap.Id).Contains(userId))
|
||||
.Select(l => l.Id)
|
||||
.ToListAsync();
|
||||
.AsEnumerable();
|
||||
}
|
||||
|
||||
public async Task<LibraryType> GetLibraryTypeAsync(int libraryId)
|
||||
|
@ -377,12 +380,21 @@ public class LibraryRepository : ILibraryRepository
|
|||
return await _context.Series.AnyAsync(s => normalized.Contains(s.FolderPath));
|
||||
}
|
||||
|
||||
public Library? GetLibraryByFolder(string folder)
|
||||
public Task<string> GetLibraryCoverImageAsync(int libraryId)
|
||||
{
|
||||
var normalized = Services.Tasks.Scanner.Parser.Parser.NormalizePath(folder);
|
||||
return _context.Library
|
||||
.Include(l => l.Folders)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefault(l => l.Folders.Select(f => f.Path).Contains(normalized));
|
||||
.Where(l => l.Id == libraryId)
|
||||
.Select(l => l.CoverImage)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
}
|
||||
|
||||
public async Task<IList<string>> GetAllCoverImagesAsync()
|
||||
{
|
||||
return await _context.ReadingList
|
||||
.Select(t => t.CoverImage)
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,18 @@ public enum SeriesIncludes
|
|||
Library = 16,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For complex queries, Library has certain restrictions where the library should not be included in results.
|
||||
/// This enum dictates which field to use for the lookup.
|
||||
/// </summary>
|
||||
public enum QueryContext
|
||||
{
|
||||
None = 1,
|
||||
Search = 2,
|
||||
Recommended = 3,
|
||||
Dashboard = 4,
|
||||
}
|
||||
|
||||
public interface ISeriesRepository
|
||||
{
|
||||
void Add(Series series);
|
||||
|
@ -62,7 +74,7 @@ public interface ISeriesRepository
|
|||
/// <param name="libraryIds"></param>
|
||||
/// <param name="searchQuery"></param>
|
||||
/// <returns></returns>
|
||||
Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, int[] libraryIds, string searchQuery);
|
||||
Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, IList<int> libraryIds, string searchQuery);
|
||||
Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId, SeriesIncludes includes = SeriesIncludes.None);
|
||||
Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId, int userId);
|
||||
Task<Series> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata);
|
||||
|
@ -257,7 +269,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
/// <returns></returns>
|
||||
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter)
|
||||
{
|
||||
var query = await CreateFilteredSearchQueryable(userId, libraryId, filter);
|
||||
var query = await CreateFilteredSearchQueryable(userId, libraryId, filter, QueryContext.None);
|
||||
|
||||
var retSeries = query
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
|
@ -267,13 +279,14 @@ public class SeriesRepository : ISeriesRepository
|
|||
return await PagedList<SeriesDto>.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
private async Task<List<int>> GetUserLibraries(int libraryId, int userId)
|
||||
private async Task<List<int>> GetUserLibrariesForFilteredQuery(int libraryId, int userId, QueryContext queryContext)
|
||||
{
|
||||
if (libraryId == 0)
|
||||
{
|
||||
return await _context.Library
|
||||
.Include(l => l.AppUsers)
|
||||
.Where(library => library.AppUsers.Any(user => user.Id == userId))
|
||||
.IsRestricted(queryContext)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Select(library => library.Id)
|
||||
|
@ -286,7 +299,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, int[] libraryIds, string searchQuery)
|
||||
public async Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, IList<int> libraryIds, string searchQuery)
|
||||
{
|
||||
const int maxRecords = 15;
|
||||
var result = new SearchResultGroupDto();
|
||||
|
@ -302,6 +315,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
result.Libraries = await _context.Library
|
||||
.Where(l => libraryIds.Contains(l.Id))
|
||||
.Where(l => EF.Functions.Like(l.Name, $"%{searchQuery}%"))
|
||||
.IsRestricted(QueryContext.Search)
|
||||
.OrderBy(l => l.Name)
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
|
@ -549,7 +563,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
/// <returns></returns>
|
||||
public async Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter)
|
||||
{
|
||||
var query = await CreateFilteredSearchQueryable(userId, libraryId, filter);
|
||||
var query = await CreateFilteredSearchQueryable(userId, libraryId, filter, QueryContext.Dashboard);
|
||||
|
||||
var retSeries = query
|
||||
.OrderByDescending(s => s.Created)
|
||||
|
@ -658,7 +672,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30);
|
||||
var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(7);
|
||||
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
|
||||
|
||||
|
@ -686,9 +700,9 @@ public class SeriesRepository : ISeriesRepository
|
|||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
private async Task<IQueryable<Series>> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter)
|
||||
private async Task<IQueryable<Series>> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter, QueryContext queryContext)
|
||||
{
|
||||
var userLibraries = await GetUserLibraries(libraryId, userId);
|
||||
var userLibraries = await GetUserLibrariesForFilteredQuery(libraryId, userId, queryContext);
|
||||
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
var formats = ExtractFilters(libraryId, userId, filter, ref userLibraries,
|
||||
|
@ -762,7 +776,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
private async Task<IQueryable<Series>> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter, IQueryable<Series> sQuery)
|
||||
{
|
||||
var userLibraries = await GetUserLibraries(libraryId, userId);
|
||||
var userLibraries = await GetUserLibrariesForFilteredQuery(libraryId, userId, QueryContext.Search);
|
||||
var formats = ExtractFilters(libraryId, userId, filter, ref userLibraries,
|
||||
out var allPeopleIds, out var hasPeopleFilter, out var hasGenresFilter,
|
||||
out var hasCollectionTagFilter, out var hasRatingFilter, out var hasProgressFilter,
|
||||
|
@ -1059,7 +1073,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
public async Task<PagedList<SeriesDto>> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
|
||||
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
@ -1086,7 +1100,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
/// <returns></returns>
|
||||
public async Task<PagedList<SeriesDto>> GetRediscover(int userId, int libraryId, UserParams userParams)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
|
||||
.Where(s => usersSeriesIds.Contains(s.SeriesId))
|
||||
|
@ -1105,7 +1119,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
public async Task<SeriesDto> GetSeriesForMangaFile(int mangaFileId, int userId)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId);
|
||||
var libraryIds = GetLibraryIdsForUser(userId, 0, QueryContext.Search);
|
||||
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
return await _context.MangaFile
|
||||
|
@ -1264,7 +1278,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
public async Task<PagedList<SeriesDto>> GetHighlyRated(int userId, int libraryId, UserParams userParams)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
var distinctSeriesIdsWithHighRating = _context.AppUserRating
|
||||
.Where(s => usersSeriesIds.Contains(s.SeriesId) && s.Rating > 4)
|
||||
|
@ -1285,7 +1299,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
public async Task<PagedList<SeriesDto>> GetQuickReads(int userId, int libraryId, UserParams userParams)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
|
||||
.Where(s => usersSeriesIds.Contains(s.SeriesId))
|
||||
|
@ -1311,7 +1325,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
public async Task<PagedList<SeriesDto>> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
|
||||
.Where(s => usersSeriesIds.Contains(s.SeriesId))
|
||||
|
@ -1341,21 +1355,27 @@ public class SeriesRepository : ISeriesRepository
|
|||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="libraryId">0 for no library filter</param>
|
||||
/// <param name="queryContext">Defaults to None - The context behind this query, so appropriate restrictions can be placed</param>
|
||||
/// <returns></returns>
|
||||
private IQueryable<int> GetLibraryIdsForUser(int userId, int libraryId = 0)
|
||||
private IQueryable<int> GetLibraryIdsForUser(int userId, int libraryId = 0, QueryContext queryContext = QueryContext.None)
|
||||
{
|
||||
var query = _context.AppUser
|
||||
var user = _context.AppUser
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == userId);
|
||||
.Where(u => u.Id == userId)
|
||||
.AsSingleQuery();
|
||||
|
||||
if (libraryId == 0)
|
||||
{
|
||||
return query.SelectMany(l => l.Libraries.Select(lib => lib.Id));
|
||||
return user.SelectMany(l => l.Libraries)
|
||||
.IsRestricted(queryContext)
|
||||
.Select(lib => lib.Id);
|
||||
}
|
||||
|
||||
return query.SelectMany(l =>
|
||||
l.Libraries.Where(lib => lib.Id == libraryId).Select(lib => lib.Id));
|
||||
return user.SelectMany(l => l.Libraries)
|
||||
.Where(lib => lib.Id == libraryId)
|
||||
.IsRestricted(queryContext)
|
||||
.Select(lib => lib.Id);
|
||||
}
|
||||
|
||||
public async Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId)
|
||||
|
@ -1430,8 +1450,9 @@ public class SeriesRepository : ISeriesRepository
|
|||
{
|
||||
var libraryIds = await _context.AppUser
|
||||
.Where(u => u.Id == userId)
|
||||
.SelectMany(u => u.Libraries.Select(l => new {LibraryId = l.Id, LibraryType = l.Type}))
|
||||
.Select(l => l.LibraryId)
|
||||
.SelectMany(u => u.Libraries)
|
||||
.Where(l => l.IncludeInDashboard)
|
||||
.Select(l => l.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var withinLastWeek = DateTime.Now - TimeSpan.FromDays(12);
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Interfaces;
|
||||
|
||||
|
@ -11,12 +9,24 @@ public class Library : IEntityDate
|
|||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
/// <summary>
|
||||
/// This is not used, but planned once we build out a Library detail page
|
||||
/// </summary>
|
||||
[Obsolete("This has never been coded for. Likely we can remove it.")]
|
||||
public string CoverImage { get; set; }
|
||||
public LibraryType Type { get; set; }
|
||||
/// <summary>
|
||||
/// If Folder Watching is enabled for this library
|
||||
/// </summary>
|
||||
public bool FolderWatching { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Include Library series on Dashboard Streams
|
||||
/// </summary>
|
||||
public bool IncludeInDashboard { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Include Library series on Recommended Streams
|
||||
/// </summary>
|
||||
public bool IncludeInRecommended { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Include library series in Search
|
||||
/// </summary>
|
||||
public bool IncludeInSearch { get; set; } = true;
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
/// <summary>
|
||||
|
@ -27,4 +37,5 @@ public class Library : IEntityDate
|
|||
public ICollection<FolderPath> Folders { get; set; }
|
||||
public ICollection<AppUser> AppUsers { get; set; }
|
||||
public ICollection<Series> Series { get; set; }
|
||||
|
||||
}
|
||||
|
|
|
@ -158,4 +158,32 @@ public static class QueryableExtensions
|
|||
|
||||
return query.AsSplitQuery();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies restriction based on if the Library has restrictions (like include in search)
|
||||
/// </summary>
|
||||
/// <param name="query"></param>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public static IQueryable<Library> IsRestricted(this IQueryable<Library> query, QueryContext context)
|
||||
{
|
||||
if (context.HasFlag(QueryContext.None)) return query;
|
||||
|
||||
if (context.HasFlag(QueryContext.Dashboard))
|
||||
{
|
||||
query = query.Where(l => l.IncludeInDashboard);
|
||||
}
|
||||
|
||||
if (context.HasFlag(QueryContext.Recommended))
|
||||
{
|
||||
query = query.Where(l => l.IncludeInRecommended);
|
||||
}
|
||||
|
||||
if (context.HasFlag(QueryContext.Search))
|
||||
{
|
||||
query = query.Where(l => l.IncludeInSearch);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ public class Program
|
|||
{
|
||||
await MigrateSeriesRelationsExport.Migrate(context, logger);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
// If fresh install, could fail and we should just carry on as it's not applicable
|
||||
}
|
||||
|
|
|
@ -167,6 +167,16 @@ public class ImageService : IImageService
|
|||
return $"v{volumeId}_c{chapterId}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the name format for a library cover image
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetLibraryFormat(int libraryId)
|
||||
{
|
||||
return $"l{libraryId}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the name format for a series cover image
|
||||
/// </summary>
|
||||
|
|
|
@ -473,7 +473,7 @@ public class SeriesService : ISeriesService
|
|||
public async Task<SeriesDetailDto> GetSeriesDetail(int seriesId, int userId)
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
var libraryIds = (await _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId));
|
||||
var libraryIds = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId);
|
||||
if (!libraryIds.Contains(series.LibraryId))
|
||||
throw new UnauthorizedAccessException("User does not have access to the library this series belongs to");
|
||||
|
||||
|
|
|
@ -162,6 +162,14 @@ public class BackupService : IBackupService
|
|||
var chapterImages = await _unitOfWork.ChapterRepository.GetCoverImagesForLockedChaptersAsync();
|
||||
_directoryService.CopyFilesToDirectory(
|
||||
chapterImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir);
|
||||
|
||||
var libraryImages = await _unitOfWork.LibraryRepository.GetAllCoverImagesAsync();
|
||||
_directoryService.CopyFilesToDirectory(
|
||||
libraryImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir);
|
||||
|
||||
var readingListImages = await _unitOfWork.ReadingListRepository.GetAllCoverImagesAsync();
|
||||
_directoryService.CopyFilesToDirectory(
|
||||
readingListImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
|
|
|
@ -77,6 +77,7 @@ public class LibraryWatcher : ILibraryWatcher
|
|||
_logger.LogInformation("[LibraryWatcher] Starting file watchers");
|
||||
|
||||
var libraryFolders = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync())
|
||||
.Where(l => l.FolderWatching)
|
||||
.SelectMany(l => l.Folders)
|
||||
.Distinct()
|
||||
.Select(Parser.Parser.NormalizePath)
|
||||
|
|
|
@ -1011,6 +1011,17 @@ public static class Parser
|
|||
return string.IsNullOrEmpty(author) ? string.Empty : author.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans user query string input
|
||||
/// </summary>
|
||||
/// <param name="query"></param>
|
||||
/// <returns></returns>
|
||||
public static string CleanQuery(string query)
|
||||
{
|
||||
return Uri.UnescapeDataString(query).Trim().Replace(@"%", string.Empty)
|
||||
.Replace(":", string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the slashes in a path to be <see cref="Path.AltDirectorySeparatorChar"/>
|
||||
/// </summary>
|
||||
|
|
|
@ -10,6 +10,7 @@ namespace API.SignalR;
|
|||
|
||||
public static class MessageFactoryEntityTypes
|
||||
{
|
||||
public const string Library = "library";
|
||||
public const string Series = "series";
|
||||
public const string Volume = "volume";
|
||||
public const string Chapter = "chapter";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue