IsEmpty Filter and other small fixes (#3142)
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
e574caf7eb
commit
07a36176de
96 changed files with 1361 additions and 1135 deletions
|
@ -12,10 +12,10 @@
|
|||
<LangVersion>latestmajor</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- <Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">-->
|
||||
<!-- <Delete Files="../openapi.json" />-->
|
||||
<!-- <Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />-->
|
||||
<!-- </Target>-->
|
||||
<Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<Delete Files="../openapi.json" />
|
||||
<Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
|
|
|
@ -7,6 +7,7 @@ using API.Extensions;
|
|||
using API.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
|
@ -31,16 +32,17 @@ public class CblController : BaseApiController
|
|||
/// If this returns errors, the cbl will always be rejected by Kavita.
|
||||
/// </summary>
|
||||
/// <param name="cbl">FormBody with parameter name of cbl</param>
|
||||
/// <param name="comicVineMatching">Use comic vine matching or not. Defaults to false</param>
|
||||
/// <param name="useComicVineMatching">Use comic vine matching or not. Defaults to false</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("validate")]
|
||||
public async Task<ActionResult<CblImportSummaryDto>> ValidateCbl(IFormFile cbl, [FromQuery] bool comicVineMatching = false)
|
||||
[SwaggerIgnore]
|
||||
public async Task<ActionResult<CblImportSummaryDto>> ValidateCbl(IFormFile cbl, [FromQuery] bool useComicVineMatching = false)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
try
|
||||
{
|
||||
var cblReadingList = await SaveAndLoadCblFile(cbl);
|
||||
var importSummary = await _readingListService.ValidateCblFile(userId, cblReadingList, comicVineMatching);
|
||||
var importSummary = await _readingListService.ValidateCblFile(userId, cblReadingList, useComicVineMatching);
|
||||
importSummary.FileName = cbl.FileName;
|
||||
return Ok(importSummary);
|
||||
}
|
||||
|
@ -82,16 +84,17 @@ public class CblController : BaseApiController
|
|||
/// </summary>
|
||||
/// <param name="cbl">FormBody with parameter name of cbl</param>
|
||||
/// <param name="dryRun">If true, will only emulate the import but not perform. This should be done to preview what will happen</param>
|
||||
/// <param name="comicVineMatching">Use comic vine matching or not. Defaults to false</param>
|
||||
/// <param name="useComicVineMatching">Use comic vine matching or not. Defaults to false</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("import")]
|
||||
public async Task<ActionResult<CblImportSummaryDto>> ImportCbl(IFormFile cbl, [FromQuery] bool dryRun = false, [FromQuery] bool comicVineMatching = false)
|
||||
[SwaggerIgnore]
|
||||
public async Task<ActionResult<CblImportSummaryDto>> ImportCbl(IFormFile cbl, [FromQuery] bool dryRun = false, [FromQuery] bool useComicVineMatching = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var cblReadingList = await SaveAndLoadCblFile(cbl);
|
||||
var importSummary = await _readingListService.CreateReadingListFromCbl(userId, cblReadingList, dryRun, comicVineMatching);
|
||||
var importSummary = await _readingListService.CreateReadingListFromCbl(userId, cblReadingList, dryRun, useComicVineMatching);
|
||||
importSummary.FileName = cbl.FileName;
|
||||
|
||||
return Ok(importSummary);
|
||||
|
|
|
@ -19,6 +19,7 @@ using API.Services.Tasks.Scanner;
|
|||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
using EasyCaching.Core;
|
||||
using Hangfire;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -132,13 +133,19 @@ public class LibraryController : BaseApiController
|
|||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library"));
|
||||
|
||||
await _libraryWatcher.RestartWatching();
|
||||
await _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();
|
||||
}
|
||||
|
||||
|
@ -409,7 +416,7 @@ public class LibraryController : BaseApiController
|
|||
_taskScheduler.CleanupChapters(chapterIds);
|
||||
}
|
||||
|
||||
await _libraryWatcher.RestartWatching();
|
||||
BackgroundJob.Enqueue(() => _libraryWatcher.RestartWatching());
|
||||
|
||||
foreach (var seriesId in seriesIds)
|
||||
{
|
||||
|
@ -496,16 +503,17 @@ public class LibraryController : BaseApiController
|
|||
_unitOfWork.LibraryRepository.Update(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 _libraryWatcher.RestartWatching();
|
||||
await _taskScheduler.ScanLibrary(library.Id);
|
||||
}
|
||||
|
||||
if (folderWatchingUpdate)
|
||||
{
|
||||
await _libraryWatcher.RestartWatching();
|
||||
}
|
||||
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
|
||||
MessageFactory.LibraryModifiedEvent(library.Id, "update"), false);
|
||||
|
||||
|
|
|
@ -873,6 +873,7 @@ public class OpdsController : BaseApiController
|
|||
feed.Links.Add(CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesId}&apiKey={apiKey}"));
|
||||
|
||||
var chapterDict = new Dictionary<int, short>();
|
||||
var fileDict = new Dictionary<int, short>();
|
||||
var seriesDetail = await _seriesService.GetSeriesDetail(seriesId, userId);
|
||||
foreach (var volume in seriesDetail.Volumes)
|
||||
{
|
||||
|
@ -881,12 +882,14 @@ public class OpdsController : BaseApiController
|
|||
foreach (var chapter in chaptersForVolume)
|
||||
{
|
||||
var chapterId = chapter.Id;
|
||||
if (chapterDict.ContainsKey(chapterId)) continue;
|
||||
if (!chapterDict.TryAdd(chapterId, 0)) continue;
|
||||
|
||||
var chapterDto = _mapper.Map<ChapterDto>(chapter);
|
||||
foreach (var mangaFile in chapter.Files)
|
||||
{
|
||||
chapterDict.Add(chapterId, 0);
|
||||
// If a chapter has multiple files that are within one chapter, this dict prevents duplicate key exception
|
||||
if (!fileDict.TryAdd(mangaFile.Id, 0)) continue;
|
||||
|
||||
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volume.Id, chapterId, _mapper.Map<MangaFileDto>(mangaFile), series,
|
||||
chapterDto, apiKey, prefix, baseUrl));
|
||||
}
|
||||
|
@ -905,6 +908,8 @@ public class OpdsController : BaseApiController
|
|||
var chapterDto = _mapper.Map<ChapterDto>(chapter);
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
// If a chapter has multiple files that are within one chapter, this dict prevents duplicate key exception
|
||||
if (!fileDict.TryAdd(mangaFile.Id, 0)) continue;
|
||||
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, chapter.VolumeId, chapter.Id, _mapper.Map<MangaFileDto>(mangaFile), series,
|
||||
chapterDto, apiKey, prefix, baseUrl));
|
||||
}
|
||||
|
@ -916,6 +921,9 @@ public class OpdsController : BaseApiController
|
|||
var chapterDto = _mapper.Map<ChapterDto>(special);
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
// If a chapter has multiple files that are within one chapter, this dict prevents duplicate key exception
|
||||
if (!fileDict.TryAdd(mangaFile.Id, 0)) continue;
|
||||
|
||||
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, special.VolumeId, special.Id, _mapper.Map<MangaFileDto>(mangaFile), series,
|
||||
chapterDto, apiKey, prefix, baseUrl));
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService
|
|||
}
|
||||
var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId);
|
||||
|
||||
return new UserDto
|
||||
{
|
||||
Username = user.UserName!,
|
||||
|
|
|
@ -319,11 +319,12 @@ public class SeriesController : BaseApiController
|
|||
/// <param name="libraryId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("all-v2")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams,
|
||||
[FromQuery] int libraryId = 0, [FromQuery] QueryContext context = QueryContext.None)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto);
|
||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto, context);
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series"));
|
||||
|
|
|
@ -277,4 +277,16 @@ public class ServerController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the Sync Themes task
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpPost("sync-themes")]
|
||||
public async Task<ActionResult> SyncThemes()
|
||||
{
|
||||
await _taskScheduler.SyncThemes();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ using Kavita.Common.Helpers;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
|
@ -370,7 +371,7 @@ public class SettingsController : BaseApiController
|
|||
return Ok(updateSettingsDto);
|
||||
}
|
||||
|
||||
public void UpdateBookmarkDirectory(string originalBookmarkDirectory, string bookmarkDirectory)
|
||||
private void UpdateBookmarkDirectory(string originalBookmarkDirectory, string bookmarkDirectory)
|
||||
{
|
||||
_directoryService.ExistOrCreate(bookmarkDirectory);
|
||||
_directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory);
|
||||
|
|
|
@ -53,4 +53,8 @@ public enum FilterComparison
|
|||
/// Is Date not between now and X seconds ago
|
||||
/// </summary>
|
||||
IsNotInLast = 15,
|
||||
/// <summary>
|
||||
/// There are no records
|
||||
/// </summary>
|
||||
IsEmpty = 16
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ public interface ISeriesRepository
|
|||
Task<int> GetAverageUserRating(int seriesId, int userId);
|
||||
Task RemoveFromOnDeck(int seriesId, int userId);
|
||||
Task ClearOnDeckRemoval(int seriesId, int userId);
|
||||
Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto);
|
||||
Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None);
|
||||
Task<PlusSeriesDto?> GetPlusSeriesDto(int seriesId);
|
||||
}
|
||||
|
||||
|
@ -693,9 +693,9 @@ public class SeriesRepository : ISeriesRepository
|
|||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto)
|
||||
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None)
|
||||
{
|
||||
var query = await CreateFilteredSearchQueryableV2(userId, filterDto, QueryContext.None);
|
||||
var query = await CreateFilteredSearchQueryableV2(userId, filterDto, queryContext);
|
||||
|
||||
var retSeries = query
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
|
@ -979,7 +979,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
.HasReleaseYear(hasReleaseYearMaxFilter, FilterComparison.LessThanEqual, filter.ReleaseYearRange?.Max)
|
||||
.HasReleaseYear(hasReleaseYearMinFilter, FilterComparison.GreaterThanEqual, filter.ReleaseYearRange?.Min)
|
||||
.HasName(hasSeriesNameFilter, FilterComparison.Matches, filter.SeriesNameQuery)
|
||||
.HasRating(hasRatingFilter, FilterComparison.GreaterThanEqual, filter.Rating, userId)
|
||||
.HasRating(hasRatingFilter, FilterComparison.GreaterThanEqual, filter.Rating / 100f, userId)
|
||||
.HasAgeRating(hasAgeRating, FilterComparison.Contains, filter.AgeRating)
|
||||
.HasPublicationStatus(hasPublicationFilter, FilterComparison.Contains, filter.PublicationStatus)
|
||||
.HasTags(hasTagsFilter, FilterComparison.Contains, filter.Tags)
|
||||
|
@ -987,7 +987,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
.HasGenre(hasGenresFilter, FilterComparison.Contains, filter.Genres)
|
||||
.HasFormat(filter.Formats != null && filter.Formats.Count > 0, FilterComparison.Contains, filter.Formats!)
|
||||
.HasAverageReadTime(true, FilterComparison.GreaterThanEqual, 0)
|
||||
.HasPeople(hasPeopleFilter, FilterComparison.Contains, allPeopleIds)
|
||||
.HasPeopleLegacy(hasPeopleFilter, FilterComparison.Contains, allPeopleIds)
|
||||
|
||||
.WhereIf(onlyParentSeries,
|
||||
s => s.RelationOf.Count == 0 || s.RelationOf.All(p => p.RelationKind == RelationKind.Prequel))
|
||||
|
@ -1215,6 +1215,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
private static IQueryable<Series> BuildFilterGroup(int userId, FilterStatementDto statement, IQueryable<Series> query)
|
||||
{
|
||||
|
||||
var value = FilterFieldValueConverter.ConvertValue(statement.Field, statement.Value);
|
||||
return statement.Field switch
|
||||
{
|
||||
|
@ -1226,21 +1227,21 @@ public class SeriesRepository : ISeriesRepository
|
|||
(IList<PublicationStatus>) value),
|
||||
FilterField.Languages => query.HasLanguage(true, statement.Comparison, (IList<string>) value),
|
||||
FilterField.AgeRating => query.HasAgeRating(true, statement.Comparison, (IList<AgeRating>) value),
|
||||
FilterField.UserRating => query.HasRating(true, statement.Comparison, (int) value, userId),
|
||||
FilterField.UserRating => query.HasRating(true, statement.Comparison, (float) value , userId),
|
||||
FilterField.Tags => query.HasTags(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Translators => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Characters => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Publisher => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Editor => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.CoverArtist => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Letterer => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Colorist => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Inker => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Imprint => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Team => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Location => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Penciller => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Writers => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Translators => query.HasPeople(true, statement.Comparison, (IList<int>) value, PersonRole.Translator),
|
||||
FilterField.Characters => query.HasPeople(true, statement.Comparison, (IList<int>) value, PersonRole.Character),
|
||||
FilterField.Publisher => query.HasPeople(true, statement.Comparison, (IList<int>) value, PersonRole.Publisher),
|
||||
FilterField.Editor => query.HasPeople(true, statement.Comparison, (IList<int>) value, PersonRole.Editor),
|
||||
FilterField.CoverArtist => query.HasPeople(true, statement.Comparison, (IList<int>) value, PersonRole.CoverArtist),
|
||||
FilterField.Letterer => query.HasPeople(true, statement.Comparison, (IList<int>) value, PersonRole.Letterer),
|
||||
FilterField.Colorist => query.HasPeople(true, statement.Comparison, (IList<int>) value, PersonRole.Inker),
|
||||
FilterField.Inker => query.HasPeople(true, statement.Comparison, (IList<int>) value, PersonRole.Inker),
|
||||
FilterField.Imprint => query.HasPeople(true, statement.Comparison, (IList<int>) value, PersonRole.Imprint),
|
||||
FilterField.Team => query.HasPeople(true, statement.Comparison, (IList<int>) value, PersonRole.Team),
|
||||
FilterField.Location => query.HasPeople(true, statement.Comparison, (IList<int>) value, PersonRole.Location),
|
||||
FilterField.Penciller => query.HasPeople(true, statement.Comparison, (IList<int>) value, PersonRole.Penciller),
|
||||
FilterField.Writers => query.HasPeople(true, statement.Comparison, (IList<int>) value, PersonRole.Writer),
|
||||
FilterField.Genres => query.HasGenre(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.CollectionTags =>
|
||||
// This is handled in the code before this as it's handled in a more general, combined manner
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
|
||||
using System;
|
||||
|
||||
namespace API.Entities;
|
||||
#nullable enable
|
||||
public class AppUserRating
|
||||
|
@ -9,7 +11,7 @@ public class AppUserRating
|
|||
/// </summary>
|
||||
public float Rating { get; set; }
|
||||
/// <summary>
|
||||
/// If the rating has been explicitly set. Otherwise the 0.0 rating should be ignored as it's not rated
|
||||
/// If the rating has been explicitly set. Otherwise, the 0.0 rating should be ignored as it's not rated
|
||||
/// </summary>
|
||||
public bool HasBeenRated { get; set; }
|
||||
/// <summary>
|
||||
|
@ -19,6 +21,7 @@ public class AppUserRating
|
|||
/// <summary>
|
||||
/// An optional tagline for the review
|
||||
/// </summary>
|
||||
[Obsolete("No longer used")]
|
||||
public string? Tagline { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public Series Series { get; set; } = null!;
|
||||
|
|
|
@ -43,6 +43,7 @@ public static class SeriesFilter
|
|||
case FilterComparison.IsAfter:
|
||||
case FilterComparison.IsInLast:
|
||||
case FilterComparison.IsNotInLast:
|
||||
case FilterComparison.IsEmpty:
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||
}
|
||||
|
@ -71,6 +72,8 @@ public static class SeriesFilter
|
|||
return queryable.Where(s => s.Metadata.ReleaseYear >= DateTime.Now.Year - (int) releaseYear);
|
||||
case FilterComparison.IsNotInLast:
|
||||
return queryable.Where(s => s.Metadata.ReleaseYear < DateTime.Now.Year - (int) releaseYear);
|
||||
case FilterComparison.IsEmpty:
|
||||
return queryable.Where(s => s.Metadata.ReleaseYear == 0);
|
||||
case FilterComparison.Matches:
|
||||
case FilterComparison.Contains:
|
||||
case FilterComparison.NotContains:
|
||||
|
@ -86,14 +89,20 @@ public static class SeriesFilter
|
|||
|
||||
|
||||
public static IQueryable<Series> HasRating(this IQueryable<Series> queryable, bool condition,
|
||||
FilterComparison comparison, int rating, int userId)
|
||||
FilterComparison comparison, float rating, int userId)
|
||||
{
|
||||
if (rating < 0 || !condition || userId <= 0) return queryable;
|
||||
|
||||
// Users see rating as %, so they are likely to pass 10%. We need to turn that into the underlying float encoding
|
||||
if (rating.IsNot(0f))
|
||||
{
|
||||
rating /= 100f;
|
||||
}
|
||||
|
||||
switch (comparison)
|
||||
{
|
||||
case FilterComparison.Equal:
|
||||
return queryable.Where(s => s.Ratings.Any(r => Math.Abs(r.Rating - rating) < FloatingPointTolerance && r.AppUserId == userId));
|
||||
return queryable.Where(s => s.Ratings.Any(r => Math.Abs(r.Rating - rating) <= FloatingPointTolerance && r.AppUserId == userId));
|
||||
case FilterComparison.GreaterThan:
|
||||
return queryable.Where(s => s.Ratings.Any(r => r.Rating > rating && r.AppUserId == userId));
|
||||
case FilterComparison.GreaterThanEqual:
|
||||
|
@ -102,10 +111,13 @@ public static class SeriesFilter
|
|||
return queryable.Where(s => s.Ratings.Any(r => r.Rating < rating && r.AppUserId == userId));
|
||||
case FilterComparison.LessThanEqual:
|
||||
return queryable.Where(s => s.Ratings.Any(r => r.Rating <= rating && r.AppUserId == userId));
|
||||
case FilterComparison.NotEqual:
|
||||
return queryable.Where(s => s.Ratings.Any(r => Math.Abs(r.Rating - rating) >= FloatingPointTolerance && r.AppUserId == userId));
|
||||
case FilterComparison.IsEmpty:
|
||||
return queryable.Where(s => s.Ratings.All(r => r.AppUserId != userId));
|
||||
case FilterComparison.Contains:
|
||||
case FilterComparison.Matches:
|
||||
case FilterComparison.NotContains:
|
||||
case FilterComparison.NotEqual:
|
||||
case FilterComparison.BeginsWith:
|
||||
case FilterComparison.EndsWith:
|
||||
case FilterComparison.IsBefore:
|
||||
|
@ -124,7 +136,7 @@ public static class SeriesFilter
|
|||
{
|
||||
if (!condition || ratings.Count == 0) return queryable;
|
||||
|
||||
var firstRating = ratings.First();
|
||||
var firstRating = ratings[0];
|
||||
switch (comparison)
|
||||
{
|
||||
case FilterComparison.Equal:
|
||||
|
@ -151,6 +163,7 @@ public static class SeriesFilter
|
|||
case FilterComparison.IsInLast:
|
||||
case FilterComparison.IsNotInLast:
|
||||
case FilterComparison.MustContains:
|
||||
case FilterComparison.IsEmpty:
|
||||
throw new KavitaException($"{comparison} not applicable for Series.AgeRating");
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||
|
@ -185,6 +198,7 @@ public static class SeriesFilter
|
|||
case FilterComparison.IsInLast:
|
||||
case FilterComparison.IsNotInLast:
|
||||
case FilterComparison.MustContains:
|
||||
case FilterComparison.IsEmpty:
|
||||
throw new KavitaException($"{comparison} not applicable for Series.AverageReadTime");
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||
|
@ -196,7 +210,7 @@ public static class SeriesFilter
|
|||
{
|
||||
if (!condition || pubStatues.Count == 0) return queryable;
|
||||
|
||||
var firstStatus = pubStatues.First();
|
||||
var firstStatus = pubStatues[0];
|
||||
switch (comparison)
|
||||
{
|
||||
case FilterComparison.Equal:
|
||||
|
@ -219,6 +233,7 @@ public static class SeriesFilter
|
|||
case FilterComparison.IsInLast:
|
||||
case FilterComparison.IsNotInLast:
|
||||
case FilterComparison.Matches:
|
||||
case FilterComparison.IsEmpty:
|
||||
throw new KavitaException($"{comparison} not applicable for Series.PublicationStatus");
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||
|
@ -269,6 +284,7 @@ public static class SeriesFilter
|
|||
case FilterComparison.NotEqual:
|
||||
subQuery = subQuery.Where(s => Math.Abs(s.Percentage - readProgress) > FloatingPointTolerance);
|
||||
break;
|
||||
case FilterComparison.IsEmpty:
|
||||
case FilterComparison.Matches:
|
||||
case FilterComparison.Contains:
|
||||
case FilterComparison.NotContains:
|
||||
|
@ -293,6 +309,7 @@ public static class SeriesFilter
|
|||
{
|
||||
if (!condition) return queryable;
|
||||
|
||||
|
||||
var subQuery = queryable
|
||||
.Where(s => s.ExternalSeriesMetadata != null)
|
||||
.Include(s => s.ExternalSeriesMetadata)
|
||||
|
@ -334,6 +351,7 @@ public static class SeriesFilter
|
|||
case FilterComparison.IsInLast:
|
||||
case FilterComparison.IsNotInLast:
|
||||
case FilterComparison.MustContains:
|
||||
case FilterComparison.IsEmpty:
|
||||
throw new KavitaException($"{comparison} not applicable for Series.AverageRating");
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||
|
@ -393,6 +411,7 @@ public static class SeriesFilter
|
|||
case FilterComparison.IsInLast:
|
||||
case FilterComparison.IsNotInLast:
|
||||
case FilterComparison.MustContains:
|
||||
case FilterComparison.IsEmpty:
|
||||
throw new KavitaException($"{comparison} not applicable for Series.ReadProgress");
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||
|
@ -424,6 +443,8 @@ public static class SeriesFilter
|
|||
queries.AddRange(tags.Select(gId => queryable.Where(s => s.Metadata.Tags.Any(p => p.Id == gId))));
|
||||
|
||||
return queries.Aggregate((q1, q2) => q1.Intersect(q2));
|
||||
case FilterComparison.IsEmpty:
|
||||
return queryable.Where(s => s.Metadata.Tags == null || s.Metadata.Tags.Count == 0);
|
||||
case FilterComparison.GreaterThan:
|
||||
case FilterComparison.GreaterThanEqual:
|
||||
case FilterComparison.LessThan:
|
||||
|
@ -442,6 +463,48 @@ public static class SeriesFilter
|
|||
}
|
||||
|
||||
public static IQueryable<Series> HasPeople(this IQueryable<Series> queryable, bool condition,
|
||||
FilterComparison comparison, IList<int> people, PersonRole role)
|
||||
{
|
||||
if (!condition || (comparison != FilterComparison.IsEmpty && people.Count == 0)) return queryable;
|
||||
|
||||
switch (comparison)
|
||||
{
|
||||
case FilterComparison.Equal:
|
||||
case FilterComparison.Contains:
|
||||
return queryable.Where(s => s.Metadata.People.Any(p => people.Contains(p.Id)));
|
||||
case FilterComparison.NotEqual:
|
||||
case FilterComparison.NotContains:
|
||||
return queryable.Where(s => s.Metadata.People.All(t => !people.Contains(t.Id)));
|
||||
case FilterComparison.MustContains:
|
||||
// Deconstruct and do a Union of a bunch of where statements since this doesn't translate
|
||||
var queries = new List<IQueryable<Series>>()
|
||||
{
|
||||
queryable
|
||||
};
|
||||
queries.AddRange(people.Select(gId => queryable.Where(s => s.Metadata.People.Any(p => p.Id == gId))));
|
||||
|
||||
return queries.Aggregate((q1, q2) => q1.Intersect(q2));
|
||||
case FilterComparison.IsEmpty:
|
||||
// Check if there are no people with specific roles (e.g., Writer, Penciller, etc.)
|
||||
return queryable.Where(s => !s.Metadata.People.Any(p => p.Role == role));
|
||||
case FilterComparison.GreaterThan:
|
||||
case FilterComparison.GreaterThanEqual:
|
||||
case FilterComparison.LessThan:
|
||||
case FilterComparison.LessThanEqual:
|
||||
case FilterComparison.BeginsWith:
|
||||
case FilterComparison.EndsWith:
|
||||
case FilterComparison.IsBefore:
|
||||
case FilterComparison.IsAfter:
|
||||
case FilterComparison.IsInLast:
|
||||
case FilterComparison.IsNotInLast:
|
||||
case FilterComparison.Matches:
|
||||
throw new KavitaException($"{comparison} not applicable for Series.People");
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static IQueryable<Series> HasPeopleLegacy(this IQueryable<Series> queryable, bool condition,
|
||||
FilterComparison comparison, IList<int> people)
|
||||
{
|
||||
if (!condition || people.Count == 0) return queryable;
|
||||
|
@ -463,6 +526,7 @@ public static class SeriesFilter
|
|||
queries.AddRange(people.Select(gId => queryable.Where(s => s.Metadata.People.Any(p => p.Id == gId))));
|
||||
|
||||
return queries.Aggregate((q1, q2) => q1.Intersect(q2));
|
||||
case FilterComparison.IsEmpty:
|
||||
case FilterComparison.GreaterThan:
|
||||
case FilterComparison.GreaterThanEqual:
|
||||
case FilterComparison.LessThan:
|
||||
|
@ -502,6 +566,8 @@ public static class SeriesFilter
|
|||
queries.AddRange(genres.Select(gId => queryable.Where(s => s.Metadata.Genres.Any(p => p.Id == gId))));
|
||||
|
||||
return queries.Aggregate((q1, q2) => q1.Intersect(q2));
|
||||
case FilterComparison.IsEmpty:
|
||||
return queryable.Where(s => s.Metadata.Genres.Count == 0);
|
||||
case FilterComparison.GreaterThan:
|
||||
case FilterComparison.GreaterThanEqual:
|
||||
case FilterComparison.LessThan:
|
||||
|
@ -544,6 +610,7 @@ public static class SeriesFilter
|
|||
case FilterComparison.IsAfter:
|
||||
case FilterComparison.IsInLast:
|
||||
case FilterComparison.IsNotInLast:
|
||||
case FilterComparison.IsEmpty:
|
||||
throw new KavitaException($"{comparison} not applicable for Series.Format");
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||
|
@ -573,6 +640,8 @@ public static class SeriesFilter
|
|||
queries.AddRange(collectionSeries.Select(gId => queryable.Where(s => collectionSeries.Any(p => p == s.Id))));
|
||||
|
||||
return queries.Aggregate((q1, q2) => q1.Intersect(q2));
|
||||
case FilterComparison.IsEmpty:
|
||||
return queryable.Where(s => collectionSeries.All(c => c != s.Id));
|
||||
case FilterComparison.GreaterThan:
|
||||
case FilterComparison.GreaterThanEqual:
|
||||
case FilterComparison.LessThan:
|
||||
|
@ -633,6 +702,7 @@ public static class SeriesFilter
|
|||
case FilterComparison.IsInLast:
|
||||
case FilterComparison.IsNotInLast:
|
||||
case FilterComparison.MustContains:
|
||||
case FilterComparison.IsEmpty:
|
||||
throw new KavitaException($"{comparison} not applicable for Series.Name");
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported");
|
||||
|
@ -656,6 +726,8 @@ public static class SeriesFilter
|
|||
return queryable.Where(s => EF.Functions.Like(s.Metadata.Summary, $"%{queryString}%"));
|
||||
case FilterComparison.NotEqual:
|
||||
return queryable.Where(s => s.Metadata.Summary != queryString);
|
||||
case FilterComparison.IsEmpty:
|
||||
return queryable.Where(s => string.IsNullOrEmpty(s.Metadata.Summary));
|
||||
case FilterComparison.NotContains:
|
||||
case FilterComparison.GreaterThan:
|
||||
case FilterComparison.GreaterThanEqual:
|
||||
|
@ -703,6 +775,7 @@ public static class SeriesFilter
|
|||
case FilterComparison.IsInLast:
|
||||
case FilterComparison.IsNotInLast:
|
||||
case FilterComparison.MustContains:
|
||||
case FilterComparison.IsEmpty:
|
||||
throw new KavitaException($"{comparison} not applicable for Series.FolderPath");
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported");
|
||||
|
@ -779,6 +852,7 @@ public static class SeriesFilter
|
|||
case FilterComparison.IsInLast:
|
||||
case FilterComparison.IsNotInLast:
|
||||
case FilterComparison.MustContains:
|
||||
case FilterComparison.IsEmpty:
|
||||
throw new KavitaException($"{comparison} not applicable for Series.FolderPath");
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported");
|
||||
|
|
|
@ -18,75 +18,94 @@ public static class FilterFieldValueConverter
|
|||
FilterField.SeriesName => value,
|
||||
FilterField.Path => value,
|
||||
FilterField.FilePath => value,
|
||||
FilterField.ReleaseYear => int.Parse(value),
|
||||
FilterField.ReleaseYear => string.IsNullOrEmpty(value) ? 0 : int.Parse(value),
|
||||
FilterField.Languages => value.Split(',').ToList(),
|
||||
FilterField.PublicationStatus => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(x => (PublicationStatus) Enum.Parse(typeof(PublicationStatus), x))
|
||||
.ToList(),
|
||||
FilterField.Summary => value,
|
||||
FilterField.AgeRating => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(x => (AgeRating) Enum.Parse(typeof(AgeRating), x))
|
||||
.ToList(),
|
||||
FilterField.UserRating => int.Parse(value),
|
||||
FilterField.UserRating => string.IsNullOrEmpty(value) ? 0 : float.Parse(value),
|
||||
FilterField.Tags => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.CollectionTags => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Translators => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Characters => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Publisher => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Editor => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.CoverArtist => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Letterer => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Colorist => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Inker => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Imprint => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Team => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Location => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Penciller => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Writers => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Genres => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Libraries => value.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.WantToRead => bool.Parse(value),
|
||||
FilterField.ReadProgress => value.AsFloat(),
|
||||
FilterField.ReadProgress => string.IsNullOrEmpty(value) ? 0f : value.AsFloat(),
|
||||
FilterField.ReadingDate => DateTime.Parse(value),
|
||||
FilterField.Formats => value.Split(',')
|
||||
.Select(x => (MangaFormat) Enum.Parse(typeof(MangaFormat), x))
|
||||
.ToList(),
|
||||
FilterField.ReadTime => int.Parse(value),
|
||||
FilterField.AverageRating => value.AsFloat(),
|
||||
FilterField.ReadTime => string.IsNullOrEmpty(value) ? 0 : int.Parse(value),
|
||||
FilterField.AverageRating => string.IsNullOrEmpty(value) ? 0f : value.AsFloat(),
|
||||
_ => throw new ArgumentException("Invalid field type")
|
||||
};
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ public class DirectoryService : IDirectoryService
|
|||
private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase;
|
||||
|
||||
private static readonly Regex ExcludeDirectories = new Regex(
|
||||
@"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle|\.@__thumb|\.caltrash|#recycle",
|
||||
@"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle|\.@__thumb|\.caltrash|#recycle|\.yacreaderlibrary",
|
||||
MatchOptions,
|
||||
Tasks.Scanner.Parser.Parser.RegexTimeout);
|
||||
private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)",
|
||||
|
|
|
@ -69,7 +69,8 @@ public class StatisticService : IStatisticService
|
|||
var totalPagesRead = await _context.AppUserProgresses
|
||||
.Where(p => p.AppUserId == userId)
|
||||
.Where(p => libraryIds.Contains(p.LibraryId))
|
||||
.SumAsync(p => p.PagesRead);
|
||||
.Select(p => (int?) p.PagesRead)
|
||||
.SumAsync() ?? 0;
|
||||
|
||||
var timeSpentReading = await TimeSpentReadingForUsersAsync(new List<int>() {userId}, libraryIds);
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ public interface ITaskScheduler
|
|||
void CovertAllCoversToEncoding();
|
||||
Task CleanupDbEntries();
|
||||
Task CheckForUpdate();
|
||||
|
||||
Task SyncThemes();
|
||||
}
|
||||
public class TaskScheduler : ITaskScheduler
|
||||
{
|
||||
|
@ -165,8 +165,8 @@ public class TaskScheduler : ITaskScheduler
|
|||
RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(),
|
||||
Cron.Monthly, RecurringJobOptions);
|
||||
|
||||
RecurringJob.AddOrUpdate(SyncThemesTaskId, () => _themeService.SyncThemes(),
|
||||
Cron.Weekly, RecurringJobOptions);
|
||||
RecurringJob.AddOrUpdate(SyncThemesTaskId, () => SyncThemes(),
|
||||
Cron.Daily, RecurringJobOptions);
|
||||
|
||||
await ScheduleKavitaPlusTasks();
|
||||
}
|
||||
|
@ -444,6 +444,11 @@ public class TaskScheduler : ITaskScheduler
|
|||
await _versionUpdaterService.PushUpdate(update);
|
||||
}
|
||||
|
||||
public async Task SyncThemes()
|
||||
{
|
||||
await _themeService.SyncThemes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If there is an enqueued or scheduled task for <see cref="ScannerService.ScanLibrary"/> method
|
||||
/// </summary>
|
||||
|
|
|
@ -1152,6 +1152,7 @@ public static class Parser
|
|||
return path.Contains("__MACOSX") || path.StartsWith("@Recently-Snapshot") || path.StartsWith("@recycle")
|
||||
|| path.StartsWith("._") || Path.GetFileName(path).StartsWith("._") || path.Contains(".qpkg")
|
||||
|| path.StartsWith("#recycle")
|
||||
|| path.Contains(".yacreaderlibrary")
|
||||
|| path.Contains(".caltrash");
|
||||
}
|
||||
|
||||
|
|
|
@ -120,7 +120,11 @@ public class ThemeService : IThemeService
|
|||
public async Task<List<DownloadableSiteThemeDto>> GetDownloadableThemes()
|
||||
{
|
||||
const string cacheKey = "browse";
|
||||
var existingThemes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos()).ToDictionary(k => k.Name);
|
||||
// Avoid a duplicate Dark issue some users faced during migration
|
||||
var existingThemes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos())
|
||||
.GroupBy(k => k.Name)
|
||||
.ToDictionary(g => g.Key, g => g.First());
|
||||
|
||||
if (_cache.TryGetValue(cacheKey, out List<DownloadableSiteThemeDto>? themes) && themes != null)
|
||||
{
|
||||
foreach (var t in themes)
|
||||
|
@ -204,6 +208,13 @@ public class ThemeService : IThemeService
|
|||
/// <returns></returns>
|
||||
private async Task<IDictionary<string, ThemeMetadata>> GetReadme()
|
||||
{
|
||||
// Try and delete a Readme file if it already exists
|
||||
var existingReadmeFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, "README.md");
|
||||
if (_directoryService.FileSystem.File.Exists(existingReadmeFile))
|
||||
{
|
||||
_directoryService.DeleteFiles([existingReadmeFile]);
|
||||
}
|
||||
|
||||
var tempDownloadFile = await GithubReadme.DownloadFileAsync(_directoryService.TempDirectory);
|
||||
|
||||
// Read file into Markdown
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"TokenKey": "super secret unguessable key that is longer because we require it",
|
||||
"Port": 5000,
|
||||
"IpAddresses": "0.0.0.0,::",
|
||||
"BaseUrl": "/",
|
||||
"BaseUrl": "/test/",
|
||||
"Cache": 75,
|
||||
"AllowIFraming": false
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue