Skip to content

Commit

Permalink
API for expression listing
Browse files Browse the repository at this point in the history
  • Loading branch information
da3dsoul committed Oct 2, 2023
1 parent 1d08c85 commit be5af99
Show file tree
Hide file tree
Showing 107 changed files with 840 additions and 24 deletions.
100 changes: 100 additions & 0 deletions Shoko.Server/API/v3/Controllers/FilterController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Shoko.Server.API.v3.Models.Common;
using Shoko.Server.API.v3.Models.Shoko;
using Shoko.Server.Filters;
using Shoko.Server.Filters.Interfaces;
using Shoko.Server.Models;
using Shoko.Server.Repositories;
using Shoko.Server.Settings;
Expand All @@ -34,6 +35,8 @@ public class FilterController : BaseController
private readonly FilterFactory _factory;
private readonly SeriesFactory _seriesFactory;
private readonly FilterEvaluator _filterEvaluator;
private static Filter.FilterExpressionHelp[] _expressionTypes;
private static Filter.SortingCriteriaHelp[] _sortingTypes;

#region Existing Filters

Expand Down Expand Up @@ -87,6 +90,103 @@ public ActionResult<Filter> AddNewFilter(Filter.Input.CreateOrUpdateFilterBody b
return filter;
}

/// <summary>
/// Lists the available expressions.
/// The word "Filterable" is used a lot. It is a generic word for a series or group, depending on what the filter is set to apply to.
/// Expression: The identifier used to create the expression. eg. And, Not, HasTag.
/// Type: Parameters have a type, and this is the type that needs to match.
/// Left, Right, Parameter, and SecondParameter show what type the expression supports as parameters.
/// Left and Right are Expressions or Selectors. Parameters are constants.
/// </summary>
[HttpGet("Expressions")]
public ActionResult<Filter.FilterExpressionHelp[]> GetExpressions()
{
// get all classes that derive from FilterExpression, but not SortingExpression
_expressionTypes ??= AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes())
.Where(a => a != typeof(FilterExpression) && !a.IsGenericType && typeof(FilterExpression).IsAssignableFrom(a) && !typeof(SortingExpression).IsAssignableFrom(a))
.OrderBy(a => a.FullName).Select(a =>
{
var expression = (FilterExpression)Activator.CreateInstance(a);
if (expression == null) return null;
Filter.FilterExpressionHelp.FilterExpressionParameterType? left = expression switch
{
IWithExpressionParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.Expression,
IWithDateSelectorParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.DateSelector,
IWithNumberSelectorParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.NumberSelector,
IWithStringSelectorParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.StringSelector,
_ => null
};
Filter.FilterExpressionHelp.FilterExpressionParameterType? right = expression switch
{
IWithSecondExpressionParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.Expression,
IWithSecondDateSelectorParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.DateSelector,
IWithSecondNumberSelectorParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.NumberSelector,
IWithSecondStringSelectorParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.StringSelector,
_ => null
};
Filter.FilterExpressionHelp.FilterExpressionParameterType? parameter = expression switch
{
IWithDateParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.Date,
IWithNumberParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.Number,
IWithStringParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.String,
IWithTimeSpanParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.TimeSpan,
_ => null
};
Filter.FilterExpressionHelp.FilterExpressionParameterType? secondParameter = expression switch
{
IWithSecondStringParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.String,
_ => null
};
var type = expression switch
{
FilterExpression<bool> => Filter.FilterExpressionHelp.FilterExpressionParameterType.Expression,
FilterExpression<DateTime?> => Filter.FilterExpressionHelp.FilterExpressionParameterType.DateSelector,
FilterExpression<double> => Filter.FilterExpressionHelp.FilterExpressionParameterType.NumberSelector,
FilterExpression<string> => Filter.FilterExpressionHelp.FilterExpressionParameterType.StringSelector,
_ => throw new Exception($"Expression {a.Name} is not a handled type for Filter Expression Help")
};
return new Filter.FilterExpressionHelp
{
Expression = a.Name.Replace("Expression", ""),
Description = expression.HelpDescription,
PossibleParameters = expression.HelpPossibleParameters,
PossibleSecondParameters = expression.HelpPossibleSecondParameters,
Left = left,
Right = right,
Parameter = parameter,
SecondParameter = secondParameter,
Type = type
};
}).Where(a => a != null).ToArray();
return _expressionTypes;
}

/// <summary>
/// Lists the available sorting expressions. These are basically selectors that the filter system uses to sort.
/// The word "Filterable" is used a lot. It is a generic word for a series or group, depending on what the filter is set to apply to.
/// Type: The identifier used to create the expression. eg. AddedDate.
/// IsInverted: Whether the sorting should be in descending order.
/// Next: If the expression returns equal values, it defers to the next expression to sort more predictably.
/// For example, MissingEpisodeCount,Descending -> AirDate, Descending would have thing with the most missing episodes, then the last aired first.
/// </summary>
[HttpGet("SortingCriteria")]
public ActionResult<Filter.SortingCriteriaHelp[]> GetSortingCriteria()
{
// get all classes that derive from FilterExpression, but not SortingExpression
_sortingTypes ??= AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).Where(a =>
a != typeof(FilterExpression) && !a.IsAbstract && !a.IsGenericType && typeof(SortingExpression).IsAssignableFrom(a)).OrderBy(a => a.FullName)
.Select(a =>
{
var criteria = (SortingExpression)Activator.CreateInstance(a);
if (criteria == null) return null;
return new Filter.SortingCriteriaHelp
{
Type = a.Name.Replace("SortingSelector", ""), Description = criteria.HelpDescription
};
}).Where(a => a != null).ToArray();
return _sortingTypes;
}

/// <summary>
/// Get the <see cref="Filter"/> for the given <paramref name="filterID"/>.
/// </summary>
Expand Down
116 changes: 116 additions & 0 deletions Shoko.Server/API/v3/Models/Shoko/Filter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Shoko.Server.API.v3.Models.Common;

// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
Expand Down Expand Up @@ -61,6 +62,7 @@ public class FilterIDs : IDs
/// <summary>
/// The <see cref="IDs.ID"/> of the parent <see cref="Filter"/>, if it has one.
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public int? ParentFilter { get; set; }
}

Expand Down Expand Up @@ -106,6 +108,119 @@ public class FilterCondition
public string? SecondParameter { get; set; }
}

public class FilterExpressionHelp
{
/// <summary>
/// The internal type name of the FilterExpression
/// This is what you give the API, not actually the internal type (it is the internal type without the word Expression)
/// </summary>
[Required]
public string Expression { get; init; }

/// <summary>
/// A description of what the expression is doing, comparing, etc
/// </summary>
[Required]
public string Description { get; init; }

/// <summary>
/// This is what the expression would be considered for parameters, for example, Air Date is a Date Selector
/// </summary>
[Required]
[JsonConverter(typeof(StringEnumConverter))]
public FilterExpressionParameterType Type { get; init; }

/// <summary>
/// The parameter type that the <see cref="FilterCondition.Left"/> property requires
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(StringEnumConverter))]
public FilterExpressionParameterType? Left { get; init; }

/// <summary>
/// The parameter types that the <see cref="FilterCondition.Right"/> property requires
/// If multiple are given, then at least one is required
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(StringEnumConverter))]
public FilterExpressionParameterType? Right { get; init; }

/// <summary>
/// The parameter type that the <see cref="FilterCondition.Parameter"/> property requires.
/// This will always be a string for simplicity in type safety, but the type is what it expects
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(StringEnumConverter))]
public FilterExpressionParameterType? Parameter { get; init; }

/// <summary>
/// This will list the possible parameters, usually with the most common ones first.
/// </summary>
public string[]? PossibleParameters { get; init; }

/// <summary>
/// This will list the possible parameters, usually with the most common ones first.
/// </summary>
public string[]? PossibleSecondParameters { get; init; }

/// <summary>
/// The parameter type that the <see cref="FilterCondition.SecondParameter"/> property requires
/// This will always be a string for simplicity in type safety, but the type is what it expects
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(StringEnumConverter))]
public FilterExpressionParameterType? SecondParameter { get; init; }

/// <summary>
/// Magical Json.Net stuff
/// </summary>
public bool ShouldSerializePossibleParameters()
{
return PossibleParameters?.Length > 0;
}

/// <summary>
/// Magical Json.Net stuff
/// </summary>
public bool ShouldSerializePossibleSecondParameters()
{
return PossibleSecondParameters?.Length > 0;
}

/// <summary>
/// The type of the parameter. Expressions return a boolean, Selectors return the type of their name, and the rest are values from the user.
/// Dates are in yyyy-MM-dd format
/// TimeSpans are in d:HH:mm:ss.ffff format (f is milliseconds)
/// </summary>
public enum FilterExpressionParameterType
{
Expression,
DateSelector,
NumberSelector,
StringSelector,
Date,
Number,
String,
TimeSpan
}
}

public class SortingCriteriaHelp
{
/// <summary>
/// The internal type name of the FilterExpression
/// This is what you give the API, not actually the internal type (it is the internal type without the word Expression)
/// </summary>
[Required]
public string Type { get; init; }

/// <summary>
/// A description of what the expression is doing, comparing, etc
/// </summary>
[Required]
public string Description { get; init; }
}

/// <summary>
/// Sorting Criteria hold info on how Group Filters sort their items.
/// It is in a List to follow an OrderBy().ThenBy().ThenBy(), allowing
Expand All @@ -124,6 +239,7 @@ public class SortingCriteria
/// <summary>
/// The next expression to fall back on when the SortingExpression is equal or invalid, for example, sort by Episode Count descending then by Name
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public SortingCriteria? Next { get; set; }

/// <summary>
Expand Down
3 changes: 3 additions & 0 deletions Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Shoko.Server.Filters.Interfaces;
using Shoko.Server.Models;

namespace Shoko.Server.Filters.Files;

Expand All @@ -14,6 +15,8 @@ public HasAudioLanguageExpression() { }
public string Parameter { get; set; }
public override bool TimeDependent => false;
public override bool UserDependent => false;
public override string HelpDescription => "This passes if any of the files have the audio language provided in the parameter";
public override string[] HelpPossibleParameters => SVR_AniDB_File.GetPossibleAudioLanguages();

public override bool Evaluate(IFilterable filterable)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Shoko.Server.Filters.Interfaces;
using Shoko.Server.Models;

namespace Shoko.Server.Filters.Files;

Expand All @@ -14,6 +15,8 @@ public HasSharedAudioLanguageExpression() { }
public string Parameter { get; set; }
public override bool TimeDependent => false;
public override bool UserDependent => false;
public override string HelpDescription => "This passes if all of the files have the audio language provided in the parameter";
public override string[] HelpPossibleParameters => SVR_AniDB_File.GetPossibleAudioLanguages();

public override bool Evaluate(IFilterable filterable)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Shoko.Server.Filters.Interfaces;
using Shoko.Server.Models;

namespace Shoko.Server.Filters.Files;

Expand All @@ -14,6 +15,8 @@ public HasSharedSubtitleLanguageExpression() { }
public string Parameter { get; set; }
public override bool TimeDependent => false;
public override bool UserDependent => false;
public override string HelpDescription => "This passes if all of the files have the subtitle language provided in the parameter";
public override string[] HelpPossibleParameters => SVR_AniDB_File.GetPossibleSubtitleLanguages();

public override bool Evaluate(IFilterable filterable)
{
Expand Down
12 changes: 12 additions & 0 deletions Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Shoko.Server.Filters.Interfaces;
using Shoko.Server.Providers.AniDB;

namespace Shoko.Server.Filters.Files;

Expand All @@ -14,6 +15,17 @@ public HasSharedVideoSourceExpression() { }
public string Parameter { get; set; }
public override bool TimeDependent => false;
public override bool UserDependent => false;
public override string HelpDescription => "This passes if all of the files have the video source provided in the parameter";
public override string[] HelpPossibleParameters => new[]
{
GetFile_Source.BluRay.ToString(), GetFile_Source.DVD.ToString(),
GetFile_Source.Web.ToString(), GetFile_Source.TV.ToString(),
GetFile_Source.HDTV.ToString(), GetFile_Source.Unknown.ToString(),
GetFile_Source.Camcorder.ToString(), GetFile_Source.DTV.ToString(),
GetFile_Source.VCD.ToString(), GetFile_Source.VHS.ToString(),
GetFile_Source.SVCD.ToString(), GetFile_Source.HDDVD.ToString(),
GetFile_Source.HKDVD.ToString(), GetFile_Source.LaserDisc.ToString()
};

public override bool Evaluate(IFilterable filterable)
{
Expand Down
3 changes: 3 additions & 0 deletions Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Shoko.Server.Filters.Interfaces;
using Shoko.Server.Models;

namespace Shoko.Server.Filters.Files;

Expand All @@ -14,6 +15,8 @@ public HasSubtitleLanguageExpression() { }
public string Parameter { get; set; }
public override bool TimeDependent => false;
public override bool UserDependent => false;
public override string HelpDescription => "This passes if any of the files have the subtitle language provided in the parameter";
public override string[] HelpPossibleParameters => SVR_AniDB_File.GetPossibleSubtitleLanguages();

public override bool Evaluate(IFilterable filterable)
{
Expand Down
12 changes: 12 additions & 0 deletions Shoko.Server/Filters/Files/HasVideoSourceExpression.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Shoko.Server.Filters.Interfaces;
using Shoko.Server.Providers.AniDB;

namespace Shoko.Server.Filters.Files;

Expand All @@ -14,6 +15,17 @@ public HasVideoSourceExpression() { }
public string Parameter { get; set; }
public override bool TimeDependent => false;
public override bool UserDependent => false;
public override string HelpDescription => "This passes if any of the files have the video source provided in the parameter";
public override string[] HelpPossibleParameters => new[]
{
GetFile_Source.BluRay.ToString(), GetFile_Source.DVD.ToString(),
GetFile_Source.Web.ToString(), GetFile_Source.TV.ToString(),
GetFile_Source.HDTV.ToString(), GetFile_Source.Unknown.ToString(),
GetFile_Source.Camcorder.ToString(), GetFile_Source.DTV.ToString(),
GetFile_Source.VCD.ToString(), GetFile_Source.VHS.ToString(),
GetFile_Source.SVCD.ToString(), GetFile_Source.HDDVD.ToString(),
GetFile_Source.HKDVD.ToString(), GetFile_Source.LaserDisc.ToString()
};

public override bool Evaluate(IFilterable filterable)
{
Expand Down
8 changes: 6 additions & 2 deletions Shoko.Server/Filters/FilterExpression.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Shoko.Server.Filters.Interfaces;
Expand All @@ -6,8 +7,11 @@ namespace Shoko.Server.Filters;

public class FilterExpression : IFilterExpression
{
[IgnoreDataMember][JsonIgnore] public virtual bool TimeDependent => false;
[IgnoreDataMember][JsonIgnore] public virtual bool UserDependent => false;
[IgnoreDataMember] [JsonIgnore] public virtual bool TimeDependent => false;
[IgnoreDataMember] [JsonIgnore] public virtual bool UserDependent => false;
[IgnoreDataMember] [JsonIgnore] public virtual string HelpDescription => string.Empty;
[IgnoreDataMember] [JsonIgnore] public virtual string[] HelpPossibleParameters => Array.Empty<string>();
[IgnoreDataMember] [JsonIgnore] public virtual string[] HelpPossibleSecondParameters => Array.Empty<string>();

protected virtual bool Equals(FilterExpression other)
{
Expand Down
Loading

0 comments on commit be5af99

Please sign in to comment.