mirror of
https://github.com/Kareadita/Kavita.git
synced 2026-04-24 10:09:30 -04:00
FilterV3 backend
This commit is contained in:
parent
68be9ab722
commit
7c8e04d3f9
13
Kavita.API/Services/IFilterV3Service.cs
Normal file
13
Kavita.API/Services/IFilterV3Service.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Threading.Tasks;
|
||||
using Kavita.Models.DTOs.Filtering.v3;
|
||||
|
||||
namespace Kavita.API.Services;
|
||||
|
||||
public interface IFilterV3Service
|
||||
{
|
||||
|
||||
Task<FilterResponse> Filter(int userId, FilterV3Dto filter);
|
||||
|
||||
FilterConfigurationDto GetConfiguration();
|
||||
|
||||
}
|
||||
24
Kavita.Models/DTOs/Filtering/v3/FilterConfigurationDto.cs
Normal file
24
Kavita.Models/DTOs/Filtering/v3/FilterConfigurationDto.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using Kavita.Models.DTOs.Filtering.v2;
|
||||
|
||||
namespace Kavita.Models.DTOs.Filtering.v3;
|
||||
|
||||
public sealed record FilterConfigurationDto
|
||||
{
|
||||
|
||||
public List<FilterEntityConfigurationDto> Series { get; set; }
|
||||
public List<FilterEntityConfigurationDto> Chapters { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public sealed record FilterEntityConfigurationDto
|
||||
{
|
||||
public FilterEntity Entity { get; set; }
|
||||
public List<FilterFieldConfigurationDto> Fields { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed record FilterFieldConfigurationDto
|
||||
{
|
||||
public FilterFieldV3 Field { get; set; }
|
||||
public List<FilterComparison> Comparisons { get; set; } = [];
|
||||
}
|
||||
13
Kavita.Models/DTOs/Filtering/v3/FilterEntity.cs
Normal file
13
Kavita.Models/DTOs/Filtering/v3/FilterEntity.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Kavita.Models.DTOs.Filtering.v3;
|
||||
|
||||
public enum FilterEntity
|
||||
{
|
||||
Series = 0,
|
||||
Chapters = 1,
|
||||
Volumes = 2,
|
||||
Annotations = 3,
|
||||
People = 4,
|
||||
ReadingLists = 5,
|
||||
Collections = 6,
|
||||
Reviews = 7,
|
||||
}
|
||||
10
Kavita.Models/DTOs/Filtering/v3/FilterFieldV3.cs
Normal file
10
Kavita.Models/DTOs/Filtering/v3/FilterFieldV3.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Kavita.Models.DTOs.Filtering.v3;
|
||||
|
||||
public enum FilterFieldV3
|
||||
{
|
||||
SeriesName = 0,
|
||||
AgeRating = 1,
|
||||
FileSize = 2,
|
||||
Library = 3,
|
||||
Progress = 4,
|
||||
}
|
||||
11
Kavita.Models/DTOs/Filtering/v3/FilterResponse.cs
Normal file
11
Kavita.Models/DTOs/Filtering/v3/FilterResponse.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Kavita.Models.DTOs.Filtering.v3;
|
||||
|
||||
public sealed record FilterResponse
|
||||
{
|
||||
|
||||
public List<SeriesDto> Series { get; set; }
|
||||
public List<ChapterDto> Chapters { get; set; }
|
||||
|
||||
}
|
||||
26
Kavita.Models/DTOs/Filtering/v3/FilterV3Dto.cs
Normal file
26
Kavita.Models/DTOs/Filtering/v3/FilterV3Dto.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using Kavita.Models.DTOs.Filtering.v2;
|
||||
|
||||
namespace Kavita.Models.DTOs.Filtering.v3;
|
||||
|
||||
public sealed record FilterV3Dto
|
||||
{
|
||||
|
||||
public List<FilterV3GroupDto> Groups { get; set; }
|
||||
public FilterCombination Combination { get; set; }
|
||||
public List<FilterEntity> RequestedEntities { get; set; }
|
||||
}
|
||||
|
||||
public sealed record FilterV3GroupDto
|
||||
{
|
||||
public FilterCombination Combination { get; set; }
|
||||
public List<FilterV3StatementDto> Statements { get; set; }
|
||||
}
|
||||
|
||||
public sealed record FilterV3StatementDto
|
||||
{
|
||||
public FilterEntity Entity { get; set; }
|
||||
public FilterComparison Comparison { get; set; }
|
||||
public FilterFieldV3 Field { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
25
Kavita.Server/Controllers/FilterV3Controller.cs
Normal file
25
Kavita.Server/Controllers/FilterV3Controller.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Threading.Tasks;
|
||||
using AutoMapper;
|
||||
using Kavita.API.Database;
|
||||
using Kavita.API.Services;
|
||||
using Kavita.Models.DTOs.Filtering.v3;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Kavita.Server.Controllers;
|
||||
|
||||
public class FilterV3Controller(IFilterV3Service filterV3Service, IDataContext dataContext, IMapper mapper): BaseApiController
|
||||
{
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<FilterResponse>> Filter([FromBody] FilterV3Dto filter)
|
||||
{
|
||||
return Ok(await filterV3Service.Filter(UserId, filter));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<FilterConfigurationDto> GetFilterConfiguration()
|
||||
{
|
||||
return Ok(filterV3Service.GetConfiguration());
|
||||
}
|
||||
|
||||
}
|
||||
@ -7,6 +7,7 @@ using Kavita.API.Services.Reading;
|
||||
using Kavita.API.Services.ReadingLists;
|
||||
using Kavita.API.Services.Scanner;
|
||||
using Kavita.API.Services.SignalR;
|
||||
using Kavita.Services.Filtering;
|
||||
using Kavita.Services.Helpers;
|
||||
using Kavita.Services.HostedServices;
|
||||
using Kavita.Services.Metadata;
|
||||
@ -57,6 +58,7 @@ public static class ApplicationServiceExtensions
|
||||
services.AddScoped<IFontService, FontService>();
|
||||
services.AddScoped<IAnnotationService, AnnotationService>();
|
||||
services.AddScoped<IOpdsService, OpdsService>();
|
||||
services.AddScoped<IFilterV3Service, FilterV3Service>();
|
||||
|
||||
services.AddScoped<ICblExportService, CblExportService>();
|
||||
services.AddScoped<ICblGithubService, CblGithubService>();
|
||||
|
||||
83
Kavita.Services/Filtering/ComparisonField.cs
Normal file
83
Kavita.Services/Filtering/ComparisonField.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Kavita.Common.Extensions;
|
||||
using Kavita.Database.Extensions;
|
||||
using Kavita.Database.Extensions.Filters;
|
||||
using Kavita.Models.DTOs.Filtering.v2;
|
||||
using Kavita.Models.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Kavita.Services.Filtering;
|
||||
|
||||
public class SeriesComparisonField : IFilterField<Series>
|
||||
{
|
||||
public IQueryable<Series> Apply(IQueryable<Series> query, FilterComparison comparison, FilterContext<string> context)
|
||||
{
|
||||
if (string.IsNullOrEmpty(context.Value)) return query;
|
||||
|
||||
var value = context.Value.AsFloat();
|
||||
|
||||
return query.HasReadingProgress(true, comparison, value, context.UserId);
|
||||
}
|
||||
|
||||
public IReadOnlySet<FilterComparison> SupportedComparisons { get; } = new ReadOnlySet<FilterComparison>(new HashSet<FilterComparison>([
|
||||
FilterComparison.Equal, FilterComparison.NotEqual,
|
||||
FilterComparison.GreaterThan, FilterComparison.GreaterThanEqual,
|
||||
FilterComparison.LessThan, FilterComparison.LessThanEqual
|
||||
]));
|
||||
}
|
||||
|
||||
public class ChapterComparisonField : IFilterField<Chapter>
|
||||
{
|
||||
public IQueryable<Chapter> Apply(IQueryable<Chapter> query, FilterComparison comparison, FilterContext<string> context)
|
||||
{
|
||||
if (string.IsNullOrEmpty(context.Value)) return query;
|
||||
|
||||
var readProgress = context.Value.AsFloat();
|
||||
|
||||
var subQuery = query
|
||||
.Select(s => new
|
||||
{
|
||||
ChapterId = s.Id,
|
||||
Percentage = s.UserProgress
|
||||
.Where(p => p != null && p.AppUserId == context.UserId)
|
||||
.Sum(p => p != null ? (p.PagesRead * 1.0f / s.Pages) : 0f) * 100f
|
||||
})
|
||||
.AsSplitQuery();
|
||||
|
||||
switch (comparison)
|
||||
{
|
||||
case FilterComparison.Equal:
|
||||
subQuery = subQuery.WhereEqual(s => s.Percentage, readProgress);
|
||||
break;
|
||||
case FilterComparison.GreaterThan:
|
||||
subQuery = subQuery.WhereGreaterThan(s => s.Percentage, readProgress);
|
||||
break;
|
||||
case FilterComparison.GreaterThanEqual:
|
||||
subQuery = subQuery.WhereGreaterThanOrEqual(s => s.Percentage, readProgress);
|
||||
break;
|
||||
case FilterComparison.LessThan:
|
||||
subQuery = subQuery.WhereLessThan(s => s.Percentage, readProgress);
|
||||
break;
|
||||
case FilterComparison.LessThanEqual:
|
||||
subQuery = subQuery.WhereLessThanOrEqual(s => s.Percentage, readProgress);
|
||||
break;
|
||||
case FilterComparison.NotEqual:
|
||||
subQuery = subQuery.WhereNotEqual(s => s.Percentage, readProgress);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null);
|
||||
}
|
||||
|
||||
var ids = subQuery.Select(s => s.ChapterId);
|
||||
return query.Where(c => ids.Contains(c.Id));
|
||||
}
|
||||
|
||||
public IReadOnlySet<FilterComparison> SupportedComparisons { get; } = new ReadOnlySet<FilterComparison>(new HashSet<FilterComparison>([
|
||||
FilterComparison.Equal, FilterComparison.NotEqual,
|
||||
FilterComparison.GreaterThan, FilterComparison.GreaterThanEqual,
|
||||
FilterComparison.LessThan, FilterComparison.LessThanEqual
|
||||
]));
|
||||
}
|
||||
50
Kavita.Services/Filtering/FilterEntityBuilder.cs
Normal file
50
Kavita.Services/Filtering/FilterEntityBuilder.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Kavita.Models.DTOs.Filtering.v2;
|
||||
using Kavita.Models.DTOs.Filtering.v3;
|
||||
|
||||
namespace Kavita.Services.Filtering;
|
||||
|
||||
public interface IFilterEntity<TEntity>
|
||||
{
|
||||
IQueryable<TEntity> Apply(IQueryable<TEntity> query, FilterV3StatementDto statement, FilterContext<string> context);
|
||||
|
||||
IReadOnlyDictionary<FilterFieldV3, IReadOnlySet<FilterComparison>> SupportedComparisons { get; }
|
||||
}
|
||||
|
||||
public class FilterEntityBuilder<TEntity>
|
||||
{
|
||||
|
||||
private readonly Dictionary<FilterFieldV3, IFilterField<TEntity>> _fields = [];
|
||||
|
||||
public FilterEntityBuilder<TEntity> WithField(FilterFieldV3 field, IFilterField<TEntity> filterField)
|
||||
{
|
||||
if (!_fields.TryAdd(field, filterField))
|
||||
throw new ArgumentException("Cannot register for the same field twice", nameof(field));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public IFilterEntity<TEntity> Build()
|
||||
{
|
||||
return new FilterEntity(_fields);
|
||||
}
|
||||
|
||||
private sealed class FilterEntity(Dictionary<FilterFieldV3, IFilterField<TEntity>> fields): IFilterEntity<TEntity>
|
||||
{
|
||||
public IQueryable<TEntity> Apply(IQueryable<TEntity> query, FilterV3StatementDto statement, FilterContext<string> context)
|
||||
{
|
||||
if (fields.TryGetValue(statement.Field, out var field))
|
||||
{
|
||||
return field.Apply(query, statement.Comparison, context);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<FilterFieldV3, IReadOnlySet<FilterComparison>> SupportedComparisons { get; } =
|
||||
fields.ToDictionary(kv => kv.Key, kv => kv.Value.SupportedComparisons).AsReadOnly();
|
||||
}
|
||||
|
||||
}
|
||||
110
Kavita.Services/Filtering/FilterFieldBuilder.cs
Normal file
110
Kavita.Services/Filtering/FilterFieldBuilder.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Kavita.Common.Extensions;
|
||||
using Kavita.Models.DTOs.Filtering.v2;
|
||||
|
||||
namespace Kavita.Services.Filtering;
|
||||
|
||||
public interface IFilterField<TEntity>
|
||||
{
|
||||
IQueryable<TEntity> Apply(IQueryable<TEntity> query, FilterComparison comparison, FilterContext<string> context);
|
||||
IReadOnlySet<FilterComparison> SupportedComparisons { get; }
|
||||
}
|
||||
|
||||
public delegate Expression<Func<TEntity, bool>> FilterExpression<TEntity, TValue>(FilterContext<TValue> ctx);
|
||||
public delegate IQueryable<TEntity> FilterFunc<TEntity, TValue>(FilterContext<TValue> ctx, IQueryable<TEntity> query);
|
||||
|
||||
public class FilterFieldBuilder<TEntity, TValue>(Func<string, TValue> convertor)
|
||||
where TEntity: class
|
||||
{
|
||||
private readonly Dictionary<FilterComparison, FilterExpression<TEntity, TValue>> _comparisons = [];
|
||||
private readonly Dictionary<FilterComparison, FilterFunc<TEntity, TValue>> _funcComparisons = [];
|
||||
private Func<FilterContext<TValue>, bool>? _guard;
|
||||
|
||||
public FilterFieldBuilder<TEntity, TValue> WithGuard(Func<FilterContext<TValue>, bool> guard)
|
||||
{
|
||||
_guard = guard;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FilterFieldBuilder<TEntity, TValue> WithComparison(FilterComparison comparison, FilterExpression<TEntity, TValue> expression)
|
||||
{
|
||||
if (_funcComparisons.ContainsKey(comparison))
|
||||
throw new ArgumentException("Cannot register the same comparison twice", nameof(comparison));
|
||||
|
||||
if (!_comparisons.TryAdd(comparison, expression))
|
||||
throw new ArgumentException("Cannot register the same comparison twice", nameof(comparison));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public FilterFieldBuilder<TEntity, TValue> WithComparison(FilterComparison combination, FilterFunc<TEntity, TValue> func)
|
||||
{
|
||||
if (_comparisons.ContainsKey(combination))
|
||||
throw new ArgumentException("Cannot register the same combination twice", nameof(combination));
|
||||
|
||||
if (!_funcComparisons.TryAdd(combination, func))
|
||||
throw new ArgumentException("Cannot register the same combination twice", nameof(combination));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public IFilterField<TEntity> Build()
|
||||
{
|
||||
return new FilterField(_comparisons, _funcComparisons, convertor, _guard);
|
||||
}
|
||||
|
||||
private sealed class FilterField(
|
||||
Dictionary<FilterComparison, FilterExpression<TEntity, TValue>> comparisons,
|
||||
Dictionary<FilterComparison, FilterFunc<TEntity, TValue>> funcComparisons,
|
||||
Func<string, TValue> converter,
|
||||
Func<FilterContext<TValue>, bool>? guard
|
||||
) : IFilterField<TEntity>
|
||||
{
|
||||
public IQueryable<TEntity> Apply(IQueryable<TEntity> query, FilterComparison comparison, FilterContext<string> context)
|
||||
{
|
||||
if (comparisons.TryGetValue(comparison, out var expression))
|
||||
{
|
||||
var value = converter(context.Value);
|
||||
var newContext = new FilterContext<TValue> { Value = value, UserId = context.UserId };
|
||||
if (guard != null && !guard(newContext)) return query;
|
||||
|
||||
return query.Where(expression(newContext));
|
||||
}
|
||||
|
||||
if (funcComparisons.TryGetValue(comparison, out var func))
|
||||
{
|
||||
var value = converter(context.Value);
|
||||
var newContext = new FilterContext<TValue> { Value = value, UserId = context.UserId };
|
||||
if (guard != null && !guard(newContext)) return query;
|
||||
|
||||
return func(newContext, query);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public IReadOnlySet<FilterComparison> SupportedComparisons { get; } = comparisons.Keys
|
||||
.Union(funcComparisons.Keys).ToHashSet();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class StringFilterFieldBuilder<TEntity>() : FilterFieldBuilder<TEntity, string>(s => s)
|
||||
where TEntity : class;
|
||||
|
||||
public class IntArrayFilterFieldBuilder<TEntity>() : FilterFieldBuilder<TEntity, IList<int>>(s => s.ParseIntArray())
|
||||
where TEntity : class;
|
||||
|
||||
public class IntFilterFieldBuilder<TEntity>() : FilterFieldBuilder<TEntity, int>(int.Parse)
|
||||
where TEntity : class;
|
||||
|
||||
public class EnumArrayFilterFieldBuilder<TEntity, TEnum>() : FilterFieldBuilder<TEntity, IList<TEnum>>(s => s
|
||||
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(Enum.Parse<TEnum>)
|
||||
.ToList()
|
||||
)
|
||||
where TEntity : class
|
||||
where TEnum : struct;
|
||||
98
Kavita.Services/Filtering/FilterPipelineBuilder.cs
Normal file
98
Kavita.Services/Filtering/FilterPipelineBuilder.cs
Normal file
@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Kavita.Models.DTOs.Filtering.v2;
|
||||
using Kavita.Models.DTOs.Filtering.v3;
|
||||
|
||||
namespace Kavita.Services.Filtering;
|
||||
|
||||
public interface IFilterPipeline<TEntity>
|
||||
{
|
||||
IQueryable<TEntity> Apply(IQueryable<TEntity> query, FilterV3Dto filter, int userId);
|
||||
|
||||
IReadOnlyDictionary<FilterEntity, IReadOnlyDictionary<FilterFieldV3, IReadOnlySet<FilterComparison>>> SupportedEntities { get; }
|
||||
|
||||
List<FilterEntityConfigurationDto> Configuration();
|
||||
}
|
||||
|
||||
public class FilterContext<TValue>
|
||||
{
|
||||
public required int UserId { get; init; }
|
||||
public required TValue Value { get; init; }
|
||||
}
|
||||
|
||||
public class FilterPipelineBuilder<TEntity>
|
||||
{
|
||||
|
||||
private readonly Dictionary<FilterEntity, IFilterEntity<TEntity>> _entities = [];
|
||||
|
||||
public FilterPipelineBuilder<TEntity> WithEntity(FilterEntity entity, IFilterEntity<TEntity> filterEntity)
|
||||
{
|
||||
if (!_entities.TryAdd(entity, filterEntity))
|
||||
throw new ArgumentException("Cannot register the same entity twice", nameof(entity));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public IFilterPipeline<TEntity> Build()
|
||||
{
|
||||
return new FilterPipeline(_entities);
|
||||
}
|
||||
|
||||
private sealed class FilterPipeline(Dictionary<FilterEntity, IFilterEntity<TEntity>> entities): IFilterPipeline<TEntity>
|
||||
{
|
||||
public IQueryable<TEntity> Apply(IQueryable<TEntity> query, FilterV3Dto filter, int userId)
|
||||
{
|
||||
var groups = filter.Groups
|
||||
.Select(group => ApplyGroup(query, group, userId))
|
||||
.ToList();
|
||||
|
||||
if (groups.Count == 0) return query; // Return everything if no filters
|
||||
|
||||
return filter.Combination == FilterCombination.And
|
||||
? groups.Aggregate((current, next) => current.Intersect(next))
|
||||
: groups.Aggregate((current, next) => current.Union(next));
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<FilterEntity, IReadOnlyDictionary<FilterFieldV3, IReadOnlySet<FilterComparison>>> SupportedEntities { get; } =
|
||||
entities.ToDictionary(kv => kv.Key, kv => kv.Value.SupportedComparisons).AsReadOnly();
|
||||
|
||||
public List<FilterEntityConfigurationDto> Configuration()
|
||||
{
|
||||
return SupportedEntities.Select(kv => new FilterEntityConfigurationDto()
|
||||
{
|
||||
Entity = kv.Key,
|
||||
Fields = kv.Value.Select(kv => new FilterFieldConfigurationDto()
|
||||
{
|
||||
Field = kv.Key,
|
||||
Comparisons = kv.Value.ToList()
|
||||
}).ToList()
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private IQueryable<TEntity> ApplyGroup(IQueryable<TEntity> query, FilterV3GroupDto group, int userId)
|
||||
{
|
||||
var statements = group.Statements
|
||||
.Select(statement => ApplyEntity(query, statement, new FilterContext<string> { UserId = userId, Value = statement.Value }))
|
||||
.ToList();
|
||||
|
||||
if (statements.Count == 0) return query;
|
||||
|
||||
return group.Combination == FilterCombination.And
|
||||
? statements.Aggregate((current, next) => current.Intersect(next))
|
||||
: statements.Aggregate((current, next) => current.Union(next));
|
||||
}
|
||||
|
||||
|
||||
private IQueryable<TEntity> ApplyEntity(IQueryable<TEntity> query, FilterV3StatementDto statement, FilterContext<string> context)
|
||||
{
|
||||
if (entities.TryGetValue(statement.Entity, out var entity))
|
||||
{
|
||||
return entity.Apply(query, statement, context);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
102
Kavita.Services/Filtering/FilterV3Service.cs
Normal file
102
Kavita.Services/Filtering/FilterV3Service.cs
Normal file
@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AutoMapper;
|
||||
using Kavita.API.Database;
|
||||
using Kavita.API.Services;
|
||||
using Kavita.Common.Extensions;
|
||||
using Kavita.Database;
|
||||
using Kavita.Database.Extensions;
|
||||
using Kavita.Models.DTOs;
|
||||
using Kavita.Models.DTOs.Filtering.v2;
|
||||
using Kavita.Models.DTOs.Filtering.v3;
|
||||
using Kavita.Models.Entities;
|
||||
using Kavita.Models.Entities.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Kavita.Services.Filtering;
|
||||
|
||||
public class FilterV3Service(DataContext dataContext, IMapper mapper): IFilterV3Service
|
||||
{
|
||||
|
||||
private static readonly IFilterPipeline<Series> SeriesFilterPipeline = new FilterPipelineBuilder<Series>()
|
||||
.WithEntity(FilterEntity.Series, new FilterEntityBuilder<Series>()
|
||||
.WithField(FilterFieldV3.AgeRating, new EnumArrayFilterFieldBuilder<Series, AgeRating>()
|
||||
.WithGuard(ctx => ctx.Value.Count > 0)
|
||||
.WithComparison(FilterComparison.LessThan, ctx => s => s.Metadata.AgeRating < ctx.Value[0])
|
||||
.WithComparison(FilterComparison.LessThanEqual, ctx => s => s.Metadata.AgeRating <= ctx.Value[0])
|
||||
.WithComparison(FilterComparison.Equal, ctx => s => s.Metadata.AgeRating == ctx.Value[0])
|
||||
.WithComparison(FilterComparison.GreaterThanEqual, ctx => s => s.Metadata.AgeRating > ctx.Value[0])
|
||||
.WithComparison(FilterComparison.GreaterThan, ctx => s => s.Metadata.AgeRating <= ctx.Value[0])
|
||||
.WithComparison(FilterComparison.Contains, ctx => s => ctx.Value.Contains(s.Metadata.AgeRating))
|
||||
.Build())
|
||||
.WithField(FilterFieldV3.SeriesName, new StringFilterFieldBuilder<Series>()
|
||||
.WithGuard(ctx => !string.IsNullOrWhiteSpace(ctx.Value))
|
||||
.WithComparison(FilterComparison.Contains, ctx => s => EF.Functions.Like(s.Name, $"%{ctx.Value}%"))
|
||||
.Build())
|
||||
.WithField(FilterFieldV3.Library, new IntArrayFilterFieldBuilder<Series>()
|
||||
.WithGuard(ctx => ctx.Value.Count > 0)
|
||||
.WithComparison(FilterComparison.Contains, ctx => s => ctx.Value.Contains(s.LibraryId))
|
||||
.Build())
|
||||
.WithField(FilterFieldV3.Progress, new SeriesComparisonField())
|
||||
.Build())
|
||||
.WithEntity(FilterEntity.Chapters, new FilterEntityBuilder<Series>()
|
||||
.WithField(FilterFieldV3.FileSize, new FilterFieldBuilder<Series, long>(s => s.ParseHumanReadableBytes())
|
||||
.WithGuard(ctx => ctx.Value > 0)
|
||||
.WithComparison(FilterComparison.GreaterThan, ctx => s => s.Volumes
|
||||
.Any(v => v.Chapters.Any(c => c.Files.Sum(f => f.Bytes) > ctx.Value)))
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
private static readonly IFilterPipeline<Chapter> ChapterFilterPipeline = new FilterPipelineBuilder<Chapter>()
|
||||
.WithEntity(FilterEntity.Chapters, new FilterEntityBuilder<Chapter>()
|
||||
.WithField(FilterFieldV3.FileSize, new FilterFieldBuilder<Chapter, long>(s => s.ParseHumanReadableBytes())
|
||||
.WithComparison(FilterComparison.GreaterThan, ctx => s => s.Files.Sum(f => f.Bytes) > ctx.Value)
|
||||
.Build())
|
||||
.WithField(FilterFieldV3.Progress, new ChapterComparisonField())
|
||||
.Build())
|
||||
.WithEntity(FilterEntity.Series, new FilterEntityBuilder<Chapter>()
|
||||
.WithField(FilterFieldV3.SeriesName, new StringFilterFieldBuilder<Chapter>()
|
||||
.WithGuard(ctx => !string.IsNullOrWhiteSpace(ctx.Value))
|
||||
.WithComparison(FilterComparison.Contains, ctx => c => EF.Functions.Like(c.Volume.Series.Name, $"%{ctx.Value}%"))
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
public async Task<FilterResponse> Filter(int userId, FilterV3Dto filter)
|
||||
{
|
||||
var seriesTask = ApplyFilterWithProgress<Series, SeriesDto>(FilterEntity.Series, SeriesFilterPipeline, filter, userId);
|
||||
var chapterTask = ApplyFilterWithProgress<Chapter, ChapterDto>(FilterEntity.Chapters, ChapterFilterPipeline, filter, userId);
|
||||
|
||||
await Task.WhenAll(seriesTask, chapterTask);
|
||||
|
||||
return new FilterResponse
|
||||
{
|
||||
Series = await seriesTask,
|
||||
Chapters = await chapterTask
|
||||
};
|
||||
}
|
||||
|
||||
public FilterConfigurationDto GetConfiguration()
|
||||
{
|
||||
return new FilterConfigurationDto
|
||||
{
|
||||
Series = SeriesFilterPipeline.Configuration(),
|
||||
Chapters = ChapterFilterPipeline.Configuration()
|
||||
};
|
||||
}
|
||||
|
||||
private Task<List<TEntityDto>> ApplyFilterWithProgress<TEntity, TEntityDto>(FilterEntity entity,
|
||||
IFilterPipeline<TEntity> filterPipeline, FilterV3Dto filter, int userId)
|
||||
where TEntity : class
|
||||
{
|
||||
if (!filter.RequestedEntities.Contains(entity))
|
||||
return Task.FromResult<List<TEntityDto>>([]);
|
||||
|
||||
return filterPipeline.Apply(dataContext.Set<TEntity>().AsNoTracking(), filter, userId)
|
||||
.ProjectToWithProgress<TEntity, TEntityDto>(mapper, userId)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user