This commit is contained in:
Joe Milazzo 2024-11-16 09:20:28 -06:00 committed by GitHub
parent 6a75291a67
commit c849eff33e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 190 additions and 35 deletions

View File

@ -119,6 +119,11 @@ public class PersonController : BaseApiController
return Ok(_mapper.Map<PersonDto>(person));
}
/// <summary>
/// Attempts to download the cover from CoversDB (Note: Not yet release in Kavita)
/// </summary>
/// <param name="personId"></param>
/// <returns></returns>
[HttpPost("fetch-cover")]
public async Task<ActionResult<string>> DownloadCoverImage([FromQuery] int personId)
{
@ -129,13 +134,13 @@ public class PersonController : BaseApiController
var personImage = await _coverDbService.DownloadPersonImageAsync(person, settings.EncodeMediaAs);
if (string.IsNullOrEmpty(personImage)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-image-doesnt-exist"));
person.CoverImage = personImage;
_imageService.UpdateColorScape(person);
_unitOfWork.PersonRepository.Update(person);
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(person.Id, "person"), false);
return Ok(personImage);
}
@ -150,6 +155,12 @@ public class PersonController : BaseApiController
return Ok(await _unitOfWork.PersonRepository.GetSeriesKnownFor(personId));
}
/// <summary>
/// Returns all individual chapters by role. Limited to 20 results.
/// </summary>
/// <param name="personId"></param>
/// <param name="role"></param>
/// <returns></returns>
[HttpGet("chapters-by-role")]
public async Task<ActionResult<IEnumerable<StandaloneChapterDto>>> GetChaptersByRole(int personId, PersonRole role)
{

View File

@ -56,7 +56,7 @@ public class VolumeDto : IHasReadTimeEstimate, IHasCoverImage
}
/// <summary>
/// Does this volume hold only specials?
/// Does this volume hold only specials
/// </summary>
/// <returns></returns>
public bool IsSpecial()

View File

@ -1192,7 +1192,7 @@ public class SeriesRepository : ISeriesRepository
private static IQueryable<Series> BuildFilterQuery(int userId, FilterV2Dto filterDto, IQueryable<Series> query)
{
if (filterDto.Statements == null || !filterDto.Statements.Any()) return query;
if (filterDto.Statements == null || filterDto.Statements.Count == 0) return query;
var queries = filterDto.Statements

View File

@ -167,6 +167,7 @@ public static class SeriesFilter
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
}
}
public static IQueryable<Series> HasAverageReadTime(this IQueryable<Series> queryable, bool condition,
FilterComparison comparison, int avgReadTime)
{
@ -175,17 +176,17 @@ public static class SeriesFilter
switch (comparison)
{
case FilterComparison.NotEqual:
return queryable.Where(s => s.AvgHoursToRead != avgReadTime);
return queryable.WhereNotEqual(s => s.AvgHoursToRead, avgReadTime);
case FilterComparison.Equal:
return queryable.Where(s => s.AvgHoursToRead == avgReadTime);
return queryable.WhereEqual(s => s.AvgHoursToRead, avgReadTime);
case FilterComparison.GreaterThan:
return queryable.Where(s => s.AvgHoursToRead > avgReadTime);
return queryable.WhereGreaterThan(s => s.AvgHoursToRead, avgReadTime);
case FilterComparison.GreaterThanEqual:
return queryable.Where(s => s.AvgHoursToRead >= avgReadTime);
return queryable.WhereGreaterThanOrEqual(s => s.AvgHoursToRead, avgReadTime);
case FilterComparison.LessThan:
return queryable.Where(s => s.AvgHoursToRead < avgReadTime);
return queryable.WhereLessThan(s => s.AvgHoursToRead, avgReadTime);
case FilterComparison.LessThanEqual:
return queryable.Where(s => s.AvgHoursToRead <= avgReadTime);
return queryable.WhereLessThanOrEqual(s => s.AvgHoursToRead, avgReadTime);
case FilterComparison.Contains:
case FilterComparison.Matches:
case FilterComparison.NotContains:
@ -257,29 +258,29 @@ public static class SeriesFilter
Series = s,
Percentage = s.Progress
.Where(p => p != null && p.AppUserId == userId)
.Sum(p => p != null ? (p.PagesRead * 1.0f / s.Pages) : 0) * 100
.Sum(p => p != null ? (p.PagesRead * 1.0f / s.Pages) : 0f) * 100f
})
.AsSplitQuery();
switch (comparison)
{
case FilterComparison.Equal:
subQuery = subQuery.Where(s => Math.Abs(s.Percentage - readProgress) < FloatingPointTolerance);
subQuery = subQuery.WhereEqual(s => s.Percentage, readProgress);
break;
case FilterComparison.GreaterThan:
subQuery = subQuery.Where(s => s.Percentage > readProgress);
subQuery = subQuery.WhereGreaterThan(s => s.Percentage, readProgress);
break;
case FilterComparison.GreaterThanEqual:
subQuery = subQuery.Where(s => s.Percentage >= readProgress);
subQuery = subQuery.WhereGreaterThanOrEqual(s => s.Percentage, readProgress);
break;
case FilterComparison.LessThan:
subQuery = subQuery.Where(s => s.Percentage < readProgress);
subQuery = subQuery.WhereLessThan(s => s.Percentage, readProgress);
break;
case FilterComparison.LessThanEqual:
subQuery = subQuery.Where(s => s.Percentage <= readProgress);
subQuery = subQuery.WhereLessThanOrEqual(s => s.Percentage, readProgress);
break;
case FilterComparison.NotEqual:
subQuery = subQuery.Where(s => Math.Abs(s.Percentage - readProgress) > FloatingPointTolerance);
subQuery = subQuery.WhereNotEqual(s => s.Percentage, readProgress);
break;
case FilterComparison.IsEmpty:
case FilterComparison.Matches:
@ -306,7 +307,6 @@ public static class SeriesFilter
{
if (!condition) return queryable;
var subQuery = queryable
.Where(s => s.ExternalSeriesMetadata != null)
.Include(s => s.ExternalSeriesMetadata)
@ -316,27 +316,27 @@ public static class SeriesFilter
AverageRating = s.ExternalSeriesMetadata.AverageExternalRating
})
.AsSplitQuery()
.AsEnumerable();
.AsQueryable();
switch (comparison)
{
case FilterComparison.Equal:
subQuery = subQuery.Where(s => Math.Abs(s.AverageRating - rating) < FloatingPointTolerance);
subQuery = subQuery.WhereEqual(s => s.AverageRating, rating);
break;
case FilterComparison.GreaterThan:
subQuery = subQuery.Where(s => s.AverageRating > rating);
subQuery = subQuery.WhereGreaterThan(s => s.AverageRating, rating);
break;
case FilterComparison.GreaterThanEqual:
subQuery = subQuery.Where(s => s.AverageRating >= rating);
subQuery = subQuery.WhereGreaterThanOrEqual(s => s.AverageRating, rating);
break;
case FilterComparison.LessThan:
subQuery = subQuery.Where(s => s.AverageRating < rating);
subQuery = subQuery.WhereLessThan(s => s.AverageRating, rating);
break;
case FilterComparison.LessThanEqual:
subQuery = subQuery.Where(s => s.AverageRating <= rating);
subQuery = subQuery.WhereLessThanOrEqual(s => s.AverageRating, rating);
break;
case FilterComparison.NotEqual:
subQuery = subQuery.Where(s => Math.Abs(s.AverageRating - rating) > FloatingPointTolerance);
subQuery = subQuery.WhereNotEqual(s => s.AverageRating, rating);
break;
case FilterComparison.Matches:
case FilterComparison.Contains:
@ -534,21 +534,21 @@ public static class SeriesFilter
{
case FilterComparison.Equal:
case FilterComparison.Contains:
return queryable.Where(s => s.Metadata.People.Any(p => people.Contains(p.PersonId)));
return queryable.Where(s => s.Metadata.People.Any(p => people.Contains(p.PersonId) && p.Role == role));
case FilterComparison.NotEqual:
case FilterComparison.NotContains:
return queryable.Where(s => s.Metadata.People.All(t => !people.Contains(t.PersonId)));
return queryable.Where(s => s.Metadata.People.All(p => !people.Contains(p.PersonId) || p.Role != role));
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.PersonId == gId))));
queries.AddRange(people.Select(personId =>
queryable.Where(s => s.Metadata.People.Any(p => p.PersonId == personId && p.Role == role))));
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.)
// Ensure no person with the given role exists
return queryable.Where(s => s.Metadata.People.All(p => p.Role != role));
case FilterComparison.GreaterThan:
case FilterComparison.GreaterThanEqual:

View File

@ -16,6 +16,8 @@ namespace API.Extensions.QueryExtensions;
public static class QueryableExtensions
{
private const float DefaultTolerance = 0.001f;
public static Task<AgeRestriction> GetUserAgeRestriction(this DbSet<AppUser> queryable, int userId)
{
if (userId < 1)
@ -125,6 +127,140 @@ public static class QueryableExtensions
return queryable.Where(lambda);
}
public static IQueryable<T> WhereGreaterThan<T>(this IQueryable<T> source,
Expression<Func<T, float>> selector,
float value,
float tolerance = DefaultTolerance)
{
var parameter = selector.Parameters[0];
var propertyAccess = selector.Body;
// Absolute difference comparison: (propertyAccess - value) > tolerance
var difference = Expression.Subtract(propertyAccess, Expression.Constant(value));
var absoluteDifference = Expression.Condition(
Expression.LessThan(difference, Expression.Constant(0f)),
Expression.Negate(difference),
difference);
var greaterThanExpression = Expression.GreaterThan(propertyAccess, Expression.Constant(value));
var toleranceExpression = Expression.GreaterThan(absoluteDifference, Expression.Constant(tolerance));
var combinedExpression = Expression.AndAlso(greaterThanExpression, toleranceExpression);
var lambda = Expression.Lambda<Func<T, bool>>(combinedExpression, parameter);
return source.Where(lambda);
}
public static IQueryable<T> WhereGreaterThanOrEqual<T>(this IQueryable<T> source,
Expression<Func<T, float>> selector,
float value,
float tolerance = DefaultTolerance)
{
var parameter = selector.Parameters[0];
var propertyAccess = selector.Body;
var difference = Expression.Subtract(propertyAccess, Expression.Constant(value));
var absoluteDifference = Expression.Condition(
Expression.LessThan(difference, Expression.Constant(0f)),
Expression.Negate(difference),
difference);
var greaterThanOrEqualExpression = Expression.GreaterThanOrEqual(propertyAccess, Expression.Constant(value));
var toleranceExpression = Expression.GreaterThanOrEqual(absoluteDifference, Expression.Constant(tolerance));
var combinedExpression = Expression.AndAlso(greaterThanOrEqualExpression, toleranceExpression);
var lambda = Expression.Lambda<Func<T, bool>>(combinedExpression, parameter);
return source.Where(lambda);
}
public static IQueryable<T> WhereLessThan<T>(this IQueryable<T> source,
Expression<Func<T, float>> selector,
float value,
float tolerance = DefaultTolerance)
{
var parameter = selector.Parameters[0];
var propertyAccess = selector.Body;
var difference = Expression.Subtract(propertyAccess, Expression.Constant(value));
var absoluteDifference = Expression.Condition(
Expression.LessThan(difference, Expression.Constant(0f)),
Expression.Negate(difference),
difference);
var lessThanExpression = Expression.LessThan(propertyAccess, Expression.Constant(value));
var toleranceExpression = Expression.LessThan(absoluteDifference, Expression.Constant(tolerance));
var combinedExpression = Expression.AndAlso(lessThanExpression, toleranceExpression);
var lambda = Expression.Lambda<Func<T, bool>>(combinedExpression, parameter);
return source.Where(lambda);
}
public static IQueryable<T> WhereLessThanOrEqual<T>(this IQueryable<T> source,
Expression<Func<T, float>> selector,
float value,
float tolerance = DefaultTolerance)
{
var parameter = selector.Parameters[0];
var propertyAccess = selector.Body;
var difference = Expression.Subtract(propertyAccess, Expression.Constant(value));
var absoluteDifference = Expression.Condition(
Expression.LessThan(difference, Expression.Constant(0f)),
Expression.Negate(difference),
difference);
var lessThanOrEqualExpression = Expression.LessThanOrEqual(propertyAccess, Expression.Constant(value));
var toleranceExpression = Expression.LessThanOrEqual(absoluteDifference, Expression.Constant(tolerance));
var combinedExpression = Expression.AndAlso(lessThanOrEqualExpression, toleranceExpression);
var lambda = Expression.Lambda<Func<T, bool>>(combinedExpression, parameter);
return source.Where(lambda);
}
public static IQueryable<T> WhereEqual<T>(this IQueryable<T> source,
Expression<Func<T, float>> selector,
float value,
float tolerance = DefaultTolerance)
{
var parameter = selector.Parameters[0];
var propertyAccess = selector.Body;
// Absolute difference comparison: Math.Abs(propertyAccess - value) < tolerance
var difference = Expression.Subtract(propertyAccess, Expression.Constant(value));
var absoluteDifference = Expression.Condition(
Expression.LessThan(difference, Expression.Constant(0f)),
Expression.Negate(difference),
difference);
var toleranceExpression = Expression.LessThan(absoluteDifference, Expression.Constant(tolerance));
var lambda = Expression.Lambda<Func<T, bool>>(toleranceExpression, parameter);
return source.Where(lambda);
}
public static IQueryable<T> WhereNotEqual<T>(this IQueryable<T> source,
Expression<Func<T, float>> selector,
float value,
float tolerance = DefaultTolerance)
{
var parameter = selector.Parameters[0];
var propertyAccess = selector.Body;
var difference = Expression.Subtract(propertyAccess, Expression.Constant(value));
var absoluteDifference = Expression.Condition(
Expression.LessThan(difference, Expression.Constant(0f)),
Expression.Negate(difference),
difference);
var toleranceExpression = Expression.GreaterThan(absoluteDifference, Expression.Constant(tolerance));
var lambda = Expression.Lambda<Func<T, bool>>(toleranceExpression, parameter);
return source.Where(lambda);
}
/// <summary>
/// Performs a WhereLike that ORs multiple fields
/// </summary>

View File

@ -213,9 +213,8 @@ public class ProcessSeries : IProcessSeries
return;
}
BackgroundJob.Enqueue(() =>
_metadataService.GenerateCoversForSeries(series.LibraryId, series.Id, false, false));
BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(series.LibraryId, series.Id, forceUpdate));
await _metadataService.GenerateCoversForSeries(series.LibraryId, series.Id, false, false);
await _wordCountAnalyzerService.ScanSeries(series.LibraryId, series.Id, forceUpdate);
}
private async Task ReportDuplicateSeriesLookup(Library library, ParserInfo firstInfo, Exception ex)

View File

@ -464,6 +464,7 @@
"version": "18.2.9",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.9.tgz",
"integrity": "sha512-4iMoRvyMmq/fdI/4Gob9HKjL/jvTlCjbS4kouAYHuGO9w9dmUhi1pY1z+mALtCEl9/Q8CzU2W8e5cU2xtV4nVg==",
"dev": true,
"dependencies": {
"@babel/core": "7.25.2",
"@jridgewell/sourcemap-codec": "^1.4.14",
@ -491,6 +492,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"dev": true,
"dependencies": {
"readdirp": "^4.0.1"
},
@ -505,6 +507,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
"dev": true,
"engines": {
"node": ">= 14.16.0"
},
@ -4007,7 +4010,8 @@
"node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"dev": true
},
"node_modules/cosmiconfig": {
"version": "8.3.6",
@ -4514,6 +4518,7 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.2"
@ -4523,6 +4528,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"optional": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@ -7464,7 +7470,8 @@
"node_modules/reflect-metadata": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"dev": true
},
"node_modules/replace-in-file": {
"version": "7.1.0",
@ -7735,7 +7742,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"devOptional": true
"dev": true
},
"node_modules/sass": {
"version": "1.77.6",
@ -7769,6 +7776,7 @@
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
},
@ -8323,6 +8331,7 @@
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"