From 32ef6ff30b59d43d469addf4a7e32d5e77a02884 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Thu, 24 Aug 2023 23:11:19 -0400 Subject: [PATCH 01/34] Start design skeleton for new Expression Filters --- .../Models/Filters/FilterExpression.cs | 8 + .../Filters/FilterExpressionArgument.cs | 7 + .../Models/Filters/IFilterWithSelector.cs | 6 + Shoko.Server/Models/Filters/IFilterable.cs | 177 ++++++++++++++++++ .../Models/Filters/Logic/AndExpression.cs | 10 + .../Models/Filters/Logic/NotExpression.cs | 9 + .../Models/Filters/Logic/OrExpression.cs | 10 + .../Models/Filters/Logic/XorExpression.cs | 10 + .../Filters/User/HasCustomTagExpression.cs | 8 + Shoko.Server/Models/Filters/readme.md | 12 ++ Shoko.Server/Shoko.Server.csproj | 3 + 11 files changed, 260 insertions(+) create mode 100644 Shoko.Server/Models/Filters/FilterExpression.cs create mode 100644 Shoko.Server/Models/Filters/FilterExpressionArgument.cs create mode 100644 Shoko.Server/Models/Filters/IFilterWithSelector.cs create mode 100644 Shoko.Server/Models/Filters/IFilterable.cs create mode 100644 Shoko.Server/Models/Filters/Logic/AndExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/NotExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/OrExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/XorExpression.cs create mode 100644 Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs create mode 100644 Shoko.Server/Models/Filters/readme.md diff --git a/Shoko.Server/Models/Filters/FilterExpression.cs b/Shoko.Server/Models/Filters/FilterExpression.cs new file mode 100644 index 000000000..b79329daf --- /dev/null +++ b/Shoko.Server/Models/Filters/FilterExpression.cs @@ -0,0 +1,8 @@ +namespace Shoko.Server.Models.Filters; + +public abstract class FilterExpression +{ + public abstract bool UserDependent { get; } + + public abstract bool Evaluate(IFilterable filterable); +} diff --git a/Shoko.Server/Models/Filters/FilterExpressionArgument.cs b/Shoko.Server/Models/Filters/FilterExpressionArgument.cs new file mode 100644 index 000000000..efe4dd46d --- /dev/null +++ b/Shoko.Server/Models/Filters/FilterExpressionArgument.cs @@ -0,0 +1,7 @@ +namespace Shoko.Server.Models.Filters; + +public class FilterExpressionArgument +{ + public string Type { get; set; } + public int Index { get; set; } +} diff --git a/Shoko.Server/Models/Filters/IFilterWithSelector.cs b/Shoko.Server/Models/Filters/IFilterWithSelector.cs new file mode 100644 index 000000000..bc32c76be --- /dev/null +++ b/Shoko.Server/Models/Filters/IFilterWithSelector.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Models.Filters; + +public interface IFilterWithSelector +{ + string Selector { get; set; } +} diff --git a/Shoko.Server/Models/Filters/IFilterable.cs b/Shoko.Server/Models/Filters/IFilterable.cs new file mode 100644 index 000000000..945e6fa77 --- /dev/null +++ b/Shoko.Server/Models/Filters/IFilterable.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; + +namespace Shoko.Server.Models.Filters; + +public interface IFilterable +{ + /// + /// Probably will be removed in the future. Custom Tags would handle this better + /// + bool IsFavorite { get; } + + /// + /// Number of Missing Episodes + /// + int MissingEpisodes { get; } + + /// + /// Number of Missing Episodes from Groups that you have + /// + int MissingEpisodesCollecting { get; } + + /// + /// The number of episodes watched + /// + int WatchedEpisodes { get; } + + /// + /// The number of episodes that have not been watched + /// + int UnwatchedEpisodes { get; } + + /// + /// All of the tags + /// + IReadOnlySet Tags { get; } + + /// + /// All of the Custom Tags + /// + IReadOnlySet CustomTags { get; } + + /// + /// The years this aired in + /// + IReadOnlySet Years { get; } + + /// + /// The seasons this aired in + /// + IReadOnlySet Seasons { get; } + + /// + /// Has at least one TvDB Link + /// + bool HasTvDBLink { get; } + + /// + /// Missing at least one TvDB Link + /// + bool HasMissingTvDbLink { get; } + + /// + /// Has at least one TMDb Link + /// + bool HasTMDbLink { get; } + + /// + /// Missing at least one TMDb Link + /// + bool HasMissingTMDbLink { get; } + + /// + /// Has at least one Trakt Link + /// + bool HasTraktLink { get; } + + /// + /// Missing at least one Trakt Link + /// + bool HasMissingTraktLink { get; } + + /// + /// Has Finished airing + /// + bool IsFinished { get; } + + /// + /// Has any user votes + /// + bool HasVotes { get; } + + /// + /// Missing any user votes + /// + bool MissingVotes { get; } + + /// + /// First Air Date + /// + DateTime? AirDate { get; } + + /// + /// Latest Air Date + /// + DateTime? LastAirDate { get; } + + /// + /// First Watched Date + /// + DateTime? WatchedDate { get; } + + /// + /// Latest Watched Date + /// + DateTime? LastWatchedDate { get; } + + /// + /// When it was first added to the collection + /// + DateTime AddedDate { get; } + + /// + /// When it was most recently added to the collection + /// + DateTime LastAddedDate { get; } + + /// + /// Highest Episode Count + /// + int EpisodeCount { get; } + + /// + /// Total Episode Count + /// + int TotalEpisodeCount { get; } + + /// + /// Lowest AniDB Rating (0-10) + /// + decimal LowestAniDBRating { get; } + + /// + /// Highest AniDB Rating (0-10) + /// + decimal HighestAniDBRating { get; } + + /// + /// Lowest User Rating (0-10) + /// + decimal LowestUserRating { get; } + + /// + /// Highest User Rating (0-10) + /// + decimal HighestUserRating { get; } + + /// + /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. + /// + IReadOnlySet VideoSources { get; } + + /// + /// The anime types (movie, series, ova, etc) + /// + IReadOnlySet AnimeTypes { get; } + + /// + /// Audio Languages + /// + IReadOnlySet AudioLanguages { get; } + + /// + /// Subtitle Languages + /// + IReadOnlySet SubtitleLanguages { get; } +} diff --git a/Shoko.Server/Models/Filters/Logic/AndExpression.cs b/Shoko.Server/Models/Filters/Logic/AndExpression.cs new file mode 100644 index 000000000..8d7a19623 --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/AndExpression.cs @@ -0,0 +1,10 @@ +namespace Shoko.Server.Models.Filters.Logic; + +public class AndExpression : FilterExpression +{ + public override bool UserDependent => Left.UserDependent || Right.UserDependent; + public override bool Evaluate(IFilterable filterable) => Left.Evaluate(filterable) && Right.Evaluate(filterable); + + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } +} diff --git a/Shoko.Server/Models/Filters/Logic/NotExpression.cs b/Shoko.Server/Models/Filters/Logic/NotExpression.cs new file mode 100644 index 000000000..4b1d84bbb --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/NotExpression.cs @@ -0,0 +1,9 @@ +namespace Shoko.Server.Models.Filters.Logic; + +public class NotExpression : FilterExpression +{ + public override bool UserDependent => Left.UserDependent; + public override bool Evaluate(IFilterable filterable) => !Left.Evaluate(filterable); + + public FilterExpression Left { get; set; } +} diff --git a/Shoko.Server/Models/Filters/Logic/OrExpression.cs b/Shoko.Server/Models/Filters/Logic/OrExpression.cs new file mode 100644 index 000000000..87cf0dcec --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/OrExpression.cs @@ -0,0 +1,10 @@ +namespace Shoko.Server.Models.Filters.Logic; + +public class OrExpression : FilterExpression +{ + public override bool UserDependent => Left.UserDependent || Right.UserDependent; + public override bool Evaluate(IFilterable filterable) => Left.Evaluate(filterable) || Right.Evaluate(filterable); + + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } +} diff --git a/Shoko.Server/Models/Filters/Logic/XorExpression.cs b/Shoko.Server/Models/Filters/Logic/XorExpression.cs new file mode 100644 index 000000000..3a826eb40 --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/XorExpression.cs @@ -0,0 +1,10 @@ +namespace Shoko.Server.Models.Filters.Logic; + +public class XorExpression : FilterExpression +{ + public override bool UserDependent => Left.UserDependent || Right.UserDependent; + public override bool Evaluate(IFilterable filterable) => Left.Evaluate(filterable) ^ Right.Evaluate(filterable); + + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } +} diff --git a/Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs b/Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs new file mode 100644 index 000000000..f49be2b23 --- /dev/null +++ b/Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs @@ -0,0 +1,8 @@ +namespace Shoko.Server.Models.Filters.User; + +public class HasCustomTagExpression : FilterExpression +{ + public string Parameter { get; set; } + public override bool UserDependent => true; + public override bool Evaluate(IFilterable filterable) => filterable.CustomTags.Contains(Parameter); +} diff --git a/Shoko.Server/Models/Filters/readme.md b/Shoko.Server/Models/Filters/readme.md new file mode 100644 index 000000000..e77c4c581 --- /dev/null +++ b/Shoko.Server/Models/Filters/readme.md @@ -0,0 +1,12 @@ +Expressions are stored with TPH discriminated on Type. Arguments should be mapped to Type + "Argument" + Index (StringArgument1) in the mapping. Expressions should not have more than 5 Arguments of each type. If it would, then it should be redesigned. For example, `And(HasTag("comedy"),HasTag("action"))`. This is to keep the database schema reasonable and expressions simple. I considered a one-to-many map for arguments, and making that automatic for navigation properties seemed like a lot of work when we can design around it. + +Acceptable database types (can be mapped to other CLR types like enums) for Arguments are: +- Integer +- String +- Decimal +- DateTime +- FilterExpression via foreign key + +FilterExpression is a single Expression, whether that's something like "Or" or "HasTag" in `And(Or(HasTag('comedy'), HasTag('action')), Not(HasTag('18 restricted')))`. An Expression should be a test of sorts: a method that takes zero or more arguments and returns a true or false result. + +This is all a design for the back end. Expressions can be communicated in many ways. For example, the above could be written `tag: 'comedy' or tag: 'action' and not tag: '18 restricted'` or `HasAnyTags('comedy', 'action') && HasNoTags('18 restriced')`. It just depends on how you map the input. diff --git a/Shoko.Server/Shoko.Server.csproj b/Shoko.Server/Shoko.Server.csproj index c7a1ff1e2..3668b8d3e 100644 --- a/Shoko.Server/Shoko.Server.csproj +++ b/Shoko.Server/Shoko.Server.csproj @@ -114,4 +114,7 @@ + + + From 627b07c9b281b0ae59f508cfe75c3de5014307ce Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Fri, 25 Aug 2023 01:02:02 -0400 Subject: [PATCH 02/34] Filters: Selectors and Some Comparator Logic --- .../TypeConverters/FilterSelectorConverter.cs | 233 ++++++++++++++++++ .../Files/HasAudioLanguageExpression.cs | 10 + .../Files/HasSubtitleLanguageExpression.cs | 10 + .../Models/Filters/FilterExpression.cs | 2 + .../Filters/FilterExpressionArgument.cs | 7 - .../Models/Filters/IFilterWithSelector.cs | 6 - .../Filters/Interfaces/IDateTimeSelector.cs | 8 + .../Filters/Interfaces/IFilterSelector.cs | 6 + .../Filters/{ => Interfaces}/IFilterable.cs | 2 +- .../Filters/Interfaces/INumberSelector.cs | 8 + .../Filters/Interfaces/IStringSelector.cs | 8 + .../Models/Filters/Logic/AndExpression.cs | 2 + .../Logic/DateTimes/EqualExpression.cs | 12 + .../Models/Filters/Logic/NotExpression.cs | 2 + .../Filters/Logic/Numbers/EqualExpression.cs | 12 + .../Numbers/GreaterThanEqualExpression.cs | 11 + .../Logic/Numbers/GreaterThanExpression.cs | 11 + .../Logic/Numbers/LessThanEqualExpression.cs | 11 + .../Logic/Numbers/LessThanExpression.cs | 11 + .../Logic/Numbers/NotEqualExpression.cs | 12 + .../Models/Filters/Logic/OrExpression.cs | 2 + .../Logic/Strings/ContainsExpression.cs | 13 + .../Filters/Logic/Strings/EqualExpression.cs | 14 ++ .../Logic/Strings/NotEqualExpression.cs | 14 ++ .../Models/Filters/Logic/XorExpression.cs | 2 + .../Selectors/AudioLanguageCountSelector.cs | 10 + .../Filters/User/HasCustomTagExpression.cs | 2 + Shoko.Server/Models/Filters/readme.md | 9 +- Shoko.Server/Shoko.Server.csproj | 3 - 29 files changed, 433 insertions(+), 20 deletions(-) create mode 100644 Shoko.Server/Databases/TypeConverters/FilterSelectorConverter.cs create mode 100644 Shoko.Server/Models/Filters/Files/HasAudioLanguageExpression.cs create mode 100644 Shoko.Server/Models/Filters/Files/HasSubtitleLanguageExpression.cs delete mode 100644 Shoko.Server/Models/Filters/FilterExpressionArgument.cs delete mode 100644 Shoko.Server/Models/Filters/IFilterWithSelector.cs create mode 100644 Shoko.Server/Models/Filters/Interfaces/IDateTimeSelector.cs create mode 100644 Shoko.Server/Models/Filters/Interfaces/IFilterSelector.cs rename Shoko.Server/Models/Filters/{ => Interfaces}/IFilterable.cs (98%) create mode 100644 Shoko.Server/Models/Filters/Interfaces/INumberSelector.cs create mode 100644 Shoko.Server/Models/Filters/Interfaces/IStringSelector.cs create mode 100644 Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs create mode 100644 Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs diff --git a/Shoko.Server/Databases/TypeConverters/FilterSelectorConverter.cs b/Shoko.Server/Databases/TypeConverters/FilterSelectorConverter.cs new file mode 100644 index 000000000..f8e9a911c --- /dev/null +++ b/Shoko.Server/Databases/TypeConverters/FilterSelectorConverter.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using NHibernate.SqlTypes; +using NHibernate.UserTypes; +using System.Data; +using System.Data.Common; +using NHibernate; +using NHibernate.Engine; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Databases.TypeConverters; + +public class FilterSelectorConverter : TypeConverter, IUserType +{ + private static Dictionary s_selectors = new(); + + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return typeof(IFilterSelector).IsAssignableFrom(sourceType); + } + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + switch (destinationType.FullName) + { + case "System.String": + return true; + default: + return false; + } + } + + public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, + object value) + { + switch (value) + { + case string s: + if (s_selectors.TryGetValue(s, out var selector)) return selector; + + var type = Type.GetType(s) ?? throw new ArgumentException($"Value was not found: {s}"); + selector = (IFilterSelector)Activator.CreateInstance(type); + s_selectors.Add(s, selector); + return selector; + default: + throw new ArgumentException("DestinationType must be string"); + } + } + + /// + /// Converts the given value object to the specified type + /// + /// Ignored + /// Ignored + /// The to convert. + /// The to convert the parameter to. + /// + /// An that represents the converted value. The value will be 1 if is true, otherwise 0 + /// + /// The parameter is . + /// The conversion could not be performed. + public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, + object value, Type destinationType) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value), @"Value can't be null"); + } + + if (value is not IFilterSelector) + { + throw new ArgumentException(@"Value isn't of type IFilterSelector", nameof(value)); + } + + return destinationType.FullName switch + { + "System.String" => value.GetType().FullName, + _ => throw new ArgumentException("DestinationType must be string or int") + }; + } + + + /// + /// Creates an instance of the Type that this is associated with (bool) + /// + /// ignored. + /// ignored. + /// + /// An of type bool. It always returns 'true' for this converter. + /// + public override object CreateInstance(ITypeDescriptorContext context, System.Collections.IDictionary propertyValues) + { + return true; + } + + #region IUserType Members + + /// + /// Reconstruct an object from the cacheable representation. At the very least this + /// method should perform a deep copy if the type is mutable. (optional operation) + /// + /// the object to be cached + /// the owner of the cached object + /// + /// a reconstructed object from the cacheable representation + /// + public object Assemble(object cached, object owner) + { + return DeepCopy(cached); + } + + /// + /// Return a deep copy of the persistent state, stopping at entities and at collections. + /// + /// generally a collection element or entity field + /// a copy + public object DeepCopy(object value) + { + return value; + } + + /// + /// Transform the object into its cacheable representation. At the very least this + /// method should perform a deep copy if the type is mutable. That may not be enough + /// for some implementations, however; for example, associations must be cached as + /// identifier values. (optional operation) + /// + /// the object to be cached + /// a cacheable representation of the object + public object Disassemble(object value) + { + return DeepCopy(value); + } + + /// + /// Returns a hash code for this instance. + /// + /// The x. + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public int GetHashCode(object x) + { + return x == null ? base.GetHashCode() : x.GetHashCode(); + } + + /// + /// Are objects of this type mutable? + /// + /// + public bool IsMutable => true; + + /// + /// Retrieve an instance of the mapped class from a JDBC resultset. + /// Implementors should handle possibility of null values. + /// + /// a IDataReader + /// column names + /// + /// the containing entity + /// + /// HibernateException + public object NullSafeGet(DbDataReader rs, string[] names, ISessionImplementor impl, object owner) + { + var rawValue = NHibernateUtil.String.NullSafeGet(rs, names[0], impl); + return rawValue == null ? null : ConvertFrom(null!, null!, rawValue); + } + + /// + /// Write an instance of the mapped class to a prepared statement. + /// Implementors should handle possibility of null values. + /// A multi-column type should be written to parameters starting from index. + /// + /// a IDbCommand + /// the object to write + /// command parameter index + /// + /// HibernateException + public void NullSafeSet(DbCommand cmd, object value, int index, ISessionImplementor session) + { + ((IDataParameter)cmd.Parameters[index]).Value = + value == null ? DBNull.Value : ConvertTo(null, null, value, typeof(string)); + } + + /// + /// During merge, replace the existing () value in the entity + /// we are merging to with a new () value from the detached + /// entity we are merging. For immutable objects, or null values, it is safe to simply + /// return the first parameter. For mutable objects, it is safe to return a copy of the + /// first parameter. For objects with component values, it might make sense to + /// recursively replace component values. + /// + /// the value from the detached entity being merged + /// the value in the managed entity + /// the managed entity + /// the value to be merged + public object Replace(object original, object target, object owner) + { + return original; + } + + /// + /// The type returned by NullSafeGet() + /// + public Type ReturnedType => typeof(string); + + /// + /// The SQL types for the columns mapped by this type. + /// + /// + public SqlType[] SqlTypes => new[] { NHibernateUtil.String.SqlType }; + + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with this instance. + /// The y. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + bool IUserType.Equals(object x, object y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + return x != null && y != null && x.Equals(y); + } + + #endregion +} diff --git a/Shoko.Server/Models/Filters/Files/HasAudioLanguageExpression.cs b/Shoko.Server/Models/Filters/Files/HasAudioLanguageExpression.cs new file mode 100644 index 000000000..81134d773 --- /dev/null +++ b/Shoko.Server/Models/Filters/Files/HasAudioLanguageExpression.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Files; + +public class HasAudioLanguageExpression : FilterExpression +{ + public string Parameter { get; set; } + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.AudioLanguages.Contains(Parameter); +} diff --git a/Shoko.Server/Models/Filters/Files/HasSubtitleLanguageExpression.cs b/Shoko.Server/Models/Filters/Files/HasSubtitleLanguageExpression.cs new file mode 100644 index 000000000..50912f824 --- /dev/null +++ b/Shoko.Server/Models/Filters/Files/HasSubtitleLanguageExpression.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Files; + +public class HasSubtitleLanguageExpression : FilterExpression +{ + public string Parameter { get; set; } + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.SubtitleLanguages.Contains(Parameter); +} diff --git a/Shoko.Server/Models/Filters/FilterExpression.cs b/Shoko.Server/Models/Filters/FilterExpression.cs index b79329daf..c077dddc6 100644 --- a/Shoko.Server/Models/Filters/FilterExpression.cs +++ b/Shoko.Server/Models/Filters/FilterExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Models.Filters.Interfaces; + namespace Shoko.Server.Models.Filters; public abstract class FilterExpression diff --git a/Shoko.Server/Models/Filters/FilterExpressionArgument.cs b/Shoko.Server/Models/Filters/FilterExpressionArgument.cs deleted file mode 100644 index efe4dd46d..000000000 --- a/Shoko.Server/Models/Filters/FilterExpressionArgument.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Shoko.Server.Models.Filters; - -public class FilterExpressionArgument -{ - public string Type { get; set; } - public int Index { get; set; } -} diff --git a/Shoko.Server/Models/Filters/IFilterWithSelector.cs b/Shoko.Server/Models/Filters/IFilterWithSelector.cs deleted file mode 100644 index bc32c76be..000000000 --- a/Shoko.Server/Models/Filters/IFilterWithSelector.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Shoko.Server.Models.Filters; - -public interface IFilterWithSelector -{ - string Selector { get; set; } -} diff --git a/Shoko.Server/Models/Filters/Interfaces/IDateTimeSelector.cs b/Shoko.Server/Models/Filters/Interfaces/IDateTimeSelector.cs new file mode 100644 index 000000000..bc5e4d4a1 --- /dev/null +++ b/Shoko.Server/Models/Filters/Interfaces/IDateTimeSelector.cs @@ -0,0 +1,8 @@ +using System; + +namespace Shoko.Server.Models.Filters.Interfaces; + +public interface IDateTimeSelector : IFilterSelector +{ + Func Selector { get; } +} diff --git a/Shoko.Server/Models/Filters/Interfaces/IFilterSelector.cs b/Shoko.Server/Models/Filters/Interfaces/IFilterSelector.cs new file mode 100644 index 000000000..596c56d9a --- /dev/null +++ b/Shoko.Server/Models/Filters/Interfaces/IFilterSelector.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Models.Filters.Interfaces; + +public interface IFilterSelector +{ + bool UserDependent { get; } +} diff --git a/Shoko.Server/Models/Filters/IFilterable.cs b/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs similarity index 98% rename from Shoko.Server/Models/Filters/IFilterable.cs rename to Shoko.Server/Models/Filters/Interfaces/IFilterable.cs index 945e6fa77..6c303d28d 100644 --- a/Shoko.Server/Models/Filters/IFilterable.cs +++ b/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Shoko.Server.Models.Filters; +namespace Shoko.Server.Models.Filters.Interfaces; public interface IFilterable { diff --git a/Shoko.Server/Models/Filters/Interfaces/INumberSelector.cs b/Shoko.Server/Models/Filters/Interfaces/INumberSelector.cs new file mode 100644 index 000000000..2b88a5ef0 --- /dev/null +++ b/Shoko.Server/Models/Filters/Interfaces/INumberSelector.cs @@ -0,0 +1,8 @@ +using System; + +namespace Shoko.Server.Models.Filters.Interfaces; + +public interface INumberSelector : IFilterSelector +{ + Func Selector { get; } +} diff --git a/Shoko.Server/Models/Filters/Interfaces/IStringSelector.cs b/Shoko.Server/Models/Filters/Interfaces/IStringSelector.cs new file mode 100644 index 000000000..7736a3b60 --- /dev/null +++ b/Shoko.Server/Models/Filters/Interfaces/IStringSelector.cs @@ -0,0 +1,8 @@ +using System; + +namespace Shoko.Server.Models.Filters.Interfaces; + +public interface IStringSelector : IFilterSelector +{ + Func Selector { get; } +} diff --git a/Shoko.Server/Models/Filters/Logic/AndExpression.cs b/Shoko.Server/Models/Filters/Logic/AndExpression.cs index 8d7a19623..f2cd38b06 100644 --- a/Shoko.Server/Models/Filters/Logic/AndExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/AndExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Models.Filters.Interfaces; + namespace Shoko.Server.Models.Filters.Logic; public class AndExpression : FilterExpression diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs new file mode 100644 index 000000000..d223bae08 --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs @@ -0,0 +1,12 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.DateTimes; + +public class EqualExpression : FilterExpression +{ + public IDateTimeSelector Selector { get; set; } + public DateTime Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + public override bool Evaluate(IFilterable filterable) => (Selector.Selector(filterable) - Parameter)?.TotalDays < 1; +} diff --git a/Shoko.Server/Models/Filters/Logic/NotExpression.cs b/Shoko.Server/Models/Filters/Logic/NotExpression.cs index 4b1d84bbb..36e4db647 100644 --- a/Shoko.Server/Models/Filters/Logic/NotExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/NotExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Models.Filters.Interfaces; + namespace Shoko.Server.Models.Filters.Logic; public class NotExpression : FilterExpression diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs new file mode 100644 index 000000000..b8a90e91e --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs @@ -0,0 +1,12 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.Numbers; + +public class EqualExpression : FilterExpression +{ + public INumberSelector Selector { get; set; } + public double Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + public override bool Evaluate(IFilterable filterable) => Math.Abs(Selector.Selector(filterable) - Parameter) < 0.001D; +} diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs new file mode 100644 index 000000000..65b325de3 --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs @@ -0,0 +1,11 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.Numbers; + +public class GreaterThanEqualExpression : FilterExpression +{ + public INumberSelector Selector { get; set; } + public double Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + public override bool Evaluate(IFilterable filterable) => Selector.Selector(filterable) >= Parameter; +} diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs new file mode 100644 index 000000000..27fe8cbc9 --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs @@ -0,0 +1,11 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.Numbers; + +public class GreaterThanExpression : FilterExpression +{ + public INumberSelector Selector { get; set; } + public double Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + public override bool Evaluate(IFilterable filterable) => Selector.Selector(filterable) > Parameter; +} diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs new file mode 100644 index 000000000..48a865a4e --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs @@ -0,0 +1,11 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.Numbers; + +public class LessThanEqualExpression : FilterExpression +{ + public INumberSelector Selector { get; set; } + public double Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + public override bool Evaluate(IFilterable filterable) => Selector.Selector(filterable) <= Parameter; +} diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs new file mode 100644 index 000000000..09fddb575 --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs @@ -0,0 +1,11 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.Numbers; + +public class LessThanExpression : FilterExpression +{ + public INumberSelector Selector { get; set; } + public double Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + public override bool Evaluate(IFilterable filterable) => Selector.Selector(filterable) < Parameter; +} diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs new file mode 100644 index 000000000..658b0b019 --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs @@ -0,0 +1,12 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.Numbers; + +public class NotEqualExpression : FilterExpression +{ + public INumberSelector Selector { get; set; } + public double Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + public override bool Evaluate(IFilterable filterable) => Math.Abs(Selector.Selector(filterable) - Parameter) >= 0.001D; +} diff --git a/Shoko.Server/Models/Filters/Logic/OrExpression.cs b/Shoko.Server/Models/Filters/Logic/OrExpression.cs index 87cf0dcec..6f9fd2e13 100644 --- a/Shoko.Server/Models/Filters/Logic/OrExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/OrExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Models.Filters.Interfaces; + namespace Shoko.Server.Models.Filters.Logic; public class OrExpression : FilterExpression diff --git a/Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs b/Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs new file mode 100644 index 000000000..85225443b --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs @@ -0,0 +1,13 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.Strings; + +public class ContainsExpression : FilterExpression +{ + public IStringSelector Selector { get; set; } + public string Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + + public override bool Evaluate(IFilterable filterable) => Parameter.Contains(Selector.Selector(filterable), StringComparison.InvariantCultureIgnoreCase); +} diff --git a/Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs new file mode 100644 index 000000000..b7a7c9386 --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs @@ -0,0 +1,14 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.Strings; + +public class EqualExpression : FilterExpression +{ + public IStringSelector Selector { get; set; } + public string Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + + public override bool Evaluate(IFilterable filterable) => + string.Equals(Selector.Selector(filterable), Parameter, StringComparison.InvariantCultureIgnoreCase); +} diff --git a/Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs new file mode 100644 index 000000000..f0796dc9e --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs @@ -0,0 +1,14 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.Strings; + +public class NotEqualExpression : FilterExpression +{ + public IStringSelector Selector { get; set; } + public string Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + + public override bool Evaluate(IFilterable filterable) => + !string.Equals(Selector.Selector(filterable), Parameter, StringComparison.InvariantCultureIgnoreCase); +} diff --git a/Shoko.Server/Models/Filters/Logic/XorExpression.cs b/Shoko.Server/Models/Filters/Logic/XorExpression.cs index 3a826eb40..cbe869f29 100644 --- a/Shoko.Server/Models/Filters/Logic/XorExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/XorExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Models.Filters.Interfaces; + namespace Shoko.Server.Models.Filters.Logic; public class XorExpression : FilterExpression diff --git a/Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs b/Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs new file mode 100644 index 000000000..fc21b53fa --- /dev/null +++ b/Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs @@ -0,0 +1,10 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Selectors; + +public class AudioLanguageCountSelector : INumberSelector +{ + public bool UserDependent => false; + public Func Selector => f => f.AudioLanguages.Count; +} diff --git a/Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs b/Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs index f49be2b23..9d3cb7fc8 100644 --- a/Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs +++ b/Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Models.Filters.Interfaces; + namespace Shoko.Server.Models.Filters.User; public class HasCustomTagExpression : FilterExpression diff --git a/Shoko.Server/Models/Filters/readme.md b/Shoko.Server/Models/Filters/readme.md index e77c4c581..ff7341272 100644 --- a/Shoko.Server/Models/Filters/readme.md +++ b/Shoko.Server/Models/Filters/readme.md @@ -1,12 +1,15 @@ -Expressions are stored with TPH discriminated on Type. Arguments should be mapped to Type + "Argument" + Index (StringArgument1) in the mapping. Expressions should not have more than 5 Arguments of each type. If it would, then it should be redesigned. For example, `And(HasTag("comedy"),HasTag("action"))`. This is to keep the database schema reasonable and expressions simple. I considered a one-to-many map for arguments, and making that automatic for navigation properties seemed like a lot of work when we can design around it. +An Expression should be a test of sorts: a method that takes zero or more arguments and returns a true or false result. Expressions are stored with TPH discriminated on Type. Arguments should be mapped to Type + "Argument" + Index (StringArgument1) in the mapping. Expressions should not have more than 5 Arguments of each type. If it would, then it should be redesigned. For example, `And(HasTag("comedy"),HasTag("action"))`. This is to keep the database schema reasonable and expressions simple. I considered a one-to-many map for arguments, and making that automatic for navigation properties seemed like a lot of work when we can design around it. Acceptable database types (can be mapped to other CLR types like enums) for Arguments are: - Integer - String - Decimal - DateTime + +Default CLR Argument mappings: - FilterExpression via foreign key +- Selector via a type converter mapped via string (these are for things like comparators) -FilterExpression is a single Expression, whether that's something like "Or" or "HasTag" in `And(Or(HasTag('comedy'), HasTag('action')), Not(HasTag('18 restricted')))`. An Expression should be a test of sorts: a method that takes zero or more arguments and returns a true or false result. +FilterExpression is a single Expression, whether that's something like "Or" or "HasTag" in `And(Or(HasTag('comedy'), HasTag('action')), Not(HasTag('18 restricted')))`. Expressions should be the least amount of work possible. NAND can be expressed as `Not(And())`. Exceptions can be made if it would take more than 2 or 3 operations, such as XOR. An Exception was made for GreaterThanEqual, NotEqual, and LessThanEqual, only because most people would expect those to exist. -This is all a design for the back end. Expressions can be communicated in many ways. For example, the above could be written `tag: 'comedy' or tag: 'action' and not tag: '18 restricted'` or `HasAnyTags('comedy', 'action') && HasNoTags('18 restriced')`. It just depends on how you map the input. +This is all a design for the back end. Expressions can be communicated in many ways. For example, the above could be written `tag: 'comedy' or tag: 'action' and not tag: '18 restricted'` or `HasAnyTags('comedy', 'action') && HasNoTags('18 restriced')`. It just depends on how you map the input. \ No newline at end of file diff --git a/Shoko.Server/Shoko.Server.csproj b/Shoko.Server/Shoko.Server.csproj index 3668b8d3e..c7a1ff1e2 100644 --- a/Shoko.Server/Shoko.Server.csproj +++ b/Shoko.Server/Shoko.Server.csproj @@ -114,7 +114,4 @@ - - - From 8b9081e28c587b6dc1d1ca4d65cd828249d51b09 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Fri, 25 Aug 2023 16:10:14 -0400 Subject: [PATCH 03/34] Filters: Add a bunch more Selectors and Expressions --- .../HasMissingEpisodesCollectingExpression.cs | 9 +++++++++ .../Info/HasMissingEpisodesExpression.cs | 9 +++++++++ .../Filters/Info/HasTMDbLinkExpression.cs | 9 +++++++++ .../Models/Filters/Info/HasTagExpression.cs | 10 ++++++++++ .../Filters/Info/HasTraktLinkExpression.cs | 9 +++++++++ .../Filters/Info/HasTvDBLinkExpression.cs | 9 +++++++++ .../Models/Filters/Info/InSeasonExpression.cs | 13 +++++++++++++ .../Models/Filters/Info/InYearExpression.cs | 10 ++++++++++ .../Models/Filters/Info/IsFinishedExpression.cs | 9 +++++++++ .../Filters/Info/MissingTMDbLinkExpression.cs | 12 ++++++++++++ .../Filters/Info/MissingTraktLinkExpression.cs | 12 ++++++++++++ .../Filters/Info/MissingTvDBLinkExpression.cs | 12 ++++++++++++ .../Models/Filters/Interfaces/IFilterable.cs | 4 ++-- .../DateTimes/GreaterThanEqualExpression.cs | 17 +++++++++++++++++ .../Logic/DateTimes/GreaterThanExpression.cs | 17 +++++++++++++++++ .../Logic/DateTimes/LessThanEqualExpression.cs | 17 +++++++++++++++++ .../Logic/DateTimes/LessThanExpression.cs | 17 +++++++++++++++++ .../Logic/DateTimes/NotEqualExpression.cs | 12 ++++++++++++ .../Filters/Selectors/AddedDateSelector.cs | 10 ++++++++++ .../Models/Filters/Selectors/AirDateSelector.cs | 10 ++++++++++ .../Filters/Selectors/EpisodeCountSelector.cs | 10 ++++++++++ .../Filters/Selectors/LastAddedDateSelector.cs | 10 ++++++++++ .../Filters/Selectors/LastAirDateSelector.cs | 10 ++++++++++ .../Selectors/LastWatchedDateSelector.cs | 10 ++++++++++ .../Selectors/SubtitleLanguageCountSelector.cs | 10 ++++++++++ .../Selectors/TotalEpisodeCountSelector.cs | 10 ++++++++++ .../Filters/Selectors/WatchedDateSelector.cs | 10 ++++++++++ .../User/HasPermanentUserVotesExpression.cs | 9 +++++++++ .../User/HasUnwatchedEpisodesExpression.cs | 9 +++++++++ .../Filters/User/HasUserVotesExpression.cs | 9 +++++++++ .../User/HasWatchedEpisodesExpression.cs | 9 +++++++++ .../Models/Filters/User/IsFavoriteExpression.cs | 9 +++++++++ 32 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 Shoko.Server/Models/Filters/Info/HasMissingEpisodesCollectingExpression.cs create mode 100644 Shoko.Server/Models/Filters/Info/HasMissingEpisodesExpression.cs create mode 100644 Shoko.Server/Models/Filters/Info/HasTMDbLinkExpression.cs create mode 100644 Shoko.Server/Models/Filters/Info/HasTagExpression.cs create mode 100644 Shoko.Server/Models/Filters/Info/HasTraktLinkExpression.cs create mode 100644 Shoko.Server/Models/Filters/Info/HasTvDBLinkExpression.cs create mode 100644 Shoko.Server/Models/Filters/Info/InSeasonExpression.cs create mode 100644 Shoko.Server/Models/Filters/Info/InYearExpression.cs create mode 100644 Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs create mode 100644 Shoko.Server/Models/Filters/Info/MissingTMDbLinkExpression.cs create mode 100644 Shoko.Server/Models/Filters/Info/MissingTraktLinkExpression.cs create mode 100644 Shoko.Server/Models/Filters/Info/MissingTvDBLinkExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs create mode 100644 Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs create mode 100644 Shoko.Server/Models/Filters/Selectors/AddedDateSelector.cs create mode 100644 Shoko.Server/Models/Filters/Selectors/AirDateSelector.cs create mode 100644 Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs create mode 100644 Shoko.Server/Models/Filters/Selectors/LastAddedDateSelector.cs create mode 100644 Shoko.Server/Models/Filters/Selectors/LastAirDateSelector.cs create mode 100644 Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs create mode 100644 Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs create mode 100644 Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs create mode 100644 Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs create mode 100644 Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs create mode 100644 Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs create mode 100644 Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs create mode 100644 Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs create mode 100644 Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs diff --git a/Shoko.Server/Models/Filters/Info/HasMissingEpisodesCollectingExpression.cs b/Shoko.Server/Models/Filters/Info/HasMissingEpisodesCollectingExpression.cs new file mode 100644 index 000000000..06d7958e0 --- /dev/null +++ b/Shoko.Server/Models/Filters/Info/HasMissingEpisodesCollectingExpression.cs @@ -0,0 +1,9 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Info; + +public class HasMissingEpisodesCollectingExpression : FilterExpression +{ + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.MissingEpisodesCollecting > 0; +} diff --git a/Shoko.Server/Models/Filters/Info/HasMissingEpisodesExpression.cs b/Shoko.Server/Models/Filters/Info/HasMissingEpisodesExpression.cs new file mode 100644 index 000000000..22fd8a996 --- /dev/null +++ b/Shoko.Server/Models/Filters/Info/HasMissingEpisodesExpression.cs @@ -0,0 +1,9 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Info; + +public class HasMissingEpisodesExpression : FilterExpression +{ + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.MissingEpisodes > 0; +} diff --git a/Shoko.Server/Models/Filters/Info/HasTMDbLinkExpression.cs b/Shoko.Server/Models/Filters/Info/HasTMDbLinkExpression.cs new file mode 100644 index 000000000..13bebfcc6 --- /dev/null +++ b/Shoko.Server/Models/Filters/Info/HasTMDbLinkExpression.cs @@ -0,0 +1,9 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Info; + +public class HasTMDbLinkExpression : FilterExpression +{ + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.HasTMDbLink; +} diff --git a/Shoko.Server/Models/Filters/Info/HasTagExpression.cs b/Shoko.Server/Models/Filters/Info/HasTagExpression.cs new file mode 100644 index 000000000..368552a5a --- /dev/null +++ b/Shoko.Server/Models/Filters/Info/HasTagExpression.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Info; + +public class HasTagExpression : FilterExpression +{ + public string Parameter { get; set; } + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.Tags.Contains(Parameter); +} diff --git a/Shoko.Server/Models/Filters/Info/HasTraktLinkExpression.cs b/Shoko.Server/Models/Filters/Info/HasTraktLinkExpression.cs new file mode 100644 index 000000000..1b5019990 --- /dev/null +++ b/Shoko.Server/Models/Filters/Info/HasTraktLinkExpression.cs @@ -0,0 +1,9 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Info; + +public class HasTraktLinkExpression : FilterExpression +{ + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.HasTraktLink; +} diff --git a/Shoko.Server/Models/Filters/Info/HasTvDBLinkExpression.cs b/Shoko.Server/Models/Filters/Info/HasTvDBLinkExpression.cs new file mode 100644 index 000000000..dbdf1bb15 --- /dev/null +++ b/Shoko.Server/Models/Filters/Info/HasTvDBLinkExpression.cs @@ -0,0 +1,9 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Info; + +public class HasTvDBLinkExpression : FilterExpression +{ + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.HasTvDBLink; +} diff --git a/Shoko.Server/Models/Filters/Info/InSeasonExpression.cs b/Shoko.Server/Models/Filters/Info/InSeasonExpression.cs new file mode 100644 index 000000000..d53987657 --- /dev/null +++ b/Shoko.Server/Models/Filters/Info/InSeasonExpression.cs @@ -0,0 +1,13 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Info; + +/// +/// Parameter is a season followed by year, ie Winter 2022 +/// +public class InSeasonExpression : FilterExpression +{ + public string Parameter { get; set; } + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.Seasons.Contains(Parameter); +} diff --git a/Shoko.Server/Models/Filters/Info/InYearExpression.cs b/Shoko.Server/Models/Filters/Info/InYearExpression.cs new file mode 100644 index 000000000..37f2acd45 --- /dev/null +++ b/Shoko.Server/Models/Filters/Info/InYearExpression.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Info; + +public class InYearExpression : FilterExpression +{ + public int Parameter { get; set; } + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.Years.Contains(Parameter); +} diff --git a/Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs b/Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs new file mode 100644 index 000000000..6c772f93d --- /dev/null +++ b/Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs @@ -0,0 +1,9 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Info; + +public class IsFinishedExpression : FilterExpression +{ + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.IsFinished; +} diff --git a/Shoko.Server/Models/Filters/Info/MissingTMDbLinkExpression.cs b/Shoko.Server/Models/Filters/Info/MissingTMDbLinkExpression.cs new file mode 100644 index 000000000..edfc41f4e --- /dev/null +++ b/Shoko.Server/Models/Filters/Info/MissingTMDbLinkExpression.cs @@ -0,0 +1,12 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Info; + +/// +/// Missing Links include logic for whether a link should exist +/// +public class MissingTMDbLinkExpression : FilterExpression +{ + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.HasMissingTMDbLink; +} diff --git a/Shoko.Server/Models/Filters/Info/MissingTraktLinkExpression.cs b/Shoko.Server/Models/Filters/Info/MissingTraktLinkExpression.cs new file mode 100644 index 000000000..c7b8a70f8 --- /dev/null +++ b/Shoko.Server/Models/Filters/Info/MissingTraktLinkExpression.cs @@ -0,0 +1,12 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Info; + +/// +/// Missing Links include logic for whether a link should exist +/// +public class MissingTraktLinkExpression : FilterExpression +{ + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.HasMissingTraktLink; +} diff --git a/Shoko.Server/Models/Filters/Info/MissingTvDBLinkExpression.cs b/Shoko.Server/Models/Filters/Info/MissingTvDBLinkExpression.cs new file mode 100644 index 000000000..40e0d0992 --- /dev/null +++ b/Shoko.Server/Models/Filters/Info/MissingTvDBLinkExpression.cs @@ -0,0 +1,12 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Info; + +/// +/// Missing Links include logic for whether a link should exist +/// +public class MissingTvDBLinkExpression : FilterExpression +{ + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.HasMissingTvDbLink; +} diff --git a/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs b/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs index 6c303d28d..6e90d4781 100644 --- a/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs +++ b/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs @@ -91,9 +91,9 @@ public interface IFilterable bool HasVotes { get; } /// - /// Missing any user votes + /// Has permanent (after finishing) user votes /// - bool MissingVotes { get; } + bool HasPermanentVotes { get; } /// /// First Air Date diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs new file mode 100644 index 000000000..f12d559d9 --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs @@ -0,0 +1,17 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.DateTimes; + +public class GreaterThanEqualExpression : FilterExpression +{ + public IDateTimeSelector Selector { get; set; } + public DateTime Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + public override bool Evaluate(IFilterable filterable) + { + var date = Selector.Selector(filterable); + if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) return false; + return date.Value.Date >= Parameter; + } +} diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs new file mode 100644 index 000000000..bcd4f33ff --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs @@ -0,0 +1,17 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.DateTimes; + +public class GreaterThanExpression : FilterExpression +{ + public IDateTimeSelector Selector { get; set; } + public DateTime Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + public override bool Evaluate(IFilterable filterable) + { + var date = Selector.Selector(filterable); + if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) return false; + return date.Value.Date > Parameter; + } +} diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs new file mode 100644 index 000000000..09bfac619 --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs @@ -0,0 +1,17 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.DateTimes; + +public class LessThanEqualExpression : FilterExpression +{ + public IDateTimeSelector Selector { get; set; } + public DateTime Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + public override bool Evaluate(IFilterable filterable) + { + var date = Selector.Selector(filterable); + if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) return false; + return date.Value.Date <= Parameter; + } +} diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs new file mode 100644 index 000000000..689385885 --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs @@ -0,0 +1,17 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.DateTimes; + +public class LessThanExpression : FilterExpression +{ + public IDateTimeSelector Selector { get; set; } + public DateTime Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + public override bool Evaluate(IFilterable filterable) + { + var date = Selector.Selector(filterable); + if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) return false; + return date.Value.Date < Parameter; + } +} diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs new file mode 100644 index 000000000..b4bfed512 --- /dev/null +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs @@ -0,0 +1,12 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Logic.DateTimes; + +public class NotEqualExpression : FilterExpression +{ + public IDateTimeSelector Selector { get; set; } + public DateTime Parameter { get; set; } + public override bool UserDependent => Selector.UserDependent; + public override bool Evaluate(IFilterable filterable) => (Selector.Selector(filterable) - Parameter)?.TotalDays >= 1; +} diff --git a/Shoko.Server/Models/Filters/Selectors/AddedDateSelector.cs b/Shoko.Server/Models/Filters/Selectors/AddedDateSelector.cs new file mode 100644 index 000000000..004c31ffb --- /dev/null +++ b/Shoko.Server/Models/Filters/Selectors/AddedDateSelector.cs @@ -0,0 +1,10 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Selectors; + +public class AddedDateSelector : IDateTimeSelector +{ + public bool UserDependent => false; + public Func Selector => f => f.AddedDate; +} diff --git a/Shoko.Server/Models/Filters/Selectors/AirDateSelector.cs b/Shoko.Server/Models/Filters/Selectors/AirDateSelector.cs new file mode 100644 index 000000000..8a4f6f04c --- /dev/null +++ b/Shoko.Server/Models/Filters/Selectors/AirDateSelector.cs @@ -0,0 +1,10 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Selectors; + +public class AirDateSelector : IDateTimeSelector +{ + public bool UserDependent => false; + public Func Selector => f => f.AirDate; +} diff --git a/Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs b/Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs new file mode 100644 index 000000000..6730c88d3 --- /dev/null +++ b/Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs @@ -0,0 +1,10 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Selectors; + +public class EpisodeCountSelector : INumberSelector +{ + public bool UserDependent => false; + public Func Selector => f => f.EpisodeCount; +} diff --git a/Shoko.Server/Models/Filters/Selectors/LastAddedDateSelector.cs b/Shoko.Server/Models/Filters/Selectors/LastAddedDateSelector.cs new file mode 100644 index 000000000..dc1cdc015 --- /dev/null +++ b/Shoko.Server/Models/Filters/Selectors/LastAddedDateSelector.cs @@ -0,0 +1,10 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Selectors; + +public class LastAddedDateSelector : IDateTimeSelector +{ + public bool UserDependent => false; + public Func Selector => f => f.LastAddedDate; +} diff --git a/Shoko.Server/Models/Filters/Selectors/LastAirDateSelector.cs b/Shoko.Server/Models/Filters/Selectors/LastAirDateSelector.cs new file mode 100644 index 000000000..089e39c01 --- /dev/null +++ b/Shoko.Server/Models/Filters/Selectors/LastAirDateSelector.cs @@ -0,0 +1,10 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Selectors; + +public class LastAirDateSelector : IDateTimeSelector +{ + public bool UserDependent => false; + public Func Selector => f => f.LastAirDate; +} diff --git a/Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs b/Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs new file mode 100644 index 000000000..1ac7cbe1d --- /dev/null +++ b/Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs @@ -0,0 +1,10 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Selectors; + +public class LastWatchedDateSelector : IDateTimeSelector +{ + public bool UserDependent => true; + public Func Selector => f => f.LastWatchedDate; +} diff --git a/Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs b/Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs new file mode 100644 index 000000000..ace5644b5 --- /dev/null +++ b/Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs @@ -0,0 +1,10 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Selectors; + +public class SubtitleLanguageCountSelector : INumberSelector +{ + public bool UserDependent => false; + public Func Selector => f => f.SubtitleLanguages.Count; +} diff --git a/Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs b/Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs new file mode 100644 index 000000000..da841d5d9 --- /dev/null +++ b/Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs @@ -0,0 +1,10 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Selectors; + +public class TotalEpisodeCountSelector : INumberSelector +{ + public bool UserDependent => false; + public Func Selector => f => f.TotalEpisodeCount; +} diff --git a/Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs b/Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs new file mode 100644 index 000000000..ae6cb3967 --- /dev/null +++ b/Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs @@ -0,0 +1,10 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Selectors; + +public class WatchedDateSelector : IDateTimeSelector +{ + public bool UserDependent => true; + public Func Selector => f => f.WatchedDate; +} diff --git a/Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs b/Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs new file mode 100644 index 000000000..70dcc347f --- /dev/null +++ b/Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs @@ -0,0 +1,9 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.User; + +public class HasPermanentUserVotesExpression : FilterExpression +{ + public override bool UserDependent => true; + public override bool Evaluate(IFilterable filterable) => filterable.HasPermanentVotes; +} diff --git a/Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs b/Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs new file mode 100644 index 000000000..7065e8605 --- /dev/null +++ b/Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs @@ -0,0 +1,9 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.User; + +public class HasUnwatchedEpisodesExpression : FilterExpression +{ + public override bool UserDependent => true; + public override bool Evaluate(IFilterable filterable) => filterable.UnwatchedEpisodes > 0; +} diff --git a/Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs b/Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs new file mode 100644 index 000000000..c1580c134 --- /dev/null +++ b/Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs @@ -0,0 +1,9 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.User; + +public class HasUserVotesExpression : FilterExpression +{ + public override bool UserDependent => true; + public override bool Evaluate(IFilterable filterable) => filterable.HasVotes; +} diff --git a/Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs b/Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs new file mode 100644 index 000000000..90dc89927 --- /dev/null +++ b/Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs @@ -0,0 +1,9 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.User; + +public class HasWatchedEpisodesExpression : FilterExpression +{ + public override bool UserDependent => true; + public override bool Evaluate(IFilterable filterable) => filterable.WatchedEpisodes > 0; +} diff --git a/Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs b/Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs new file mode 100644 index 000000000..6e742caca --- /dev/null +++ b/Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs @@ -0,0 +1,9 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.User; + +public class IsFavoriteExpression : FilterExpression +{ + public override bool UserDependent => true; + public override bool Evaluate(IFilterable filterable) => filterable.IsFavorite; +} From a00832b587796448d04184135987e3d1bb86af74 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Fri, 25 Aug 2023 17:03:31 -0400 Subject: [PATCH 04/34] Filters: The Rest of the Selectors and Expressions --- .../Models/Filters/Info/HasAnimeTypeExpression.cs | 10 ++++++++++ .../Models/Filters/Info/HasVideoSourceExpression.cs | 10 ++++++++++ .../Filters/Selectors/HighestAniDBRatingSelector.cs | 10 ++++++++++ .../Filters/Selectors/HighestUserRatingSelector.cs | 10 ++++++++++ .../Filters/Selectors/LowestAniDBRatingSelector.cs | 10 ++++++++++ .../Filters/Selectors/LowestUserRatingSelector.cs | 10 ++++++++++ 6 files changed, 60 insertions(+) create mode 100644 Shoko.Server/Models/Filters/Info/HasAnimeTypeExpression.cs create mode 100644 Shoko.Server/Models/Filters/Info/HasVideoSourceExpression.cs create mode 100644 Shoko.Server/Models/Filters/Selectors/HighestAniDBRatingSelector.cs create mode 100644 Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs create mode 100644 Shoko.Server/Models/Filters/Selectors/LowestAniDBRatingSelector.cs create mode 100644 Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs diff --git a/Shoko.Server/Models/Filters/Info/HasAnimeTypeExpression.cs b/Shoko.Server/Models/Filters/Info/HasAnimeTypeExpression.cs new file mode 100644 index 000000000..cfaeee3dd --- /dev/null +++ b/Shoko.Server/Models/Filters/Info/HasAnimeTypeExpression.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Info; + +public class HasAnimeTypeExpression : FilterExpression +{ + public string Parameter { get; set; } + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.AnimeTypes.Contains(Parameter); +} diff --git a/Shoko.Server/Models/Filters/Info/HasVideoSourceExpression.cs b/Shoko.Server/Models/Filters/Info/HasVideoSourceExpression.cs new file mode 100644 index 000000000..8e78fc892 --- /dev/null +++ b/Shoko.Server/Models/Filters/Info/HasVideoSourceExpression.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Info; + +public class HasVideoSourceExpression : FilterExpression +{ + public string Parameter { get; set; } + public override bool UserDependent => false; + public override bool Evaluate(IFilterable filterable) => filterable.VideoSources.Contains(Parameter); +} diff --git a/Shoko.Server/Models/Filters/Selectors/HighestAniDBRatingSelector.cs b/Shoko.Server/Models/Filters/Selectors/HighestAniDBRatingSelector.cs new file mode 100644 index 000000000..6d59e6012 --- /dev/null +++ b/Shoko.Server/Models/Filters/Selectors/HighestAniDBRatingSelector.cs @@ -0,0 +1,10 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Selectors; + +public class HighestAniDBRatingSelector : INumberSelector +{ + public bool UserDependent => false; + public Func Selector => f => Convert.ToDouble(f.HighestAniDBRating); +} diff --git a/Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs b/Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs new file mode 100644 index 000000000..a0ea7213a --- /dev/null +++ b/Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs @@ -0,0 +1,10 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Selectors; + +public class HighestUserRatingSelector : INumberSelector +{ + public bool UserDependent => true; + public Func Selector => f => Convert.ToDouble(f.HighestUserRating); +} diff --git a/Shoko.Server/Models/Filters/Selectors/LowestAniDBRatingSelector.cs b/Shoko.Server/Models/Filters/Selectors/LowestAniDBRatingSelector.cs new file mode 100644 index 000000000..d15d8f465 --- /dev/null +++ b/Shoko.Server/Models/Filters/Selectors/LowestAniDBRatingSelector.cs @@ -0,0 +1,10 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Selectors; + +public class LowestAniDBRatingSelector : INumberSelector +{ + public bool UserDependent => false; + public Func Selector => f => Convert.ToDouble(f.LowestAniDBRating); +} diff --git a/Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs b/Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs new file mode 100644 index 000000000..2caa023a9 --- /dev/null +++ b/Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs @@ -0,0 +1,10 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Selectors; + +public class LowestUserRatingSelector : INumberSelector +{ + public bool UserDependent => true; + public Func Selector => f => Convert.ToDouble(f.LowestUserRating); +} From 56c533bb834bec4302dc05eeb6ae8531b44a9578 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sat, 26 Aug 2023 13:41:54 -0400 Subject: [PATCH 05/34] Redesign Selectors and Functions to be Expressions --- .../TypeConverters/FilterSelectorConverter.cs | 233 ------------------ .../Files/HasAudioLanguageExpression.cs | 3 +- .../Files/HasSubtitleLanguageExpression.cs | 3 +- .../Models/Filters/FilterExpression.cs | 5 +- .../Filters/Functions/DateAddFunction.cs | 15 ++ .../Filters/Functions/DateDiffFunction.cs | 15 ++ .../Models/Filters/Functions/TodayFunction.cs | 12 + .../Filters/Info/HasAnimeTypeExpression.cs | 3 +- .../HasMissingEpisodesCollectingExpression.cs | 3 +- .../Info/HasMissingEpisodesExpression.cs | 3 +- .../Filters/Info/HasTMDbLinkExpression.cs | 3 +- .../Models/Filters/Info/HasTagExpression.cs | 3 +- .../Filters/Info/HasTraktLinkExpression.cs | 3 +- .../Filters/Info/HasTvDBLinkExpression.cs | 3 +- .../Filters/Info/HasVideoSourceExpression.cs | 3 +- .../Models/Filters/Info/InSeasonExpression.cs | 3 +- .../Models/Filters/Info/InYearExpression.cs | 3 +- .../Filters/Info/IsFinishedExpression.cs | 3 +- .../Filters/Info/MissingTMDbLinkExpression.cs | 3 +- .../Info/MissingTraktLinkExpression.cs | 3 +- .../Filters/Info/MissingTvDBLinkExpression.cs | 3 +- .../Filters/Interfaces/IDateTimeSelector.cs | 8 - .../Filters/Interfaces/IFilterExpression.cs | 12 + .../Filters/Interfaces/IFilterSelector.cs | 6 - .../Filters/Interfaces/INumberSelector.cs | 8 - .../Filters/Interfaces/IStringSelector.cs | 8 - .../Models/Filters/Logic/AndExpression.cs | 7 +- .../Logic/DateTimes/EqualExpression.cs | 7 +- .../DateTimes/GreaterThanEqualExpression.cs | 7 +- .../Logic/DateTimes/GreaterThanExpression.cs | 7 +- .../DateTimes/LessThanEqualExpression.cs | 7 +- .../Logic/DateTimes/LessThanExpression.cs | 7 +- .../Logic/DateTimes/NotEqualExpression.cs | 7 +- .../Models/Filters/Logic/NotExpression.cs | 5 +- .../Filters/Logic/Numbers/EqualExpression.cs | 7 +- .../Numbers/GreaterThanEqualExpression.cs | 7 +- .../Logic/Numbers/GreaterThanExpression.cs | 7 +- .../Logic/Numbers/LessThanEqualExpression.cs | 7 +- .../Logic/Numbers/LessThanExpression.cs | 7 +- .../Logic/Numbers/NotEqualExpression.cs | 7 +- .../Models/Filters/Logic/OrExpression.cs | 7 +- .../Logic/Strings/ContainsExpression.cs | 7 +- .../Filters/Logic/Strings/EqualExpression.cs | 7 +- .../Logic/Strings/NotEqualExpression.cs | 7 +- .../Models/Filters/Logic/XorExpression.cs | 7 +- .../Filters/Selectors/AddedDateSelector.cs | 7 +- .../Filters/Selectors/AirDateSelector.cs | 7 +- .../Selectors/AudioLanguageCountSelector.cs | 8 +- .../Filters/Selectors/EpisodeCountSelector.cs | 8 +- .../Selectors/HighestAniDBRatingSelector.cs | 7 +- .../Selectors/HighestUserRatingSelector.cs | 7 +- .../Selectors/LastAddedDateSelector.cs | 7 +- .../Filters/Selectors/LastAirDateSelector.cs | 7 +- .../Selectors/LastWatchedDateSelector.cs | 7 +- .../Selectors/LowestAniDBRatingSelector.cs | 7 +- .../Selectors/LowestUserRatingSelector.cs | 7 +- .../SubtitleLanguageCountSelector.cs | 8 +- .../Selectors/TotalEpisodeCountSelector.cs | 8 +- .../Filters/Selectors/WatchedDateSelector.cs | 7 +- .../Filters/User/HasCustomTagExpression.cs | 3 +- .../User/HasPermanentUserVotesExpression.cs | 3 +- .../User/HasUnwatchedEpisodesExpression.cs | 3 +- .../Filters/User/HasUserVotesExpression.cs | 3 +- .../User/HasWatchedEpisodesExpression.cs | 3 +- .../Filters/User/IsFavoriteExpression.cs | 3 +- Shoko.Server/Models/Filters/readme.md | 6 +- 66 files changed, 234 insertions(+), 393 deletions(-) delete mode 100644 Shoko.Server/Databases/TypeConverters/FilterSelectorConverter.cs create mode 100644 Shoko.Server/Models/Filters/Functions/DateAddFunction.cs create mode 100644 Shoko.Server/Models/Filters/Functions/DateDiffFunction.cs create mode 100644 Shoko.Server/Models/Filters/Functions/TodayFunction.cs delete mode 100644 Shoko.Server/Models/Filters/Interfaces/IDateTimeSelector.cs create mode 100644 Shoko.Server/Models/Filters/Interfaces/IFilterExpression.cs delete mode 100644 Shoko.Server/Models/Filters/Interfaces/IFilterSelector.cs delete mode 100644 Shoko.Server/Models/Filters/Interfaces/INumberSelector.cs delete mode 100644 Shoko.Server/Models/Filters/Interfaces/IStringSelector.cs diff --git a/Shoko.Server/Databases/TypeConverters/FilterSelectorConverter.cs b/Shoko.Server/Databases/TypeConverters/FilterSelectorConverter.cs deleted file mode 100644 index f8e9a911c..000000000 --- a/Shoko.Server/Databases/TypeConverters/FilterSelectorConverter.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using NHibernate.SqlTypes; -using NHibernate.UserTypes; -using System.Data; -using System.Data.Common; -using NHibernate; -using NHibernate.Engine; -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Databases.TypeConverters; - -public class FilterSelectorConverter : TypeConverter, IUserType -{ - private static Dictionary s_selectors = new(); - - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return typeof(IFilterSelector).IsAssignableFrom(sourceType); - } - - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - switch (destinationType.FullName) - { - case "System.String": - return true; - default: - return false; - } - } - - public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, - object value) - { - switch (value) - { - case string s: - if (s_selectors.TryGetValue(s, out var selector)) return selector; - - var type = Type.GetType(s) ?? throw new ArgumentException($"Value was not found: {s}"); - selector = (IFilterSelector)Activator.CreateInstance(type); - s_selectors.Add(s, selector); - return selector; - default: - throw new ArgumentException("DestinationType must be string"); - } - } - - /// - /// Converts the given value object to the specified type - /// - /// Ignored - /// Ignored - /// The to convert. - /// The to convert the parameter to. - /// - /// An that represents the converted value. The value will be 1 if is true, otherwise 0 - /// - /// The parameter is . - /// The conversion could not be performed. - public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, - object value, Type destinationType) - { - if (value == null) - { - throw new ArgumentNullException(nameof(value), @"Value can't be null"); - } - - if (value is not IFilterSelector) - { - throw new ArgumentException(@"Value isn't of type IFilterSelector", nameof(value)); - } - - return destinationType.FullName switch - { - "System.String" => value.GetType().FullName, - _ => throw new ArgumentException("DestinationType must be string or int") - }; - } - - - /// - /// Creates an instance of the Type that this is associated with (bool) - /// - /// ignored. - /// ignored. - /// - /// An of type bool. It always returns 'true' for this converter. - /// - public override object CreateInstance(ITypeDescriptorContext context, System.Collections.IDictionary propertyValues) - { - return true; - } - - #region IUserType Members - - /// - /// Reconstruct an object from the cacheable representation. At the very least this - /// method should perform a deep copy if the type is mutable. (optional operation) - /// - /// the object to be cached - /// the owner of the cached object - /// - /// a reconstructed object from the cacheable representation - /// - public object Assemble(object cached, object owner) - { - return DeepCopy(cached); - } - - /// - /// Return a deep copy of the persistent state, stopping at entities and at collections. - /// - /// generally a collection element or entity field - /// a copy - public object DeepCopy(object value) - { - return value; - } - - /// - /// Transform the object into its cacheable representation. At the very least this - /// method should perform a deep copy if the type is mutable. That may not be enough - /// for some implementations, however; for example, associations must be cached as - /// identifier values. (optional operation) - /// - /// the object to be cached - /// a cacheable representation of the object - public object Disassemble(object value) - { - return DeepCopy(value); - } - - /// - /// Returns a hash code for this instance. - /// - /// The x. - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public int GetHashCode(object x) - { - return x == null ? base.GetHashCode() : x.GetHashCode(); - } - - /// - /// Are objects of this type mutable? - /// - /// - public bool IsMutable => true; - - /// - /// Retrieve an instance of the mapped class from a JDBC resultset. - /// Implementors should handle possibility of null values. - /// - /// a IDataReader - /// column names - /// - /// the containing entity - /// - /// HibernateException - public object NullSafeGet(DbDataReader rs, string[] names, ISessionImplementor impl, object owner) - { - var rawValue = NHibernateUtil.String.NullSafeGet(rs, names[0], impl); - return rawValue == null ? null : ConvertFrom(null!, null!, rawValue); - } - - /// - /// Write an instance of the mapped class to a prepared statement. - /// Implementors should handle possibility of null values. - /// A multi-column type should be written to parameters starting from index. - /// - /// a IDbCommand - /// the object to write - /// command parameter index - /// - /// HibernateException - public void NullSafeSet(DbCommand cmd, object value, int index, ISessionImplementor session) - { - ((IDataParameter)cmd.Parameters[index]).Value = - value == null ? DBNull.Value : ConvertTo(null, null, value, typeof(string)); - } - - /// - /// During merge, replace the existing () value in the entity - /// we are merging to with a new () value from the detached - /// entity we are merging. For immutable objects, or null values, it is safe to simply - /// return the first parameter. For mutable objects, it is safe to return a copy of the - /// first parameter. For objects with component values, it might make sense to - /// recursively replace component values. - /// - /// the value from the detached entity being merged - /// the value in the managed entity - /// the managed entity - /// the value to be merged - public object Replace(object original, object target, object owner) - { - return original; - } - - /// - /// The type returned by NullSafeGet() - /// - public Type ReturnedType => typeof(string); - - /// - /// The SQL types for the columns mapped by this type. - /// - /// - public SqlType[] SqlTypes => new[] { NHibernateUtil.String.SqlType }; - - /// - /// Determines whether the specified is equal to this instance. - /// - /// The to compare with this instance. - /// The y. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - bool IUserType.Equals(object x, object y) - { - if (ReferenceEquals(x, y)) - { - return true; - } - - return x != null && y != null && x.Equals(y); - } - - #endregion -} diff --git a/Shoko.Server/Models/Filters/Files/HasAudioLanguageExpression.cs b/Shoko.Server/Models/Filters/Files/HasAudioLanguageExpression.cs index 81134d773..feb4e0779 100644 --- a/Shoko.Server/Models/Filters/Files/HasAudioLanguageExpression.cs +++ b/Shoko.Server/Models/Filters/Files/HasAudioLanguageExpression.cs @@ -2,9 +2,10 @@ namespace Shoko.Server.Models.Filters.Files; -public class HasAudioLanguageExpression : FilterExpression +public class HasAudioLanguageExpression : FilterExpression { public string Parameter { get; set; } + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.AudioLanguages.Contains(Parameter); } diff --git a/Shoko.Server/Models/Filters/Files/HasSubtitleLanguageExpression.cs b/Shoko.Server/Models/Filters/Files/HasSubtitleLanguageExpression.cs index 50912f824..7bc335581 100644 --- a/Shoko.Server/Models/Filters/Files/HasSubtitleLanguageExpression.cs +++ b/Shoko.Server/Models/Filters/Files/HasSubtitleLanguageExpression.cs @@ -2,9 +2,10 @@ namespace Shoko.Server.Models.Filters.Files; -public class HasSubtitleLanguageExpression : FilterExpression +public class HasSubtitleLanguageExpression : FilterExpression { public string Parameter { get; set; } + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.SubtitleLanguages.Contains(Parameter); } diff --git a/Shoko.Server/Models/Filters/FilterExpression.cs b/Shoko.Server/Models/Filters/FilterExpression.cs index c077dddc6..a48f47f3a 100644 --- a/Shoko.Server/Models/Filters/FilterExpression.cs +++ b/Shoko.Server/Models/Filters/FilterExpression.cs @@ -2,9 +2,10 @@ namespace Shoko.Server.Models.Filters; -public abstract class FilterExpression +public abstract class FilterExpression : IFilterExpression { + public abstract bool TimeDependent { get; } public abstract bool UserDependent { get; } - public abstract bool Evaluate(IFilterable filterable); + public abstract T Evaluate(IFilterable f); } diff --git a/Shoko.Server/Models/Filters/Functions/DateAddFunction.cs b/Shoko.Server/Models/Filters/Functions/DateAddFunction.cs new file mode 100644 index 000000000..db8ebf4a9 --- /dev/null +++ b/Shoko.Server/Models/Filters/Functions/DateAddFunction.cs @@ -0,0 +1,15 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Functions; + +public class DateAddFunction : FilterExpression +{ + public FilterExpression Selector { get; set; } + public TimeSpan Parameter { get; set; } + + public override bool TimeDependent => Selector.TimeDependent; + public override bool UserDependent => Selector.UserDependent; + + public override DateTime? Evaluate(IFilterable f) => Selector.Evaluate(f) + Parameter; +} diff --git a/Shoko.Server/Models/Filters/Functions/DateDiffFunction.cs b/Shoko.Server/Models/Filters/Functions/DateDiffFunction.cs new file mode 100644 index 000000000..109d54374 --- /dev/null +++ b/Shoko.Server/Models/Filters/Functions/DateDiffFunction.cs @@ -0,0 +1,15 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Functions; + +public class DateDiffFunction : FilterExpression +{ + public FilterExpression Selector { get; set; } + public TimeSpan Parameter { get; set; } + + public override bool TimeDependent => Selector.TimeDependent; + public override bool UserDependent => Selector.UserDependent; + + public override DateTime? Evaluate(IFilterable f) => Selector.Evaluate(f) - Parameter; +} diff --git a/Shoko.Server/Models/Filters/Functions/TodayFunction.cs b/Shoko.Server/Models/Filters/Functions/TodayFunction.cs new file mode 100644 index 000000000..1afd221c2 --- /dev/null +++ b/Shoko.Server/Models/Filters/Functions/TodayFunction.cs @@ -0,0 +1,12 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.Functions; + +public class TodayFunction : FilterExpression +{ + public override bool TimeDependent => true; + public override bool UserDependent => false; + + public override DateTime? Evaluate(IFilterable f) => DateTime.Today; +} diff --git a/Shoko.Server/Models/Filters/Info/HasAnimeTypeExpression.cs b/Shoko.Server/Models/Filters/Info/HasAnimeTypeExpression.cs index cfaeee3dd..fbd85b13f 100644 --- a/Shoko.Server/Models/Filters/Info/HasAnimeTypeExpression.cs +++ b/Shoko.Server/Models/Filters/Info/HasAnimeTypeExpression.cs @@ -2,9 +2,10 @@ namespace Shoko.Server.Models.Filters.Info; -public class HasAnimeTypeExpression : FilterExpression +public class HasAnimeTypeExpression : FilterExpression { public string Parameter { get; set; } + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.AnimeTypes.Contains(Parameter); } diff --git a/Shoko.Server/Models/Filters/Info/HasMissingEpisodesCollectingExpression.cs b/Shoko.Server/Models/Filters/Info/HasMissingEpisodesCollectingExpression.cs index 06d7958e0..873a989ef 100644 --- a/Shoko.Server/Models/Filters/Info/HasMissingEpisodesCollectingExpression.cs +++ b/Shoko.Server/Models/Filters/Info/HasMissingEpisodesCollectingExpression.cs @@ -2,8 +2,9 @@ namespace Shoko.Server.Models.Filters.Info; -public class HasMissingEpisodesCollectingExpression : FilterExpression +public class HasMissingEpisodesCollectingExpression : FilterExpression { + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.MissingEpisodesCollecting > 0; } diff --git a/Shoko.Server/Models/Filters/Info/HasMissingEpisodesExpression.cs b/Shoko.Server/Models/Filters/Info/HasMissingEpisodesExpression.cs index 22fd8a996..0ea2c9278 100644 --- a/Shoko.Server/Models/Filters/Info/HasMissingEpisodesExpression.cs +++ b/Shoko.Server/Models/Filters/Info/HasMissingEpisodesExpression.cs @@ -2,8 +2,9 @@ namespace Shoko.Server.Models.Filters.Info; -public class HasMissingEpisodesExpression : FilterExpression +public class HasMissingEpisodesExpression : FilterExpression { + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.MissingEpisodes > 0; } diff --git a/Shoko.Server/Models/Filters/Info/HasTMDbLinkExpression.cs b/Shoko.Server/Models/Filters/Info/HasTMDbLinkExpression.cs index 13bebfcc6..212f25503 100644 --- a/Shoko.Server/Models/Filters/Info/HasTMDbLinkExpression.cs +++ b/Shoko.Server/Models/Filters/Info/HasTMDbLinkExpression.cs @@ -2,8 +2,9 @@ namespace Shoko.Server.Models.Filters.Info; -public class HasTMDbLinkExpression : FilterExpression +public class HasTMDbLinkExpression : FilterExpression { + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.HasTMDbLink; } diff --git a/Shoko.Server/Models/Filters/Info/HasTagExpression.cs b/Shoko.Server/Models/Filters/Info/HasTagExpression.cs index 368552a5a..53c656c5a 100644 --- a/Shoko.Server/Models/Filters/Info/HasTagExpression.cs +++ b/Shoko.Server/Models/Filters/Info/HasTagExpression.cs @@ -2,9 +2,10 @@ namespace Shoko.Server.Models.Filters.Info; -public class HasTagExpression : FilterExpression +public class HasTagExpression : FilterExpression { public string Parameter { get; set; } + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.Tags.Contains(Parameter); } diff --git a/Shoko.Server/Models/Filters/Info/HasTraktLinkExpression.cs b/Shoko.Server/Models/Filters/Info/HasTraktLinkExpression.cs index 1b5019990..fbc718fd2 100644 --- a/Shoko.Server/Models/Filters/Info/HasTraktLinkExpression.cs +++ b/Shoko.Server/Models/Filters/Info/HasTraktLinkExpression.cs @@ -2,8 +2,9 @@ namespace Shoko.Server.Models.Filters.Info; -public class HasTraktLinkExpression : FilterExpression +public class HasTraktLinkExpression : FilterExpression { + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.HasTraktLink; } diff --git a/Shoko.Server/Models/Filters/Info/HasTvDBLinkExpression.cs b/Shoko.Server/Models/Filters/Info/HasTvDBLinkExpression.cs index dbdf1bb15..2499dd6fc 100644 --- a/Shoko.Server/Models/Filters/Info/HasTvDBLinkExpression.cs +++ b/Shoko.Server/Models/Filters/Info/HasTvDBLinkExpression.cs @@ -2,8 +2,9 @@ namespace Shoko.Server.Models.Filters.Info; -public class HasTvDBLinkExpression : FilterExpression +public class HasTvDBLinkExpression : FilterExpression { + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.HasTvDBLink; } diff --git a/Shoko.Server/Models/Filters/Info/HasVideoSourceExpression.cs b/Shoko.Server/Models/Filters/Info/HasVideoSourceExpression.cs index 8e78fc892..c83923b44 100644 --- a/Shoko.Server/Models/Filters/Info/HasVideoSourceExpression.cs +++ b/Shoko.Server/Models/Filters/Info/HasVideoSourceExpression.cs @@ -2,9 +2,10 @@ namespace Shoko.Server.Models.Filters.Info; -public class HasVideoSourceExpression : FilterExpression +public class HasVideoSourceExpression : FilterExpression { public string Parameter { get; set; } + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.VideoSources.Contains(Parameter); } diff --git a/Shoko.Server/Models/Filters/Info/InSeasonExpression.cs b/Shoko.Server/Models/Filters/Info/InSeasonExpression.cs index d53987657..58a7ac96e 100644 --- a/Shoko.Server/Models/Filters/Info/InSeasonExpression.cs +++ b/Shoko.Server/Models/Filters/Info/InSeasonExpression.cs @@ -5,9 +5,10 @@ namespace Shoko.Server.Models.Filters.Info; /// /// Parameter is a season followed by year, ie Winter 2022 /// -public class InSeasonExpression : FilterExpression +public class InSeasonExpression : FilterExpression { public string Parameter { get; set; } + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.Seasons.Contains(Parameter); } diff --git a/Shoko.Server/Models/Filters/Info/InYearExpression.cs b/Shoko.Server/Models/Filters/Info/InYearExpression.cs index 37f2acd45..bf6a7637d 100644 --- a/Shoko.Server/Models/Filters/Info/InYearExpression.cs +++ b/Shoko.Server/Models/Filters/Info/InYearExpression.cs @@ -2,9 +2,10 @@ namespace Shoko.Server.Models.Filters.Info; -public class InYearExpression : FilterExpression +public class InYearExpression : FilterExpression { public int Parameter { get; set; } + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.Years.Contains(Parameter); } diff --git a/Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs b/Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs index 6c772f93d..4cbf1b07d 100644 --- a/Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs +++ b/Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs @@ -2,8 +2,9 @@ namespace Shoko.Server.Models.Filters.Info; -public class IsFinishedExpression : FilterExpression +public class IsFinishedExpression : FilterExpression { + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.IsFinished; } diff --git a/Shoko.Server/Models/Filters/Info/MissingTMDbLinkExpression.cs b/Shoko.Server/Models/Filters/Info/MissingTMDbLinkExpression.cs index edfc41f4e..754c9fe39 100644 --- a/Shoko.Server/Models/Filters/Info/MissingTMDbLinkExpression.cs +++ b/Shoko.Server/Models/Filters/Info/MissingTMDbLinkExpression.cs @@ -5,8 +5,9 @@ namespace Shoko.Server.Models.Filters.Info; /// /// Missing Links include logic for whether a link should exist /// -public class MissingTMDbLinkExpression : FilterExpression +public class MissingTMDbLinkExpression : FilterExpression { + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.HasMissingTMDbLink; } diff --git a/Shoko.Server/Models/Filters/Info/MissingTraktLinkExpression.cs b/Shoko.Server/Models/Filters/Info/MissingTraktLinkExpression.cs index c7b8a70f8..865007c0e 100644 --- a/Shoko.Server/Models/Filters/Info/MissingTraktLinkExpression.cs +++ b/Shoko.Server/Models/Filters/Info/MissingTraktLinkExpression.cs @@ -5,8 +5,9 @@ namespace Shoko.Server.Models.Filters.Info; /// /// Missing Links include logic for whether a link should exist /// -public class MissingTraktLinkExpression : FilterExpression +public class MissingTraktLinkExpression : FilterExpression { + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.HasMissingTraktLink; } diff --git a/Shoko.Server/Models/Filters/Info/MissingTvDBLinkExpression.cs b/Shoko.Server/Models/Filters/Info/MissingTvDBLinkExpression.cs index 40e0d0992..41f46a2b9 100644 --- a/Shoko.Server/Models/Filters/Info/MissingTvDBLinkExpression.cs +++ b/Shoko.Server/Models/Filters/Info/MissingTvDBLinkExpression.cs @@ -5,8 +5,9 @@ namespace Shoko.Server.Models.Filters.Info; /// /// Missing Links include logic for whether a link should exist /// -public class MissingTvDBLinkExpression : FilterExpression +public class MissingTvDBLinkExpression : FilterExpression { + public override bool TimeDependent => false; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.HasMissingTvDbLink; } diff --git a/Shoko.Server/Models/Filters/Interfaces/IDateTimeSelector.cs b/Shoko.Server/Models/Filters/Interfaces/IDateTimeSelector.cs deleted file mode 100644 index bc5e4d4a1..000000000 --- a/Shoko.Server/Models/Filters/Interfaces/IDateTimeSelector.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Shoko.Server.Models.Filters.Interfaces; - -public interface IDateTimeSelector : IFilterSelector -{ - Func Selector { get; } -} diff --git a/Shoko.Server/Models/Filters/Interfaces/IFilterExpression.cs b/Shoko.Server/Models/Filters/Interfaces/IFilterExpression.cs new file mode 100644 index 000000000..5df33f2e7 --- /dev/null +++ b/Shoko.Server/Models/Filters/Interfaces/IFilterExpression.cs @@ -0,0 +1,12 @@ +namespace Shoko.Server.Models.Filters.Interfaces; + +public interface IFilterExpression +{ + bool TimeDependent { get; } + bool UserDependent { get; } +} + +public interface IFilterExpression : IFilterExpression +{ + T Evaluate(IFilterable f); +} diff --git a/Shoko.Server/Models/Filters/Interfaces/IFilterSelector.cs b/Shoko.Server/Models/Filters/Interfaces/IFilterSelector.cs deleted file mode 100644 index 596c56d9a..000000000 --- a/Shoko.Server/Models/Filters/Interfaces/IFilterSelector.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Shoko.Server.Models.Filters.Interfaces; - -public interface IFilterSelector -{ - bool UserDependent { get; } -} diff --git a/Shoko.Server/Models/Filters/Interfaces/INumberSelector.cs b/Shoko.Server/Models/Filters/Interfaces/INumberSelector.cs deleted file mode 100644 index 2b88a5ef0..000000000 --- a/Shoko.Server/Models/Filters/Interfaces/INumberSelector.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Shoko.Server.Models.Filters.Interfaces; - -public interface INumberSelector : IFilterSelector -{ - Func Selector { get; } -} diff --git a/Shoko.Server/Models/Filters/Interfaces/IStringSelector.cs b/Shoko.Server/Models/Filters/Interfaces/IStringSelector.cs deleted file mode 100644 index 7736a3b60..000000000 --- a/Shoko.Server/Models/Filters/Interfaces/IStringSelector.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Shoko.Server.Models.Filters.Interfaces; - -public interface IStringSelector : IFilterSelector -{ - Func Selector { get; } -} diff --git a/Shoko.Server/Models/Filters/Logic/AndExpression.cs b/Shoko.Server/Models/Filters/Logic/AndExpression.cs index f2cd38b06..8a58d98a8 100644 --- a/Shoko.Server/Models/Filters/Logic/AndExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/AndExpression.cs @@ -2,11 +2,12 @@ namespace Shoko.Server.Models.Filters.Logic; -public class AndExpression : FilterExpression +public class AndExpression : FilterExpression { + public override bool TimeDependent => Left.TimeDependent || Right.TimeDependent; public override bool UserDependent => Left.UserDependent || Right.UserDependent; public override bool Evaluate(IFilterable filterable) => Left.Evaluate(filterable) && Right.Evaluate(filterable); - public FilterExpression Left { get; set; } - public FilterExpression Right { get; set; } + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } } diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs index d223bae08..2f2c1129e 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs @@ -3,10 +3,11 @@ namespace Shoko.Server.Models.Filters.Logic.DateTimes; -public class EqualExpression : FilterExpression +public class EqualExpression : FilterExpression { - public IDateTimeSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public DateTime Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => (Selector.Selector(filterable) - Parameter)?.TotalDays < 1; + public override bool Evaluate(IFilterable filterable) => (Selector.Evaluate(filterable) - Parameter)?.TotalDays < 1; } diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs index f12d559d9..e8b0bc6f2 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs @@ -3,14 +3,15 @@ namespace Shoko.Server.Models.Filters.Logic.DateTimes; -public class GreaterThanEqualExpression : FilterExpression +public class GreaterThanEqualExpression : FilterExpression { - public IDateTimeSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public DateTime Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; public override bool Evaluate(IFilterable filterable) { - var date = Selector.Selector(filterable); + var date = Selector.Evaluate(filterable); if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) return false; return date.Value.Date >= Parameter; } diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs index bcd4f33ff..8c440e086 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs @@ -3,14 +3,15 @@ namespace Shoko.Server.Models.Filters.Logic.DateTimes; -public class GreaterThanExpression : FilterExpression +public class GreaterThanExpression : FilterExpression { - public IDateTimeSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public DateTime Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; public override bool Evaluate(IFilterable filterable) { - var date = Selector.Selector(filterable); + var date = Selector.Evaluate(filterable); if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) return false; return date.Value.Date > Parameter; } diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs index 09bfac619..ae41f5636 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs @@ -3,14 +3,15 @@ namespace Shoko.Server.Models.Filters.Logic.DateTimes; -public class LessThanEqualExpression : FilterExpression +public class LessThanEqualExpression : FilterExpression { - public IDateTimeSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public DateTime Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; public override bool Evaluate(IFilterable filterable) { - var date = Selector.Selector(filterable); + var date = Selector.Evaluate(filterable); if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) return false; return date.Value.Date <= Parameter; } diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs index 689385885..54414929c 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs @@ -3,14 +3,15 @@ namespace Shoko.Server.Models.Filters.Logic.DateTimes; -public class LessThanExpression : FilterExpression +public class LessThanExpression : FilterExpression { - public IDateTimeSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public DateTime Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; public override bool Evaluate(IFilterable filterable) { - var date = Selector.Selector(filterable); + var date = Selector.Evaluate(filterable); if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) return false; return date.Value.Date < Parameter; } diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs index b4bfed512..fe9aee7a9 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs @@ -3,10 +3,11 @@ namespace Shoko.Server.Models.Filters.Logic.DateTimes; -public class NotEqualExpression : FilterExpression +public class NotEqualExpression : FilterExpression { - public IDateTimeSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public DateTime Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => (Selector.Selector(filterable) - Parameter)?.TotalDays >= 1; + public override bool Evaluate(IFilterable filterable) => (Selector.Evaluate(filterable) - Parameter)?.TotalDays >= 1; } diff --git a/Shoko.Server/Models/Filters/Logic/NotExpression.cs b/Shoko.Server/Models/Filters/Logic/NotExpression.cs index 36e4db647..4e30ef52e 100644 --- a/Shoko.Server/Models/Filters/Logic/NotExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/NotExpression.cs @@ -2,10 +2,11 @@ namespace Shoko.Server.Models.Filters.Logic; -public class NotExpression : FilterExpression +public class NotExpression : FilterExpression { + public override bool TimeDependent => Left.TimeDependent; public override bool UserDependent => Left.UserDependent; public override bool Evaluate(IFilterable filterable) => !Left.Evaluate(filterable); - public FilterExpression Left { get; set; } + public FilterExpression Left { get; set; } } diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs index b8a90e91e..dea51ca57 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs @@ -3,10 +3,11 @@ namespace Shoko.Server.Models.Filters.Logic.Numbers; -public class EqualExpression : FilterExpression +public class EqualExpression : FilterExpression { - public INumberSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public double Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => Math.Abs(Selector.Selector(filterable) - Parameter) < 0.001D; + public override bool Evaluate(IFilterable filterable) => Math.Abs(Selector.Evaluate(filterable) - Parameter) < 0.001D; } diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs index 65b325de3..2c7978b91 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs @@ -2,10 +2,11 @@ namespace Shoko.Server.Models.Filters.Logic.Numbers; -public class GreaterThanEqualExpression : FilterExpression +public class GreaterThanEqualExpression : FilterExpression { - public INumberSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public double Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => Selector.Selector(filterable) >= Parameter; + public override bool Evaluate(IFilterable filterable) => Selector.Evaluate(filterable) >= Parameter; } diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs index 27fe8cbc9..120879c53 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs @@ -2,10 +2,11 @@ namespace Shoko.Server.Models.Filters.Logic.Numbers; -public class GreaterThanExpression : FilterExpression +public class GreaterThanExpression : FilterExpression { - public INumberSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public double Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => Selector.Selector(filterable) > Parameter; + public override bool Evaluate(IFilterable filterable) => Selector.Evaluate(filterable) > Parameter; } diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs index 48a865a4e..d9b8c004d 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs @@ -2,10 +2,11 @@ namespace Shoko.Server.Models.Filters.Logic.Numbers; -public class LessThanEqualExpression : FilterExpression +public class LessThanEqualExpression : FilterExpression { - public INumberSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public double Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => Selector.Selector(filterable) <= Parameter; + public override bool Evaluate(IFilterable filterable) => Selector.Evaluate(filterable) <= Parameter; } diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs index 09fddb575..1a4b9d42c 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs @@ -2,10 +2,11 @@ namespace Shoko.Server.Models.Filters.Logic.Numbers; -public class LessThanExpression : FilterExpression +public class LessThanExpression : FilterExpression { - public INumberSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public double Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => Selector.Selector(filterable) < Parameter; + public override bool Evaluate(IFilterable filterable) => Selector.Evaluate(filterable) < Parameter; } diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs index 658b0b019..2ee3f8972 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs @@ -3,10 +3,11 @@ namespace Shoko.Server.Models.Filters.Logic.Numbers; -public class NotEqualExpression : FilterExpression +public class NotEqualExpression : FilterExpression { - public INumberSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public double Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => Math.Abs(Selector.Selector(filterable) - Parameter) >= 0.001D; + public override bool Evaluate(IFilterable filterable) => Math.Abs(Selector.Evaluate(filterable) - Parameter) >= 0.001D; } diff --git a/Shoko.Server/Models/Filters/Logic/OrExpression.cs b/Shoko.Server/Models/Filters/Logic/OrExpression.cs index 6f9fd2e13..a3eaffba4 100644 --- a/Shoko.Server/Models/Filters/Logic/OrExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/OrExpression.cs @@ -2,11 +2,12 @@ namespace Shoko.Server.Models.Filters.Logic; -public class OrExpression : FilterExpression +public class OrExpression : FilterExpression { + public override bool TimeDependent => Left.TimeDependent || Right.TimeDependent; public override bool UserDependent => Left.UserDependent || Right.UserDependent; public override bool Evaluate(IFilterable filterable) => Left.Evaluate(filterable) || Right.Evaluate(filterable); - public FilterExpression Left { get; set; } - public FilterExpression Right { get; set; } + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } } diff --git a/Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs b/Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs index 85225443b..90169ebe1 100644 --- a/Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs @@ -3,11 +3,12 @@ namespace Shoko.Server.Models.Filters.Logic.Strings; -public class ContainsExpression : FilterExpression +public class ContainsExpression : FilterExpression { - public IStringSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public string Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => Parameter.Contains(Selector.Selector(filterable), StringComparison.InvariantCultureIgnoreCase); + public override bool Evaluate(IFilterable filterable) => Parameter.Contains(Selector.Evaluate(filterable), StringComparison.InvariantCultureIgnoreCase); } diff --git a/Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs index b7a7c9386..69b63680a 100644 --- a/Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs @@ -3,12 +3,13 @@ namespace Shoko.Server.Models.Filters.Logic.Strings; -public class EqualExpression : FilterExpression +public class EqualExpression : FilterExpression { - public IStringSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public string Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; public override bool Evaluate(IFilterable filterable) => - string.Equals(Selector.Selector(filterable), Parameter, StringComparison.InvariantCultureIgnoreCase); + string.Equals(Selector.Evaluate(filterable), Parameter, StringComparison.InvariantCultureIgnoreCase); } diff --git a/Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs index f0796dc9e..2bbb3a79b 100644 --- a/Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs @@ -3,12 +3,13 @@ namespace Shoko.Server.Models.Filters.Logic.Strings; -public class NotEqualExpression : FilterExpression +public class NotEqualExpression : FilterExpression { - public IStringSelector Selector { get; set; } + public FilterExpression Selector { get; set; } public string Parameter { get; set; } + public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; public override bool Evaluate(IFilterable filterable) => - !string.Equals(Selector.Selector(filterable), Parameter, StringComparison.InvariantCultureIgnoreCase); + !string.Equals(Selector.Evaluate(filterable), Parameter, StringComparison.InvariantCultureIgnoreCase); } diff --git a/Shoko.Server/Models/Filters/Logic/XorExpression.cs b/Shoko.Server/Models/Filters/Logic/XorExpression.cs index cbe869f29..7e1120e2c 100644 --- a/Shoko.Server/Models/Filters/Logic/XorExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/XorExpression.cs @@ -2,11 +2,12 @@ namespace Shoko.Server.Models.Filters.Logic; -public class XorExpression : FilterExpression +public class XorExpression : FilterExpression { + public override bool TimeDependent => Left.TimeDependent || Right.TimeDependent; public override bool UserDependent => Left.UserDependent || Right.UserDependent; public override bool Evaluate(IFilterable filterable) => Left.Evaluate(filterable) ^ Right.Evaluate(filterable); - public FilterExpression Left { get; set; } - public FilterExpression Right { get; set; } + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } } diff --git a/Shoko.Server/Models/Filters/Selectors/AddedDateSelector.cs b/Shoko.Server/Models/Filters/Selectors/AddedDateSelector.cs index 004c31ffb..707ebf820 100644 --- a/Shoko.Server/Models/Filters/Selectors/AddedDateSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/AddedDateSelector.cs @@ -3,8 +3,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class AddedDateSelector : IDateTimeSelector +public class AddedDateSelector : FilterExpression { - public bool UserDependent => false; - public Func Selector => f => f.AddedDate; + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override DateTime? Evaluate(IFilterable f) => f.AddedDate; } diff --git a/Shoko.Server/Models/Filters/Selectors/AirDateSelector.cs b/Shoko.Server/Models/Filters/Selectors/AirDateSelector.cs index 8a4f6f04c..da7d67211 100644 --- a/Shoko.Server/Models/Filters/Selectors/AirDateSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/AirDateSelector.cs @@ -3,8 +3,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class AirDateSelector : IDateTimeSelector +public class AirDateSelector : FilterExpression { - public bool UserDependent => false; - public Func Selector => f => f.AirDate; + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override DateTime? Evaluate(IFilterable f) => f.AirDate; } diff --git a/Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs b/Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs index fc21b53fa..44e651295 100644 --- a/Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs @@ -1,10 +1,10 @@ -using System; using Shoko.Server.Models.Filters.Interfaces; namespace Shoko.Server.Models.Filters.Selectors; -public class AudioLanguageCountSelector : INumberSelector +public class AudioLanguageCountSelector : FilterExpression { - public bool UserDependent => false; - public Func Selector => f => f.AudioLanguages.Count; + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override double Evaluate(IFilterable f) => f.AudioLanguages.Count; } diff --git a/Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs b/Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs index 6730c88d3..2cb3d13d4 100644 --- a/Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs @@ -1,10 +1,10 @@ -using System; using Shoko.Server.Models.Filters.Interfaces; namespace Shoko.Server.Models.Filters.Selectors; -public class EpisodeCountSelector : INumberSelector +public class EpisodeCountSelector : FilterExpression { - public bool UserDependent => false; - public Func Selector => f => f.EpisodeCount; + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override double Evaluate(IFilterable f) => f.EpisodeCount; } diff --git a/Shoko.Server/Models/Filters/Selectors/HighestAniDBRatingSelector.cs b/Shoko.Server/Models/Filters/Selectors/HighestAniDBRatingSelector.cs index 6d59e6012..20188c847 100644 --- a/Shoko.Server/Models/Filters/Selectors/HighestAniDBRatingSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/HighestAniDBRatingSelector.cs @@ -3,8 +3,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class HighestAniDBRatingSelector : INumberSelector +public class HighestAniDBRatingSelector : FilterExpression { - public bool UserDependent => false; - public Func Selector => f => Convert.ToDouble(f.HighestAniDBRating); + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override double Evaluate(IFilterable f) => Convert.ToDouble(f.HighestAniDBRating); } diff --git a/Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs b/Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs index a0ea7213a..6e77faf43 100644 --- a/Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs @@ -3,8 +3,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class HighestUserRatingSelector : INumberSelector +public class HighestUserRatingSelector : FilterExpression { - public bool UserDependent => true; - public Func Selector => f => Convert.ToDouble(f.HighestUserRating); + public override bool TimeDependent => false; + public override bool UserDependent => true; + public override double Evaluate(IFilterable f) => Convert.ToDouble(f.HighestUserRating); } diff --git a/Shoko.Server/Models/Filters/Selectors/LastAddedDateSelector.cs b/Shoko.Server/Models/Filters/Selectors/LastAddedDateSelector.cs index dc1cdc015..042b66652 100644 --- a/Shoko.Server/Models/Filters/Selectors/LastAddedDateSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/LastAddedDateSelector.cs @@ -3,8 +3,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class LastAddedDateSelector : IDateTimeSelector +public class LastAddedDateSelector : FilterExpression { - public bool UserDependent => false; - public Func Selector => f => f.LastAddedDate; + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override DateTime? Evaluate(IFilterable f) => f.LastAddedDate; } diff --git a/Shoko.Server/Models/Filters/Selectors/LastAirDateSelector.cs b/Shoko.Server/Models/Filters/Selectors/LastAirDateSelector.cs index 089e39c01..8d976a678 100644 --- a/Shoko.Server/Models/Filters/Selectors/LastAirDateSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/LastAirDateSelector.cs @@ -3,8 +3,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class LastAirDateSelector : IDateTimeSelector +public class LastAirDateSelector : FilterExpression { - public bool UserDependent => false; - public Func Selector => f => f.LastAirDate; + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override DateTime? Evaluate(IFilterable f) => f.LastAirDate; } diff --git a/Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs b/Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs index 1ac7cbe1d..0d032086b 100644 --- a/Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs @@ -3,8 +3,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class LastWatchedDateSelector : IDateTimeSelector +public class LastWatchedDateSelector : FilterExpression { - public bool UserDependent => true; - public Func Selector => f => f.LastWatchedDate; + public override bool TimeDependent => false; + public override bool UserDependent => true; + public override DateTime? Evaluate(IFilterable f) => f.LastWatchedDate; } diff --git a/Shoko.Server/Models/Filters/Selectors/LowestAniDBRatingSelector.cs b/Shoko.Server/Models/Filters/Selectors/LowestAniDBRatingSelector.cs index d15d8f465..738795bcc 100644 --- a/Shoko.Server/Models/Filters/Selectors/LowestAniDBRatingSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/LowestAniDBRatingSelector.cs @@ -3,8 +3,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class LowestAniDBRatingSelector : INumberSelector +public class LowestAniDBRatingSelector : FilterExpression { - public bool UserDependent => false; - public Func Selector => f => Convert.ToDouble(f.LowestAniDBRating); + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override double Evaluate(IFilterable f) => Convert.ToDouble(f.LowestAniDBRating); } diff --git a/Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs b/Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs index 2caa023a9..9dbf4a537 100644 --- a/Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs @@ -3,8 +3,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class LowestUserRatingSelector : INumberSelector +public class LowestUserRatingSelector : FilterExpression { - public bool UserDependent => true; - public Func Selector => f => Convert.ToDouble(f.LowestUserRating); + public override bool TimeDependent => false; + public override bool UserDependent => true; + public override double Evaluate(IFilterable f) => Convert.ToDouble(f.LowestUserRating); } diff --git a/Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs b/Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs index ace5644b5..81f421d68 100644 --- a/Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs @@ -1,10 +1,10 @@ -using System; using Shoko.Server.Models.Filters.Interfaces; namespace Shoko.Server.Models.Filters.Selectors; -public class SubtitleLanguageCountSelector : INumberSelector +public class SubtitleLanguageCountSelector : FilterExpression { - public bool UserDependent => false; - public Func Selector => f => f.SubtitleLanguages.Count; + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override double Evaluate(IFilterable f) => f.SubtitleLanguages.Count; } diff --git a/Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs b/Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs index da841d5d9..923de467e 100644 --- a/Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs @@ -1,10 +1,10 @@ -using System; using Shoko.Server.Models.Filters.Interfaces; namespace Shoko.Server.Models.Filters.Selectors; -public class TotalEpisodeCountSelector : INumberSelector +public class TotalEpisodeCountSelector : FilterExpression { - public bool UserDependent => false; - public Func Selector => f => f.TotalEpisodeCount; + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override double Evaluate(IFilterable f) => f.TotalEpisodeCount; } diff --git a/Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs b/Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs index ae6cb3967..726c9dbb1 100644 --- a/Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs @@ -3,8 +3,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class WatchedDateSelector : IDateTimeSelector +public class WatchedDateSelector : FilterExpression { - public bool UserDependent => true; - public Func Selector => f => f.WatchedDate; + public override bool TimeDependent => false; + public override bool UserDependent => true; + public override DateTime? Evaluate(IFilterable f) => f.WatchedDate; } diff --git a/Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs b/Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs index 9d3cb7fc8..d6f6cf8e0 100644 --- a/Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs +++ b/Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs @@ -2,9 +2,10 @@ namespace Shoko.Server.Models.Filters.User; -public class HasCustomTagExpression : FilterExpression +public class HasCustomTagExpression : FilterExpression { public string Parameter { get; set; } + public override bool TimeDependent => false; public override bool UserDependent => true; public override bool Evaluate(IFilterable filterable) => filterable.CustomTags.Contains(Parameter); } diff --git a/Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs b/Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs index 70dcc347f..0119fc1b6 100644 --- a/Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs +++ b/Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs @@ -2,8 +2,9 @@ namespace Shoko.Server.Models.Filters.User; -public class HasPermanentUserVotesExpression : FilterExpression +public class HasPermanentUserVotesExpression : FilterExpression { + public override bool TimeDependent => false; public override bool UserDependent => true; public override bool Evaluate(IFilterable filterable) => filterable.HasPermanentVotes; } diff --git a/Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs b/Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs index 7065e8605..684479e50 100644 --- a/Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs +++ b/Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs @@ -2,8 +2,9 @@ namespace Shoko.Server.Models.Filters.User; -public class HasUnwatchedEpisodesExpression : FilterExpression +public class HasUnwatchedEpisodesExpression : FilterExpression { + public override bool TimeDependent => false; public override bool UserDependent => true; public override bool Evaluate(IFilterable filterable) => filterable.UnwatchedEpisodes > 0; } diff --git a/Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs b/Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs index c1580c134..1ed90e910 100644 --- a/Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs +++ b/Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs @@ -2,8 +2,9 @@ namespace Shoko.Server.Models.Filters.User; -public class HasUserVotesExpression : FilterExpression +public class HasUserVotesExpression : FilterExpression { + public override bool TimeDependent => false; public override bool UserDependent => true; public override bool Evaluate(IFilterable filterable) => filterable.HasVotes; } diff --git a/Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs b/Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs index 90dc89927..1b054dbe4 100644 --- a/Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs +++ b/Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs @@ -2,8 +2,9 @@ namespace Shoko.Server.Models.Filters.User; -public class HasWatchedEpisodesExpression : FilterExpression +public class HasWatchedEpisodesExpression : FilterExpression { + public override bool TimeDependent => false; public override bool UserDependent => true; public override bool Evaluate(IFilterable filterable) => filterable.WatchedEpisodes > 0; } diff --git a/Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs b/Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs index 6e742caca..cd2f045c1 100644 --- a/Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs +++ b/Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs @@ -2,8 +2,9 @@ namespace Shoko.Server.Models.Filters.User; -public class IsFavoriteExpression : FilterExpression +public class IsFavoriteExpression : FilterExpression { + public override bool TimeDependent => false; public override bool UserDependent => true; public override bool Evaluate(IFilterable filterable) => filterable.IsFavorite; } diff --git a/Shoko.Server/Models/Filters/readme.md b/Shoko.Server/Models/Filters/readme.md index ff7341272..fd6caacb3 100644 --- a/Shoko.Server/Models/Filters/readme.md +++ b/Shoko.Server/Models/Filters/readme.md @@ -1,14 +1,12 @@ -An Expression should be a test of sorts: a method that takes zero or more arguments and returns a true or false result. Expressions are stored with TPH discriminated on Type. Arguments should be mapped to Type + "Argument" + Index (StringArgument1) in the mapping. Expressions should not have more than 5 Arguments of each type. If it would, then it should be redesigned. For example, `And(HasTag("comedy"),HasTag("action"))`. This is to keep the database schema reasonable and expressions simple. I considered a one-to-many map for arguments, and making that automatic for navigation properties seemed like a lot of work when we can design around it. +An Expression is anything that transforms data: a method that takes zero or more arguments and returns a result. Expressions are stored with TPH discriminated on Type. Arguments should be mapped to Type + "Argument" + Index (StringArgument1) in the mapping. Expressions should not have more than 5 Arguments of each type. If it would, then it should be redesigned. For example, `And(HasTag("comedy"),HasTag("action"))`. This is to keep the database schema reasonable and expressions simple. I considered a one-to-many map for arguments, and making that automatic for navigation properties seemed like a lot of work when we can design around it. Acceptable database types (can be mapped to other CLR types like enums) for Arguments are: -- Integer - String -- Decimal +- Double (integers should be coerced to double for simplicity) - DateTime Default CLR Argument mappings: - FilterExpression via foreign key -- Selector via a type converter mapped via string (these are for things like comparators) FilterExpression is a single Expression, whether that's something like "Or" or "HasTag" in `And(Or(HasTag('comedy'), HasTag('action')), Not(HasTag('18 restricted')))`. Expressions should be the least amount of work possible. NAND can be expressed as `Not(And())`. Exceptions can be made if it would take more than 2 or 3 operations, such as XOR. An Exception was made for GreaterThanEqual, NotEqual, and LessThanEqual, only because most people would expect those to exist. From a8ee5c50e39edaf8340576fbdfe3467f51b96780 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sat, 26 Aug 2023 20:05:04 -0400 Subject: [PATCH 06/34] Filters: Work on User Dependent Filters. Make an SVR_AnimeSeries.ToFilterable() extension. Write a simple test to ensure that the generics work --- .../Models/Filters/FilterExtensions.cs | 131 ++++++++++++++++++ Shoko.Server/Models/Filters/Filterable.cs | 35 +++++ .../{User => Info}/HasCustomTagExpression.cs | 2 +- .../Models/Filters/Info/InSeasonExpression.cs | 9 +- .../Models/Filters/Info/InYearExpression.cs | 2 +- .../Filters/Info/IsFinishedExpression.cs | 2 +- .../Filters/Interfaces/IFilterExpression.cs | 7 +- .../Models/Filters/Interfaces/IFilterable.cs | 50 +------ .../Interfaces/IUserDependentFilterable.cs | 57 ++++++++ .../Selectors/HighestUserRatingSelector.cs | 4 +- .../Selectors/LastWatchedDateSelector.cs | 4 +- .../Selectors/LowestUserRatingSelector.cs | 4 +- .../Filters/Selectors/WatchedDateSelector.cs | 4 +- .../User/HasPermanentUserVotesExpression.cs | 4 +- .../User/HasUnwatchedEpisodesExpression.cs | 4 +- .../Filters/User/HasUserVotesExpression.cs | 4 +- .../User/HasWatchedEpisodesExpression.cs | 4 +- .../Filters/User/IsFavoriteExpression.cs | 4 +- .../MissingPermanentUserVotesExpression.cs | 10 ++ .../Filters/UserDependentFilterExpression.cs | 17 +++ .../Models/Filters/UserDependentFilterable.cs | 19 +++ Shoko.Server/Models/SVR_GroupFilter.cs | 3 +- Shoko.Tests/Shoko.Tests/FilterTests.cs | 35 +++++ 23 files changed, 339 insertions(+), 76 deletions(-) create mode 100644 Shoko.Server/Models/Filters/FilterExtensions.cs create mode 100644 Shoko.Server/Models/Filters/Filterable.cs rename Shoko.Server/Models/Filters/{User => Info}/HasCustomTagExpression.cs (89%) create mode 100644 Shoko.Server/Models/Filters/Interfaces/IUserDependentFilterable.cs create mode 100644 Shoko.Server/Models/Filters/User/MissingPermanentUserVotesExpression.cs create mode 100644 Shoko.Server/Models/Filters/UserDependentFilterExpression.cs create mode 100644 Shoko.Server/Models/Filters/UserDependentFilterable.cs create mode 100644 Shoko.Tests/Shoko.Tests/FilterTests.cs diff --git a/Shoko.Server/Models/Filters/FilterExtensions.cs b/Shoko.Server/Models/Filters/FilterExtensions.cs new file mode 100644 index 000000000..2a8020dce --- /dev/null +++ b/Shoko.Server/Models/Filters/FilterExtensions.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Shoko.Commons.Extensions; +using Shoko.Models.Enums; +using Shoko.Server.Models.Filters.Info; +using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Models.Filters.Logic; +using Shoko.Server.Models.Filters.User; +using Shoko.Server.Repositories; + +namespace Shoko.Server.Models.Filters; + +public static class FilterExtensions +{ + public static IFilterable ToFilterable(this SVR_AnimeSeries series) + { + var anime = series.GetAnime(); + // TODO optimize this a bunch. Lots of duplicate calls. Contract should be severely trimmed + var filterable = new Filterable + { + AirDate = anime?.AirDate, + MissingEpisodes = series.Contract?.MissingEpisodeCount ?? 0, + MissingEpisodesCollecting = series.Contract?.MissingEpisodeCountGroups ?? 0, + Tags = anime?.GetAllTags() ?? new HashSet(), + CustomTags = series.Contract?.AniDBAnime?.CustomTags?.Select(a => a.TagName).ToHashSet(StringComparer.InvariantCultureIgnoreCase) ?? new HashSet(), + Years = GetYears(series), + Seasons = anime.GetSeasons().ToHashSet(), + HasTvDBLink = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(), + HasMissingTvDbLink = HasMissingTvDBLink(series), + HasTMDbLink = series.Contract?.CrossRefAniDBMovieDB != null, + HasMissingTMDbLink = HasMissingTMDbLink(series), + HasTraktLink = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), + HasMissingTraktLink = !RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), + IsFinished = series.Contract?.AniDBAnime?.AniDBAnime?.EndDate != null && series.Contract.AniDBAnime.AniDBAnime.EndDate.Value < DateTime.Now, + LastAirDate = series.EndDate ?? series.GetAnimeEpisodes().Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), + AddedDate = series.DateTimeCreated, + LastAddedDate = series.GetVideoLocals().Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), + EpisodeCount = anime?.EpisodeCountNormal ?? 0, + TotalEpisodeCount = anime?.EpisodeCount ?? 0, + LowestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 10, 1, MidpointRounding.AwayFromZero), + HighestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 10, 1, MidpointRounding.AwayFromZero), + AnimeTypes = anime == null ? new HashSet() : new HashSet(StringComparer.InvariantCultureIgnoreCase) { ((AnimeType)anime.AnimeType).ToString() }, + VideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet(), + AudioLanguages = series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet(), + SubtitleLanguages = series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet() + }; + + return filterable; + } + + private static IReadOnlySet GetYears(SVR_AnimeSeries series) + { + var contract = series.Contract?.AniDBAnime; + var startyear = contract?.AniDBAnime?.BeginYear ?? 0; + if (startyear == 0) return new HashSet(); + var endyear = contract?.AniDBAnime?.EndYear ?? 0; + if (endyear == 0) endyear = DateTime.Today.Year; + if (endyear < startyear) endyear = startyear; + if (startyear == endyear) return new HashSet { startyear }; + return new HashSet(Enumerable.Range(startyear, endyear - startyear + 1).Where(contract.IsInYear)); + } + + private static bool HasMissingTMDbLink(SVR_AnimeSeries series) + { + var anime = series.GetAnime(); + if (anime == null) return false; + // TODO update this with the TMDB refactor + if (anime.AnimeType != (int)AnimeType.Movie) return false; + if (anime.Restricted > 0) return false; + return series.Contract?.CrossRefAniDBMovieDB == null; + } + + private static bool HasMissingTvDBLink(SVR_AnimeSeries series) + { + var anime = series.GetAnime(); + if (anime == null) return false; + if (anime.AnimeType == (int)AnimeType.Movie) return false; + if (anime.Restricted > 0) return false; + return !RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(); + } + + public static UserDependentFilterable ToUserDependentFilterable(this SVR_AnimeSeries series, int userID) + { + var anime = series.GetAnime(); + var user = series.GetUserRecord(userID); + var vote = anime?.UserVote; + var watchedDates = series.GetVideoLocals().Select(a => a.GetUserRecord(userID)?.WatchedDate).Where(a => a != null).OrderBy(a => a).ToList(); + var filterable = new UserDependentFilterable + { + AirDate = anime?.AirDate, + MissingEpisodes = series.Contract?.MissingEpisodeCount ?? 0, + MissingEpisodesCollecting = series.Contract?.MissingEpisodeCountGroups ?? 0, + Tags = anime?.GetAllTags() ?? new HashSet(), + CustomTags = series.Contract?.AniDBAnime?.CustomTags?.Select(a => a.TagName).ToHashSet(StringComparer.InvariantCultureIgnoreCase) ?? new HashSet(), + Years = GetYears(series), + Seasons = anime?.GetSeasons().ToHashSet(), + HasTvDBLink = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(), + HasMissingTvDbLink = HasMissingTvDBLink(series), + HasTMDbLink = series.Contract?.CrossRefAniDBMovieDB != null, + HasMissingTMDbLink = HasMissingTMDbLink(series), + HasTraktLink = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), + HasMissingTraktLink = !RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), + IsFinished = series.Contract?.AniDBAnime?.AniDBAnime?.EndDate != null && series.Contract.AniDBAnime.AniDBAnime.EndDate.Value < DateTime.Now, + LastAirDate = series.EndDate ?? series.GetAnimeEpisodes().Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), + AddedDate = series.DateTimeCreated, + LastAddedDate = series.GetVideoLocals().Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), + EpisodeCount = anime?.EpisodeCountNormal ?? 0, + TotalEpisodeCount = anime?.EpisodeCount ?? 0, + LowestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 10, 1, MidpointRounding.AwayFromZero), + HighestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 10, 1, MidpointRounding.AwayFromZero), + AnimeTypes = anime == null ? new HashSet() : new HashSet(StringComparer.InvariantCultureIgnoreCase) { ((AnimeType)anime.AnimeType).ToString() }, + VideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet(), + AudioLanguages = series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet(), + SubtitleLanguages = series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet(), + IsFavorite = false, + WatchedEpisodes = user?.WatchedCount ?? 0, + UnwatchedEpisodes = (anime?.EpisodeCount ?? 0) - (user?.WatchedCount ?? 0), + LowestUserRating = vote?.VoteValue ?? 0, + HighestUserRating = vote?.VoteValue ?? 0, + HasVotes = vote != null, + HasPermanentVotes = vote is { VoteType: (int)AniDBVoteType.Anime }, + MissingPermanentVotes = vote is not { VoteType: (int)AniDBVoteType.Anime } && anime?.EndDate != null && anime.EndDate > DateTime.Now, + WatchedDate = watchedDates.FirstOrDefault(), + LastWatchedDate = watchedDates.LastOrDefault() + }; + + return filterable; + } +} diff --git a/Shoko.Server/Models/Filters/Filterable.cs b/Shoko.Server/Models/Filters/Filterable.cs new file mode 100644 index 000000000..308f4f0d5 --- /dev/null +++ b/Shoko.Server/Models/Filters/Filterable.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Shoko.Models.Enums; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters; + +public class Filterable : IFilterable +{ + public int MissingEpisodes { get; init; } + public int MissingEpisodesCollecting { get; init; } + public IReadOnlySet Tags { get; init; } + public IReadOnlySet CustomTags { get; init; } + public IReadOnlySet Years { get; init; } + public IReadOnlySet<(int year, AnimeSeason season)> Seasons { get; init; } + public bool HasTvDBLink { get; init; } + public bool HasMissingTvDbLink { get; init; } + public bool HasTMDbLink { get; init; } + public bool HasMissingTMDbLink { get; init; } + public bool HasTraktLink { get; init; } + public bool HasMissingTraktLink { get; init; } + public bool IsFinished { get; init; } + public DateTime? AirDate { get; init; } + public DateTime? LastAirDate { get; init; } + public DateTime AddedDate { get; init; } + public DateTime LastAddedDate { get; init; } + public int EpisodeCount { get; init; } + public int TotalEpisodeCount { get; init; } + public decimal LowestAniDBRating { get; init; } + public decimal HighestAniDBRating { get; init; } + public IReadOnlySet VideoSources { get; init; } + public IReadOnlySet AnimeTypes { get; init; } + public IReadOnlySet AudioLanguages { get; init; } + public IReadOnlySet SubtitleLanguages { get; init; } +} diff --git a/Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs b/Shoko.Server/Models/Filters/Info/HasCustomTagExpression.cs similarity index 89% rename from Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs rename to Shoko.Server/Models/Filters/Info/HasCustomTagExpression.cs index d6f6cf8e0..7c45f4949 100644 --- a/Shoko.Server/Models/Filters/User/HasCustomTagExpression.cs +++ b/Shoko.Server/Models/Filters/Info/HasCustomTagExpression.cs @@ -1,6 +1,6 @@ using Shoko.Server.Models.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.User; +namespace Shoko.Server.Models.Filters.Info; public class HasCustomTagExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Info/InSeasonExpression.cs b/Shoko.Server/Models/Filters/Info/InSeasonExpression.cs index 58a7ac96e..07ac0facc 100644 --- a/Shoko.Server/Models/Filters/Info/InSeasonExpression.cs +++ b/Shoko.Server/Models/Filters/Info/InSeasonExpression.cs @@ -1,14 +1,13 @@ +using Shoko.Models.Enums; using Shoko.Server.Models.Filters.Interfaces; namespace Shoko.Server.Models.Filters.Info; -/// -/// Parameter is a season followed by year, ie Winter 2022 -/// public class InSeasonExpression : FilterExpression { - public string Parameter { get; set; } + public int Year { get; set; } + public AnimeSeason Season { get; set; } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.Seasons.Contains(Parameter); + public override bool Evaluate(IFilterable filterable) => filterable.Seasons.Contains((Year, Season)); } diff --git a/Shoko.Server/Models/Filters/Info/InYearExpression.cs b/Shoko.Server/Models/Filters/Info/InYearExpression.cs index bf6a7637d..eac6af19e 100644 --- a/Shoko.Server/Models/Filters/Info/InYearExpression.cs +++ b/Shoko.Server/Models/Filters/Info/InYearExpression.cs @@ -5,7 +5,7 @@ namespace Shoko.Server.Models.Filters.Info; public class InYearExpression : FilterExpression { public int Parameter { get; set; } - public override bool TimeDependent => false; + public override bool TimeDependent => true; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.Years.Contains(Parameter); } diff --git a/Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs b/Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs index 4cbf1b07d..44d4a9e7c 100644 --- a/Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs +++ b/Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs @@ -4,7 +4,7 @@ namespace Shoko.Server.Models.Filters.Info; public class IsFinishedExpression : FilterExpression { - public override bool TimeDependent => false; + public override bool TimeDependent => true; public override bool UserDependent => false; public override bool Evaluate(IFilterable filterable) => filterable.IsFinished; } diff --git a/Shoko.Server/Models/Filters/Interfaces/IFilterExpression.cs b/Shoko.Server/Models/Filters/Interfaces/IFilterExpression.cs index 5df33f2e7..f00c33866 100644 --- a/Shoko.Server/Models/Filters/Interfaces/IFilterExpression.cs +++ b/Shoko.Server/Models/Filters/Interfaces/IFilterExpression.cs @@ -6,7 +6,12 @@ public interface IFilterExpression bool UserDependent { get; } } -public interface IFilterExpression : IFilterExpression +public interface IFilterExpression { T Evaluate(IFilterable f); } + +public interface IUserDependentFilterExpression +{ + T Evaluate(IUserDependentFilterable f); +} diff --git a/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs b/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs index 6e90d4781..0ba07bd7a 100644 --- a/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs +++ b/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs @@ -1,15 +1,11 @@ using System; using System.Collections.Generic; +using Shoko.Models.Enums; namespace Shoko.Server.Models.Filters.Interfaces; public interface IFilterable { - /// - /// Probably will be removed in the future. Custom Tags would handle this better - /// - bool IsFavorite { get; } - /// /// Number of Missing Episodes /// @@ -20,23 +16,13 @@ public interface IFilterable /// int MissingEpisodesCollecting { get; } - /// - /// The number of episodes watched - /// - int WatchedEpisodes { get; } - - /// - /// The number of episodes that have not been watched - /// - int UnwatchedEpisodes { get; } - /// /// All of the tags /// IReadOnlySet Tags { get; } /// - /// All of the Custom Tags + /// All of the custom tags /// IReadOnlySet CustomTags { get; } @@ -48,7 +34,7 @@ public interface IFilterable /// /// The seasons this aired in /// - IReadOnlySet Seasons { get; } + IReadOnlySet<(int year, AnimeSeason season)> Seasons { get; } /// /// Has at least one TvDB Link @@ -85,16 +71,6 @@ public interface IFilterable /// bool IsFinished { get; } - /// - /// Has any user votes - /// - bool HasVotes { get; } - - /// - /// Has permanent (after finishing) user votes - /// - bool HasPermanentVotes { get; } - /// /// First Air Date /// @@ -105,16 +81,6 @@ public interface IFilterable /// DateTime? LastAirDate { get; } - /// - /// First Watched Date - /// - DateTime? WatchedDate { get; } - - /// - /// Latest Watched Date - /// - DateTime? LastWatchedDate { get; } - /// /// When it was first added to the collection /// @@ -145,16 +111,6 @@ public interface IFilterable /// decimal HighestAniDBRating { get; } - /// - /// Lowest User Rating (0-10) - /// - decimal LowestUserRating { get; } - - /// - /// Highest User Rating (0-10) - /// - decimal HighestUserRating { get; } - /// /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. /// diff --git a/Shoko.Server/Models/Filters/Interfaces/IUserDependentFilterable.cs b/Shoko.Server/Models/Filters/Interfaces/IUserDependentFilterable.cs new file mode 100644 index 000000000..69bcbe3b9 --- /dev/null +++ b/Shoko.Server/Models/Filters/Interfaces/IUserDependentFilterable.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; + +namespace Shoko.Server.Models.Filters.Interfaces; + +public interface IUserDependentFilterable : IFilterable +{ + /// + /// Probably will be removed in the future. Custom Tags would handle this better + /// + bool IsFavorite { get; } + + /// + /// The number of episodes watched + /// + int WatchedEpisodes { get; } + + /// + /// The number of episodes that have not been watched + /// + int UnwatchedEpisodes { get; } + + /// + /// Has any user votes + /// + bool HasVotes { get; } + + /// + /// Has permanent (after finishing) user votes + /// + bool HasPermanentVotes { get; } + + /// + /// Has permanent (after finishing) user votes + /// + bool MissingPermanentVotes { get; } + + /// + /// First Watched Date + /// + DateTime? WatchedDate { get; } + + /// + /// Latest Watched Date + /// + DateTime? LastWatchedDate { get; } + + /// + /// Lowest User Rating (0-10) + /// + decimal LowestUserRating { get; } + + /// + /// Highest User Rating (0-10) + /// + decimal HighestUserRating { get; } +} diff --git a/Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs b/Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs index 6e77faf43..6ba6c7fa0 100644 --- a/Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs @@ -3,9 +3,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class HighestUserRatingSelector : FilterExpression +public class HighestUserRatingSelector : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override double Evaluate(IFilterable f) => Convert.ToDouble(f.HighestUserRating); + public override double Evaluate(IUserDependentFilterable f) => Convert.ToDouble(f.HighestUserRating); } diff --git a/Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs b/Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs index 0d032086b..564a12a9f 100644 --- a/Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs @@ -3,9 +3,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class LastWatchedDateSelector : FilterExpression +public class LastWatchedDateSelector : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override DateTime? Evaluate(IFilterable f) => f.LastWatchedDate; + public override DateTime? Evaluate(IUserDependentFilterable f) => f.LastWatchedDate; } diff --git a/Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs b/Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs index 9dbf4a537..c35146005 100644 --- a/Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs @@ -3,9 +3,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class LowestUserRatingSelector : FilterExpression +public class LowestUserRatingSelector : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override double Evaluate(IFilterable f) => Convert.ToDouble(f.LowestUserRating); + public override double Evaluate(IUserDependentFilterable f) => Convert.ToDouble(f.LowestUserRating); } diff --git a/Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs b/Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs index 726c9dbb1..83396a7f5 100644 --- a/Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs @@ -3,9 +3,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class WatchedDateSelector : FilterExpression +public class WatchedDateSelector : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override DateTime? Evaluate(IFilterable f) => f.WatchedDate; + public override DateTime? Evaluate(IUserDependentFilterable f) => f.WatchedDate; } diff --git a/Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs b/Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs index 0119fc1b6..eea3d41e5 100644 --- a/Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs +++ b/Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs @@ -2,9 +2,9 @@ namespace Shoko.Server.Models.Filters.User; -public class HasPermanentUserVotesExpression : FilterExpression +public class HasPermanentUserVotesExpression : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(IFilterable filterable) => filterable.HasPermanentVotes; + public override bool Evaluate(IUserDependentFilterable filterable) => filterable.HasPermanentVotes; } diff --git a/Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs b/Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs index 684479e50..e3a37a411 100644 --- a/Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs +++ b/Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs @@ -2,9 +2,9 @@ namespace Shoko.Server.Models.Filters.User; -public class HasUnwatchedEpisodesExpression : FilterExpression +public class HasUnwatchedEpisodesExpression : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(IFilterable filterable) => filterable.UnwatchedEpisodes > 0; + public override bool Evaluate(IUserDependentFilterable filterable) => filterable.UnwatchedEpisodes > 0; } diff --git a/Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs b/Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs index 1ed90e910..6064e30b5 100644 --- a/Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs +++ b/Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs @@ -2,9 +2,9 @@ namespace Shoko.Server.Models.Filters.User; -public class HasUserVotesExpression : FilterExpression +public class HasUserVotesExpression : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(IFilterable filterable) => filterable.HasVotes; + public override bool Evaluate(IUserDependentFilterable filterable) => filterable.HasVotes; } diff --git a/Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs b/Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs index 1b054dbe4..75d193d01 100644 --- a/Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs +++ b/Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs @@ -2,9 +2,9 @@ namespace Shoko.Server.Models.Filters.User; -public class HasWatchedEpisodesExpression : FilterExpression +public class HasWatchedEpisodesExpression : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(IFilterable filterable) => filterable.WatchedEpisodes > 0; + public override bool Evaluate(IUserDependentFilterable filterable) => filterable.WatchedEpisodes > 0; } diff --git a/Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs b/Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs index cd2f045c1..45c7d0e5c 100644 --- a/Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs +++ b/Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs @@ -2,9 +2,9 @@ namespace Shoko.Server.Models.Filters.User; -public class IsFavoriteExpression : FilterExpression +public class IsFavoriteExpression : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(IFilterable filterable) => filterable.IsFavorite; + public override bool Evaluate(IUserDependentFilterable filterable) => filterable.IsFavorite; } diff --git a/Shoko.Server/Models/Filters/User/MissingPermanentUserVotesExpression.cs b/Shoko.Server/Models/Filters/User/MissingPermanentUserVotesExpression.cs new file mode 100644 index 000000000..44424a650 --- /dev/null +++ b/Shoko.Server/Models/Filters/User/MissingPermanentUserVotesExpression.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.User; + +public class MissingPermanentUserVotesExpression : UserDependentFilterExpression +{ + public override bool TimeDependent => true; + public override bool UserDependent => true; + public override bool Evaluate(IUserDependentFilterable filterable) => filterable.MissingPermanentVotes; +} diff --git a/Shoko.Server/Models/Filters/UserDependentFilterExpression.cs b/Shoko.Server/Models/Filters/UserDependentFilterExpression.cs new file mode 100644 index 000000000..a3da711ec --- /dev/null +++ b/Shoko.Server/Models/Filters/UserDependentFilterExpression.cs @@ -0,0 +1,17 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters; + +public abstract class UserDependentFilterExpression : FilterExpression, IUserDependentFilterExpression +{ + public override T Evaluate(IFilterable f) + { + if (UserDependent && f is not IUserDependentFilterable) + throw new ArgumentException("User Dependent Filter was given an IFilterable, rather than an IUserDependentFilterable"); + + return Evaluate((IUserDependentFilterable)f); + } + + public abstract T Evaluate(IUserDependentFilterable f); +} diff --git a/Shoko.Server/Models/Filters/UserDependentFilterable.cs b/Shoko.Server/Models/Filters/UserDependentFilterable.cs new file mode 100644 index 000000000..169bbd256 --- /dev/null +++ b/Shoko.Server/Models/Filters/UserDependentFilterable.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters; + +public class UserDependentFilterable : Filterable, IUserDependentFilterable +{ + public bool IsFavorite { get; init; } + public int WatchedEpisodes { get; init; } + public int UnwatchedEpisodes { get; init; } + public bool HasVotes { get; init; } + public bool HasPermanentVotes { get; init; } + public bool MissingPermanentVotes { get; init; } + public DateTime? WatchedDate { get; init; } + public DateTime? LastWatchedDate { get; init; } + public decimal LowestUserRating { get; init; } + public decimal HighestUserRating { get; init; } +} diff --git a/Shoko.Server/Models/SVR_GroupFilter.cs b/Shoko.Server/Models/SVR_GroupFilter.cs index 207799571..20bc9e8a6 100644 --- a/Shoko.Server/Models/SVR_GroupFilter.cs +++ b/Shoko.Server/Models/SVR_GroupFilter.cs @@ -1680,8 +1680,7 @@ private bool EvaluateConditions(CL_AnimeSeries_User contractSerie, GroupFilterCo break; case GroupFilterConditionType.UserVoted: - var voted = contractSerie.AniDBAnime.UserVote != null && - contractSerie.AniDBAnime.UserVote.VoteType == (int)AniDBVoteType.Anime; + var voted = contractSerie.AniDBAnime.UserVote is { VoteType: (int)AniDBVoteType.Anime }; if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && !voted) { return false; diff --git a/Shoko.Tests/Shoko.Tests/FilterTests.cs b/Shoko.Tests/Shoko.Tests/FilterTests.cs new file mode 100644 index 000000000..7678a2b81 --- /dev/null +++ b/Shoko.Tests/Shoko.Tests/FilterTests.cs @@ -0,0 +1,35 @@ +using Shoko.Server.Models.Filters.Info; +using Shoko.Server.Models.Filters.Logic; +using Shoko.Server.Models.Filters.User; +using Xunit; + +namespace Shoko.Tests; + +public class FilterTests +{ + [Fact] + public void UserDependentInitTest() + { + var top = new AndExpression + { + Left = new AndExpression + { + Left = new HasTagExpression + { + Parameter = "comedy" + }, + Right = new NotExpression + { + Left = new HasTagExpression + { + Parameter = "18 restricted" + } + } + }, + Right = new HasWatchedEpisodesExpression() + }; + + Assert.True(top.UserDependent); + Assert.False(top.TimeDependent); + } +} From e9272f08a933f8ba7861dadab531a6210e625620 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sat, 26 Aug 2023 23:50:39 -0400 Subject: [PATCH 07/34] Filters: Add Group Extensions. Add better Tests --- .../Models/Filters/FilterExtensions.cs | 203 ++++++++++++++---- Shoko.Server/Models/Filters/Filterable.cs | 25 ++- Shoko.Tests/Shoko.Tests/FilterTests.cs | 131 ++++++++++- 3 files changed, 309 insertions(+), 50 deletions(-) diff --git a/Shoko.Server/Models/Filters/FilterExtensions.cs b/Shoko.Server/Models/Filters/FilterExtensions.cs index 2a8020dce..22f8dd58c 100644 --- a/Shoko.Server/Models/Filters/FilterExtensions.cs +++ b/Shoko.Server/Models/Filters/FilterExtensions.cs @@ -1,14 +1,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using Shoko.Commons.Extensions; using Shoko.Models.Enums; -using Shoko.Server.Models.Filters.Info; using Shoko.Server.Models.Filters.Interfaces; -using Shoko.Server.Models.Filters.Logic; -using Shoko.Server.Models.Filters.User; +using Shoko.Server.Providers.AniDB; using Shoko.Server.Repositories; +using AnimeType = Shoko.Models.Enums.AnimeType; namespace Shoko.Server.Models.Filters; @@ -39,8 +37,8 @@ public static IFilterable ToFilterable(this SVR_AnimeSeries series) LastAddedDate = series.GetVideoLocals().Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), EpisodeCount = anime?.EpisodeCountNormal ?? 0, TotalEpisodeCount = anime?.EpisodeCount ?? 0, - LowestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 10, 1, MidpointRounding.AwayFromZero), - HighestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 10, 1, MidpointRounding.AwayFromZero), + LowestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero), + HighestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero), AnimeTypes = anime == null ? new HashSet() : new HashSet(StringComparer.InvariantCultureIgnoreCase) { ((AnimeType)anime.AnimeType).ToString() }, VideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet(), AudioLanguages = series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet(), @@ -50,37 +48,6 @@ public static IFilterable ToFilterable(this SVR_AnimeSeries series) return filterable; } - private static IReadOnlySet GetYears(SVR_AnimeSeries series) - { - var contract = series.Contract?.AniDBAnime; - var startyear = contract?.AniDBAnime?.BeginYear ?? 0; - if (startyear == 0) return new HashSet(); - var endyear = contract?.AniDBAnime?.EndYear ?? 0; - if (endyear == 0) endyear = DateTime.Today.Year; - if (endyear < startyear) endyear = startyear; - if (startyear == endyear) return new HashSet { startyear }; - return new HashSet(Enumerable.Range(startyear, endyear - startyear + 1).Where(contract.IsInYear)); - } - - private static bool HasMissingTMDbLink(SVR_AnimeSeries series) - { - var anime = series.GetAnime(); - if (anime == null) return false; - // TODO update this with the TMDB refactor - if (anime.AnimeType != (int)AnimeType.Movie) return false; - if (anime.Restricted > 0) return false; - return series.Contract?.CrossRefAniDBMovieDB == null; - } - - private static bool HasMissingTvDBLink(SVR_AnimeSeries series) - { - var anime = series.GetAnime(); - if (anime == null) return false; - if (anime.AnimeType == (int)AnimeType.Movie) return false; - if (anime.Restricted > 0) return false; - return !RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(); - } - public static UserDependentFilterable ToUserDependentFilterable(this SVR_AnimeSeries series, int userID) { var anime = series.GetAnime(); @@ -108,8 +75,8 @@ public static UserDependentFilterable ToUserDependentFilterable(this SVR_AnimeSe LastAddedDate = series.GetVideoLocals().Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), EpisodeCount = anime?.EpisodeCountNormal ?? 0, TotalEpisodeCount = anime?.EpisodeCount ?? 0, - LowestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 10, 1, MidpointRounding.AwayFromZero), - HighestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 10, 1, MidpointRounding.AwayFromZero), + LowestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero), + HighestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero), AnimeTypes = anime == null ? new HashSet() : new HashSet(StringComparer.InvariantCultureIgnoreCase) { ((AnimeType)anime.AnimeType).ToString() }, VideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet(), AudioLanguages = series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet(), @@ -128,4 +95,162 @@ public static UserDependentFilterable ToUserDependentFilterable(this SVR_AnimeSe return filterable; } + + private static HashSet GetYears(SVR_AnimeSeries series) + { + var contract = series.Contract?.AniDBAnime; + var startyear = contract?.AniDBAnime?.BeginYear ?? 0; + if (startyear == 0) return new HashSet(); + var endyear = contract?.AniDBAnime?.EndYear ?? 0; + if (endyear == 0) endyear = DateTime.Today.Year; + if (endyear < startyear) endyear = startyear; + if (startyear == endyear) return new HashSet { startyear }; + return new HashSet(Enumerable.Range(startyear, endyear - startyear + 1).Where(contract.IsInYear)); + } + + private static bool HasMissingTMDbLink(SVR_AnimeSeries series) + { + var anime = series.GetAnime(); + if (anime == null) return false; + // TODO update this with the TMDB refactor + if (anime.AnimeType != (int)AnimeType.Movie) return false; + if (anime.Restricted > 0) return false; + return series.Contract?.CrossRefAniDBMovieDB == null; + } + + private static bool HasMissingTvDBLink(SVR_AnimeSeries series) + { + var anime = series.GetAnime(); + if (anime == null) return false; + if (anime.AnimeType == (int)AnimeType.Movie) return false; + if (anime.Restricted > 0) return false; + return !RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(); + } + + public static IFilterable ToFilterable(this SVR_AnimeGroup group) + { + var series = group.GetAllSeries(); + var hasTrakt = series.All(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()); + // TODO optimize this a bunch. Lots of duplicate calls. Contract should be severely trimmed + var filterable = new Filterable + { + AirDate = group.Contract.Stat_AirDate_Min, + LastAirDate = group.Contract?.Stat_EndDate ?? group.GetAllSeries().SelectMany(a => a.GetAnimeEpisodes()).Select(a => + a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), + MissingEpisodes = group.Contract?.MissingEpisodeCount ?? 0, + MissingEpisodesCollecting = group.Contract?.MissingEpisodeCountGroups ?? 0, + Tags = group.Contract?.Stat_AllTags ?? new HashSet(), + CustomTags = group.Contract?.Stat_AllCustomTags ?? new HashSet(), + Years = group.Contract?.Stat_AllYears ?? new HashSet(), + Seasons = group.Contract?.Stat_AllSeasons.Select(a => + { + var parts = a.Split(' '); + return (int.Parse(parts[1]), Enum.Parse(parts[0])); + }).ToHashSet(), + HasTvDBLink = series.All(a => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(a.AniDB_ID).Any()), + HasMissingTvDbLink = HasMissingTvDBLink(group), + HasTMDbLink = group.Contract?.Stat_HasMovieDBLink ?? false, + HasMissingTMDbLink = HasMissingTMDbLink(group), + HasTraktLink = hasTrakt, + HasMissingTraktLink = !hasTrakt, + IsFinished = group.Contract?.Stat_HasFinishedAiring ?? false, + AddedDate = group.DateTimeCreated, + LastAddedDate = series.SelectMany(a => a.GetVideoLocals()).Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), + EpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCountNormal ?? 0), + TotalEpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCount ?? 0), + LowestAniDBRating = group.Anime.DefaultIfEmpty().Min(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), + HighestAniDBRating = group.Anime.DefaultIfEmpty().Max(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), + AnimeTypes = new HashSet(group.Anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), + VideoSources = group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet(), + AudioLanguages = group.Contract?.Stat_AudioLanguages ?? new HashSet(), + SubtitleLanguages = group.Contract?.Stat_SubtitleLanguages ?? new HashSet() + }; + + return filterable; + } + + public static IFilterable ToUserDependentFilterable(this SVR_AnimeGroup group, int userID) + { + var series = group.GetAllSeries(); + var hasTrakt = series.All(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()); + var user = group.GetUserRecord(userID); + var vote = group.Anime.Select(a => a.UserVote).Where(a => a is { VoteType: (int)VoteType.AnimePermanent or (int)VoteType.AnimeTemporary }) + .Select(a => a.VoteValue).OrderBy(a => a).ToList(); + var hasPermanent = group.Anime.Select(a => a.UserVote).Any(a => a is { VoteType: (int)VoteType.AnimePermanent }); + var missingPermanent = + group.Anime.Any(a => a.UserVote is not { VoteType: (int)VoteType.AnimePermanent } && a.EndDate != null && a.EndDate > DateTime.Now); + var watchedDates = series.SelectMany(a => a.GetVideoLocals()).Select(a => a.GetUserRecord(userID)?.WatchedDate).Where(a => a != null).OrderBy(a => a) + .ToList(); + // TODO optimize this a bunch. Lots of duplicate calls. Contract should be severely trimmed + var filterable = new UserDependentFilterable + { + AirDate = group.Contract.Stat_AirDate_Min, + LastAirDate = group.Contract?.Stat_EndDate ?? group.GetAllSeries().SelectMany(a => a.GetAnimeEpisodes()).Select(a => + a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), + MissingEpisodes = group.Contract?.MissingEpisodeCount ?? 0, + MissingEpisodesCollecting = group.Contract?.MissingEpisodeCountGroups ?? 0, + Tags = group.Contract?.Stat_AllTags ?? new HashSet(), + CustomTags = group.Contract?.Stat_AllCustomTags ?? new HashSet(), + Years = group.Contract?.Stat_AllYears ?? new HashSet(), + Seasons = group.Contract?.Stat_AllSeasons.Select(a => + { + var parts = a.Split(' '); + return (int.Parse(parts[1]), Enum.Parse(parts[0])); + }).ToHashSet(), + HasTvDBLink = series.All(a => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(a.AniDB_ID).Any()), + HasMissingTvDbLink = HasMissingTvDBLink(group), + HasTMDbLink = group.Contract?.Stat_HasMovieDBLink ?? false, + HasMissingTMDbLink = HasMissingTMDbLink(group), + HasTraktLink = hasTrakt, + HasMissingTraktLink = !hasTrakt, + IsFinished = group.Contract?.Stat_HasFinishedAiring ?? false, + AddedDate = group.DateTimeCreated, + LastAddedDate = series.SelectMany(a => a.GetVideoLocals()).Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), + EpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCountNormal ?? 0), + TotalEpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCount ?? 0), + LowestAniDBRating = group.Anime.DefaultIfEmpty().Min(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), + HighestAniDBRating = group.Anime.DefaultIfEmpty().Max(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), + AnimeTypes = new HashSet(group.Anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), + VideoSources = group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet(), + AudioLanguages = group.Contract?.Stat_AudioLanguages ?? new HashSet(), + SubtitleLanguages = group.Contract?.Stat_SubtitleLanguages ?? new HashSet(), + IsFavorite = user?.IsFave == 1, + WatchedEpisodes = user?.WatchedCount ?? 0, + UnwatchedEpisodes = user?.UnwatchedEpisodeCount ?? 0, + LowestUserRating = vote.FirstOrDefault(), + HighestUserRating = vote.LastOrDefault(), + HasVotes = vote.Any(), + HasPermanentVotes = hasPermanent, + MissingPermanentVotes = missingPermanent, + WatchedDate = watchedDates.FirstOrDefault(), + LastWatchedDate = watchedDates.LastOrDefault() + }; + + return filterable; + } + + private static bool HasMissingTMDbLink(SVR_AnimeGroup group) + { + return group.GetAllSeries().Any(series => + { + var anime = series.GetAnime(); + if (anime == null) return false; + // TODO update this with the TMDB refactor + if (anime.AnimeType != (int)AnimeType.Movie) return false; + if (anime.Restricted > 0) return false; + return series.Contract?.CrossRefAniDBMovieDB == null; + }); + } + + private static bool HasMissingTvDBLink(SVR_AnimeGroup group) + { + return group.GetAllSeries().Any(series => + { + var anime = series.GetAnime(); + if (anime == null) return false; + if (anime.AnimeType == (int)AnimeType.Movie) return false; + if (anime.Restricted > 0) return false; + return !RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(); + }); + } } diff --git a/Shoko.Server/Models/Filters/Filterable.cs b/Shoko.Server/Models/Filters/Filterable.cs index 308f4f0d5..4bf83d544 100644 --- a/Shoko.Server/Models/Filters/Filterable.cs +++ b/Shoko.Server/Models/Filters/Filterable.cs @@ -7,12 +7,17 @@ namespace Shoko.Server.Models.Filters; public class Filterable : IFilterable { + // The explicit implementations of IReadOnlySet make it easier to deserialize into public int MissingEpisodes { get; init; } public int MissingEpisodesCollecting { get; init; } - public IReadOnlySet Tags { get; init; } - public IReadOnlySet CustomTags { get; init; } - public IReadOnlySet Years { get; init; } - public IReadOnlySet<(int year, AnimeSeason season)> Seasons { get; init; } + public HashSet Tags { get; init; } + IReadOnlySet IFilterable.Tags => Tags; + public HashSet CustomTags { get; init; } + IReadOnlySet IFilterable.CustomTags => CustomTags; + public HashSet Years { get; init; } + IReadOnlySet IFilterable.Years => Years; + public HashSet<(int year, AnimeSeason season)> Seasons { get; init; } + IReadOnlySet<(int year, AnimeSeason season)> IFilterable.Seasons => Seasons; public bool HasTvDBLink { get; init; } public bool HasMissingTvDbLink { get; init; } public bool HasTMDbLink { get; init; } @@ -28,8 +33,12 @@ public class Filterable : IFilterable public int TotalEpisodeCount { get; init; } public decimal LowestAniDBRating { get; init; } public decimal HighestAniDBRating { get; init; } - public IReadOnlySet VideoSources { get; init; } - public IReadOnlySet AnimeTypes { get; init; } - public IReadOnlySet AudioLanguages { get; init; } - public IReadOnlySet SubtitleLanguages { get; init; } + public HashSet VideoSources { get; init; } + IReadOnlySet IFilterable.VideoSources => VideoSources; + public HashSet AnimeTypes { get; init; } + IReadOnlySet IFilterable.AnimeTypes => AnimeTypes; + public HashSet AudioLanguages { get; init; } + IReadOnlySet IFilterable.AudioLanguages => AudioLanguages; + public HashSet SubtitleLanguages { get; init; } + IReadOnlySet IFilterable.SubtitleLanguages => SubtitleLanguages; } diff --git a/Shoko.Tests/Shoko.Tests/FilterTests.cs b/Shoko.Tests/Shoko.Tests/FilterTests.cs index 7678a2b81..fe0b0bd17 100644 --- a/Shoko.Tests/Shoko.Tests/FilterTests.cs +++ b/Shoko.Tests/Shoko.Tests/FilterTests.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Shoko.Server.Models.Filters; using Shoko.Server.Models.Filters.Info; using Shoko.Server.Models.Filters.Logic; using Shoko.Server.Models.Filters.User; @@ -7,8 +11,129 @@ namespace Shoko.Tests; public class FilterTests { - [Fact] - public void UserDependentInitTest() + private const string GroupFilterableString = + "{\"MissingEpisodes\":0,\"MissingEpisodesCollecting\":0,\"Tags\":[\"Earth\",\"Japan\",\"Asia\",\"friendship\",\"daily life\",\"high school\",\"school life\",\"comedy\",\"Kyoto\",\"dynamic\",\"themes\",\"source material\",\"setting\",\"elements\",\"place\",\"manga\",\"funny expressions\",\"storytelling\",\"character driven\",\"facial distortion\",\"narration\",\"origin\",\"episodic\",\"Japanese production\"],\"CustomTags\":[],\"Years\":[2022],\"Seasons\":[{\"Item1\":2022,\"Item2\":1}],\"HasTvDBLink\":true,\"HasMissingTvDbLink\":false,\"HasTMDbLink\":true,\"HasMissingTMDbLink\":false,\"HasTraktLink\":false,\"HasMissingTraktLink\":true,\"IsFinished\":true,\"AirDate\":\"2022-04-06T20:00:00\",\"LastAirDate\":\"2022-06-22T20:00:00\",\"AddedDate\":\"2023-05-05T14:42:24.4477733\",\"LastAddedDate\":\"2023-05-05T13:37:50.32298\",\"EpisodeCount\":12,\"TotalEpisodeCount\":12,\"LowestAniDBRating\":7.4,\"HighestAniDBRating\":7.4,\"VideoSources\":[\"Web\"],\"AnimeTypes\":[\"TVSeries\"],\"AudioLanguages\":[\"japanese\"],\"SubtitleLanguages\":[\"english\"]}"; + private const string GroupUserFilterableString = + "{\"IsFavorite\":false,\"WatchedEpisodes\":12,\"UnwatchedEpisodes\":0,\"HasVotes\":false,\"HasPermanentVotes\":false,\"MissingPermanentVotes\":false,\"WatchedDate\":\"2023-05-05T13:42:13.3933582\",\"LastWatchedDate\":\"2023-05-05T13:43:21.3729042\",\"LowestUserRating\":0.0,\"HighestUserRating\":0.0,\"MissingEpisodes\":0,\"MissingEpisodesCollecting\":0,\"Tags\":[\"Earth\",\"Japan\",\"Asia\",\"friendship\",\"daily life\",\"high school\",\"school life\",\"comedy\",\"Kyoto\",\"dynamic\",\"themes\",\"source material\",\"setting\",\"elements\",\"place\",\"manga\",\"funny expressions\",\"storytelling\",\"character driven\",\"facial distortion\",\"narration\",\"origin\",\"episodic\",\"Japanese production\"],\"CustomTags\":[],\"Years\":[2022],\"Seasons\":[{\"Item1\":2022,\"Item2\":1}],\"HasTvDBLink\":true,\"HasMissingTvDbLink\":false,\"HasTMDbLink\":true,\"HasMissingTMDbLink\":false,\"HasTraktLink\":false,\"HasMissingTraktLink\":true,\"IsFinished\":true,\"AirDate\":\"2022-04-06T20:00:00\",\"LastAirDate\":\"2022-06-22T20:00:00\",\"AddedDate\":\"2023-05-05T14:42:24.4477733\",\"LastAddedDate\":\"2023-05-05T13:37:50.32298\",\"EpisodeCount\":12,\"TotalEpisodeCount\":12,\"LowestAniDBRating\":7.4,\"HighestAniDBRating\":7.4,\"VideoSources\":[\"Web\"],\"AnimeTypes\":[\"TVSeries\"],\"AudioLanguages\":[\"japanese\"],\"SubtitleLanguages\":[\"english\"]}"; + private const string SeriesFilterableString = + "{\"MissingEpisodes\":0,\"MissingEpisodesCollecting\":0,\"Tags\":[\"high school\",\"dynamic\",\"themes\",\"source material\",\"setting\",\"elements\",\"place\",\"Earth\",\"Japan\",\"Kyoto\",\"manga\",\"Asia\",\"comedy\",\"friendship\",\"daily life\",\"school life\",\"funny expressions\",\"storytelling\",\"character driven\",\"facial distortion\",\"narration\",\"origin\",\"episodic\",\"Japanese production\"],\"CustomTags\":[],\"Years\":[2022],\"Seasons\":[{\"Item1\":2022,\"Item2\":1}],\"HasTvDBLink\":true,\"HasMissingTvDbLink\":false,\"HasTMDbLink\":false,\"HasMissingTMDbLink\":false,\"HasTraktLink\":false,\"HasMissingTraktLink\":true,\"IsFinished\":true,\"AirDate\":\"2022-04-06T20:00:00\",\"LastAirDate\":\"2022-06-22T20:00:00\",\"AddedDate\":\"2023-05-05T14:42:24.3131538\",\"LastAddedDate\":\"2023-05-05T13:37:50.32298\",\"EpisodeCount\":12,\"TotalEpisodeCount\":12,\"LowestAniDBRating\":7.4,\"HighestAniDBRating\":7.4,\"VideoSources\":[\"Web\"],\"AnimeTypes\":[\"TVSeries\"],\"AudioLanguages\":[\"japanese\"],\"SubtitleLanguages\":[\"english\"]}"; + private const string SeriesUserFilterableString = + "{\"IsFavorite\":false,\"WatchedEpisodes\":12,\"UnwatchedEpisodes\":0,\"HasVotes\":false,\"HasPermanentVotes\":false,\"MissingPermanentVotes\":false,\"WatchedDate\":\"2023-05-05T13:42:13.3933582\",\"LastWatchedDate\":\"2023-05-05T13:43:21.3729042\",\"LowestUserRating\":0.0,\"HighestUserRating\":0.0,\"MissingEpisodes\":0,\"MissingEpisodesCollecting\":0,\"Tags\":[\"high school\",\"dynamic\",\"themes\",\"source material\",\"setting\",\"elements\",\"place\",\"Earth\",\"Japan\",\"Kyoto\",\"manga\",\"Asia\",\"comedy\",\"friendship\",\"daily life\",\"school life\",\"funny expressions\",\"storytelling\",\"character driven\",\"facial distortion\",\"narration\",\"origin\",\"episodic\",\"Japanese production\"],\"CustomTags\":[],\"Years\":[2022],\"Seasons\":[{\"Item1\":2022,\"Item2\":1}],\"HasTvDBLink\":true,\"HasMissingTvDbLink\":false,\"HasTMDbLink\":false,\"HasMissingTMDbLink\":false,\"HasTraktLink\":false,\"HasMissingTraktLink\":true,\"IsFinished\":true,\"AirDate\":\"2022-04-06T20:00:00\",\"LastAirDate\":\"2022-06-22T20:00:00\",\"AddedDate\":\"2023-05-05T14:42:24.3131538\",\"LastAddedDate\":\"2023-05-05T13:37:50.32298\",\"EpisodeCount\":12,\"TotalEpisodeCount\":12,\"LowestAniDBRating\":7.4,\"HighestAniDBRating\":7.4,\"VideoSources\":[\"Web\"],\"AnimeTypes\":[\"TVSeries\"],\"AudioLanguages\":[\"japanese\"],\"SubtitleLanguages\":[\"english\"]}"; + + public static readonly IEnumerable GroupFilterable = new[] { new[] { JsonConvert.DeserializeObject(GroupFilterableString) }}; + public static readonly IEnumerable GroupUserFilterable = new[] { new[] { JsonConvert.DeserializeObject(GroupUserFilterableString) }}; + public static readonly IEnumerable SeriesFilterable = new[] { new[] { JsonConvert.DeserializeObject(SeriesFilterableString) }}; + public static readonly IEnumerable SeriesUserFilterable = new[] { new[] { JsonConvert.DeserializeObject(SeriesUserFilterableString) }}; + + [Theory, MemberData(nameof(GroupFilterable))] + public void GroupFilterable_WithUserFilter(Filterable group) + { + var top = new AndExpression + { + Left = new AndExpression + { + Left = new HasTagExpression + { + Parameter = "comedy" + }, + Right = new NotExpression + { + Left = new HasTagExpression + { + Parameter = "18 restricted" + } + } + }, + Right = new HasWatchedEpisodesExpression() + }; + + Assert.True(top.UserDependent); + Assert.Throws(() => top.Evaluate(group)); + } + + [Theory, MemberData(nameof(GroupFilterable))] + public void GroupFilterable_WithoutUserFilter(Filterable group) + { + var top = new AndExpression + { + Left = new AndExpression + { + Left = new HasTagExpression + { + Parameter = "comedy" + }, + Right = new NotExpression + { + Left = new HasTagExpression + { + Parameter = "18 restricted" + } + } + }, + Right = new HasVideoSourceExpression + { + Parameter = "Web" + } + }; + + Assert.False(top.UserDependent); + Assert.True(top.Evaluate(group)); + } + + [Theory, MemberData(nameof(GroupUserFilterable))] + public void GroupUserFilterable_WithUserFilter(UserDependentFilterable group) + { + var top = new AndExpression + { + Left = new AndExpression + { + Left = new HasTagExpression + { + Parameter = "comedy" + }, + Right = new NotExpression + { + Left = new HasTagExpression + { + Parameter = "18 restricted" + } + } + }, + Right = new HasWatchedEpisodesExpression() + }; + + Assert.True(top.UserDependent); + Assert.True(top.Evaluate(group)); + } + + [Theory, MemberData(nameof(SeriesFilterable))] + public void SeriesFilterable_WithUserFilter(Filterable series) + { + var top = new AndExpression + { + Left = new AndExpression + { + Left = new HasTagExpression + { + Parameter = "comedy" + }, + Right = new NotExpression + { + Left = new HasTagExpression + { + Parameter = "18 restricted" + } + } + }, + Right = new HasWatchedEpisodesExpression() + }; + + Assert.True(top.UserDependent); + Assert.Throws(() => top.Evaluate(series)); + } + + [Theory, MemberData(nameof(SeriesUserFilterable))] + public void SeriesUserFilterable_WithUserFilter(UserDependentFilterable series) { var top = new AndExpression { @@ -30,6 +155,6 @@ public void UserDependentInitTest() }; Assert.True(top.UserDependent); - Assert.False(top.TimeDependent); + Assert.True(top.Evaluate(series)); } } From 85e2dbc059fe1cc9caa4eba027ae8d84e5827c94 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sun, 27 Aug 2023 00:36:49 -0400 Subject: [PATCH 08/34] Filters: Allow two expression arguments for comparators. More Tests --- .../Logic/DateTimes/EqualExpression.cs | 19 +++- .../DateTimes/GreaterThanEqualExpression.cs | 18 ++-- .../Logic/DateTimes/GreaterThanExpression.cs | 18 ++-- .../DateTimes/LessThanEqualExpression.cs | 13 +-- .../Logic/DateTimes/LessThanExpression.cs | 18 ++-- .../Logic/DateTimes/NotEqualExpression.cs | 19 +++- .../Filters/Logic/Numbers/EqualExpression.cs | 17 ++-- .../Numbers/GreaterThanEqualExpression.cs | 18 ++-- .../Logic/Numbers/GreaterThanExpression.cs | 17 ++-- .../Logic/Numbers/LessThanEqualExpression.cs | 18 ++-- .../Logic/Numbers/LessThanExpression.cs | 17 ++-- .../Logic/Numbers/NotEqualExpression.cs | 17 ++-- .../Logic/Strings/ContainsExpression.cs | 15 +++- .../Filters/Logic/Strings/EqualExpression.cs | 15 ++-- .../Logic/Strings/NotEqualExpression.cs | 15 ++-- Shoko.Tests/Shoko.Tests/FilterTests.cs | 87 +++++++++++++++++-- 16 files changed, 261 insertions(+), 80 deletions(-) diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs index 2f2c1129e..764aa53f7 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs @@ -5,9 +5,20 @@ namespace Shoko.Server.Models.Filters.Logic.DateTimes; public class EqualExpression : FilterExpression { - public FilterExpression Selector { get; set; } + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } public DateTime Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => (Selector.Evaluate(filterable) - Parameter)?.TotalDays < 1; + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + public override bool Evaluate(IFilterable filterable) + { + var date = Left.Evaluate(filterable); + var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; + var operand = Right == null ? Parameter : Right.Evaluate(filterable); + var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; + if (dateIsNull && operandIsNull) return true; + if (dateIsNull) return false; + if (operandIsNull) return false; + return (date > operand ? date - operand : operand - date).Value.TotalDays < 1; + } } diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs index e8b0bc6f2..747e4b2ca 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs @@ -5,14 +5,20 @@ namespace Shoko.Server.Models.Filters.Logic.DateTimes; public class GreaterThanEqualExpression : FilterExpression { - public FilterExpression Selector { get; set; } + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } public DateTime Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); public override bool Evaluate(IFilterable filterable) { - var date = Selector.Evaluate(filterable); - if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) return false; - return date.Value.Date >= Parameter; + var date = Left.Evaluate(filterable); + var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; + var operand = Right == null ? Parameter : Right.Evaluate(filterable); + var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; + if (dateIsNull && operandIsNull) return true; + if (dateIsNull) return false; + if (operandIsNull) return false; + return date >= operand; } } diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs index 8c440e086..246926192 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs @@ -5,14 +5,20 @@ namespace Shoko.Server.Models.Filters.Logic.DateTimes; public class GreaterThanExpression : FilterExpression { - public FilterExpression Selector { get; set; } + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } public DateTime Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); public override bool Evaluate(IFilterable filterable) { - var date = Selector.Evaluate(filterable); - if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) return false; - return date.Value.Date > Parameter; + var date = Left.Evaluate(filterable); + var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; + var operand = Right == null ? Parameter : Right.Evaluate(filterable); + var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; + if (dateIsNull && operandIsNull) return false; + if (dateIsNull) return true; + if (operandIsNull) return true; + return date > operand; } } diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs index ae41f5636..a5cd29e91 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs @@ -5,14 +5,17 @@ namespace Shoko.Server.Models.Filters.Logic.DateTimes; public class LessThanEqualExpression : FilterExpression { - public FilterExpression Selector { get; set; } + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } public DateTime Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); public override bool Evaluate(IFilterable filterable) { - var date = Selector.Evaluate(filterable); + var date = Left.Evaluate(filterable); if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) return false; - return date.Value.Date <= Parameter; + var operand = Right == null ? Parameter : Right.Evaluate(filterable); + if (operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch) return false; + return date <= operand; } } diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs index 54414929c..167faa956 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs @@ -5,14 +5,20 @@ namespace Shoko.Server.Models.Filters.Logic.DateTimes; public class LessThanExpression : FilterExpression { - public FilterExpression Selector { get; set; } + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } public DateTime Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); public override bool Evaluate(IFilterable filterable) { - var date = Selector.Evaluate(filterable); - if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) return false; - return date.Value.Date < Parameter; + var date = Left.Evaluate(filterable); + var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; + var operand = Right == null ? Parameter : Right.Evaluate(filterable); + var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; + if (dateIsNull && operandIsNull) return true; + if (dateIsNull) return false; + if (operandIsNull) return false; + return date < operand; } } diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs index fe9aee7a9..699e25240 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs @@ -5,9 +5,20 @@ namespace Shoko.Server.Models.Filters.Logic.DateTimes; public class NotEqualExpression : FilterExpression { - public FilterExpression Selector { get; set; } + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } public DateTime Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => (Selector.Evaluate(filterable) - Parameter)?.TotalDays >= 1; + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + public override bool Evaluate(IFilterable filterable) + { + var date = Left.Evaluate(filterable); + var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; + var operand = Right == null ? Parameter : Right.Evaluate(filterable); + var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; + if (dateIsNull && operandIsNull) return false; + if (dateIsNull) return true; + if (operandIsNull) return true; + return (date > operand ? date - operand : operand - date).Value.TotalDays >= 1; + } } diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs index dea51ca57..abe9f8047 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs @@ -5,9 +5,16 @@ namespace Shoko.Server.Models.Filters.Logic.Numbers; public class EqualExpression : FilterExpression { - public FilterExpression Selector { get; set; } - public double Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => Math.Abs(Selector.Evaluate(filterable) - Parameter) < 0.001D; + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } + public double? Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Parameter ?? Right.Evaluate(filterable); + return Math.Abs(left - right) < 0.001D; + } } diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs index 2c7978b91..48315d92d 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs @@ -1,12 +1,20 @@ +using System; using Shoko.Server.Models.Filters.Interfaces; namespace Shoko.Server.Models.Filters.Logic.Numbers; public class GreaterThanEqualExpression : FilterExpression { - public FilterExpression Selector { get; set; } - public double Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => Selector.Evaluate(filterable) >= Parameter; + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } + public double? Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Parameter ?? Right.Evaluate(filterable); + return Math.Abs(left - right) < 0.001D || left > right; + } } diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs index 120879c53..341462b0f 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs @@ -4,9 +4,16 @@ namespace Shoko.Server.Models.Filters.Logic.Numbers; public class GreaterThanExpression : FilterExpression { - public FilterExpression Selector { get; set; } - public double Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => Selector.Evaluate(filterable) > Parameter; + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } + public double? Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Parameter ?? Right.Evaluate(filterable); + return left > right; + } } diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs index d9b8c004d..a2c9a14e3 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs @@ -1,12 +1,20 @@ +using System; using Shoko.Server.Models.Filters.Interfaces; namespace Shoko.Server.Models.Filters.Logic.Numbers; public class LessThanEqualExpression : FilterExpression { - public FilterExpression Selector { get; set; } - public double Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => Selector.Evaluate(filterable) <= Parameter; + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } + public double? Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Parameter ?? Right.Evaluate(filterable); + return Math.Abs(left - right) < 0.001D || left < right; + } } diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs index 1a4b9d42c..c78ca209e 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs @@ -4,9 +4,16 @@ namespace Shoko.Server.Models.Filters.Logic.Numbers; public class LessThanExpression : FilterExpression { - public FilterExpression Selector { get; set; } - public double Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => Selector.Evaluate(filterable) < Parameter; + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } + public double? Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Parameter ?? Right.Evaluate(filterable); + return left < right; + } } diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs index 2ee3f8972..83cc645a9 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs @@ -5,9 +5,16 @@ namespace Shoko.Server.Models.Filters.Logic.Numbers; public class NotEqualExpression : FilterExpression { - public FilterExpression Selector { get; set; } - public double Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; - public override bool Evaluate(IFilterable filterable) => Math.Abs(Selector.Evaluate(filterable) - Parameter) >= 0.001D; + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } + public double? Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Parameter ?? Right.Evaluate(filterable); + return Math.Abs(left - right) >= 0.001D; + } } diff --git a/Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs b/Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs index 90169ebe1..331787745 100644 --- a/Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs @@ -5,10 +5,17 @@ namespace Shoko.Server.Models.Filters.Logic.Strings; public class ContainsExpression : FilterExpression { - public FilterExpression Selector { get; set; } + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } public string Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) => Parameter.Contains(Selector.Evaluate(filterable), StringComparison.InvariantCultureIgnoreCase); + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Parameter ?? Right?.Evaluate(filterable); + if (string.IsNullOrEmpty(left) || string.IsNullOrEmpty(right)) return false; + return left.Contains(right, StringComparison.InvariantCultureIgnoreCase); + } } diff --git a/Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs index 69b63680a..21f5a2286 100644 --- a/Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs @@ -5,11 +5,16 @@ namespace Shoko.Server.Models.Filters.Logic.Strings; public class EqualExpression : FilterExpression { - public FilterExpression Selector { get; set; } + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } public string Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) => - string.Equals(Selector.Evaluate(filterable), Parameter, StringComparison.InvariantCultureIgnoreCase); + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Parameter ?? Right?.Evaluate(filterable); + return string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase); + } } diff --git a/Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs b/Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs index 2bbb3a79b..ed52a38c9 100644 --- a/Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs +++ b/Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs @@ -5,11 +5,16 @@ namespace Shoko.Server.Models.Filters.Logic.Strings; public class NotEqualExpression : FilterExpression { - public FilterExpression Selector { get; set; } + public FilterExpression Left { get; set; } + public FilterExpression Right { get; set; } public string Parameter { get; set; } - public override bool TimeDependent => Selector.TimeDependent; - public override bool UserDependent => Selector.UserDependent; + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) => - !string.Equals(Selector.Evaluate(filterable), Parameter, StringComparison.InvariantCultureIgnoreCase); + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Parameter ?? Right?.Evaluate(filterable); + return !string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase); + } } diff --git a/Shoko.Tests/Shoko.Tests/FilterTests.cs b/Shoko.Tests/Shoko.Tests/FilterTests.cs index fe0b0bd17..a3aec0579 100644 --- a/Shoko.Tests/Shoko.Tests/FilterTests.cs +++ b/Shoko.Tests/Shoko.Tests/FilterTests.cs @@ -2,8 +2,11 @@ using System.Collections.Generic; using Newtonsoft.Json; using Shoko.Server.Models.Filters; +using Shoko.Server.Models.Filters.Functions; using Shoko.Server.Models.Filters.Info; using Shoko.Server.Models.Filters.Logic; +using Shoko.Server.Models.Filters.Logic.DateTimes; +using Shoko.Server.Models.Filters.Selectors; using Shoko.Server.Models.Filters.User; using Xunit; @@ -26,7 +29,7 @@ public class FilterTests public static readonly IEnumerable SeriesUserFilterable = new[] { new[] { JsonConvert.DeserializeObject(SeriesUserFilterableString) }}; [Theory, MemberData(nameof(GroupFilterable))] - public void GroupFilterable_WithUserFilter(Filterable group) + public void GroupFilterable_WithUserFilter_ExpectsException(Filterable group) { var top = new AndExpression { @@ -52,7 +55,7 @@ public void GroupFilterable_WithUserFilter(Filterable group) } [Theory, MemberData(nameof(GroupFilterable))] - public void GroupFilterable_WithoutUserFilter(Filterable group) + public void GroupFilterable_WithoutUserFilter_ExpectsTrue(Filterable group) { var top = new AndExpression { @@ -80,8 +83,82 @@ public void GroupFilterable_WithoutUserFilter(Filterable group) Assert.True(top.Evaluate(group)); } + [Theory, MemberData(nameof(GroupFilterable))] + public void GroupFilterable_WithDateFunctionFilter_ExpectsFalse(Filterable group) + { + var top = new AndExpression + { + Left = new AndExpression + { + Left = new HasTagExpression + { + Parameter = "comedy" + }, + Right = new NotExpression + { + Left = new HasTagExpression + { + Parameter = "18 restricted" + } + } + }, + Right = new GreaterThanEqualExpression + { + Left = new LastAddedDateSelector(), + Right = new DateDiffFunction + { + Selector = new DateAddFunction + { + Selector = new TodayFunction(), Parameter = TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1) + }, + Parameter = TimeSpan.FromDays(30) + }, + } + }; + + Assert.False(top.UserDependent); + Assert.False(top.Evaluate(group)); + } + + [Theory, MemberData(nameof(GroupFilterable))] + public void GroupFilterable_WithDateFunctionFilter_ExpectsTrue(Filterable group) + { + var top = new AndExpression + { + Left = new AndExpression + { + Left = new HasTagExpression + { + Parameter = "comedy" + }, + Right = new NotExpression + { + Left = new HasTagExpression + { + Parameter = "18 restricted" + } + } + }, + Right = new GreaterThanEqualExpression + { + Left = new LastAddedDateSelector(), + Right = new DateDiffFunction + { + Selector = new DateAddFunction + { + Selector = new TodayFunction(), Parameter = TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1) + }, + Parameter = TimeSpan.FromDays(120) + }, + } + }; + + Assert.False(top.UserDependent); + Assert.True(top.Evaluate(group)); + } + [Theory, MemberData(nameof(GroupUserFilterable))] - public void GroupUserFilterable_WithUserFilter(UserDependentFilterable group) + public void GroupUserFilterable_WithUserFilter_ExpectsTrue(UserDependentFilterable group) { var top = new AndExpression { @@ -107,7 +184,7 @@ public void GroupUserFilterable_WithUserFilter(UserDependentFilterable group) } [Theory, MemberData(nameof(SeriesFilterable))] - public void SeriesFilterable_WithUserFilter(Filterable series) + public void SeriesFilterable_WithUserFilter_ExpectsException(Filterable series) { var top = new AndExpression { @@ -133,7 +210,7 @@ public void SeriesFilterable_WithUserFilter(Filterable series) } [Theory, MemberData(nameof(SeriesUserFilterable))] - public void SeriesUserFilterable_WithUserFilter(UserDependentFilterable series) + public void SeriesUserFilterable_WithUserFilter_ExpectsTrue(UserDependentFilterable series) { var top = new AndExpression { From df563d676c2039ceff45e6b109fe4c04c8408e3d Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sat, 2 Sep 2023 20:07:52 -0400 Subject: [PATCH 09/34] Filters: Adding Sorting Selectors. Add Repository. --- .../FilterExpressionConverter.cs | 207 +++++++ Shoko.Server/Extensions/StringExtensions.cs | 8 + Shoko.Server/Models/Filters/Filter.cs | 18 + .../Models/Filters/FilterExpression.cs | 7 +- .../Models/Filters/FilterExtensions.cs | 15 + Shoko.Server/Models/Filters/Filterable.cs | 3 + .../Models/Filters/Interfaces/IFilterable.cs | 15 + .../Filters/Interfaces/ISortingExpression.cs | 17 + .../Selectors/AudioLanguageCountSelector.cs | 4 +- .../Filters/Selectors/EpisodeCountSelector.cs | 4 +- .../SubtitleLanguageCountSelector.cs | 4 +- .../Selectors/TotalEpisodeCountSelector.cs | 4 +- .../Models/Filters/SortingExpression.cs | 14 + .../AddedDateSortingSelector.cs | 11 + .../AirDateSortingSelector.cs | 12 + .../AudioLanguageCountSortingSelector.cs | 10 + .../EpisodeCountSortingSelector.cs | 10 + .../HighestAniDBRatingSortingSelector.cs | 11 + .../HighestUserRatingSortingSelector.cs | 11 + .../LastAddedDateSortingSelector.cs | 11 + .../LastAirDateSortingSelector.cs | 12 + .../LastWatchedDateSortingSelector.cs | 12 + .../LowestAniDBRatingSortingSelector.cs | 11 + .../LowestUserRatingSortingSelector.cs | 11 + ...ngEpisodeCollectingCountSortingSelector.cs | 10 + .../MissingEpisodeCountSortingSelector.cs | 10 + .../SortingSelectors/NameSortingSelector.cs | 10 + .../SortingNameSortingSelector.cs | 10 + .../SubtitleLanguageCountSortingSelector.cs | 10 + .../TotalEpisodeCountSortingSelector.cs | 10 + .../WatchedDateSortingSelector.cs | 12 + .../Filters/UserDependentSortingExpression.cs | 17 + .../Repositories/Cached/FilterRepository.cs | 569 ++++++++++++++++++ Shoko.Server/Repositories/RepoFactory.cs | 1 + Shoko.Server/Server/Constants.cs | 6 +- Shoko.Server/Tasks/AnimeGroupCreator.cs | 5 +- 36 files changed, 1101 insertions(+), 11 deletions(-) create mode 100644 Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs create mode 100644 Shoko.Server/Models/Filters/Filter.cs create mode 100644 Shoko.Server/Models/Filters/Interfaces/ISortingExpression.cs create mode 100644 Shoko.Server/Models/Filters/SortingExpression.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/AddedDateSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/AirDateSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/EpisodeCountSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/LastAddedDateSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/LastAirDateSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/NameSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/SortingNameSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/SortingSelectors/WatchedDateSortingSelector.cs create mode 100644 Shoko.Server/Models/Filters/UserDependentSortingExpression.cs create mode 100644 Shoko.Server/Repositories/Cached/FilterRepository.cs diff --git a/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs b/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs new file mode 100644 index 000000000..0519a75dd --- /dev/null +++ b/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs @@ -0,0 +1,207 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using NHibernate.SqlTypes; +using NHibernate.UserTypes; +using System.Data; +using System.Data.Common; +using Newtonsoft.Json; +using NHibernate; +using NHibernate.Engine; +using Shoko.Server.Models.Filters; + +namespace Shoko.Server.Databases.TypeConverters; + +public class FilterExpressionConverter : TypeConverter, IUserType +{ + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return typeof(FilterExpression).IsAssignableFrom(sourceType); + } + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return destinationType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, + object value) + { + var s = value as string ?? throw new ArgumentException("Can only convert from string"); + var baseFilter = JsonConvert.DeserializeObject(s, new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore }); + if (string.IsNullOrEmpty(baseFilter.Type)) throw new ValidationException("Type deserialized as empty"); + var type = Type.GetType(baseFilter.Type); + if (type == null) throw new NullReferenceException("Type not found"); + return JsonConvert.DeserializeObject(s, type); + } + + /// + /// Converts the given value object to the specified type + /// + /// Ignored + /// Ignored + /// The to convert. + /// The to convert the parameter to. + /// + /// An that represents the converted value. The value will be 1 if is true, otherwise 0 + /// + /// The parameter is . + /// The conversion could not be performed. + public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, + object value, Type destinationType) + { + if (value == null) return null; + return JsonConvert.SerializeObject(value); + } + + + /// + /// Creates an instance of the Type that this is associated with (bool) + /// + /// ignored. + /// ignored. + /// + /// An of type bool. It always returns 'true' for this converter. + /// + public override object CreateInstance(ITypeDescriptorContext context, System.Collections.IDictionary propertyValues) + { + return true; + } + + #region IUserType Members + + /// + /// Reconstruct an object from the cacheable representation. At the very least this + /// method should perform a deep copy if the type is mutable. (optional operation) + /// + /// the object to be cached + /// the owner of the cached object + /// + /// a reconstructed object from the cacheable representation + /// + public object Assemble(object cached, object owner) + { + return DeepCopy(cached); + } + + /// + /// Return a deep copy of the persistent state, stopping at entities and at collections. + /// + /// generally a collection element or entity field + /// a copy + public object DeepCopy(object value) + { + return value; + } + + /// + /// Transform the object into its cacheable representation. At the very least this + /// method should perform a deep copy if the type is mutable. That may not be enough + /// for some implementations, however; for example, associations must be cached as + /// identifier values. (optional operation) + /// + /// the object to be cached + /// a cacheable representation of the object + public object Disassemble(object value) + { + return DeepCopy(value); + } + + /// + /// Returns a hash code for this instance. + /// + /// The x. + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public int GetHashCode(object x) + { + return x == null ? base.GetHashCode() : x.GetHashCode(); + } + + /// + /// Are objects of this type mutable? + /// + /// + public bool IsMutable => true; + + /// + /// Retrieve an instance of the mapped class from a JDBC resultset. + /// Implementors should handle possibility of null values. + /// + /// a IDataReader + /// column names + /// + /// the containing entity + /// + /// HibernateException + public object NullSafeGet(DbDataReader rs, string[] names, ISessionImplementor impl, object owner) + { + var rawValue = NHibernateUtil.String.NullSafeGet(rs, names[0], impl); + return rawValue == null ? null : ConvertFrom(null!, null!, rawValue); + } + + /// + /// Write an instance of the mapped class to a prepared statement. + /// Implementors should handle possibility of null values. + /// A multi-column type should be written to parameters starting from index. + /// + /// a IDbCommand + /// the object to write + /// command parameter index + /// + /// HibernateException + public void NullSafeSet(DbCommand cmd, object value, int index, ISessionImplementor session) + { + ((IDataParameter)cmd.Parameters[index]).Value = + value == null ? DBNull.Value : ConvertTo(null, null, value, typeof(string)); + } + + /// + /// During merge, replace the existing () value in the entity + /// we are merging to with a new () value from the detached + /// entity we are merging. For immutable objects, or null values, it is safe to simply + /// return the first parameter. For mutable objects, it is safe to return a copy of the + /// first parameter. For objects with component values, it might make sense to + /// recursively replace component values. + /// + /// the value from the detached entity being merged + /// the value in the managed entity + /// the managed entity + /// the value to be merged + public object Replace(object original, object target, object owner) + { + return original; + } + + /// + /// The type returned by NullSafeGet() + /// + public Type ReturnedType => typeof(string); + + /// + /// The SQL types for the columns mapped by this type. + /// + /// + public SqlType[] SqlTypes => new[] { NHibernateUtil.String.SqlType }; + + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with this instance. + /// The y. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + bool IUserType.Equals(object x, object y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + return x != null && y != null && x.Equals(y); + } + + #endregion +} diff --git a/Shoko.Server/Extensions/StringExtensions.cs b/Shoko.Server/Extensions/StringExtensions.cs index 97d7c7dba..0caef6c3d 100644 --- a/Shoko.Server/Extensions/StringExtensions.cs +++ b/Shoko.Server/Extensions/StringExtensions.cs @@ -168,4 +168,12 @@ public static string SplitCamelCaseToWords(this string strInput) return strOutput.ToString(); } + + public static string GetSortName(this string name) + { + if (name.StartsWith("A ")) name = name[2..]; + if (name.StartsWith("An ")) name = name[3..]; + if (name.StartsWith("The ")) name = name[4..]; + return name; + } } diff --git a/Shoko.Server/Models/Filters/Filter.cs b/Shoko.Server/Models/Filters/Filter.cs new file mode 100644 index 000000000..bd4f27fdd --- /dev/null +++ b/Shoko.Server/Models/Filters/Filter.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Shoko.Models.Enums; + +namespace Shoko.Server.Models.Filters; + +public class Filter +{ + public int FilterID { get; set; } + public int? ParentFilterID { get; set; } + public string Name { get; set; } + public bool ApplyAtSeriesLevel { get; set; } + public bool Locked { get; set; } + public GroupFilterType FilterType { get; set; } + public bool Hidden { get; set; } + + public FilterExpression Expression { get; set; } + public List SortingExpressions { get; set; } +} diff --git a/Shoko.Server/Models/Filters/FilterExpression.cs b/Shoko.Server/Models/Filters/FilterExpression.cs index a48f47f3a..52619fe1d 100644 --- a/Shoko.Server/Models/Filters/FilterExpression.cs +++ b/Shoko.Server/Models/Filters/FilterExpression.cs @@ -2,10 +2,15 @@ namespace Shoko.Server.Models.Filters; -public abstract class FilterExpression : IFilterExpression +public abstract class FilterExpression { + public int FilterExpressionID { get; set; } + public string Type { get; set; } public abstract bool TimeDependent { get; } public abstract bool UserDependent { get; } +} +public abstract class FilterExpression : FilterExpression, IFilterExpression +{ public abstract T Evaluate(IFilterable f); } diff --git a/Shoko.Server/Models/Filters/FilterExtensions.cs b/Shoko.Server/Models/Filters/FilterExtensions.cs index 22f8dd58c..23f314955 100644 --- a/Shoko.Server/Models/Filters/FilterExtensions.cs +++ b/Shoko.Server/Models/Filters/FilterExtensions.cs @@ -3,6 +3,7 @@ using System.Linq; using Shoko.Commons.Extensions; using Shoko.Models.Enums; +using Shoko.Server.Extensions; using Shoko.Server.Models.Filters.Interfaces; using Shoko.Server.Providers.AniDB; using Shoko.Server.Repositories; @@ -15,9 +16,13 @@ public static class FilterExtensions public static IFilterable ToFilterable(this SVR_AnimeSeries series) { var anime = series.GetAnime(); + var name = series.GetSeriesName(); // TODO optimize this a bunch. Lots of duplicate calls. Contract should be severely trimmed var filterable = new Filterable { + Name = name, + SortingName = name.GetSortName(), + SeriesCount = 1, AirDate = anime?.AirDate, MissingEpisodes = series.Contract?.MissingEpisodeCount ?? 0, MissingEpisodesCollecting = series.Contract?.MissingEpisodeCountGroups ?? 0, @@ -54,8 +59,12 @@ public static UserDependentFilterable ToUserDependentFilterable(this SVR_AnimeSe var user = series.GetUserRecord(userID); var vote = anime?.UserVote; var watchedDates = series.GetVideoLocals().Select(a => a.GetUserRecord(userID)?.WatchedDate).Where(a => a != null).OrderBy(a => a).ToList(); + var name = series.GetSeriesName(); var filterable = new UserDependentFilterable { + Name = name, + SortingName = name.GetSortName(), + SeriesCount = 1, AirDate = anime?.AirDate, MissingEpisodes = series.Contract?.MissingEpisodeCount ?? 0, MissingEpisodesCollecting = series.Contract?.MissingEpisodeCountGroups ?? 0, @@ -134,6 +143,9 @@ public static IFilterable ToFilterable(this SVR_AnimeGroup group) // TODO optimize this a bunch. Lots of duplicate calls. Contract should be severely trimmed var filterable = new Filterable { + Name = group.GroupName, + SortingName = group.GroupName.GetSortName(), + SeriesCount = series.Count, AirDate = group.Contract.Stat_AirDate_Min, LastAirDate = group.Contract?.Stat_EndDate ?? group.GetAllSeries().SelectMany(a => a.GetAnimeEpisodes()).Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), @@ -184,6 +196,9 @@ public static IFilterable ToUserDependentFilterable(this SVR_AnimeGroup group, i // TODO optimize this a bunch. Lots of duplicate calls. Contract should be severely trimmed var filterable = new UserDependentFilterable { + Name = group.GroupName, + SortingName = group.GroupName.GetSortName(), + SeriesCount = series.Count, AirDate = group.Contract.Stat_AirDate_Min, LastAirDate = group.Contract?.Stat_EndDate ?? group.GetAllSeries().SelectMany(a => a.GetAnimeEpisodes()).Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), diff --git a/Shoko.Server/Models/Filters/Filterable.cs b/Shoko.Server/Models/Filters/Filterable.cs index 4bf83d544..91f777161 100644 --- a/Shoko.Server/Models/Filters/Filterable.cs +++ b/Shoko.Server/Models/Filters/Filterable.cs @@ -8,6 +8,9 @@ namespace Shoko.Server.Models.Filters; public class Filterable : IFilterable { // The explicit implementations of IReadOnlySet make it easier to deserialize into + public string Name { get; init; } + public string SortingName { get; init; } + public int SeriesCount { get; init; } public int MissingEpisodes { get; init; } public int MissingEpisodesCollecting { get; init; } public HashSet Tags { get; init; } diff --git a/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs b/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs index 0ba07bd7a..174d0f25c 100644 --- a/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs +++ b/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs @@ -6,6 +6,21 @@ namespace Shoko.Server.Models.Filters.Interfaces; public interface IFilterable { + /// + /// Name + /// + string Name { get; } + + /// + /// Sorting Name + /// + string SortingName { get; } + + /// + /// The number of series in a group + /// + int SeriesCount { get; } + /// /// Number of Missing Episodes /// diff --git a/Shoko.Server/Models/Filters/Interfaces/ISortingExpression.cs b/Shoko.Server/Models/Filters/Interfaces/ISortingExpression.cs new file mode 100644 index 000000000..e88d283a3 --- /dev/null +++ b/Shoko.Server/Models/Filters/Interfaces/ISortingExpression.cs @@ -0,0 +1,17 @@ +namespace Shoko.Server.Models.Filters.Interfaces; + +public interface ISortingExpression +{ + bool TimeDependent { get; } + bool UserDependent { get; } +} + +public interface ISortingExpression +{ + T Evaluate(IFilterable f); +} + +public interface IUserDependentSortingExpression +{ + T Evaluate(IUserDependentFilterable f); +} diff --git a/Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs b/Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs index 44e651295..f5cbe711b 100644 --- a/Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs @@ -2,9 +2,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class AudioLanguageCountSelector : FilterExpression +public class AudioLanguageCountSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(IFilterable f) => f.AudioLanguages.Count; + public override int Evaluate(IFilterable f) => f.AudioLanguages.Count; } diff --git a/Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs b/Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs index 2cb3d13d4..6a673db99 100644 --- a/Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs @@ -2,9 +2,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class EpisodeCountSelector : FilterExpression +public class EpisodeCountSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(IFilterable f) => f.EpisodeCount; + public override int Evaluate(IFilterable f) => f.EpisodeCount; } diff --git a/Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs b/Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs index 81f421d68..9435f193d 100644 --- a/Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs @@ -2,9 +2,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class SubtitleLanguageCountSelector : FilterExpression +public class SubtitleLanguageCountSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(IFilterable f) => f.SubtitleLanguages.Count; + public override int Evaluate(IFilterable f) => f.SubtitleLanguages.Count; } diff --git a/Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs b/Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs index 923de467e..a28f683dd 100644 --- a/Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs +++ b/Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs @@ -2,9 +2,9 @@ namespace Shoko.Server.Models.Filters.Selectors; -public class TotalEpisodeCountSelector : FilterExpression +public class TotalEpisodeCountSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(IFilterable f) => f.TotalEpisodeCount; + public override int Evaluate(IFilterable f) => f.TotalEpisodeCount; } diff --git a/Shoko.Server/Models/Filters/SortingExpression.cs b/Shoko.Server/Models/Filters/SortingExpression.cs new file mode 100644 index 000000000..9d4102158 --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingExpression.cs @@ -0,0 +1,14 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters; + +public abstract class SortingExpression : FilterExpression, ISortingExpression +{ + public bool Descending { get; set; } // take advantage of default(bool) being false +} + +public abstract class SortingExpression : SortingExpression, ISortingExpression where T : IComparable +{ + public abstract T Evaluate(IFilterable f); +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/AddedDateSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/AddedDateSortingSelector.cs new file mode 100644 index 000000000..990128980 --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/AddedDateSortingSelector.cs @@ -0,0 +1,11 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class AddedDateSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override DateTime Evaluate(IFilterable f) => f.AddedDate; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/AirDateSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/AirDateSortingSelector.cs new file mode 100644 index 000000000..fe7b48536 --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/AirDateSortingSelector.cs @@ -0,0 +1,12 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class AirDateSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public DateTime DefaultValue { get; set; } + public override DateTime Evaluate(IFilterable f) => f.AirDate ?? DefaultValue; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs new file mode 100644 index 000000000..d20b7d2a7 --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class AudioLanguageCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override int Evaluate(IFilterable f) => f.AudioLanguages.Count; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/EpisodeCountSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/EpisodeCountSortingSelector.cs new file mode 100644 index 000000000..5247f45de --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/EpisodeCountSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class EpisodeCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override int Evaluate(IFilterable f) => f.EpisodeCount; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs new file mode 100644 index 000000000..9c2a2b831 --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs @@ -0,0 +1,11 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class HighestAniDBRatingSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override double Evaluate(IFilterable f) => Convert.ToDouble(f.HighestAniDBRating); +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs new file mode 100644 index 000000000..d2fb5388b --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs @@ -0,0 +1,11 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class HighestUserRatingSortingSelector : UserDependentSortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + public override double Evaluate(IUserDependentFilterable f) => Convert.ToDouble(f.HighestUserRating); +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/LastAddedDateSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/LastAddedDateSortingSelector.cs new file mode 100644 index 000000000..eb2102e48 --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/LastAddedDateSortingSelector.cs @@ -0,0 +1,11 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class LastAddedDateSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override DateTime Evaluate(IFilterable f) => f.LastAddedDate; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/LastAirDateSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/LastAirDateSortingSelector.cs new file mode 100644 index 000000000..080a24c25 --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/LastAirDateSortingSelector.cs @@ -0,0 +1,12 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class LastAirDateSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public DateTime DefaultValue { get; set; } + public override DateTime Evaluate(IFilterable f) => f.LastAirDate ?? DefaultValue; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs new file mode 100644 index 000000000..212defb43 --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs @@ -0,0 +1,12 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class LastWatchedDateSortingSelector : UserDependentSortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + public DateTime DefaultValue { get; set; } + public override DateTime Evaluate(IUserDependentFilterable f) => f.LastWatchedDate ?? DefaultValue; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs new file mode 100644 index 000000000..483c6d6de --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs @@ -0,0 +1,11 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class LowestAniDBRatingSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override double Evaluate(IFilterable f) => Convert.ToDouble(f.LowestAniDBRating); +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs new file mode 100644 index 000000000..e64c6eec6 --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs @@ -0,0 +1,11 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class LowestUserRatingSortingSelector : UserDependentSortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + public override double Evaluate(IUserDependentFilterable f) => Convert.ToDouble(f.LowestUserRating); +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs new file mode 100644 index 000000000..5b09bb232 --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class MissingEpisodeCollectingCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override int Evaluate(IFilterable f) => f.MissingEpisodesCollecting; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs new file mode 100644 index 000000000..f4dcddd99 --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class MissingEpisodeCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override int Evaluate(IFilterable f) => f.MissingEpisodes; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/NameSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/NameSortingSelector.cs new file mode 100644 index 000000000..516175915 --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/NameSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class NameSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override string Evaluate(IFilterable f) => f.Name; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/SortingNameSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/SortingNameSortingSelector.cs new file mode 100644 index 000000000..2e96534a4 --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/SortingNameSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class SortingNameSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override string Evaluate(IFilterable f) => f.SortingName; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs new file mode 100644 index 000000000..07a6fcbdf --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class SubtitleLanguageCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override int Evaluate(IFilterable f) => f.SubtitleLanguages.Count; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs new file mode 100644 index 000000000..6afb52f9e --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class TotalEpisodeCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override int Evaluate(IFilterable f) => f.TotalEpisodeCount; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/WatchedDateSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/WatchedDateSortingSelector.cs new file mode 100644 index 000000000..bf16d82ff --- /dev/null +++ b/Shoko.Server/Models/Filters/SortingSelectors/WatchedDateSortingSelector.cs @@ -0,0 +1,12 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters.SortingSelectors; + +public class WatchedDateSortingSelector : UserDependentSortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + public DateTime DefaultValue { get; set; } + public override DateTime Evaluate(IUserDependentFilterable f) => f.WatchedDate ?? DefaultValue; +} diff --git a/Shoko.Server/Models/Filters/UserDependentSortingExpression.cs b/Shoko.Server/Models/Filters/UserDependentSortingExpression.cs new file mode 100644 index 000000000..5fc70539f --- /dev/null +++ b/Shoko.Server/Models/Filters/UserDependentSortingExpression.cs @@ -0,0 +1,17 @@ +using System; +using Shoko.Server.Models.Filters.Interfaces; + +namespace Shoko.Server.Models.Filters; + +public abstract class UserDependentSortingExpression : SortingExpression, IUserDependentSortingExpression where T : IComparable +{ + public override T Evaluate(IFilterable f) + { + if (UserDependent && f is not IUserDependentFilterable) + throw new ArgumentException("User Dependent Filter was given an IFilterable, rather than an IUserDependentFilterable"); + + return Evaluate((IUserDependentFilterable)f); + } + + public abstract T Evaluate(IUserDependentFilterable f); +} diff --git a/Shoko.Server/Repositories/Cached/FilterRepository.cs b/Shoko.Server/Repositories/Cached/FilterRepository.cs new file mode 100644 index 000000000..96c81f639 --- /dev/null +++ b/Shoko.Server/Repositories/Cached/FilterRepository.cs @@ -0,0 +1,569 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using NutzCode.InMemoryIndex; +using Shoko.Commons.Extensions; +using Shoko.Commons.Properties; +using Shoko.Models.Enums; +using Shoko.Server.Extensions; +using Shoko.Server.Models; +using Shoko.Server.Models.Filters; +using Shoko.Server.Models.Filters.Info; +using Shoko.Server.Models.Filters.Logic; +using Shoko.Server.Models.Filters.SortingSelectors; +using Shoko.Server.Models.Filters.User; +using Shoko.Server.Repositories.NHibernate; +using Shoko.Server.Server; +using Constants = Shoko.Server.Server.Constants; + +namespace Shoko.Server.Repositories.Cached; + +public class FilterRepository : BaseCachedRepository +{ + private PocoIndex Parents; + private readonly ChangeTracker Changes = new(); + + public FilterRepository() + { + EndSaveCallback = obj => + { + Changes.AddOrUpdate(obj.FilterID); + }; + EndDeleteCallback = obj => + { + Changes.Remove(obj.FilterID); + }; + } + + protected override int SelectKey(Filter entity) + { + return entity.FilterID; + } + + public override void PopulateIndexes() + { + Changes.AddOrUpdateRange(Cache.Keys); + Parents = Cache.CreateIndex(a => a.ParentFilterID ?? 0); + } + + public override void RegenerateDb() { } + + + public override void PostProcess() + { + const string t = "Filter"; + + // Clean up. This will populate empty conditions and remove duplicate filters + ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, + t, + " " + Resources.GroupFilter_Cleanup); + var all = GetAll(); + var set = new HashSet(all); + var notin = all.Except(set).ToList(); + Delete(notin); + } + + public void CleanUpEmptyDirectoryFilters() + { + var toremove = GetAll().Where(a => (a.FilterType & GroupFilterType.Directory) != 0) + .Where(gf => // TODO evaluate + false).ToList(); + if (toremove.Count > 0) + { + Delete(toremove); + } + } + + public void CreateOrVerifyLockedFilters() + { + const string t = "GroupFilter"; + + var lockedGFs = GetLockedGroupFilters(); + + ServerState.Instance.ServerStartingStatus = string.Format( + Resources.Database_Validating, t, + " " + Resources.Filter_CreateContinueWatching); + + if (!lockedGFs.Any(a => a.Name == Constants.GroupFilterName.ContinueWatching)) + { + var gf = new Filter + { + Name = Constants.GroupFilterName.ContinueWatching, + Locked = true, + ApplyAtSeriesLevel = false, + FilterType = GroupFilterType.ContinueWatching, + Expression = new AndExpression{ Left = new HasWatchedEpisodesExpression(), Right = new HasUnwatchedEpisodesExpression() }, + SortingExpressions = new List { new LastWatchedDateSortingSelector { Descending = true } } + }; + Save(gf); + } + + //Create All filter + if (!lockedGFs.Any(a => a.Name == Constants.GroupFilterName.All)) + { + var gf = new Filter + { + Name = Constants.GroupFilterName.All, + Locked = true, + FilterType = GroupFilterType.All, + SortingExpressions = new List { new NameSortingSelector() } + }; + Save(gf); + } + + if (!lockedGFs.Any(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Tag))) + { + var gf = new Filter + { + Name = Constants.GroupFilterName.Tags, + FilterType = (GroupFilterType.Directory | GroupFilterType.Tag), + Locked = true, + SortingExpressions = new List { new NameSortingSelector() } + }; + Save(gf); + } + + if (!lockedGFs.Any(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Year))) + { + var gf = new Filter + { + Name = Constants.GroupFilterName.Years, + FilterType = (GroupFilterType.Directory | GroupFilterType.Year), + Locked = true, + SortingExpressions = new List { new NameSortingSelector() } + }; + Save(gf); + } + + if (!lockedGFs.Any(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Season))) + { + var gf = new Filter + { + Name = Constants.GroupFilterName.Seasons, + FilterType = (GroupFilterType.Directory | GroupFilterType.Season), + Locked = true, + SortingExpressions = new List { new NameSortingSelector() } + }; + Save(gf); + } + + CreateOrVerifyDirectoryFilters(true); + } + + public void CreateOrVerifyDirectoryFilters(bool frominit = false, HashSet tags = null, + HashSet airdate = null, SortedSet<(int Year, AnimeSeason Season)> seasons = null) + { + const string t = "GroupFilter"; + + var lockedGFs = GetLockedGroupFilters(); + + + var tagsdirec = lockedGFs.FirstOrDefault(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Tag)); + if (tagsdirec != null) + { + HashSet alltags; + if (tags == null) + { + alltags = new HashSet( + RepoFactory.AniDB_Tag.GetAllForLocalSeries().Select(a => a.TagName.Replace('`', '\'')), + StringComparer.InvariantCultureIgnoreCase); + } + else + { + alltags = new HashSet(tags, StringComparer.InvariantCultureIgnoreCase); + } + + var existingTags = + new HashSet( + lockedGFs.Where(a => a.FilterType == GroupFilterType.Tag).Select(a => a.Expression).Cast().Select(a => a.Parameter), + StringComparer.InvariantCultureIgnoreCase); + alltags.ExceptWith(existingTags); + + var max = alltags.Count; + var cnt = 0; + //AniDB Tags are in english so we use en-us culture + var tinfo = new CultureInfo("en-US", false).TextInfo; + foreach (var s in alltags) + { + cnt++; + if (frominit) + { + ServerState.Instance.ServerStartingStatus = string.Format( + Resources.Database_Validating, t, + Resources.Filter_CreatingTag + " " + + Resources.Filter_Filter + " " + cnt + "/" + max + " - " + s); + } + + var yf = new Filter + { + ParentFilterID = tagsdirec.FilterID, + FilterType = GroupFilterType.Tag, + ApplyAtSeriesLevel = true, + Name = tinfo.ToTitleCase(s), + Locked = true, + Expression = new HasTagExpression { Parameter = s }, + SortingExpressions = new List { new NameSortingSelector() } + }; + Save(yf); + } + } + + var yearsdirec = lockedGFs.FirstOrDefault(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Year)); + if (yearsdirec != null) + { + HashSet allyears; + if (airdate == null || airdate.Count == 0) + { + var grps = RepoFactory.AnimeSeries.GetAll().Select(a => a.Contract).Where(a => a != null).ToList(); + + allyears = new HashSet(); + foreach (var ser in grps) + { + var endyear = ser.AniDBAnime.AniDBAnime.EndYear; + var startyear = ser.AniDBAnime.AniDBAnime.BeginYear; + if (endyear <= 0) endyear = DateTime.Today.Year; + if (endyear < startyear || endyear - startyear + 1 >= int.MaxValue) endyear = startyear; + if (startyear != 0) allyears.UnionWith(Enumerable.Range(startyear, endyear - startyear + 1).Select(a => a)); + } + } + else + { + allyears = new HashSet(airdate.Select(a => a)); + } + + var notin = new HashSet(lockedGFs.Where(a => a.FilterType == GroupFilterType.Year).Select(a => a.Expression).Cast() + .Select(a => a.Parameter)); + allyears.ExceptWith(notin); + var max = allyears.Count; + var cnt = 0; + foreach (var s in allyears) + { + cnt++; + if (frominit) + { + ServerState.Instance.ServerStartingStatus = string.Format( + Resources.Database_Validating, t, + Resources.Filter_CreatingYear + " " + + Resources.Filter_Filter + " " + cnt + "/" + max + " - " + s); + } + + var yf = new Filter + { + ParentFilterID = yearsdirec.FilterID, + Name = s.ToString(), + FilterType = GroupFilterType.Year, + Locked = true, + ApplyAtSeriesLevel = true, + Expression = new InYearExpression { Parameter = s }, + SortingExpressions = new List { new NameSortingSelector() } + }; + Save(yf); + } + } + + var seasonsdirectory = lockedGFs.FirstOrDefault(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Season)); + if (seasonsdirectory != null) + { + SortedSet<(int Year, AnimeSeason Season)> allseasons; + if (seasons == null) + { + var grps = RepoFactory.AnimeSeries.GetAll().ToList(); + + allseasons = new SortedSet<(int Year, AnimeSeason Season)>(); + foreach (var ser in grps) + { + var seriesSeasons = ser?.Contract?.AniDBAnime?.AniDBAnime?.GetSeasons().ToList(); + if ((seriesSeasons?.Count ?? 0) == 0) ser?.UpdateContract(); + if ((seriesSeasons?.Count ?? 0) == 0) continue; + allseasons.UnionWith(seriesSeasons); + } + } + else + { + allseasons = seasons; + } + + var notin = new HashSet<(int Year, AnimeSeason Season)>(lockedGFs.Where(a => a.FilterType == GroupFilterType.Season).Select(a => a.Expression) + .Cast().Select(a => (a.Year, a.Season))); + allseasons.ExceptWith(notin); + + var max = allseasons.Count; + var cnt = 0; + foreach (var season in allseasons) + { + cnt++; + if (frominit) + { + ServerState.Instance.ServerStartingStatus = string.Format( + Resources.Database_Validating, t, + Resources.Filter_CreatingSeason + " " + + Resources.Filter_Filter + " " + cnt + "/" + max + " - " + season); + } + + var yf = new Filter + { + ParentFilterID = seasonsdirectory.FilterID, + Name = season.Season + " " + season.Year, + Locked = true, + FilterType = GroupFilterType.Season, + ApplyAtSeriesLevel = true, + Expression = new InSeasonExpression { Season = season.Season, Year = season.Year }, + SortingExpressions = new List { new NameSortingSelector() } + }; + Save(yf); + } + } + + CleanUpEmptyDirectoryFilters(); + } + + public override void Save(Filter obj) + { + WriteLock(() => { base.Save(obj); }); + } + + public override void Save(IReadOnlyCollection objs) + { + foreach (var obj in objs) + { + Save(obj); + } + } + + public override void Delete(IReadOnlyCollection objs) + { + foreach (var cr in objs) + { + base.Delete(cr); + } + } + + /// + /// Updates a batch of s. + /// + /// + /// This method ONLY updates existing s. It will not insert any that don't already exist. + /// + /// The NHibernate session. + /// The batch of s to update. + /// or is null. + public void BatchUpdate(ISessionWrapper session, IEnumerable groupFilters) + { + if (session == null) + { + throw new ArgumentNullException(nameof(session)); + } + + if (groupFilters == null) + { + throw new ArgumentNullException(nameof(groupFilters)); + } + + foreach (var groupFilter in groupFilters) + { + session.Update(groupFilter); + UpdateCache(groupFilter); + Changes.AddOrUpdate(groupFilter.FilterID); + } + } + + public List GetByParentID(int parentid) + { + return ReadLock(() => Parents.GetMultiple(parentid)); + } + + public List GetTopLevel() + { + return GetByParentID(0); + } + + /// + /// Calculates what groups should belong to tag related group filters. + /// + /// The NHibernate session. + /// A that maps group filter ID to anime group IDs. + /// is null. + public void CalculateAnimeSeriesPerTagGroupFilter(ISessionWrapper session) + { + if (session == null) + { + throw new ArgumentNullException(nameof(session)); + } + + var tagsdirec = GetAll(session).FirstOrDefault(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Tag)); + + DropAllTagFilters(session); + + // user -> tag -> series + var somethingDictionary = + new ConcurrentDictionary>>(); + var users = new List { null }; + users.AddRange(RepoFactory.JMMUser.GetAll()); + var tags = RepoFactory.AniDB_Tag.GetAll().ToLookup(a => a?.TagName?.ToLowerInvariant()); + + Parallel.ForEach(tags, tag => + { + foreach (var series in tag.ToList().SelectMany(a => RepoFactory.AniDB_Anime_Tag.GetAnimeWithTag(a.TagID))) + { + var seriesTags = series.GetAnime()?.GetAllTags(); + foreach (var user in users) + { + if (user?.GetHideCategories().FindInEnumerable(seriesTags) ?? false) continue; + + if (somethingDictionary.ContainsKey(user?.JMMUserID ?? 0)) + { + if (somethingDictionary[user?.JMMUserID ?? 0].ContainsKey(tag.Key)) + { + somethingDictionary[user?.JMMUserID ?? 0][tag.Key] + .Add(series.AnimeSeriesID); + } + else + { + somethingDictionary[user?.JMMUserID ?? 0].AddOrUpdate(tag.Key, + new HashSet { series.AnimeSeriesID }, (oldTag, oldIDs) => + { + lock (oldIDs) + { + oldIDs.Add(series.AnimeSeriesID); + } + + return oldIDs; + }); + } + } + else + { + somethingDictionary.AddOrUpdate(user?.JMMUserID ?? 0, id => + { + var newdict = new ConcurrentDictionary>(); + newdict.AddOrUpdate(tag.Key, new HashSet { series.AnimeSeriesID }, + (oldTag, oldIDs) => + { + lock (oldIDs) + { + oldIDs.Add(series.AnimeSeriesID); + } + + return oldIDs; + }); + return newdict; + }, + (i, value) => + { + if (value.ContainsKey(tag.Key)) + { + value[tag.Key] + .Add(series.AnimeSeriesID); + } + else + { + value.AddOrUpdate(tag.Key, + new HashSet { series.AnimeSeriesID }, (oldTag, oldIDs) => + { + lock (oldIDs) + { + oldIDs.Add(series.AnimeSeriesID); + } + + return oldIDs; + }); + } + + return value; + }); + } + } + } + }); + + var lookup = somethingDictionary.Keys.Where(a => somethingDictionary[a] != null).ToDictionary(key => key, key => + somethingDictionary[key].Where(a => a.Value != null) + .SelectMany(p => p.Value.Select(a => Tuple.Create(p.Key, a))) + .ToLookup(p => p.Item1, p => p.Item2)); + + CreateAllTagFilters(session, tagsdirec, lookup); + } + + private void DropAllTagFilters(ISessionWrapper session) + { + ClearCache(); + Lock(() => + { + using var trans = session.BeginTransaction(); + session.CreateQuery($"DELETE FROM {nameof(Filter)} WHERE FilterType = {(int)GroupFilterType.Tag};") + .ExecuteUpdate(); + trans.Commit(); + }); + } + + private void CreateAllTagFilters(ISessionWrapper session, Filter tagsdirec, + Dictionary> lookup) + { + if (tagsdirec == null) + { + return; + } + + var alltags = new HashSet( + RepoFactory.AniDB_Tag.GetAllForLocalSeries().Select(a => a.TagName.Replace('`', '\'')), + StringComparer.InvariantCultureIgnoreCase); + var toAdd = new List(alltags.Count); + + var users = RepoFactory.JMMUser.GetAll().ToList(); + + //AniDB Tags are in english so we use en-us culture + var tinfo = new CultureInfo("en-US", false).TextInfo; + foreach (var s in alltags) + { + // this is creating new tag filters, so locking isn't completely necessary. + // Ideally, we would create a blank filter to ensure an ID exists, but then it would be empty, + // and would have data inconsistencies, anyway + var yf = new Filter + { + ParentFilterID = tagsdirec.FilterID, + Hidden = false, + ApplyAtSeriesLevel = true, + Name = tinfo.ToTitleCase(s), + Locked = true, + FilterType = GroupFilterType.Tag, + Expression = new HasTagExpression { Parameter = s }, + SortingExpressions = new List { new NameSortingSelector() } + }; + + Lock(() => + { + using var trans = session.BeginTransaction(); + // get an ID + session.Insert(yf); + trans.Commit(); + }); + + toAdd.Add(yf); + } + + Populate(session, false); + } + + public List GetLockedGroupFilters() + { + return ReadLock(() => Cache.Values.Where(a => a.Locked).ToList()); + } + + public List GetTimeDependentFilters() + { + return ReadLock( + () => + { + return GetAll().Where(a => a.Expression.TimeDependent).ToList(); + } + ); + } + + public ChangeTracker GetChangeTracker() + { + return Changes; + } +} diff --git a/Shoko.Server/Repositories/RepoFactory.cs b/Shoko.Server/Repositories/RepoFactory.cs index 5b21bcdf2..2777c2147 100644 --- a/Shoko.Server/Repositories/RepoFactory.cs +++ b/Shoko.Server/Repositories/RepoFactory.cs @@ -94,6 +94,7 @@ public static class RepoFactory public static AnimeStaffRepository AnimeStaff { get; } = new(); public static CrossRef_Anime_StaffRepository CrossRef_Anime_Staff { get; } = new(); public static GroupFilterRepository GroupFilter { get; } = new(); + public static FilterRepository Filter { get; } = new(); /************** DEPRECATED **************/ /* We need to delete them at some point */ diff --git a/Shoko.Server/Server/Constants.cs b/Shoko.Server/Server/Constants.cs index d89b69044..5257eadf3 100644 --- a/Shoko.Server/Server/Constants.cs +++ b/Shoko.Server/Server/Constants.cs @@ -24,7 +24,11 @@ public static class Constants public struct GroupFilterName { - public static readonly string ContinueWatching = Resources.Filter_Continue; + public const string All = "All"; + public const string ContinueWatching = "Continue Watching"; + public const string Tags = "Tags"; + public const string Seasons = "Seasons"; + public const string Years = "Years"; } public struct DatabaseType diff --git a/Shoko.Server/Tasks/AnimeGroupCreator.cs b/Shoko.Server/Tasks/AnimeGroupCreator.cs index 4e5e86041..2fec46274 100644 --- a/Shoko.Server/Tasks/AnimeGroupCreator.cs +++ b/Shoko.Server/Tasks/AnimeGroupCreator.cs @@ -417,7 +417,10 @@ public SVR_AnimeGroup GetOrCreateSingleGroupForSeries(SVR_AnimeSeries series) { // Override the group name if the group is not manually named. if (animeGroup.IsManuallyNamed == 0) - animeGroup.GroupName = animeGroup.SortName = series.GetSeriesName(); + { + animeGroup.GroupName = series.GetSeriesName(); + animeGroup.SortName = animeGroup.GroupName.GetSortName(); + } // Override the group desc. if the group doesn't have an override. if (animeGroup.OverrideDescription == 0) animeGroup.Description = series.GetAnime().Description; From 423a2a9231e3894610830ec257f893b8213f43be Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sun, 3 Sep 2023 01:21:53 -0400 Subject: [PATCH 10/34] Filters: Database mappings done. Initial Data done --- Shoko.Commons | 2 +- Shoko.Server/Databases/BaseDatabase.cs | 159 +----------------- Shoko.Server/Databases/SQLite.cs | 6 +- .../FilterExpressionConverter.cs | 18 +- Shoko.Server/Mappings/FilterMap.cs | 25 +++ Shoko.Server/Models/Filters/Filter.cs | 4 +- .../Models/Filters/FilterExpression.cs | 10 +- .../Models/Filters/SortingExpression.cs | 13 +- .../Repositories/Cached/FilterRepository.cs | 152 +++++++++++++++-- .../Cached/GroupFilterRepository.cs | 157 +++++++++++++++++ Shoko.Server/Repositories/RepoFactory.cs | 2 +- Shoko.Server/Server/Constants.cs | 7 + 12 files changed, 370 insertions(+), 185 deletions(-) create mode 100644 Shoko.Server/Mappings/FilterMap.cs diff --git a/Shoko.Commons b/Shoko.Commons index 59a7ecd92..539d499f7 160000 --- a/Shoko.Commons +++ b/Shoko.Commons @@ -1 +1 @@ -Subproject commit 59a7ecd92839d82015485d9d7956c3f934f55b15 +Subproject commit 539d499f75184ced0fba34910b3317bcdbd17db2 diff --git a/Shoko.Server/Databases/BaseDatabase.cs b/Shoko.Server/Databases/BaseDatabase.cs index 7abb991d8..246da07c2 100644 --- a/Shoko.Server/Databases/BaseDatabase.cs +++ b/Shoko.Server/Databases/BaseDatabase.cs @@ -281,166 +281,13 @@ public void PopulateInitialData() public void CreateOrVerifyLockedFilters() { RepoFactory.GroupFilter.CreateOrVerifyLockedFilters(); + RepoFactory.Filter.CreateOrVerifyLockedFilters(); } private void CreateInitialGroupFilters() { - // group filters - // Do to DatabaseFixes, some filters may be made, namely directory filters - // All, Continue Watching, Years, Seasons, Tags... 6 seems to be enough to tell for now - // We can't just check the existence of anything specific, as the user can delete most of these - if (RepoFactory.GroupFilter.GetTopLevel().Count() > 6) - { - return; - } - - // Favorites - var gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Favorites, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - var gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.Favourite, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - - // Missing Episodes - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_MissingEpisodes, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.MissingEpisodesCollecting, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - - - // Newly Added Series - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Added, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.SeriesCreatedDate, - ConditionOperator = (int)GroupFilterOperator.LastXDays, - ConditionParameter = "10" - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - - // Newly Airing Series - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Airing, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.AirDate, - ConditionOperator = (int)GroupFilterOperator.LastXDays, - ConditionParameter = "30" - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - - // Votes Needed - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Votes, - ApplyToSeries = 1, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.CompletedSeries, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.HasUnwatchedEpisodes, - ConditionOperator = (int)GroupFilterOperator.Exclude, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.UserVotedAny, - ConditionOperator = (int)GroupFilterOperator.Exclude, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - - // Recently Watched - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_RecentlyWatched, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.EpisodeWatchedDate, - ConditionOperator = (int)GroupFilterOperator.LastXDays, - ConditionParameter = "10" - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - - // TvDB/MovieDB Link Missing - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_LinkMissing, - ApplyToSeries = 1, // This makes far more sense as applied to series - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.AssignedTvDBOrMovieDBInfo, - ConditionOperator = (int)GroupFilterOperator.Exclude, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); + RepoFactory.GroupFilter.CreateInitialGroupFilters(); + RepoFactory.Filter.CreateInitialFilters(); } private void CreateInitialUsers() diff --git a/Shoko.Server/Databases/SQLite.cs b/Shoko.Server/Databases/SQLite.cs index f4d213414..02ba12317 100644 --- a/Shoko.Server/Databases/SQLite.cs +++ b/Shoko.Server/Databases/SQLite.cs @@ -22,7 +22,7 @@ public class SQLite : BaseDatabase { public override string Name { get; } = "SQLite"; - public override int RequiredVersion { get; } = 104; + public override int RequiredVersion { get; } = 105; public override void BackupDatabase(string fullfilename) @@ -672,6 +672,10 @@ public override void CreateDatabase() new(103, 2, "ALTER TABLE VideoLocal ADD LastAVDumpVersion text;"), new(104, 1, DatabaseFixes.FixAnimeSourceLinks), new(104, 2, DatabaseFixes.FixOrphanedShokoEpisodes), + new DatabaseCommand(105, 1, + "CREATE TABLE Filter( FilterID INTEGER PRIMARY KEY AUTOINCREMENT, ParentFilterID int, Name text NOT NULL, FilterType int NOT NULL, Locked int NOT NULL, Hidden int NOT NULL, ApplyAtSeriesLevel int NOT NULL, Expression text, SortingExpression text ); "), + new DatabaseCommand(105, 2, + "CREATE INDEX IX_Filter_ParentFilterID ON Filter(ParentFilterID); CREATE INDEX IX_Filter_Name ON Filter(Name); CREATE INDEX IX_Filter_FilterType ON Filter(FilterType); CREATE INDEX IX_Filter_LockedHidden ON Filter(Locked, Hidden);"), }; private static Tuple DropLanguage(object connection) diff --git a/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs b/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs index 0519a75dd..96ec14d34 100644 --- a/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs +++ b/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using NHibernate.SqlTypes; @@ -28,11 +29,12 @@ public override object ConvertFrom(ITypeDescriptorContext context, System.Global object value) { var s = value as string ?? throw new ArgumentException("Can only convert from string"); - var baseFilter = JsonConvert.DeserializeObject(s, new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore }); - if (string.IsNullOrEmpty(baseFilter.Type)) throw new ValidationException("Type deserialized as empty"); - var type = Type.GetType(baseFilter.Type); - if (type == null) throw new NullReferenceException("Type not found"); - return JsonConvert.DeserializeObject(s, type); + return JsonConvert.DeserializeObject(s, new JsonSerializerSettings + { + MissingMemberHandling = MissingMemberHandling.Ignore, + TypeNameHandling = TypeNameHandling.Objects + //Converters = new List { new FilterExpressionJsonConverter() }, + }); } /// @@ -51,7 +53,11 @@ public override object ConvertTo(ITypeDescriptorContext context, System.Globaliz object value, Type destinationType) { if (value == null) return null; - return JsonConvert.SerializeObject(value); + return JsonConvert.SerializeObject(value, new JsonSerializerSettings + { + MissingMemberHandling = MissingMemberHandling.Ignore, + TypeNameHandling = TypeNameHandling.Objects + }); } diff --git a/Shoko.Server/Mappings/FilterMap.cs b/Shoko.Server/Mappings/FilterMap.cs new file mode 100644 index 000000000..60c404ca4 --- /dev/null +++ b/Shoko.Server/Mappings/FilterMap.cs @@ -0,0 +1,25 @@ +using FluentNHibernate.Mapping; +using Shoko.Models.Enums; +using Shoko.Server.Databases.TypeConverters; +using Shoko.Server.Models.Filters; + +namespace Shoko.Server.Mappings; + +public class FilterMap : ClassMap +{ + public FilterMap() + { + Table("Filter"); + Not.LazyLoad(); + Id(x => x.FilterID); + Map(x => x.ParentFilterID).Nullable(); + //References(x => x.Parent).Nullable().PropertyRef(x => x.ParentFilterID); + Map(x => x.Name).Not.Nullable(); + Map(x => x.FilterType).Not.Nullable().CustomType(); + Map(x => x.Locked).Not.Nullable(); + Map(x => x.Hidden).Not.Nullable(); + Map(x => x.ApplyAtSeriesLevel).Not.Nullable(); + Map(x => x.Expression).Nullable().CustomType(); + Map(x => x.SortingExpression).Nullable().CustomType(); + } +} diff --git a/Shoko.Server/Models/Filters/Filter.cs b/Shoko.Server/Models/Filters/Filter.cs index bd4f27fdd..da64a2848 100644 --- a/Shoko.Server/Models/Filters/Filter.cs +++ b/Shoko.Server/Models/Filters/Filter.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Shoko.Models.Enums; namespace Shoko.Server.Models.Filters; @@ -6,6 +5,7 @@ namespace Shoko.Server.Models.Filters; public class Filter { public int FilterID { get; set; } + //public virtual Filter Parent { get; set; } public int? ParentFilterID { get; set; } public string Name { get; set; } public bool ApplyAtSeriesLevel { get; set; } @@ -14,5 +14,5 @@ public class Filter public bool Hidden { get; set; } public FilterExpression Expression { get; set; } - public List SortingExpressions { get; set; } + public SortingExpression SortingExpression { get; set; } } diff --git a/Shoko.Server/Models/Filters/FilterExpression.cs b/Shoko.Server/Models/Filters/FilterExpression.cs index 52619fe1d..b299cf2bc 100644 --- a/Shoko.Server/Models/Filters/FilterExpression.cs +++ b/Shoko.Server/Models/Filters/FilterExpression.cs @@ -1,13 +1,15 @@ +using System.Runtime.Serialization; using Shoko.Server.Models.Filters.Interfaces; namespace Shoko.Server.Models.Filters; -public abstract class FilterExpression +public class FilterExpression : IFilterExpression { public int FilterExpressionID { get; set; } - public string Type { get; set; } - public abstract bool TimeDependent { get; } - public abstract bool UserDependent { get; } + [IgnoreDataMember] + public virtual bool TimeDependent => false; + [IgnoreDataMember] + public virtual bool UserDependent => false; } public abstract class FilterExpression : FilterExpression, IFilterExpression diff --git a/Shoko.Server/Models/Filters/SortingExpression.cs b/Shoko.Server/Models/Filters/SortingExpression.cs index 9d4102158..0b7ac204c 100644 --- a/Shoko.Server/Models/Filters/SortingExpression.cs +++ b/Shoko.Server/Models/Filters/SortingExpression.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Shoko.Server.Models.Filters.Interfaces; namespace Shoko.Server.Models.Filters; @@ -8,7 +9,17 @@ public abstract class SortingExpression : FilterExpression, ISortingExpression public bool Descending { get; set; } // take advantage of default(bool) being false } -public abstract class SortingExpression : SortingExpression, ISortingExpression where T : IComparable +public abstract class SortingExpression : SortingExpression, ISortingExpression, IComparer where T : IComparable { + public SortingExpression Next { get; set; } public abstract T Evaluate(IFilterable f); + public virtual int Compare(IFilterable x, IFilterable y) + { + var valueX = Evaluate(x); + var valueY = Evaluate(y); + if (Equals(valueX, valueY)) return Next?.Compare(x, y) ?? 0; + if (valueX == null) return 1; + if (valueY == null) return -1; + return valueX.CompareTo(valueY); + } } diff --git a/Shoko.Server/Repositories/Cached/FilterRepository.cs b/Shoko.Server/Repositories/Cached/FilterRepository.cs index 96c81f639..0e4d51d7b 100644 --- a/Shoko.Server/Repositories/Cached/FilterRepository.cs +++ b/Shoko.Server/Repositories/Cached/FilterRepository.cs @@ -11,8 +11,11 @@ using Shoko.Server.Extensions; using Shoko.Server.Models; using Shoko.Server.Models.Filters; +using Shoko.Server.Models.Filters.Functions; using Shoko.Server.Models.Filters.Info; using Shoko.Server.Models.Filters.Logic; +using Shoko.Server.Models.Filters.Logic.DateTimes; +using Shoko.Server.Models.Filters.Selectors; using Shoko.Server.Models.Filters.SortingSelectors; using Shoko.Server.Models.Filters.User; using Shoko.Server.Repositories.NHibernate; @@ -94,9 +97,9 @@ public void CreateOrVerifyLockedFilters() Name = Constants.GroupFilterName.ContinueWatching, Locked = true, ApplyAtSeriesLevel = false, - FilterType = GroupFilterType.ContinueWatching, + FilterType = GroupFilterType.None, Expression = new AndExpression{ Left = new HasWatchedEpisodesExpression(), Right = new HasUnwatchedEpisodesExpression() }, - SortingExpressions = new List { new LastWatchedDateSortingSelector { Descending = true } } + SortingExpression = new LastWatchedDateSortingSelector { Descending = true } }; Save(gf); } @@ -109,7 +112,7 @@ public void CreateOrVerifyLockedFilters() Name = Constants.GroupFilterName.All, Locked = true, FilterType = GroupFilterType.All, - SortingExpressions = new List { new NameSortingSelector() } + SortingExpression = new NameSortingSelector() }; Save(gf); } @@ -120,8 +123,7 @@ public void CreateOrVerifyLockedFilters() { Name = Constants.GroupFilterName.Tags, FilterType = (GroupFilterType.Directory | GroupFilterType.Tag), - Locked = true, - SortingExpressions = new List { new NameSortingSelector() } + Locked = true }; Save(gf); } @@ -132,8 +134,7 @@ public void CreateOrVerifyLockedFilters() { Name = Constants.GroupFilterName.Years, FilterType = (GroupFilterType.Directory | GroupFilterType.Year), - Locked = true, - SortingExpressions = new List { new NameSortingSelector() } + Locked = true }; Save(gf); } @@ -144,8 +145,7 @@ public void CreateOrVerifyLockedFilters() { Name = Constants.GroupFilterName.Seasons, FilterType = (GroupFilterType.Directory | GroupFilterType.Season), - Locked = true, - SortingExpressions = new List { new NameSortingSelector() } + Locked = true }; Save(gf); } @@ -205,7 +205,7 @@ public void CreateOrVerifyDirectoryFilters(bool frominit = false, HashSet { new NameSortingSelector() } + SortingExpression = new NameSortingSelector() }; Save(yf); } @@ -258,7 +258,7 @@ public void CreateOrVerifyDirectoryFilters(bool frominit = false, HashSet { new NameSortingSelector() } + SortingExpression = new NameSortingSelector() }; Save(yf); } @@ -311,7 +311,7 @@ public void CreateOrVerifyDirectoryFilters(bool frominit = false, HashSet { new NameSortingSelector() } + SortingExpression = new NameSortingSelector() }; Save(yf); } @@ -319,6 +319,132 @@ public void CreateOrVerifyDirectoryFilters(bool frominit = false, HashSet 6) return; + + // Favorites + var gf = new Filter + { + Name = Constants.GroupFilterName.Favorites, + FilterType = GroupFilterType.UserDefined, + Expression = new IsFavoriteExpression(), + SortingExpression = new NameSortingSelector() + }; + Save(gf); + + // Missing Episodes + gf = new Filter + { + Name = Constants.GroupFilterName.MissingEpisodes, + FilterType = GroupFilterType.UserDefined, + Expression = new HasMissingEpisodesCollectingExpression(), + SortingExpression = new MissingEpisodeCollectingCountSortingSelector{ Descending = true} + }; + Save(gf); + + + // Newly Added Series + gf = new Filter + { + Name = Constants.GroupFilterName.NewlyAddedSeries, + FilterType = GroupFilterType.UserDefined, + Expression = new GreaterThanEqualExpression + { + Left = new DateAddFunction { Selector = new LastAddedDateSelector(), Parameter = TimeSpan.FromDays(10) }, + Right = new TodayFunction() + }, + SortingExpression = new LastAddedDateSortingSelector { Descending = true} + }; + Save(gf); + + // Newly Airing Series + gf = new Filter + { + Name = Constants.GroupFilterName.NewlyAiringSeries, + FilterType = GroupFilterType.UserDefined, + Expression = new GreaterThanEqualExpression + { + Left = new DateAddFunction { Selector = new LastAirDateSelector(), Parameter = TimeSpan.FromDays(30) }, + Right = new TodayFunction() + }, + SortingExpression = new LastAirDateSortingSelector { Descending = true} + }; + Save(gf); + + // Votes Needed + gf = new Filter + { + Name = Constants.GroupFilterName.MissingVotes, + ApplyAtSeriesLevel = true, + FilterType = GroupFilterType.UserDefined, + Expression = new AndExpression + { + // all watched and none missing + Left = new AndExpression + { + // all watched, aka no episodes unwatched + Left = new NotExpression + { + Left = new HasUnwatchedEpisodesExpression() + }, + // no missing episodes + Right = new NotExpression + { + Left = new HasMissingEpisodesExpression() + } + }, + // does not have votes + Right = new NotExpression + { + Left = new HasUserVotesExpression() + } + }, + SortingExpression = new NameSortingSelector() + }; + Save(gf); + + // Recently Watched + gf = new Filter + { + Name = Constants.GroupFilterName.RecentlyWatched, + FilterType = GroupFilterType.UserDefined, + Expression = new AndExpression + { + Left = new HasWatchedEpisodesExpression(), + Right = new GreaterThanEqualExpression + { + Left = new DateAddFunction { Selector = new LastWatchedDateSelector(), Parameter = TimeSpan.FromDays(10) }, + Right = new TodayFunction() + }, + }, + SortingExpression = new LastWatchedDateSortingSelector + { + Descending = true + } + }; + Save(gf); + + // TvDB/MovieDB Link Missing + gf = new Filter + { + Name = Constants.GroupFilterName.MissingLinks, + ApplyAtSeriesLevel = true, + FilterType = GroupFilterType.UserDefined, + Expression = new OrExpression + { + Left = new MissingTvDBLinkExpression(), + Right = new MissingTMDbLinkExpression() + }, + SortingExpression = new NameSortingSelector() + }; + Save(gf); + } public override void Save(Filter obj) { @@ -530,7 +656,7 @@ private void CreateAllTagFilters(ISessionWrapper session, Filter tagsdirec, Locked = true, FilterType = GroupFilterType.Tag, Expression = new HasTagExpression { Parameter = s }, - SortingExpressions = new List { new NameSortingSelector() } + SortingExpression = new NameSortingSelector() }; Lock(() => diff --git a/Shoko.Server/Repositories/Cached/GroupFilterRepository.cs b/Shoko.Server/Repositories/Cached/GroupFilterRepository.cs index 32653098c..565347aeb 100644 --- a/Shoko.Server/Repositories/Cached/GroupFilterRepository.cs +++ b/Shoko.Server/Repositories/Cached/GroupFilterRepository.cs @@ -494,6 +494,163 @@ public void CreateOrVerifyDirectoryFilters(bool frominit = false, HashSet 6) return; + + // Favorites + var gf = new SVR_GroupFilter + { + GroupFilterName = Resources.Filter_Favorites, + ApplyToSeries = 0, + BaseCondition = 1, + Locked = 0, + FilterType = (int)GroupFilterType.UserDefined + }; + var gfc = new GroupFilterCondition + { + ConditionType = (int)GroupFilterConditionType.Favourite, + ConditionOperator = (int)GroupFilterOperator.Include, + ConditionParameter = string.Empty + }; + gf.Conditions.Add(gfc); + gf.CalculateGroupsAndSeries(); + Save(gf); + + // Missing Episodes + gf = new SVR_GroupFilter + { + GroupFilterName = Resources.Filter_MissingEpisodes, + ApplyToSeries = 0, + BaseCondition = 1, + Locked = 0, + FilterType = (int)GroupFilterType.UserDefined + }; + gfc = new GroupFilterCondition + { + ConditionType = (int)GroupFilterConditionType.MissingEpisodesCollecting, + ConditionOperator = (int)GroupFilterOperator.Include, + ConditionParameter = string.Empty + }; + gf.Conditions.Add(gfc); + gf.CalculateGroupsAndSeries(); + Save(gf); + + + // Newly Added Series + gf = new SVR_GroupFilter + { + GroupFilterName = Resources.Filter_Added, + ApplyToSeries = 0, + BaseCondition = 1, + Locked = 0, + FilterType = (int)GroupFilterType.UserDefined + }; + gfc = new GroupFilterCondition + { + ConditionType = (int)GroupFilterConditionType.SeriesCreatedDate, + ConditionOperator = (int)GroupFilterOperator.LastXDays, + ConditionParameter = "10" + }; + gf.Conditions.Add(gfc); + gf.CalculateGroupsAndSeries(); + Save(gf); + + // Newly Airing Series + gf = new SVR_GroupFilter + { + GroupFilterName = Resources.Filter_Airing, + ApplyToSeries = 0, + BaseCondition = 1, + Locked = 0, + FilterType = (int)GroupFilterType.UserDefined + }; + gfc = new GroupFilterCondition + { + ConditionType = (int)GroupFilterConditionType.AirDate, + ConditionOperator = (int)GroupFilterOperator.LastXDays, + ConditionParameter = "30" + }; + gf.Conditions.Add(gfc); + gf.CalculateGroupsAndSeries(); + Save(gf); + + // Votes Needed + gf = new SVR_GroupFilter + { + GroupFilterName = Resources.Filter_Votes, + ApplyToSeries = 1, + BaseCondition = 1, + Locked = 0, + FilterType = (int)GroupFilterType.UserDefined + }; + gfc = new GroupFilterCondition + { + ConditionType = (int)GroupFilterConditionType.CompletedSeries, + ConditionOperator = (int)GroupFilterOperator.Include, + ConditionParameter = string.Empty + }; + gf.Conditions.Add(gfc); + gfc = new GroupFilterCondition + { + ConditionType = (int)GroupFilterConditionType.HasUnwatchedEpisodes, + ConditionOperator = (int)GroupFilterOperator.Exclude, + ConditionParameter = string.Empty + }; + gf.Conditions.Add(gfc); + gfc = new GroupFilterCondition + { + ConditionType = (int)GroupFilterConditionType.UserVotedAny, + ConditionOperator = (int)GroupFilterOperator.Exclude, + ConditionParameter = string.Empty + }; + gf.Conditions.Add(gfc); + gf.CalculateGroupsAndSeries(); + Save(gf); + + // Recently Watched + gf = new SVR_GroupFilter + { + GroupFilterName = Resources.Filter_RecentlyWatched, + ApplyToSeries = 0, + BaseCondition = 1, + Locked = 0, + FilterType = (int)GroupFilterType.UserDefined + }; + gfc = new GroupFilterCondition + { + ConditionType = (int)GroupFilterConditionType.EpisodeWatchedDate, + ConditionOperator = (int)GroupFilterOperator.LastXDays, + ConditionParameter = "10" + }; + gf.Conditions.Add(gfc); + gf.CalculateGroupsAndSeries(); + Save(gf); + + // TvDB/MovieDB Link Missing + gf = new SVR_GroupFilter + { + GroupFilterName = Resources.Filter_LinkMissing, + ApplyToSeries = 1, // This makes far more sense as applied to series + BaseCondition = 1, + Locked = 0, + FilterType = (int)GroupFilterType.UserDefined + }; + gfc = new GroupFilterCondition + { + ConditionType = (int)GroupFilterConditionType.AssignedTvDBOrMovieDBInfo, + ConditionOperator = (int)GroupFilterOperator.Exclude, + ConditionParameter = string.Empty + }; + gf.Conditions.Add(gfc); + gf.CalculateGroupsAndSeries(); + Save(gf); + } + public override void Save(SVR_GroupFilter obj) { Save(obj, false); diff --git a/Shoko.Server/Repositories/RepoFactory.cs b/Shoko.Server/Repositories/RepoFactory.cs index 2777c2147..f8eee98e2 100644 --- a/Shoko.Server/Repositories/RepoFactory.cs +++ b/Shoko.Server/Repositories/RepoFactory.cs @@ -124,7 +124,7 @@ public static void PostInit() CleanUpMemory(); } - public static async Task Init() + public static void Init() { try { diff --git a/Shoko.Server/Server/Constants.cs b/Shoko.Server/Server/Constants.cs index 5257eadf3..a595e48fa 100644 --- a/Shoko.Server/Server/Constants.cs +++ b/Shoko.Server/Server/Constants.cs @@ -26,6 +26,13 @@ public struct GroupFilterName { public const string All = "All"; public const string ContinueWatching = "Continue Watching"; + public const string Favorites = "Favorites"; + public const string MissingEpisodes = "Missing Episodes"; + public const string NewlyAddedSeries = "Newly Added Series"; + public const string NewlyAiringSeries = "Newly Airing Series"; + public const string MissingVotes = "Missing Votes"; + public const string MissingLinks = "Missing Links"; + public const string RecentlyWatched = "Recently Watched"; public const string Tags = "Tags"; public const string Seasons = "Seasons"; public const string Years = "Years"; From 92a67c21fe8e7348a2f716d5b1ce21459d2bd5e1 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sun, 3 Sep 2023 23:45:33 -0400 Subject: [PATCH 11/34] Filters: Reorganize. Create and Update Directory Filters --- Shoko.CLI/Program.cs | 34 +++++++- Shoko.Server/API/v2/Modules/Common.cs | 12 +-- .../CommandRequest_RefreshGroupFilter.cs | 1 + .../FilterExpressionConverter.cs | 9 ++- .../Files/HasAudioLanguageExpression.cs | 4 +- .../Files/HasSubtitleLanguageExpression.cs | 4 +- Shoko.Server/Filters/FilterEvaluator.cs | 77 +++++++++++++++++++ .../{Models => }/Filters/FilterExpression.cs | 4 +- .../{Models => }/Filters/FilterExtensions.cs | 5 +- .../{Models => }/Filters/Filterable.cs | 4 +- .../Filters/Functions/DateAddFunction.cs | 4 +- .../Filters/Functions/DateDiffFunction.cs | 4 +- .../Filters/Functions/TodayFunction.cs | 4 +- .../Filters/Info/HasAnimeTypeExpression.cs | 4 +- .../Filters/Info/HasCustomTagExpression.cs | 4 +- .../HasMissingEpisodesCollectingExpression.cs | 4 +- .../Info/HasMissingEpisodesExpression.cs | 4 +- .../Filters/Info/HasTMDbLinkExpression.cs | 4 +- .../Filters/Info/HasTagExpression.cs | 4 +- .../Filters/Info/HasTraktLinkExpression.cs | 4 +- .../Filters/Info/HasTvDBLinkExpression.cs | 4 +- .../Filters/Info/HasVideoSourceExpression.cs | 4 +- .../Filters/Info/InSeasonExpression.cs | 4 +- .../Filters/Info/InYearExpression.cs | 4 +- .../Filters/Info/IsFinishedExpression.cs | 4 +- .../Filters/Info/MissingTMDbLinkExpression.cs | 4 +- .../Info/MissingTraktLinkExpression.cs | 4 +- .../Filters/Info/MissingTvDBLinkExpression.cs | 4 +- .../Filters/Interfaces/IFilterExpression.cs | 2 +- .../Filters/Interfaces/IFilterable.cs | 2 +- .../Filters/Interfaces/ISortingExpression.cs | 5 ++ .../Interfaces/IUserDependentFilterable.cs | 3 +- .../Filters/Logic/AndExpression.cs | 4 +- .../Logic/DateTimes/EqualExpression.cs | 4 +- .../DateTimes/GreaterThanEqualExpression.cs | 4 +- .../Logic/DateTimes/GreaterThanExpression.cs | 4 +- .../DateTimes/LessThanEqualExpression.cs | 4 +- .../Logic/DateTimes/LessThanExpression.cs | 4 +- .../Logic/DateTimes/NotEqualExpression.cs | 4 +- .../Filters/Logic/NotExpression.cs | 4 +- .../Filters/Logic/Numbers/EqualExpression.cs | 4 +- .../Numbers/GreaterThanEqualExpression.cs | 4 +- .../Logic/Numbers/GreaterThanExpression.cs | 4 +- .../Logic/Numbers/LessThanEqualExpression.cs | 4 +- .../Logic/Numbers/LessThanExpression.cs | 4 +- .../Logic/Numbers/NotEqualExpression.cs | 4 +- .../Filters/Logic/OrExpression.cs | 4 +- .../Logic/Strings/ContainsExpression.cs | 4 +- .../Filters/Logic/Strings/EqualExpression.cs | 4 +- .../Logic/Strings/NotEqualExpression.cs | 4 +- .../Filters/Logic/XorExpression.cs | 4 +- .../Filters/Selectors/AddedDateSelector.cs | 4 +- .../Filters/Selectors/AirDateSelector.cs | 4 +- .../Selectors/AudioLanguageCountSelector.cs | 4 +- .../Filters/Selectors/EpisodeCountSelector.cs | 4 +- .../Selectors/HighestAniDBRatingSelector.cs | 4 +- .../Selectors/HighestUserRatingSelector.cs | 4 +- .../Selectors/LastAddedDateSelector.cs | 4 +- .../Filters/Selectors/LastAirDateSelector.cs | 4 +- .../Selectors/LastWatchedDateSelector.cs | 4 +- .../Selectors/LowestAniDBRatingSelector.cs | 4 +- .../Selectors/LowestUserRatingSelector.cs | 4 +- .../SubtitleLanguageCountSelector.cs | 4 +- .../Selectors/TotalEpisodeCountSelector.cs | 4 +- .../Filters/Selectors/WatchedDateSelector.cs | 4 +- Shoko.Server/Filters/SortingExpression.cs | 9 +++ .../AddedDateSortingSelector.cs | 10 +++ .../AirDateSortingSelector.cs | 12 +++ .../AudioLanguageCountSortingSelector.cs | 10 +++ .../EpisodeCountSortingSelector.cs | 10 +++ .../HighestAniDBRatingSortingSelector.cs | 8 +- .../HighestUserRatingSortingSelector.cs | 8 +- .../LastAddedDateSortingSelector.cs | 10 +++ .../LastAirDateSortingSelector.cs | 12 +++ .../LastWatchedDateSortingSelector.cs | 12 +++ .../LowestAniDBRatingSortingSelector.cs | 8 +- .../LowestUserRatingSortingSelector.cs | 8 +- ...ngEpisodeCollectingCountSortingSelector.cs | 10 +++ .../MissingEpisodeCountSortingSelector.cs | 10 +++ .../SortingSelectors/NameSortingSelector.cs | 6 +- .../SortingNameSortingSelector.cs | 10 +++ .../SubtitleLanguageCountSortingSelector.cs | 10 +++ .../TotalEpisodeCountSortingSelector.cs | 10 +++ .../WatchedDateSortingSelector.cs | 8 +- .../User/HasPermanentUserVotesExpression.cs | 4 +- .../User/HasUnwatchedEpisodesExpression.cs | 4 +- .../Filters/User/HasUserVotesExpression.cs | 4 +- .../User/HasWatchedEpisodesExpression.cs | 4 +- .../Filters/User/IsFavoriteExpression.cs | 4 +- .../MissingPermanentUserVotesExpression.cs | 4 +- .../Filters/UserDependentFilterExpression.cs | 4 +- .../Filters/UserDependentFilterable.cs | 5 +- .../Filters/UserDependentSortingExpression.cs | 17 ++++ Shoko.Server/{Models => }/Filters/readme.md | 0 Shoko.Server/Models/{Filters => }/Filter.cs | 2 + .../Filters/Interfaces/ISortingExpression.cs | 17 ---- .../Models/Filters/SortingExpression.cs | 25 ------ .../AddedDateSortingSelector.cs | 11 --- .../AirDateSortingSelector.cs | 12 --- .../AudioLanguageCountSortingSelector.cs | 10 --- .../EpisodeCountSortingSelector.cs | 10 --- .../LastAddedDateSortingSelector.cs | 11 --- .../LastAirDateSortingSelector.cs | 12 --- .../LastWatchedDateSortingSelector.cs | 12 --- ...ngEpisodeCollectingCountSortingSelector.cs | 10 --- .../MissingEpisodeCountSortingSelector.cs | 10 --- .../SortingNameSortingSelector.cs | 10 --- .../SubtitleLanguageCountSortingSelector.cs | 10 --- .../TotalEpisodeCountSortingSelector.cs | 10 --- .../Filters/UserDependentSortingExpression.cs | 17 ---- .../Cached/AnimeGroupRepository.cs | 3 + .../Cached/AnimeSeriesRepository.cs | 2 + .../Repositories/Cached/FilterRepository.cs | 20 ++--- Shoko.Server/Server/Startup.cs | 2 + Shoko.Tests/Shoko.Tests/FilterTests.cs | 13 ++-- 115 files changed, 462 insertions(+), 368 deletions(-) rename Shoko.Server/{Models => }/Filters/Files/HasAudioLanguageExpression.cs (77%) rename Shoko.Server/{Models => }/Filters/Files/HasSubtitleLanguageExpression.cs (78%) create mode 100644 Shoko.Server/Filters/FilterEvaluator.cs rename Shoko.Server/{Models => }/Filters/FilterExpression.cs (83%) rename Shoko.Server/{Models => }/Filters/FilterExtensions.cs (99%) rename Shoko.Server/{Models => }/Filters/Filterable.cs (96%) rename Shoko.Server/{Models => }/Filters/Functions/DateAddFunction.cs (81%) rename Shoko.Server/{Models => }/Filters/Functions/DateDiffFunction.cs (81%) rename Shoko.Server/{Models => }/Filters/Functions/TodayFunction.cs (72%) rename Shoko.Server/{Models => }/Filters/Info/HasAnimeTypeExpression.cs (77%) rename Shoko.Server/{Models => }/Filters/Info/HasCustomTagExpression.cs (77%) rename Shoko.Server/{Models => }/Filters/Info/HasMissingEpisodesCollectingExpression.cs (75%) rename Shoko.Server/{Models => }/Filters/Info/HasMissingEpisodesExpression.cs (74%) rename Shoko.Server/{Models => }/Filters/Info/HasTMDbLinkExpression.cs (73%) rename Shoko.Server/{Models => }/Filters/Info/HasTagExpression.cs (76%) rename Shoko.Server/{Models => }/Filters/Info/HasTraktLinkExpression.cs (73%) rename Shoko.Server/{Models => }/Filters/Info/HasTvDBLinkExpression.cs (73%) rename Shoko.Server/{Models => }/Filters/Info/HasVideoSourceExpression.cs (77%) rename Shoko.Server/{Models => }/Filters/Info/InSeasonExpression.cs (80%) rename Shoko.Server/{Models => }/Filters/Info/InYearExpression.cs (76%) rename Shoko.Server/{Models => }/Filters/Info/IsFinishedExpression.cs (73%) rename Shoko.Server/{Models => }/Filters/Info/MissingTMDbLinkExpression.cs (79%) rename Shoko.Server/{Models => }/Filters/Info/MissingTraktLinkExpression.cs (79%) rename Shoko.Server/{Models => }/Filters/Info/MissingTvDBLinkExpression.cs (79%) rename Shoko.Server/{Models => }/Filters/Interfaces/IFilterExpression.cs (85%) rename Shoko.Server/{Models => }/Filters/Interfaces/IFilterable.cs (98%) create mode 100644 Shoko.Server/Filters/Interfaces/ISortingExpression.cs rename Shoko.Server/{Models => }/Filters/Interfaces/IUserDependentFilterable.cs (93%) rename Shoko.Server/{Models => }/Filters/Logic/AndExpression.cs (83%) rename Shoko.Server/{Models => }/Filters/Logic/DateTimes/EqualExpression.cs (91%) rename Shoko.Server/{Models => }/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs (91%) rename Shoko.Server/{Models => }/Filters/Logic/DateTimes/GreaterThanExpression.cs (91%) rename Shoko.Server/{Models => }/Filters/Logic/DateTimes/LessThanEqualExpression.cs (90%) rename Shoko.Server/{Models => }/Filters/Logic/DateTimes/LessThanExpression.cs (91%) rename Shoko.Server/{Models => }/Filters/Logic/DateTimes/NotEqualExpression.cs (91%) rename Shoko.Server/{Models => }/Filters/Logic/NotExpression.cs (78%) rename Shoko.Server/{Models => }/Filters/Logic/Numbers/EqualExpression.cs (86%) rename Shoko.Server/{Models => }/Filters/Logic/Numbers/GreaterThanEqualExpression.cs (87%) rename Shoko.Server/{Models => }/Filters/Logic/Numbers/GreaterThanExpression.cs (86%) rename Shoko.Server/{Models => }/Filters/Logic/Numbers/LessThanEqualExpression.cs (87%) rename Shoko.Server/{Models => }/Filters/Logic/Numbers/LessThanExpression.cs (86%) rename Shoko.Server/{Models => }/Filters/Logic/Numbers/NotEqualExpression.cs (86%) rename Shoko.Server/{Models => }/Filters/Logic/OrExpression.cs (83%) rename Shoko.Server/{Models => }/Filters/Logic/Strings/ContainsExpression.cs (88%) rename Shoko.Server/{Models => }/Filters/Logic/Strings/EqualExpression.cs (87%) rename Shoko.Server/{Models => }/Filters/Logic/Strings/NotEqualExpression.cs (87%) rename Shoko.Server/{Models => }/Filters/Logic/XorExpression.cs (83%) rename Shoko.Server/{Models => }/Filters/Selectors/AddedDateSelector.cs (72%) rename Shoko.Server/{Models => }/Filters/Selectors/AirDateSelector.cs (72%) rename Shoko.Server/{Models => }/Filters/Selectors/AudioLanguageCountSelector.cs (71%) rename Shoko.Server/{Models => }/Filters/Selectors/EpisodeCountSelector.cs (70%) rename Shoko.Server/{Models => }/Filters/Selectors/HighestAniDBRatingSelector.cs (74%) rename Shoko.Server/{Models => }/Filters/Selectors/HighestUserRatingSelector.cs (76%) rename Shoko.Server/{Models => }/Filters/Selectors/LastAddedDateSelector.cs (73%) rename Shoko.Server/{Models => }/Filters/Selectors/LastAirDateSelector.cs (72%) rename Shoko.Server/{Models => }/Filters/Selectors/LastWatchedDateSelector.cs (75%) rename Shoko.Server/{Models => }/Filters/Selectors/LowestAniDBRatingSelector.cs (74%) rename Shoko.Server/{Models => }/Filters/Selectors/LowestUserRatingSelector.cs (75%) rename Shoko.Server/{Models => }/Filters/Selectors/SubtitleLanguageCountSelector.cs (72%) rename Shoko.Server/{Models => }/Filters/Selectors/TotalEpisodeCountSelector.cs (71%) rename Shoko.Server/{Models => }/Filters/Selectors/WatchedDateSelector.cs (74%) create mode 100644 Shoko.Server/Filters/SortingExpression.cs create mode 100644 Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs create mode 100644 Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs create mode 100644 Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs create mode 100644 Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs rename Shoko.Server/{Models => }/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs (54%) rename Shoko.Server/{Models => }/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs (54%) create mode 100644 Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs create mode 100644 Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs create mode 100644 Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs rename Shoko.Server/{Models => }/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs (54%) rename Shoko.Server/{Models => }/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs (54%) create mode 100644 Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs create mode 100644 Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs rename Shoko.Server/{Models => }/Filters/SortingSelectors/NameSortingSelector.cs (50%) create mode 100644 Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs create mode 100644 Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs create mode 100644 Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs rename Shoko.Server/{Models => }/Filters/SortingSelectors/WatchedDateSortingSelector.cs (50%) rename Shoko.Server/{Models => }/Filters/User/HasPermanentUserVotesExpression.cs (76%) rename Shoko.Server/{Models => }/Filters/User/HasUnwatchedEpisodesExpression.cs (76%) rename Shoko.Server/{Models => }/Filters/User/HasUserVotesExpression.cs (75%) rename Shoko.Server/{Models => }/Filters/User/HasWatchedEpisodesExpression.cs (76%) rename Shoko.Server/{Models => }/Filters/User/IsFavoriteExpression.cs (75%) rename Shoko.Server/{Models => }/Filters/User/MissingPermanentUserVotesExpression.cs (76%) rename Shoko.Server/{Models => }/Filters/UserDependentFilterExpression.cs (85%) rename Shoko.Server/{Models => }/Filters/UserDependentFilterable.cs (82%) create mode 100644 Shoko.Server/Filters/UserDependentSortingExpression.cs rename Shoko.Server/{Models => }/Filters/readme.md (100%) rename Shoko.Server/Models/{Filters => }/Filter.cs (92%) delete mode 100644 Shoko.Server/Models/Filters/Interfaces/ISortingExpression.cs delete mode 100644 Shoko.Server/Models/Filters/SortingExpression.cs delete mode 100644 Shoko.Server/Models/Filters/SortingSelectors/AddedDateSortingSelector.cs delete mode 100644 Shoko.Server/Models/Filters/SortingSelectors/AirDateSortingSelector.cs delete mode 100644 Shoko.Server/Models/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs delete mode 100644 Shoko.Server/Models/Filters/SortingSelectors/EpisodeCountSortingSelector.cs delete mode 100644 Shoko.Server/Models/Filters/SortingSelectors/LastAddedDateSortingSelector.cs delete mode 100644 Shoko.Server/Models/Filters/SortingSelectors/LastAirDateSortingSelector.cs delete mode 100644 Shoko.Server/Models/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs delete mode 100644 Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs delete mode 100644 Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs delete mode 100644 Shoko.Server/Models/Filters/SortingSelectors/SortingNameSortingSelector.cs delete mode 100644 Shoko.Server/Models/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs delete mode 100644 Shoko.Server/Models/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs delete mode 100644 Shoko.Server/Models/Filters/UserDependentSortingExpression.cs diff --git a/Shoko.CLI/Program.cs b/Shoko.CLI/Program.cs index 3f6365fa3..17c32b604 100644 --- a/Shoko.CLI/Program.cs +++ b/Shoko.CLI/Program.cs @@ -1,9 +1,14 @@ #region using System; using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NLog.Extensions.Logging; using Shoko.Server.Commands; +using Shoko.Server.Filters; +using Shoko.Server.Repositories; using Shoko.Server.Server; using Shoko.Server.Settings; using Shoko.Server.Utilities; @@ -13,6 +18,7 @@ namespace Shoko.CLI; public static class Program { + private static ILogger _logger; public static void Main() { try @@ -26,7 +32,7 @@ public static void Main() Utils.SetInstance(); Utils.InitLogger(); var logFactory = new LoggerFactory().AddNLog(); - var logger = logFactory.CreateLogger("Main"); + _logger = logFactory.CreateLogger("Main"); try { @@ -36,14 +42,38 @@ public static void Main() var startup = new Startup(logFactory.CreateLogger(), settingsProvider); startup.Start(); AddEventHandlers(); + // TODO Remove this after filter merge + Utils.ShokoServer.DBSetupCompleted += OnShokoServerOnDBSetupCompleted; startup.WaitForShutdown(); } catch (Exception e) { - logger.LogCritical(e, "The server failed to start"); + _logger.LogCritical(e, "The server failed to start"); } } + private static void OnShokoServerOnDBSetupCompleted(object? o, EventArgs eventArgs) + { + var comedyFilter = RepoFactory.Filter.GetAll().FirstOrDefault(a => a.Name.Equals("comedy", StringComparison.InvariantCultureIgnoreCase)); + if (comedyFilter == null) return; + var filterEvaluator = Utils.ServiceContainer.GetRequiredService(); + var s = Stopwatch.StartNew(); + var result = filterEvaluator.EvaluateFilter(comedyFilter, null); + s.Stop(); + _logger.LogInformation("Filtering took {Time}ms", s.ElapsedMilliseconds); + s.Restart(); + var groups = result.SelectMany(a => a.Select(b => new + { + Group = RepoFactory.AnimeGroup.GetByID(a.Key), Series = RepoFactory.AnimeSeries.GetByID(b) + })) + .GroupBy(a => a.Group, a => a.Series) + .ToDictionary(a => a.Key, a => a.ToList()); + s.Stop(); + _logger.LogInformation("Projecting results took {Time}ms", s.ElapsedMilliseconds); + _logger.LogInformation("Finished"); + } + + private static void AddEventHandlers() { Utils.YesNoRequired += OnUtilsOnYesNoRequired; diff --git a/Shoko.Server/API/v2/Modules/Common.cs b/Shoko.Server/API/v2/Modules/Common.cs index a63f68675..454680d6a 100644 --- a/Shoko.Server/API/v2/Modules/Common.cs +++ b/Shoko.Server/API/v2/Modules/Common.cs @@ -15,7 +15,6 @@ using Quartz; using QuartzJobFactory; using Shoko.Commons.Extensions; -using Shoko.Commons.Utils; using Shoko.Models.Enums; using Shoko.Models.Server; using Shoko.Server.API.v2.Models.common; @@ -29,6 +28,7 @@ using Shoko.Server.Server; using Shoko.Server.Settings; using Shoko.Server.Utilities; +using APIFilters = Shoko.Server.API.v2.Models.common.Filters; namespace Shoko.Server.API.v2.Modules; @@ -2727,7 +2727,7 @@ public object GetFilters([FromQuery] API_Call_Parameters para) internal object GetAllFilters(int uid, bool nocast, bool notag, int level, bool all, bool allpic, int pic, TagFilter.Filter tagfilter) { - var filters = new Filters + var filters = new APIFilters { id = 0, name = "Filters", viewed = 0, url = APIV2Helper.ConstructFilterUrl(HttpContext) }; @@ -2736,11 +2736,11 @@ internal object GetAllFilters(int uid, bool nocast, bool notag, int level, bool ((a.GroupsIds.ContainsKey(uid) && a.GroupsIds[uid].Count > 0) || a.IsDirectory)) .ToList(); - var _filters = new List(); + var _filters = new List(); foreach (var gf in allGfs) { - Filters filter; + APIFilters filter; if (!gf.IsDirectory) { filter = Filter.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, @@ -2748,7 +2748,7 @@ internal object GetAllFilters(int uid, bool nocast, bool notag, int level, bool } else { - filter = Filters.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, + filter = APIFilters.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, tagfilter); } @@ -2796,7 +2796,7 @@ internal object GetFilter(int id, int uid, bool nocast, bool notag, int level, b if (gf.IsDirectory) { // if it's a directory, it IS a filter-inception; - var fgs = Filters.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, + var fgs = APIFilters.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, tagfilter); return fgs; } diff --git a/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs b/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs index 7db5db0a7..3d1c50377 100644 --- a/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs +++ b/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs @@ -27,6 +27,7 @@ protected override void Process() if (GroupFilterID == 0) { RepoFactory.GroupFilter.CreateOrVerifyLockedFilters(); + RepoFactory.Filter.CreateOrVerifyLockedFilters(); return; } diff --git a/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs b/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs index 96ec14d34..e28d24e8b 100644 --- a/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs +++ b/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs @@ -9,6 +9,8 @@ using Newtonsoft.Json; using NHibernate; using NHibernate.Engine; +using NLog; +using Shoko.Server.Filters; using Shoko.Server.Models.Filters; namespace Shoko.Server.Databases.TypeConverters; @@ -32,7 +34,12 @@ public override object ConvertFrom(ITypeDescriptorContext context, System.Global return JsonConvert.DeserializeObject(s, new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore, - TypeNameHandling = TypeNameHandling.Objects + TypeNameHandling = TypeNameHandling.Objects, + Error = (sender, args) => + { + LogManager.GetCurrentClassLogger().Error(args.ErrorContext.Error); + args.ErrorContext.Handled = true; + } //Converters = new List { new FilterExpressionJsonConverter() }, }); } diff --git a/Shoko.Server/Models/Filters/Files/HasAudioLanguageExpression.cs b/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs similarity index 77% rename from Shoko.Server/Models/Filters/Files/HasAudioLanguageExpression.cs rename to Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs index feb4e0779..d8c5af40d 100644 --- a/Shoko.Server/Models/Filters/Files/HasAudioLanguageExpression.cs +++ b/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Files; +namespace Shoko.Server.Filters.Files; public class HasAudioLanguageExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Files/HasSubtitleLanguageExpression.cs b/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs similarity index 78% rename from Shoko.Server/Models/Filters/Files/HasSubtitleLanguageExpression.cs rename to Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs index 7bc335581..83e65775f 100644 --- a/Shoko.Server/Models/Filters/Files/HasSubtitleLanguageExpression.cs +++ b/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Files; +namespace Shoko.Server.Filters.Files; public class HasSubtitleLanguageExpression : FilterExpression { diff --git a/Shoko.Server/Filters/FilterEvaluator.cs b/Shoko.Server/Filters/FilterEvaluator.cs new file mode 100644 index 000000000..4bc961104 --- /dev/null +++ b/Shoko.Server/Filters/FilterEvaluator.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Shoko.Server.Filters.Interfaces; +using Shoko.Server.Filters.SortingSelectors; +using Shoko.Server.Models.Filters; +using Shoko.Server.Repositories; +using Shoko.Server.Repositories.Cached; + +namespace Shoko.Server.Filters; + +public class FilterEvaluator +{ + private record FilterableWithID(int SeriesID, int GroupID, IFilterable Filterable); + + private record Grouping(int GroupID, int[] SeriesIDs) : IGrouping + { + public IEnumerator GetEnumerator() => ((IEnumerable)SeriesIDs).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public int Key => GroupID; + } + + private readonly AnimeSeriesRepository _series = RepoFactory.AnimeSeries; + private readonly AnimeGroupRepository _groups = RepoFactory.AnimeGroup; + + /// + /// Evaluate the given filter, applying the necessary logic + /// + /// + /// + /// SeriesIDs, grouped by GroupID + /// + public IEnumerable> EvaluateFilter(Filter filter, int? userID) + { + ArgumentNullException.ThrowIfNull(filter); + var user = filter.Expression?.UserDependent ?? false; + if (user && userID == null) throw new ArgumentNullException(nameof(userID)); + var filterables = filter.ApplyAtSeriesLevel switch + { + true when user => _series.GetAll().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), + true => _series.GetAll().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToFilterable())), + false when user => _groups.GetAll().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), + false => _groups.GetAll().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToFilterable())), + }; + + // Filtering + var filtered = filterables.Where(a => filter.Expression?.Evaluate(a.Filterable) ?? true); + + // ordering + var ordered = OrderFilterables(filter, filtered); + + var result = ordered.GroupBy(a => a.GroupID, a => a.SeriesID); + if (!filter.ApplyAtSeriesLevel) + result = result.Select(a => new Grouping(a.Key, _series.GetByGroupID(a.Key).Select(ser => ser.AnimeSeriesID).ToArray())); + + return result; + } + + private static IOrderedEnumerable OrderFilterables(Filter filter, IEnumerable filtered) + { + var nameSorter = new NameSortingSelector(); + var ordered = filter.SortingExpression == null ? filtered.OrderBy(a => nameSorter.Evaluate(a.Filterable)) : + !filter.SortingExpression.Descending ? filtered.OrderBy(a => filter.SortingExpression.Evaluate(a.Filterable)) : + filtered.OrderByDescending(a => filter.SortingExpression.Evaluate(a.Filterable)); + + var next = filter.SortingExpression?.Next; + while (next != null) + { + var expr = next; + ordered = !next.Descending ? ordered.ThenBy(a => expr.Evaluate(a.Filterable)) : ordered.ThenByDescending(a => expr.Evaluate(a.Filterable)); + next = next.Next; + } + + return ordered; + } +} diff --git a/Shoko.Server/Models/Filters/FilterExpression.cs b/Shoko.Server/Filters/FilterExpression.cs similarity index 83% rename from Shoko.Server/Models/Filters/FilterExpression.cs rename to Shoko.Server/Filters/FilterExpression.cs index b299cf2bc..b2bb959c6 100644 --- a/Shoko.Server/Models/Filters/FilterExpression.cs +++ b/Shoko.Server/Filters/FilterExpression.cs @@ -1,7 +1,7 @@ using System.Runtime.Serialization; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters; +namespace Shoko.Server.Filters; public class FilterExpression : IFilterExpression { diff --git a/Shoko.Server/Models/Filters/FilterExtensions.cs b/Shoko.Server/Filters/FilterExtensions.cs similarity index 99% rename from Shoko.Server/Models/Filters/FilterExtensions.cs rename to Shoko.Server/Filters/FilterExtensions.cs index 23f314955..2280423f3 100644 --- a/Shoko.Server/Models/Filters/FilterExtensions.cs +++ b/Shoko.Server/Filters/FilterExtensions.cs @@ -4,12 +4,13 @@ using Shoko.Commons.Extensions; using Shoko.Models.Enums; using Shoko.Server.Extensions; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; +using Shoko.Server.Models; using Shoko.Server.Providers.AniDB; using Shoko.Server.Repositories; using AnimeType = Shoko.Models.Enums.AnimeType; -namespace Shoko.Server.Models.Filters; +namespace Shoko.Server.Filters; public static class FilterExtensions { diff --git a/Shoko.Server/Models/Filters/Filterable.cs b/Shoko.Server/Filters/Filterable.cs similarity index 96% rename from Shoko.Server/Models/Filters/Filterable.cs rename to Shoko.Server/Filters/Filterable.cs index 91f777161..992ab7901 100644 --- a/Shoko.Server/Models/Filters/Filterable.cs +++ b/Shoko.Server/Filters/Filterable.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using Shoko.Models.Enums; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters; +namespace Shoko.Server.Filters; public class Filterable : IFilterable { diff --git a/Shoko.Server/Models/Filters/Functions/DateAddFunction.cs b/Shoko.Server/Filters/Functions/DateAddFunction.cs similarity index 81% rename from Shoko.Server/Models/Filters/Functions/DateAddFunction.cs rename to Shoko.Server/Filters/Functions/DateAddFunction.cs index db8ebf4a9..40d943494 100644 --- a/Shoko.Server/Models/Filters/Functions/DateAddFunction.cs +++ b/Shoko.Server/Filters/Functions/DateAddFunction.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Functions; +namespace Shoko.Server.Filters.Functions; public class DateAddFunction : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Functions/DateDiffFunction.cs b/Shoko.Server/Filters/Functions/DateDiffFunction.cs similarity index 81% rename from Shoko.Server/Models/Filters/Functions/DateDiffFunction.cs rename to Shoko.Server/Filters/Functions/DateDiffFunction.cs index 109d54374..5e80bd332 100644 --- a/Shoko.Server/Models/Filters/Functions/DateDiffFunction.cs +++ b/Shoko.Server/Filters/Functions/DateDiffFunction.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Functions; +namespace Shoko.Server.Filters.Functions; public class DateDiffFunction : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Functions/TodayFunction.cs b/Shoko.Server/Filters/Functions/TodayFunction.cs similarity index 72% rename from Shoko.Server/Models/Filters/Functions/TodayFunction.cs rename to Shoko.Server/Filters/Functions/TodayFunction.cs index 1afd221c2..46a6f55df 100644 --- a/Shoko.Server/Models/Filters/Functions/TodayFunction.cs +++ b/Shoko.Server/Filters/Functions/TodayFunction.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Functions; +namespace Shoko.Server.Filters.Functions; public class TodayFunction : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Info/HasAnimeTypeExpression.cs b/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs similarity index 77% rename from Shoko.Server/Models/Filters/Info/HasAnimeTypeExpression.cs rename to Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs index fbd85b13f..3b8a6f9b4 100644 --- a/Shoko.Server/Models/Filters/Info/HasAnimeTypeExpression.cs +++ b/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; public class HasAnimeTypeExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Info/HasCustomTagExpression.cs b/Shoko.Server/Filters/Info/HasCustomTagExpression.cs similarity index 77% rename from Shoko.Server/Models/Filters/Info/HasCustomTagExpression.cs rename to Shoko.Server/Filters/Info/HasCustomTagExpression.cs index 7c45f4949..8faeece59 100644 --- a/Shoko.Server/Models/Filters/Info/HasCustomTagExpression.cs +++ b/Shoko.Server/Filters/Info/HasCustomTagExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; public class HasCustomTagExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Info/HasMissingEpisodesCollectingExpression.cs b/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs similarity index 75% rename from Shoko.Server/Models/Filters/Info/HasMissingEpisodesCollectingExpression.cs rename to Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs index 873a989ef..15e371c39 100644 --- a/Shoko.Server/Models/Filters/Info/HasMissingEpisodesCollectingExpression.cs +++ b/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; public class HasMissingEpisodesCollectingExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Info/HasMissingEpisodesExpression.cs b/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs similarity index 74% rename from Shoko.Server/Models/Filters/Info/HasMissingEpisodesExpression.cs rename to Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs index 0ea2c9278..0a109cd65 100644 --- a/Shoko.Server/Models/Filters/Info/HasMissingEpisodesExpression.cs +++ b/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; public class HasMissingEpisodesExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Info/HasTMDbLinkExpression.cs b/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs similarity index 73% rename from Shoko.Server/Models/Filters/Info/HasTMDbLinkExpression.cs rename to Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs index 212f25503..830ef057d 100644 --- a/Shoko.Server/Models/Filters/Info/HasTMDbLinkExpression.cs +++ b/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; public class HasTMDbLinkExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Info/HasTagExpression.cs b/Shoko.Server/Filters/Info/HasTagExpression.cs similarity index 76% rename from Shoko.Server/Models/Filters/Info/HasTagExpression.cs rename to Shoko.Server/Filters/Info/HasTagExpression.cs index 53c656c5a..a5f8763f9 100644 --- a/Shoko.Server/Models/Filters/Info/HasTagExpression.cs +++ b/Shoko.Server/Filters/Info/HasTagExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; public class HasTagExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Info/HasTraktLinkExpression.cs b/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs similarity index 73% rename from Shoko.Server/Models/Filters/Info/HasTraktLinkExpression.cs rename to Shoko.Server/Filters/Info/HasTraktLinkExpression.cs index fbc718fd2..5a7464fe5 100644 --- a/Shoko.Server/Models/Filters/Info/HasTraktLinkExpression.cs +++ b/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; public class HasTraktLinkExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Info/HasTvDBLinkExpression.cs b/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs similarity index 73% rename from Shoko.Server/Models/Filters/Info/HasTvDBLinkExpression.cs rename to Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs index 2499dd6fc..8656acb86 100644 --- a/Shoko.Server/Models/Filters/Info/HasTvDBLinkExpression.cs +++ b/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; public class HasTvDBLinkExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Info/HasVideoSourceExpression.cs b/Shoko.Server/Filters/Info/HasVideoSourceExpression.cs similarity index 77% rename from Shoko.Server/Models/Filters/Info/HasVideoSourceExpression.cs rename to Shoko.Server/Filters/Info/HasVideoSourceExpression.cs index c83923b44..cf5d80a31 100644 --- a/Shoko.Server/Models/Filters/Info/HasVideoSourceExpression.cs +++ b/Shoko.Server/Filters/Info/HasVideoSourceExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; public class HasVideoSourceExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Info/InSeasonExpression.cs b/Shoko.Server/Filters/Info/InSeasonExpression.cs similarity index 80% rename from Shoko.Server/Models/Filters/Info/InSeasonExpression.cs rename to Shoko.Server/Filters/Info/InSeasonExpression.cs index 07ac0facc..3b0a4a7ab 100644 --- a/Shoko.Server/Models/Filters/Info/InSeasonExpression.cs +++ b/Shoko.Server/Filters/Info/InSeasonExpression.cs @@ -1,7 +1,7 @@ using Shoko.Models.Enums; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; public class InSeasonExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Info/InYearExpression.cs b/Shoko.Server/Filters/Info/InYearExpression.cs similarity index 76% rename from Shoko.Server/Models/Filters/Info/InYearExpression.cs rename to Shoko.Server/Filters/Info/InYearExpression.cs index eac6af19e..eff4821be 100644 --- a/Shoko.Server/Models/Filters/Info/InYearExpression.cs +++ b/Shoko.Server/Filters/Info/InYearExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; public class InYearExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs b/Shoko.Server/Filters/Info/IsFinishedExpression.cs similarity index 73% rename from Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs rename to Shoko.Server/Filters/Info/IsFinishedExpression.cs index 44d4a9e7c..fa53b319d 100644 --- a/Shoko.Server/Models/Filters/Info/IsFinishedExpression.cs +++ b/Shoko.Server/Filters/Info/IsFinishedExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; public class IsFinishedExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Info/MissingTMDbLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs similarity index 79% rename from Shoko.Server/Models/Filters/Info/MissingTMDbLinkExpression.cs rename to Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs index 754c9fe39..e76a47562 100644 --- a/Shoko.Server/Models/Filters/Info/MissingTMDbLinkExpression.cs +++ b/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; /// /// Missing Links include logic for whether a link should exist diff --git a/Shoko.Server/Models/Filters/Info/MissingTraktLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs similarity index 79% rename from Shoko.Server/Models/Filters/Info/MissingTraktLinkExpression.cs rename to Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs index 865007c0e..721d58b0b 100644 --- a/Shoko.Server/Models/Filters/Info/MissingTraktLinkExpression.cs +++ b/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; /// /// Missing Links include logic for whether a link should exist diff --git a/Shoko.Server/Models/Filters/Info/MissingTvDBLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs similarity index 79% rename from Shoko.Server/Models/Filters/Info/MissingTvDBLinkExpression.cs rename to Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs index 41f46a2b9..553e7fb9c 100644 --- a/Shoko.Server/Models/Filters/Info/MissingTvDBLinkExpression.cs +++ b/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Info; +namespace Shoko.Server.Filters.Info; /// /// Missing Links include logic for whether a link should exist diff --git a/Shoko.Server/Models/Filters/Interfaces/IFilterExpression.cs b/Shoko.Server/Filters/Interfaces/IFilterExpression.cs similarity index 85% rename from Shoko.Server/Models/Filters/Interfaces/IFilterExpression.cs rename to Shoko.Server/Filters/Interfaces/IFilterExpression.cs index f00c33866..07341ed18 100644 --- a/Shoko.Server/Models/Filters/Interfaces/IFilterExpression.cs +++ b/Shoko.Server/Filters/Interfaces/IFilterExpression.cs @@ -1,4 +1,4 @@ -namespace Shoko.Server.Models.Filters.Interfaces; +namespace Shoko.Server.Filters.Interfaces; public interface IFilterExpression { diff --git a/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs b/Shoko.Server/Filters/Interfaces/IFilterable.cs similarity index 98% rename from Shoko.Server/Models/Filters/Interfaces/IFilterable.cs rename to Shoko.Server/Filters/Interfaces/IFilterable.cs index 174d0f25c..bd140d353 100644 --- a/Shoko.Server/Models/Filters/Interfaces/IFilterable.cs +++ b/Shoko.Server/Filters/Interfaces/IFilterable.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Shoko.Models.Enums; -namespace Shoko.Server.Models.Filters.Interfaces; +namespace Shoko.Server.Filters.Interfaces; public interface IFilterable { diff --git a/Shoko.Server/Filters/Interfaces/ISortingExpression.cs b/Shoko.Server/Filters/Interfaces/ISortingExpression.cs new file mode 100644 index 000000000..037a7ec4d --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/ISortingExpression.cs @@ -0,0 +1,5 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface ISortingExpression : IFilterExpression { } + +public interface IUserDependentSortingExpression : ISortingExpression, IUserDependentFilterExpression { } diff --git a/Shoko.Server/Models/Filters/Interfaces/IUserDependentFilterable.cs b/Shoko.Server/Filters/Interfaces/IUserDependentFilterable.cs similarity index 93% rename from Shoko.Server/Models/Filters/Interfaces/IUserDependentFilterable.cs rename to Shoko.Server/Filters/Interfaces/IUserDependentFilterable.cs index 69bcbe3b9..5e3cbde9e 100644 --- a/Shoko.Server/Models/Filters/Interfaces/IUserDependentFilterable.cs +++ b/Shoko.Server/Filters/Interfaces/IUserDependentFilterable.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; -namespace Shoko.Server.Models.Filters.Interfaces; +namespace Shoko.Server.Filters.Interfaces; public interface IUserDependentFilterable : IFilterable { diff --git a/Shoko.Server/Models/Filters/Logic/AndExpression.cs b/Shoko.Server/Filters/Logic/AndExpression.cs similarity index 83% rename from Shoko.Server/Models/Filters/Logic/AndExpression.cs rename to Shoko.Server/Filters/Logic/AndExpression.cs index 8a58d98a8..f197d4fd7 100644 --- a/Shoko.Server/Models/Filters/Logic/AndExpression.cs +++ b/Shoko.Server/Filters/Logic/AndExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic; +namespace Shoko.Server.Filters.Logic; public class AndExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/EqualExpression.cs similarity index 91% rename from Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs rename to Shoko.Server/Filters/Logic/DateTimes/EqualExpression.cs index 764aa53f7..ea91d392d 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/EqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/EqualExpression.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.DateTimes; +namespace Shoko.Server.Filters.Logic.DateTimes; public class EqualExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs similarity index 91% rename from Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs rename to Shoko.Server/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs index 747e4b2ca..47c1cb329 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.DateTimes; +namespace Shoko.Server.Filters.Logic.DateTimes; public class GreaterThanEqualExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/GreaterThanExpression.cs similarity index 91% rename from Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs rename to Shoko.Server/Filters/Logic/DateTimes/GreaterThanExpression.cs index 246926192..7f486135d 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/GreaterThanExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/GreaterThanExpression.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.DateTimes; +namespace Shoko.Server.Filters.Logic.DateTimes; public class GreaterThanExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/LessThanEqualExpression.cs similarity index 90% rename from Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs rename to Shoko.Server/Filters/Logic/DateTimes/LessThanEqualExpression.cs index a5cd29e91..90372327c 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/LessThanEqualExpression.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.DateTimes; +namespace Shoko.Server.Filters.Logic.DateTimes; public class LessThanEqualExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/LessThanExpression.cs similarity index 91% rename from Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs rename to Shoko.Server/Filters/Logic/DateTimes/LessThanExpression.cs index 167faa956..73e300335 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/LessThanExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/LessThanExpression.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.DateTimes; +namespace Shoko.Server.Filters.Logic.DateTimes; public class LessThanExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/NotEqualExpression.cs similarity index 91% rename from Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs rename to Shoko.Server/Filters/Logic/DateTimes/NotEqualExpression.cs index 699e25240..67fcc97fc 100644 --- a/Shoko.Server/Models/Filters/Logic/DateTimes/NotEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/NotEqualExpression.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.DateTimes; +namespace Shoko.Server.Filters.Logic.DateTimes; public class NotEqualExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/NotExpression.cs b/Shoko.Server/Filters/Logic/NotExpression.cs similarity index 78% rename from Shoko.Server/Models/Filters/Logic/NotExpression.cs rename to Shoko.Server/Filters/Logic/NotExpression.cs index 4e30ef52e..385a68633 100644 --- a/Shoko.Server/Models/Filters/Logic/NotExpression.cs +++ b/Shoko.Server/Filters/Logic/NotExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic; +namespace Shoko.Server.Filters.Logic; public class NotExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/EqualExpression.cs similarity index 86% rename from Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs rename to Shoko.Server/Filters/Logic/Numbers/EqualExpression.cs index abe9f8047..b8017021a 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/EqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/EqualExpression.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.Numbers; +namespace Shoko.Server.Filters.Logic.Numbers; public class EqualExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/GreaterThanEqualExpression.cs similarity index 87% rename from Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs rename to Shoko.Server/Filters/Logic/Numbers/GreaterThanEqualExpression.cs index 48315d92d..776d00355 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/GreaterThanEqualExpression.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.Numbers; +namespace Shoko.Server.Filters.Logic.Numbers; public class GreaterThanEqualExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs b/Shoko.Server/Filters/Logic/Numbers/GreaterThanExpression.cs similarity index 86% rename from Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs rename to Shoko.Server/Filters/Logic/Numbers/GreaterThanExpression.cs index 341462b0f..369898896 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/GreaterThanExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/GreaterThanExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.Numbers; +namespace Shoko.Server.Filters.Logic.Numbers; public class GreaterThanExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/LessThanEqualExpression.cs similarity index 87% rename from Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs rename to Shoko.Server/Filters/Logic/Numbers/LessThanEqualExpression.cs index a2c9a14e3..e065bec0b 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/LessThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/LessThanEqualExpression.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.Numbers; +namespace Shoko.Server.Filters.Logic.Numbers; public class LessThanEqualExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs b/Shoko.Server/Filters/Logic/Numbers/LessThanExpression.cs similarity index 86% rename from Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs rename to Shoko.Server/Filters/Logic/Numbers/LessThanExpression.cs index c78ca209e..539c58638 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/LessThanExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/LessThanExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.Numbers; +namespace Shoko.Server.Filters.Logic.Numbers; public class LessThanExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NotEqualExpression.cs similarity index 86% rename from Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs rename to Shoko.Server/Filters/Logic/Numbers/NotEqualExpression.cs index 83cc645a9..e66565360 100644 --- a/Shoko.Server/Models/Filters/Logic/Numbers/NotEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NotEqualExpression.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.Numbers; +namespace Shoko.Server.Filters.Logic.Numbers; public class NotEqualExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/OrExpression.cs b/Shoko.Server/Filters/Logic/OrExpression.cs similarity index 83% rename from Shoko.Server/Models/Filters/Logic/OrExpression.cs rename to Shoko.Server/Filters/Logic/OrExpression.cs index a3eaffba4..6fc84e5d5 100644 --- a/Shoko.Server/Models/Filters/Logic/OrExpression.cs +++ b/Shoko.Server/Filters/Logic/OrExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic; +namespace Shoko.Server.Filters.Logic; public class OrExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs b/Shoko.Server/Filters/Logic/Strings/ContainsExpression.cs similarity index 88% rename from Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs rename to Shoko.Server/Filters/Logic/Strings/ContainsExpression.cs index 331787745..505c7ec42 100644 --- a/Shoko.Server/Models/Filters/Logic/Strings/ContainsExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/ContainsExpression.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.Strings; +namespace Shoko.Server.Filters.Logic.Strings; public class ContainsExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs b/Shoko.Server/Filters/Logic/Strings/EqualExpression.cs similarity index 87% rename from Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs rename to Shoko.Server/Filters/Logic/Strings/EqualExpression.cs index 21f5a2286..586b17377 100644 --- a/Shoko.Server/Models/Filters/Logic/Strings/EqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/EqualExpression.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.Strings; +namespace Shoko.Server.Filters.Logic.Strings; public class EqualExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs b/Shoko.Server/Filters/Logic/Strings/NotEqualExpression.cs similarity index 87% rename from Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs rename to Shoko.Server/Filters/Logic/Strings/NotEqualExpression.cs index ed52a38c9..80e3f0e7f 100644 --- a/Shoko.Server/Models/Filters/Logic/Strings/NotEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/NotEqualExpression.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic.Strings; +namespace Shoko.Server.Filters.Logic.Strings; public class NotEqualExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Logic/XorExpression.cs b/Shoko.Server/Filters/Logic/XorExpression.cs similarity index 83% rename from Shoko.Server/Models/Filters/Logic/XorExpression.cs rename to Shoko.Server/Filters/Logic/XorExpression.cs index 7e1120e2c..867d16b59 100644 --- a/Shoko.Server/Models/Filters/Logic/XorExpression.cs +++ b/Shoko.Server/Filters/Logic/XorExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Logic; +namespace Shoko.Server.Filters.Logic; public class XorExpression : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Selectors/AddedDateSelector.cs b/Shoko.Server/Filters/Selectors/AddedDateSelector.cs similarity index 72% rename from Shoko.Server/Models/Filters/Selectors/AddedDateSelector.cs rename to Shoko.Server/Filters/Selectors/AddedDateSelector.cs index 707ebf820..93118f37f 100644 --- a/Shoko.Server/Models/Filters/Selectors/AddedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/AddedDateSelector.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Selectors; +namespace Shoko.Server.Filters.Selectors; public class AddedDateSelector : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Selectors/AirDateSelector.cs b/Shoko.Server/Filters/Selectors/AirDateSelector.cs similarity index 72% rename from Shoko.Server/Models/Filters/Selectors/AirDateSelector.cs rename to Shoko.Server/Filters/Selectors/AirDateSelector.cs index da7d67211..f2f3a0599 100644 --- a/Shoko.Server/Models/Filters/Selectors/AirDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/AirDateSelector.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Selectors; +namespace Shoko.Server.Filters.Selectors; public class AirDateSelector : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs b/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs similarity index 71% rename from Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs rename to Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs index f5cbe711b..204df8857 100644 --- a/Shoko.Server/Models/Filters/Selectors/AudioLanguageCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Selectors; +namespace Shoko.Server.Filters.Selectors; public class AudioLanguageCountSelector : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs b/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs similarity index 70% rename from Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs rename to Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs index 6a673db99..95452aae6 100644 --- a/Shoko.Server/Models/Filters/Selectors/EpisodeCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Selectors; +namespace Shoko.Server.Filters.Selectors; public class EpisodeCountSelector : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Selectors/HighestAniDBRatingSelector.cs b/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs similarity index 74% rename from Shoko.Server/Models/Filters/Selectors/HighestAniDBRatingSelector.cs rename to Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs index 20188c847..380912729 100644 --- a/Shoko.Server/Models/Filters/Selectors/HighestAniDBRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Selectors; +namespace Shoko.Server.Filters.Selectors; public class HighestAniDBRatingSelector : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs b/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs similarity index 76% rename from Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs rename to Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs index 6ba6c7fa0..a935b2fd8 100644 --- a/Shoko.Server/Models/Filters/Selectors/HighestUserRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Selectors; +namespace Shoko.Server.Filters.Selectors; public class HighestUserRatingSelector : UserDependentFilterExpression { diff --git a/Shoko.Server/Models/Filters/Selectors/LastAddedDateSelector.cs b/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs similarity index 73% rename from Shoko.Server/Models/Filters/Selectors/LastAddedDateSelector.cs rename to Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs index 042b66652..cf9873396 100644 --- a/Shoko.Server/Models/Filters/Selectors/LastAddedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Selectors; +namespace Shoko.Server.Filters.Selectors; public class LastAddedDateSelector : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Selectors/LastAirDateSelector.cs b/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs similarity index 72% rename from Shoko.Server/Models/Filters/Selectors/LastAirDateSelector.cs rename to Shoko.Server/Filters/Selectors/LastAirDateSelector.cs index 8d976a678..cc4771dba 100644 --- a/Shoko.Server/Models/Filters/Selectors/LastAirDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Selectors; +namespace Shoko.Server.Filters.Selectors; public class LastAirDateSelector : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs b/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs similarity index 75% rename from Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs rename to Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs index 564a12a9f..acdc8fafb 100644 --- a/Shoko.Server/Models/Filters/Selectors/LastWatchedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Selectors; +namespace Shoko.Server.Filters.Selectors; public class LastWatchedDateSelector : UserDependentFilterExpression { diff --git a/Shoko.Server/Models/Filters/Selectors/LowestAniDBRatingSelector.cs b/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs similarity index 74% rename from Shoko.Server/Models/Filters/Selectors/LowestAniDBRatingSelector.cs rename to Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs index 738795bcc..c19a7abfd 100644 --- a/Shoko.Server/Models/Filters/Selectors/LowestAniDBRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Selectors; +namespace Shoko.Server.Filters.Selectors; public class LowestAniDBRatingSelector : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs b/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs similarity index 75% rename from Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs rename to Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs index c35146005..778e7faaa 100644 --- a/Shoko.Server/Models/Filters/Selectors/LowestUserRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Selectors; +namespace Shoko.Server.Filters.Selectors; public class LowestUserRatingSelector : UserDependentFilterExpression { diff --git a/Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs b/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs similarity index 72% rename from Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs rename to Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs index 9435f193d..09d61d841 100644 --- a/Shoko.Server/Models/Filters/Selectors/SubtitleLanguageCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Selectors; +namespace Shoko.Server.Filters.Selectors; public class SubtitleLanguageCountSelector : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs b/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs similarity index 71% rename from Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs rename to Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs index a28f683dd..affde8724 100644 --- a/Shoko.Server/Models/Filters/Selectors/TotalEpisodeCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Selectors; +namespace Shoko.Server.Filters.Selectors; public class TotalEpisodeCountSelector : FilterExpression { diff --git a/Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs b/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs similarity index 74% rename from Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs rename to Shoko.Server/Filters/Selectors/WatchedDateSelector.cs index 83396a7f5..6d8a173fd 100644 --- a/Shoko.Server/Models/Filters/Selectors/WatchedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.Selectors; +namespace Shoko.Server.Filters.Selectors; public class WatchedDateSelector : UserDependentFilterExpression { diff --git a/Shoko.Server/Filters/SortingExpression.cs b/Shoko.Server/Filters/SortingExpression.cs new file mode 100644 index 000000000..bbc663c14 --- /dev/null +++ b/Shoko.Server/Filters/SortingExpression.cs @@ -0,0 +1,9 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters; + +public abstract class SortingExpression : FilterExpression, ISortingExpression +{ + public bool Descending { get; set; } // take advantage of default(bool) being false + public SortingExpression Next { get; set; } +} diff --git a/Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs new file mode 100644 index 000000000..7a57cb95c --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class AddedDateSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override object Evaluate(IFilterable f) => f.AddedDate; +} diff --git a/Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs new file mode 100644 index 000000000..0176e12fd --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs @@ -0,0 +1,12 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class AirDateSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public DateTime DefaultValue { get; set; } + public override object Evaluate(IFilterable f) => f.AirDate ?? DefaultValue; +} diff --git a/Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs new file mode 100644 index 000000000..7c31fb8bb --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class AudioLanguageCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override object Evaluate(IFilterable f) => f.AudioLanguages.Count; +} diff --git a/Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs new file mode 100644 index 000000000..9bbeca041 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class EpisodeCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override object Evaluate(IFilterable f) => f.EpisodeCount; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs similarity index 54% rename from Shoko.Server/Models/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs rename to Shoko.Server/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs index 9c2a2b831..1cebbc22d 100644 --- a/Shoko.Server/Models/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs @@ -1,11 +1,11 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.SortingSelectors; +namespace Shoko.Server.Filters.SortingSelectors; -public class HighestAniDBRatingSortingSelector : SortingExpression +public class HighestAniDBRatingSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(IFilterable f) => Convert.ToDouble(f.HighestAniDBRating); + public override object Evaluate(IFilterable f) => Convert.ToDouble(f.HighestAniDBRating); } diff --git a/Shoko.Server/Models/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs similarity index 54% rename from Shoko.Server/Models/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs rename to Shoko.Server/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs index d2fb5388b..dbe1eda9b 100644 --- a/Shoko.Server/Models/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs @@ -1,11 +1,11 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.SortingSelectors; +namespace Shoko.Server.Filters.SortingSelectors; -public class HighestUserRatingSortingSelector : UserDependentSortingExpression +public class HighestUserRatingSortingSelector : UserDependentSortingExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override double Evaluate(IUserDependentFilterable f) => Convert.ToDouble(f.HighestUserRating); + public override object Evaluate(IUserDependentFilterable f) => Convert.ToDouble(f.HighestUserRating); } diff --git a/Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs new file mode 100644 index 000000000..b3eb79c1d --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class LastAddedDateSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override object Evaluate(IFilterable f) => f.LastAddedDate; +} diff --git a/Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs new file mode 100644 index 000000000..4f0bf0e39 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs @@ -0,0 +1,12 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class LastAirDateSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public DateTime DefaultValue { get; set; } + public override object Evaluate(IFilterable f) => f.LastAirDate ?? DefaultValue; +} diff --git a/Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs new file mode 100644 index 000000000..3186ee6ee --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs @@ -0,0 +1,12 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class LastWatchedDateSortingSelector : UserDependentSortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + public DateTime DefaultValue { get; set; } + public override object Evaluate(IUserDependentFilterable f) => f.LastWatchedDate ?? DefaultValue; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs similarity index 54% rename from Shoko.Server/Models/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs rename to Shoko.Server/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs index 483c6d6de..2ad2ee6df 100644 --- a/Shoko.Server/Models/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs @@ -1,11 +1,11 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.SortingSelectors; +namespace Shoko.Server.Filters.SortingSelectors; -public class LowestAniDBRatingSortingSelector : SortingExpression +public class LowestAniDBRatingSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(IFilterable f) => Convert.ToDouble(f.LowestAniDBRating); + public override object Evaluate(IFilterable f) => Convert.ToDouble(f.LowestAniDBRating); } diff --git a/Shoko.Server/Models/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs similarity index 54% rename from Shoko.Server/Models/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs rename to Shoko.Server/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs index e64c6eec6..74c9193c3 100644 --- a/Shoko.Server/Models/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs @@ -1,11 +1,11 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.SortingSelectors; +namespace Shoko.Server.Filters.SortingSelectors; -public class LowestUserRatingSortingSelector : UserDependentSortingExpression +public class LowestUserRatingSortingSelector : UserDependentSortingExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override double Evaluate(IUserDependentFilterable f) => Convert.ToDouble(f.LowestUserRating); + public override object Evaluate(IUserDependentFilterable f) => Convert.ToDouble(f.LowestUserRating); } diff --git a/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs new file mode 100644 index 000000000..c3fa2dfc9 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class MissingEpisodeCollectingCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override object Evaluate(IFilterable f) => f.MissingEpisodesCollecting; +} diff --git a/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs new file mode 100644 index 000000000..9917bfd10 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class MissingEpisodeCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override object Evaluate(IFilterable f) => f.MissingEpisodes; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/NameSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/NameSortingSelector.cs similarity index 50% rename from Shoko.Server/Models/Filters/SortingSelectors/NameSortingSelector.cs rename to Shoko.Server/Filters/SortingSelectors/NameSortingSelector.cs index 516175915..243e1dddf 100644 --- a/Shoko.Server/Models/Filters/SortingSelectors/NameSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/NameSortingSelector.cs @@ -1,8 +1,8 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.SortingSelectors; +namespace Shoko.Server.Filters.SortingSelectors; -public class NameSortingSelector : SortingExpression +public class NameSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; diff --git a/Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs new file mode 100644 index 000000000..f6007d55c --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class SortingNameSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override object Evaluate(IFilterable f) => f.SortingName; +} diff --git a/Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs new file mode 100644 index 000000000..13306cadc --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class SubtitleLanguageCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override object Evaluate(IFilterable f) => f.SubtitleLanguages.Count; +} diff --git a/Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs new file mode 100644 index 000000000..159d6df52 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs @@ -0,0 +1,10 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class TotalEpisodeCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override object Evaluate(IFilterable f) => f.TotalEpisodeCount; +} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/WatchedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/WatchedDateSortingSelector.cs similarity index 50% rename from Shoko.Server/Models/Filters/SortingSelectors/WatchedDateSortingSelector.cs rename to Shoko.Server/Filters/SortingSelectors/WatchedDateSortingSelector.cs index bf16d82ff..a9c57f0ee 100644 --- a/Shoko.Server/Models/Filters/SortingSelectors/WatchedDateSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/WatchedDateSortingSelector.cs @@ -1,12 +1,12 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.SortingSelectors; +namespace Shoko.Server.Filters.SortingSelectors; -public class WatchedDateSortingSelector : UserDependentSortingExpression +public class WatchedDateSortingSelector : UserDependentSortingExpression { public override bool TimeDependent => false; public override bool UserDependent => true; public DateTime DefaultValue { get; set; } - public override DateTime Evaluate(IUserDependentFilterable f) => f.WatchedDate ?? DefaultValue; + public override object Evaluate(IUserDependentFilterable f) => f.WatchedDate ?? DefaultValue; } diff --git a/Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs b/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs similarity index 76% rename from Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs rename to Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs index eea3d41e5..c6a45ccc0 100644 --- a/Shoko.Server/Models/Filters/User/HasPermanentUserVotesExpression.cs +++ b/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.User; +namespace Shoko.Server.Filters.User; public class HasPermanentUserVotesExpression : UserDependentFilterExpression { diff --git a/Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs b/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs similarity index 76% rename from Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs rename to Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs index e3a37a411..3ddee3412 100644 --- a/Shoko.Server/Models/Filters/User/HasUnwatchedEpisodesExpression.cs +++ b/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.User; +namespace Shoko.Server.Filters.User; public class HasUnwatchedEpisodesExpression : UserDependentFilterExpression { diff --git a/Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs b/Shoko.Server/Filters/User/HasUserVotesExpression.cs similarity index 75% rename from Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs rename to Shoko.Server/Filters/User/HasUserVotesExpression.cs index 6064e30b5..084e476c5 100644 --- a/Shoko.Server/Models/Filters/User/HasUserVotesExpression.cs +++ b/Shoko.Server/Filters/User/HasUserVotesExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.User; +namespace Shoko.Server.Filters.User; public class HasUserVotesExpression : UserDependentFilterExpression { diff --git a/Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs b/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs similarity index 76% rename from Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs rename to Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs index 75d193d01..806c569ea 100644 --- a/Shoko.Server/Models/Filters/User/HasWatchedEpisodesExpression.cs +++ b/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.User; +namespace Shoko.Server.Filters.User; public class HasWatchedEpisodesExpression : UserDependentFilterExpression { diff --git a/Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs b/Shoko.Server/Filters/User/IsFavoriteExpression.cs similarity index 75% rename from Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs rename to Shoko.Server/Filters/User/IsFavoriteExpression.cs index 45c7d0e5c..eb77598d1 100644 --- a/Shoko.Server/Models/Filters/User/IsFavoriteExpression.cs +++ b/Shoko.Server/Filters/User/IsFavoriteExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.User; +namespace Shoko.Server.Filters.User; public class IsFavoriteExpression : UserDependentFilterExpression { diff --git a/Shoko.Server/Models/Filters/User/MissingPermanentUserVotesExpression.cs b/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs similarity index 76% rename from Shoko.Server/Models/Filters/User/MissingPermanentUserVotesExpression.cs rename to Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs index 44424a650..e10db005a 100644 --- a/Shoko.Server/Models/Filters/User/MissingPermanentUserVotesExpression.cs +++ b/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs @@ -1,6 +1,6 @@ -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters.User; +namespace Shoko.Server.Filters.User; public class MissingPermanentUserVotesExpression : UserDependentFilterExpression { diff --git a/Shoko.Server/Models/Filters/UserDependentFilterExpression.cs b/Shoko.Server/Filters/UserDependentFilterExpression.cs similarity index 85% rename from Shoko.Server/Models/Filters/UserDependentFilterExpression.cs rename to Shoko.Server/Filters/UserDependentFilterExpression.cs index a3da711ec..3a734efc1 100644 --- a/Shoko.Server/Models/Filters/UserDependentFilterExpression.cs +++ b/Shoko.Server/Filters/UserDependentFilterExpression.cs @@ -1,7 +1,7 @@ using System; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters; +namespace Shoko.Server.Filters; public abstract class UserDependentFilterExpression : FilterExpression, IUserDependentFilterExpression { diff --git a/Shoko.Server/Models/Filters/UserDependentFilterable.cs b/Shoko.Server/Filters/UserDependentFilterable.cs similarity index 82% rename from Shoko.Server/Models/Filters/UserDependentFilterable.cs rename to Shoko.Server/Filters/UserDependentFilterable.cs index 169bbd256..aa12ec5fe 100644 --- a/Shoko.Server/Models/Filters/UserDependentFilterable.cs +++ b/Shoko.Server/Filters/UserDependentFilterable.cs @@ -1,8 +1,7 @@ using System; -using System.Collections.Generic; -using Shoko.Server.Models.Filters.Interfaces; +using Shoko.Server.Filters.Interfaces; -namespace Shoko.Server.Models.Filters; +namespace Shoko.Server.Filters; public class UserDependentFilterable : Filterable, IUserDependentFilterable { diff --git a/Shoko.Server/Filters/UserDependentSortingExpression.cs b/Shoko.Server/Filters/UserDependentSortingExpression.cs new file mode 100644 index 000000000..cacc6224a --- /dev/null +++ b/Shoko.Server/Filters/UserDependentSortingExpression.cs @@ -0,0 +1,17 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters; + +public abstract class UserDependentSortingExpression : SortingExpression, IUserDependentSortingExpression +{ + public override object Evaluate(IFilterable f) + { + if (UserDependent && f is not IUserDependentFilterable) + throw new ArgumentException("User Dependent Filter was given an IFilterable, rather than an IUserDependentFilterable"); + + return Evaluate((IUserDependentFilterable)f); + } + + public abstract object Evaluate(IUserDependentFilterable f); +} diff --git a/Shoko.Server/Models/Filters/readme.md b/Shoko.Server/Filters/readme.md similarity index 100% rename from Shoko.Server/Models/Filters/readme.md rename to Shoko.Server/Filters/readme.md diff --git a/Shoko.Server/Models/Filters/Filter.cs b/Shoko.Server/Models/Filter.cs similarity index 92% rename from Shoko.Server/Models/Filters/Filter.cs rename to Shoko.Server/Models/Filter.cs index da64a2848..7e6dec661 100644 --- a/Shoko.Server/Models/Filters/Filter.cs +++ b/Shoko.Server/Models/Filter.cs @@ -1,4 +1,6 @@ +using System; using Shoko.Models.Enums; +using Shoko.Server.Filters; namespace Shoko.Server.Models.Filters; diff --git a/Shoko.Server/Models/Filters/Interfaces/ISortingExpression.cs b/Shoko.Server/Models/Filters/Interfaces/ISortingExpression.cs deleted file mode 100644 index e88d283a3..000000000 --- a/Shoko.Server/Models/Filters/Interfaces/ISortingExpression.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Shoko.Server.Models.Filters.Interfaces; - -public interface ISortingExpression -{ - bool TimeDependent { get; } - bool UserDependent { get; } -} - -public interface ISortingExpression -{ - T Evaluate(IFilterable f); -} - -public interface IUserDependentSortingExpression -{ - T Evaluate(IUserDependentFilterable f); -} diff --git a/Shoko.Server/Models/Filters/SortingExpression.cs b/Shoko.Server/Models/Filters/SortingExpression.cs deleted file mode 100644 index 0b7ac204c..000000000 --- a/Shoko.Server/Models/Filters/SortingExpression.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Models.Filters; - -public abstract class SortingExpression : FilterExpression, ISortingExpression -{ - public bool Descending { get; set; } // take advantage of default(bool) being false -} - -public abstract class SortingExpression : SortingExpression, ISortingExpression, IComparer where T : IComparable -{ - public SortingExpression Next { get; set; } - public abstract T Evaluate(IFilterable f); - public virtual int Compare(IFilterable x, IFilterable y) - { - var valueX = Evaluate(x); - var valueY = Evaluate(y); - if (Equals(valueX, valueY)) return Next?.Compare(x, y) ?? 0; - if (valueX == null) return 1; - if (valueY == null) return -1; - return valueX.CompareTo(valueY); - } -} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/AddedDateSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/AddedDateSortingSelector.cs deleted file mode 100644 index 990128980..000000000 --- a/Shoko.Server/Models/Filters/SortingSelectors/AddedDateSortingSelector.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Models.Filters.SortingSelectors; - -public class AddedDateSortingSelector : SortingExpression -{ - public override bool TimeDependent => false; - public override bool UserDependent => false; - public override DateTime Evaluate(IFilterable f) => f.AddedDate; -} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/AirDateSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/AirDateSortingSelector.cs deleted file mode 100644 index fe7b48536..000000000 --- a/Shoko.Server/Models/Filters/SortingSelectors/AirDateSortingSelector.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Models.Filters.SortingSelectors; - -public class AirDateSortingSelector : SortingExpression -{ - public override bool TimeDependent => false; - public override bool UserDependent => false; - public DateTime DefaultValue { get; set; } - public override DateTime Evaluate(IFilterable f) => f.AirDate ?? DefaultValue; -} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs deleted file mode 100644 index d20b7d2a7..000000000 --- a/Shoko.Server/Models/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Models.Filters.SortingSelectors; - -public class AudioLanguageCountSortingSelector : SortingExpression -{ - public override bool TimeDependent => false; - public override bool UserDependent => false; - public override int Evaluate(IFilterable f) => f.AudioLanguages.Count; -} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/EpisodeCountSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/EpisodeCountSortingSelector.cs deleted file mode 100644 index 5247f45de..000000000 --- a/Shoko.Server/Models/Filters/SortingSelectors/EpisodeCountSortingSelector.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Models.Filters.SortingSelectors; - -public class EpisodeCountSortingSelector : SortingExpression -{ - public override bool TimeDependent => false; - public override bool UserDependent => false; - public override int Evaluate(IFilterable f) => f.EpisodeCount; -} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/LastAddedDateSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/LastAddedDateSortingSelector.cs deleted file mode 100644 index eb2102e48..000000000 --- a/Shoko.Server/Models/Filters/SortingSelectors/LastAddedDateSortingSelector.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Models.Filters.SortingSelectors; - -public class LastAddedDateSortingSelector : SortingExpression -{ - public override bool TimeDependent => false; - public override bool UserDependent => false; - public override DateTime Evaluate(IFilterable f) => f.LastAddedDate; -} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/LastAirDateSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/LastAirDateSortingSelector.cs deleted file mode 100644 index 080a24c25..000000000 --- a/Shoko.Server/Models/Filters/SortingSelectors/LastAirDateSortingSelector.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Models.Filters.SortingSelectors; - -public class LastAirDateSortingSelector : SortingExpression -{ - public override bool TimeDependent => false; - public override bool UserDependent => false; - public DateTime DefaultValue { get; set; } - public override DateTime Evaluate(IFilterable f) => f.LastAirDate ?? DefaultValue; -} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs deleted file mode 100644 index 212defb43..000000000 --- a/Shoko.Server/Models/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Models.Filters.SortingSelectors; - -public class LastWatchedDateSortingSelector : UserDependentSortingExpression -{ - public override bool TimeDependent => false; - public override bool UserDependent => true; - public DateTime DefaultValue { get; set; } - public override DateTime Evaluate(IUserDependentFilterable f) => f.LastWatchedDate ?? DefaultValue; -} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs deleted file mode 100644 index 5b09bb232..000000000 --- a/Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Models.Filters.SortingSelectors; - -public class MissingEpisodeCollectingCountSortingSelector : SortingExpression -{ - public override bool TimeDependent => false; - public override bool UserDependent => false; - public override int Evaluate(IFilterable f) => f.MissingEpisodesCollecting; -} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs deleted file mode 100644 index f4dcddd99..000000000 --- a/Shoko.Server/Models/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Models.Filters.SortingSelectors; - -public class MissingEpisodeCountSortingSelector : SortingExpression -{ - public override bool TimeDependent => false; - public override bool UserDependent => false; - public override int Evaluate(IFilterable f) => f.MissingEpisodes; -} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/SortingNameSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/SortingNameSortingSelector.cs deleted file mode 100644 index 2e96534a4..000000000 --- a/Shoko.Server/Models/Filters/SortingSelectors/SortingNameSortingSelector.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Models.Filters.SortingSelectors; - -public class SortingNameSortingSelector : SortingExpression -{ - public override bool TimeDependent => false; - public override bool UserDependent => false; - public override string Evaluate(IFilterable f) => f.SortingName; -} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs deleted file mode 100644 index 07a6fcbdf..000000000 --- a/Shoko.Server/Models/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Models.Filters.SortingSelectors; - -public class SubtitleLanguageCountSortingSelector : SortingExpression -{ - public override bool TimeDependent => false; - public override bool UserDependent => false; - public override int Evaluate(IFilterable f) => f.SubtitleLanguages.Count; -} diff --git a/Shoko.Server/Models/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs b/Shoko.Server/Models/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs deleted file mode 100644 index 6afb52f9e..000000000 --- a/Shoko.Server/Models/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Models.Filters.SortingSelectors; - -public class TotalEpisodeCountSortingSelector : SortingExpression -{ - public override bool TimeDependent => false; - public override bool UserDependent => false; - public override int Evaluate(IFilterable f) => f.TotalEpisodeCount; -} diff --git a/Shoko.Server/Models/Filters/UserDependentSortingExpression.cs b/Shoko.Server/Models/Filters/UserDependentSortingExpression.cs deleted file mode 100644 index 5fc70539f..000000000 --- a/Shoko.Server/Models/Filters/UserDependentSortingExpression.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using Shoko.Server.Models.Filters.Interfaces; - -namespace Shoko.Server.Models.Filters; - -public abstract class UserDependentSortingExpression : SortingExpression, IUserDependentSortingExpression where T : IComparable -{ - public override T Evaluate(IFilterable f) - { - if (UserDependent && f is not IUserDependentFilterable) - throw new ArgumentException("User Dependent Filter was given an IFilterable, rather than an IUserDependentFilterable"); - - return Evaluate((IUserDependentFilterable)f); - } - - public abstract T Evaluate(IUserDependentFilterable f); -} diff --git a/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs b/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs index e66aae9d8..31067ab74 100644 --- a/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs @@ -3,6 +3,7 @@ using System.Linq; using NLog; using NutzCode.InMemoryIndex; +using Shoko.Commons.Extensions; using Shoko.Commons.Properties; using Shoko.Models.Server; using Shoko.Server.Databases; @@ -124,6 +125,8 @@ public void Save(SVR_AnimeGroup grp, bool updategrpcontractstats, bool recursive { RepoFactory.GroupFilter.CreateOrVerifyDirectoryFilters(false, grp.Contract?.Stat_AllTags, grp.Contract?.Stat_AllYears, grp.Contract?.Stat_AllSeasons); + RepoFactory.Filter.CreateOrVerifyDirectoryFilters(false, grp.Contract?.Stat_AllTags, + grp.Contract?.Stat_AllYears, grp.Contract?.GetSeasons()); //This call will create extra years or tags if the Group have a new year or tag grp.UpdateGroupFilters(types); } diff --git a/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs b/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs index fdb108e99..c4ba9e0cb 100644 --- a/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs @@ -285,6 +285,8 @@ private static void UpdateGroupFilters(SVR_AnimeSeries obj, Stopwatch sw, string $"Saving Series {animeID} | Updating Group Filters for Years ({string.Join(",", (IEnumerable)allyears?.OrderBy(a => a) ?? Array.Empty())}) and Seasons ({string.Join(",", (IEnumerable)seasons ?? Array.Empty())})"); RepoFactory.GroupFilter.CreateOrVerifyDirectoryFilters(false, obj.Contract?.AniDBAnime?.AniDBAnime?.GetAllTags(), allyears, seasons); + RepoFactory.Filter.CreateOrVerifyDirectoryFilters(false, + obj.Contract?.AniDBAnime?.AniDBAnime?.GetAllTags(), allyears, obj.Contract?.AniDBAnime?.AniDBAnime?.GetSeasons().ToHashSet()); // Update other existing filters obj.UpdateGroupFilters(types); diff --git a/Shoko.Server/Repositories/Cached/FilterRepository.cs b/Shoko.Server/Repositories/Cached/FilterRepository.cs index 0e4d51d7b..567d81247 100644 --- a/Shoko.Server/Repositories/Cached/FilterRepository.cs +++ b/Shoko.Server/Repositories/Cached/FilterRepository.cs @@ -9,15 +9,15 @@ using Shoko.Commons.Properties; using Shoko.Models.Enums; using Shoko.Server.Extensions; +using Shoko.Server.Filters.Functions; +using Shoko.Server.Filters.Info; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Filters.Logic.DateTimes; +using Shoko.Server.Filters.Selectors; +using Shoko.Server.Filters.SortingSelectors; +using Shoko.Server.Filters.User; using Shoko.Server.Models; using Shoko.Server.Models.Filters; -using Shoko.Server.Models.Filters.Functions; -using Shoko.Server.Models.Filters.Info; -using Shoko.Server.Models.Filters.Logic; -using Shoko.Server.Models.Filters.Logic.DateTimes; -using Shoko.Server.Models.Filters.Selectors; -using Shoko.Server.Models.Filters.SortingSelectors; -using Shoko.Server.Models.Filters.User; using Shoko.Server.Repositories.NHibernate; using Shoko.Server.Server; using Constants = Shoko.Server.Server.Constants; @@ -153,8 +153,8 @@ public void CreateOrVerifyLockedFilters() CreateOrVerifyDirectoryFilters(true); } - public void CreateOrVerifyDirectoryFilters(bool frominit = false, HashSet tags = null, - HashSet airdate = null, SortedSet<(int Year, AnimeSeason Season)> seasons = null) + public void CreateOrVerifyDirectoryFilters(bool frominit = false, ISet tags = null, + ISet airdate = null, ISet<(int Year, AnimeSeason Season)> seasons = null) { const string t = "GroupFilter"; @@ -267,7 +267,7 @@ public void CreateOrVerifyDirectoryFilters(bool frominit = false, HashSet a.FilterType == (GroupFilterType.Directory | GroupFilterType.Season)); if (seasonsdirectory != null) { - SortedSet<(int Year, AnimeSeason Season)> allseasons; + ISet<(int Year, AnimeSeason Season)> allseasons; if (seasons == null) { var grps = RepoFactory.AnimeSeries.GetAll().ToList(); diff --git a/Shoko.Server/Server/Startup.cs b/Shoko.Server/Server/Startup.cs index e5edabbe5..d0526a0f2 100644 --- a/Shoko.Server/Server/Startup.cs +++ b/Shoko.Server/Server/Startup.cs @@ -13,6 +13,7 @@ using Shoko.Plugin.Abstractions.Services; using Shoko.Server.API; using Shoko.Server.Commands; +using Shoko.Server.Filters; using Shoko.Server.PlexAndKodi; using Shoko.Server.Plugin; using Shoko.Server.Providers.AniDB; @@ -53,6 +54,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddScoped(); services.AddSingleton(ShokoEventHandler.Instance); services.AddSingleton(); diff --git a/Shoko.Tests/Shoko.Tests/FilterTests.cs b/Shoko.Tests/Shoko.Tests/FilterTests.cs index a3aec0579..6a4999d75 100644 --- a/Shoko.Tests/Shoko.Tests/FilterTests.cs +++ b/Shoko.Tests/Shoko.Tests/FilterTests.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; using Newtonsoft.Json; +using Shoko.Server.Filters; +using Shoko.Server.Filters.Functions; +using Shoko.Server.Filters.Info; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Filters.Logic.DateTimes; +using Shoko.Server.Filters.Selectors; +using Shoko.Server.Filters.User; using Shoko.Server.Models.Filters; -using Shoko.Server.Models.Filters.Functions; -using Shoko.Server.Models.Filters.Info; -using Shoko.Server.Models.Filters.Logic; -using Shoko.Server.Models.Filters.Logic.DateTimes; -using Shoko.Server.Models.Filters.Selectors; -using Shoko.Server.Models.Filters.User; using Xunit; namespace Shoko.Tests; From e5770fc2731eefb394d22d960c9f3d20d4ffff51 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sun, 3 Sep 2023 23:45:56 -0400 Subject: [PATCH 12/34] update commons --- Shoko.Commons | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shoko.Commons b/Shoko.Commons index 539d499f7..9e08c74a3 160000 --- a/Shoko.Commons +++ b/Shoko.Commons @@ -1 +1 @@ -Subproject commit 539d499f75184ced0fba34910b3317bcdbd17db2 +Subproject commit 9e08c74a395fb3cbf4b8d99c6a67f911b28b5a81 From 94ab3493ae8cd5712db1cdb4a678cd8ca9ad9afe Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Mon, 4 Sep 2023 12:46:16 -0400 Subject: [PATCH 13/34] Filters: Remove Interfaces for models. Not much point. Add constructors for everything --- Shoko.Server/Databases/MySQL.cs | 6 +- Shoko.Server/Databases/SQLServer.cs | 6 +- Shoko.Server/Databases/SQLite.cs | 1 - .../FilterExpressionConverter.cs | 1 - .../Files/HasAudioLanguageExpression.cs | 14 +- .../Files/HasSubtitleLanguageExpression.cs | 14 +- Shoko.Server/Filters/FilterEvaluator.cs | 40 ++-- Shoko.Server/Filters/FilterExpression.cs | 8 +- Shoko.Server/Filters/FilterExtensions.cs | 156 +++++++++++---- Shoko.Server/Filters/Filterable.cs | 112 +++++++++-- .../Filters/Functions/DateAddFunction.cs | 16 +- .../Filters/Functions/DateDiffFunction.cs | 13 +- .../Filters/Functions/TodayFunction.cs | 6 +- .../Filters/Info/HasAnimeTypeExpression.cs | 14 +- .../Filters/Info/HasCustomTagExpression.cs | 14 +- .../HasMissingEpisodesCollectingExpression.cs | 8 +- .../Info/HasMissingEpisodesExpression.cs | 8 +- .../Filters/Info/HasTMDbLinkExpression.cs | 8 +- Shoko.Server/Filters/Info/HasTagExpression.cs | 14 +- .../Filters/Info/HasTraktLinkExpression.cs | 8 +- .../Filters/Info/HasTvDBLinkExpression.cs | 8 +- .../Filters/Info/HasVideoSourceExpression.cs | 14 +- .../Filters/Info/InSeasonExpression.cs | 14 +- Shoko.Server/Filters/Info/InYearExpression.cs | 14 +- .../Filters/Info/IsFinishedExpression.cs | 8 +- .../Filters/Info/MissingTMDbLinkExpression.cs | 10 +- .../Info/MissingTraktLinkExpression.cs | 10 +- .../Filters/Info/MissingTvDBLinkExpression.cs | 10 +- .../Filters/Interfaces/IFilterExpression.cs | 4 +- .../Filters/Interfaces/IFilterable.cs | 148 --------------- .../Filters/Interfaces/ISortingExpression.cs | 8 +- .../Interfaces/IUserDependentFilterable.cs | 56 ------ Shoko.Server/Filters/Logic/AndExpression.cs | 16 +- .../Logic/DateTimes/EqualExpression.cs | 34 +++- .../DateTimes/GreaterThanEqualExpression.cs | 34 +++- .../Logic/DateTimes/GreaterThanExpression.cs | 34 +++- .../DateTimes/LessThanEqualExpression.cs | 28 ++- .../Logic/DateTimes/LessThanExpression.cs | 34 +++- .../Logic/DateTimes/NotEqualExpression.cs | 34 +++- Shoko.Server/Filters/Logic/NotExpression.cs | 14 +- .../Filters/Logic/Numbers/EqualExpression.cs | 15 +- .../Numbers/GreaterThanEqualExpression.cs | 15 +- .../Logic/Numbers/GreaterThanExpression.cs | 16 +- .../Logic/Numbers/LessThanEqualExpression.cs | 15 +- .../Logic/Numbers/LessThanExpression.cs | 16 +- .../Logic/Numbers/NotEqualExpression.cs | 15 +- Shoko.Server/Filters/Logic/OrExpression.cs | 16 +- .../Logic/Strings/ContainsExpression.cs | 23 ++- .../Filters/Logic/Strings/EqualExpression.cs | 15 +- .../Logic/Strings/NotEqualExpression.cs | 15 +- Shoko.Server/Filters/Logic/XorExpression.cs | 16 +- .../Filters/Selectors/AddedDateSelector.cs | 7 +- .../Filters/Selectors/AirDateSelector.cs | 7 +- .../Selectors/AudioLanguageCountSelector.cs | 8 +- .../Filters/Selectors/EpisodeCountSelector.cs | 8 +- .../Selectors/HighestAniDBRatingSelector.cs | 7 +- .../Selectors/HighestUserRatingSelector.cs | 7 +- .../Selectors/LastAddedDateSelector.cs | 7 +- .../Filters/Selectors/LastAirDateSelector.cs | 7 +- .../Selectors/LastWatchedDateSelector.cs | 7 +- .../Selectors/LowestAniDBRatingSelector.cs | 7 +- .../Selectors/LowestUserRatingSelector.cs | 7 +- .../SubtitleLanguageCountSelector.cs | 8 +- .../Selectors/TotalEpisodeCountSelector.cs | 8 +- .../Filters/Selectors/WatchedDateSelector.cs | 7 +- .../AddedDateSortingSelector.cs | 8 +- .../AirDateSortingSelector.cs | 7 +- .../AudioLanguageCountSortingSelector.cs | 8 +- .../EpisodeCountSortingSelector.cs | 8 +- .../HighestAniDBRatingSortingSelector.cs | 7 +- .../HighestUserRatingSortingSelector.cs | 7 +- .../LastAddedDateSortingSelector.cs | 8 +- .../LastAirDateSortingSelector.cs | 7 +- .../LastWatchedDateSortingSelector.cs | 7 +- .../LowestAniDBRatingSortingSelector.cs | 7 +- .../LowestUserRatingSortingSelector.cs | 7 +- ...ngEpisodeCollectingCountSortingSelector.cs | 8 +- .../MissingEpisodeCountSortingSelector.cs | 8 +- .../SortingSelectors/NameSortingSelector.cs | 8 +- .../SortingNameSortingSelector.cs | 8 +- .../SubtitleLanguageCountSortingSelector.cs | 8 +- .../TotalEpisodeCountSortingSelector.cs | 8 +- .../WatchedDateSortingSelector.cs | 7 +- .../User/HasPermanentUserVotesExpression.cs | 8 +- .../User/HasUnwatchedEpisodesExpression.cs | 8 +- .../Filters/User/HasUserVotesExpression.cs | 8 +- .../User/HasWatchedEpisodesExpression.cs | 8 +- .../Filters/User/IsFavoriteExpression.cs | 8 +- .../MissingPermanentUserVotesExpression.cs | 8 +- .../Filters/UserDependentFilterExpression.cs | 15 +- .../Filters/UserDependentFilterable.cs | 33 +++- .../Filters/UserDependentSortingExpression.cs | 12 +- Shoko.Server/Filters/readme.md | 19 +- .../Repositories/Cached/FilterRepository.cs | 34 +--- Shoko.Tests/Shoko.Tests/FilterTests.cs | 177 +++--------------- .../Shoko.Tests/IReadOnlySetConverter.cs | 27 +++ 96 files changed, 1078 insertions(+), 701 deletions(-) delete mode 100644 Shoko.Server/Filters/Interfaces/IFilterable.cs delete mode 100644 Shoko.Server/Filters/Interfaces/IUserDependentFilterable.cs create mode 100644 Shoko.Tests/Shoko.Tests/IReadOnlySetConverter.cs diff --git a/Shoko.Server/Databases/MySQL.cs b/Shoko.Server/Databases/MySQL.cs index 388adc848..1e1ea1211 100644 --- a/Shoko.Server/Databases/MySQL.cs +++ b/Shoko.Server/Databases/MySQL.cs @@ -20,7 +20,7 @@ namespace Shoko.Server.Databases; public class MySQL : BaseDatabase { public override string Name { get; } = "MySQL"; - public override int RequiredVersion { get; } = 118; + public override int RequiredVersion { get; } = 119; private List createVersionTable = new() @@ -737,6 +737,10 @@ public class MySQL : BaseDatabase new(117, 2, "ALTER TABLE VideoLocal ADD LastAVDumpVersion nvarchar(128);"), new(118, 1, DatabaseFixes.FixAnimeSourceLinks), new(118, 2, DatabaseFixes.FixOrphanedShokoEpisodes), + new DatabaseCommand(119, 1, + "CREATE TABLE Filter( FilterID INT NOT NULL AUTO_INCREMENT, ParentFilterID int, Name text NOT NULL, FilterType int NOT NULL, Locked bit NOT NULL, Hidden bit NOT NULL, ApplyAtSeriesLevel bit NOT NULL, Expression longtext, SortingExpression longtext, PRIMARY KEY (`FilterID`) ); "), + new DatabaseCommand(119, 2, + "ALTER TABLE Filter ADD INDEX IX_Filter_ParentFilterID (ParentFilterID); ALTER TABLE Filter ADD INDEX IX_Filter_Name (Name); ALTER TABLE Filter ADD INDEX IX_Filter_FilterType (FilterType); ALTER TABLE Filter ADD INDEX IX_Filter_LockedHidden (Locked, Hidden);"), }; private DatabaseCommand linuxTableVersionsFix = new("RENAME TABLE versions TO Versions;"); diff --git a/Shoko.Server/Databases/SQLServer.cs b/Shoko.Server/Databases/SQLServer.cs index 0cd73c560..4944299e9 100644 --- a/Shoko.Server/Databases/SQLServer.cs +++ b/Shoko.Server/Databases/SQLServer.cs @@ -22,7 +22,7 @@ namespace Shoko.Server.Databases; public class SQLServer : BaseDatabase { public override string Name { get; } = "SQLServer"; - public override int RequiredVersion { get; } = 111; + public override int RequiredVersion { get; } = 112; public override void BackupDatabase(string fullfilename) { @@ -680,6 +680,10 @@ public override bool HasVersionsTable() new DatabaseCommand(110, 2, "ALTER TABLE VideoLocal ADD LastAVDumpVersion nvarchar(128);"), new DatabaseCommand(111, 1, DatabaseFixes.FixAnimeSourceLinks), new DatabaseCommand(111, 2, DatabaseFixes.FixOrphanedShokoEpisodes), + new DatabaseCommand(112, 1, + "CREATE TABLE Filter( FilterID INT IDENTITY(1,1), ParentFilterID int, Name nvarchar(max) NOT NULL, FilterType int NOT NULL, Locked bit NOT NULL, Hidden bit NOT NULL, ApplyAtSeriesLevel bit NOT NULL, Expression nvarchar(max), SortingExpression nvarchar(max) ); "), + new DatabaseCommand(112, 2, + "CREATE INDEX IX_Filter_ParentFilterID ON Filter(ParentFilterID); CREATE INDEX IX_Filter_Name ON Filter(Name); CREATE INDEX IX_Filter_FilterType ON Filter(FilterType); CREATE INDEX IX_Filter_LockedHidden ON Filter(Locked, Hidden);"), }; private static Tuple DropDefaultsOnAnimeEpisode_User(object connection) diff --git a/Shoko.Server/Databases/SQLite.cs b/Shoko.Server/Databases/SQLite.cs index 02ba12317..d4d60e426 100644 --- a/Shoko.Server/Databases/SQLite.cs +++ b/Shoko.Server/Databases/SQLite.cs @@ -11,7 +11,6 @@ using Shoko.Server.Databases.SqliteFixes; using Shoko.Server.Repositories; using Shoko.Server.Server; -using Shoko.Server.Settings; using Shoko.Server.Utilities; // ReSharper disable InconsistentNaming diff --git a/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs b/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs index e28d24e8b..25b3576ce 100644 --- a/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs +++ b/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs @@ -11,7 +11,6 @@ using NHibernate.Engine; using NLog; using Shoko.Server.Filters; -using Shoko.Server.Models.Filters; namespace Shoko.Server.Databases.TypeConverters; diff --git a/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs b/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs index d8c5af40d..88f1648e0 100644 --- a/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs +++ b/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs @@ -1,11 +1,19 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Files; public class HasAudioLanguageExpression : FilterExpression { + public HasAudioLanguageExpression(string parameter) + { + Parameter = parameter; + } + public HasAudioLanguageExpression() { } + public string Parameter { get; set; } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.AudioLanguages.Contains(Parameter); + + public override bool Evaluate(Filterable filterable) + { + return filterable.AudioLanguages.Contains(Parameter); + } } diff --git a/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs b/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs index 83e65775f..68d6a8d0a 100644 --- a/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs +++ b/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs @@ -1,11 +1,19 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Files; public class HasSubtitleLanguageExpression : FilterExpression { + public HasSubtitleLanguageExpression(string parameter) + { + Parameter = parameter; + } + public HasSubtitleLanguageExpression() { } + public string Parameter { get; set; } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.SubtitleLanguages.Contains(Parameter); + + public override bool Evaluate(Filterable filterable) + { + return filterable.SubtitleLanguages.Contains(Parameter); + } } diff --git a/Shoko.Server/Filters/FilterEvaluator.cs b/Shoko.Server/Filters/FilterEvaluator.cs index 4bc961104..ef8ed7b20 100644 --- a/Shoko.Server/Filters/FilterEvaluator.cs +++ b/Shoko.Server/Filters/FilterEvaluator.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using Shoko.Server.Filters.Interfaces; using Shoko.Server.Filters.SortingSelectors; using Shoko.Server.Models.Filters; using Shoko.Server.Repositories; @@ -12,20 +11,12 @@ namespace Shoko.Server.Filters; public class FilterEvaluator { - private record FilterableWithID(int SeriesID, int GroupID, IFilterable Filterable); - - private record Grouping(int GroupID, int[] SeriesIDs) : IGrouping - { - public IEnumerator GetEnumerator() => ((IEnumerable)SeriesIDs).GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public int Key => GroupID; - } + private readonly AnimeGroupRepository _groups = RepoFactory.AnimeGroup; private readonly AnimeSeriesRepository _series = RepoFactory.AnimeSeries; - private readonly AnimeGroupRepository _groups = RepoFactory.AnimeGroup; /// - /// Evaluate the given filter, applying the necessary logic + /// Evaluate the given filter, applying the necessary logic /// /// /// @@ -35,13 +26,17 @@ public IEnumerable> EvaluateFilter(Filter filter, int? userI { ArgumentNullException.ThrowIfNull(filter); var user = filter.Expression?.UserDependent ?? false; - if (user && userID == null) throw new ArgumentNullException(nameof(userID)); + if (user && userID == null) + { + throw new ArgumentNullException(nameof(userID)); + } + var filterables = filter.ApplyAtSeriesLevel switch { true when user => _series.GetAll().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), true => _series.GetAll().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToFilterable())), false when user => _groups.GetAll().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), - false => _groups.GetAll().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToFilterable())), + false => _groups.GetAll().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToFilterable())) }; // Filtering @@ -52,7 +47,9 @@ public IEnumerable> EvaluateFilter(Filter filter, int? userI var result = ordered.GroupBy(a => a.GroupID, a => a.SeriesID); if (!filter.ApplyAtSeriesLevel) + { result = result.Select(a => new Grouping(a.Key, _series.GetByGroupID(a.Key).Select(ser => ser.AnimeSeriesID).ToArray())); + } return result; } @@ -74,4 +71,21 @@ private static IOrderedEnumerable OrderFilterables(Filter filt return ordered; } + + private record FilterableWithID(int SeriesID, int GroupID, Filterable Filterable); + + private record Grouping(int GroupID, int[] SeriesIDs) : IGrouping + { + public IEnumerator GetEnumerator() + { + return ((IEnumerable)SeriesIDs).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public int Key => GroupID; + } } diff --git a/Shoko.Server/Filters/FilterExpression.cs b/Shoko.Server/Filters/FilterExpression.cs index b2bb959c6..8b28c719f 100644 --- a/Shoko.Server/Filters/FilterExpression.cs +++ b/Shoko.Server/Filters/FilterExpression.cs @@ -6,13 +6,11 @@ namespace Shoko.Server.Filters; public class FilterExpression : IFilterExpression { public int FilterExpressionID { get; set; } - [IgnoreDataMember] - public virtual bool TimeDependent => false; - [IgnoreDataMember] - public virtual bool UserDependent => false; + [IgnoreDataMember] public virtual bool TimeDependent => false; + [IgnoreDataMember] public virtual bool UserDependent => false; } public abstract class FilterExpression : FilterExpression, IFilterExpression { - public abstract T Evaluate(IFilterable f); + public abstract T Evaluate(Filterable f); } diff --git a/Shoko.Server/Filters/FilterExtensions.cs b/Shoko.Server/Filters/FilterExtensions.cs index 2280423f3..6281ca161 100644 --- a/Shoko.Server/Filters/FilterExtensions.cs +++ b/Shoko.Server/Filters/FilterExtensions.cs @@ -4,7 +4,6 @@ using Shoko.Commons.Extensions; using Shoko.Models.Enums; using Shoko.Server.Extensions; -using Shoko.Server.Filters.Interfaces; using Shoko.Server.Models; using Shoko.Server.Providers.AniDB; using Shoko.Server.Repositories; @@ -14,7 +13,7 @@ namespace Shoko.Server.Filters; public static class FilterExtensions { - public static IFilterable ToFilterable(this SVR_AnimeSeries series) + public static Filterable ToFilterable(this SVR_AnimeSeries series) { var anime = series.GetAnime(); var name = series.GetSeriesName(); @@ -28,7 +27,8 @@ public static IFilterable ToFilterable(this SVR_AnimeSeries series) MissingEpisodes = series.Contract?.MissingEpisodeCount ?? 0, MissingEpisodesCollecting = series.Contract?.MissingEpisodeCountGroups ?? 0, Tags = anime?.GetAllTags() ?? new HashSet(), - CustomTags = series.Contract?.AniDBAnime?.CustomTags?.Select(a => a.TagName).ToHashSet(StringComparer.InvariantCultureIgnoreCase) ?? new HashSet(), + CustomTags = + series.Contract?.AniDBAnime?.CustomTags?.Select(a => a.TagName).ToHashSet(StringComparer.InvariantCultureIgnoreCase) ?? new HashSet(), Years = GetYears(series), Seasons = anime.GetSeasons().ToHashSet(), HasTvDBLink = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(), @@ -38,14 +38,20 @@ public static IFilterable ToFilterable(this SVR_AnimeSeries series) HasTraktLink = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), HasMissingTraktLink = !RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), IsFinished = series.Contract?.AniDBAnime?.AniDBAnime?.EndDate != null && series.Contract.AniDBAnime.AniDBAnime.EndDate.Value < DateTime.Now, - LastAirDate = series.EndDate ?? series.GetAnimeEpisodes().Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), + LastAirDate = + series.EndDate ?? series.GetAnimeEpisodes().Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), AddedDate = series.DateTimeCreated, LastAddedDate = series.GetVideoLocals().Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), EpisodeCount = anime?.EpisodeCountNormal ?? 0, TotalEpisodeCount = anime?.EpisodeCount ?? 0, LowestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero), HighestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero), - AnimeTypes = anime == null ? new HashSet() : new HashSet(StringComparer.InvariantCultureIgnoreCase) { ((AnimeType)anime.AnimeType).ToString() }, + AnimeTypes = anime == null + ? new HashSet() + : new HashSet(StringComparer.InvariantCultureIgnoreCase) + { + ((AnimeType)anime.AnimeType).ToString() + }, VideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet(), AudioLanguages = series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet(), SubtitleLanguages = series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet() @@ -70,7 +76,8 @@ public static UserDependentFilterable ToUserDependentFilterable(this SVR_AnimeSe MissingEpisodes = series.Contract?.MissingEpisodeCount ?? 0, MissingEpisodesCollecting = series.Contract?.MissingEpisodeCountGroups ?? 0, Tags = anime?.GetAllTags() ?? new HashSet(), - CustomTags = series.Contract?.AniDBAnime?.CustomTags?.Select(a => a.TagName).ToHashSet(StringComparer.InvariantCultureIgnoreCase) ?? new HashSet(), + CustomTags = + series.Contract?.AniDBAnime?.CustomTags?.Select(a => a.TagName).ToHashSet(StringComparer.InvariantCultureIgnoreCase) ?? new HashSet(), Years = GetYears(series), Seasons = anime?.GetSeasons().ToHashSet(), HasTvDBLink = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(), @@ -80,14 +87,20 @@ public static UserDependentFilterable ToUserDependentFilterable(this SVR_AnimeSe HasTraktLink = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), HasMissingTraktLink = !RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), IsFinished = series.Contract?.AniDBAnime?.AniDBAnime?.EndDate != null && series.Contract.AniDBAnime.AniDBAnime.EndDate.Value < DateTime.Now, - LastAirDate = series.EndDate ?? series.GetAnimeEpisodes().Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), + LastAirDate = + series.EndDate ?? series.GetAnimeEpisodes().Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), AddedDate = series.DateTimeCreated, LastAddedDate = series.GetVideoLocals().Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), EpisodeCount = anime?.EpisodeCountNormal ?? 0, TotalEpisodeCount = anime?.EpisodeCount ?? 0, LowestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero), HighestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero), - AnimeTypes = anime == null ? new HashSet() : new HashSet(StringComparer.InvariantCultureIgnoreCase) { ((AnimeType)anime.AnimeType).ToString() }, + AnimeTypes = anime == null + ? new HashSet() + : new HashSet(StringComparer.InvariantCultureIgnoreCase) + { + ((AnimeType)anime.AnimeType).ToString() + }, VideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet(), AudioLanguages = series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet(), SubtitleLanguages = series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet(), @@ -110,34 +123,77 @@ private static HashSet GetYears(SVR_AnimeSeries series) { var contract = series.Contract?.AniDBAnime; var startyear = contract?.AniDBAnime?.BeginYear ?? 0; - if (startyear == 0) return new HashSet(); + if (startyear == 0) + { + return new HashSet(); + } + var endyear = contract?.AniDBAnime?.EndYear ?? 0; - if (endyear == 0) endyear = DateTime.Today.Year; - if (endyear < startyear) endyear = startyear; - if (startyear == endyear) return new HashSet { startyear }; + if (endyear == 0) + { + endyear = DateTime.Today.Year; + } + + if (endyear < startyear) + { + endyear = startyear; + } + + if (startyear == endyear) + { + return new HashSet + { + startyear + }; + } + return new HashSet(Enumerable.Range(startyear, endyear - startyear + 1).Where(contract.IsInYear)); } private static bool HasMissingTMDbLink(SVR_AnimeSeries series) { var anime = series.GetAnime(); - if (anime == null) return false; + if (anime == null) + { + return false; + } + // TODO update this with the TMDB refactor - if (anime.AnimeType != (int)AnimeType.Movie) return false; - if (anime.Restricted > 0) return false; + if (anime.AnimeType != (int)AnimeType.Movie) + { + return false; + } + + if (anime.Restricted > 0) + { + return false; + } + return series.Contract?.CrossRefAniDBMovieDB == null; } private static bool HasMissingTvDBLink(SVR_AnimeSeries series) { var anime = series.GetAnime(); - if (anime == null) return false; - if (anime.AnimeType == (int)AnimeType.Movie) return false; - if (anime.Restricted > 0) return false; + if (anime == null) + { + return false; + } + + if (anime.AnimeType == (int)AnimeType.Movie) + { + return false; + } + + if (anime.Restricted > 0) + { + return false; + } + return !RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(); } - - public static IFilterable ToFilterable(this SVR_AnimeGroup group) + + public static Filterable ToFilterable(this SVR_AnimeGroup group) { var series = group.GetAllSeries(); var hasTrakt = series.All(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()); @@ -148,7 +204,7 @@ public static IFilterable ToFilterable(this SVR_AnimeGroup group) SortingName = group.GroupName.GetSortName(), SeriesCount = series.Count, AirDate = group.Contract.Stat_AirDate_Min, - LastAirDate = group.Contract?.Stat_EndDate ?? group.GetAllSeries().SelectMany(a => a.GetAnimeEpisodes()).Select(a => + LastAirDate = group.Contract?.Stat_EndDate ?? group.GetAllSeries().SelectMany(a => a.GetAnimeEpisodes()).Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), MissingEpisodes = group.Contract?.MissingEpisodeCount ?? 0, MissingEpisodesCollecting = group.Contract?.MissingEpisodeCountGroups ?? 0, @@ -164,15 +220,17 @@ public static IFilterable ToFilterable(this SVR_AnimeGroup group) HasMissingTvDbLink = HasMissingTvDBLink(group), HasTMDbLink = group.Contract?.Stat_HasMovieDBLink ?? false, HasMissingTMDbLink = HasMissingTMDbLink(group), - HasTraktLink = hasTrakt, + HasTraktLink = hasTrakt, HasMissingTraktLink = !hasTrakt, IsFinished = group.Contract?.Stat_HasFinishedAiring ?? false, AddedDate = group.DateTimeCreated, LastAddedDate = series.SelectMany(a => a.GetVideoLocals()).Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), EpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCountNormal ?? 0), TotalEpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCount ?? 0), - LowestAniDBRating = group.Anime.DefaultIfEmpty().Min(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), - HighestAniDBRating = group.Anime.DefaultIfEmpty().Max(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), + LowestAniDBRating = + group.Anime.DefaultIfEmpty().Min(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), + HighestAniDBRating = + group.Anime.DefaultIfEmpty().Max(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), AnimeTypes = new HashSet(group.Anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), VideoSources = group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet(), AudioLanguages = group.Contract?.Stat_AudioLanguages ?? new HashSet(), @@ -182,7 +240,7 @@ public static IFilterable ToFilterable(this SVR_AnimeGroup group) return filterable; } - public static IFilterable ToUserDependentFilterable(this SVR_AnimeGroup group, int userID) + public static Filterable ToUserDependentFilterable(this SVR_AnimeGroup group, int userID) { var series = group.GetAllSeries(); var hasTrakt = series.All(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()); @@ -201,7 +259,7 @@ public static IFilterable ToUserDependentFilterable(this SVR_AnimeGroup group, i SortingName = group.GroupName.GetSortName(), SeriesCount = series.Count, AirDate = group.Contract.Stat_AirDate_Min, - LastAirDate = group.Contract?.Stat_EndDate ?? group.GetAllSeries().SelectMany(a => a.GetAnimeEpisodes()).Select(a => + LastAirDate = group.Contract?.Stat_EndDate ?? group.GetAllSeries().SelectMany(a => a.GetAnimeEpisodes()).Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), MissingEpisodes = group.Contract?.MissingEpisodeCount ?? 0, MissingEpisodesCollecting = group.Contract?.MissingEpisodeCountGroups ?? 0, @@ -217,15 +275,17 @@ public static IFilterable ToUserDependentFilterable(this SVR_AnimeGroup group, i HasMissingTvDbLink = HasMissingTvDBLink(group), HasTMDbLink = group.Contract?.Stat_HasMovieDBLink ?? false, HasMissingTMDbLink = HasMissingTMDbLink(group), - HasTraktLink = hasTrakt, + HasTraktLink = hasTrakt, HasMissingTraktLink = !hasTrakt, IsFinished = group.Contract?.Stat_HasFinishedAiring ?? false, AddedDate = group.DateTimeCreated, LastAddedDate = series.SelectMany(a => a.GetVideoLocals()).Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), EpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCountNormal ?? 0), TotalEpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCount ?? 0), - LowestAniDBRating = group.Anime.DefaultIfEmpty().Min(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), - HighestAniDBRating = group.Anime.DefaultIfEmpty().Max(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), + LowestAniDBRating = + group.Anime.DefaultIfEmpty().Min(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), + HighestAniDBRating = + group.Anime.DefaultIfEmpty().Max(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), AnimeTypes = new HashSet(group.Anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), VideoSources = group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet(), AudioLanguages = group.Contract?.Stat_AudioLanguages ?? new HashSet(), @@ -244,16 +304,28 @@ public static IFilterable ToUserDependentFilterable(this SVR_AnimeGroup group, i return filterable; } - + private static bool HasMissingTMDbLink(SVR_AnimeGroup group) { return group.GetAllSeries().Any(series => { var anime = series.GetAnime(); - if (anime == null) return false; + if (anime == null) + { + return false; + } + // TODO update this with the TMDB refactor - if (anime.AnimeType != (int)AnimeType.Movie) return false; - if (anime.Restricted > 0) return false; + if (anime.AnimeType != (int)AnimeType.Movie) + { + return false; + } + + if (anime.Restricted > 0) + { + return false; + } + return series.Contract?.CrossRefAniDBMovieDB == null; }); } @@ -263,9 +335,21 @@ private static bool HasMissingTvDBLink(SVR_AnimeGroup group) return group.GetAllSeries().Any(series => { var anime = series.GetAnime(); - if (anime == null) return false; - if (anime.AnimeType == (int)AnimeType.Movie) return false; - if (anime.Restricted > 0) return false; + if (anime == null) + { + return false; + } + + if (anime.AnimeType == (int)AnimeType.Movie) + { + return false; + } + + if (anime.Restricted > 0) + { + return false; + } + return !RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(); }); } diff --git a/Shoko.Server/Filters/Filterable.cs b/Shoko.Server/Filters/Filterable.cs index 992ab7901..8dafbeecd 100644 --- a/Shoko.Server/Filters/Filterable.cs +++ b/Shoko.Server/Filters/Filterable.cs @@ -1,47 +1,121 @@ using System; using System.Collections.Generic; using Shoko.Models.Enums; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters; -public class Filterable : IFilterable +public class Filterable { - // The explicit implementations of IReadOnlySet make it easier to deserialize into + /// + /// Name + /// public string Name { get; init; } + /// + /// Sorting Name + /// public string SortingName { get; init; } + /// + /// The number of series in a group + /// public int SeriesCount { get; init; } + /// + /// Number of Missing Episodes + /// public int MissingEpisodes { get; init; } + /// + /// Number of Missing Episodes from Groups that you have + /// public int MissingEpisodesCollecting { get; init; } - public HashSet Tags { get; init; } - IReadOnlySet IFilterable.Tags => Tags; - public HashSet CustomTags { get; init; } - IReadOnlySet IFilterable.CustomTags => CustomTags; - public HashSet Years { get; init; } - IReadOnlySet IFilterable.Years => Years; - public HashSet<(int year, AnimeSeason season)> Seasons { get; init; } - IReadOnlySet<(int year, AnimeSeason season)> IFilterable.Seasons => Seasons; + /// + /// All of the tags + /// + public IReadOnlySet Tags { get; init; } + /// + /// All of the custom tags + /// + public IReadOnlySet CustomTags { get; init; } + /// + /// The years this aired in + /// + public IReadOnlySet Years { get; init; } + /// + /// The seasons this aired in + /// + public IReadOnlySet<(int year, AnimeSeason season)> Seasons { get; init; } + /// + /// Has at least one TvDB Link + /// public bool HasTvDBLink { get; init; } + /// + /// Missing at least one TvDB Link + /// public bool HasMissingTvDbLink { get; init; } + /// + /// Has at least one TMDb Link + /// public bool HasTMDbLink { get; init; } + /// + /// Missing at least one TMDb Link + /// public bool HasMissingTMDbLink { get; init; } + /// + /// Has at least one Trakt Link + /// public bool HasTraktLink { get; init; } + /// + /// Missing at least one Trakt Link + /// public bool HasMissingTraktLink { get; init; } + /// + /// Has Finished airing + /// public bool IsFinished { get; init; } + /// + /// First Air Date + /// public DateTime? AirDate { get; init; } + /// + /// Latest Air Date + /// public DateTime? LastAirDate { get; init; } + /// + /// When it was first added to the collection + /// public DateTime AddedDate { get; init; } + /// + /// When it was most recently added to the collection + /// public DateTime LastAddedDate { get; init; } + /// + /// Highest Episode Count + /// public int EpisodeCount { get; init; } + /// + /// Total Episode Count + /// public int TotalEpisodeCount { get; init; } + /// + /// Lowest AniDB Rating (0-10) + /// public decimal LowestAniDBRating { get; init; } + /// + /// Highest AniDB Rating (0-10) + /// public decimal HighestAniDBRating { get; init; } - public HashSet VideoSources { get; init; } - IReadOnlySet IFilterable.VideoSources => VideoSources; - public HashSet AnimeTypes { get; init; } - IReadOnlySet IFilterable.AnimeTypes => AnimeTypes; - public HashSet AudioLanguages { get; init; } - IReadOnlySet IFilterable.AudioLanguages => AudioLanguages; - public HashSet SubtitleLanguages { get; init; } - IReadOnlySet IFilterable.SubtitleLanguages => SubtitleLanguages; + /// + /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. + /// + public IReadOnlySet VideoSources { get; init; } + /// + /// The anime types (movie, series, ova, etc) + /// + public IReadOnlySet AnimeTypes { get; init; } + /// + /// Audio Languages + /// + public IReadOnlySet AudioLanguages { get; init; } + /// + /// Subtitle Languages + /// + public IReadOnlySet SubtitleLanguages { get; init; } } diff --git a/Shoko.Server/Filters/Functions/DateAddFunction.cs b/Shoko.Server/Filters/Functions/DateAddFunction.cs index 40d943494..4f2d9e498 100644 --- a/Shoko.Server/Filters/Functions/DateAddFunction.cs +++ b/Shoko.Server/Filters/Functions/DateAddFunction.cs @@ -1,15 +1,27 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Functions; public class DateAddFunction : FilterExpression { + public DateAddFunction() + { + } + + public DateAddFunction(FilterExpression selector, TimeSpan parameter) + { + Selector = selector; + Parameter = parameter; + } + public FilterExpression Selector { get; set; } public TimeSpan Parameter { get; set; } public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; - public override DateTime? Evaluate(IFilterable f) => Selector.Evaluate(f) + Parameter; + public override DateTime? Evaluate(Filterable f) + { + return Selector.Evaluate(f) + Parameter; + } } diff --git a/Shoko.Server/Filters/Functions/DateDiffFunction.cs b/Shoko.Server/Filters/Functions/DateDiffFunction.cs index 5e80bd332..a1586f2db 100644 --- a/Shoko.Server/Filters/Functions/DateDiffFunction.cs +++ b/Shoko.Server/Filters/Functions/DateDiffFunction.cs @@ -1,15 +1,24 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Functions; public class DateDiffFunction : FilterExpression { + public DateDiffFunction(FilterExpression selector, TimeSpan parameter) + { + Selector = selector; + Parameter = parameter; + } + public DateDiffFunction() { } + public FilterExpression Selector { get; set; } public TimeSpan Parameter { get; set; } public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; - public override DateTime? Evaluate(IFilterable f) => Selector.Evaluate(f) - Parameter; + public override DateTime? Evaluate(Filterable f) + { + return Selector.Evaluate(f) - Parameter; + } } diff --git a/Shoko.Server/Filters/Functions/TodayFunction.cs b/Shoko.Server/Filters/Functions/TodayFunction.cs index 46a6f55df..20e288b05 100644 --- a/Shoko.Server/Filters/Functions/TodayFunction.cs +++ b/Shoko.Server/Filters/Functions/TodayFunction.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Functions; @@ -8,5 +7,8 @@ public class TodayFunction : FilterExpression public override bool TimeDependent => true; public override bool UserDependent => false; - public override DateTime? Evaluate(IFilterable f) => DateTime.Today; + public override DateTime? Evaluate(Filterable f) + { + return DateTime.Today; + } } diff --git a/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs b/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs index 3b8a6f9b4..22e1a2d1c 100644 --- a/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs +++ b/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs @@ -1,11 +1,19 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Info; public class HasAnimeTypeExpression : FilterExpression { + public HasAnimeTypeExpression(string parameter) + { + Parameter = parameter; + } + public HasAnimeTypeExpression() { } + public string Parameter { get; set; } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.AnimeTypes.Contains(Parameter); + + public override bool Evaluate(Filterable filterable) + { + return filterable.AnimeTypes.Contains(Parameter); + } } diff --git a/Shoko.Server/Filters/Info/HasCustomTagExpression.cs b/Shoko.Server/Filters/Info/HasCustomTagExpression.cs index 8faeece59..ee3639c71 100644 --- a/Shoko.Server/Filters/Info/HasCustomTagExpression.cs +++ b/Shoko.Server/Filters/Info/HasCustomTagExpression.cs @@ -1,11 +1,19 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Info; public class HasCustomTagExpression : FilterExpression { + public HasCustomTagExpression(string parameter) + { + Parameter = parameter; + } + public HasCustomTagExpression() { } + public string Parameter { get; set; } public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(IFilterable filterable) => filterable.CustomTags.Contains(Parameter); + + public override bool Evaluate(Filterable filterable) + { + return filterable.CustomTags.Contains(Parameter); + } } diff --git a/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs b/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs index 15e371c39..f6438de24 100644 --- a/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs +++ b/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Info; public class HasMissingEpisodesCollectingExpression : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.MissingEpisodesCollecting > 0; + + public override bool Evaluate(Filterable filterable) + { + return filterable.MissingEpisodesCollecting > 0; + } } diff --git a/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs b/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs index 0a109cd65..7cec7bf81 100644 --- a/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs +++ b/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Info; public class HasMissingEpisodesExpression : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.MissingEpisodes > 0; + + public override bool Evaluate(Filterable filterable) + { + return filterable.MissingEpisodes > 0; + } } diff --git a/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs b/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs index 830ef057d..5a5043119 100644 --- a/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs +++ b/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Info; public class HasTMDbLinkExpression : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.HasTMDbLink; + + public override bool Evaluate(Filterable filterable) + { + return filterable.HasTMDbLink; + } } diff --git a/Shoko.Server/Filters/Info/HasTagExpression.cs b/Shoko.Server/Filters/Info/HasTagExpression.cs index a5f8763f9..2092443a8 100644 --- a/Shoko.Server/Filters/Info/HasTagExpression.cs +++ b/Shoko.Server/Filters/Info/HasTagExpression.cs @@ -1,11 +1,19 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Info; public class HasTagExpression : FilterExpression { + public HasTagExpression(string parameter) + { + Parameter = parameter; + } + public HasTagExpression() { } + public string Parameter { get; set; } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.Tags.Contains(Parameter); + + public override bool Evaluate(Filterable filterable) + { + return filterable.Tags.Contains(Parameter); + } } diff --git a/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs b/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs index 5a7464fe5..86ea790c7 100644 --- a/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs +++ b/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Info; public class HasTraktLinkExpression : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.HasTraktLink; + + public override bool Evaluate(Filterable filterable) + { + return filterable.HasTraktLink; + } } diff --git a/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs b/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs index 8656acb86..0eb17f73f 100644 --- a/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs +++ b/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Info; public class HasTvDBLinkExpression : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.HasTvDBLink; + + public override bool Evaluate(Filterable filterable) + { + return filterable.HasTvDBLink; + } } diff --git a/Shoko.Server/Filters/Info/HasVideoSourceExpression.cs b/Shoko.Server/Filters/Info/HasVideoSourceExpression.cs index cf5d80a31..eb193a0b4 100644 --- a/Shoko.Server/Filters/Info/HasVideoSourceExpression.cs +++ b/Shoko.Server/Filters/Info/HasVideoSourceExpression.cs @@ -1,11 +1,19 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Info; public class HasVideoSourceExpression : FilterExpression { + public HasVideoSourceExpression(string parameter) + { + Parameter = parameter; + } + public HasVideoSourceExpression() { } + public string Parameter { get; set; } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.VideoSources.Contains(Parameter); + + public override bool Evaluate(Filterable filterable) + { + return filterable.VideoSources.Contains(Parameter); + } } diff --git a/Shoko.Server/Filters/Info/InSeasonExpression.cs b/Shoko.Server/Filters/Info/InSeasonExpression.cs index 3b0a4a7ab..91cd713c3 100644 --- a/Shoko.Server/Filters/Info/InSeasonExpression.cs +++ b/Shoko.Server/Filters/Info/InSeasonExpression.cs @@ -1,13 +1,23 @@ using Shoko.Models.Enums; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Info; public class InSeasonExpression : FilterExpression { + public InSeasonExpression(int year, AnimeSeason season) + { + Year = year; + Season = season; + } + public InSeasonExpression() { } + public int Year { get; set; } public AnimeSeason Season { get; set; } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.Seasons.Contains((Year, Season)); + + public override bool Evaluate(Filterable filterable) + { + return filterable.Seasons.Contains((Year, Season)); + } } diff --git a/Shoko.Server/Filters/Info/InYearExpression.cs b/Shoko.Server/Filters/Info/InYearExpression.cs index eff4821be..ac8d41723 100644 --- a/Shoko.Server/Filters/Info/InYearExpression.cs +++ b/Shoko.Server/Filters/Info/InYearExpression.cs @@ -1,11 +1,19 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Info; public class InYearExpression : FilterExpression { + public InYearExpression(int parameter) + { + Parameter = parameter; + } + public InYearExpression() { } + public int Parameter { get; set; } public override bool TimeDependent => true; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.Years.Contains(Parameter); + + public override bool Evaluate(Filterable filterable) + { + return filterable.Years.Contains(Parameter); + } } diff --git a/Shoko.Server/Filters/Info/IsFinishedExpression.cs b/Shoko.Server/Filters/Info/IsFinishedExpression.cs index fa53b319d..906e79af4 100644 --- a/Shoko.Server/Filters/Info/IsFinishedExpression.cs +++ b/Shoko.Server/Filters/Info/IsFinishedExpression.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Info; public class IsFinishedExpression : FilterExpression { public override bool TimeDependent => true; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.IsFinished; + + public override bool Evaluate(Filterable filterable) + { + return filterable.IsFinished; + } } diff --git a/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs index e76a47562..adf6282e6 100644 --- a/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs +++ b/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs @@ -1,13 +1,15 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Info; /// -/// Missing Links include logic for whether a link should exist +/// Missing Links include logic for whether a link should exist /// public class MissingTMDbLinkExpression : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.HasMissingTMDbLink; + + public override bool Evaluate(Filterable filterable) + { + return filterable.HasMissingTMDbLink; + } } diff --git a/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs index 721d58b0b..42ca5009f 100644 --- a/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs +++ b/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs @@ -1,13 +1,15 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Info; /// -/// Missing Links include logic for whether a link should exist +/// Missing Links include logic for whether a link should exist /// public class MissingTraktLinkExpression : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.HasMissingTraktLink; + + public override bool Evaluate(Filterable filterable) + { + return filterable.HasMissingTraktLink; + } } diff --git a/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs index 553e7fb9c..7e9f9185c 100644 --- a/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs +++ b/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs @@ -1,13 +1,15 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Info; /// -/// Missing Links include logic for whether a link should exist +/// Missing Links include logic for whether a link should exist /// public class MissingTvDBLinkExpression : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(IFilterable filterable) => filterable.HasMissingTvDbLink; + + public override bool Evaluate(Filterable filterable) + { + return filterable.HasMissingTvDbLink; + } } diff --git a/Shoko.Server/Filters/Interfaces/IFilterExpression.cs b/Shoko.Server/Filters/Interfaces/IFilterExpression.cs index 07341ed18..041465f97 100644 --- a/Shoko.Server/Filters/Interfaces/IFilterExpression.cs +++ b/Shoko.Server/Filters/Interfaces/IFilterExpression.cs @@ -8,10 +8,10 @@ public interface IFilterExpression public interface IFilterExpression { - T Evaluate(IFilterable f); + T Evaluate(Filterable f); } public interface IUserDependentFilterExpression { - T Evaluate(IUserDependentFilterable f); + T Evaluate(UserDependentFilterable f); } diff --git a/Shoko.Server/Filters/Interfaces/IFilterable.cs b/Shoko.Server/Filters/Interfaces/IFilterable.cs deleted file mode 100644 index bd140d353..000000000 --- a/Shoko.Server/Filters/Interfaces/IFilterable.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Collections.Generic; -using Shoko.Models.Enums; - -namespace Shoko.Server.Filters.Interfaces; - -public interface IFilterable -{ - /// - /// Name - /// - string Name { get; } - - /// - /// Sorting Name - /// - string SortingName { get; } - - /// - /// The number of series in a group - /// - int SeriesCount { get; } - - /// - /// Number of Missing Episodes - /// - int MissingEpisodes { get; } - - /// - /// Number of Missing Episodes from Groups that you have - /// - int MissingEpisodesCollecting { get; } - - /// - /// All of the tags - /// - IReadOnlySet Tags { get; } - - /// - /// All of the custom tags - /// - IReadOnlySet CustomTags { get; } - - /// - /// The years this aired in - /// - IReadOnlySet Years { get; } - - /// - /// The seasons this aired in - /// - IReadOnlySet<(int year, AnimeSeason season)> Seasons { get; } - - /// - /// Has at least one TvDB Link - /// - bool HasTvDBLink { get; } - - /// - /// Missing at least one TvDB Link - /// - bool HasMissingTvDbLink { get; } - - /// - /// Has at least one TMDb Link - /// - bool HasTMDbLink { get; } - - /// - /// Missing at least one TMDb Link - /// - bool HasMissingTMDbLink { get; } - - /// - /// Has at least one Trakt Link - /// - bool HasTraktLink { get; } - - /// - /// Missing at least one Trakt Link - /// - bool HasMissingTraktLink { get; } - - /// - /// Has Finished airing - /// - bool IsFinished { get; } - - /// - /// First Air Date - /// - DateTime? AirDate { get; } - - /// - /// Latest Air Date - /// - DateTime? LastAirDate { get; } - - /// - /// When it was first added to the collection - /// - DateTime AddedDate { get; } - - /// - /// When it was most recently added to the collection - /// - DateTime LastAddedDate { get; } - - /// - /// Highest Episode Count - /// - int EpisodeCount { get; } - - /// - /// Total Episode Count - /// - int TotalEpisodeCount { get; } - - /// - /// Lowest AniDB Rating (0-10) - /// - decimal LowestAniDBRating { get; } - - /// - /// Highest AniDB Rating (0-10) - /// - decimal HighestAniDBRating { get; } - - /// - /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. - /// - IReadOnlySet VideoSources { get; } - - /// - /// The anime types (movie, series, ova, etc) - /// - IReadOnlySet AnimeTypes { get; } - - /// - /// Audio Languages - /// - IReadOnlySet AudioLanguages { get; } - - /// - /// Subtitle Languages - /// - IReadOnlySet SubtitleLanguages { get; } -} diff --git a/Shoko.Server/Filters/Interfaces/ISortingExpression.cs b/Shoko.Server/Filters/Interfaces/ISortingExpression.cs index 037a7ec4d..b016e2771 100644 --- a/Shoko.Server/Filters/Interfaces/ISortingExpression.cs +++ b/Shoko.Server/Filters/Interfaces/ISortingExpression.cs @@ -1,5 +1,9 @@ namespace Shoko.Server.Filters.Interfaces; -public interface ISortingExpression : IFilterExpression { } +public interface ISortingExpression : IFilterExpression +{ +} -public interface IUserDependentSortingExpression : ISortingExpression, IUserDependentFilterExpression { } +public interface IUserDependentSortingExpression : ISortingExpression, IUserDependentFilterExpression +{ +} diff --git a/Shoko.Server/Filters/Interfaces/IUserDependentFilterable.cs b/Shoko.Server/Filters/Interfaces/IUserDependentFilterable.cs deleted file mode 100644 index 5e3cbde9e..000000000 --- a/Shoko.Server/Filters/Interfaces/IUserDependentFilterable.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; - -namespace Shoko.Server.Filters.Interfaces; - -public interface IUserDependentFilterable : IFilterable -{ - /// - /// Probably will be removed in the future. Custom Tags would handle this better - /// - bool IsFavorite { get; } - - /// - /// The number of episodes watched - /// - int WatchedEpisodes { get; } - - /// - /// The number of episodes that have not been watched - /// - int UnwatchedEpisodes { get; } - - /// - /// Has any user votes - /// - bool HasVotes { get; } - - /// - /// Has permanent (after finishing) user votes - /// - bool HasPermanentVotes { get; } - - /// - /// Has permanent (after finishing) user votes - /// - bool MissingPermanentVotes { get; } - - /// - /// First Watched Date - /// - DateTime? WatchedDate { get; } - - /// - /// Latest Watched Date - /// - DateTime? LastWatchedDate { get; } - - /// - /// Lowest User Rating (0-10) - /// - decimal LowestUserRating { get; } - - /// - /// Highest User Rating (0-10) - /// - decimal HighestUserRating { get; } -} diff --git a/Shoko.Server/Filters/Logic/AndExpression.cs b/Shoko.Server/Filters/Logic/AndExpression.cs index f197d4fd7..6a3dd0536 100644 --- a/Shoko.Server/Filters/Logic/AndExpression.cs +++ b/Shoko.Server/Filters/Logic/AndExpression.cs @@ -1,13 +1,23 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Logic; public class AndExpression : FilterExpression { + public AndExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + + public AndExpression() { } + public override bool TimeDependent => Left.TimeDependent || Right.TimeDependent; public override bool UserDependent => Left.UserDependent || Right.UserDependent; - public override bool Evaluate(IFilterable filterable) => Left.Evaluate(filterable) && Right.Evaluate(filterable); public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } + + public override bool Evaluate(Filterable filterable) + { + return Left.Evaluate(filterable) && Right.Evaluate(filterable); + } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/EqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/EqualExpression.cs index ea91d392d..13ce61645 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/EqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/EqualExpression.cs @@ -1,24 +1,48 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.DateTimes; public class EqualExpression : FilterExpression { + public EqualExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + public EqualExpression(FilterExpression left, DateTime parameter) + { + Left = left; + Parameter = parameter; + } + public EqualExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public DateTime Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + + public override bool Evaluate(Filterable filterable) { var date = Left.Evaluate(filterable); var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; var operand = Right == null ? Parameter : Right.Evaluate(filterable); var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; - if (dateIsNull && operandIsNull) return true; - if (dateIsNull) return false; - if (operandIsNull) return false; + if (dateIsNull && operandIsNull) + { + return true; + } + + if (dateIsNull) + { + return false; + } + + if (operandIsNull) + { + return false; + } + return (date > operand ? date - operand : operand - date).Value.TotalDays < 1; } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs index 47c1cb329..30f8dc70c 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs @@ -1,24 +1,48 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.DateTimes; public class GreaterThanEqualExpression : FilterExpression { + public GreaterThanEqualExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + public GreaterThanEqualExpression(FilterExpression left, DateTime parameter) + { + Left = left; + Parameter = parameter; + } + public GreaterThanEqualExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public DateTime Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + + public override bool Evaluate(Filterable filterable) { var date = Left.Evaluate(filterable); var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; var operand = Right == null ? Parameter : Right.Evaluate(filterable); var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; - if (dateIsNull && operandIsNull) return true; - if (dateIsNull) return false; - if (operandIsNull) return false; + if (dateIsNull && operandIsNull) + { + return true; + } + + if (dateIsNull) + { + return false; + } + + if (operandIsNull) + { + return false; + } + return date >= operand; } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/GreaterThanExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/GreaterThanExpression.cs index 7f486135d..25db8312b 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/GreaterThanExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/GreaterThanExpression.cs @@ -1,24 +1,48 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.DateTimes; public class GreaterThanExpression : FilterExpression { + public GreaterThanExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + public GreaterThanExpression(FilterExpression left, DateTime parameter) + { + Left = left; + Parameter = parameter; + } + public GreaterThanExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public DateTime Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + + public override bool Evaluate(Filterable filterable) { var date = Left.Evaluate(filterable); var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; var operand = Right == null ? Parameter : Right.Evaluate(filterable); var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; - if (dateIsNull && operandIsNull) return false; - if (dateIsNull) return true; - if (operandIsNull) return true; + if (dateIsNull && operandIsNull) + { + return false; + } + + if (dateIsNull) + { + return true; + } + + if (operandIsNull) + { + return true; + } + return date > operand; } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/LessThanEqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/LessThanEqualExpression.cs index 90372327c..fdfdd7743 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/LessThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/LessThanEqualExpression.cs @@ -1,21 +1,41 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.DateTimes; public class LessThanEqualExpression : FilterExpression { + public LessThanEqualExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + public LessThanEqualExpression(FilterExpression left, DateTime parameter) + { + Left = left; + Parameter = parameter; + } + public LessThanEqualExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public DateTime Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + + public override bool Evaluate(Filterable filterable) { var date = Left.Evaluate(filterable); - if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) return false; + if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) + { + return false; + } + var operand = Right == null ? Parameter : Right.Evaluate(filterable); - if (operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch) return false; + if (operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch) + { + return false; + } + return date <= operand; } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/LessThanExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/LessThanExpression.cs index 73e300335..a5d8d546f 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/LessThanExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/LessThanExpression.cs @@ -1,24 +1,48 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.DateTimes; public class LessThanExpression : FilterExpression { + public LessThanExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + public LessThanExpression(FilterExpression left, DateTime parameter) + { + Left = left; + Parameter = parameter; + } + public LessThanExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public DateTime Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + + public override bool Evaluate(Filterable filterable) { var date = Left.Evaluate(filterable); var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; var operand = Right == null ? Parameter : Right.Evaluate(filterable); var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; - if (dateIsNull && operandIsNull) return true; - if (dateIsNull) return false; - if (operandIsNull) return false; + if (dateIsNull && operandIsNull) + { + return true; + } + + if (dateIsNull) + { + return false; + } + + if (operandIsNull) + { + return false; + } + return date < operand; } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/NotEqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/NotEqualExpression.cs index 67fcc97fc..2fcbd068e 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/NotEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/NotEqualExpression.cs @@ -1,24 +1,48 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.DateTimes; public class NotEqualExpression : FilterExpression { + public NotEqualExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + public NotEqualExpression(FilterExpression left, DateTime parameter) + { + Left = left; + Parameter = parameter; + } + public NotEqualExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public DateTime Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + + public override bool Evaluate(Filterable filterable) { var date = Left.Evaluate(filterable); var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; var operand = Right == null ? Parameter : Right.Evaluate(filterable); var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; - if (dateIsNull && operandIsNull) return false; - if (dateIsNull) return true; - if (operandIsNull) return true; + if (dateIsNull && operandIsNull) + { + return false; + } + + if (dateIsNull) + { + return true; + } + + if (operandIsNull) + { + return true; + } + return (date > operand ? date - operand : operand - date).Value.TotalDays >= 1; } } diff --git a/Shoko.Server/Filters/Logic/NotExpression.cs b/Shoko.Server/Filters/Logic/NotExpression.cs index 385a68633..c6ec290d8 100644 --- a/Shoko.Server/Filters/Logic/NotExpression.cs +++ b/Shoko.Server/Filters/Logic/NotExpression.cs @@ -1,12 +1,20 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Logic; public class NotExpression : FilterExpression { + public NotExpression(FilterExpression left) + { + Left = left; + } + + public NotExpression() { } public override bool TimeDependent => Left.TimeDependent; public override bool UserDependent => Left.UserDependent; - public override bool Evaluate(IFilterable filterable) => !Left.Evaluate(filterable); public FilterExpression Left { get; set; } + + public override bool Evaluate(Filterable filterable) + { + return !Left.Evaluate(filterable); + } } diff --git a/Shoko.Server/Filters/Logic/Numbers/EqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/EqualExpression.cs index b8017021a..761b40fac 100644 --- a/Shoko.Server/Filters/Logic/Numbers/EqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/EqualExpression.cs @@ -1,17 +1,28 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Numbers; public class EqualExpression : FilterExpression { + public EqualExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + public EqualExpression(FilterExpression left, double parameter) + { + Left = left; + Parameter = parameter; + } + public EqualExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public double? Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + public override bool Evaluate(Filterable filterable) { var left = Left.Evaluate(filterable); var right = Parameter ?? Right.Evaluate(filterable); diff --git a/Shoko.Server/Filters/Logic/Numbers/GreaterThanEqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/GreaterThanEqualExpression.cs index 776d00355..483622ecc 100644 --- a/Shoko.Server/Filters/Logic/Numbers/GreaterThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/GreaterThanEqualExpression.cs @@ -1,17 +1,28 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Numbers; public class GreaterThanEqualExpression : FilterExpression { + public GreaterThanEqualExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + public GreaterThanEqualExpression(FilterExpression left, double parameter) + { + Left = left; + Parameter = parameter; + } + public GreaterThanEqualExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public double? Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + public override bool Evaluate(Filterable filterable) { var left = Left.Evaluate(filterable); var right = Parameter ?? Right.Evaluate(filterable); diff --git a/Shoko.Server/Filters/Logic/Numbers/GreaterThanExpression.cs b/Shoko.Server/Filters/Logic/Numbers/GreaterThanExpression.cs index 369898896..a9c9685c2 100644 --- a/Shoko.Server/Filters/Logic/Numbers/GreaterThanExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/GreaterThanExpression.cs @@ -1,16 +1,26 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Logic.Numbers; public class GreaterThanExpression : FilterExpression { + public GreaterThanExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + public GreaterThanExpression(FilterExpression left, double parameter) + { + Left = left; + Parameter = parameter; + } + public GreaterThanExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public double? Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + public override bool Evaluate(Filterable filterable) { var left = Left.Evaluate(filterable); var right = Parameter ?? Right.Evaluate(filterable); diff --git a/Shoko.Server/Filters/Logic/Numbers/LessThanEqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/LessThanEqualExpression.cs index e065bec0b..c90f86bfb 100644 --- a/Shoko.Server/Filters/Logic/Numbers/LessThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/LessThanEqualExpression.cs @@ -1,17 +1,28 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Numbers; public class LessThanEqualExpression : FilterExpression { + public LessThanEqualExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + public LessThanEqualExpression(FilterExpression left, double parameter) + { + Left = left; + Parameter = parameter; + } + public LessThanEqualExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public double? Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + public override bool Evaluate(Filterable filterable) { var left = Left.Evaluate(filterable); var right = Parameter ?? Right.Evaluate(filterable); diff --git a/Shoko.Server/Filters/Logic/Numbers/LessThanExpression.cs b/Shoko.Server/Filters/Logic/Numbers/LessThanExpression.cs index 539c58638..4524fb1a7 100644 --- a/Shoko.Server/Filters/Logic/Numbers/LessThanExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/LessThanExpression.cs @@ -1,16 +1,26 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Logic.Numbers; public class LessThanExpression : FilterExpression { + public LessThanExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + public LessThanExpression(FilterExpression left, double parameter) + { + Left = left; + Parameter = parameter; + } + public LessThanExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public double? Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + public override bool Evaluate(Filterable filterable) { var left = Left.Evaluate(filterable); var right = Parameter ?? Right.Evaluate(filterable); diff --git a/Shoko.Server/Filters/Logic/Numbers/NotEqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NotEqualExpression.cs index e66565360..2d62f7d0d 100644 --- a/Shoko.Server/Filters/Logic/Numbers/NotEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NotEqualExpression.cs @@ -1,17 +1,28 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Numbers; public class NotEqualExpression : FilterExpression { + public NotEqualExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + public NotEqualExpression(FilterExpression left, double parameter) + { + Left = left; + Parameter = parameter; + } + public NotEqualExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public double? Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + public override bool Evaluate(Filterable filterable) { var left = Left.Evaluate(filterable); var right = Parameter ?? Right.Evaluate(filterable); diff --git a/Shoko.Server/Filters/Logic/OrExpression.cs b/Shoko.Server/Filters/Logic/OrExpression.cs index 6fc84e5d5..6464ca8e7 100644 --- a/Shoko.Server/Filters/Logic/OrExpression.cs +++ b/Shoko.Server/Filters/Logic/OrExpression.cs @@ -1,13 +1,23 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Logic; public class OrExpression : FilterExpression { + public OrExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + + public OrExpression() { } + public override bool TimeDependent => Left.TimeDependent || Right.TimeDependent; public override bool UserDependent => Left.UserDependent || Right.UserDependent; - public override bool Evaluate(IFilterable filterable) => Left.Evaluate(filterable) || Right.Evaluate(filterable); public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } + + public override bool Evaluate(Filterable filterable) + { + return Left.Evaluate(filterable) || Right.Evaluate(filterable); + } } diff --git a/Shoko.Server/Filters/Logic/Strings/ContainsExpression.cs b/Shoko.Server/Filters/Logic/Strings/ContainsExpression.cs index 505c7ec42..2d60ded64 100644 --- a/Shoko.Server/Filters/Logic/Strings/ContainsExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/ContainsExpression.cs @@ -1,21 +1,38 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Strings; public class ContainsExpression : FilterExpression { + public ContainsExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + + public ContainsExpression(FilterExpression left, string parameter) + { + Left = left; + Parameter = parameter; + } + + public ContainsExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public string Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + public override bool Evaluate(Filterable filterable) { var left = Left.Evaluate(filterable); var right = Parameter ?? Right?.Evaluate(filterable); - if (string.IsNullOrEmpty(left) || string.IsNullOrEmpty(right)) return false; + if (string.IsNullOrEmpty(left) || string.IsNullOrEmpty(right)) + { + return false; + } + return left.Contains(right, StringComparison.InvariantCultureIgnoreCase); } } diff --git a/Shoko.Server/Filters/Logic/Strings/EqualExpression.cs b/Shoko.Server/Filters/Logic/Strings/EqualExpression.cs index 586b17377..fae2c6806 100644 --- a/Shoko.Server/Filters/Logic/Strings/EqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/EqualExpression.cs @@ -1,17 +1,28 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Strings; public class EqualExpression : FilterExpression { + public EqualExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + public EqualExpression(FilterExpression left, string parameter) + { + Left = left; + Parameter = parameter; + } + public EqualExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public string Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + public override bool Evaluate(Filterable filterable) { var left = Left.Evaluate(filterable); var right = Parameter ?? Right?.Evaluate(filterable); diff --git a/Shoko.Server/Filters/Logic/Strings/NotEqualExpression.cs b/Shoko.Server/Filters/Logic/Strings/NotEqualExpression.cs index 80e3f0e7f..015393b79 100644 --- a/Shoko.Server/Filters/Logic/Strings/NotEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/NotEqualExpression.cs @@ -1,17 +1,28 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Strings; public class NotEqualExpression : FilterExpression { + public NotEqualExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + public NotEqualExpression(FilterExpression left, string parameter) + { + Left = left; + Parameter = parameter; + } + public NotEqualExpression() { } + public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } public string Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(IFilterable filterable) + public override bool Evaluate(Filterable filterable) { var left = Left.Evaluate(filterable); var right = Parameter ?? Right?.Evaluate(filterable); diff --git a/Shoko.Server/Filters/Logic/XorExpression.cs b/Shoko.Server/Filters/Logic/XorExpression.cs index 867d16b59..cac1bc133 100644 --- a/Shoko.Server/Filters/Logic/XorExpression.cs +++ b/Shoko.Server/Filters/Logic/XorExpression.cs @@ -1,13 +1,23 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Logic; public class XorExpression : FilterExpression { + public XorExpression(FilterExpression left, FilterExpression right) + { + Left = left; + Right = right; + } + + public XorExpression() { } + public override bool TimeDependent => Left.TimeDependent || Right.TimeDependent; public override bool UserDependent => Left.UserDependent || Right.UserDependent; - public override bool Evaluate(IFilterable filterable) => Left.Evaluate(filterable) ^ Right.Evaluate(filterable); public FilterExpression Left { get; set; } public FilterExpression Right { get; set; } + + public override bool Evaluate(Filterable filterable) + { + return Left.Evaluate(filterable) ^ Right.Evaluate(filterable); + } } diff --git a/Shoko.Server/Filters/Selectors/AddedDateSelector.cs b/Shoko.Server/Filters/Selectors/AddedDateSelector.cs index 93118f37f..769f24710 100644 --- a/Shoko.Server/Filters/Selectors/AddedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/AddedDateSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,5 +6,9 @@ public class AddedDateSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override DateTime? Evaluate(IFilterable f) => f.AddedDate; + + public override DateTime? Evaluate(Filterable f) + { + return f.AddedDate; + } } diff --git a/Shoko.Server/Filters/Selectors/AirDateSelector.cs b/Shoko.Server/Filters/Selectors/AirDateSelector.cs index f2f3a0599..fc67d4406 100644 --- a/Shoko.Server/Filters/Selectors/AirDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/AirDateSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,5 +6,9 @@ public class AirDateSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override DateTime? Evaluate(IFilterable f) => f.AirDate; + + public override DateTime? Evaluate(Filterable f) + { + return f.AirDate; + } } diff --git a/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs b/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs index 204df8857..6b252b4a9 100644 --- a/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Selectors; public class AudioLanguageCountSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override int Evaluate(IFilterable f) => f.AudioLanguages.Count; + + public override int Evaluate(Filterable f) + { + return f.AudioLanguages.Count; + } } diff --git a/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs b/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs index 95452aae6..828d4181a 100644 --- a/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Selectors; public class EpisodeCountSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override int Evaluate(IFilterable f) => f.EpisodeCount; + + public override int Evaluate(Filterable f) + { + return f.EpisodeCount; + } } diff --git a/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs b/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs index 380912729..1b2bc3861 100644 --- a/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,5 +6,9 @@ public class HighestAniDBRatingSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(IFilterable f) => Convert.ToDouble(f.HighestAniDBRating); + + public override double Evaluate(Filterable f) + { + return Convert.ToDouble(f.HighestAniDBRating); + } } diff --git a/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs b/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs index a935b2fd8..260ac190a 100644 --- a/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,5 +6,9 @@ public class HighestUserRatingSelector : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override double Evaluate(IUserDependentFilterable f) => Convert.ToDouble(f.HighestUserRating); + + public override double Evaluate(UserDependentFilterable f) + { + return Convert.ToDouble(f.HighestUserRating); + } } diff --git a/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs b/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs index cf9873396..3db1eaa8e 100644 --- a/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,5 +6,9 @@ public class LastAddedDateSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override DateTime? Evaluate(IFilterable f) => f.LastAddedDate; + + public override DateTime? Evaluate(Filterable f) + { + return f.LastAddedDate; + } } diff --git a/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs b/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs index cc4771dba..efe67636c 100644 --- a/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,5 +6,9 @@ public class LastAirDateSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override DateTime? Evaluate(IFilterable f) => f.LastAirDate; + + public override DateTime? Evaluate(Filterable f) + { + return f.LastAirDate; + } } diff --git a/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs b/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs index acdc8fafb..0c65bc7cc 100644 --- a/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,5 +6,9 @@ public class LastWatchedDateSelector : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override DateTime? Evaluate(IUserDependentFilterable f) => f.LastWatchedDate; + + public override DateTime? Evaluate(UserDependentFilterable f) + { + return f.LastWatchedDate; + } } diff --git a/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs b/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs index c19a7abfd..cb80b3569 100644 --- a/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,5 +6,9 @@ public class LowestAniDBRatingSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(IFilterable f) => Convert.ToDouble(f.LowestAniDBRating); + + public override double Evaluate(Filterable f) + { + return Convert.ToDouble(f.LowestAniDBRating); + } } diff --git a/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs b/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs index 778e7faaa..5640cead1 100644 --- a/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,5 +6,9 @@ public class LowestUserRatingSelector : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override double Evaluate(IUserDependentFilterable f) => Convert.ToDouble(f.LowestUserRating); + + public override double Evaluate(UserDependentFilterable f) + { + return Convert.ToDouble(f.LowestUserRating); + } } diff --git a/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs b/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs index 09d61d841..59212b4bf 100644 --- a/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Selectors; public class SubtitleLanguageCountSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override int Evaluate(IFilterable f) => f.SubtitleLanguages.Count; + + public override int Evaluate(Filterable f) + { + return f.SubtitleLanguages.Count; + } } diff --git a/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs b/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs index affde8724..cb961d6a7 100644 --- a/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.Selectors; public class TotalEpisodeCountSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override int Evaluate(IFilterable f) => f.TotalEpisodeCount; + + public override int Evaluate(Filterable f) + { + return f.TotalEpisodeCount; + } } diff --git a/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs b/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs index 6d8a173fd..7bfc7c91d 100644 --- a/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,5 +6,9 @@ public class WatchedDateSelector : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override DateTime? Evaluate(IUserDependentFilterable f) => f.WatchedDate; + + public override DateTime? Evaluate(UserDependentFilterable f) + { + return f.WatchedDate; + } } diff --git a/Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs index 7a57cb95c..37fd984d0 100644 --- a/Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.SortingSelectors; public class AddedDateSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(IFilterable f) => f.AddedDate; + + public override object Evaluate(Filterable f) + { + return f.AddedDate; + } } diff --git a/Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs index 0176e12fd..0fb63fc7d 100644 --- a/Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -8,5 +7,9 @@ public class AirDateSortingSelector : SortingExpression public override bool TimeDependent => false; public override bool UserDependent => false; public DateTime DefaultValue { get; set; } - public override object Evaluate(IFilterable f) => f.AirDate ?? DefaultValue; + + public override object Evaluate(Filterable f) + { + return f.AirDate ?? DefaultValue; + } } diff --git a/Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs index 7c31fb8bb..59bf2a82a 100644 --- a/Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.SortingSelectors; public class AudioLanguageCountSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(IFilterable f) => f.AudioLanguages.Count; + + public override object Evaluate(Filterable f) + { + return f.AudioLanguages.Count; + } } diff --git a/Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs index 9bbeca041..1a42fef55 100644 --- a/Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.SortingSelectors; public class EpisodeCountSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(IFilterable f) => f.EpisodeCount; + + public override object Evaluate(Filterable f) + { + return f.EpisodeCount; + } } diff --git a/Shoko.Server/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs index 1cebbc22d..c98d4bfb6 100644 --- a/Shoko.Server/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -7,5 +6,9 @@ public class HighestAniDBRatingSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(IFilterable f) => Convert.ToDouble(f.HighestAniDBRating); + + public override object Evaluate(Filterable f) + { + return Convert.ToDouble(f.HighestAniDBRating); + } } diff --git a/Shoko.Server/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs index dbe1eda9b..b56db0f15 100644 --- a/Shoko.Server/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -7,5 +6,9 @@ public class HighestUserRatingSortingSelector : UserDependentSortingExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override object Evaluate(IUserDependentFilterable f) => Convert.ToDouble(f.HighestUserRating); + + public override object Evaluate(UserDependentFilterable f) + { + return Convert.ToDouble(f.HighestUserRating); + } } diff --git a/Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs index b3eb79c1d..9e780b9de 100644 --- a/Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.SortingSelectors; public class LastAddedDateSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(IFilterable f) => f.LastAddedDate; + + public override object Evaluate(Filterable f) + { + return f.LastAddedDate; + } } diff --git a/Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs index 4f0bf0e39..f4065f0aa 100644 --- a/Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -8,5 +7,9 @@ public class LastAirDateSortingSelector : SortingExpression public override bool TimeDependent => false; public override bool UserDependent => false; public DateTime DefaultValue { get; set; } - public override object Evaluate(IFilterable f) => f.LastAirDate ?? DefaultValue; + + public override object Evaluate(Filterable f) + { + return f.LastAirDate ?? DefaultValue; + } } diff --git a/Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs index 3186ee6ee..907cc37a9 100644 --- a/Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -8,5 +7,9 @@ public class LastWatchedDateSortingSelector : UserDependentSortingExpression public override bool TimeDependent => false; public override bool UserDependent => true; public DateTime DefaultValue { get; set; } - public override object Evaluate(IUserDependentFilterable f) => f.LastWatchedDate ?? DefaultValue; + + public override object Evaluate(UserDependentFilterable f) + { + return f.LastWatchedDate ?? DefaultValue; + } } diff --git a/Shoko.Server/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs index 2ad2ee6df..2c836a42c 100644 --- a/Shoko.Server/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -7,5 +6,9 @@ public class LowestAniDBRatingSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(IFilterable f) => Convert.ToDouble(f.LowestAniDBRating); + + public override object Evaluate(Filterable f) + { + return Convert.ToDouble(f.LowestAniDBRating); + } } diff --git a/Shoko.Server/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs index 74c9193c3..256642d2b 100644 --- a/Shoko.Server/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -7,5 +6,9 @@ public class LowestUserRatingSortingSelector : UserDependentSortingExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override object Evaluate(IUserDependentFilterable f) => Convert.ToDouble(f.LowestUserRating); + + public override object Evaluate(UserDependentFilterable f) + { + return Convert.ToDouble(f.LowestUserRating); + } } diff --git a/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs index c3fa2dfc9..d5924c616 100644 --- a/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.SortingSelectors; public class MissingEpisodeCollectingCountSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(IFilterable f) => f.MissingEpisodesCollecting; + + public override object Evaluate(Filterable f) + { + return f.MissingEpisodesCollecting; + } } diff --git a/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs index 9917bfd10..387647486 100644 --- a/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.SortingSelectors; public class MissingEpisodeCountSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(IFilterable f) => f.MissingEpisodes; + + public override object Evaluate(Filterable f) + { + return f.MissingEpisodes; + } } diff --git a/Shoko.Server/Filters/SortingSelectors/NameSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/NameSortingSelector.cs index 243e1dddf..b347d49b8 100644 --- a/Shoko.Server/Filters/SortingSelectors/NameSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/NameSortingSelector.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.SortingSelectors; public class NameSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override string Evaluate(IFilterable f) => f.Name; + + public override string Evaluate(Filterable f) + { + return f.Name; + } } diff --git a/Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs index f6007d55c..00572f570 100644 --- a/Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.SortingSelectors; public class SortingNameSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(IFilterable f) => f.SortingName; + + public override object Evaluate(Filterable f) + { + return f.SortingName; + } } diff --git a/Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs index 13306cadc..8f9948080 100644 --- a/Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.SortingSelectors; public class SubtitleLanguageCountSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(IFilterable f) => f.SubtitleLanguages.Count; + + public override object Evaluate(Filterable f) + { + return f.SubtitleLanguages.Count; + } } diff --git a/Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs index 159d6df52..6e4a80a28 100644 --- a/Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.SortingSelectors; public class TotalEpisodeCountSortingSelector : SortingExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(IFilterable f) => f.TotalEpisodeCount; + + public override object Evaluate(Filterable f) + { + return f.TotalEpisodeCount; + } } diff --git a/Shoko.Server/Filters/SortingSelectors/WatchedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/WatchedDateSortingSelector.cs index a9c57f0ee..a7a7efbd4 100644 --- a/Shoko.Server/Filters/SortingSelectors/WatchedDateSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/WatchedDateSortingSelector.cs @@ -1,5 +1,4 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -8,5 +7,9 @@ public class WatchedDateSortingSelector : UserDependentSortingExpression public override bool TimeDependent => false; public override bool UserDependent => true; public DateTime DefaultValue { get; set; } - public override object Evaluate(IUserDependentFilterable f) => f.WatchedDate ?? DefaultValue; + + public override object Evaluate(UserDependentFilterable f) + { + return f.WatchedDate ?? DefaultValue; + } } diff --git a/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs b/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs index c6a45ccc0..26b4ed935 100644 --- a/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs +++ b/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.User; public class HasPermanentUserVotesExpression : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(IUserDependentFilterable filterable) => filterable.HasPermanentVotes; + + public override bool Evaluate(UserDependentFilterable filterable) + { + return filterable.HasPermanentVotes; + } } diff --git a/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs b/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs index 3ddee3412..3a25cb67f 100644 --- a/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs +++ b/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.User; public class HasUnwatchedEpisodesExpression : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(IUserDependentFilterable filterable) => filterable.UnwatchedEpisodes > 0; + + public override bool Evaluate(UserDependentFilterable filterable) + { + return filterable.UnwatchedEpisodes > 0; + } } diff --git a/Shoko.Server/Filters/User/HasUserVotesExpression.cs b/Shoko.Server/Filters/User/HasUserVotesExpression.cs index 084e476c5..d4ea9cb8a 100644 --- a/Shoko.Server/Filters/User/HasUserVotesExpression.cs +++ b/Shoko.Server/Filters/User/HasUserVotesExpression.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.User; public class HasUserVotesExpression : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(IUserDependentFilterable filterable) => filterable.HasVotes; + + public override bool Evaluate(UserDependentFilterable filterable) + { + return filterable.HasVotes; + } } diff --git a/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs b/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs index 806c569ea..f38d05452 100644 --- a/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs +++ b/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.User; public class HasWatchedEpisodesExpression : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(IUserDependentFilterable filterable) => filterable.WatchedEpisodes > 0; + + public override bool Evaluate(UserDependentFilterable filterable) + { + return filterable.WatchedEpisodes > 0; + } } diff --git a/Shoko.Server/Filters/User/IsFavoriteExpression.cs b/Shoko.Server/Filters/User/IsFavoriteExpression.cs index eb77598d1..d58e52c54 100644 --- a/Shoko.Server/Filters/User/IsFavoriteExpression.cs +++ b/Shoko.Server/Filters/User/IsFavoriteExpression.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.User; public class IsFavoriteExpression : UserDependentFilterExpression { public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(IUserDependentFilterable filterable) => filterable.IsFavorite; + + public override bool Evaluate(UserDependentFilterable filterable) + { + return filterable.IsFavorite; + } } diff --git a/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs b/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs index e10db005a..f58200c4d 100644 --- a/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs +++ b/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs @@ -1,10 +1,12 @@ -using Shoko.Server.Filters.Interfaces; - namespace Shoko.Server.Filters.User; public class MissingPermanentUserVotesExpression : UserDependentFilterExpression { public override bool TimeDependent => true; public override bool UserDependent => true; - public override bool Evaluate(IUserDependentFilterable filterable) => filterable.MissingPermanentVotes; + + public override bool Evaluate(UserDependentFilterable filterable) + { + return filterable.MissingPermanentVotes; + } } diff --git a/Shoko.Server/Filters/UserDependentFilterExpression.cs b/Shoko.Server/Filters/UserDependentFilterExpression.cs index 3a734efc1..272d928eb 100644 --- a/Shoko.Server/Filters/UserDependentFilterExpression.cs +++ b/Shoko.Server/Filters/UserDependentFilterExpression.cs @@ -5,13 +5,16 @@ namespace Shoko.Server.Filters; public abstract class UserDependentFilterExpression : FilterExpression, IUserDependentFilterExpression { - public override T Evaluate(IFilterable f) + + public abstract T Evaluate(UserDependentFilterable f); + + public override T Evaluate(Filterable f) { - if (UserDependent && f is not IUserDependentFilterable) - throw new ArgumentException("User Dependent Filter was given an IFilterable, rather than an IUserDependentFilterable"); + if (UserDependent && f is not UserDependentFilterable) + { + throw new ArgumentException("User Dependent Filter was given an Filterable, rather than an UserDependentFilterable"); + } - return Evaluate((IUserDependentFilterable)f); + return Evaluate((UserDependentFilterable)f); } - - public abstract T Evaluate(IUserDependentFilterable f); } diff --git a/Shoko.Server/Filters/UserDependentFilterable.cs b/Shoko.Server/Filters/UserDependentFilterable.cs index aa12ec5fe..ec8e61fd8 100644 --- a/Shoko.Server/Filters/UserDependentFilterable.cs +++ b/Shoko.Server/Filters/UserDependentFilterable.cs @@ -1,18 +1,47 @@ using System; -using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters; -public class UserDependentFilterable : Filterable, IUserDependentFilterable +public class UserDependentFilterable : Filterable { + /// + /// Probably will be removed in the future. Custom Tags would handle this better + /// public bool IsFavorite { get; init; } + /// + /// The number of episodes watched + /// public int WatchedEpisodes { get; init; } + /// + /// The number of episodes that have not been watched + /// public int UnwatchedEpisodes { get; init; } + /// + /// Has any user votes + /// public bool HasVotes { get; init; } + /// + /// Has permanent (after finishing) user votes + /// public bool HasPermanentVotes { get; init; } + /// + /// Has permanent (after finishing) user votes + /// public bool MissingPermanentVotes { get; init; } + /// + /// First Watched Date + /// public DateTime? WatchedDate { get; init; } + /// + /// Latest Watched Date + /// public DateTime? LastWatchedDate { get; init; } + /// + /// Lowest User Rating (0-10) + /// public decimal LowestUserRating { get; init; } + /// + /// Highest User Rating (0-10) + /// public decimal HighestUserRating { get; init; } } diff --git a/Shoko.Server/Filters/UserDependentSortingExpression.cs b/Shoko.Server/Filters/UserDependentSortingExpression.cs index cacc6224a..32b6170e8 100644 --- a/Shoko.Server/Filters/UserDependentSortingExpression.cs +++ b/Shoko.Server/Filters/UserDependentSortingExpression.cs @@ -5,13 +5,15 @@ namespace Shoko.Server.Filters; public abstract class UserDependentSortingExpression : SortingExpression, IUserDependentSortingExpression { - public override object Evaluate(IFilterable f) + public override object Evaluate(Filterable f) { - if (UserDependent && f is not IUserDependentFilterable) - throw new ArgumentException("User Dependent Filter was given an IFilterable, rather than an IUserDependentFilterable"); + if (UserDependent && f is not UserDependentFilterable) + { + throw new ArgumentException("User Dependent Filter was given an Filterable, rather than an UserDependentFilterable"); + } - return Evaluate((IUserDependentFilterable)f); + return Evaluate((UserDependentFilterable)f); } - public abstract object Evaluate(IUserDependentFilterable f); + public abstract object Evaluate(UserDependentFilterable f); } diff --git a/Shoko.Server/Filters/readme.md b/Shoko.Server/Filters/readme.md index fd6caacb3..f630ef53c 100644 --- a/Shoko.Server/Filters/readme.md +++ b/Shoko.Server/Filters/readme.md @@ -1,13 +1,26 @@ -An Expression is anything that transforms data: a method that takes zero or more arguments and returns a result. Expressions are stored with TPH discriminated on Type. Arguments should be mapped to Type + "Argument" + Index (StringArgument1) in the mapping. Expressions should not have more than 5 Arguments of each type. If it would, then it should be redesigned. For example, `And(HasTag("comedy"),HasTag("action"))`. This is to keep the database schema reasonable and expressions simple. I considered a one-to-many map for arguments, and making that automatic for navigation properties seemed like a lot of work when we can design around it. +An Expression is anything that transforms data: a method that takes zero or more arguments and returns a result. +Expressions are stored with TPH discriminated on Type. Arguments should be mapped to Type + "Argument" + Index ( +StringArgument1) in the mapping. Expressions should not have more than 5 Arguments of each type. If it would, then it +should be redesigned. For example, `And(HasTag("comedy"),HasTag("action"))`. This is to keep the database schema +reasonable and expressions simple. I considered a one-to-many map for arguments, and making that automatic for +navigation properties seemed like a lot of work when we can design around it. Acceptable database types (can be mapped to other CLR types like enums) for Arguments are: + - String - Double (integers should be coerced to double for simplicity) - DateTime Default CLR Argument mappings: + - FilterExpression via foreign key -FilterExpression is a single Expression, whether that's something like "Or" or "HasTag" in `And(Or(HasTag('comedy'), HasTag('action')), Not(HasTag('18 restricted')))`. Expressions should be the least amount of work possible. NAND can be expressed as `Not(And())`. Exceptions can be made if it would take more than 2 or 3 operations, such as XOR. An Exception was made for GreaterThanEqual, NotEqual, and LessThanEqual, only because most people would expect those to exist. +FilterExpression is a single Expression, whether that's something like "Or" or "HasTag" +in `And(Or(HasTag('comedy'), HasTag('action')), Not(HasTag('18 restricted')))`. Expressions should be the least amount +of work possible. NAND can be expressed as `Not(And())`. Exceptions can be made if it would take more than 2 or 3 +operations, such as XOR. An Exception was made for GreaterThanEqual, NotEqual, and LessThanEqual, only because most +people would expect those to exist. -This is all a design for the back end. Expressions can be communicated in many ways. For example, the above could be written `tag: 'comedy' or tag: 'action' and not tag: '18 restricted'` or `HasAnyTags('comedy', 'action') && HasNoTags('18 restriced')`. It just depends on how you map the input. \ No newline at end of file +This is all a design for the back end. Expressions can be communicated in many ways. For example, the above could be +written `tag: 'comedy' or tag: 'action' and not tag: '18 restricted'` +or `HasAnyTags('comedy', 'action') && HasNoTags('18 restriced')`. It just depends on how you map the input. \ No newline at end of file diff --git a/Shoko.Server/Repositories/Cached/FilterRepository.cs b/Shoko.Server/Repositories/Cached/FilterRepository.cs index 567d81247..ac93596d5 100644 --- a/Shoko.Server/Repositories/Cached/FilterRepository.cs +++ b/Shoko.Server/Repositories/Cached/FilterRepository.cs @@ -356,7 +356,7 @@ public void CreateInitialFilters() FilterType = GroupFilterType.UserDefined, Expression = new GreaterThanEqualExpression { - Left = new DateAddFunction { Selector = new LastAddedDateSelector(), Parameter = TimeSpan.FromDays(10) }, + Left = new DateAddFunction(new LastAddedDateSelector(), TimeSpan.FromDays(10)), Right = new TodayFunction() }, SortingExpression = new LastAddedDateSortingSelector { Descending = true} @@ -368,12 +368,8 @@ public void CreateInitialFilters() { Name = Constants.GroupFilterName.NewlyAiringSeries, FilterType = GroupFilterType.UserDefined, - Expression = new GreaterThanEqualExpression - { - Left = new DateAddFunction { Selector = new LastAirDateSelector(), Parameter = TimeSpan.FromDays(30) }, - Right = new TodayFunction() - }, - SortingExpression = new LastAirDateSortingSelector { Descending = true} + Expression = new GreaterThanEqualExpression(new DateAddFunction(new LastAirDateSelector(), TimeSpan.FromDays(30)), new TodayFunction()), + SortingExpression = new LastAirDateSortingSelector { Descending = true } }; Save(gf); @@ -414,15 +410,8 @@ public void CreateInitialFilters() { Name = Constants.GroupFilterName.RecentlyWatched, FilterType = GroupFilterType.UserDefined, - Expression = new AndExpression - { - Left = new HasWatchedEpisodesExpression(), - Right = new GreaterThanEqualExpression - { - Left = new DateAddFunction { Selector = new LastWatchedDateSelector(), Parameter = TimeSpan.FromDays(10) }, - Right = new TodayFunction() - }, - }, + Expression = new AndExpression(new HasWatchedEpisodesExpression(), new + GreaterThanEqualExpression(new DateAddFunction(new LastWatchedDateSelector(), TimeSpan.FromDays(10)), new TodayFunction())), SortingExpression = new LastWatchedDateSortingSelector { Descending = true @@ -436,11 +425,7 @@ public void CreateInitialFilters() Name = Constants.GroupFilterName.MissingLinks, ApplyAtSeriesLevel = true, FilterType = GroupFilterType.UserDefined, - Expression = new OrExpression - { - Left = new MissingTvDBLinkExpression(), - Right = new MissingTMDbLinkExpression() - }, + Expression = new OrExpression(new MissingTvDBLinkExpression(), new MissingTMDbLinkExpression()), SortingExpression = new NameSortingSelector() }; Save(gf); @@ -680,12 +665,7 @@ public List GetLockedGroupFilters() public List GetTimeDependentFilters() { - return ReadLock( - () => - { - return GetAll().Where(a => a.Expression.TimeDependent).ToList(); - } - ); + return ReadLock(() => GetAll().Where(a => a.Expression.TimeDependent).ToList()); } public ChangeTracker GetChangeTracker() diff --git a/Shoko.Tests/Shoko.Tests/FilterTests.cs b/Shoko.Tests/Shoko.Tests/FilterTests.cs index 6a4999d75..ed4727a77 100644 --- a/Shoko.Tests/Shoko.Tests/FilterTests.cs +++ b/Shoko.Tests/Shoko.Tests/FilterTests.cs @@ -8,7 +8,6 @@ using Shoko.Server.Filters.Logic.DateTimes; using Shoko.Server.Filters.Selectors; using Shoko.Server.Filters.User; -using Shoko.Server.Models.Filters; using Xunit; namespace Shoko.Tests; @@ -24,32 +23,17 @@ public class FilterTests private const string SeriesUserFilterableString = "{\"IsFavorite\":false,\"WatchedEpisodes\":12,\"UnwatchedEpisodes\":0,\"HasVotes\":false,\"HasPermanentVotes\":false,\"MissingPermanentVotes\":false,\"WatchedDate\":\"2023-05-05T13:42:13.3933582\",\"LastWatchedDate\":\"2023-05-05T13:43:21.3729042\",\"LowestUserRating\":0.0,\"HighestUserRating\":0.0,\"MissingEpisodes\":0,\"MissingEpisodesCollecting\":0,\"Tags\":[\"high school\",\"dynamic\",\"themes\",\"source material\",\"setting\",\"elements\",\"place\",\"Earth\",\"Japan\",\"Kyoto\",\"manga\",\"Asia\",\"comedy\",\"friendship\",\"daily life\",\"school life\",\"funny expressions\",\"storytelling\",\"character driven\",\"facial distortion\",\"narration\",\"origin\",\"episodic\",\"Japanese production\"],\"CustomTags\":[],\"Years\":[2022],\"Seasons\":[{\"Item1\":2022,\"Item2\":1}],\"HasTvDBLink\":true,\"HasMissingTvDbLink\":false,\"HasTMDbLink\":false,\"HasMissingTMDbLink\":false,\"HasTraktLink\":false,\"HasMissingTraktLink\":true,\"IsFinished\":true,\"AirDate\":\"2022-04-06T20:00:00\",\"LastAirDate\":\"2022-06-22T20:00:00\",\"AddedDate\":\"2023-05-05T14:42:24.3131538\",\"LastAddedDate\":\"2023-05-05T13:37:50.32298\",\"EpisodeCount\":12,\"TotalEpisodeCount\":12,\"LowestAniDBRating\":7.4,\"HighestAniDBRating\":7.4,\"VideoSources\":[\"Web\"],\"AnimeTypes\":[\"TVSeries\"],\"AudioLanguages\":[\"japanese\"],\"SubtitleLanguages\":[\"english\"]}"; - public static readonly IEnumerable GroupFilterable = new[] { new[] { JsonConvert.DeserializeObject(GroupFilterableString) }}; - public static readonly IEnumerable GroupUserFilterable = new[] { new[] { JsonConvert.DeserializeObject(GroupUserFilterableString) }}; - public static readonly IEnumerable SeriesFilterable = new[] { new[] { JsonConvert.DeserializeObject(SeriesFilterableString) }}; - public static readonly IEnumerable SeriesUserFilterable = new[] { new[] { JsonConvert.DeserializeObject(SeriesUserFilterableString) }}; + public static readonly IEnumerable GroupFilterable = new[] { new[] { JsonConvert.DeserializeObject(GroupFilterableString, new IReadOnlySetConverter()) }}; + public static readonly IEnumerable GroupUserFilterable = new[] { new[] { JsonConvert.DeserializeObject(GroupUserFilterableString, new IReadOnlySetConverter()) }}; + public static readonly IEnumerable SeriesFilterable = new[] { new[] { JsonConvert.DeserializeObject(SeriesFilterableString, new IReadOnlySetConverter()) }}; + public static readonly IEnumerable SeriesUserFilterable = new[] { new[] { JsonConvert.DeserializeObject(SeriesUserFilterableString, new IReadOnlySetConverter()) }}; [Theory, MemberData(nameof(GroupFilterable))] public void GroupFilterable_WithUserFilter_ExpectsException(Filterable group) { - var top = new AndExpression - { - Left = new AndExpression - { - Left = new HasTagExpression - { - Parameter = "comedy" - }, - Right = new NotExpression - { - Left = new HasTagExpression - { - Parameter = "18 restricted" - } - } - }, - Right = new HasWatchedEpisodesExpression() - }; + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), + new NotExpression(new HasTagExpression("18 restricted"))), + new HasWatchedEpisodesExpression()); Assert.True(top.UserDependent); Assert.Throws(() => top.Evaluate(group)); @@ -58,27 +42,8 @@ public void GroupFilterable_WithUserFilter_ExpectsException(Filterable group) [Theory, MemberData(nameof(GroupFilterable))] public void GroupFilterable_WithoutUserFilter_ExpectsTrue(Filterable group) { - var top = new AndExpression - { - Left = new AndExpression - { - Left = new HasTagExpression - { - Parameter = "comedy" - }, - Right = new NotExpression - { - Left = new HasTagExpression - { - Parameter = "18 restricted" - } - } - }, - Right = new HasVideoSourceExpression - { - Parameter = "Web" - } - }; + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), + new HasVideoSourceExpression("Web")); Assert.False(top.UserDependent); Assert.True(top.Evaluate(group)); @@ -87,35 +52,9 @@ public void GroupFilterable_WithoutUserFilter_ExpectsTrue(Filterable group) [Theory, MemberData(nameof(GroupFilterable))] public void GroupFilterable_WithDateFunctionFilter_ExpectsFalse(Filterable group) { - var top = new AndExpression - { - Left = new AndExpression - { - Left = new HasTagExpression - { - Parameter = "comedy" - }, - Right = new NotExpression - { - Left = new HasTagExpression - { - Parameter = "18 restricted" - } - } - }, - Right = new GreaterThanEqualExpression - { - Left = new LastAddedDateSelector(), - Right = new DateDiffFunction - { - Selector = new DateAddFunction - { - Selector = new TodayFunction(), Parameter = TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1) - }, - Parameter = TimeSpan.FromDays(30) - }, - } - }; + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), + new GreaterThanEqualExpression(new LastAddedDateSelector(), + new DateDiffFunction(new DateAddFunction(new TodayFunction(), TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1)), TimeSpan.FromDays(30)))); Assert.False(top.UserDependent); Assert.False(top.Evaluate(group)); @@ -124,35 +63,9 @@ public void GroupFilterable_WithDateFunctionFilter_ExpectsFalse(Filterable group [Theory, MemberData(nameof(GroupFilterable))] public void GroupFilterable_WithDateFunctionFilter_ExpectsTrue(Filterable group) { - var top = new AndExpression - { - Left = new AndExpression - { - Left = new HasTagExpression - { - Parameter = "comedy" - }, - Right = new NotExpression - { - Left = new HasTagExpression - { - Parameter = "18 restricted" - } - } - }, - Right = new GreaterThanEqualExpression - { - Left = new LastAddedDateSelector(), - Right = new DateDiffFunction - { - Selector = new DateAddFunction - { - Selector = new TodayFunction(), Parameter = TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1) - }, - Parameter = TimeSpan.FromDays(120) - }, - } - }; + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), + new GreaterThanEqualExpression(new LastAddedDateSelector(), new DateDiffFunction( + new DateAddFunction(new TodayFunction(), TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1)), TimeSpan.FromDays(120)))); Assert.False(top.UserDependent); Assert.True(top.Evaluate(group)); @@ -161,24 +74,8 @@ public void GroupFilterable_WithDateFunctionFilter_ExpectsTrue(Filterable group) [Theory, MemberData(nameof(GroupUserFilterable))] public void GroupUserFilterable_WithUserFilter_ExpectsTrue(UserDependentFilterable group) { - var top = new AndExpression - { - Left = new AndExpression - { - Left = new HasTagExpression - { - Parameter = "comedy" - }, - Right = new NotExpression - { - Left = new HasTagExpression - { - Parameter = "18 restricted" - } - } - }, - Right = new HasWatchedEpisodesExpression() - }; + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), + new HasWatchedEpisodesExpression()); Assert.True(top.UserDependent); Assert.True(top.Evaluate(group)); @@ -187,24 +84,8 @@ public void GroupUserFilterable_WithUserFilter_ExpectsTrue(UserDependentFilterab [Theory, MemberData(nameof(SeriesFilterable))] public void SeriesFilterable_WithUserFilter_ExpectsException(Filterable series) { - var top = new AndExpression - { - Left = new AndExpression - { - Left = new HasTagExpression - { - Parameter = "comedy" - }, - Right = new NotExpression - { - Left = new HasTagExpression - { - Parameter = "18 restricted" - } - } - }, - Right = new HasWatchedEpisodesExpression() - }; + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), + new HasWatchedEpisodesExpression()); Assert.True(top.UserDependent); Assert.Throws(() => top.Evaluate(series)); @@ -213,24 +94,8 @@ public void SeriesFilterable_WithUserFilter_ExpectsException(Filterable series) [Theory, MemberData(nameof(SeriesUserFilterable))] public void SeriesUserFilterable_WithUserFilter_ExpectsTrue(UserDependentFilterable series) { - var top = new AndExpression - { - Left = new AndExpression - { - Left = new HasTagExpression - { - Parameter = "comedy" - }, - Right = new NotExpression - { - Left = new HasTagExpression - { - Parameter = "18 restricted" - } - } - }, - Right = new HasWatchedEpisodesExpression() - }; + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), + new HasWatchedEpisodesExpression()); Assert.True(top.UserDependent); Assert.True(top.Evaluate(series)); diff --git a/Shoko.Tests/Shoko.Tests/IReadOnlySetConverter.cs b/Shoko.Tests/Shoko.Tests/IReadOnlySetConverter.cs new file mode 100644 index 000000000..28f26ccd8 --- /dev/null +++ b/Shoko.Tests/Shoko.Tests/IReadOnlySetConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Shoko.Tests; + +public class IReadOnlySetConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + if (!objectType.IsGenericType) return false; + var arguments = objectType.GetGenericArguments(); + if (arguments.Length > 1) return false; + return typeof(IReadOnlySet<>) == objectType.GetGenericTypeDefinition(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var argument = objectType.GetGenericArguments()[0]; + var newType = typeof(HashSet<>).MakeGenericType(argument); + return serializer.Deserialize(reader, newType); + } +} From 45a39f845c5eec240dd0d6e6cf3da3865cfd2133 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sun, 10 Sep 2023 23:06:19 -0400 Subject: [PATCH 14/34] Start migrating to Filters --- Shoko.CLI/Program.cs | 36 +-- Shoko.Server/API/v3/Models/Shoko/Filter.cs | 72 +++--- Shoko.Server/API/v3/Models/Shoko/WebUI.cs | 9 +- Shoko.Server/Filters/FilterEvaluator.cs | 2 +- Shoko.Server/Filters/FilterExtensions.cs | 2 + Shoko.Server/Mappings/FilterMap.cs | 2 +- Shoko.Server/Models/Filter.cs | 3 +- .../Repositories/Cached/FilterRepository.cs | 205 +----------------- Shoko.Server/Tasks/AnimeGroupCreator.cs | 32 +-- 9 files changed, 74 insertions(+), 289 deletions(-) diff --git a/Shoko.CLI/Program.cs b/Shoko.CLI/Program.cs index 17c32b604..d19fec2ca 100644 --- a/Shoko.CLI/Program.cs +++ b/Shoko.CLI/Program.cs @@ -43,7 +43,7 @@ public static void Main() startup.Start(); AddEventHandlers(); // TODO Remove this after filter merge - Utils.ShokoServer.DBSetupCompleted += OnShokoServerOnDBSetupCompleted; + //Utils.ShokoServer.DBSetupCompleted += OnShokoServerOnDBSetupCompleted; startup.WaitForShutdown(); } catch (Exception e) @@ -54,22 +54,30 @@ public static void Main() private static void OnShokoServerOnDBSetupCompleted(object? o, EventArgs eventArgs) { - var comedyFilter = RepoFactory.Filter.GetAll().FirstOrDefault(a => a.Name.Equals("comedy", StringComparison.InvariantCultureIgnoreCase)); - if (comedyFilter == null) return; + var filterEvaluator = Utils.ServiceContainer.GetRequiredService(); var s = Stopwatch.StartNew(); - var result = filterEvaluator.EvaluateFilter(comedyFilter, null); - s.Stop(); - _logger.LogInformation("Filtering took {Time}ms", s.ElapsedMilliseconds); - s.Restart(); - var groups = result.SelectMany(a => a.Select(b => new - { - Group = RepoFactory.AnimeGroup.GetByID(a.Key), Series = RepoFactory.AnimeSeries.GetByID(b) - })) - .GroupBy(a => a.Group, a => a.Series) - .ToDictionary(a => a.Key, a => a.ToList()); + RepoFactory.Filter.CreateOrVerifyDirectoryFilters(); s.Stop(); - _logger.LogInformation("Projecting results took {Time}ms", s.ElapsedMilliseconds); + _logger.LogInformation("Generating Directories took {Time}ms", s.ElapsedMilliseconds); + var comedyFilter = RepoFactory.Filter.GetAll().FirstOrDefault(a => a.Name.Equals("comedy", StringComparison.InvariantCultureIgnoreCase)); + if (comedyFilter != null) + { + s.Restart(); + var result = filterEvaluator.EvaluateFilter(comedyFilter, null); + s.Stop(); + _logger.LogInformation("Filtering took {Time}ms", s.ElapsedMilliseconds); + s.Restart(); + var groups = result.SelectMany(a => a.Select(b => new + { + Group = RepoFactory.AnimeGroup.GetByID(a.Key), Series = RepoFactory.AnimeSeries.GetByID(b) + })) + .GroupBy(a => a.Group, a => a.Series) + .ToDictionary(a => a.Key, a => a.ToList()); + s.Stop(); + _logger.LogInformation("Projecting results took {Time}ms", s.ElapsedMilliseconds); + } + _logger.LogInformation("Finished"); } diff --git a/Shoko.Server/API/v3/Models/Shoko/Filter.cs b/Shoko.Server/API/v3/Models/Shoko/Filter.cs index aa545dcc8..d6508ac2e 100644 --- a/Shoko.Server/API/v3/Models/Shoko/Filter.cs +++ b/Shoko.Server/API/v3/Models/Shoko/Filter.cs @@ -1,16 +1,19 @@ -using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Shoko.Models.Enums; using Shoko.Models.Server; using Shoko.Server.API.v3.Models.Common; +using Shoko.Server.Filters; +using Shoko.Server.Filters.Logic; using Shoko.Server.Models; using Shoko.Server.Repositories; +using FilterPreset = Shoko.Server.Models.Filter; // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global // ReSharper disable UnusedAutoPropertyAccessor.Global @@ -73,27 +76,25 @@ public class Filter : BaseModel [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public List? Sorting { get; set; } - public Filter(HttpContext ctx, SVR_GroupFilter groupFilter, bool fullModel = false) + public Filter(HttpContext ctx, FilterPreset groupFilter, bool fullModel = false) { var user = ctx.GetUser(); - IDs = new FilterIDs { ID = groupFilter.GroupFilterID, ParentFilter = groupFilter.ParentGroupFilterID }; - Name = groupFilter.GroupFilterName; - IsLocked = groupFilter.IsLocked; - IsDirectory = groupFilter.IsDirectory; - IsInverted = groupFilter.BaseCondition == (int)GroupFilterBaseCondition.Exclude; - IsHidden = groupFilter.IsHidden; - ApplyAtSeriesLevel = groupFilter.ApplyToSeries == 1; + IDs = new FilterIDs { ID = groupFilter.FilterID, ParentFilter = groupFilter.ParentFilterID }; + Name = groupFilter.Name; + IsLocked = groupFilter.Locked; + IsDirectory = groupFilter.IsDirectory(); + IsInverted = groupFilter.Expression is NotExpression; + IsHidden = groupFilter.Hidden; + ApplyAtSeriesLevel = groupFilter.ApplyAtSeriesLevel; if (fullModel) { Conditions = groupFilter.Conditions.Select(condition => new FilterCondition(condition)).ToList(); Sorting = groupFilter.SortCriteriaList.Select(sort => new SortingCriteria(sort)).ToList(); } - Size = IsDirectory ? ( - RepoFactory.GroupFilter.GetByParentID(groupFilter.GroupFilterID).Count - ) : ( - groupFilter.GroupsIds.TryGetValue(user.JMMUserID, out var groupSet) ? groupSet.Count : 0 - ); + + var evaluator = ctx.RequestServices.GetRequiredService(); + Size = IsDirectory ? RepoFactory.Filter.GetByParentID(groupFilter.FilterID).Count : evaluator.EvaluateFilter(groupFilter, user?.JMMUserID).Count(); } /// @@ -233,14 +234,13 @@ public class CreateOrUpdateFilterBody public CreateOrUpdateFilterBody() { } - public CreateOrUpdateFilterBody(SVR_GroupFilter groupFilter) + public CreateOrUpdateFilterBody(FilterPreset groupFilter) { - Name = groupFilter.GroupFilterName; - ParentID = groupFilter.ParentGroupFilterID; - IsDirectory = groupFilter.IsDirectory; - IsInverted = groupFilter.BaseCondition == (int)GroupFilterBaseCondition.Exclude; - IsHidden = groupFilter.IsHidden; - ApplyAtSeriesLevel = groupFilter.ApplyToSeries == 1; + Name = groupFilter.Name; + ParentID = groupFilter.ParentFilterID; + IsDirectory = groupFilter.IsDirectory(); + IsHidden = groupFilter.Hidden; + ApplyAtSeriesLevel = groupFilter.ApplyAtSeriesLevel; if (!IsDirectory) { Conditions = groupFilter.Conditions.Select(condition => new FilterCondition(condition)).ToList(); @@ -248,9 +248,9 @@ public CreateOrUpdateFilterBody(SVR_GroupFilter groupFilter) } } - public Filter? MergeWithExisting(HttpContext ctx, SVR_GroupFilter groupFilter, ModelStateDictionary modelState, bool skipSave = false) + public Filter? MergeWithExisting(HttpContext ctx, FilterPreset groupFilter, ModelStateDictionary modelState, bool skipSave = false) { - if (groupFilter.IsLocked) + if (groupFilter.Locked) modelState.AddModelError("IsLocked", "Filter is locked."); // Defer to `null` if the id is `0`. @@ -259,17 +259,17 @@ public CreateOrUpdateFilterBody(SVR_GroupFilter groupFilter) if (ParentID.HasValue) { - var parentFilter = RepoFactory.GroupFilter.GetByID(ParentID.Value); + var parentFilter = RepoFactory.Filter.GetByID(ParentID.Value); if (parentFilter == null) { modelState.AddModelError(nameof(ParentID), $"Unable to find parent filter with id {ParentID.Value}"); } else { - if (parentFilter.IsLocked) + if (parentFilter.Locked) modelState.AddModelError(nameof(ParentID), $"Unable to add a sub-filter to a filter that is locked."); - if (!parentFilter.IsDirectory) + if (!parentFilter.IsDirectory()) modelState.AddModelError(nameof(ParentID), $"Unable to add a sub-filter to a filter that is not a directorty filter."); } } @@ -287,7 +287,7 @@ public CreateOrUpdateFilterBody(SVR_GroupFilter groupFilter) } else { - var subFilters = groupFilter.GroupFilterID != 0 ? RepoFactory.GroupFilter.GetByParentID(groupFilter.GroupFilterID) : new(); + var subFilters = groupFilter.FilterID != 0 ? RepoFactory.Filter.GetByParentID(groupFilter.FilterID) : new(); if (subFilters.Count > 0) modelState.AddModelError(nameof(IsDirectory), "Cannot turn a directory filter with sub-filters into a normal filter without first removing the sub-filters"); } @@ -296,15 +296,13 @@ public CreateOrUpdateFilterBody(SVR_GroupFilter groupFilter) if (!modelState.IsValid) return null; - groupFilter.ParentGroupFilterID = ParentID; - groupFilter.FilterType = (int)(IsDirectory ? GroupFilterType.UserDefined | GroupFilterType.Directory : GroupFilterType.UserDefined); - groupFilter.GroupFilterName = Name ?? string.Empty; - groupFilter.IsHidden = IsHidden; - groupFilter.ApplyToSeries = ApplyAtSeriesLevel ? 1 : 0; + groupFilter.ParentFilterID = ParentID; + groupFilter.FilterType = IsDirectory ? GroupFilterType.UserDefined | GroupFilterType.Directory : GroupFilterType.UserDefined; + groupFilter.Name = Name; + groupFilter.Hidden = IsHidden; + groupFilter.ApplyAtSeriesLevel = ApplyAtSeriesLevel; if (IsDirectory) { - groupFilter.BaseCondition = (int)GroupFilterBaseCondition.Include; - groupFilter.Conditions = new(); groupFilter.SortCriteriaList = new() { new GroupFilterSortingCriteria() @@ -316,7 +314,6 @@ public CreateOrUpdateFilterBody(SVR_GroupFilter groupFilter) } else { - groupFilter.BaseCondition = (int)(IsInverted ? GroupFilterBaseCondition.Exclude : GroupFilterBaseCondition.Include); if (Conditions != null) { groupFilter.Conditions = Conditions @@ -340,12 +337,9 @@ public CreateOrUpdateFilterBody(SVR_GroupFilter groupFilter) } } - // Re-calculate the groups and series that belong to the group filter. - groupFilter.CalculateGroupsAndSeries(); - // Skip saving if we're just going to preview a group filter. if (!skipSave) - RepoFactory.GroupFilter.Save(groupFilter); + RepoFactory.Filter.Save(groupFilter); return new Filter(ctx, groupFilter, true); } diff --git a/Shoko.Server/API/v3/Models/Shoko/WebUI.cs b/Shoko.Server/API/v3/Models/Shoko/WebUI.cs index 62355ef9e..3ac10133d 100644 --- a/Shoko.Server/API/v3/Models/Shoko/WebUI.cs +++ b/Shoko.Server/API/v3/Models/Shoko/WebUI.cs @@ -214,10 +214,11 @@ private Filter GetFirstAiringSeasonGroupFilter(HttpContext ctx, SVR_AniDB_Anime return null; var seasonName = $"{season} {year}"; - var seasonsFilterID = RepoFactory.GroupFilter.GetTopLevel() - .FirstOrDefault(f => f.GroupFilterName == "Seasons").GroupFilterID; - var firstAirSeason = RepoFactory.GroupFilter.GetByParentID(seasonsFilterID) - .FirstOrDefault(f => f.GroupFilterName == seasonName); + var seasonsFilterID = RepoFactory.Filter.GetTopLevel() + .FirstOrDefault(f => f.FilterType == (GroupFilterType.Directory | GroupFilterType.Season))?.FilterID; + if (seasonsFilterID == null) return null; + var firstAirSeason = RepoFactory.Filter.GetByParentID(seasonsFilterID.Value) + .FirstOrDefault(f => f.Name == seasonName); if (firstAirSeason == null) return null; diff --git a/Shoko.Server/Filters/FilterEvaluator.cs b/Shoko.Server/Filters/FilterEvaluator.cs index ef8ed7b20..f7b766053 100644 --- a/Shoko.Server/Filters/FilterEvaluator.cs +++ b/Shoko.Server/Filters/FilterEvaluator.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using Shoko.Server.Filters.SortingSelectors; -using Shoko.Server.Models.Filters; +using Shoko.Server.Models; using Shoko.Server.Repositories; using Shoko.Server.Repositories.Cached; diff --git a/Shoko.Server/Filters/FilterExtensions.cs b/Shoko.Server/Filters/FilterExtensions.cs index 6281ca161..448f731e4 100644 --- a/Shoko.Server/Filters/FilterExtensions.cs +++ b/Shoko.Server/Filters/FilterExtensions.cs @@ -13,6 +13,8 @@ namespace Shoko.Server.Filters; public static class FilterExtensions { + public static bool IsDirectory(this Filter filter) => (filter.FilterType & GroupFilterType.Directory) != 0; + public static Filterable ToFilterable(this SVR_AnimeSeries series) { var anime = series.GetAnime(); diff --git a/Shoko.Server/Mappings/FilterMap.cs b/Shoko.Server/Mappings/FilterMap.cs index 60c404ca4..3573747af 100644 --- a/Shoko.Server/Mappings/FilterMap.cs +++ b/Shoko.Server/Mappings/FilterMap.cs @@ -1,7 +1,7 @@ using FluentNHibernate.Mapping; using Shoko.Models.Enums; using Shoko.Server.Databases.TypeConverters; -using Shoko.Server.Models.Filters; +using Shoko.Server.Models; namespace Shoko.Server.Mappings; diff --git a/Shoko.Server/Models/Filter.cs b/Shoko.Server/Models/Filter.cs index 7e6dec661..2a36c2b6a 100644 --- a/Shoko.Server/Models/Filter.cs +++ b/Shoko.Server/Models/Filter.cs @@ -1,8 +1,7 @@ -using System; using Shoko.Models.Enums; using Shoko.Server.Filters; -namespace Shoko.Server.Models.Filters; +namespace Shoko.Server.Models; public class Filter { diff --git a/Shoko.Server/Repositories/Cached/FilterRepository.cs b/Shoko.Server/Repositories/Cached/FilterRepository.cs index ac93596d5..44dc85134 100644 --- a/Shoko.Server/Repositories/Cached/FilterRepository.cs +++ b/Shoko.Server/Repositories/Cached/FilterRepository.cs @@ -17,7 +17,6 @@ using Shoko.Server.Filters.SortingSelectors; using Shoko.Server.Filters.User; using Shoko.Server.Models; -using Shoko.Server.Models.Filters; using Shoko.Server.Repositories.NHibernate; using Shoko.Server.Server; using Constants = Shoko.Server.Server.Constants; @@ -217,13 +216,13 @@ public void CreateOrVerifyDirectoryFilters(bool frominit = false, ISet t HashSet allyears; if (airdate == null || airdate.Count == 0) { - var grps = RepoFactory.AnimeSeries.GetAll().Select(a => a.Contract).Where(a => a != null).ToList(); + var grps = RepoFactory.AnimeSeries.GetAll().Select(a => a.GetAnime()); allyears = new HashSet(); - foreach (var ser in grps) + foreach (var anime in grps) { - var endyear = ser.AniDBAnime.AniDBAnime.EndYear; - var startyear = ser.AniDBAnime.AniDBAnime.BeginYear; + var endyear = anime.EndYear; + var startyear = anime.BeginYear; if (endyear <= 0) endyear = DateTime.Today.Year; if (endyear < startyear || endyear - startyear + 1 >= int.MaxValue) endyear = startyear; if (startyear != 0) allyears.UnionWith(Enumerable.Range(startyear, endyear - startyear + 1).Select(a => a)); @@ -452,35 +451,6 @@ public override void Delete(IReadOnlyCollection objs) } } - /// - /// Updates a batch of s. - /// - /// - /// This method ONLY updates existing s. It will not insert any that don't already exist. - /// - /// The NHibernate session. - /// The batch of s to update. - /// or is null. - public void BatchUpdate(ISessionWrapper session, IEnumerable groupFilters) - { - if (session == null) - { - throw new ArgumentNullException(nameof(session)); - } - - if (groupFilters == null) - { - throw new ArgumentNullException(nameof(groupFilters)); - } - - foreach (var groupFilter in groupFilters) - { - session.Update(groupFilter); - UpdateCache(groupFilter); - Changes.AddOrUpdate(groupFilter.FilterID); - } - } - public List GetByParentID(int parentid) { return ReadLock(() => Parents.GetMultiple(parentid)); @@ -491,173 +461,6 @@ public List GetTopLevel() return GetByParentID(0); } - /// - /// Calculates what groups should belong to tag related group filters. - /// - /// The NHibernate session. - /// A that maps group filter ID to anime group IDs. - /// is null. - public void CalculateAnimeSeriesPerTagGroupFilter(ISessionWrapper session) - { - if (session == null) - { - throw new ArgumentNullException(nameof(session)); - } - - var tagsdirec = GetAll(session).FirstOrDefault(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Tag)); - - DropAllTagFilters(session); - - // user -> tag -> series - var somethingDictionary = - new ConcurrentDictionary>>(); - var users = new List { null }; - users.AddRange(RepoFactory.JMMUser.GetAll()); - var tags = RepoFactory.AniDB_Tag.GetAll().ToLookup(a => a?.TagName?.ToLowerInvariant()); - - Parallel.ForEach(tags, tag => - { - foreach (var series in tag.ToList().SelectMany(a => RepoFactory.AniDB_Anime_Tag.GetAnimeWithTag(a.TagID))) - { - var seriesTags = series.GetAnime()?.GetAllTags(); - foreach (var user in users) - { - if (user?.GetHideCategories().FindInEnumerable(seriesTags) ?? false) continue; - - if (somethingDictionary.ContainsKey(user?.JMMUserID ?? 0)) - { - if (somethingDictionary[user?.JMMUserID ?? 0].ContainsKey(tag.Key)) - { - somethingDictionary[user?.JMMUserID ?? 0][tag.Key] - .Add(series.AnimeSeriesID); - } - else - { - somethingDictionary[user?.JMMUserID ?? 0].AddOrUpdate(tag.Key, - new HashSet { series.AnimeSeriesID }, (oldTag, oldIDs) => - { - lock (oldIDs) - { - oldIDs.Add(series.AnimeSeriesID); - } - - return oldIDs; - }); - } - } - else - { - somethingDictionary.AddOrUpdate(user?.JMMUserID ?? 0, id => - { - var newdict = new ConcurrentDictionary>(); - newdict.AddOrUpdate(tag.Key, new HashSet { series.AnimeSeriesID }, - (oldTag, oldIDs) => - { - lock (oldIDs) - { - oldIDs.Add(series.AnimeSeriesID); - } - - return oldIDs; - }); - return newdict; - }, - (i, value) => - { - if (value.ContainsKey(tag.Key)) - { - value[tag.Key] - .Add(series.AnimeSeriesID); - } - else - { - value.AddOrUpdate(tag.Key, - new HashSet { series.AnimeSeriesID }, (oldTag, oldIDs) => - { - lock (oldIDs) - { - oldIDs.Add(series.AnimeSeriesID); - } - - return oldIDs; - }); - } - - return value; - }); - } - } - } - }); - - var lookup = somethingDictionary.Keys.Where(a => somethingDictionary[a] != null).ToDictionary(key => key, key => - somethingDictionary[key].Where(a => a.Value != null) - .SelectMany(p => p.Value.Select(a => Tuple.Create(p.Key, a))) - .ToLookup(p => p.Item1, p => p.Item2)); - - CreateAllTagFilters(session, tagsdirec, lookup); - } - - private void DropAllTagFilters(ISessionWrapper session) - { - ClearCache(); - Lock(() => - { - using var trans = session.BeginTransaction(); - session.CreateQuery($"DELETE FROM {nameof(Filter)} WHERE FilterType = {(int)GroupFilterType.Tag};") - .ExecuteUpdate(); - trans.Commit(); - }); - } - - private void CreateAllTagFilters(ISessionWrapper session, Filter tagsdirec, - Dictionary> lookup) - { - if (tagsdirec == null) - { - return; - } - - var alltags = new HashSet( - RepoFactory.AniDB_Tag.GetAllForLocalSeries().Select(a => a.TagName.Replace('`', '\'')), - StringComparer.InvariantCultureIgnoreCase); - var toAdd = new List(alltags.Count); - - var users = RepoFactory.JMMUser.GetAll().ToList(); - - //AniDB Tags are in english so we use en-us culture - var tinfo = new CultureInfo("en-US", false).TextInfo; - foreach (var s in alltags) - { - // this is creating new tag filters, so locking isn't completely necessary. - // Ideally, we would create a blank filter to ensure an ID exists, but then it would be empty, - // and would have data inconsistencies, anyway - var yf = new Filter - { - ParentFilterID = tagsdirec.FilterID, - Hidden = false, - ApplyAtSeriesLevel = true, - Name = tinfo.ToTitleCase(s), - Locked = true, - FilterType = GroupFilterType.Tag, - Expression = new HasTagExpression { Parameter = s }, - SortingExpression = new NameSortingSelector() - }; - - Lock(() => - { - using var trans = session.BeginTransaction(); - // get an ID - session.Insert(yf); - trans.Commit(); - }); - - toAdd.Add(yf); - } - - Populate(session, false); - } - public List GetLockedGroupFilters() { return ReadLock(() => Cache.Values.Where(a => a.Locked).ToList()); diff --git a/Shoko.Server/Tasks/AnimeGroupCreator.cs b/Shoko.Server/Tasks/AnimeGroupCreator.cs index 2fec46274..5a4dfaba6 100644 --- a/Shoko.Server/Tasks/AnimeGroupCreator.cs +++ b/Shoko.Server/Tasks/AnimeGroupCreator.cs @@ -14,7 +14,6 @@ using Shoko.Server.Repositories.Cached; using Shoko.Server.Repositories.NHibernate; using Shoko.Server.Server; -using Shoko.Server.Settings; using Shoko.Server.Utilities; namespace Shoko.Server.Tasks; @@ -30,6 +29,7 @@ internal class AnimeGroupCreator private readonly AnimeGroupRepository _animeGroupRepo = RepoFactory.AnimeGroup; private readonly AnimeGroup_UserRepository _animeGroupUserRepo = RepoFactory.AnimeGroup_User; private readonly GroupFilterRepository _groupFilterRepo = RepoFactory.GroupFilter; + private readonly FilterRepository _filterRepo = RepoFactory.Filter; private readonly JMMUserRepository _userRepo = RepoFactory.JMMUser; private readonly bool _autoGroupSeries; @@ -200,35 +200,13 @@ private void UpdateAnimeGroupsAndTheirContracts(IReadOnlyCollection /// Assumes that all caches are up to date. /// - private void UpdateGroupFilters(ISessionWrapper session) + private void UpdateGroupFilters() { _log.Info("Updating Group Filters"); _log.Info("Calculating Tag Filters"); ServerState.Instance.DatabaseBlocked = new ServerState.DatabaseBlockedInfo { Blocked = true, Status = "Calculating Tag Filters" }; - _groupFilterRepo.CalculateAnimeSeriesPerTagGroupFilter(session); - _log.Info("Calculating All Other Filters"); - ServerState.Instance.DatabaseBlocked = - new ServerState.DatabaseBlockedInfo { Blocked = true, Status = "Calculating Non-Tag Filters" }; - IEnumerable grpFilters = _groupFilterRepo.GetAll(session).Where(a => - a.FilterType != (int)GroupFilterType.Tag && !a.IsDirectory).ToList(); - - // The main reason for doing this in parallel is because UpdateEntityReferenceStrings does JSON encoding - // and is enough work that it can benefit from running in parallel - Parallel.ForEach( - grpFilters, filter => - { - filter.SeriesIds.Clear(); - filter.CalculateGroupsAndSeries(); - filter.UpdateEntityReferenceStrings(); - }); - - BaseRepository.Lock(() => - { - using var trans = session.BeginTransaction(); - _groupFilterRepo.BatchUpdate(session, grpFilters); - trans.Commit(); - }); + _filterRepo.CreateOrVerifyDirectoryFilters(); _log.Info("Group Filters updated"); } @@ -510,7 +488,7 @@ public void RecreateAllGroups(ISessionWrapper session) _animeGroupUserRepo.Populate(session, false); _groupFilterRepo.Populate(session, false); - UpdateGroupFilters(session); + UpdateGroupFilters(); _log.Info("Successfuly completed re-creating all groups"); } @@ -584,7 +562,7 @@ public void RecalculateStatsContractsForGroup(SVR_AnimeGroup group) // update filters _log.Info($"Recalculating Filters for Group: {group.GroupName} ({group.AnimeGroupID})"); - UpdateGroupFilters(session); + UpdateGroupFilters(); _log.Info($"Done Recalculating Stats and Contracts for Group: {group.GroupName} ({group.AnimeGroupID})"); } From c12397b9d0099aefabd746988031e6090d91d9e2 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Wed, 13 Sep 2023 17:55:06 -0400 Subject: [PATCH 15/34] Filters: Add missing filters, changing a few in meaning. Work on backwards compatibility --- Shoko.Server/API/v3/Models/Shoko/Filter.cs | 29 +- .../Files/HasAudioLanguageExpression.cs | 45 +++ .../Files/HasSharedAudioLanguageExpression.cs | 64 ++++ .../HasSharedSubtitleLanguageExpression.cs | 64 ++++ .../Files/HasSharedVideoSourceExpression.cs | 61 ++++ .../Files/HasSubtitleLanguageExpression.cs | 42 +++ .../Filters/Files/HasVideoSourceExpression.cs | 61 ++++ Shoko.Server/Filters/FilterExpression.cs | 40 +++ Shoko.Server/Filters/FilterExtensions.cs | 46 ++- Shoko.Server/Filters/Filterable.cs | 68 ++-- .../Filters/Functions/DateAddFunction.cs | 40 +++ .../Filters/Functions/DateDiffFunction.cs | 40 +++ .../Filters/Functions/TodayFunction.cs | 40 +++ .../Filters/Info/HasAnimeTypeExpression.cs | 42 +++ .../Filters/Info/HasCustomTagExpression.cs | 42 +++ .../HasMissingEpisodesCollectingExpression.cs | 40 +++ .../Info/HasMissingEpisodesExpression.cs | 40 +++ .../Filters/Info/HasNameExpression.cs | 61 ++++ .../Filters/Info/HasTMDbLinkExpression.cs | 40 +++ Shoko.Server/Filters/Info/HasTagExpression.cs | 42 +++ .../Filters/Info/HasTraktLinkExpression.cs | 40 +++ .../Filters/Info/HasTvDBLinkExpression.cs | 40 +++ .../Filters/Info/HasVideoSourceExpression.cs | 19 -- .../Filters/Info/InSeasonExpression.cs | 41 +++ Shoko.Server/Filters/Info/InYearExpression.cs | 42 +++ .../Filters/Info/IsFinishedExpression.cs | 40 +++ .../Filters/Info/MissingTMDbLinkExpression.cs | 40 +++ .../Info/MissingTraktLinkExpression.cs | 40 +++ .../Filters/Info/MissingTvDBLinkExpression.cs | 40 +++ Shoko.Server/Filters/LegacyFilterConverter.cs | 122 ++++++++ Shoko.Server/Filters/LegacyMappings.cs | 293 ++++++++++++++++++ Shoko.Server/Filters/Logic/AndExpression.cs | 42 +++ .../Logic/DateTimes/EqualExpression.cs | 40 +++ .../DateTimes/GreaterThanEqualExpression.cs | 40 +++ .../Logic/DateTimes/GreaterThanExpression.cs | 40 +++ .../DateTimes/LessThanEqualExpression.cs | 40 +++ .../Logic/DateTimes/LessThanExpression.cs | 40 +++ .../Logic/DateTimes/NotEqualExpression.cs | 40 +++ Shoko.Server/Filters/Logic/NotExpression.cs | 42 +++ .../Filters/Logic/Numbers/EqualExpression.cs | 40 +++ .../Numbers/GreaterThanEqualExpression.cs | 40 +++ .../Logic/Numbers/GreaterThanExpression.cs | 42 +++ .../Logic/Numbers/LessThanEqualExpression.cs | 40 +++ .../Logic/Numbers/LessThanExpression.cs | 42 +++ .../Logic/Numbers/NotEqualExpression.cs | 40 +++ Shoko.Server/Filters/Logic/OrExpression.cs | 42 +++ .../Logic/Strings/ContainsExpression.cs | 40 +++ .../Filters/Logic/Strings/EqualExpression.cs | 40 +++ .../Logic/Strings/NotEqualExpression.cs | 40 +++ Shoko.Server/Filters/Logic/XorExpression.cs | 42 +++ .../Filters/Selectors/AddedDateSelector.cs | 40 +++ .../Filters/Selectors/AirDateSelector.cs | 40 +++ .../Selectors/AudioLanguageCountSelector.cs | 44 ++- .../Filters/Selectors/EpisodeCountSelector.cs | 44 ++- .../Selectors/HighestAniDBRatingSelector.cs | 40 +++ .../Selectors/HighestUserRatingSelector.cs | 40 +++ .../Selectors/LastAddedDateSelector.cs | 40 +++ .../Filters/Selectors/LastAirDateSelector.cs | 40 +++ .../Selectors/LastWatchedDateSelector.cs | 40 +++ .../Selectors/LowestAniDBRatingSelector.cs | 40 +++ .../Selectors/LowestUserRatingSelector.cs | 40 +++ .../Filters/Selectors/SeriesCountSelector.cs | 52 ++++ .../SubtitleLanguageCountSelector.cs | 44 ++- .../Selectors/TotalEpisodeCountSelector.cs | 44 ++- .../Filters/Selectors/WatchedDateSelector.cs | 40 +++ .../User/HasPermanentUserVotesExpression.cs | 40 +++ .../User/HasUnwatchedEpisodesExpression.cs | 40 +++ .../Filters/User/HasUserVotesExpression.cs | 40 +++ .../User/HasWatchedEpisodesExpression.cs | 40 +++ .../Filters/User/IsFavoriteExpression.cs | 40 +++ .../MissingPermanentUserVotesExpression.cs | 40 +++ Shoko.Tests/Shoko.Tests/FilterTests.cs | 1 + 72 files changed, 3264 insertions(+), 81 deletions(-) create mode 100644 Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs create mode 100644 Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs create mode 100644 Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs create mode 100644 Shoko.Server/Filters/Files/HasVideoSourceExpression.cs create mode 100644 Shoko.Server/Filters/Info/HasNameExpression.cs delete mode 100644 Shoko.Server/Filters/Info/HasVideoSourceExpression.cs create mode 100644 Shoko.Server/Filters/LegacyFilterConverter.cs create mode 100644 Shoko.Server/Filters/LegacyMappings.cs create mode 100644 Shoko.Server/Filters/Selectors/SeriesCountSelector.cs diff --git a/Shoko.Server/API/v3/Models/Shoko/Filter.cs b/Shoko.Server/API/v3/Models/Shoko/Filter.cs index d6508ac2e..4de255353 100644 --- a/Shoko.Server/API/v3/Models/Shoko/Filter.cs +++ b/Shoko.Server/API/v3/Models/Shoko/Filter.cs @@ -89,8 +89,9 @@ public Filter(HttpContext ctx, FilterPreset groupFilter, bool fullModel = false) ApplyAtSeriesLevel = groupFilter.ApplyAtSeriesLevel; if (fullModel) { - Conditions = groupFilter.Conditions.Select(condition => new FilterCondition(condition)).ToList(); - Sorting = groupFilter.SortCriteriaList.Select(sort => new SortingCriteria(sort)).ToList(); + var legacyConverter = ctx.RequestServices.GetRequiredService(); + Conditions = legacyConverter.GetConditions(groupFilter).Select(condition => new FilterCondition(condition)).ToList(); + Sorting = legacyConverter.GetSortingCriteria(groupFilter).Select(sort => new SortingCriteria(sort)).ToList(); } var evaluator = ctx.RequestServices.GetRequiredService(); @@ -301,41 +302,31 @@ public CreateOrUpdateFilterBody(FilterPreset groupFilter) groupFilter.Name = Name; groupFilter.Hidden = IsHidden; groupFilter.ApplyAtSeriesLevel = ApplyAtSeriesLevel; - if (IsDirectory) - { - groupFilter.SortCriteriaList = new() - { - new GroupFilterSortingCriteria() - { - SortType = GroupFilterSorting.GroupFilterName, - SortDirection = GroupFilterSortDirection.Asc, - }, - }; - } - else + if (!IsDirectory) { if (Conditions != null) { groupFilter.Conditions = Conditions .Select(c => new GroupFilterCondition() { - ConditionOperator = (int)c.Operator, - ConditionParameter = c.Parameter, - ConditionType = (int)c.Type, + ConditionOperator = (int)c.Operator, ConditionParameter = c.Parameter, ConditionType = (int)c.Type, }) .ToList(); } + if (Sorting != null) { groupFilter.SortCriteriaList = Sorting .Select(s => new GroupFilterSortingCriteria { - SortType = s.Type, - SortDirection = s.IsInverted ? GroupFilterSortDirection.Desc : GroupFilterSortDirection.Asc + SortType = s.Type, SortDirection = s.IsInverted ? GroupFilterSortDirection.Desc : GroupFilterSortDirection.Asc }) .ToList(); } } + else + { + } // Skip saving if we're just going to preview a group filter. if (!skipSave) diff --git a/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs b/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs index 88f1648e0..c81abf350 100644 --- a/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs +++ b/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs @@ -1,3 +1,5 @@ +using System; + namespace Shoko.Server.Filters.Files; public class HasAudioLanguageExpression : FilterExpression @@ -16,4 +18,47 @@ public override bool Evaluate(Filterable filterable) { return filterable.AudioLanguages.Contains(Parameter); } + + protected bool Equals(HasAudioLanguageExpression other) + { + return base.Equals(other) && string.Equals(Parameter, other.Parameter, StringComparison.InvariantCultureIgnoreCase); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasAudioLanguageExpression)obj); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(base.GetHashCode()); + hashCode.Add(Parameter, StringComparer.InvariantCultureIgnoreCase); + return hashCode.ToHashCode(); + } + + public static bool operator ==(HasAudioLanguageExpression left, HasAudioLanguageExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasAudioLanguageExpression left, HasAudioLanguageExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs b/Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs new file mode 100644 index 000000000..760a95fe3 --- /dev/null +++ b/Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs @@ -0,0 +1,64 @@ +using System; + +namespace Shoko.Server.Filters.Files; + +public class HasSharedAudioLanguageExpression : FilterExpression +{ + public HasSharedAudioLanguageExpression(string parameter) + { + Parameter = parameter; + } + public HasSharedAudioLanguageExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(Filterable filterable) + { + return filterable.SharedAudioLanguages.Contains(Parameter); + } + + protected bool Equals(HasSharedAudioLanguageExpression other) + { + return base.Equals(other) && string.Equals(Parameter, other.Parameter, StringComparison.InvariantCultureIgnoreCase); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasSharedAudioLanguageExpression)obj); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(base.GetHashCode()); + hashCode.Add(Parameter, StringComparer.InvariantCultureIgnoreCase); + return hashCode.ToHashCode(); + } + + public static bool operator ==(HasSharedAudioLanguageExpression left, HasSharedAudioLanguageExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasSharedAudioLanguageExpression left, HasSharedAudioLanguageExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs b/Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs new file mode 100644 index 000000000..42ea0eaee --- /dev/null +++ b/Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs @@ -0,0 +1,64 @@ +using System; + +namespace Shoko.Server.Filters.Files; + +public class HasSharedSubtitleLanguageExpression : FilterExpression +{ + public HasSharedSubtitleLanguageExpression(string parameter) + { + Parameter = parameter; + } + public HasSharedSubtitleLanguageExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(Filterable filterable) + { + return filterable.SharedSubtitleLanguages.Contains(Parameter); + } + + protected bool Equals(HasSharedSubtitleLanguageExpression other) + { + return base.Equals(other) && string.Equals(Parameter, other.Parameter, StringComparison.InvariantCultureIgnoreCase); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasSharedSubtitleLanguageExpression)obj); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(base.GetHashCode()); + hashCode.Add(Parameter, StringComparer.InvariantCultureIgnoreCase); + return hashCode.ToHashCode(); + } + + public static bool operator ==(HasSharedSubtitleLanguageExpression left, HasSharedSubtitleLanguageExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasSharedSubtitleLanguageExpression left, HasSharedSubtitleLanguageExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs b/Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs new file mode 100644 index 000000000..fb3f81f87 --- /dev/null +++ b/Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs @@ -0,0 +1,61 @@ +using System; + +namespace Shoko.Server.Filters.Files; + +public class HasSharedVideoSourceExpression : FilterExpression +{ + public HasSharedVideoSourceExpression(string parameter) + { + Parameter = parameter; + } + public HasSharedVideoSourceExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(Filterable filterable) + { + return filterable.SharedVideoSources.Contains(Parameter); + } + + protected bool Equals(HasSharedVideoSourceExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasSharedVideoSourceExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasSharedVideoSourceExpression left, HasSharedVideoSourceExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasSharedVideoSourceExpression left, HasSharedVideoSourceExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs b/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs index 68d6a8d0a..0a0e7457d 100644 --- a/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs +++ b/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs @@ -1,3 +1,5 @@ +using System; + namespace Shoko.Server.Filters.Files; public class HasSubtitleLanguageExpression : FilterExpression @@ -16,4 +18,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.SubtitleLanguages.Contains(Parameter); } + + protected bool Equals(HasSubtitleLanguageExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasSubtitleLanguageExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasSubtitleLanguageExpression left, HasSubtitleLanguageExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasSubtitleLanguageExpression left, HasSubtitleLanguageExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Files/HasVideoSourceExpression.cs b/Shoko.Server/Filters/Files/HasVideoSourceExpression.cs new file mode 100644 index 000000000..a8a2041a3 --- /dev/null +++ b/Shoko.Server/Filters/Files/HasVideoSourceExpression.cs @@ -0,0 +1,61 @@ +using System; + +namespace Shoko.Server.Filters.Files; + +public class HasVideoSourceExpression : FilterExpression +{ + public HasVideoSourceExpression(string parameter) + { + Parameter = parameter; + } + public HasVideoSourceExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(Filterable filterable) + { + return filterable.VideoSources.Contains(Parameter); + } + + protected bool Equals(HasVideoSourceExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasVideoSourceExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasVideoSourceExpression left, HasVideoSourceExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasVideoSourceExpression left, HasVideoSourceExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/FilterExpression.cs b/Shoko.Server/Filters/FilterExpression.cs index 8b28c719f..85a84b537 100644 --- a/Shoko.Server/Filters/FilterExpression.cs +++ b/Shoko.Server/Filters/FilterExpression.cs @@ -8,6 +8,46 @@ public class FilterExpression : IFilterExpression public int FilterExpressionID { get; set; } [IgnoreDataMember] public virtual bool TimeDependent => false; [IgnoreDataMember] public virtual bool UserDependent => false; + + protected virtual bool Equals(FilterExpression other) + { + return true; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((FilterExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(FilterExpression left, FilterExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(FilterExpression left, FilterExpression right) + { + return !Equals(left, right); + } } public abstract class FilterExpression : FilterExpression, IFilterExpression diff --git a/Shoko.Server/Filters/FilterExtensions.cs b/Shoko.Server/Filters/FilterExtensions.cs index 448f731e4..5a68d7b02 100644 --- a/Shoko.Server/Filters/FilterExtensions.cs +++ b/Shoko.Server/Filters/FilterExtensions.cs @@ -54,9 +54,16 @@ public static Filterable ToFilterable(this SVR_AnimeSeries series) { ((AnimeType)anime.AnimeType).ToString() }, - VideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet(), + VideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality ?? new HashSet(), + SharedVideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet(), AudioLanguages = series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet(), - SubtitleLanguages = series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet() + SharedAudioLanguages = + series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Languages.Select(b => b.LanguageName)) + .Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(), + SubtitleLanguages = series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet(), + SharedSubtitleLanguages = + series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Subtitles.Select(b => b.LanguageName)) + .Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(), }; return filterable; @@ -103,9 +110,16 @@ public static UserDependentFilterable ToUserDependentFilterable(this SVR_AnimeSe { ((AnimeType)anime.AnimeType).ToString() }, - VideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet(), + VideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality ?? new HashSet(), + SharedVideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet(), AudioLanguages = series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet(), + SharedAudioLanguages = + series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Languages.Select(b => b.LanguageName)) + .Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(), SubtitleLanguages = series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet(), + SharedSubtitleLanguages = + series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Subtitles.Select(b => b.LanguageName)) + .Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(), IsFavorite = false, WatchedEpisodes = user?.WatchedCount ?? 0, UnwatchedEpisodes = (anime?.EpisodeCount ?? 0) - (user?.WatchedCount ?? 0), @@ -234,9 +248,18 @@ public static Filterable ToFilterable(this SVR_AnimeGroup group) HighestAniDBRating = group.Anime.DefaultIfEmpty().Max(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), AnimeTypes = new HashSet(group.Anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), - VideoSources = group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet(), + VideoSources = group.Contract?.Stat_AllVideoQuality ?? new HashSet(), + SharedVideoSources = group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet(), AudioLanguages = group.Contract?.Stat_AudioLanguages ?? new HashSet(), - SubtitleLanguages = group.Contract?.Stat_SubtitleLanguages ?? new HashSet() + SharedAudioLanguages = + series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Languages.Select(b => b.LanguageName)).Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) + .ToHashSet(), + SubtitleLanguages = group.Contract?.Stat_SubtitleLanguages ?? new HashSet(), + SharedSubtitleLanguages = + series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Subtitles.Select(b => b.LanguageName)).Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) + .ToHashSet(), }; return filterable; @@ -244,7 +267,7 @@ public static Filterable ToFilterable(this SVR_AnimeGroup group) public static Filterable ToUserDependentFilterable(this SVR_AnimeGroup group, int userID) { - var series = group.GetAllSeries(); + var series = group.GetAllSeries(true); var hasTrakt = series.All(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()); var user = group.GetUserRecord(userID); var vote = group.Anime.Select(a => a.UserVote).Where(a => a is { VoteType: (int)VoteType.AnimePermanent or (int)VoteType.AnimeTemporary }) @@ -289,9 +312,18 @@ public static Filterable ToUserDependentFilterable(this SVR_AnimeGroup group, in HighestAniDBRating = group.Anime.DefaultIfEmpty().Max(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), AnimeTypes = new HashSet(group.Anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), - VideoSources = group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet(), + VideoSources = group.Contract?.Stat_AllVideoQuality ?? new HashSet(), + SharedVideoSources = group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet(), AudioLanguages = group.Contract?.Stat_AudioLanguages ?? new HashSet(), + SharedAudioLanguages = + series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Languages.Select(b => b.LanguageName)).Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) + .ToHashSet(), SubtitleLanguages = group.Contract?.Stat_SubtitleLanguages ?? new HashSet(), + SharedSubtitleLanguages = + series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Subtitles.Select(b => b.LanguageName)).Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) + .ToHashSet(), IsFavorite = user?.IsFave == 1, WatchedEpisodes = user?.WatchedCount ?? 0, UnwatchedEpisodes = user?.UnwatchedEpisodeCount ?? 0, diff --git a/Shoko.Server/Filters/Filterable.cs b/Shoko.Server/Filters/Filterable.cs index 8dafbeecd..b0d2eef9e 100644 --- a/Shoko.Server/Filters/Filterable.cs +++ b/Shoko.Server/Filters/Filterable.cs @@ -7,115 +7,127 @@ namespace Shoko.Server.Filters; public class Filterable { /// - /// Name + /// Name /// public string Name { get; init; } /// - /// Sorting Name + /// Sorting Name /// public string SortingName { get; init; } /// - /// The number of series in a group + /// The number of series in a group /// public int SeriesCount { get; init; } /// - /// Number of Missing Episodes + /// Number of Missing Episodes /// public int MissingEpisodes { get; init; } /// - /// Number of Missing Episodes from Groups that you have + /// Number of Missing Episodes from Groups that you have /// public int MissingEpisodesCollecting { get; init; } /// - /// All of the tags + /// All of the tags /// public IReadOnlySet Tags { get; init; } /// - /// All of the custom tags + /// All of the custom tags /// public IReadOnlySet CustomTags { get; init; } /// - /// The years this aired in + /// The years this aired in /// public IReadOnlySet Years { get; init; } /// - /// The seasons this aired in + /// The seasons this aired in /// public IReadOnlySet<(int year, AnimeSeason season)> Seasons { get; init; } /// - /// Has at least one TvDB Link + /// Has at least one TvDB Link /// public bool HasTvDBLink { get; init; } /// - /// Missing at least one TvDB Link + /// Missing at least one TvDB Link /// public bool HasMissingTvDbLink { get; init; } /// - /// Has at least one TMDb Link + /// Has at least one TMDb Link /// public bool HasTMDbLink { get; init; } /// - /// Missing at least one TMDb Link + /// Missing at least one TMDb Link /// public bool HasMissingTMDbLink { get; init; } /// - /// Has at least one Trakt Link + /// Has at least one Trakt Link /// public bool HasTraktLink { get; init; } /// - /// Missing at least one Trakt Link + /// Missing at least one Trakt Link /// public bool HasMissingTraktLink { get; init; } /// - /// Has Finished airing + /// Has Finished airing /// public bool IsFinished { get; init; } /// - /// First Air Date + /// First Air Date /// public DateTime? AirDate { get; init; } /// - /// Latest Air Date + /// Latest Air Date /// public DateTime? LastAirDate { get; init; } /// - /// When it was first added to the collection + /// When it was first added to the collection /// public DateTime AddedDate { get; init; } /// - /// When it was most recently added to the collection + /// When it was most recently added to the collection /// public DateTime LastAddedDate { get; init; } /// - /// Highest Episode Count + /// Highest Episode Count /// public int EpisodeCount { get; init; } /// - /// Total Episode Count + /// Total Episode Count /// public int TotalEpisodeCount { get; init; } /// - /// Lowest AniDB Rating (0-10) + /// Lowest AniDB Rating (0-10) /// public decimal LowestAniDBRating { get; init; } /// - /// Highest AniDB Rating (0-10) + /// Highest AniDB Rating (0-10) /// public decimal HighestAniDBRating { get; init; } /// - /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. + /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. /// public IReadOnlySet VideoSources { get; init; } /// - /// The anime types (movie, series, ova, etc) + /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. (only sources that are in every file) + /// + public IReadOnlySet SharedVideoSources { get; init; } + /// + /// The anime types (movie, series, ova, etc) /// public IReadOnlySet AnimeTypes { get; init; } /// - /// Audio Languages + /// Audio Languages /// public IReadOnlySet AudioLanguages { get; init; } /// - /// Subtitle Languages + /// Audio Languages (only languages that are in every file) + /// + public IReadOnlySet SharedAudioLanguages { get; init; } + /// + /// Subtitle Languages /// public IReadOnlySet SubtitleLanguages { get; init; } + /// + /// Subtitle Languages (only languages that are in every file) + /// + public IReadOnlySet SharedSubtitleLanguages { get; init; } } diff --git a/Shoko.Server/Filters/Functions/DateAddFunction.cs b/Shoko.Server/Filters/Functions/DateAddFunction.cs index 4f2d9e498..30a02aae7 100644 --- a/Shoko.Server/Filters/Functions/DateAddFunction.cs +++ b/Shoko.Server/Filters/Functions/DateAddFunction.cs @@ -24,4 +24,44 @@ public DateAddFunction(FilterExpression selector, TimeSpan parameter) { return Selector.Evaluate(f) + Parameter; } + + protected bool Equals(DateAddFunction other) + { + return base.Equals(other) && Equals(Selector, other.Selector) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((DateAddFunction)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Selector, Parameter); + } + + public static bool operator ==(DateAddFunction left, DateAddFunction right) + { + return Equals(left, right); + } + + public static bool operator !=(DateAddFunction left, DateAddFunction right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Functions/DateDiffFunction.cs b/Shoko.Server/Filters/Functions/DateDiffFunction.cs index a1586f2db..2132a6161 100644 --- a/Shoko.Server/Filters/Functions/DateDiffFunction.cs +++ b/Shoko.Server/Filters/Functions/DateDiffFunction.cs @@ -21,4 +21,44 @@ public DateDiffFunction() { } { return Selector.Evaluate(f) - Parameter; } + + protected bool Equals(DateDiffFunction other) + { + return base.Equals(other) && Equals(Selector, other.Selector) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((DateDiffFunction)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Selector, Parameter); + } + + public static bool operator ==(DateDiffFunction left, DateDiffFunction right) + { + return Equals(left, right); + } + + public static bool operator !=(DateDiffFunction left, DateDiffFunction right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Functions/TodayFunction.cs b/Shoko.Server/Filters/Functions/TodayFunction.cs index 20e288b05..d31926140 100644 --- a/Shoko.Server/Filters/Functions/TodayFunction.cs +++ b/Shoko.Server/Filters/Functions/TodayFunction.cs @@ -11,4 +11,44 @@ public class TodayFunction : FilterExpression { return DateTime.Today; } + + protected bool Equals(TodayFunction other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((TodayFunction)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(TodayFunction left, TodayFunction right) + { + return Equals(left, right); + } + + public static bool operator !=(TodayFunction left, TodayFunction right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs b/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs index 22e1a2d1c..ed2733b1c 100644 --- a/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs +++ b/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs @@ -1,3 +1,5 @@ +using System; + namespace Shoko.Server.Filters.Info; public class HasAnimeTypeExpression : FilterExpression @@ -16,4 +18,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.AnimeTypes.Contains(Parameter); } + + protected bool Equals(HasAnimeTypeExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasAnimeTypeExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasAnimeTypeExpression left, HasAnimeTypeExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasAnimeTypeExpression left, HasAnimeTypeExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Info/HasCustomTagExpression.cs b/Shoko.Server/Filters/Info/HasCustomTagExpression.cs index ee3639c71..4a6bc9bd1 100644 --- a/Shoko.Server/Filters/Info/HasCustomTagExpression.cs +++ b/Shoko.Server/Filters/Info/HasCustomTagExpression.cs @@ -1,3 +1,5 @@ +using System; + namespace Shoko.Server.Filters.Info; public class HasCustomTagExpression : FilterExpression @@ -16,4 +18,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.CustomTags.Contains(Parameter); } + + protected bool Equals(HasCustomTagExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasCustomTagExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasCustomTagExpression left, HasCustomTagExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasCustomTagExpression left, HasCustomTagExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs b/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs index f6438de24..11b129dc3 100644 --- a/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs +++ b/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs @@ -9,4 +9,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.MissingEpisodesCollecting > 0; } + + protected bool Equals(HasMissingEpisodesCollectingExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasMissingEpisodesCollectingExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasMissingEpisodesCollectingExpression left, HasMissingEpisodesCollectingExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasMissingEpisodesCollectingExpression left, HasMissingEpisodesCollectingExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs b/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs index 7cec7bf81..01e723b46 100644 --- a/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs +++ b/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs @@ -9,4 +9,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.MissingEpisodes > 0; } + + protected bool Equals(HasMissingEpisodesExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasMissingEpisodesExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasMissingEpisodesExpression left, HasMissingEpisodesExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasMissingEpisodesExpression left, HasMissingEpisodesExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Info/HasNameExpression.cs b/Shoko.Server/Filters/Info/HasNameExpression.cs new file mode 100644 index 000000000..785e33258 --- /dev/null +++ b/Shoko.Server/Filters/Info/HasNameExpression.cs @@ -0,0 +1,61 @@ +using System; + +namespace Shoko.Server.Filters.Info; + +public class HasNameExpression : FilterExpression +{ + public HasNameExpression(string parameter) + { + Parameter = parameter; + } + public HasNameExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(Filterable filterable) + { + return filterable.Name.Equals(Parameter, StringComparison.InvariantCultureIgnoreCase); + } + + protected bool Equals(HasAnimeTypeExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasAnimeTypeExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasNameExpression left, HasNameExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasNameExpression left, HasNameExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs b/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs index 5a5043119..de9580fb0 100644 --- a/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs +++ b/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs @@ -9,4 +9,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.HasTMDbLink; } + + protected bool Equals(HasTMDbLinkExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasTMDbLinkExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasTMDbLinkExpression left, HasTMDbLinkExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasTMDbLinkExpression left, HasTMDbLinkExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Info/HasTagExpression.cs b/Shoko.Server/Filters/Info/HasTagExpression.cs index 2092443a8..d9a44e38c 100644 --- a/Shoko.Server/Filters/Info/HasTagExpression.cs +++ b/Shoko.Server/Filters/Info/HasTagExpression.cs @@ -1,3 +1,5 @@ +using System; + namespace Shoko.Server.Filters.Info; public class HasTagExpression : FilterExpression @@ -16,4 +18,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.Tags.Contains(Parameter); } + + protected bool Equals(HasTagExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasTagExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasTagExpression left, HasTagExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasTagExpression left, HasTagExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs b/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs index 86ea790c7..b912a8903 100644 --- a/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs +++ b/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs @@ -9,4 +9,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.HasTraktLink; } + + protected bool Equals(HasTraktLinkExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasTraktLinkExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasTraktLinkExpression left, HasTraktLinkExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasTraktLinkExpression left, HasTraktLinkExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs b/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs index 0eb17f73f..f3fbecbe1 100644 --- a/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs +++ b/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs @@ -9,4 +9,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.HasTvDBLink; } + + protected bool Equals(HasTvDBLinkExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasTvDBLinkExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasTvDBLinkExpression left, HasTvDBLinkExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasTvDBLinkExpression left, HasTvDBLinkExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Info/HasVideoSourceExpression.cs b/Shoko.Server/Filters/Info/HasVideoSourceExpression.cs deleted file mode 100644 index eb193a0b4..000000000 --- a/Shoko.Server/Filters/Info/HasVideoSourceExpression.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Shoko.Server.Filters.Info; - -public class HasVideoSourceExpression : FilterExpression -{ - public HasVideoSourceExpression(string parameter) - { - Parameter = parameter; - } - public HasVideoSourceExpression() { } - - public string Parameter { get; set; } - public override bool TimeDependent => false; - public override bool UserDependent => false; - - public override bool Evaluate(Filterable filterable) - { - return filterable.VideoSources.Contains(Parameter); - } -} diff --git a/Shoko.Server/Filters/Info/InSeasonExpression.cs b/Shoko.Server/Filters/Info/InSeasonExpression.cs index 91cd713c3..847116580 100644 --- a/Shoko.Server/Filters/Info/InSeasonExpression.cs +++ b/Shoko.Server/Filters/Info/InSeasonExpression.cs @@ -1,3 +1,4 @@ +using System; using Shoko.Models.Enums; namespace Shoko.Server.Filters.Info; @@ -20,4 +21,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.Seasons.Contains((Year, Season)); } + + protected bool Equals(InSeasonExpression other) + { + return base.Equals(other) && Year == other.Year && Season == other.Season; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((InSeasonExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Year, (int)Season); + } + + public static bool operator ==(InSeasonExpression left, InSeasonExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(InSeasonExpression left, InSeasonExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Info/InYearExpression.cs b/Shoko.Server/Filters/Info/InYearExpression.cs index ac8d41723..054917fd2 100644 --- a/Shoko.Server/Filters/Info/InYearExpression.cs +++ b/Shoko.Server/Filters/Info/InYearExpression.cs @@ -1,3 +1,5 @@ +using System; + namespace Shoko.Server.Filters.Info; public class InYearExpression : FilterExpression @@ -16,4 +18,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.Years.Contains(Parameter); } + + protected bool Equals(InYearExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((InYearExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(InYearExpression left, InYearExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(InYearExpression left, InYearExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Info/IsFinishedExpression.cs b/Shoko.Server/Filters/Info/IsFinishedExpression.cs index 906e79af4..60df07970 100644 --- a/Shoko.Server/Filters/Info/IsFinishedExpression.cs +++ b/Shoko.Server/Filters/Info/IsFinishedExpression.cs @@ -9,4 +9,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.IsFinished; } + + protected bool Equals(IsFinishedExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((IsFinishedExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(IsFinishedExpression left, IsFinishedExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(IsFinishedExpression left, IsFinishedExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs index adf6282e6..4b97b9e55 100644 --- a/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs +++ b/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs @@ -12,4 +12,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.HasMissingTMDbLink; } + + protected bool Equals(MissingTMDbLinkExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((MissingTMDbLinkExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(MissingTMDbLinkExpression left, MissingTMDbLinkExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(MissingTMDbLinkExpression left, MissingTMDbLinkExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs index 42ca5009f..c5d9a344a 100644 --- a/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs +++ b/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs @@ -12,4 +12,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.HasMissingTraktLink; } + + protected bool Equals(MissingTraktLinkExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((MissingTraktLinkExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(MissingTraktLinkExpression left, MissingTraktLinkExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(MissingTraktLinkExpression left, MissingTraktLinkExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs index 7e9f9185c..aa794eab1 100644 --- a/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs +++ b/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs @@ -12,4 +12,44 @@ public override bool Evaluate(Filterable filterable) { return filterable.HasMissingTvDbLink; } + + protected bool Equals(MissingTvDBLinkExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((MissingTvDBLinkExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(MissingTvDBLinkExpression left, MissingTvDBLinkExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(MissingTvDBLinkExpression left, MissingTvDBLinkExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/LegacyFilterConverter.cs b/Shoko.Server/Filters/LegacyFilterConverter.cs new file mode 100644 index 000000000..b9c86a8ed --- /dev/null +++ b/Shoko.Server/Filters/LegacyFilterConverter.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Shoko.Models.Enums; +using Shoko.Models.Server; +using Shoko.Server.Filters.Info; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Filters.User; +using Shoko.Server.Models; + +namespace Shoko.Server.Filters; + +public class LegacyFilterConverter +{ + public List GetConditions(Filter filter) + { + // TODO traverse the tree and replace with pre-set mappings + return new List(); + } + + public List GetSortingCriteria(Filter filter) + { + // TODO traverse the tree and replace with pre-set mappings + return new List + { + new() + { + GroupFilterID = filter.FilterID, SortType = GroupFilterSorting.SortName, SortDirection = GroupFilterSortDirection.Asc + } + }; + } + + public FilterExpression GetExpression(List conditions, bool suppressErrors = false) + { + // forward compatibility is easier. Just map the old conditions to an expression + if (conditions == null || conditions.Count < 1) return null; + var first = conditions.Select((a, index) => new {Expression=GetExpression(a, suppressErrors), Index=index}).FirstOrDefault(a => a.Expression != null); + if (first == null) return null; + return conditions.Count == 1 ? first.Expression : conditions.Skip(first.Index + 1).Aggregate(first.Expression, (a, b) => + { + var result = GetExpression(b, suppressErrors); + return result == null ? a : new AndExpression(a, result); + }); + } + + private FilterExpression GetExpression(GroupFilterCondition condition, bool suppressErrors = false) + { + var op = (GroupFilterOperator)condition.ConditionOperator; + var parameter = condition.ConditionParameter; + switch ((GroupFilterConditionType)condition.ConditionType) + { + case GroupFilterConditionType.CompletedSeries: + return new AndExpression( + new AndExpression(new NotExpression(new HasUnwatchedEpisodesExpression()), new NotExpression(new HasMissingEpisodesExpression())), + new IsFinishedExpression()); + case GroupFilterConditionType.MissingEpisodes: + return new HasMissingEpisodesExpression(); + case GroupFilterConditionType.MissingEpisodesCollecting: + return new HasMissingEpisodesCollectingExpression(); + case GroupFilterConditionType.HasUnwatchedEpisodes: + return new HasUnwatchedEpisodesExpression(); + case GroupFilterConditionType.HasWatchedEpisodes: + return new HasWatchedEpisodesExpression(); + case GroupFilterConditionType.UserVoted: + return new HasPermanentUserVotesExpression(); + case GroupFilterConditionType.UserVotedAny: + return new HasUserVotesExpression(); + case GroupFilterConditionType.Tag: + return LegacyMappings.GetTagExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.CustomTags: + return LegacyMappings.GetCustomTagExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AirDate: + return LegacyMappings.GetAirDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.LatestEpisodeAirDate: + return LegacyMappings.GetLastAirDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.SeriesCreatedDate: + return LegacyMappings.GetAddedDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.EpisodeAddedDate: + return LegacyMappings.GetLastAddedDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.EpisodeWatchedDate: + return LegacyMappings.GetWatchedDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AssignedTvDBInfo: + return new HasTvDBLinkExpression(); + case GroupFilterConditionType.AnimeType: + return LegacyMappings.GetAnimeTypeExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.VideoQuality: + return LegacyMappings.GetVideoQualityExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AudioLanguage: + return LegacyMappings.GetAudioLanguageExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.SubtitleLanguage: + return LegacyMappings.GetSubtitleLanguageExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.Favourite: + return new IsFavoriteExpression(); + case GroupFilterConditionType.AnimeGroup: + return LegacyMappings.GetGroupExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AniDBRating: + return LegacyMappings.GetRatingExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.UserRating: + return LegacyMappings.GetUserRatingExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.FinishedAiring: + return new IsFinishedExpression(); + case GroupFilterConditionType.AssignedTvDBOrMovieDBInfo: + return new OrExpression(new HasTvDBLinkExpression(), new HasTMDbLinkExpression()); + case GroupFilterConditionType.AssignedMovieDBInfo: + return new HasTMDbLinkExpression(); + case GroupFilterConditionType.AssignedMALInfo: + return suppressErrors ? null : throw new NotSupportedException("MAL is Deprecated"); + case GroupFilterConditionType.EpisodeCount: + return LegacyMappings.GetEpisodeCountExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.Year: + return LegacyMappings.GetYearExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.Season: + return LegacyMappings.GetSeasonExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AssignedTraktInfo: + return new HasTraktLinkExpression(); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(condition), $@"ConditionType {condition.ConditionType} is not valid"); + } + } + +} diff --git a/Shoko.Server/Filters/LegacyMappings.cs b/Shoko.Server/Filters/LegacyMappings.cs new file mode 100644 index 000000000..8544dfd3e --- /dev/null +++ b/Shoko.Server/Filters/LegacyMappings.cs @@ -0,0 +1,293 @@ +using System; +using Shoko.Models.Enums; +using Shoko.Server.Filters.Files; +using Shoko.Server.Filters.Functions; +using Shoko.Server.Filters.Info; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Filters.Logic.Numbers; +using Shoko.Server.Filters.Logic.DateTimes; +using Shoko.Server.Filters.Selectors; + +using DateGreaterEqual = Shoko.Server.Filters.Logic.DateTimes.GreaterThanEqualExpression; +using DateGreater = Shoko.Server.Filters.Logic.DateTimes.GreaterThanExpression; +using DateLess = Shoko.Server.Filters.Logic.DateTimes.LessThanExpression; +using NumberGreater = Shoko.Server.Filters.Logic.Numbers.GreaterThanExpression; +using NumberLess = Shoko.Server.Filters.Logic.Numbers.LessThanExpression; + +namespace Shoko.Server.Filters; + +public class LegacyMappings +{ + public static bool TryParseDate(string sDate, out DateTime result) + { + // yyyyMMdd or yyyy-MM-dd + result = DateTime.Today; + if (sDate.Length != 8 && sDate.Length != 10) return false; + if (!int.TryParse(sDate[..4], out var year)) return false; + int month; + int day; + if (sDate.Length == 8) + { + if (!int.TryParse(sDate[4..6], out month)) return false; + if (!int.TryParse(sDate[6..8], out day)) return false; + } + else + { + if (!int.TryParse(sDate[5..7], out month)) return false; + if (!int.TryParse(sDate[8..10], out day)) return false; + } + + result = new DateTime(year, month, day); + return true; + } + + public static FilterExpression GetTagExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + switch (op) + { + case GroupFilterOperator.Include: + case GroupFilterOperator.In: + return new HasTagExpression(parameter); + case GroupFilterOperator.Exclude: + case GroupFilterOperator.NotIn: + return new NotExpression(new HasTagExpression(parameter)); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), + $@"ConditionOperator {op} not applicable for Tags"); + } + } + + public static FilterExpression GetCustomTagExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + switch (op) + { + case GroupFilterOperator.Include: + case GroupFilterOperator.In: + return new HasCustomTagExpression(parameter); + case GroupFilterOperator.Exclude: + case GroupFilterOperator.NotIn: + return new NotExpression(new HasCustomTagExpression(parameter)); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), + $@"ConditionOperator {op} not applicable for Tags"); + } + } + + public static FilterExpression GetAirDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new AirDateSelector(), op, parameter, suppressErrors); + } + + public static FilterExpression GetLastAirDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new LastAirDateSelector(), op, parameter, suppressErrors); + } + + public static FilterExpression GetAddedDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new AddedDateSelector(), op, parameter, suppressErrors); + } + + public static FilterExpression GetLastAddedDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new LastAddedDateSelector(), op, parameter, suppressErrors); + } + + public static FilterExpression GetWatchedDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new LastWatchedDateSelector(), op, parameter, suppressErrors); + } + + private static FilterExpression GetDateExpression(FilterExpression selector, GroupFilterOperator op, string parameter, bool suppressErrors) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + switch (op) + { + case GroupFilterOperator.LastXDays: + { + if (!int.TryParse(parameter, out var lastX)) + return suppressErrors ? null : throw new ArgumentException(@"Parameter is not a number", nameof(parameter)); + return new DateGreaterEqual(selector, + new DateDiffFunction(new DateAddFunction(new TodayFunction(), TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1)), + TimeSpan.FromDays(lastX))); + } + case GroupFilterOperator.GreaterThan: + { + if (!TryParseDate(parameter, out var date)) + return suppressErrors + ? null + : throw new ArgumentException($@"Parameter {parameter} was not a date in format of yyyyMMdd", nameof(parameter)); + return new DateGreater(selector, date); + } + case GroupFilterOperator.LessThan: + { + if (!TryParseDate(parameter, out var date)) + return suppressErrors + ? null + : throw new ArgumentException($@"Parameter {parameter} was not a date in format of yyyyMMdd", nameof(parameter)); + return new DateGreater(selector, date); + } + default: + return suppressErrors + ? null + : throw new ArgumentOutOfRangeException(nameof(op), + $@"ConditionOperator {op} not applicable for Date filters"); + } + } + + public static FilterExpression GetVideoQualityExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + switch (op) + { + case GroupFilterOperator.In: + return new HasVideoSourceExpression(parameter); + case GroupFilterOperator.InAllEpisodes: + return new HasSharedVideoSourceExpression(parameter); + case GroupFilterOperator.NotIn: + return new NotExpression(new HasVideoSourceExpression(parameter)); + case GroupFilterOperator.NotInAllEpisodes: + return new NotExpression(new HasSharedVideoSourceExpression(parameter)); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Video Quality"); + } + } + + public static FilterExpression GetAudioLanguageExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + switch (op) + { + case GroupFilterOperator.In: + return new HasAudioLanguageExpression(parameter); + case GroupFilterOperator.NotIn: + return new NotExpression(new HasAudioLanguageExpression(parameter)); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Audio Languages"); + } + } + + public static FilterExpression GetSubtitleLanguageExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + switch (op) + { + case GroupFilterOperator.In: + return new HasSubtitleLanguageExpression(parameter); + case GroupFilterOperator.NotIn: + return new NotExpression(new HasSubtitleLanguageExpression(parameter)); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Subtitle Languages"); + } + } + + public static FilterExpression GetAnimeTypeExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + switch (op) + { + case GroupFilterOperator.In: + return new HasAnimeTypeExpression(parameter); + case GroupFilterOperator.NotIn: + return new NotExpression(new HasAnimeTypeExpression(parameter)); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Anime Type"); + } + } + + public static FilterExpression GetGroupExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + switch (op) + { + case GroupFilterOperator.In: + return new HasNameExpression(parameter); + case GroupFilterOperator.NotIn: + return new NotExpression(new HasNameExpression(parameter)); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Group Name"); + } + } + + public static FilterExpression GetRatingExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (!double.TryParse(parameter, out var rating)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + switch (op) + { + // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand + case GroupFilterOperator.GreaterThan: + return new NumberLess(new HighestAniDBRatingSelector(), rating); + case GroupFilterOperator.LessThan: + return new NumberGreater(new HighestAniDBRatingSelector(), rating); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Rating"); + } + } + + public static FilterExpression GetUserRatingExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (!double.TryParse(parameter, out var rating)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + switch (op) + { + // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand + case GroupFilterOperator.GreaterThan: + return new NumberLess(new HighestUserRatingSelector(), rating); + case GroupFilterOperator.LessThan: + return new NumberGreater(new HighestUserRatingSelector(), rating); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for User Rating"); + } + } + + public static FilterExpression GetEpisodeCountExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (!int.TryParse(parameter, out var count)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + switch (op) + { + // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand + case GroupFilterOperator.GreaterThan: + return new NumberLess(new EpisodeCountSelector(), count); + case GroupFilterOperator.LessThan: + return new NumberGreater(new HighestUserRatingSelector(), count); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Episode Count"); + } + } + + public static FilterExpression GetYearExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (!int.TryParse(parameter, out var year)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + switch (op) + { + case GroupFilterOperator.In: + case GroupFilterOperator.Include: + return new InYearExpression(year); + case GroupFilterOperator.NotIn: + case GroupFilterOperator.Exclude: + return new NotExpression(new InYearExpression(year)); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Years"); + } + } + + public static FilterExpression GetSeasonExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + var parts = parameter.Split(' '); + if (!int.TryParse(parts[1], out var year)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid year", nameof(parameter)); + if (!Enum.TryParse(parts[0], out var season)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid season", nameof(parameter)); + switch (op) + { + case GroupFilterOperator.In: + case GroupFilterOperator.Include: + return new InSeasonExpression(year, season); + case GroupFilterOperator.NotIn: + case GroupFilterOperator.Exclude: + return new NotExpression(new InSeasonExpression(year, season)); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Seasons"); + } + } +} diff --git a/Shoko.Server/Filters/Logic/AndExpression.cs b/Shoko.Server/Filters/Logic/AndExpression.cs index 6a3dd0536..9e52d9839 100644 --- a/Shoko.Server/Filters/Logic/AndExpression.cs +++ b/Shoko.Server/Filters/Logic/AndExpression.cs @@ -1,3 +1,5 @@ +using System; + namespace Shoko.Server.Filters.Logic; public class AndExpression : FilterExpression @@ -20,4 +22,44 @@ public override bool Evaluate(Filterable filterable) { return Left.Evaluate(filterable) && Right.Evaluate(filterable); } + + protected bool Equals(AndExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((AndExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right); + } + + public static bool operator ==(AndExpression left, AndExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(AndExpression left, AndExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/EqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/EqualExpression.cs index 13ce61645..403b92783 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/EqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/EqualExpression.cs @@ -45,4 +45,44 @@ public override bool Evaluate(Filterable filterable) return (date > operand ? date - operand : operand - date).Value.TotalDays < 1; } + + protected bool Equals(EqualExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((EqualExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(EqualExpression left, EqualExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(EqualExpression left, EqualExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs index 30f8dc70c..ffd46db64 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs @@ -45,4 +45,44 @@ public override bool Evaluate(Filterable filterable) return date >= operand; } + + protected bool Equals(GreaterThanEqualExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((GreaterThanEqualExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(GreaterThanEqualExpression left, GreaterThanEqualExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(GreaterThanEqualExpression left, GreaterThanEqualExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/GreaterThanExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/GreaterThanExpression.cs index 25db8312b..3960332e8 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/GreaterThanExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/GreaterThanExpression.cs @@ -45,4 +45,44 @@ public override bool Evaluate(Filterable filterable) return date > operand; } + + protected bool Equals(GreaterThanExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((GreaterThanExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(GreaterThanExpression left, GreaterThanExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(GreaterThanExpression left, GreaterThanExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/LessThanEqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/LessThanEqualExpression.cs index fdfdd7743..c7514ce91 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/LessThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/LessThanEqualExpression.cs @@ -38,4 +38,44 @@ public override bool Evaluate(Filterable filterable) return date <= operand; } + + protected bool Equals(LessThanEqualExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((LessThanEqualExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(LessThanEqualExpression left, LessThanEqualExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(LessThanEqualExpression left, LessThanEqualExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/LessThanExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/LessThanExpression.cs index a5d8d546f..646eef952 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/LessThanExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/LessThanExpression.cs @@ -45,4 +45,44 @@ public override bool Evaluate(Filterable filterable) return date < operand; } + + protected bool Equals(LessThanExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((LessThanExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(LessThanExpression left, LessThanExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(LessThanExpression left, LessThanExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/NotEqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/NotEqualExpression.cs index 2fcbd068e..bd73a496c 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/NotEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/NotEqualExpression.cs @@ -45,4 +45,44 @@ public override bool Evaluate(Filterable filterable) return (date > operand ? date - operand : operand - date).Value.TotalDays >= 1; } + + protected bool Equals(NotEqualExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((NotEqualExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(NotEqualExpression left, NotEqualExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(NotEqualExpression left, NotEqualExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/NotExpression.cs b/Shoko.Server/Filters/Logic/NotExpression.cs index c6ec290d8..b758f3be8 100644 --- a/Shoko.Server/Filters/Logic/NotExpression.cs +++ b/Shoko.Server/Filters/Logic/NotExpression.cs @@ -1,3 +1,5 @@ +using System; + namespace Shoko.Server.Filters.Logic; public class NotExpression : FilterExpression @@ -17,4 +19,44 @@ public override bool Evaluate(Filterable filterable) { return !Left.Evaluate(filterable); } + + protected bool Equals(NotExpression other) + { + return base.Equals(other) && Equals(Left, other.Left); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((NotExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left); + } + + public static bool operator ==(NotExpression left, NotExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(NotExpression left, NotExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/Numbers/EqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/EqualExpression.cs index 761b40fac..47ab9b706 100644 --- a/Shoko.Server/Filters/Logic/Numbers/EqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/EqualExpression.cs @@ -28,4 +28,44 @@ public override bool Evaluate(Filterable filterable) var right = Parameter ?? Right.Evaluate(filterable); return Math.Abs(left - right) < 0.001D; } + + protected bool Equals(EqualExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((EqualExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(EqualExpression left, EqualExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(EqualExpression left, EqualExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/Numbers/GreaterThanEqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/GreaterThanEqualExpression.cs index 483622ecc..eb6c8e545 100644 --- a/Shoko.Server/Filters/Logic/Numbers/GreaterThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/GreaterThanEqualExpression.cs @@ -28,4 +28,44 @@ public override bool Evaluate(Filterable filterable) var right = Parameter ?? Right.Evaluate(filterable); return Math.Abs(left - right) < 0.001D || left > right; } + + protected bool Equals(GreaterThanEqualExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((GreaterThanEqualExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(GreaterThanEqualExpression left, GreaterThanEqualExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(GreaterThanEqualExpression left, GreaterThanEqualExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/Numbers/GreaterThanExpression.cs b/Shoko.Server/Filters/Logic/Numbers/GreaterThanExpression.cs index a9c9685c2..efd836e86 100644 --- a/Shoko.Server/Filters/Logic/Numbers/GreaterThanExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/GreaterThanExpression.cs @@ -1,3 +1,5 @@ +using System; + namespace Shoko.Server.Filters.Logic.Numbers; public class GreaterThanExpression : FilterExpression @@ -26,4 +28,44 @@ public override bool Evaluate(Filterable filterable) var right = Parameter ?? Right.Evaluate(filterable); return left > right; } + + protected bool Equals(GreaterThanExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((GreaterThanExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(GreaterThanExpression left, GreaterThanExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(GreaterThanExpression left, GreaterThanExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/Numbers/LessThanEqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/LessThanEqualExpression.cs index c90f86bfb..e198fd7b8 100644 --- a/Shoko.Server/Filters/Logic/Numbers/LessThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/LessThanEqualExpression.cs @@ -28,4 +28,44 @@ public override bool Evaluate(Filterable filterable) var right = Parameter ?? Right.Evaluate(filterable); return Math.Abs(left - right) < 0.001D || left < right; } + + protected bool Equals(LessThanEqualExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((LessThanEqualExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(LessThanEqualExpression left, LessThanEqualExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(LessThanEqualExpression left, LessThanEqualExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/Numbers/LessThanExpression.cs b/Shoko.Server/Filters/Logic/Numbers/LessThanExpression.cs index 4524fb1a7..dfa234dfe 100644 --- a/Shoko.Server/Filters/Logic/Numbers/LessThanExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/LessThanExpression.cs @@ -1,3 +1,5 @@ +using System; + namespace Shoko.Server.Filters.Logic.Numbers; public class LessThanExpression : FilterExpression @@ -26,4 +28,44 @@ public override bool Evaluate(Filterable filterable) var right = Parameter ?? Right.Evaluate(filterable); return left < right; } + + protected bool Equals(LessThanExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((LessThanExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(LessThanExpression left, LessThanExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(LessThanExpression left, LessThanExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/Numbers/NotEqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NotEqualExpression.cs index 2d62f7d0d..81ba89cc5 100644 --- a/Shoko.Server/Filters/Logic/Numbers/NotEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NotEqualExpression.cs @@ -28,4 +28,44 @@ public override bool Evaluate(Filterable filterable) var right = Parameter ?? Right.Evaluate(filterable); return Math.Abs(left - right) >= 0.001D; } + + protected bool Equals(NotEqualExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((NotEqualExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(NotEqualExpression left, NotEqualExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(NotEqualExpression left, NotEqualExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/OrExpression.cs b/Shoko.Server/Filters/Logic/OrExpression.cs index 6464ca8e7..257d7bc7c 100644 --- a/Shoko.Server/Filters/Logic/OrExpression.cs +++ b/Shoko.Server/Filters/Logic/OrExpression.cs @@ -1,3 +1,5 @@ +using System; + namespace Shoko.Server.Filters.Logic; public class OrExpression : FilterExpression @@ -20,4 +22,44 @@ public override bool Evaluate(Filterable filterable) { return Left.Evaluate(filterable) || Right.Evaluate(filterable); } + + protected bool Equals(OrExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((OrExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right); + } + + public static bool operator ==(OrExpression left, OrExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(OrExpression left, OrExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/Strings/ContainsExpression.cs b/Shoko.Server/Filters/Logic/Strings/ContainsExpression.cs index 2d60ded64..bee1a721c 100644 --- a/Shoko.Server/Filters/Logic/Strings/ContainsExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/ContainsExpression.cs @@ -35,4 +35,44 @@ public override bool Evaluate(Filterable filterable) return left.Contains(right, StringComparison.InvariantCultureIgnoreCase); } + + protected bool Equals(ContainsExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((ContainsExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(ContainsExpression left, ContainsExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(ContainsExpression left, ContainsExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/Strings/EqualExpression.cs b/Shoko.Server/Filters/Logic/Strings/EqualExpression.cs index fae2c6806..23d4cd910 100644 --- a/Shoko.Server/Filters/Logic/Strings/EqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/EqualExpression.cs @@ -28,4 +28,44 @@ public override bool Evaluate(Filterable filterable) var right = Parameter ?? Right?.Evaluate(filterable); return string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase); } + + protected bool Equals(EqualExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((EqualExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(EqualExpression left, EqualExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(EqualExpression left, EqualExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/Strings/NotEqualExpression.cs b/Shoko.Server/Filters/Logic/Strings/NotEqualExpression.cs index 015393b79..91926a343 100644 --- a/Shoko.Server/Filters/Logic/Strings/NotEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/NotEqualExpression.cs @@ -28,4 +28,44 @@ public override bool Evaluate(Filterable filterable) var right = Parameter ?? Right?.Evaluate(filterable); return !string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase); } + + protected bool Equals(NotEqualExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((NotEqualExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(NotEqualExpression left, NotEqualExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(NotEqualExpression left, NotEqualExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Logic/XorExpression.cs b/Shoko.Server/Filters/Logic/XorExpression.cs index cac1bc133..69889f240 100644 --- a/Shoko.Server/Filters/Logic/XorExpression.cs +++ b/Shoko.Server/Filters/Logic/XorExpression.cs @@ -1,3 +1,5 @@ +using System; + namespace Shoko.Server.Filters.Logic; public class XorExpression : FilterExpression @@ -20,4 +22,44 @@ public override bool Evaluate(Filterable filterable) { return Left.Evaluate(filterable) ^ Right.Evaluate(filterable); } + + protected bool Equals(XorExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((XorExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right); + } + + public static bool operator ==(XorExpression left, XorExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(XorExpression left, XorExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Selectors/AddedDateSelector.cs b/Shoko.Server/Filters/Selectors/AddedDateSelector.cs index 769f24710..5024f9323 100644 --- a/Shoko.Server/Filters/Selectors/AddedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/AddedDateSelector.cs @@ -11,4 +11,44 @@ public class AddedDateSelector : FilterExpression { return f.AddedDate; } + + protected bool Equals(AddedDateSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((AddedDateSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(AddedDateSelector left, AddedDateSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(AddedDateSelector left, AddedDateSelector right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Selectors/AirDateSelector.cs b/Shoko.Server/Filters/Selectors/AirDateSelector.cs index fc67d4406..9f08e5d79 100644 --- a/Shoko.Server/Filters/Selectors/AirDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/AirDateSelector.cs @@ -11,4 +11,44 @@ public class AirDateSelector : FilterExpression { return f.AirDate; } + + protected bool Equals(AirDateSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((AirDateSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(AirDateSelector left, AirDateSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(AirDateSelector left, AirDateSelector right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs b/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs index 6b252b4a9..f40eb07c6 100644 --- a/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs @@ -1,12 +1,52 @@ namespace Shoko.Server.Filters.Selectors; -public class AudioLanguageCountSelector : FilterExpression +public class AudioLanguageCountSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override int Evaluate(Filterable f) + public override double Evaluate(Filterable f) { return f.AudioLanguages.Count; } + + protected bool Equals(AudioLanguageCountSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((AudioLanguageCountSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(AudioLanguageCountSelector left, AudioLanguageCountSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(AudioLanguageCountSelector left, AudioLanguageCountSelector right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs b/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs index 828d4181a..15eadfc94 100644 --- a/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs @@ -1,12 +1,52 @@ namespace Shoko.Server.Filters.Selectors; -public class EpisodeCountSelector : FilterExpression +public class EpisodeCountSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override int Evaluate(Filterable f) + public override double Evaluate(Filterable f) { return f.EpisodeCount; } + + protected bool Equals(EpisodeCountSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((EpisodeCountSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(EpisodeCountSelector left, EpisodeCountSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(EpisodeCountSelector left, EpisodeCountSelector right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs b/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs index 1b2bc3861..fc39fe9c8 100644 --- a/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs @@ -11,4 +11,44 @@ public override double Evaluate(Filterable f) { return Convert.ToDouble(f.HighestAniDBRating); } + + protected bool Equals(HighestAniDBRatingSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HighestAniDBRatingSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HighestAniDBRatingSelector left, HighestAniDBRatingSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(HighestAniDBRatingSelector left, HighestAniDBRatingSelector right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs b/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs index 260ac190a..aead73c55 100644 --- a/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs @@ -11,4 +11,44 @@ public override double Evaluate(UserDependentFilterable f) { return Convert.ToDouble(f.HighestUserRating); } + + protected bool Equals(HighestUserRatingSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HighestUserRatingSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HighestUserRatingSelector left, HighestUserRatingSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(HighestUserRatingSelector left, HighestUserRatingSelector right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs b/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs index 3db1eaa8e..2fb1fdf46 100644 --- a/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs @@ -11,4 +11,44 @@ public class LastAddedDateSelector : FilterExpression { return f.LastAddedDate; } + + protected bool Equals(LastAddedDateSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((LastAddedDateSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(LastAddedDateSelector left, LastAddedDateSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(LastAddedDateSelector left, LastAddedDateSelector right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs b/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs index efe67636c..7ca6c358b 100644 --- a/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs @@ -11,4 +11,44 @@ public class LastAirDateSelector : FilterExpression { return f.LastAirDate; } + + protected bool Equals(LastAirDateSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((LastAirDateSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(LastAirDateSelector left, LastAirDateSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(LastAirDateSelector left, LastAirDateSelector right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs b/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs index 0c65bc7cc..7b5dc3c5d 100644 --- a/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs @@ -11,4 +11,44 @@ public class LastWatchedDateSelector : UserDependentFilterExpression { return f.LastWatchedDate; } + + protected bool Equals(LastWatchedDateSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((LastWatchedDateSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(LastWatchedDateSelector left, LastWatchedDateSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(LastWatchedDateSelector left, LastWatchedDateSelector right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs b/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs index cb80b3569..55a5ff498 100644 --- a/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs @@ -11,4 +11,44 @@ public override double Evaluate(Filterable f) { return Convert.ToDouble(f.LowestAniDBRating); } + + protected bool Equals(LowestAniDBRatingSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((LowestAniDBRatingSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(LowestAniDBRatingSelector left, LowestAniDBRatingSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(LowestAniDBRatingSelector left, LowestAniDBRatingSelector right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs b/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs index 5640cead1..407c78057 100644 --- a/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs @@ -11,4 +11,44 @@ public override double Evaluate(UserDependentFilterable f) { return Convert.ToDouble(f.LowestUserRating); } + + protected bool Equals(LowestUserRatingSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((LowestUserRatingSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(LowestUserRatingSelector left, LowestUserRatingSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(LowestUserRatingSelector left, LowestUserRatingSelector right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Selectors/SeriesCountSelector.cs b/Shoko.Server/Filters/Selectors/SeriesCountSelector.cs new file mode 100644 index 000000000..e136338ca --- /dev/null +++ b/Shoko.Server/Filters/Selectors/SeriesCountSelector.cs @@ -0,0 +1,52 @@ +namespace Shoko.Server.Filters.Selectors; + +public class SeriesCountSelector : FilterExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override double Evaluate(Filterable f) + { + return f.SeriesCount; + } + + protected bool Equals(SeriesCountSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((SeriesCountSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(SeriesCountSelector left, SeriesCountSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(SeriesCountSelector left, SeriesCountSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs b/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs index 59212b4bf..929d3937f 100644 --- a/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs @@ -1,12 +1,52 @@ namespace Shoko.Server.Filters.Selectors; -public class SubtitleLanguageCountSelector : FilterExpression +public class SubtitleLanguageCountSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override int Evaluate(Filterable f) + public override double Evaluate(Filterable f) { return f.SubtitleLanguages.Count; } + + protected bool Equals(SubtitleLanguageCountSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((SubtitleLanguageCountSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(SubtitleLanguageCountSelector left, SubtitleLanguageCountSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(SubtitleLanguageCountSelector left, SubtitleLanguageCountSelector right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs b/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs index cb961d6a7..c9ce31e67 100644 --- a/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs @@ -1,12 +1,52 @@ namespace Shoko.Server.Filters.Selectors; -public class TotalEpisodeCountSelector : FilterExpression +public class TotalEpisodeCountSelector : FilterExpression { public override bool TimeDependent => false; public override bool UserDependent => false; - public override int Evaluate(Filterable f) + public override double Evaluate(Filterable f) { return f.TotalEpisodeCount; } + + protected bool Equals(TotalEpisodeCountSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((TotalEpisodeCountSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(TotalEpisodeCountSelector left, TotalEpisodeCountSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(TotalEpisodeCountSelector left, TotalEpisodeCountSelector right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs b/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs index 7bfc7c91d..ec3aa7c9e 100644 --- a/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs @@ -11,4 +11,44 @@ public class WatchedDateSelector : UserDependentFilterExpression { return f.WatchedDate; } + + protected bool Equals(WatchedDateSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((WatchedDateSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(WatchedDateSelector left, WatchedDateSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(WatchedDateSelector left, WatchedDateSelector right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs b/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs index 26b4ed935..65d0bb58a 100644 --- a/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs +++ b/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs @@ -9,4 +9,44 @@ public override bool Evaluate(UserDependentFilterable filterable) { return filterable.HasPermanentVotes; } + + protected bool Equals(HasPermanentUserVotesExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasPermanentUserVotesExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasPermanentUserVotesExpression left, HasPermanentUserVotesExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasPermanentUserVotesExpression left, HasPermanentUserVotesExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs b/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs index 3a25cb67f..8cc24c350 100644 --- a/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs +++ b/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs @@ -9,4 +9,44 @@ public override bool Evaluate(UserDependentFilterable filterable) { return filterable.UnwatchedEpisodes > 0; } + + protected bool Equals(HasUnwatchedEpisodesExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasUnwatchedEpisodesExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasUnwatchedEpisodesExpression left, HasUnwatchedEpisodesExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasUnwatchedEpisodesExpression left, HasUnwatchedEpisodesExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/User/HasUserVotesExpression.cs b/Shoko.Server/Filters/User/HasUserVotesExpression.cs index d4ea9cb8a..f2e99db19 100644 --- a/Shoko.Server/Filters/User/HasUserVotesExpression.cs +++ b/Shoko.Server/Filters/User/HasUserVotesExpression.cs @@ -9,4 +9,44 @@ public override bool Evaluate(UserDependentFilterable filterable) { return filterable.HasVotes; } + + protected bool Equals(HasUserVotesExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasUserVotesExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasUserVotesExpression left, HasUserVotesExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasUserVotesExpression left, HasUserVotesExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs b/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs index f38d05452..95b63c644 100644 --- a/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs +++ b/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs @@ -9,4 +9,44 @@ public override bool Evaluate(UserDependentFilterable filterable) { return filterable.WatchedEpisodes > 0; } + + protected bool Equals(HasWatchedEpisodesExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasWatchedEpisodesExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasWatchedEpisodesExpression left, HasWatchedEpisodesExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasWatchedEpisodesExpression left, HasWatchedEpisodesExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/User/IsFavoriteExpression.cs b/Shoko.Server/Filters/User/IsFavoriteExpression.cs index d58e52c54..36ac614d9 100644 --- a/Shoko.Server/Filters/User/IsFavoriteExpression.cs +++ b/Shoko.Server/Filters/User/IsFavoriteExpression.cs @@ -9,4 +9,44 @@ public override bool Evaluate(UserDependentFilterable filterable) { return filterable.IsFavorite; } + + protected bool Equals(IsFavoriteExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((IsFavoriteExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(IsFavoriteExpression left, IsFavoriteExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(IsFavoriteExpression left, IsFavoriteExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs b/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs index f58200c4d..677dfb972 100644 --- a/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs +++ b/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs @@ -9,4 +9,44 @@ public override bool Evaluate(UserDependentFilterable filterable) { return filterable.MissingPermanentVotes; } + + protected bool Equals(MissingPermanentUserVotesExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((MissingPermanentUserVotesExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(MissingPermanentUserVotesExpression left, MissingPermanentUserVotesExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(MissingPermanentUserVotesExpression left, MissingPermanentUserVotesExpression right) + { + return !Equals(left, right); + } } diff --git a/Shoko.Tests/Shoko.Tests/FilterTests.cs b/Shoko.Tests/Shoko.Tests/FilterTests.cs index ed4727a77..5a79366e5 100644 --- a/Shoko.Tests/Shoko.Tests/FilterTests.cs +++ b/Shoko.Tests/Shoko.Tests/FilterTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Newtonsoft.Json; using Shoko.Server.Filters; +using Shoko.Server.Filters.Files; using Shoko.Server.Filters.Functions; using Shoko.Server.Filters.Info; using Shoko.Server.Filters.Logic; From 68a9dc0277d68f3e64bcc91c89eb0fa92df3599f Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Fri, 15 Sep 2023 16:45:20 -0400 Subject: [PATCH 16/34] Filter API. Not done. Finally Builds again --- Shoko.CLI/Program.cs | 4 +- Shoko.Server/API/APIExtensions.cs | 4 + .../API/v3/Controllers/ActionController.cs | 5 +- .../API/v3/Controllers/DashboardController.cs | 10 +- .../API/v3/Controllers/FilterController.cs | 204 ++-- .../v3/Controllers/ReverseTreeController.cs | 22 +- .../API/v3/Controllers/SeriesController.cs | 80 +- .../API/v3/Controllers/TreeController.cs | 136 +-- .../API/v3/Controllers/WebUIController.cs | 9 +- .../v3/Helpers/APIGroupFilterSortingHelper.cs | 128 --- Shoko.Server/API/v3/Helpers/FilterFactory.cs | 256 ++++++ Shoko.Server/API/v3/Helpers/ModelHelper.cs | 2 +- Shoko.Server/API/v3/Helpers/SeriesFactory.cs | 868 ++++++++++++++++++ Shoko.Server/API/v3/Helpers/WebUIFactory.cs | 70 ++ Shoko.Server/API/v3/Models/Shoko/Dashboard.cs | 5 +- Shoko.Server/API/v3/Models/Shoko/Filter.cs | 246 +---- Shoko.Server/API/v3/Models/Shoko/Group.cs | 6 +- Shoko.Server/API/v3/Models/Shoko/Series.cs | 845 +---------------- Shoko.Server/API/v3/Models/Shoko/WebUI.cs | 70 -- .../CommandRequest_RefreshGroupFilter.cs | 2 +- Shoko.Server/Databases/BaseDatabase.cs | 4 +- Shoko.Server/Databases/MySQL.cs | 4 +- Shoko.Server/Databases/SQLServer.cs | 4 +- Shoko.Server/Databases/SQLite.cs | 4 +- .../Files/HasAudioLanguageExpression.cs | 3 +- .../Files/HasSharedAudioLanguageExpression.cs | 3 +- .../HasSharedSubtitleLanguageExpression.cs | 3 +- .../Files/HasSharedVideoSourceExpression.cs | 3 +- .../Files/HasSubtitleLanguageExpression.cs | 3 +- .../Filters/Files/HasVideoSourceExpression.cs | 3 +- Shoko.Server/Filters/FilterEvaluator.cs | 8 +- Shoko.Server/Filters/FilterExtensions.cs | 2 +- .../Filters/Functions/DateAddFunction.cs | 9 +- .../Filters/Functions/DateDiffFunction.cs | 9 +- .../Filters/Info/HasAnimeTypeExpression.cs | 3 +- .../Filters/Info/HasCustomTagExpression.cs | 3 +- .../Filters/Info/HasNameExpression.cs | 3 +- Shoko.Server/Filters/Info/HasTagExpression.cs | 3 +- .../Filters/Info/InSeasonExpression.cs | 15 +- Shoko.Server/Filters/Info/InYearExpression.cs | 9 +- .../Filters/Interfaces/IWithDateParameter.cs | 8 + .../Interfaces/IWithDateSelectorParameter.cs | 8 + .../Interfaces/IWithExpressionParameter.cs | 6 + .../Interfaces/IWithNumberParameter.cs | 6 + .../IWithNumberSelectorParameter.cs | 6 + .../IWithSecondDateSelectorParameter.cs | 8 + .../IWithSecondExpressionParameter.cs | 6 + .../IWithSecondNumberSelectorParameter.cs | 6 + .../Interfaces/IWithSecondStringParameter.cs | 6 + .../IWithSecondStringSelectorParameter.cs | 6 + .../Interfaces/IWithStringParameter.cs | 6 + .../IWithStringSelectorParameter.cs | 6 + .../Interfaces/IWithTimeSpanParameter.cs | 8 + Shoko.Server/Filters/LegacyFilterConverter.cs | 7 +- Shoko.Server/Filters/LegacyMappings.cs | 24 +- Shoko.Server/Filters/Logic/AndExpression.cs | 3 +- ...lExpression.cs => DateEqualsExpression.cs} | 17 +- ....cs => DateGreaterThanEqualsExpression.cs} | 17 +- ...ession.cs => DateGreaterThanExpression.cs} | 17 +- ...ion.cs => DateLessThanEqualsExpression.cs} | 17 +- ...xpression.cs => DateLessThanExpression.cs} | 17 +- ...pression.cs => DateNotEqualsExpression.cs} | 17 +- Shoko.Server/Filters/Logic/NotExpression.cs | 3 +- ...xpression.cs => NumberEqualsExpression.cs} | 17 +- ...s => NumberGreaterThanEqualsExpression.cs} | 17 +- ...sion.cs => NumberGreaterThanExpression.cs} | 17 +- ...n.cs => NumberLessThanEqualsExpression.cs} | 17 +- ...ression.cs => NumberLessThanExpression.cs} | 17 +- ...ession.cs => NumberNotEqualsExpression.cs} | 17 +- Shoko.Server/Filters/Logic/OrExpression.cs | 3 +- ...ression.cs => StringContainsExpression.cs} | 17 +- ...xpression.cs => StringEqualsExpression.cs} | 17 +- ...ession.cs => StringNotEqualsExpression.cs} | 17 +- Shoko.Server/Filters/Logic/XorExpression.cs | 3 +- .../{FilterMap.cs => FilterPresetMap.cs} | 10 +- .../Models/{Filter.cs => FilterPreset.cs} | 6 +- .../Cached/AnimeGroupRepository.cs | 2 +- .../Cached/AnimeSeriesRepository.cs | 2 +- ...epository.cs => FilterPresetRepository.cs} | 90 +- Shoko.Server/Repositories/RepoFactory.cs | 2 +- Shoko.Server/Server/Startup.cs | 1 + Shoko.Server/Tasks/AnimeGroupCreator.cs | 2 +- Shoko.Tests/Shoko.Tests/FilterTests.cs | 8 +- 83 files changed, 1901 insertions(+), 1676 deletions(-) delete mode 100644 Shoko.Server/API/v3/Helpers/APIGroupFilterSortingHelper.cs create mode 100644 Shoko.Server/API/v3/Helpers/FilterFactory.cs create mode 100644 Shoko.Server/API/v3/Helpers/SeriesFactory.cs create mode 100644 Shoko.Server/API/v3/Helpers/WebUIFactory.cs create mode 100644 Shoko.Server/Filters/Interfaces/IWithDateParameter.cs create mode 100644 Shoko.Server/Filters/Interfaces/IWithDateSelectorParameter.cs create mode 100644 Shoko.Server/Filters/Interfaces/IWithExpressionParameter.cs create mode 100644 Shoko.Server/Filters/Interfaces/IWithNumberParameter.cs create mode 100644 Shoko.Server/Filters/Interfaces/IWithNumberSelectorParameter.cs create mode 100644 Shoko.Server/Filters/Interfaces/IWithSecondDateSelectorParameter.cs create mode 100644 Shoko.Server/Filters/Interfaces/IWithSecondExpressionParameter.cs create mode 100644 Shoko.Server/Filters/Interfaces/IWithSecondNumberSelectorParameter.cs create mode 100644 Shoko.Server/Filters/Interfaces/IWithSecondStringParameter.cs create mode 100644 Shoko.Server/Filters/Interfaces/IWithSecondStringSelectorParameter.cs create mode 100644 Shoko.Server/Filters/Interfaces/IWithStringParameter.cs create mode 100644 Shoko.Server/Filters/Interfaces/IWithStringSelectorParameter.cs create mode 100644 Shoko.Server/Filters/Interfaces/IWithTimeSpanParameter.cs rename Shoko.Server/Filters/Logic/DateTimes/{EqualExpression.cs => DateEqualsExpression.cs} (74%) rename Shoko.Server/Filters/Logic/DateTimes/{GreaterThanEqualExpression.cs => DateGreaterThanEqualsExpression.cs} (71%) rename Shoko.Server/Filters/Logic/DateTimes/{GreaterThanExpression.cs => DateGreaterThanExpression.cs} (73%) rename Shoko.Server/Filters/Logic/DateTimes/{LessThanEqualExpression.cs => DateLessThanEqualsExpression.cs} (70%) rename Shoko.Server/Filters/Logic/DateTimes/{LessThanExpression.cs => DateLessThanExpression.cs} (73%) rename Shoko.Server/Filters/Logic/DateTimes/{NotEqualExpression.cs => DateNotEqualsExpression.cs} (74%) rename Shoko.Server/Filters/Logic/Numbers/{EqualExpression.cs => NumberEqualsExpression.cs} (67%) rename Shoko.Server/Filters/Logic/Numbers/{GreaterThanEqualExpression.cs => NumberGreaterThanEqualsExpression.cs} (64%) rename Shoko.Server/Filters/Logic/Numbers/{GreaterThanExpression.cs => NumberGreaterThanExpression.cs} (65%) rename Shoko.Server/Filters/Logic/Numbers/{LessThanEqualExpression.cs => NumberLessThanEqualsExpression.cs} (65%) rename Shoko.Server/Filters/Logic/Numbers/{LessThanExpression.cs => NumberLessThanExpression.cs} (66%) rename Shoko.Server/Filters/Logic/Numbers/{NotEqualExpression.cs => NumberNotEqualsExpression.cs} (66%) rename Shoko.Server/Filters/Logic/Strings/{ContainsExpression.cs => StringContainsExpression.cs} (68%) rename Shoko.Server/Filters/Logic/Strings/{EqualExpression.cs => StringEqualsExpression.cs} (67%) rename Shoko.Server/Filters/Logic/Strings/{NotEqualExpression.cs => StringNotEqualsExpression.cs} (66%) rename Shoko.Server/Mappings/{FilterMap.cs => FilterPresetMap.cs} (78%) rename Shoko.Server/Models/{Filter.cs => FilterPreset.cs} (79%) rename Shoko.Server/Repositories/Cached/{FilterRepository.cs => FilterPresetRepository.cs} (85%) diff --git a/Shoko.CLI/Program.cs b/Shoko.CLI/Program.cs index d19fec2ca..f5d95e65b 100644 --- a/Shoko.CLI/Program.cs +++ b/Shoko.CLI/Program.cs @@ -57,10 +57,10 @@ private static void OnShokoServerOnDBSetupCompleted(object? o, EventArgs eventAr var filterEvaluator = Utils.ServiceContainer.GetRequiredService(); var s = Stopwatch.StartNew(); - RepoFactory.Filter.CreateOrVerifyDirectoryFilters(); + RepoFactory.FilterPreset.CreateOrVerifyDirectoryFilters(); s.Stop(); _logger.LogInformation("Generating Directories took {Time}ms", s.ElapsedMilliseconds); - var comedyFilter = RepoFactory.Filter.GetAll().FirstOrDefault(a => a.Name.Equals("comedy", StringComparison.InvariantCultureIgnoreCase)); + var comedyFilter = RepoFactory.FilterPreset.GetAll().FirstOrDefault(a => a.Name.Equals("comedy", StringComparison.InvariantCultureIgnoreCase)); if (comedyFilter != null) { s.Restart(); diff --git a/Shoko.Server/API/APIExtensions.cs b/Shoko.Server/API/APIExtensions.cs index 283f0c3ea..05a7c7095 100644 --- a/Shoko.Server/API/APIExtensions.cs +++ b/Shoko.Server/API/APIExtensions.cs @@ -21,6 +21,7 @@ using Shoko.Server.API.SignalR.Aggregate; using Shoko.Server.API.SignalR.Legacy; using Shoko.Server.API.Swagger; +using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Shoko; using Shoko.Server.API.WebUI; using Shoko.Server.Plugin; @@ -50,6 +51,9 @@ public static IServiceCollection AddAPI(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddAuthentication(options => { diff --git a/Shoko.Server/API/v3/Controllers/ActionController.cs b/Shoko.Server/API/v3/Controllers/ActionController.cs index c887aba41..ee5a6c98c 100644 --- a/Shoko.Server/API/v3/Controllers/ActionController.cs +++ b/Shoko.Server/API/v3/Controllers/ActionController.cs @@ -9,6 +9,7 @@ using Quartz; using QuartzJobFactory; using Shoko.Server.API.Annotations; +using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Shoko; using Shoko.Server.Commands; using Shoko.Server.Commands.AniDB; @@ -257,7 +258,7 @@ public ActionResult UpdateMissingAniDBXML() if (rawXml != null) continue; - Series.QueueAniDBRefresh(_commandFactory, _httpHandler, animeID, true, false, false); + SeriesFactory.QueueAniDBRefresh(_commandFactory, _httpHandler, animeID, true, false, false); queuedAnimeSet.Add(animeID); } @@ -277,7 +278,7 @@ public ActionResult UpdateMissingAniDBXML() if (++index % 10 == 1) _logger.LogInformation("Queueing {MissingAnimeCount} anime that needs an update — {CurrentCount}/{MissingAnimeCount}", missingAnimeSet.Count, index + 1, missingAnimeSet.Count); - Series.QueueAniDBRefresh(_commandFactory, _httpHandler, animeID, false, true, true); + SeriesFactory.QueueAniDBRefresh(_commandFactory, _httpHandler, animeID, false, true, true); queuedAnimeSet.Add(animeID); } diff --git a/Shoko.Server/API/v3/Controllers/DashboardController.cs b/Shoko.Server/API/v3/Controllers/DashboardController.cs index d77b20535..ee20ea8e2 100644 --- a/Shoko.Server/API/v3/Controllers/DashboardController.cs +++ b/Shoko.Server/API/v3/Controllers/DashboardController.cs @@ -7,6 +7,7 @@ using Shoko.Commons.Extensions; using Shoko.Models.Enums; using Shoko.Server.API.Annotations; +using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.API.v3.Models.Shoko; using Shoko.Server.Models; @@ -23,6 +24,8 @@ namespace Shoko.Server.API.v3.Controllers; [Authorize] public class DashboardController : BaseController { + private readonly SeriesFactory _seriesFactory; + /// /// Get the counters of various collection stats /// @@ -327,14 +330,14 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] bool includeRestr if (pageSize <= 0) { return seriesList - .Select(a => new Series(HttpContext, a)) + .Select(a => _seriesFactory.GetSeries(a)) .ToList(); } return seriesList .Skip(pageSize * (page - 1)) .Take(pageSize) - .Select(a => new Series(HttpContext, a)) + .Select(a => _seriesFactory.GetSeries(a)) .ToList(); } @@ -499,7 +502,8 @@ public Dashboard.EpisodeDetails GetEpisodeDetailsForSeriesAndEpisode(SVR_JMMUser .ToList(); } - public DashboardController(ISettingsProvider settingsProvider) : base(settingsProvider) + public DashboardController(ISettingsProvider settingsProvider, SeriesFactory seriesFactory) : base(settingsProvider) { + _seriesFactory = seriesFactory; } } diff --git a/Shoko.Server/API/v3/Controllers/FilterController.cs b/Shoko.Server/API/v3/Controllers/FilterController.cs index f3463a80e..2470e6639 100644 --- a/Shoko.Server/API/v3/Controllers/FilterController.cs +++ b/Shoko.Server/API/v3/Controllers/FilterController.cs @@ -3,10 +3,8 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.JsonPatch; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Caching.Memory; using Shoko.Models.Enums; using Shoko.Server.API.Annotations; @@ -14,6 +12,7 @@ using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.API.v3.Models.Shoko; +using Shoko.Server.Filters; using Shoko.Server.Models; using Shoko.Server.Repositories; using Shoko.Server.Settings; @@ -26,12 +25,16 @@ namespace Shoko.Server.API.v3.Controllers; [Authorize] public class FilterController : BaseController { - private static IMemoryCache PreviewCache = new MemoryCache(new MemoryCacheOptions() { + private static readonly IMemoryCache PreviewCache = new MemoryCache(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromMinutes(50), }); internal const string FilterNotFound = "No Filter entry for the given filterID"; + private readonly FilterFactory _factory; + private readonly SeriesFactory _seriesFactory; + private readonly FilterEvaluator _filterEvaluator; + #region Existing Filters /// @@ -49,25 +52,21 @@ public ActionResult> GetAllFilters([FromQuery] bool includeEm [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] bool withConditions = false) { var user = User; - return RepoFactory.GroupFilter.GetTopLevel() + return RepoFactory.FilterPreset.GetTopLevel() .Where(filter => { - if (!showHidden && filter.IsHidden) + if (!showHidden && filter.Hidden) return false; - if (includeEmpty || (filter.IsDirectory ? ( - // Check if the directory filter have any sub-directories - RepoFactory.GroupFilter.GetByParentID(filter.GroupFilterID).Count > 0 - ) : ( - // Check if the filter have any groups for the current user. - filter.GroupsIds.ContainsKey(user.JMMUserID) && filter.GroupsIds[user.JMMUserID].Count > 0 - ))) + if (includeEmpty || (filter.IsDirectory() + ? RepoFactory.FilterPreset.GetByParentID(filter.FilterPresetID).Count > 0 + : _filterEvaluator.EvaluateFilter(filter, user.JMMUserID).Any())) return true; return false; }) - .OrderBy(filter => filter.GroupFilterName) - .ToListResult(filter => new Filter(HttpContext, filter, withConditions), page, pageSize); + .OrderBy(filter => filter.Name) + .ToListResult(filter => _factory.GetFilter(filter, withConditions), page, pageSize); } /// @@ -79,8 +78,8 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] bool withConditio [HttpPost] public ActionResult AddNewFilter(Filter.Input.CreateOrUpdateFilterBody body) { - var groupFilter = new SVR_GroupFilter { FilterType = (int)GroupFilterType.UserDefined }; - var filter = body.MergeWithExisting(HttpContext, groupFilter, ModelState); + var filterPreset = new FilterPreset { FilterType = GroupFilterType.UserDefined }; + var filter = _factory.MergeWithExisting(body, filterPreset, ModelState); if (!ModelState.IsValid) return ValidationProblem(ModelState); @@ -96,11 +95,11 @@ public ActionResult AddNewFilter(Filter.Input.CreateOrUpdateFilterBody b [HttpGet("{filterID}")] public ActionResult GetFilter([FromRoute] int filterID, [FromQuery] bool withConditions = false) { - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterNotFound); - return new Filter(HttpContext, groupFilter, withConditions); + return _factory.GetFilter(filterPreset, withConditions); } /// @@ -114,16 +113,16 @@ public ActionResult GetFilter([FromRoute] int filterID, [FromQuery] bool [HttpPatch("{filterID}")] public ActionResult PatchFilter([FromRoute] int filterID, JsonPatchDocument document) { - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterNotFound); - var body = new Filter.Input.CreateOrUpdateFilterBody(groupFilter); + var body = _factory.CreateOrUpdateFilterBody(filterPreset); document.ApplyTo(body, ModelState); if (!ModelState.IsValid) return ValidationProblem(ModelState); - var filter = body.MergeWithExisting(HttpContext, groupFilter, ModelState); + var filter = _factory.MergeWithExisting(body, filterPreset, ModelState); if (!ModelState.IsValid) return ValidationProblem(ModelState); @@ -140,11 +139,11 @@ public ActionResult PatchFilter([FromRoute] int filterID, JsonPatchDocum [HttpPut("{filterID}")] public ActionResult PutFilter([FromRoute] int filterID, Filter.Input.CreateOrUpdateFilterBody body) { - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterNotFound); - var filter = body.MergeWithExisting(HttpContext, groupFilter, ModelState); + var filter = _factory.MergeWithExisting(body, filterPreset, ModelState); if (!ModelState.IsValid) return ValidationProblem(ModelState); @@ -160,11 +159,11 @@ public ActionResult PutFilter([FromRoute] int filterID, Filter.Input.Cre [HttpDelete("{filterID}")] public ActionResult DeleteFilter(int filterID) { - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterNotFound); - RepoFactory.GroupFilter.Delete(groupFilter); + RepoFactory.FilterPreset.Delete(filterPreset); return NoContent(); } @@ -175,34 +174,29 @@ public ActionResult DeleteFilter(int filterID) #region Preview/On-the-fly Filter [NonAction] - internal static SVR_GroupFilter GetDefaultFilterForUser(SVR_JMMUser user) + internal static FilterPreset GetDefaultFilterForUser(SVR_JMMUser user) { - var groupFilter = new SVR_GroupFilter + var filterPreset = new FilterPreset { - FilterType = (int)GroupFilterType.UserDefined, - GroupFilterName = "Live Filtering", - InvisibleInClients = 0, - ApplyToSeries = 0, - BaseCondition = (int)GroupFilterBaseCondition.Include, - Conditions = new(), - SortCriteriaList = new(), + FilterType = GroupFilterType.UserDefined, + Name = "Live Filtering", }; // TODO: Update default filter for user here. - return groupFilter; + return filterPreset; } [NonAction] - internal static SVR_GroupFilter GetPreviewFilterForUser(SVR_JMMUser user) + internal static FilterPreset GetPreviewFilterForUser(SVR_JMMUser user) { var userId = user.JMMUserID; var key = $"User={userId}"; - if (!PreviewCache.TryGetValue(key, out SVR_GroupFilter groupFilter)) - groupFilter = PreviewCache.Set(key, GetDefaultFilterForUser(user), new MemoryCacheEntryOptions() { SlidingExpiration = TimeSpan.FromHours(1) }); + if (!PreviewCache.TryGetValue(key, out FilterPreset filterPreset)) + filterPreset = PreviewCache.Set(key, GetDefaultFilterForUser(user), new MemoryCacheEntryOptions() { SlidingExpiration = TimeSpan.FromHours(1) }); - return groupFilter; + return filterPreset; } [NonAction] @@ -210,7 +204,7 @@ internal static bool ResetPreviewFilterForUser(SVR_JMMUser user) { var userId = user.JMMUserID; var key = $"User={userId}"; - if (!PreviewCache.TryGetValue(key, out SVR_GroupFilter groupFilter)) + if (!PreviewCache.TryGetValue(key, out FilterPreset _)) return false; PreviewCache.Remove(key); @@ -225,8 +219,8 @@ internal static bool ResetPreviewFilterForUser(SVR_JMMUser user) [HttpGet("Preview")] public ActionResult GetPreviewFilter() { - var groupFilter = GetPreviewFilterForUser(User); - return new Filter.Input.CreateOrUpdateFilterBody(groupFilter); + var filterPreset = GetPreviewFilterForUser(User); + return _factory.CreateOrUpdateFilterBody(filterPreset); } /// @@ -238,18 +232,18 @@ internal static bool ResetPreviewFilterForUser(SVR_JMMUser user) [HttpPatch("Preview")] public ActionResult PatchPreviewFilter([FromBody] JsonPatchDocument document) { - var groupFilter = GetPreviewFilterForUser(User); + var filterPreset = GetPreviewFilterForUser(User); - var body = new Filter.Input.CreateOrUpdateFilterBody(groupFilter); + var body = _factory.CreateOrUpdateFilterBody(filterPreset); document.ApplyTo(body, ModelState); if (!ModelState.IsValid) return ValidationProblem(ModelState); - var filter = body.MergeWithExisting(HttpContext, groupFilter, ModelState, true); + var filter = _factory.MergeWithExisting(body, filterPreset, ModelState, true); if (!ModelState.IsValid) return ValidationProblem(ModelState); - return new Filter.Input.CreateOrUpdateFilterBody(groupFilter); + return _factory.CreateOrUpdateFilterBody(filterPreset); } /// @@ -260,12 +254,12 @@ internal static bool ResetPreviewFilterForUser(SVR_JMMUser user) [HttpPut("Preview")] public ActionResult PutPreviewFilter([FromBody] Filter.Input.CreateOrUpdateFilterBody body) { - var groupFilter = GetPreviewFilterForUser(User); - var filter = body.MergeWithExisting(HttpContext, groupFilter, ModelState, true); + var filterPreset = GetPreviewFilterForUser(User); + var filter = _factory.MergeWithExisting(body, filterPreset, ModelState, true); if (!ModelState.IsValid) return ValidationProblem(ModelState); - return new Filter.Input.CreateOrUpdateFilterBody(groupFilter); + return _factory.CreateOrUpdateFilterBody(filterPreset); } /// @@ -294,30 +288,25 @@ [FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValu [FromQuery] bool includeEmpty = false, [FromQuery] bool randomImages = false, [FromQuery] bool orderByName = false) { // Directories should only contain sub-filters, not groups and series. - var groupFilter = GetPreviewFilterForUser(User); - if (groupFilter.IsDirectory) + var filterPreset = GetPreviewFilterForUser(User); + if (filterPreset.IsDirectory()) return new ListResult(); // Fast path when user is not in the filter. - if (!groupFilter.GroupsIds.TryGetValue(User.JMMUserID, out var groupIds)) - return new ListResult(); + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID); + if (!results.Any()) return new ListResult(); - var groups = groupIds - .Select(group => RepoFactory.AnimeGroup.GetByID(group)) + var groups = results + .Select(group => RepoFactory.AnimeGroup.GetByID(group.Key)) .Where(group => { + // not top level groups if (group == null || group.AnimeGroupParentID.HasValue) - { return false; - } return includeEmpty || group.GetAllSeries() .Any(s => s.GetAnimeEpisodes().Any(e => e.GetVideoLocals().Count > 0)); }); - - groups = orderByName ? groups.OrderBy(group => group.GetSortName()) : - groups.OrderByGroupFilter(groupFilter); - return groups .ToListResult(group => new Group(HttpContext, group, randomImages), page, pageSize); } @@ -334,19 +323,20 @@ public ActionResult> GetPreviewGroupNameLettersInFilter([F { // Directories should only contain sub-filters, not groups and series. var user = User; - var groupFilter = GetPreviewFilterForUser(user); - if (groupFilter.IsDirectory) + var filterPreset = GetPreviewFilterForUser(user); + if (filterPreset.IsDirectory()) return new Dictionary(); // Fast path when user is not in the filter - if (!groupFilter.GroupsIds.TryGetValue(user.JMMUserID, out var groupIds)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new Dictionary(); - return groupIds - .Select(group => RepoFactory.AnimeGroup.GetByID(group)) + return results + .Select(group => RepoFactory.AnimeGroup.GetByID(group.Key)) .Where(group => { - if (group == null || group.AnimeGroupParentID.HasValue) + if (group is not { AnimeGroupParentID: null }) return false; return includeEmpty || group.GetAllSeries() @@ -373,25 +363,20 @@ [FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValu { // Directories should only contain sub-filters, not groups and series. var user = User; - var groupFilter = GetPreviewFilterForUser(user); - if (groupFilter.IsDirectory) + var filterPreset = GetPreviewFilterForUser(user); + if (filterPreset.IsDirectory()) return new ListResult(); - // Return all series if group filter is not applied to series. - if (groupFilter.ApplyToSeries != 1) - return RepoFactory.AnimeSeries.GetAll() - .Where(series => user.AllowedSeries(series) && (includeMissing || series.GetVideoLocals().Count > 0)) - .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) - .ToListResult(series => new Series(HttpContext, series, randomImages), page, pageSize); - // Return early if every series will be filtered out. - if (!groupFilter.SeriesIds.TryGetValue(user.JMMUserID, out var seriesIDs)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new ListResult(); - return seriesIDs.Select(id => RepoFactory.AnimeSeries.GetByID(id)) + // We don't need separate logic for ApplyAtSeriesLevel, as the FilterEvaluator handles that + return results.SelectMany(a => a.Select(id => RepoFactory.AnimeSeries.GetByID(id))) .Where(series => series != null && user.AllowedSeries(series) && (includeMissing || series.GetVideoLocals().Count > 0)) .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) - .ToListResult(series => new Series(HttpContext, series, randomImages), page, pageSize); + .ToListResult(series => _seriesFactory.GetSeries(series, randomImages), page, pageSize); } /// @@ -406,7 +391,7 @@ public ActionResult> GetPreviewFilteredSubGroups([FromRoute] int gro [FromQuery] bool randomImages = false, [FromQuery] bool includeEmpty = false) { var user = User; - var groupFilter = GetPreviewFilterForUser(user); + var filterPreset = GetPreviewFilterForUser(user); // Check if the group exists. var group = RepoFactory.AnimeGroup.GetByID(groupID); @@ -417,13 +402,19 @@ public ActionResult> GetPreviewFilteredSubGroups([FromRoute] int gro return Forbid(GroupController.GroupForbiddenForUser); // Directories should only contain sub-filters, not groups and series. - if (groupFilter.IsDirectory) + if (filterPreset.IsDirectory()) return new List(); // Just return early because the every group will be filtered out. - if (!groupFilter.SeriesIds.TryGetValue(user.JMMUserID, out var seriesIDs)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new List(); - + + // Subgroups are weird. We'll take the group, build a set of all subgroup IDs, and use that to determine if a group should be included + // This should maintain the order of results, but have every group in the tree for those results + var orderedGroups = results.SelectMany(a => RepoFactory.AnimeGroup.GetByID(a.Key).TopLevelAnimeGroup.GetAllChildGroups().Select(b => b.AnimeGroupID)).ToArray(); + var groups = orderedGroups.ToHashSet(); + return group.GetChildGroups() .Where(subGroup => { @@ -437,13 +428,10 @@ public ActionResult> GetPreviewFilteredSubGroups([FromRoute] int gro .Any(s => s.GetAnimeEpisodes().Any(e => e.GetVideoLocals().Count > 0))) return false; - if (groupFilter.ApplyToSeries != 1) - return true; - - return subGroup.GetAllSeries().Any(series => seriesIDs.Contains(series.AnimeSeriesID)); + return groups.Contains(subGroup.AnimeGroupID); }) - .OrderByGroupFilter(groupFilter) - .Select(group => new Group(HttpContext, group, randomImages)) + .OrderBy(a => Array.IndexOf(orderedGroups, a.AnimeGroupID)) + .Select(g => new Group(HttpContext, g, randomImages)) .ToList(); } @@ -462,7 +450,7 @@ public ActionResult> GetPreviewSeriesInFilteredGroup([FromRoute] in [FromQuery] bool randomImages = false, [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet includeDataFrom = null) { var user = User; - var groupFilter = GetPreviewFilterForUser(user); + var filterPreset = GetPreviewFilterForUser(user); // Check if the group exists. var group = RepoFactory.AnimeGroup.GetByID(groupID); @@ -473,32 +461,40 @@ public ActionResult> GetPreviewSeriesInFilteredGroup([FromRoute] in return Forbid(GroupController.GroupForbiddenForUser); // Directories should only contain sub-filters, not groups and series. - if (groupFilter.IsDirectory) + if (filterPreset.IsDirectory()) return new List(); - if (groupFilter.ApplyToSeries != 1) + if (!filterPreset.ApplyAtSeriesLevel) return (recursive ? group.GetAllSeries() : group.GetSeries()) .Where(a => user.AllowedSeries(a)) .OrderBy(series => series.GetAnime()?.AirDate ?? DateTime.MaxValue) - .Select(series => new Series(HttpContext, series, randomImages, includeDataFrom)) + .Select(series => _seriesFactory.GetSeries(series, randomImages, includeDataFrom)) .Where(series => series.Size > 0 || includeMissing) .ToList(); // Just return early because the every series will be filtered out. - if (!groupFilter.SeriesIds.TryGetValue(user.JMMUserID, out var seriesIDs)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new List(); - return (recursive ? group.GetAllSeries() : group.GetSeries()) - .Where(series => seriesIDs.Contains(series.AnimeSeriesID)) - .OrderBy(series => series.GetAnime()?.AirDate ?? DateTime.MaxValue) - .Select(series => new Series(HttpContext, series, randomImages, includeDataFrom)) - .Where(series => series.Size > 0 || includeMissing) + var seriesIDs = recursive + ? group.GetAllChildGroups().SelectMany(a => results.FirstOrDefault(b => b.Key == a.AnimeGroupID)) + : results.FirstOrDefault(a => a.Key == groupID); + + var series = seriesIDs?.Select(a => RepoFactory.AnimeSeries.GetByID(a)).Where(a => a.GetVideoLocals().Any() || includeMissing) ?? + Array.Empty(); + + return series + .Select(a => _seriesFactory.GetSeries(a, randomImages, includeDataFrom)) .ToList(); } #endregion - public FilterController(ISettingsProvider settingsProvider) : base(settingsProvider) + public FilterController(ISettingsProvider settingsProvider, FilterFactory factory, SeriesFactory seriesFactory, FilterEvaluator filterEvaluator) : base(settingsProvider) { + _factory = factory; + _seriesFactory = seriesFactory; + _filterEvaluator = filterEvaluator; } } diff --git a/Shoko.Server/API/v3/Controllers/ReverseTreeController.cs b/Shoko.Server/API/v3/Controllers/ReverseTreeController.cs index 0c85d4234..3e5ce6cc8 100644 --- a/Shoko.Server/API/v3/Controllers/ReverseTreeController.cs +++ b/Shoko.Server/API/v3/Controllers/ReverseTreeController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Shoko.Server.API.Annotations; using Shoko.Server.API.ModelBinders; +using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.API.v3.Models.Shoko; using Shoko.Server.Repositories; @@ -21,6 +22,9 @@ namespace Shoko.Server.API.v3.Controllers; [Authorize] public class ReverseTreeController : BaseController { + private readonly FilterFactory _filterFactory; + private readonly SeriesFactory _seriesFactory; + /// /// Get the parent for the with the given . /// @@ -35,26 +39,26 @@ public class ReverseTreeController : BaseController /// Always get the top-level /// [HttpGet("Filter/{filterID}/Parent")] - public ActionResult GetFilterFromFilter([FromRoute] int filterID, [FromQuery] bool topLevel = false) + public ActionResult GetParentFromFilter([FromRoute] int filterID, [FromQuery] bool topLevel = false) { - var filter = RepoFactory.GroupFilter.GetByID(filterID); + var filter = RepoFactory.FilterPreset.GetByID(filterID); if (filter == null) { return NotFound(FilterController.FilterNotFound); } - if (!filter.ParentGroupFilterID.HasValue || filter.ParentGroupFilterID.Value == 0) + if (filter.ParentFilterPresetID is null or 0) { return ValidationProblem("Unable to get parent Filter for a top-level Filter", "filterID"); } - var parentGroup = topLevel ? filter.TopLevelGroupFilter : filter.Parent; + var parentGroup = topLevel ? RepoFactory.FilterPreset.GetTopLevelFilter(filter.ParentFilterPresetID.Value) : RepoFactory.FilterPreset.GetByID(filter.ParentFilterPresetID.Value); if (parentGroup == null) { return InternalError("No parent Filter entry for the given filterID"); } - return new Filter(HttpContext, parentGroup); + return _filterFactory.GetFilter(parentGroup); } /// @@ -71,7 +75,7 @@ public ActionResult GetFilterFromFilter([FromRoute] int filterID, [FromQ /// Always get the top-level /// [HttpGet("Group/{groupID}/Parent")] - public ActionResult GetGroupFromGroup([FromRoute] int groupID, [FromQuery] bool topLevel = false) + public ActionResult GetParentFromGroup([FromRoute] int groupID, [FromQuery] bool topLevel = false) { var group = RepoFactory.AnimeGroup.GetByID(groupID); if (group == null) @@ -160,7 +164,7 @@ public ActionResult GetSeriesFromEpisode([FromRoute] int episodeID, [Fro return Forbid(EpisodeController.EpisodeForbiddenForUser); } - return new Series(HttpContext, series, randomImages, includeDataFrom); + return _seriesFactory.GetSeries(series, randomImages, includeDataFrom); } /// @@ -190,7 +194,9 @@ public ActionResult> GetEpisodeFromFile([FromRoute] int fileID, .ToList(); } - public ReverseTreeController(ISettingsProvider settingsProvider) : base(settingsProvider) + public ReverseTreeController(ISettingsProvider settingsProvider, FilterFactory filterFactory, SeriesFactory seriesFactory) : base(settingsProvider) { + _filterFactory = filterFactory; + _seriesFactory = seriesFactory; } } diff --git a/Shoko.Server/API/v3/Controllers/SeriesController.cs b/Shoko.Server/API/v3/Controllers/SeriesController.cs index 064f5e789..0ffca178a 100644 --- a/Shoko.Server/API/v3/Controllers/SeriesController.cs +++ b/Shoko.Server/API/v3/Controllers/SeriesController.cs @@ -36,12 +36,14 @@ namespace Shoko.Server.API.v3.Controllers; public class SeriesController : BaseController { private readonly ICommandRequestFactory _commandFactory; + private readonly SeriesFactory _seriesFactory; private readonly IHttpConnectionHandler _httpHandler; - public SeriesController(ICommandRequestFactory commandFactory, IHttpConnectionHandler httpHandler, ISettingsProvider settingsProvider) : base(settingsProvider) + public SeriesController(ICommandRequestFactory commandFactory, IHttpConnectionHandler httpHandler, ISettingsProvider settingsProvider, SeriesFactory seriesFactory) : base(settingsProvider) { _commandFactory = commandFactory; _httpHandler = httpHandler; + _seriesFactory = seriesFactory; } #region Return messages @@ -96,7 +98,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] string startsWith return user.AllowedSeries(series); }) .OrderBy(a => a.seriesName) - .ToListResult(tuple => new Series(HttpContext, tuple.series), page, pageSize); + .ToListResult(tuple => _seriesFactory.GetSeries(tuple.series), page, pageSize); } /// @@ -121,7 +123,7 @@ public ActionResult GetSeries([FromRoute] int seriesID, [FromQuery] bool return Forbid(SeriesForbiddenForUser); } - return new Series(HttpContext, series, randomImages, includeDataFrom); + return _seriesFactory.GetSeries(series, randomImages, includeDataFrom); } /// @@ -256,7 +258,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) return RepoFactory.AnimeSeries.GetAll() .Where(series => user.AllowedSeries(series) && series.GetVideoLocals().Count == 0) .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) - .ToListResult(series => new Series(HttpContext, series), page, pageSize); + .ToListResult(series => _seriesFactory.GetSeries(series), page, pageSize); } /// @@ -273,7 +275,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) return RepoFactory.AnimeSeries.GetAll() .Where(series => user.AllowedSeries(series) && series.GetVideoLocals(CrossRefSource.User).Count() != 0) .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) - .ToListResult(series => new Series(HttpContext, series), page, pageSize); + .ToListResult(series => _seriesFactory.GetSeries(series), page, pageSize); } #endregion @@ -288,7 +290,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) /// Search only for anime with a main title that start with the given query. /// [HttpGet("AniDB")] - public ActionResult> GetAllAnime([FromQuery] [Range(0, 100)] int pageSize = 50, + public ActionResult> GetAllAnime([FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] string startsWith = "") { startsWith = startsWith.ToLowerInvariant(); @@ -306,7 +308,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] string startsWith return user.AllowedAnime(anime); }) .OrderBy(a => a.animeTitle) - .ToListResult(tuple => new Series.AniDBWithDate(tuple.anime), page, pageSize); + .ToListResult(tuple => _seriesFactory.GetAniDB(tuple.anime), page, pageSize); } /// @@ -332,7 +334,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) /// Shoko ID /// [HttpGet("{seriesID}/AniDB")] - public ActionResult GetSeriesAnidbBySeriesID([FromRoute] int seriesID) + public ActionResult GetSeriesAnidbBySeriesID([FromRoute] int seriesID) { var series = RepoFactory.AnimeSeries.GetByID(seriesID); if (series == null) @@ -351,7 +353,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) return InternalError(AnidbNotFoundForSeriesID); } - return new Series.AniDBWithDate(anidb, series); + return _seriesFactory.GetAniDB(anidb, series); } /// @@ -380,7 +382,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) } return RepoFactory.AniDB_Anime_Similar.GetByAnimeID(anidb.AnimeID) - .Select(similar => new Series.AniDB(similar)) + .Select(similar => _seriesFactory.GetAniDB(similar)) .ToList(); } @@ -410,7 +412,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) } return RepoFactory.AniDB_Anime_Relation.GetByAnimeID(anidb.AnimeID) - .Select(relation => new Series.AniDB(relation)) + .Select(relation => _seriesFactory.GetAniDB(relation)) .ToList(); } @@ -510,7 +512,7 @@ [FromQuery] [Range(0, 1)] double? approval = null var similarToCount = similarTo.Count(); return new Series.AniDBRecommendedForYou() { - Anime = new Series.AniDBWithDate(anime, series), SimilarTo = similarToCount + Anime = _seriesFactory.GetAniDB(anime, series), SimilarTo = similarToCount }; }) .OrderByDescending(e => e.SimilarTo) @@ -597,7 +599,7 @@ private List GetWatchedAnimeForPeriod(SVR_JMMUser user, DateTim /// AniDB ID /// [HttpGet("AniDB/{anidbID}")] - public ActionResult GetSeriesAnidbByAnidbID([FromRoute] int anidbID) + public ActionResult GetSeriesAnidbByAnidbID([FromRoute] int anidbID) { var anidb = RepoFactory.AniDB_Anime.GetByAnimeID(anidbID); if (anidb == null) @@ -610,7 +612,7 @@ private List GetWatchedAnimeForPeriod(SVR_JMMUser user, DateTim return Forbid(AnidbForbiddenForUser); } - return new Series.AniDBWithDate(anidb); + return _seriesFactory.GetAniDB(anidb); } /// @@ -633,7 +635,7 @@ private List GetWatchedAnimeForPeriod(SVR_JMMUser user, DateTim } return RepoFactory.AniDB_Anime_Similar.GetByAnimeID(anidbID) - .Select(similar => new Series.AniDB(similar)) + .Select(similar => _seriesFactory.GetAniDB(similar)) .ToList(); } @@ -657,7 +659,7 @@ private List GetWatchedAnimeForPeriod(SVR_JMMUser user, DateTim } return RepoFactory.AniDB_Anime_Relation.GetByAnimeID(anidbID) - .Select(relation => new Series.AniDB(relation)) + .Select(relation => _seriesFactory.GetAniDB(relation)) .ToList(); } @@ -707,7 +709,7 @@ public ActionResult GetSeriesByAnidbID([FromRoute] int anidbID, [FromQue return Forbid(SeriesForbiddenForUser); } - return new Series(HttpContext, series, randomImages, includeDataFrom); + return _seriesFactory.GetSeries(series, randomImages, includeDataFrom); } /// @@ -735,7 +737,8 @@ public ActionResult RefreshAniDBByAniDBID([FromRoute] int anidbID, [FromQu createSeriesEntry = settings.AniDb.AutomaticallyImportSeries; } - return Series.QueueAniDBRefresh(_commandFactory, _httpHandler, anidbID, force, downloadRelations, + // TODO No + return SeriesFactory.QueueAniDBRefresh(_commandFactory, _httpHandler, anidbID, force, downloadRelations, createSeriesEntry.Value, immediate, cacheOnly); } @@ -781,7 +784,8 @@ public ActionResult RefreshAniDBBySeriesID([FromRoute] int seriesID, [From return InternalError(AnidbNotFoundForSeriesID); } - return Series.QueueAniDBRefresh(_commandFactory, _httpHandler, anidb.AnimeID, force, downloadRelations, + // TODO No + return SeriesFactory.QueueAniDBRefresh(_commandFactory, _httpHandler, anidb.AnimeID, force, downloadRelations, createSeriesEntry.Value, immediate, cacheOnly); } @@ -818,7 +822,7 @@ public ActionResult RefreshAniDBFromXML([FromRoute] int seriesID) return Forbid(TvdbForbiddenForUser); } - return Series.GetTvDBInfo(HttpContext, series); + return _seriesFactory.GetTvDBInfo(series); } /// @@ -893,8 +897,9 @@ public ActionResult RefreshSeriesTvdbBySeriesID([FromRoute] int seriesID, [FromQ } var tvSeriesList = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID); + // TODO No foreach (var crossRef in tvSeriesList) - Series.QueueTvDBRefresh(_commandFactory, crossRef.TvDBID, force); + SeriesFactory.QueueTvDBRefresh(_commandFactory, crossRef.TvDBID, force); return Ok(); } @@ -930,7 +935,7 @@ public ActionResult RefreshSeriesTvdbBySeriesID([FromRoute] int seriesID, [FromQ return Forbid(TvdbForbiddenForUser); } - return new Series.TvDB(HttpContext, tvdb, series); + return _seriesFactory.GetTvDB(tvdb, series); } /// @@ -944,7 +949,8 @@ public ActionResult RefreshSeriesTvdbBySeriesID([FromRoute] int seriesID, [FromQ [HttpPost("TvDB/{tvdbID}/Refresh")] public ActionResult RefreshSeriesTvdbByTvdbId([FromRoute] int tvdbID, [FromQuery] bool force = false, [FromQuery] bool immediate = false) { - return Series.QueueTvDBRefresh(_commandFactory, tvdbID,force, immediate); + // TODO No + return SeriesFactory.QueueTvDBRefresh(_commandFactory, tvdbID,force, immediate); } /// @@ -973,7 +979,7 @@ public ActionResult> GetSeriesByTvdbID([FromRoute] int tvdbID) } return seriesList - .Select(series => new Series(HttpContext, series)) + .Select(series => _seriesFactory.GetSeries(series)) .ToList(); } @@ -1002,7 +1008,7 @@ public ActionResult PostSeriesUserVote([FromRoute] int seriesID, [FromBody] Vote if (vote.Value > vote.MaxValue) return ValidationProblem($"Value must be less than or equal to the set max value ({vote.MaxValue}).", nameof(vote.Value)); - Series.AddSeriesVote(_commandFactory, series, User.JMMUserID, vote); + SeriesFactory.AddSeriesVote(_commandFactory, series, User.JMMUserID, vote); return NoContent(); } @@ -1042,7 +1048,7 @@ public ActionResult GetSeriesImages([FromRoute] int seriesID, [FromQuery return Forbid(SeriesForbiddenForUser); } - return Series.GetArt(HttpContext, series.AniDB_ID, includeDisabled); + return SeriesFactory.GetArt(series.AniDB_ID, includeDisabled); } #endregion @@ -1070,13 +1076,13 @@ public ActionResult GetSeriesDefaultImageForType([FromRoute] int seriesID return Forbid(SeriesForbiddenForUser); var imageSizeType = Image.GetImageSizeTypeFromType(imageType); - var defaultBanner = Series.GetDefaultImage(series.AniDB_ID, imageSizeType); + var defaultBanner = SeriesFactory.GetDefaultImage(series.AniDB_ID, imageSizeType); if (defaultBanner != null) { return defaultBanner; } - var images = Series.GetArt(HttpContext, series.AniDB_ID); + var images = SeriesFactory.GetArt(series.AniDB_ID); return imageSizeType switch { ImageSizeType.Poster => images.Posters.FirstOrDefault(), @@ -1295,7 +1301,7 @@ public ActionResult> GetSeriesTags([FromRoute] int seriesID, [FromQuer return new List(); } - return Series.GetTags(anidb, filter, excludeDescriptions, orderByName, onlyVerified); + return _seriesFactory.GetTags(anidb, filter, excludeDescriptions, orderByName, onlyVerified); } /// @@ -1339,7 +1345,7 @@ public ActionResult> GetSeriesCast([FromRoute] int seriesID, return Forbid(SeriesForbiddenForUser); } - return Series.GetCast(series.AniDB_ID, roleType); + return _seriesFactory.GetCast(series.AniDB_ID, roleType); } #endregion @@ -1413,7 +1419,7 @@ internal ActionResult> SearchInternal(string que flags |= SeriesSearch.SearchFlags.Fuzzy; return SeriesSearch.SearchSeries(User, query, limit, flags) - .Select(result => new SeriesSearchResult(HttpContext, result)) + .Select(result => _seriesFactory.GetSeriesSearchResult(result)) .ToList(); } @@ -1463,7 +1469,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) if (anime != null) { return new ListResult(1, - new List { new Series.AniDB(anime, includeTitles) }); + new List { _seriesFactory.GetAniDB(anime, includeTitles: includeTitles) }); } // Check the title cache for a match. @@ -1471,7 +1477,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) if (result != null) { return new ListResult(1, - new List { new Series.AniDB(result, includeTitles) }); + new List { _seriesFactory.GetAniDB(result, includeTitles: includeTitles) }); } return new ListResult(); @@ -1489,7 +1495,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) return null; } - return new Series.AniDB(result, series, includeTitles); + return _seriesFactory.GetAniDB(result, series, includeTitles); }) .Where(result => result != null) .ToListResult(page, pageSize); @@ -1504,7 +1510,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) return null; } - return new Series.AniDB(result, series, includeTitles); + return _seriesFactory.GetAniDB(result, series, includeTitles); }) .Where(result => result != null) .ToListResult(page, pageSize); @@ -1537,7 +1543,7 @@ public ActionResult> StartsWith([FromRoute] string quer foreach (var (ser, match) in series) { - seriesList.Add(new SeriesSearchResult(HttpContext, new() { Result = ser, Match = match })); + seriesList.Add(_seriesFactory.GetSeriesSearchResult(new() { Result = ser, Match = match })); if (seriesList.Count >= limit) { break; @@ -1580,7 +1586,7 @@ public ActionResult> PathEndsWith([FromRoute] string path) }) .SelectMany(a => a.VideoLocal?.GetAnimeEpisodes() ?? Enumerable.Empty()).Select(a => a.GetAnimeSeries()) .Distinct() - .Where(ser => ser != null && user.AllowedSeries(ser)).Select(a => new Series(HttpContext, a)).ToList(); + .Where(ser => ser != null && user.AllowedSeries(ser)).Select(a => _seriesFactory.GetSeries(a)).ToList(); } #region Helpers diff --git a/Shoko.Server/API/v3/Controllers/TreeController.cs b/Shoko.Server/API/v3/Controllers/TreeController.cs index ac67cd249..83c1640fd 100644 --- a/Shoko.Server/API/v3/Controllers/TreeController.cs +++ b/Shoko.Server/API/v3/Controllers/TreeController.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Shoko.Models.Enums; -using Shoko.Models.Server; using Shoko.Plugin.Abstractions.DataModels; using Shoko.Plugin.Abstractions.Extensions; using Shoko.Server.API.Annotations; @@ -13,6 +12,7 @@ using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.API.v3.Models.Shoko; +using Shoko.Server.Filters; using Shoko.Server.Models; using Shoko.Server.Repositories; using Shoko.Server.Settings; @@ -34,6 +34,9 @@ namespace Shoko.Server.API.v3.Controllers; [Authorize] public class TreeController : BaseController { + private readonly FilterFactory _filterFactory; + private readonly SeriesFactory _seriesFactory; + private readonly FilterEvaluator _filterEvaluator; #region Import Folder /// @@ -84,17 +87,17 @@ public ActionResult> GetSubFilters([FromRoute] int filterID, [FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] bool showHidden = false) { - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterController.FilterNotFound); - if (!groupFilter.IsDirectory) + if (!filterPreset.IsDirectory()) return new ListResult(); - return RepoFactory.GroupFilter.GetByParentID(filterID) - .Where(filter => showHidden || !filter.IsHidden) - .OrderBy(filter => filter.GroupFilterName) - .ToListResult(filter => new Filter(HttpContext, filter), page, pageSize); + return RepoFactory.FilterPreset.GetByParentID(filterID) + .Where(filter => showHidden || !filter.Hidden) + .OrderBy(filter => filter.Name) + .ToListResult(filter => _filterFactory.GetFilter(filter), page, pageSize); } /// @@ -137,30 +140,29 @@ [FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValu } else { - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterController.FilterNotFound); // Directories should only contain sub-filters, not groups and series. - if (groupFilter.IsDirectory) + if (filterPreset.IsDirectory()) return new ListResult(); - // Fast path when user is not in the filter - if (!groupFilter.GroupsIds.TryGetValue(User.JMMUserID, out var groupIds)) - return new ListResult(); + // Gets Group and Series IDs in a filter, already sorted by the filter + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID); + if (!results.Any()) return new ListResult(); - groups = groupIds - .Select(group => RepoFactory.AnimeGroup.GetByID(group)) + groups = results + .Select(group => RepoFactory.AnimeGroup.GetByID(group.Key)) .Where(group => { + // not top level groups if (group == null || group.AnimeGroupParentID.HasValue) return false; return includeEmpty || group.GetAllSeries() .Any(s => s.GetAnimeEpisodes().Any(e => e.GetVideoLocals().Count > 0)); }); - groups = orderByName ? groups.OrderBy(group => group.GetSortName()) : - groups.OrderByGroupFilter(groupFilter); } return groups @@ -186,23 +188,24 @@ public ActionResult> GetGroupNameLettersInFilter([FromRout var user = User; if (filterID.HasValue && filterID > 0) { - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID.Value); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID.Value); + if (filterPreset == null) return NotFound(FilterController.FilterNotFound); // Directories should only contain sub-filters, not groups and series. - if (groupFilter.IsDirectory) + if (filterPreset.IsDirectory()) return new Dictionary(); // Fast path when user is not in the filter - if (!groupFilter.GroupsIds.TryGetValue(user.JMMUserID, out var groupIds)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new Dictionary(); - return groupIds - .Select(group => RepoFactory.AnimeGroup.GetByID(group)) + return results + .Select(group => RepoFactory.AnimeGroup.GetByID(group.Key)) .Where(group => { - if (group == null || group.AnimeGroupParentID.HasValue) + if (group is not { AnimeGroupParentID: null }) return false; return includeEmpty || group.GetAllSeries() @@ -257,32 +260,26 @@ [FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValu return RepoFactory.AnimeSeries.GetAll() .Where(series => user.AllowedSeries(series) && (includeMissing || series.GetVideoLocals().Count > 0)) .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) - .ToListResult(series => new Series(HttpContext, series, randomImages), page, pageSize); + .ToListResult(series => _seriesFactory.GetSeries(series, randomImages), page, pageSize); // Check if the group filter exists. - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterController.FilterNotFound); // Directories should only contain sub-filters, not groups and series. - if (groupFilter.IsDirectory) + if (filterPreset.IsDirectory()) return new ListResult(); - // Return all series if group filter is not applied to series. - if (groupFilter.ApplyToSeries != 1) - return RepoFactory.AnimeSeries.GetAll() - .Where(series => user.AllowedSeries(series) && (includeMissing || series.GetVideoLocals().Count > 0)) - .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) - .ToListResult(series => new Series(HttpContext, series, randomImages), page, pageSize); - - // Return early if every series will be filtered out. - if (!groupFilter.SeriesIds.TryGetValue(user.JMMUserID, out var seriesIDs)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new ListResult(); - return seriesIDs.Select(id => RepoFactory.AnimeSeries.GetByID(id)) + // We don't need separate logic for ApplyAtSeriesLevel, as the FilterEvaluator handles that + return results.SelectMany(a => a.Select(id => RepoFactory.AnimeSeries.GetByID(id))) .Where(series => series != null && user.AllowedSeries(series) && (includeMissing || series.GetVideoLocals().Count > 0)) .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) - .ToListResult(series => new Series(HttpContext, series, randomImages), page, pageSize); + .ToListResult(series => _seriesFactory.GetSeries(series, randomImages), page, pageSize); } /// @@ -305,8 +302,8 @@ public ActionResult> GetFilteredSubGroups([FromRoute] int filterID, if (filterID == 0) return GetSubGroups(groupID, randomImages, includeEmpty); - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterController.FilterNotFound); // Check if the group exists. @@ -319,13 +316,19 @@ public ActionResult> GetFilteredSubGroups([FromRoute] int filterID, return Forbid(GroupController.GroupForbiddenForUser); // Directories should only contain sub-filters, not groups and series. - if (groupFilter.IsDirectory) + if (filterPreset.IsDirectory()) return new List(); // Just return early because the every group will be filtered out. - if (!groupFilter.SeriesIds.TryGetValue(user.JMMUserID, out var seriesIDs)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new List(); - + + // Subgroups are weird. We'll take the group, build a set of all subgroup IDs, and use that to determine if a group should be included + // This should maintain the order of results, but have every group in the tree for those results + var orderedGroups = results.SelectMany(a => RepoFactory.AnimeGroup.GetByID(a.Key).TopLevelAnimeGroup.GetAllChildGroups().Select(b => b.AnimeGroupID)).ToArray(); + var groups = orderedGroups.ToHashSet(); + return group.GetChildGroups() .Where(subGroup => { @@ -339,13 +342,10 @@ public ActionResult> GetFilteredSubGroups([FromRoute] int filterID, .Any(s => s.GetAnimeEpisodes().Any(e => e.GetVideoLocals().Count > 0))) return false; - if (groupFilter.ApplyToSeries != 1) - return true; - - return subGroup.GetAllSeries().Any(series => seriesIDs.Contains(series.AnimeSeriesID)); + return groups.Contains(subGroup.AnimeGroupID); }) - .OrderByGroupFilter(groupFilter) - .Select(group => new Group(HttpContext, group, randomImages)) + .OrderBy(a => Array.IndexOf(orderedGroups, a.AnimeGroupID)) + .Select(g => new Group(HttpContext, g, randomImages)) .ToList(); } @@ -375,11 +375,11 @@ public ActionResult> GetSeriesInFilteredGroup([FromRoute] int filte return GetSeriesInGroup(groupID, recursive, includeMissing, randomImages, includeDataFrom); // Check if the group filter exists. - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterController.FilterNotFound); - if (groupFilter.ApplyToSeries != 1) + if (!filterPreset.ApplyAtSeriesLevel) return GetSeriesInGroup(groupID, recursive, includeMissing, randomImages); // Check if the group exists. @@ -392,18 +392,23 @@ public ActionResult> GetSeriesInFilteredGroup([FromRoute] int filte return Forbid(GroupController.GroupForbiddenForUser); // Directories should only contain sub-filters, not groups and series. - if (groupFilter.IsDirectory) + if (filterPreset.IsDirectory()) return new List(); // Just return early because the every series will be filtered out. - if (!groupFilter.SeriesIds.TryGetValue(user.JMMUserID, out var seriesIDs)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new List(); - return (recursive ? group.GetAllSeries() : group.GetSeries()) - .Where(series => seriesIDs.Contains(series.AnimeSeriesID)) - .OrderBy(series => series.GetAnime()?.AirDate ?? DateTime.MaxValue) - .Select(series => new Series(HttpContext, series, randomImages, includeDataFrom)) - .Where(series => series.Size > 0 || includeMissing) + var seriesIDs = recursive + ? group.GetAllChildGroups().SelectMany(a => results.FirstOrDefault(b => b.Key == a.AnimeGroupID)) + : results.FirstOrDefault(a => a.Key == groupID); + + var series = seriesIDs?.Select(a => RepoFactory.AnimeSeries.GetByID(a)).Where(a => a.GetVideoLocals().Any() || includeMissing) ?? + Array.Empty(); + + return series + .Select(a => _seriesFactory.GetSeries(a, randomImages, includeDataFrom)) .ToList(); } @@ -485,7 +490,7 @@ public ActionResult> GetSeriesInGroup([FromRoute] int groupID, [Fro return (recursive ? group.GetAllSeries() : group.GetSeries()) .Where(a => user.AllowedSeries(a)) .OrderBy(series => series.GetAnime()?.AirDate ?? DateTime.MaxValue) - .Select(series => new Series(HttpContext, series, randomImages, includeDataFrom)) + .Select(series => _seriesFactory.GetSeries(series, randomImages, includeDataFrom)) .Where(series => series.Size > 0 || includeMissing) .ToList(); } @@ -525,7 +530,7 @@ public ActionResult GetMainSeriesInGroup([FromRoute] int groupID, [FromQ return InternalError("Unable to find main series for group."); } - return new Series(HttpContext, mainSeries, randomImages, includeDataFrom); + return _seriesFactory.GetSeries(mainSeries, randomImages, includeDataFrom); } #endregion @@ -905,7 +910,10 @@ public ActionResult> GetFilesForEpisode([FromRoute] int episodeID, [F #endregion - public TreeController(ISettingsProvider settingsProvider) : base(settingsProvider) + public TreeController(ISettingsProvider settingsProvider, FilterFactory filterFactory, FilterEvaluator filterEvaluator, SeriesFactory seriesFactory) : base(settingsProvider) { + _filterFactory = filterFactory; + _filterEvaluator = filterEvaluator; + _seriesFactory = seriesFactory; } } diff --git a/Shoko.Server/API/v3/Controllers/WebUIController.cs b/Shoko.Server/API/v3/Controllers/WebUIController.cs index f75a3b667..0e1dfcfc7 100644 --- a/Shoko.Server/API/v3/Controllers/WebUIController.cs +++ b/Shoko.Server/API/v3/Controllers/WebUIController.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Caching.Memory; using Shoko.Server.API.Annotations; using Shoko.Server.API.ModelBinders; +using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.API.v3.Models.Shoko; using Shoko.Server.API.WebUI; @@ -42,6 +43,7 @@ public class WebUIController : BaseController }); private static readonly TimeSpan CacheTTL = TimeSpan.FromHours(1); + private readonly WebUIFactory _webUIFactory; /// /// Retrieves the list of available themes. @@ -208,7 +210,7 @@ public ActionResult> GetGroupView([FromBody] Input.WebUIGr return null; } - return new WebUIGroupExtra(group, series, anime, body.TagFilter, body.OrderByName, + return _webUIFactory.GetWebUIGroupExtra(group, series, anime, body.TagFilter, body.OrderByName, body.TagLimit); }) .ToList(); @@ -234,7 +236,7 @@ public ActionResult GetSeries([FromRoute] int seriesID) return Forbid(SeriesController.SeriesForbiddenForUser); } - return new WebUISeriesExtra(HttpContext, series); + return _webUIFactory.GetWebUISeriesExtra(series); } /// @@ -562,7 +564,8 @@ public ActionResult LatestServerWebUIVersion([FromQuery] Relea } - public WebUIController(ISettingsProvider settingsProvider) : base(settingsProvider) + public WebUIController(ISettingsProvider settingsProvider, WebUIFactory webUIFactory) : base(settingsProvider) { + _webUIFactory = webUIFactory; } } diff --git a/Shoko.Server/API/v3/Helpers/APIGroupFilterSortingHelper.cs b/Shoko.Server/API/v3/Helpers/APIGroupFilterSortingHelper.cs deleted file mode 100644 index 82e37e141..000000000 --- a/Shoko.Server/API/v3/Helpers/APIGroupFilterSortingHelper.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Shoko.Models.Enums; -using Shoko.Server.Models; - -namespace Shoko.Server.API.v3.Helpers; - -public static class APIGroupFilterSortingHelper -{ - public static IEnumerable OrderByGroupFilter(this IEnumerable groups, - SVR_GroupFilter gf) - { - var isFirst = true; - var query = groups; - foreach (var gfsc in gf.SortCriteriaList) - { - query = Order(query, gfsc, isFirst); - isFirst = false; - } - - return query; - } - - public static IEnumerable OrderByGroupFilter(this IEnumerable series, - SVR_GroupFilter gf) - { - var isFirst = true; - var result = series; - foreach (var gfsc in gf.SortCriteriaList) - { - result = Order(result, gfsc, isFirst); - isFirst = false; - } - - return result; - } - - private static IOrderedEnumerable Order(IEnumerable groups, - GroupFilterSortingCriteria gfsc, bool isFirst) - { - var desc = gfsc.SortDirection == GroupFilterSortDirection.Desc; - switch (gfsc.SortType) - { - case GroupFilterSorting.Year: - return !desc - ? Order(groups, a => a.Contract.Stat_AirDate_Min, false, isFirst) - : Order(groups, a => a.Contract.Stat_AirDate_Max, true, isFirst); - case GroupFilterSorting.AniDBRating: - return Order(groups, a => a.Contract.Stat_AniDBRating, desc, isFirst); - case GroupFilterSorting.EpisodeAddedDate: - return Order(groups, a => a.EpisodeAddedDate, desc, isFirst); - case GroupFilterSorting.EpisodeAirDate: - return Order(groups, a => a.LatestEpisodeAirDate, desc, isFirst); - case GroupFilterSorting.EpisodeWatchedDate: - return Order(groups, a => a.Contract.WatchedDate, desc, isFirst); - case GroupFilterSorting.MissingEpisodeCount: - return Order(groups, a => a.MissingEpisodeCount, desc, isFirst); - case GroupFilterSorting.SeriesAddedDate: - return Order(groups, a => a.Contract.Stat_SeriesCreatedDate, desc, isFirst); - case GroupFilterSorting.SeriesCount: - return Order(groups, a => a.Contract.Stat_SeriesCount, desc, isFirst); - case GroupFilterSorting.SortName: - return Order(groups, a => a.SortName, desc, isFirst); - case GroupFilterSorting.UnwatchedEpisodeCount: - return Order(groups, a => a.Contract.UnwatchedEpisodeCount, desc, isFirst); - case GroupFilterSorting.UserRating: - return Order(groups, a => a.Contract.Stat_UserVoteOverall, desc, isFirst); - case GroupFilterSorting.GroupName: - case GroupFilterSorting.GroupFilterName: - return Order(groups, a => a.GroupName, desc, isFirst); - default: - return Order(groups, a => a.GroupName, desc, isFirst); - } - } - - private static IOrderedEnumerable Order(IEnumerable groups, - GroupFilterSortingCriteria gfsc, bool isFirst) - { - var desc = gfsc.SortDirection == GroupFilterSortDirection.Desc; - switch (gfsc.SortType) - { - case GroupFilterSorting.Year: - return Order(groups, a => a.Contract.AniDBAnime.AniDBAnime.AirDate, desc, isFirst); - case GroupFilterSorting.AniDBRating: - return Order(groups, a => a.Contract.AniDBAnime.AniDBAnime.Rating, desc, isFirst); - case GroupFilterSorting.EpisodeAddedDate: - return Order(groups, a => a.EpisodeAddedDate, desc, isFirst); - case GroupFilterSorting.EpisodeAirDate: - return Order(groups, a => a.LatestEpisodeAirDate, desc, isFirst); - case GroupFilterSorting.EpisodeWatchedDate: - return Order(groups, a => a.Contract.WatchedDate, desc, isFirst); - case GroupFilterSorting.MissingEpisodeCount: - return Order(groups, a => a.MissingEpisodeCount, desc, isFirst); - case GroupFilterSorting.SeriesAddedDate: - return Order(groups, a => a.Contract.DateTimeCreated, desc, isFirst); - case GroupFilterSorting.SeriesCount: - return Order(groups, a => 1, desc, isFirst); - case GroupFilterSorting.SortName: - return Order(groups, a => a.GetSeriesName(), desc, isFirst); - case GroupFilterSorting.UnwatchedEpisodeCount: - return Order(groups, a => a.Contract.UnwatchedEpisodeCount, desc, isFirst); - case GroupFilterSorting.UserRating: - return Order(groups, a => a.Contract.AniDBAnime.UserVote.VoteValue, desc, isFirst); - case GroupFilterSorting.GroupName: - case GroupFilterSorting.GroupFilterName: - return Order(groups, a => a.GetSeriesName(), desc, isFirst); - default: - return Order(groups, a => a.GetSeriesName(), desc, isFirst); - } - } - - private static IOrderedEnumerable Order(IEnumerable groups, - Func o, - bool descending, bool isFirst) - { - if (isFirst) - { - return descending - ? groups.OrderByDescending(o) - : groups.OrderBy(o); - } - - return descending - ? ((IOrderedEnumerable)groups).ThenByDescending(o) - : ((IOrderedEnumerable)groups).ThenBy(o); - } -} diff --git a/Shoko.Server/API/v3/Helpers/FilterFactory.cs b/Shoko.Server/API/v3/Helpers/FilterFactory.cs new file mode 100644 index 000000000..2ad7bcf90 --- /dev/null +++ b/Shoko.Server/API/v3/Helpers/FilterFactory.cs @@ -0,0 +1,256 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Shoko.Commons.Extensions; +using Shoko.Models.Enums; +using Shoko.Server.API.v3.Models.Shoko; +using Shoko.Server.Filters; +using Shoko.Server.Filters.Interfaces; +using Shoko.Server.Models; +using Shoko.Server.Repositories; + +namespace Shoko.Server.API.v3.Helpers; + +public class FilterFactory +{ + private readonly HttpContext _context; + private readonly FilterEvaluator _evaluator; + + public FilterFactory(HttpContext context, FilterEvaluator evaluator) + { + _context = context; + _evaluator = evaluator; + } + + public Filter GetFilter(FilterPreset groupFilter, bool fullModel = false) + { + var user = _context.GetUser(); + var filter = new Filter + { + IDs = new Filter.FilterIDs + { + ID = groupFilter.FilterPresetID, ParentFilter = groupFilter.ParentFilterPresetID + }, + Name = groupFilter.Name, + IsLocked = groupFilter.Locked, + IsDirectory = groupFilter.IsDirectory(), + IsHidden = groupFilter.Hidden, + ApplyAtSeriesLevel = groupFilter.ApplyAtSeriesLevel, + }; + + if (fullModel) + { + filter.Expression = GetExpressionTree(groupFilter.Expression); + filter.Sorting = GetSortingCriteria(groupFilter.SortingExpression); + } + + filter.Size = filter.IsDirectory + ? RepoFactory.FilterPreset.GetByParentID(groupFilter.FilterPresetID).Count + : _evaluator.EvaluateFilter(groupFilter, user?.JMMUserID).Count(); + return filter; + } + + public static Filter.FilterCondition GetExpressionTree(FilterExpression expression) + { + if (expression is null) return null; + var result = new Filter.FilterCondition + { + Type = expression.GetType().Name.Replace("Expression", "") + }; + + // Left/First + switch (expression) + { + case IWithExpressionParameter left: + result.Left = GetExpressionTree(left.Left); + break; + case IWithDateSelectorParameter left: + result.Left = GetExpressionTree(left.Left); + break; + case IWithNumberSelectorParameter left: + result.Left = GetExpressionTree(left.Left); + break; + case IWithStringSelectorParameter left: + result.Left = GetExpressionTree(left.Left); + break; + } + + // Parameters + switch (expression) + { + case IWithStringParameter parameter: + result.Parameter = parameter.Parameter; + break; + case IWithNumberParameter parameter: + result.Parameter = parameter.Parameter.ToString(); + break; + case IWithDateParameter parameter: + result.Parameter = parameter.Parameter.ToString("yyyy-MM-dd"); + break; + case IWithTimeSpanParameter parameter: + result.Parameter = parameter.Parameter.ToString("G"); + break; + } + + // Right/Second + switch (expression) + { + case IWithSecondExpressionParameter right: + result.Right = GetExpressionTree(right.Right); + break; + case IWithSecondDateSelectorParameter right: + result.Right = GetExpressionTree(right.Right); + break; + case IWithSecondStringSelectorParameter right: + result.Right = GetExpressionTree(right.Right); + break; + case IWithSecondNumberSelectorParameter right: + result.Right = GetExpressionTree(right.Right); + break; + case IWithSecondStringParameter right: + result.SecondParameter = right.SecondParameter; + break; + } + + return result; + } + + public static Filter.SortingCriteria GetSortingCriteria(SortingExpression expression) + { + var result = new Filter.SortingCriteria + { + Type = expression.GetType().Name.Replace("Selector", ""), + IsInverted = expression.Descending + }; + + var currentCriteria = result; + var currentExpression = expression; + while (currentExpression.Next != null) + { + currentCriteria.Next = new Filter.SortingCriteria + { + Type = currentExpression.GetType().Name.Replace("Selector", ""), + IsInverted = currentExpression.Descending + }; + currentCriteria = currentCriteria.Next; + currentExpression = currentExpression.Next; + } + + return result; + } + + public Filter.Input.CreateOrUpdateFilterBody CreateOrUpdateFilterBody(FilterPreset groupFilter) + { + var result = new Filter.Input.CreateOrUpdateFilterBody + { + Name = groupFilter.Name, + ParentID = groupFilter.ParentFilterPresetID, + IsDirectory = groupFilter.IsDirectory(), + IsHidden = groupFilter.Hidden, + ApplyAtSeriesLevel = groupFilter.ApplyAtSeriesLevel + }; + + if (!result.IsDirectory) + { + result.Expression = GetExpressionTree(groupFilter.Expression); + result.Sorting = GetSortingCriteria(groupFilter.SortingExpression); + } + + return result; + } + + public Filter MergeWithExisting(Filter.Input.CreateOrUpdateFilterBody body, FilterPreset groupFilter, ModelStateDictionary modelState, bool skipSave = false) + { + if (groupFilter.Locked) + modelState.AddModelError("IsLocked", "Filter is locked."); + + // Defer to `null` if the id is `0`. + if (body.ParentID is 0) + body.ParentID = null; + + if (body.ParentID.HasValue) + { + var parentFilter = RepoFactory.FilterPreset.GetByID(body.ParentID.Value); + if (parentFilter == null) + { + modelState.AddModelError(nameof(body.ParentID), $"Unable to find parent filter with id {body.ParentID.Value}"); + } + else + { + if (parentFilter.Locked) + modelState.AddModelError(nameof(body.ParentID), $"Unable to add a sub-filter to a filter that is locked."); + + if (!parentFilter.IsDirectory()) + modelState.AddModelError(nameof(body.ParentID), $"Unable to add a sub-filter to a filter that is not a directorty filter."); + } + } + + if (body.IsDirectory) + { + if (body.Expression != null) + modelState.AddModelError(nameof(body.Expression), "Directory filters cannot have any conditions applied to them."); + + if (body.Sorting != null) + modelState.AddModelError(nameof(body.Sorting), "Directory filters cannot have custom sorting applied to them."); + } + else + { + var subFilters = groupFilter.FilterPresetID != 0 ? RepoFactory.FilterPreset.GetByParentID(groupFilter.FilterPresetID) : new(); + if (subFilters.Count > 0) + modelState.AddModelError(nameof(body.IsDirectory), "Cannot turn a directory filter with sub-filters into a normal filter without first removing the sub-filters"); + } + + // Return now if we encountered any validation errors. + if (!modelState.IsValid) + return null; + + groupFilter.ParentFilterPresetID = body.ParentID; + groupFilter.FilterType = body.IsDirectory ? GroupFilterType.UserDefined | GroupFilterType.Directory : GroupFilterType.UserDefined; + groupFilter.Name = body.Name; + groupFilter.Hidden = body.IsHidden; + groupFilter.ApplyAtSeriesLevel = body.ApplyAtSeriesLevel; + if (!body.IsDirectory) + { + if (body.Expression != null) + { + // TODO convert back from API model + //groupFilter.Expression = body.Expression + } + + if (body.Sorting != null) + { + // TODO Convert back from API model + } + } + + // Skip saving if we're just going to preview a group filter. + if (!skipSave) + RepoFactory.FilterPreset.Save(groupFilter); + + return GetFilter(groupFilter, true); + } + + public Filter GetFirstAiringSeasonGroupFilter(SVR_AniDB_Anime anime) + { + var type = (AnimeType)anime.AnimeType; + if (type != AnimeType.TVSeries && type != AnimeType.Web) + return null; + + var (year, season) = anime.GetSeasons() + .FirstOrDefault(); + if (year == 0) + return null; + + var seasonName = $"{season} {year}"; + var seasonsFilterID = RepoFactory.FilterPreset.GetTopLevel() + .FirstOrDefault(f => f.FilterType == (GroupFilterType.Directory | GroupFilterType.Season))?.FilterPresetID; + if (seasonsFilterID == null) return null; + var firstAirSeason = RepoFactory.FilterPreset.GetByParentID(seasonsFilterID.Value) + .FirstOrDefault(f => f.Name == seasonName); + if (firstAirSeason == null) + return null; + + return GetFilter(firstAirSeason); + } +} diff --git a/Shoko.Server/API/v3/Helpers/ModelHelper.cs b/Shoko.Server/API/v3/Helpers/ModelHelper.cs index a360470f2..479b61496 100644 --- a/Shoko.Server/API/v3/Helpers/ModelHelper.cs +++ b/Shoko.Server/API/v3/Helpers/ModelHelper.cs @@ -339,7 +339,7 @@ public static GroupSizes GenerateGroupSizes(List seriesList, Li foreach (var series in seriesList) { var anime = series.GetAnime(); - switch (Series.GetAniDBSeriesType(anime?.AnimeType)) + switch (SeriesFactory.GetAniDBSeriesType(anime?.AnimeType)) { case SeriesType.Unknown: sizes.SeriesTypes.Unknown++; diff --git a/Shoko.Server/API/v3/Helpers/SeriesFactory.cs b/Shoko.Server/API/v3/Helpers/SeriesFactory.cs new file mode 100644 index 000000000..3f469d27a --- /dev/null +++ b/Shoko.Server/API/v3/Helpers/SeriesFactory.cs @@ -0,0 +1,868 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Shoko.Commons.Extensions; +using Shoko.Models.Enums; +using Shoko.Models.Server; +using Shoko.Server.API.v3.Models.Common; +using Shoko.Server.API.v3.Models.Shoko; +using Shoko.Server.Commands; +using Shoko.Server.Commands.AniDB; +using Shoko.Server.Models; +using Shoko.Server.Providers.AniDB.Interfaces; +using Shoko.Server.Providers.AniDB.Titles; +using Shoko.Server.Repositories; +using Shoko.Server.Repositories.Cached; +using Shoko.Server.Utilities; +using AniDBAnimeType = Shoko.Models.Enums.AnimeType; +using AniDBEpisodeType = Shoko.Models.Enums.EpisodeType; +using Image = Shoko.Server.API.v3.Models.Common.Image; +using Series = Shoko.Server.API.v3.Models.Shoko.Series; + +namespace Shoko.Server.API.v3.Helpers; + +public class SeriesFactory +{ + private readonly HttpContext _context; + private readonly CrossRef_Anime_StaffRepository _crossRefAnimeStaffRepository; + private readonly AnimeCharacterRepository _animeCharacterRepository; + private readonly AnimeStaffRepository _animeStaffRepository; + private readonly CustomTagRepository _customTagRepository; + private readonly AniDB_TagRepository _aniDBTagRepository; + private readonly AniDB_Anime_TagRepository _aniDBAnimeTagRepository; + + public SeriesFactory(HttpContext context) + { + _context = context; + _crossRefAnimeStaffRepository = RepoFactory.CrossRef_Anime_Staff; + _animeCharacterRepository = RepoFactory.AnimeCharacter; + _animeStaffRepository = RepoFactory.AnimeStaff; + _customTagRepository = RepoFactory.CustomTag; + _aniDBTagRepository = RepoFactory.AniDB_Tag; + _aniDBAnimeTagRepository = RepoFactory.AniDB_Anime_Tag; + } + + public Series GetSeries(SVR_AnimeSeries ser, bool randomiseImages = false, HashSet includeDataFrom = null) + { + var uid = _context.GetUser()?.JMMUserID ?? 0; + var anime = ser.GetAnime(); + var animeType = (AniDBAnimeType)anime.AnimeType; + + var result = new Series(); + AddBasicAniDBInfo(result, ser, anime); + + var ael = ser.GetAnimeEpisodes(); + var contract = ser.Contract; + if (contract == null) + { + ser.UpdateContract(); + } + + result.IDs = GetIDs(ser); + result.Images = GetDefaultImages(ser, randomiseImages); + result.AirsOn = animeType == AniDBAnimeType.TVSeries || animeType == AniDBAnimeType.Web ? ser.GetAirsOnDaysOfWeek(ael) : new(); + + result.Name = ser.GetSeriesName(); + result.Sizes = ModelHelper.GenerateSeriesSizes(ael, uid); + result.Size = result.Sizes.Local.Credits + result.Sizes.Local.Episodes + result.Sizes.Local.Others + result.Sizes.Local.Parodies + + result.Sizes.Local.Specials + result.Sizes.Local.Trailers; + + result.Created = ser.DateTimeCreated.ToUniversalTime(); + result.Updated = ser.DateTimeUpdated.ToUniversalTime(); + + if (includeDataFrom?.Contains(DataSource.AniDB) ?? false) + { + result._AniDB = GetAniDB(anime, ser); + } + if (includeDataFrom?.Contains(DataSource.TvDB) ?? false) + result._TvDB = GetTvDBInfo(ser); + + return result; + } + + private void AddBasicAniDBInfo(Series result, SVR_AnimeSeries series, SVR_AniDB_Anime anime) + { + if (anime == null) + { + return; + } + + result.Links = new(); + if (!string.IsNullOrEmpty(anime.Site_EN)) + foreach (var site in anime.Site_EN.Split('|')) + result.Links.Add(new() { Type = "source", Name = "Official Site (EN)", URL = site }); + + if (!string.IsNullOrEmpty(anime.Site_JP)) + foreach (var site in anime.Site_JP.Split('|')) + result.Links.Add(new() { Type = "source", Name = "Official Site (JP)", URL = site }); + + if (!string.IsNullOrEmpty(anime.Wikipedia_ID)) + result.Links.Add(new() { Type = "wiki", Name = "Wikipedia (EN)", URL = $"https://en.wikipedia.org/{anime.Wikipedia_ID}" }); + + if (!string.IsNullOrEmpty(anime.WikipediaJP_ID)) + result.Links.Add(new() { Type = "wiki", Name = "Wikipedia (JP)", URL = $"https://en.wikipedia.org/{anime.WikipediaJP_ID}" }); + + if (!string.IsNullOrEmpty(anime.CrunchyrollID)) + result.Links.Add(new() { Type = "streaming", Name = "Crunchyroll", URL = $"https://crunchyroll.com/anime/{anime.CrunchyrollID}" }); + + if (!string.IsNullOrEmpty(anime.FunimationID)) + result.Links.Add(new() { Type = "streaming", Name = "Funimation", URL = anime.FunimationID }); + + if (!string.IsNullOrEmpty(anime.HiDiveID)) + result.Links.Add(new() { Type = "streaming", Name = "HiDive", URL = $"https://www.hidive.com/{anime.HiDiveID}" }); + + if (anime.AllCinemaID.HasValue && anime.AllCinemaID.Value > 0) + result.Links.Add(new() { Type = "foreign-metadata", Name = "allcinema", URL = $"https://allcinema.net/cinema/{anime.AllCinemaID.Value}" }); + + if (anime.AnisonID.HasValue && anime.AnisonID.Value > 0) + result.Links.Add(new() { Type = "foreign-metadata", Name = "Anison", URL = $"https://anison.info/data/program/{anime.AnisonID.Value}.html" }); + + if (anime.SyoboiID.HasValue && anime.SyoboiID.Value > 0) + result.Links.Add(new() { Type = "foreign-metadata", Name = "syoboi", URL = $"https://cal.syoboi.jp/tid/{anime.SyoboiID.Value}/time" }); + + if (anime.BangumiID.HasValue && anime.BangumiID.Value > 0) + result.Links.Add(new() { Type = "foreign-metadata", Name = "bangumi", URL = $"https://bgm.tv/subject/{anime.BangumiID.Value}" }); + + if (anime.LainID.HasValue && anime.LainID.Value > 0) + result.Links.Add(new() { Type = "foreign-metadata", Name = ".lain", URL = $"http://lain.gr.jp/mediadb/media/{anime.LainID.Value}" }); + + if (anime.ANNID.HasValue && anime.ANNID.Value > 0) + result.Links.Add(new() { Type = "english-metadata", Name = "AnimeNewsNetwork", URL = $"https://www.animenewsnetwork.com/encyclopedia/anime.php?id={anime.ANNID.Value}" }); + + if (anime.VNDBID.HasValue && anime.VNDBID.Value > 0) + result.Links.Add(new() { Type = "english-metadata", Name = "VNDB", URL = $"https://vndb.org/v{anime.VNDBID.Value}" }); + + var vote = RepoFactory.AniDB_Vote.GetByEntityAndType(anime.AnimeID, AniDBVoteType.Anime) ?? + RepoFactory.AniDB_Vote.GetByEntityAndType(anime.AnimeID, AniDBVoteType.AnimeTemp); + if (vote != null) + { + var voteType = (AniDBVoteType)vote.VoteType == AniDBVoteType.Anime ? "Permanent" : "Temporary"; + result.UserRating = new Rating + { + Value = (decimal)Math.Round(vote.VoteValue / 100D, 1), + MaxValue = 10, + Type = voteType, + Source = "User" + }; + } + } + + public static bool QueueAniDBRefresh(ICommandRequestFactory commandFactory, IHttpConnectionHandler handler, + int animeID, bool force, bool downloadRelations, bool createSeriesEntry, bool immediate = false, + bool cacheOnly = false) + { + if (force) + return QueueForcedAniDBRefresh(commandFactory, handler, animeID, downloadRelations, createSeriesEntry, immediate); + + var command = commandFactory.Create(c => + { + c.AnimeID = animeID; + c.DownloadRelations = downloadRelations; + c.ForceRefresh = force; + c.CacheOnly = !force && cacheOnly; + c.CreateSeriesEntry = createSeriesEntry; + c.BubbleExceptions = immediate; + }); + if (immediate) + { + try + { + command.ProcessCommand(); + } + catch + { + return false; + } + + return command.Result != null; + } + + commandFactory.Save(command); + return false; + } + + private static bool QueueForcedAniDBRefresh(ICommandRequestFactory commandFactory, IHttpConnectionHandler handler, + int animeID, bool downloadRelations, bool createSeriesEntry, bool immediate = false) + { + var command = commandFactory.Create(c => + { + c.AnimeID = animeID; + c.DownloadRelations = downloadRelations; + c.CreateSeriesEntry = createSeriesEntry; + c.BubbleExceptions = immediate; + }); + if (immediate && !handler.IsBanned) + { + try + { + command.ProcessCommand(); + } + catch + { + return false; + } + + return command.Result != null; + } + + commandFactory.Save(command); + return false; + } + + public static bool QueueTvDBRefresh(ICommandRequestFactory commandFactory, int tvdbID, bool force, bool immediate = false) + { + var command = commandFactory.Create(c => + { + c.TvDBSeriesID = tvdbID; + c.ForceRefresh = force; + c.BubbleExceptions = immediate; + }); + if (immediate) + { + try + { + command.ProcessCommand(); + } + catch + { + return false; + } + + return command.Result != null; + } + + commandFactory.Save(command); + return false; + } + + public static SeriesIDs GetIDs(SVR_AnimeSeries ser) + { + // Shoko + var ids = new SeriesIDs + { + ID = ser.AnimeSeriesID, + ParentGroup = ser.AnimeGroupID, + TopLevelGroup = ser.TopLevelAnimeGroup.AnimeGroupID + }; + + // AniDB + var anidbId = ser.GetAnime()?.AnimeID; + if (anidbId.HasValue) + { + ids.AniDB = anidbId.Value; + } + + // TvDB + var tvdbIds = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(ser.AniDB_ID); + if (tvdbIds.Any()) + { + ids.TvDB.AddRange(tvdbIds.Select(a => a.TvDBID).Distinct()); + } + + // TODO: Cache the rest of these, so that they don't severely slow down the API + + // TMDB + // TODO: make this able to support more than one, in fact move it to its own and remove CrossRef_Other + var tmdbId = RepoFactory.CrossRef_AniDB_Other.GetByAnimeIDAndType(ser.AniDB_ID, CrossRefType.MovieDB); + if (tmdbId != null && int.TryParse(tmdbId.CrossRefID, out var movieID)) + { + ids.TMDB.Add(movieID); + } + + // Trakt + // var traktIds = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(ser.AniDB_ID).Select(a => a.TraktID) + // .Distinct().ToList(); + // if (traktIds.Any()) ids.TraktTv.AddRange(traktIds); + + // MAL + var malIds = RepoFactory.CrossRef_AniDB_MAL.GetByAnimeID(ser.AniDB_ID).Select(a => a.MALID).Distinct() + .ToList(); + if (malIds.Any()) + { + ids.MAL.AddRange(malIds); + } + + // TODO: AniList later + return ids; + } + + public static Image GetDefaultImage(int anidbId, ImageSizeType imageSizeType, + ImageEntityType? imageEntityType = null) + { + var defaultImage = imageEntityType.HasValue + ? RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(anidbId, + imageSizeType, imageEntityType.Value) + : RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeType(anidbId, imageSizeType); + return defaultImage != null + ? new Image(defaultImage.ImageParentID, (ImageEntityType)defaultImage.ImageParentType, true) + : null; + } + + public Images GetDefaultImages(SVR_AnimeSeries ser, bool randomiseImages = false) + { + var images = new Images(); + var random = _context.Items["Random"] as Random; + var allImages = GetArt(ser.AniDB_ID); + + var poster = randomiseImages + ? allImages.Posters.GetRandomElement(random) + : GetDefaultImage(ser.AniDB_ID, ImageSizeType.Poster) ?? allImages.Posters.FirstOrDefault(); + if (poster != null) + { + images.Posters.Add(poster); + } + + var fanart = randomiseImages + ? allImages.Fanarts.GetRandomElement(random) + : GetDefaultImage(ser.AniDB_ID, ImageSizeType.Fanart) ?? allImages.Fanarts.FirstOrDefault(); + if (fanart != null) + { + images.Fanarts.Add(fanart); + } + + var banner = randomiseImages + ? allImages.Banners.GetRandomElement(random) + : GetDefaultImage(ser.AniDB_ID, ImageSizeType.WideBanner) ?? allImages.Banners.FirstOrDefault(); + if (banner != null) + { + images.Banners.Add(banner); + } + + return images; + } + + public List GetTvDBInfo(SVR_AnimeSeries ser) + { + var ael = ser.GetAnimeEpisodes(true); + return RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(ser.AniDB_ID) + .Select(xref => RepoFactory.TvDB_Series.GetByTvDBID(xref.TvDBID)) + .Select(tvdbSer => GetTvDB(tvdbSer, ser, ael)) + .ToList(); + } + + public static void AddSeriesVote(ICommandRequestFactory commandFactory, SVR_AnimeSeries ser, int userID, Vote vote) + { + var voteType = (vote.Type?.ToLowerInvariant() ?? "") switch + { + "temporary" => (int)AniDBVoteType.AnimeTemp, + "permanent" => (int)AniDBVoteType.Anime, + _ => ser.GetAnime()?.GetFinishedAiring() ?? false ? (int)AniDBVoteType.Anime : (int)AniDBVoteType.AnimeTemp + }; + + var dbVote = RepoFactory.AniDB_Vote.GetByEntityAndType(ser.AniDB_ID, AniDBVoteType.AnimeTemp) ?? + RepoFactory.AniDB_Vote.GetByEntityAndType(ser.AniDB_ID, AniDBVoteType.Anime); + + if (dbVote == null) + { + dbVote = new AniDB_Vote { EntityID = ser.AniDB_ID }; + } + + dbVote.VoteValue = (int)Math.Floor(vote.GetRating(1000)); + dbVote.VoteType = voteType; + + RepoFactory.AniDB_Vote.Save(dbVote); + + commandFactory.CreateAndSave( + c => + { + c.AnimeID = ser.AniDB_ID; + c.VoteType = voteType; + c.VoteValue = vote.GetRating(); + } + ); + } + + public static Images GetArt(int animeID, bool includeDisabled = false) + { + var images = new Images(); + AddAniDBPoster(images, animeID); + AddTvDBImages(images, animeID, includeDisabled); + // AddMovieDBImages(ctx, images, animeID, includeDisabled); + return images; + } + + private static void AddAniDBPoster(Images images, int animeID) + { + images.Posters.Add(GetAniDBPoster(animeID)); + } + + public static Image GetAniDBPoster(int animeID) + { + var defaultImage = RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeType(animeID, + ImageSizeType.Poster); + var preferred = defaultImage != null && defaultImage.ImageParentType == (int)ImageEntityType.AniDB_Cover; + return new Image(animeID, ImageEntityType.AniDB_Cover, preferred); + } + + private static void AddTvDBImages(Images images, int animeID, bool includeDisabled = false) + { + var tvdbIDs = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(animeID).ToList(); + + var defaultFanart = + RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, + ImageSizeType.Fanart, ImageEntityType.TvDB_FanArt); + var fanarts = tvdbIDs.SelectMany(a => RepoFactory.TvDB_ImageFanart.GetBySeriesID(a.TvDBID)).ToList(); + images.Fanarts.AddRange(fanarts.Where(a => includeDisabled || a.Enabled != 0).Select(a => + { + var preferred = defaultFanart != null && defaultFanart.ImageParentID == a.TvDB_ImageFanartID; + return new Image(a.TvDB_ImageFanartID, ImageEntityType.TvDB_FanArt, preferred, a.Enabled == 0); + })); + + var defaultBanner = + RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, + ImageSizeType.WideBanner, ImageEntityType.TvDB_Banner); + var banners = tvdbIDs.SelectMany(a => RepoFactory.TvDB_ImageWideBanner.GetBySeriesID(a.TvDBID)).ToList(); + images.Banners.AddRange(banners.Where(a => includeDisabled || a.Enabled != 0).Select(a => + { + var preferred = defaultBanner != null && defaultBanner.ImageParentID == a.TvDB_ImageWideBannerID; + return new Image(a.TvDB_ImageWideBannerID, ImageEntityType.TvDB_Banner, preferred, a.Enabled == 0); + })); + + var defaultPoster = + RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, + ImageSizeType.Poster, ImageEntityType.TvDB_Cover); + var posters = tvdbIDs.SelectMany(a => RepoFactory.TvDB_ImagePoster.GetBySeriesID(a.TvDBID)).ToList(); + images.Posters.AddRange(posters.Where(a => includeDisabled || a.Enabled != 0).Select(a => + { + var preferred = defaultPoster != null && defaultPoster.ImageParentID == a.TvDB_ImagePosterID; + return new Image(a.TvDB_ImagePosterID, ImageEntityType.TvDB_Cover, preferred, a.Enabled == 0); + })); + } + + private static void AddMovieDBImages(Images images, int animeID, bool includeDisabled = false) + { + var moviedb = RepoFactory.CrossRef_AniDB_Other.GetByAnimeIDAndType(animeID, CrossRefType.MovieDB); + + var moviedbPosters = moviedb == null + ? new List() + : RepoFactory.MovieDB_Poster.GetByMovieID(int.Parse(moviedb.CrossRefID)); + var defaultPoster = + RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, + ImageSizeType.Poster, ImageEntityType.MovieDB_Poster); + images.Posters.AddRange(moviedbPosters.Where(a => includeDisabled || a.Enabled != 0).Select(a => + { + var preferred = defaultPoster != null && defaultPoster.ImageParentID == a.MovieDB_PosterID; + return new Image(a.MovieDB_PosterID, ImageEntityType.MovieDB_Poster, preferred, a.Enabled == 1); + })); + + var moviedbFanarts = moviedb == null + ? new List() + : RepoFactory.MovieDB_Fanart.GetByMovieID(int.Parse(moviedb.CrossRefID)); + var defaultFanart = + RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, + ImageSizeType.Fanart, ImageEntityType.MovieDB_FanArt); + images.Fanarts.AddRange(moviedbFanarts.Where(a => includeDisabled || a.Enabled != 0).Select(a => + { + var preferred = defaultFanart != null && defaultFanart.ImageParentID == a.MovieDB_FanartID; + return new Image(a.MovieDB_FanartID, ImageEntityType.MovieDB_FanArt, preferred, a.Enabled == 1); + })); + } + + public Series.AniDB GetAniDB(SVR_AniDB_Anime anime, SVR_AnimeSeries series = null, bool includeTitles = true) + { + series ??= RepoFactory.AnimeSeries.GetByAnimeID(anime.AnimeID); + var seriesTitle = series?.GetSeriesName() ?? anime.PreferredTitle; + var result = new Series.AniDB + { + ID = anime.AnimeID, + ShokoID = series?.AnimeSeriesID, + Type = GetAniDBSeriesType(anime.AnimeType), + Title = seriesTitle, + Titles = includeTitles + ? anime.GetTitles().Select(title => new Title + { + Name = title.Title, + Language = title.LanguageCode, + Type = title.TitleType, + Default = string.Equals(title.Title, seriesTitle), + Source = "AniDB" + } + ).ToList() + : null, + Description = anime.Description, + Restricted = anime.Restricted == 1, + Poster = GetAniDBPoster(anime.AnimeID), + EpisodeCount = anime.EpisodeCountNormal, + Rating = new Rating + { + Source = "AniDB", Value = anime.Rating, MaxValue = 1000, Votes = anime.VoteCount + }, + UserApproval = null, + Relation = null, + }; + + if (anime.AirDate.HasValue) result.AirDate = anime.AirDate.Value; + if (anime.EndDate.HasValue) result.EndDate = anime.EndDate.Value; + + return result; + } + + public Series.AniDB GetAniDB(ResponseAniDBTitles.Anime result, SVR_AnimeSeries series = null, bool includeTitles = false) + { + if (series == null) + { + series = RepoFactory.AnimeSeries.GetByAnimeID(result.AnimeID); + } + + var anime = series != null ? series.GetAnime() : RepoFactory.AniDB_Anime.GetByAnimeID(result.AnimeID); + + var animeTitle = series?.GetSeriesName() ?? anime?.PreferredTitle ?? result.MainTitle; + var anidb = new Series.AniDB + { + ID = result.AnimeID, + ShokoID = series?.AnimeSeriesID, + Type = GetAniDBSeriesType(anime?.AnimeType), + Title = animeTitle, + Titles = includeTitles + ? result.Titles.Select(title => new Title + { + Language = title.LanguageCode, + Name = title.Title, + Type = title.TitleType, + Default = string.Equals(title.Title, animeTitle), + Source = "AniDB" + } + ).ToList() + : null, + Description = anime?.Description, + Restricted = anime is { Restricted: 1 }, + EpisodeCount = anime?.EpisodeCount, + Poster = GetAniDBPoster(result.AnimeID), + }; + if (anime?.AirDate != null) anidb.AirDate = anime.AirDate.Value; + if (anime?.EndDate != null) anidb.EndDate = anime.EndDate.Value; + return anidb; + } + + public Series.AniDB GetAniDB(SVR_AniDB_Anime_Relation relation, SVR_AnimeSeries series = null, bool includeTitles = true) + { + series ??= RepoFactory.AnimeSeries.GetByAnimeID(relation.RelatedAnimeID); + var result = new Series.AniDB + { + ID = relation.RelatedAnimeID, + ShokoID = series?.AnimeSeriesID, + Poster = GetAniDBPoster(relation.RelatedAnimeID), + Rating = null, + UserApproval = null, + Relation = SeriesRelation.GetRelationTypeFromAnidbRelationType(relation.RelationType), + }; + SetAniDBTitles(result, relation, series, includeTitles); + return result; + } + + private void SetAniDBTitles(Series.AniDB aniDB, AniDB_Anime_Relation relation, SVR_AnimeSeries series, bool includeTitles) + { + var anime = RepoFactory.AniDB_Anime.GetByAnimeID(relation.RelatedAnimeID); + if (anime is not null) + { + aniDB.Type = GetAniDBSeriesType(anime.AnimeType); + aniDB.Title = series?.GetSeriesName() ?? anime.PreferredTitle; + aniDB.Titles = includeTitles + ? anime.GetTitles().Select( + title => new Title + { + Name = title.Title, + Language = title.LanguageCode, + Type = title.TitleType, + Default = string.Equals(title.Title, aniDB.Title), + Source = "AniDB" + } + ).ToList() + : null; + aniDB.Description = anime.Description; + aniDB.Restricted = anime.Restricted == 1; + aniDB.EpisodeCount = anime.EpisodeCountNormal; + return; + } + + var result = Utils.AniDBTitleHelper.SearchAnimeID(relation.RelatedAnimeID); + if (result != null) + { + aniDB.Type = SeriesType.Unknown; + aniDB.Title = result.PreferredTitle; + aniDB.Titles = includeTitles + ? result.Titles.Select( + title => new Title + { + Language = title.LanguageCode, + Name = title.Title, + Type = title.TitleType, + Default = string.Equals(title.Title, aniDB.Title), + Source = "AniDB" + } + ).ToList() + : null; + aniDB.Description = null; + // If the other anime is present we assume they're of the same kind. Be it restricted or unrestricted. + anime = RepoFactory.AniDB_Anime.GetByAnimeID(relation.AnimeID); + aniDB.Restricted = anime is not null && anime.Restricted == 1; + return; + } + + aniDB.Type = SeriesType.Unknown; + aniDB.Titles = includeTitles ? new List() : null; + aniDB.Restricted = false; + } + + public Series.AniDB GetAniDB(AniDB_Anime_Similar similar, SVR_AnimeSeries series = null, bool includeTitles = true) + { + series ??= RepoFactory.AnimeSeries.GetByAnimeID(similar.SimilarAnimeID); + var result = new Series.AniDB + { + ID = similar.SimilarAnimeID, + ShokoID = series?.AnimeSeriesID, + Poster = GetAniDBPoster(similar.SimilarAnimeID), + Rating = null, + UserApproval = new Rating + { + Value = new Vote(similar.Approval, similar.Total).GetRating(100), + MaxValue = 100, + Votes = similar.Total, + Source = "AniDB", + Type = "User Approval" + }, + Relation = null, + Restricted = false, + }; + SetAniDBTitles(result, similar, series, includeTitles); + return result; + } + + private void SetAniDBTitles(Series.AniDB aniDB, AniDB_Anime_Similar similar, SVR_AnimeSeries series, bool includeTitles) + { + var anime = RepoFactory.AniDB_Anime.GetByAnimeID(similar.SimilarAnimeID); + if (anime is not null) + { + aniDB.Type = GetAniDBSeriesType(anime.AnimeType); + aniDB.Title = series?.GetSeriesName() ?? anime.PreferredTitle; + aniDB.Titles = includeTitles + ? anime.GetTitles().Select( + title => new Title + { + Name = title.Title, + Language = title.LanguageCode, + Type = title.TitleType, + Default = string.Equals(title.Title, aniDB.Title), + Source = "AniDB" + } + ).ToList() + : null; + aniDB.Description = anime.Description; + aniDB.Restricted = anime.Restricted == 1; + return; + } + + var result = Utils.AniDBTitleHelper.SearchAnimeID(similar.SimilarAnimeID); + if (result != null) + { + aniDB.Type = SeriesType.Unknown; + aniDB.Title = result.PreferredTitle; + aniDB.Titles = includeTitles + ? result.Titles.Select( + title => new Title + { + Language = title.LanguageCode, + Name = title.Title, + Type = title.TitleType, + Default = string.Equals(title.Title, aniDB.Title), + Source = "AniDB" + } + ).ToList() + : null; + aniDB.Description = null; + // If the other anime is present we assume they're of the same kind. Be it restricted or unrestricted. + anime = RepoFactory.AniDB_Anime.GetByAnimeID(similar.AnimeID); + aniDB.Restricted = anime is not null && anime.Restricted == 1; + return; + } + + aniDB.Type = SeriesType.Unknown; + aniDB.Title = null; + aniDB.Titles = includeTitles ? new List<Title>() : null; + aniDB.Description = null; + aniDB.Restricted = false; + } + + /// <summary> + /// Cast is aggregated, and therefore not in each provider + /// </summary> + /// <param name="animeID"></param> + /// <param name="roleTypes"></param> + /// <returns></returns> + public List<Role> GetCast(int animeID, HashSet<Role.CreatorRoleType> roleTypes = null) + { + var roles = new List<Role>(); + var xrefAnimeStaff = _crossRefAnimeStaffRepository.GetByAnimeID(animeID); + foreach (var xref in xrefAnimeStaff) + { + // Filter out any roles that are not of the desired type. + if (roleTypes != null && !roleTypes.Contains((Role.CreatorRoleType)xref.RoleType)) + continue; + + var character = xref.RoleID.HasValue ? _animeCharacterRepository.GetByID(xref.RoleID.Value) : null; + var staff = _animeStaffRepository.GetByID(xref.StaffID); + if (staff == null) + continue; + + var role = new Role + { + Character = + character != null + ? new Role.Person + { + Name = character.Name, + AlternateName = character.AlternateName, + Image = new Image(character.CharacterID, ImageEntityType.Character), + Description = character.Description + } + : null, + Staff = new Role.Person + { + Name = staff.Name, + AlternateName = staff.AlternateName, + Description = staff.Description, + Image = staff.ImagePath != null ? new Image(staff.StaffID, ImageEntityType.Staff) : null + }, + RoleName = (Role.CreatorRoleType)xref.RoleType, + RoleDetails = xref.Role + }; + roles.Add(role); + } + + return roles; + } + + public List<Tag> GetTags(SVR_AniDB_Anime anime, TagFilter.Filter filter, + bool excludeDescriptions = false, bool orderByName = false, bool onlyVerified = true) + { + // Only get the user tags if we don't exclude it (false == false), or if we invert the logic and want to include it (true == true). + IEnumerable<Tag> userTags = new List<Tag>(); + if (filter.HasFlag(TagFilter.Filter.User) == filter.HasFlag(TagFilter.Filter.Invert)) + { + userTags = _customTagRepository.GetByAnimeID(anime.AnimeID) + .Select(tag => new Tag(tag, excludeDescriptions)); + } + + var selectedTags = anime.GetAniDBTags(onlyVerified) + .DistinctBy(a => a.TagName) + .ToList(); + var tagFilter = new TagFilter<AniDB_Tag>(name => _aniDBTagRepository.GetByName(name).FirstOrDefault(), tag => tag.TagName, + name => new AniDB_Tag { TagNameSource = name }); + var anidbTags = tagFilter + .ProcessTags(filter, selectedTags) + .Select(tag => + { + var xref = _aniDBAnimeTagRepository.GetByTagID(tag.TagID).FirstOrDefault(xref => xref.AnimeID == anime.AnimeID); + return new Tag(tag, excludeDescriptions) { Weight = xref?.Weight ?? 0, IsLocalSpoiler = xref?.LocalSpoiler }; + }); + + if (orderByName) + return userTags.Concat(anidbTags) + .OrderByDescending(tag => tag.Source) + .ThenBy(tag => tag.Name) + .ToList(); + + return userTags.Concat(anidbTags) + .OrderByDescending(tag => tag.Source) + .ThenByDescending(tag => tag.Weight) + .ThenBy(tag => tag.Name) + .ToList(); + } + + public static SeriesType GetAniDBSeriesType(int? animeType) + { + return animeType.HasValue ? GetAniDBSeriesType((AniDBAnimeType)animeType.Value) : SeriesType.Unknown; + } + + public static SeriesType GetAniDBSeriesType(AniDBAnimeType animeType) + { + switch (animeType) + { + default: + case AniDBAnimeType.None: + return SeriesType.Unknown; + case AniDBAnimeType.TVSeries: + return SeriesType.TV; + case AniDBAnimeType.Movie: + return SeriesType.Movie; + case AniDBAnimeType.OVA: + return SeriesType.OVA; + case AniDBAnimeType.TVSpecial: + return SeriesType.TVSpecial; + case AniDBAnimeType.Web: + return SeriesType.Web; + case AniDBAnimeType.Other: + return SeriesType.Other; + } + } + + public Series.TvDB GetTvDB(TvDB_Series tbdbSeries, SVR_AnimeSeries series, + List<SVR_AnimeEpisode> episodeList = null) + { + if (episodeList == null) + { + episodeList = series.GetAnimeEpisodes(true); + } + + var images = new Images(); + AddTvDBImages(images, series.AniDB_ID); + + // Aggregate stuff + var firstEp = episodeList.FirstOrDefault(a => + a.AniDB_Episode != null && a.AniDB_Episode.EpisodeType == (int)AniDBEpisodeType.Episode && + a.AniDB_Episode.EpisodeNumber == 1) + ?.TvDBEpisode; + + var lastEp = episodeList + .Where(a => a.AniDB_Episode != null && a.AniDB_Episode.EpisodeType == (int)AniDBEpisodeType.Episode) + .OrderBy(a => a.AniDB_Episode.EpisodeType) + .ThenBy(a => a.AniDB_Episode.EpisodeNumber).LastOrDefault() + ?.TvDBEpisode; + + var result = new Series.TvDB + { + ID = tbdbSeries.SeriesID, + Description = tbdbSeries.Overview, + Title = tbdbSeries.SeriesName, + Posters = images.Posters, + Fanarts = images.Fanarts, + Banners = images.Banners, + Season = firstEp?.SeasonNumber, + AirDate = firstEp?.AirDate, + EndDate = lastEp?.AirDate, + }; + if (tbdbSeries.Rating != null) + { + result.Rating = new Rating { Source = "TvDB", Value = tbdbSeries.Rating.Value, MaxValue = 10 }; + } + + return result; + } + + public SeriesSearchResult GetSeriesSearchResult(SeriesSearch.SearchResult<SVR_AnimeSeries> result) + { + var series = GetSeries(result.Result); + var searchResult = new SeriesSearchResult + { + Name = series.Name, + IDs = series.IDs, + Size = series.Size, + Sizes = series.Sizes, + Created = series.Created, + Updated = series.Updated, + AirsOn = series.AirsOn, + UserRating = series.UserRating, + Images = series.Images, + Links = series.Links, + _AniDB = series._AniDB, + _TvDB = series._TvDB, + ExactMatch = result.ExactMatch, + Index = result.Index, + Distance = result.Distance, + LengthDifference = result.LengthDifference, + Match = result.Match, + }; + return searchResult; + } +} diff --git a/Shoko.Server/API/v3/Helpers/WebUIFactory.cs b/Shoko.Server/API/v3/Helpers/WebUIFactory.cs new file mode 100644 index 000000000..e47654594 --- /dev/null +++ b/Shoko.Server/API/v3/Helpers/WebUIFactory.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Shoko.Server.API.v3.Models.Common; +using Shoko.Server.Models; + +namespace Shoko.Server.API.v3.Helpers; + +public class WebUIFactory +{ + private readonly HttpContext _context; + private readonly FilterFactory _filterFactory; + private readonly SeriesFactory _seriesFactory; + + public WebUIFactory(HttpContext context, FilterFactory filterFactory, SeriesFactory seriesFactory) + { + _context = context; + _filterFactory = filterFactory; + _seriesFactory = seriesFactory; + } + + // TODO the rest of this + public Models.Shoko.WebUI.WebUISeriesExtra GetWebUISeriesExtra(SVR_AnimeSeries series) + { + var anime = series.GetAnime(); + var cast = _seriesFactory.GetCast(anime.AnimeID, new () { Role.CreatorRoleType.Studio, Role.CreatorRoleType.Producer }); + + var result = new Models.Shoko.WebUI.WebUISeriesExtra + { + FirstAirSeason = _filterFactory.GetFirstAiringSeasonGroupFilter(anime), + Studios = cast.Where(role => role.RoleName == Role.CreatorRoleType.Studio).Select(role => role.Staff).ToList(), + Producers = cast.Where(role => role.RoleName == Role.CreatorRoleType.Producer).Select(role => role.Staff).ToList(), + SourceMaterial = _seriesFactory.GetTags(anime, TagFilter.Filter.Invert | TagFilter.Filter.Source, excludeDescriptions: true).FirstOrDefault()?.Name ?? "Original Work", + }; + return result; + } + + public Models.Shoko.WebUI.WebUIGroupExtra GetWebUIGroupExtra(SVR_AnimeGroup group, SVR_AnimeSeries series, SVR_AniDB_Anime anime, + TagFilter.Filter filter = TagFilter.Filter.None, bool orderByName = false, int tagLimit = 30) + { + var result = new Models.Shoko.WebUI.WebUIGroupExtra{ + ID = group.AnimeGroupID, + Type = SeriesFactory.GetAniDBSeriesType(anime.AnimeType), + Rating = new Rating { Source = "AniDB", Value = anime.Rating, MaxValue = 1000, Votes = anime.VoteCount } + }; + if (anime.AirDate != null) + { + var airdate = anime.AirDate.Value; + if (airdate != DateTime.MinValue) + { + result.AirDate = airdate; + } + } + + if (anime.EndDate != null) + { + var enddate = anime.EndDate.Value; + if (enddate != DateTime.MinValue) + { + result.EndDate = enddate; + } + } + + result.Tags = _seriesFactory.GetTags(anime, filter, excludeDescriptions: true, orderByName) + .Take(tagLimit) + .ToList(); + + return result; + } +} diff --git a/Shoko.Server/API/v3/Models/Shoko/Dashboard.cs b/Shoko.Server/API/v3/Models/Shoko/Dashboard.cs index 887e339f2..3e4271aec 100644 --- a/Shoko.Server/API/v3/Models/Shoko/Dashboard.cs +++ b/Shoko.Server/API/v3/Models/Shoko/Dashboard.cs @@ -5,6 +5,7 @@ using Shoko.Models.Enums; using Shoko.Models.Server; using Shoko.Server.API.Converters; +using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.Models; using Shoko.Server.Repositories; @@ -150,8 +151,8 @@ public EpisodeDetails(AniDB_Episode episode, SVR_AniDB_Anime anime, SVR_AnimeSer ResumePosition = userRecord?.ResumePositionTimeSpan; Watched = userRecord?.WatchedDate?.ToUniversalTime(); SeriesTitle = series?.GetSeriesName() ?? anime.PreferredTitle; - SeriesPoster = Series.GetDefaultImage(anime.AnimeID, ImageSizeType.Poster) ?? - Series.GetAniDBPoster(anime.AnimeID); + SeriesPoster = SeriesFactory.GetDefaultImage(anime.AnimeID, ImageSizeType.Poster) ?? + SeriesFactory.GetAniDBPoster(anime.AnimeID); } /// <summary> diff --git a/Shoko.Server/API/v3/Models/Shoko/Filter.cs b/Shoko.Server/API/v3/Models/Shoko/Filter.cs index 4de255353..d73055bb8 100644 --- a/Shoko.Server/API/v3/Models/Shoko/Filter.cs +++ b/Shoko.Server/API/v3/Models/Shoko/Filter.cs @@ -1,19 +1,6 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Shoko.Models.Enums; -using Shoko.Models.Server; using Shoko.Server.API.v3.Models.Common; -using Shoko.Server.Filters; -using Shoko.Server.Filters.Logic; -using Shoko.Server.Models; -using Shoko.Server.Repositories; -using FilterPreset = Shoko.Server.Models.Filter; // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global // ReSharper disable UnusedAutoPropertyAccessor.Global @@ -47,66 +34,27 @@ public class Filter : BaseModel public bool IsDirectory { get; set; } /// <summary> - /// Indicates that the filter is inverted and all conditions applied - /// to it will be used to exclude groups and series instead of - /// include them. - /// </summary> - public bool IsInverted { get; set; } - - /// <summary> - /// Indicates the filter should be hidden unless explictly requested. This will hide the filter from the normal UIs. + /// Indicates the filter should be hidden unless explicitly requested. This will hide the filter from the normal UIs. /// </summary> public bool IsHidden { get; set; } /// <summary> - /// Inidcates the filter should be applied at the series level. + /// Indicates the filter should be applied at the series level. /// Filter conditions like like Seasons, Years, Tags, etc only count series individually, rather than by group. /// </summary> public bool ApplyAtSeriesLevel { get; set; } /// <summary> - /// List of Conditions. Order doesn't matter. + /// The FilterExpression tree /// </summary> [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public List<FilterCondition>? Conditions { get; set; } + public FilterCondition? Expression { get; set; } /// <summary> - /// The sorting criteria. Order matters. + /// The sorting criteria /// </summary> [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public List<SortingCriteria>? Sorting { get; set; } - - public Filter(HttpContext ctx, FilterPreset groupFilter, bool fullModel = false) - { - var user = ctx.GetUser(); - - IDs = new FilterIDs { ID = groupFilter.FilterID, ParentFilter = groupFilter.ParentFilterID }; - Name = groupFilter.Name; - IsLocked = groupFilter.Locked; - IsDirectory = groupFilter.IsDirectory(); - IsInverted = groupFilter.Expression is NotExpression; - IsHidden = groupFilter.Hidden; - ApplyAtSeriesLevel = groupFilter.ApplyAtSeriesLevel; - if (fullModel) - { - var legacyConverter = ctx.RequestServices.GetRequiredService<LegacyFilterConverter>(); - Conditions = legacyConverter.GetConditions(groupFilter).Select(condition => new FilterCondition(condition)).ToList(); - Sorting = legacyConverter.GetSortingCriteria(groupFilter).Select(sort => new SortingCriteria(sort)).ToList(); - } - - var evaluator = ctx.RequestServices.GetRequiredService<FilterEvaluator>(); - Size = IsDirectory ? RepoFactory.Filter.GetByParentID(groupFilter.FilterID).Count : evaluator.EvaluateFilter(groupFilter, user?.JMMUserID).Count(); - } - - /// <summary> - /// Get the Sorting Criteria for the Group Filter. ORDER DOES MATTER - /// </summary> - /// <param name="gf"></param> - /// <returns></returns> - public static List<SortingCriteria> GetSortingCriteria(SVR_GroupFilter gf) - { - return gf.SortCriteriaList.Select(a => new SortingCriteria(a)).ToList(); - } + public SortingCriteria? Sorting { get; set; } public class FilterIDs : IDs { @@ -119,32 +67,43 @@ public class FilterIDs : IDs public class FilterCondition { /// <summary> - /// Condition Type. What it does + /// Condition Type. What it does. + /// This is not the GroupFilterConditionType, but the type of the FilterExpression, with 'Expression' removed. + /// ex. And, Or, Not, HasAudioLanguage /// </summary> [Required] - [JsonConverter(typeof(StringEnumConverter))] - public GroupFilterConditionType Type { get; set; } + public string Type { get; set; } /// <summary> - /// Condition Operator, how it applies + /// The first, or left, child expression. + /// This might be another logic operator like And, a selector for data like Today's Date, or an expression like HasAudioLanguage. + /// Whether this is included depends on the expression. /// </summary> - [Required] - [JsonConverter(typeof(StringEnumConverter))] - public GroupFilterOperator Operator { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public FilterCondition? Left { get; set; } /// <summary> - /// The actual value to compare + /// The second, or right, child expression. + /// This might be another logic operator like And, a selector for data like Today's Date, or an expression like HasAudioLanguage. + /// Whether this is included depends on the expression. /// </summary> - public string Parameter { get; set; } = string.Empty; + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public FilterCondition? Right { get; set; } - public FilterCondition() { } + /// <summary> + /// The actual value to compare. Dependent on the expression type. + /// Coerced this to string to make things easier. + /// </summary> + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string? Parameter { get; set; } - public FilterCondition(GroupFilterCondition condition) - { - Type = (GroupFilterConditionType)condition.ConditionType; - Operator = (GroupFilterOperator)condition.ConditionOperator; - Parameter = condition.ConditionParameter ?? string.Empty; - } + /// <summary> + /// The actual value to compare. Dependent on the expression type. + /// Very few things have a second parameter. Seasons are one of them + /// Coerced this to string to make things easier. + /// </summary> + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string? SecondParameter { get; set; } } /// <summary> @@ -155,25 +114,23 @@ public FilterCondition(GroupFilterCondition condition) public class SortingCriteria { /// <summary> - /// The sorting type. What it is sorted on + /// The sorting type. What it is sorted on. + /// This is not the GroupFilterSorting, but the type of the SortingExpression, with 'Expression' removed. + /// ex. And, Or, Not, HasAudioLanguage /// </summary> [Required] - [JsonConverter(typeof(StringEnumConverter))] - public GroupFilterSorting Type { get; set; } + public string Type { get; set; } + + /// <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> + public SortingCriteria? Next { get; set; } /// <summary> /// Assumed Ascending unless this is specified. You must set this if you want highest rating, for example /// </summary> [Required] public bool IsInverted { get; set; } - - public SortingCriteria() { } - - public SortingCriteria(GroupFilterSortingCriteria criteria) - { - Type = criteria.SortType; - IsInverted = criteria.SortDirection == GroupFilterSortDirection.Desc; - } } public class Input @@ -205,13 +162,6 @@ public class CreateOrUpdateFilterBody /// </remarks> public bool IsDirectory { get; set; } - /// <summary> - /// Indicates that the filter is inverted and all conditions applied - /// to it will be used to exclude groups and series instead of - /// include them. - /// </summary> - public bool IsInverted { get; set; } - /// <summary> /// Indicates the filter should be hidden unless explictly requested. This will hide the filter from the normal UIs. /// </summary> @@ -224,116 +174,16 @@ public class CreateOrUpdateFilterBody public bool ApplyAtSeriesLevel { get; set; } /// <summary> - /// List of Conditions. Order doesn't matter. + /// The FilterExpression tree /// </summary> - public List<FilterCondition>? Conditions { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public FilterCondition? Expression { get; set; } /// <summary> - /// The sorting criteria. Order matters. + /// The sorting criteria /// </summary> - public List<SortingCriteria>? Sorting { get; set; } - - public CreateOrUpdateFilterBody() { } - - public CreateOrUpdateFilterBody(FilterPreset groupFilter) - { - Name = groupFilter.Name; - ParentID = groupFilter.ParentFilterID; - IsDirectory = groupFilter.IsDirectory(); - IsHidden = groupFilter.Hidden; - ApplyAtSeriesLevel = groupFilter.ApplyAtSeriesLevel; - if (!IsDirectory) - { - Conditions = groupFilter.Conditions.Select(condition => new FilterCondition(condition)).ToList(); - Sorting = groupFilter.SortCriteriaList.Select(sort => new SortingCriteria(sort)).ToList(); - } - } - - public Filter? MergeWithExisting(HttpContext ctx, FilterPreset groupFilter, ModelStateDictionary modelState, bool skipSave = false) - { - if (groupFilter.Locked) - modelState.AddModelError("IsLocked", "Filter is locked."); - - // Defer to `null` if the id is `0`. - if (ParentID.HasValue && ParentID.Value == 0) - ParentID = null; - - if (ParentID.HasValue) - { - var parentFilter = RepoFactory.Filter.GetByID(ParentID.Value); - if (parentFilter == null) - { - modelState.AddModelError(nameof(ParentID), $"Unable to find parent filter with id {ParentID.Value}"); - } - else - { - if (parentFilter.Locked) - modelState.AddModelError(nameof(ParentID), $"Unable to add a sub-filter to a filter that is locked."); - - if (!parentFilter.IsDirectory()) - modelState.AddModelError(nameof(ParentID), $"Unable to add a sub-filter to a filter that is not a directorty filter."); - } - } - - if (IsDirectory) - { - if (IsInverted) - modelState.AddModelError(nameof(IsInverted), "Cannot invert the filter conditions for a directory filter."); - - if (Conditions != null && Conditions.Count > 0) - modelState.AddModelError(nameof(Conditions), "Directory filters cannot have any conditions applied to them."); - - if (Sorting != null && Sorting.Count > 0) - modelState.AddModelError(nameof(Sorting), "Directory filters cannot have custom sorting applied to them."); - } - else - { - var subFilters = groupFilter.FilterID != 0 ? RepoFactory.Filter.GetByParentID(groupFilter.FilterID) : new(); - if (subFilters.Count > 0) - modelState.AddModelError(nameof(IsDirectory), "Cannot turn a directory filter with sub-filters into a normal filter without first removing the sub-filters"); - } - - // Return now if we encountered any validation errors. - if (!modelState.IsValid) - return null; - - groupFilter.ParentFilterID = ParentID; - groupFilter.FilterType = IsDirectory ? GroupFilterType.UserDefined | GroupFilterType.Directory : GroupFilterType.UserDefined; - groupFilter.Name = Name; - groupFilter.Hidden = IsHidden; - groupFilter.ApplyAtSeriesLevel = ApplyAtSeriesLevel; - if (!IsDirectory) - { - if (Conditions != null) - { - groupFilter.Conditions = Conditions - .Select(c => new GroupFilterCondition() - { - ConditionOperator = (int)c.Operator, ConditionParameter = c.Parameter, ConditionType = (int)c.Type, - }) - .ToList(); - } - - if (Sorting != null) - { - groupFilter.SortCriteriaList = Sorting - .Select(s => new GroupFilterSortingCriteria - { - SortType = s.Type, SortDirection = s.IsInverted ? GroupFilterSortDirection.Desc : GroupFilterSortDirection.Asc - }) - .ToList(); - } - } - else - { - } - - // Skip saving if we're just going to preview a group filter. - if (!skipSave) - RepoFactory.Filter.Save(groupFilter); - - return new Filter(ctx, groupFilter, true); - } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public SortingCriteria? Sorting { get; set; } } } } diff --git a/Shoko.Server/API/v3/Models/Shoko/Group.cs b/Shoko.Server/API/v3/Models/Shoko/Group.cs index 202ff6252..00f553946 100644 --- a/Shoko.Server/API/v3/Models/Shoko/Group.cs +++ b/Shoko.Server/API/v3/Models/Shoko/Group.cs @@ -92,11 +92,13 @@ public Group(HttpContext ctx, SVR_AnimeGroup group, bool randomiseImages = false SortName = group.SortName; Description = group.Description; Sizes = ModelHelper.GenerateGroupSizes(allSeries, episodes, subGroupCount, userID); - Size = allSeries.Where(series => series.AnimeGroupID == group.AnimeGroupID).Count(); + Size = allSeries.Count(series => series.AnimeGroupID == group.AnimeGroupID); HasCustomName = group.IsManuallyNamed == 1; HasCustomDescription = group.OverrideDescription == 1; - Images = mainSeries == null ? new Images() : Series.GetDefaultImages(ctx, mainSeries, randomiseImages); + // TODO make a factory for this file. Not feeling it rn + var factory = new SeriesFactory(ctx); + Images = mainSeries == null ? new Images() : factory.GetDefaultImages(mainSeries, randomiseImages); } #endregion diff --git a/Shoko.Server/API/v3/Models/Shoko/Series.cs b/Shoko.Server/API/v3/Models/Shoko/Series.cs index 9f651401d..ad40007ad 100644 --- a/Shoko.Server/API/v3/Models/Shoko/Series.cs +++ b/Shoko.Server/API/v3/Models/Shoko/Series.cs @@ -1,23 +1,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using Shoko.Commons.Extensions; -using Shoko.Models.Enums; -using Shoko.Models.Server; using Shoko.Server.API.Converters; -using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; -using Shoko.Server.Commands; -using Shoko.Server.Commands.AniDB; using Shoko.Server.Models; -using Shoko.Server.Providers.AniDB.Interfaces; -using Shoko.Server.Providers.AniDB.Titles; using Shoko.Server.Repositories; -using Shoko.Server.Utilities; using AniDBEpisodeType = Shoko.Models.Enums.EpisodeType; using AniDBAnimeType = Shoko.Models.Enums.AnimeType; @@ -85,7 +74,7 @@ public class Series : BaseModel /// included in the data to add. /// </summary> [JsonProperty("AniDB", NullValueHandling = NullValueHandling.Ignore)] - public AniDBWithDate _AniDB { get; set; } + public AniDB _AniDB { get; set; } /// <summary> /// The <see cref="Series.TvDB"/> entries, if <see cref="DataSource.TvDB"/> @@ -94,534 +83,6 @@ public class Series : BaseModel [JsonProperty("TvDB", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable<TvDB> _TvDB { get; set; } - #region Constructors and Helper Methods - - public Series() { } - - public Series(HttpContext ctx, SVR_AnimeSeries ser, bool randomiseImages = false, HashSet<DataSource> includeDataFrom = null) - { - var uid = ctx.GetUser()?.JMMUserID ?? 0; - var anime = ser.GetAnime(); - var animeType = (AniDBAnimeType)anime.AnimeType; - - AddBasicAniDBInfo(ctx, ser, anime); - - var ael = ser.GetAnimeEpisodes(); - var contract = ser.Contract; - if (contract == null) - { - ser.UpdateContract(); - } - - IDs = GetIDs(ser); - Images = GetDefaultImages(ctx, ser, randomiseImages); - AirsOn = animeType == AniDBAnimeType.TVSeries || animeType == AniDBAnimeType.Web ? ser.GetAirsOnDaysOfWeek(ael) : new(); - - Name = ser.GetSeriesName(); - Sizes = ModelHelper.GenerateSeriesSizes(ael, uid); - Size = Sizes.Local.Credits + Sizes.Local.Episodes + Sizes.Local.Others + Sizes.Local.Parodies + - Sizes.Local.Specials + Sizes.Local.Trailers; - - Created = ser.DateTimeCreated.ToUniversalTime(); - Updated = ser.DateTimeUpdated.ToUniversalTime(); - - if (includeDataFrom?.Contains(DataSource.AniDB) ?? false) - this._AniDB = new Series.AniDBWithDate(anime, ser); - if (includeDataFrom?.Contains(DataSource.TvDB) ?? false) - this._TvDB = GetTvDBInfo(ctx, ser); - } - - private void AddBasicAniDBInfo(HttpContext ctx, SVR_AnimeSeries series, SVR_AniDB_Anime anime) - { - if (anime == null) - { - return; - } - - Links = new(); - if (!string.IsNullOrEmpty(anime.Site_EN)) - foreach (var site in anime.Site_EN.Split('|')) - Links.Add(new() { Type = "source", Name = "Official Site (EN)", URL = site }); - - if (!string.IsNullOrEmpty(anime.Site_JP)) - foreach (var site in anime.Site_JP.Split('|')) - Links.Add(new() { Type = "source", Name = "Official Site (JP)", URL = site }); - - if (!string.IsNullOrEmpty(anime.Wikipedia_ID)) - Links.Add(new() { Type = "wiki", Name = "Wikipedia (EN)", URL = $"https://en.wikipedia.org/{anime.Wikipedia_ID}" }); - - if (!string.IsNullOrEmpty(anime.WikipediaJP_ID)) - Links.Add(new() { Type = "wiki", Name = "Wikipedia (JP)", URL = $"https://en.wikipedia.org/{anime.WikipediaJP_ID}" }); - - if (!string.IsNullOrEmpty(anime.CrunchyrollID)) - Links.Add(new() { Type = "streaming", Name = "Crunchyroll", URL = $"https://crunchyroll.com/anime/{anime.CrunchyrollID}" }); - - if (!string.IsNullOrEmpty(anime.FunimationID)) - Links.Add(new() { Type = "streaming", Name = "Funimation", URL = anime.FunimationID }); - - if (!string.IsNullOrEmpty(anime.HiDiveID)) - Links.Add(new() { Type = "streaming", Name = "HiDive", URL = $"https://www.hidive.com/{anime.HiDiveID}" }); - - if (anime.AllCinemaID.HasValue && anime.AllCinemaID.Value > 0) - Links.Add(new() { Type = "foreign-metadata", Name = "allcinema", URL = $"https://allcinema.net/cinema/{anime.AllCinemaID.Value}" }); - - if (anime.AnisonID.HasValue && anime.AnisonID.Value > 0) - Links.Add(new() { Type = "foreign-metadata", Name = "Anison", URL = $"https://anison.info/data/program/{anime.AnisonID.Value}.html" }); - - if (anime.SyoboiID.HasValue && anime.SyoboiID.Value > 0) - Links.Add(new() { Type = "foreign-metadata", Name = "syoboi", URL = $"https://cal.syoboi.jp/tid/{anime.SyoboiID.Value}/time" }); - - if (anime.BangumiID.HasValue && anime.BangumiID.Value > 0) - Links.Add(new() { Type = "foreign-metadata", Name = "bangumi", URL = $"https://bgm.tv/subject/{anime.BangumiID.Value}" }); - - if (anime.LainID.HasValue && anime.LainID.Value > 0) - Links.Add(new() { Type = "foreign-metadata", Name = ".lain", URL = $"http://lain.gr.jp/mediadb/media/{anime.LainID.Value}" }); - - if (anime.ANNID.HasValue && anime.ANNID.Value > 0) - Links.Add(new() { Type = "english-metadata", Name = "AnimeNewsNetwork", URL = $"https://www.animenewsnetwork.com/encyclopedia/anime.php?id={anime.ANNID.Value}" }); - - if (anime.VNDBID.HasValue && anime.VNDBID.Value > 0) - Links.Add(new() { Type = "english-metadata", Name = "VNDB", URL = $"https://vndb.org/v{anime.VNDBID.Value}" }); - - var vote = RepoFactory.AniDB_Vote.GetByEntityAndType(anime.AnimeID, AniDBVoteType.Anime) ?? - RepoFactory.AniDB_Vote.GetByEntityAndType(anime.AnimeID, AniDBVoteType.AnimeTemp); - if (vote != null) - { - var voteType = (AniDBVoteType)vote.VoteType == AniDBVoteType.Anime ? "Permanent" : "Temporary"; - UserRating = new Rating - { - Value = (decimal)Math.Round(vote.VoteValue / 100D, 1), - MaxValue = 10, - Type = voteType, - Source = "User" - }; - } - } - - public static bool QueueAniDBRefresh(ICommandRequestFactory commandFactory, IHttpConnectionHandler handler, - int animeID, bool force, bool downloadRelations, bool createSeriesEntry, bool immediate = false, - bool cacheOnly = false) - { - if (force) - return QueueForcedAniDBRefresh(commandFactory, handler, animeID, downloadRelations, createSeriesEntry, immediate); - - var command = commandFactory.Create<CommandRequest_GetAnimeHTTP>(c => - { - c.AnimeID = animeID; - c.DownloadRelations = downloadRelations; - c.ForceRefresh = force; - c.CacheOnly = !force && cacheOnly; - c.CreateSeriesEntry = createSeriesEntry; - c.BubbleExceptions = immediate; - }); - if (immediate) - { - try - { - command.ProcessCommand(); - } - catch - { - return false; - } - - return command.Result != null; - } - - commandFactory.Save(command); - return false; - } - - private static bool QueueForcedAniDBRefresh(ICommandRequestFactory commandFactory, IHttpConnectionHandler handler, - int animeID, bool downloadRelations, bool createSeriesEntry, bool immediate = false) - { - var command = commandFactory.Create<CommandRequest_GetAnimeHTTP_Force>(c => - { - c.AnimeID = animeID; - c.DownloadRelations = downloadRelations; - c.CreateSeriesEntry = createSeriesEntry; - c.BubbleExceptions = immediate; - }); - if (immediate && !handler.IsBanned) - { - try - { - command.ProcessCommand(); - } - catch - { - return false; - } - - return command.Result != null; - } - - commandFactory.Save(command); - return false; - } - - public static bool QueueTvDBRefresh(ICommandRequestFactory commandFactory, int tvdbID, bool force, bool immediate = false) - { - var command = commandFactory.Create<CommandRequest_TvDBUpdateSeries>(c => - { - c.TvDBSeriesID = tvdbID; - c.ForceRefresh = force; - c.BubbleExceptions = immediate; - }); - if (immediate) - { - try - { - command.ProcessCommand(); - } - catch - { - return false; - } - - return command.Result != null; - } - - commandFactory.Save(command); - return false; - } - - public static SeriesIDs GetIDs(SVR_AnimeSeries ser) - { - // Shoko - var ids = new SeriesIDs - { - ID = ser.AnimeSeriesID, - ParentGroup = ser.AnimeGroupID, - TopLevelGroup = ser.TopLevelAnimeGroup.AnimeGroupID - }; - - // AniDB - var anidbId = ser.GetAnime()?.AnimeID; - if (anidbId.HasValue) - { - ids.AniDB = anidbId.Value; - } - - // TvDB - var tvdbIds = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(ser.AniDB_ID); - if (tvdbIds.Any()) - { - ids.TvDB.AddRange(tvdbIds.Select(a => a.TvDBID).Distinct()); - } - - // TODO: Cache the rest of these, so that they don't severely slow down the API - - // TMDB - // TODO: make this able to support more than one, in fact move it to its own and remove CrossRef_Other - var tmdbId = RepoFactory.CrossRef_AniDB_Other.GetByAnimeIDAndType(ser.AniDB_ID, CrossRefType.MovieDB); - if (tmdbId != null && int.TryParse(tmdbId.CrossRefID, out var movieID)) - { - ids.TMDB.Add(movieID); - } - - // Trakt - // var traktIds = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(ser.AniDB_ID).Select(a => a.TraktID) - // .Distinct().ToList(); - // if (traktIds.Any()) ids.TraktTv.AddRange(traktIds); - - // MAL - var malIds = RepoFactory.CrossRef_AniDB_MAL.GetByAnimeID(ser.AniDB_ID).Select(a => a.MALID).Distinct() - .ToList(); - if (malIds.Any()) - { - ids.MAL.AddRange(malIds); - } - - // TODO: AniList later - return ids; - } - - public static Image GetDefaultImage(int anidbId, ImageSizeType imageSizeType, - ImageEntityType? imageEntityType = null) - { - var defaultImage = imageEntityType.HasValue - ? RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(anidbId, - imageSizeType, imageEntityType.Value) - : RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeType(anidbId, imageSizeType); - return defaultImage != null - ? new Image(defaultImage.ImageParentID, (ImageEntityType)defaultImage.ImageParentType, true) - : null; - } - - public static Images GetDefaultImages(HttpContext ctx, SVR_AnimeSeries ser, bool randomiseImages = false) - { - var images = new Images(); - var random = ctx.Items["Random"] as Random; - var allImages = GetArt(ctx, ser.AniDB_ID); - - var poster = randomiseImages - ? allImages.Posters.GetRandomElement(random) - : GetDefaultImage(ser.AniDB_ID, ImageSizeType.Poster) ?? allImages.Posters.FirstOrDefault(); - if (poster != null) - { - images.Posters.Add(poster); - } - - var fanart = randomiseImages - ? allImages.Fanarts.GetRandomElement(random) - : GetDefaultImage(ser.AniDB_ID, ImageSizeType.Fanart) ?? allImages.Fanarts.FirstOrDefault(); - if (fanart != null) - { - images.Fanarts.Add(fanart); - } - - var banner = randomiseImages - ? allImages.Banners.GetRandomElement(random) - : GetDefaultImage(ser.AniDB_ID, ImageSizeType.WideBanner) ?? allImages.Banners.FirstOrDefault(); - if (banner != null) - { - images.Banners.Add(banner); - } - - return images; - } - - /// <summary> - /// Cast is aggregated, and therefore not in each provider - /// </summary> - /// <param name="animeID"></param> - /// <param name="roleTypes"></param> - /// <returns></returns> - public static List<Role> GetCast(int animeID, HashSet<Role.CreatorRoleType> roleTypes = null) - { - var roles = new List<Role>(); - var xrefAnimeStaff = RepoFactory.CrossRef_Anime_Staff.GetByAnimeID(animeID); - foreach (var xref in xrefAnimeStaff) - { - // Filter out any roles that are not of the desired type. - if (roleTypes != null && !roleTypes.Contains((Role.CreatorRoleType)xref.RoleType)) - continue; - - var character = xref.RoleID.HasValue ? RepoFactory.AnimeCharacter.GetByID(xref.RoleID.Value) : null; - var staff = RepoFactory.AnimeStaff.GetByID(xref.StaffID); - if (staff == null) - continue; - - var role = new Role - { - Character = - character != null - ? new Role.Person - { - Name = character.Name, - AlternateName = character.AlternateName, - Image = new Image(character.CharacterID, ImageEntityType.Character), - Description = character.Description - } - : null, - Staff = new Role.Person - { - Name = staff.Name, - AlternateName = staff.AlternateName, - Description = staff.Description, - Image = staff.ImagePath != null ? new Image(staff.StaffID, ImageEntityType.Staff) : null - }, - RoleName = (Role.CreatorRoleType)xref.RoleType, - RoleDetails = xref.Role - }; - roles.Add(role); - } - - return roles; - } - - public static List<Tag> GetTags(SVR_AniDB_Anime anime, TagFilter.Filter filter, - bool excludeDescriptions = false, bool orderByName = false, bool onlyVerified = true) - { - // Only get the user tags if we don't exclude it (false == false), or if we invert the logic and want to include it (true == true). - IEnumerable<Tag> userTags = new List<Tag>(); - if (filter.HasFlag(TagFilter.Filter.User) == filter.HasFlag(TagFilter.Filter.Invert)) - userTags = RepoFactory.CustomTag.GetByAnimeID(anime.AnimeID) - .Select(tag => new Tag(tag, excludeDescriptions)); - - var selectedTags = anime.GetAniDBTags(onlyVerified) - .DistinctBy(a => a.TagName) - .ToList(); - var tagFilter = new TagFilter<AniDB_Tag>(name => RepoFactory.AniDB_Tag.GetByName(name).FirstOrDefault(), tag => tag.TagName, - name => new AniDB_Tag { TagNameSource = name }); - var anidbTags = tagFilter - .ProcessTags(filter, selectedTags) - .Select(tag => - { - var xref = RepoFactory.AniDB_Anime_Tag.GetByTagID(tag.TagID).FirstOrDefault(xref => xref.AnimeID == anime.AnimeID); - return new Tag(tag, excludeDescriptions) { Weight = xref?.Weight ?? 0, IsLocalSpoiler = xref?.LocalSpoiler }; - }); - - if (orderByName) - return userTags.Concat(anidbTags) - .OrderByDescending(tag => tag.Source) - .ThenBy(tag => tag.Name) - .ToList(); - - return userTags.Concat(anidbTags) - .OrderByDescending(tag => tag.Source) - .ThenByDescending(tag => tag.Weight) - .ThenBy(tag => tag.Name) - .ToList(); - } - - public static SeriesType GetAniDBSeriesType(int? animeType) - { - return animeType.HasValue ? GetAniDBSeriesType((AniDBAnimeType)animeType.Value) : SeriesType.Unknown; - } - - public static SeriesType GetAniDBSeriesType(AniDBAnimeType animeType) - { - switch (animeType) - { - default: - case AniDBAnimeType.None: - return SeriesType.Unknown; - case AniDBAnimeType.TVSeries: - return SeriesType.TV; - case AniDBAnimeType.Movie: - return SeriesType.Movie; - case AniDBAnimeType.OVA: - return SeriesType.OVA; - case AniDBAnimeType.TVSpecial: - return SeriesType.TVSpecial; - case AniDBAnimeType.Web: - return SeriesType.Web; - case AniDBAnimeType.Other: - return SeriesType.Other; - } - } - - public static List<TvDB> GetTvDBInfo(HttpContext ctx, SVR_AnimeSeries ser) - { - var ael = ser.GetAnimeEpisodes(true); - return RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(ser.AniDB_ID) - .Select(xref => RepoFactory.TvDB_Series.GetByTvDBID(xref.TvDBID)) - .Select(tvdbSer => new TvDB(ctx, tvdbSer, ser, ael)) - .ToList(); - } - - public static void AddSeriesVote(ICommandRequestFactory commandFactory, SVR_AnimeSeries ser, int userID, Vote vote) - { - var voteType = (vote.Type?.ToLowerInvariant() ?? "") switch - { - "temporary" => (int)AniDBVoteType.AnimeTemp, - "permanent" => (int)AniDBVoteType.Anime, - _ => ser.GetAnime()?.GetFinishedAiring() ?? false ? (int)AniDBVoteType.Anime : (int)AniDBVoteType.AnimeTemp - }; - - var dbVote = RepoFactory.AniDB_Vote.GetByEntityAndType(ser.AniDB_ID, AniDBVoteType.AnimeTemp) ?? - RepoFactory.AniDB_Vote.GetByEntityAndType(ser.AniDB_ID, AniDBVoteType.Anime); - - if (dbVote == null) - { - dbVote = new AniDB_Vote { EntityID = ser.AniDB_ID }; - } - - dbVote.VoteValue = (int)Math.Floor(vote.GetRating(1000)); - dbVote.VoteType = voteType; - - RepoFactory.AniDB_Vote.Save(dbVote); - - commandFactory.CreateAndSave<CommandRequest_VoteAnime>( - c => - { - c.AnimeID = ser.AniDB_ID; - c.VoteType = voteType; - c.VoteValue = vote.GetRating(); - } - ); - } - - public static Images GetArt(HttpContext ctx, int animeID, bool includeDisabled = false) - { - var images = new Images(); - AddAniDBPoster(ctx, images, animeID); - AddTvDBImages(ctx, images, animeID, includeDisabled); - // AddMovieDBImages(ctx, images, animeID, includeDisabled); - return images; - } - - private static void AddAniDBPoster(HttpContext ctx, Images images, int animeID) - { - images.Posters.Add(GetAniDBPoster(animeID)); - } - - public static Image GetAniDBPoster(int animeID) - { - var defaultImage = RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeType(animeID, - ImageSizeType.Poster); - var preferred = defaultImage != null && defaultImage.ImageParentType == (int)ImageEntityType.AniDB_Cover; - return new Image(animeID, ImageEntityType.AniDB_Cover, preferred); - } - - private static void AddTvDBImages(HttpContext ctx, Images images, int animeID, bool includeDisabled = false) - { - var tvdbIDs = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(animeID).ToList(); - - var defaultFanart = - RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, - ImageSizeType.Fanart, ImageEntityType.TvDB_FanArt); - var fanarts = tvdbIDs.SelectMany(a => RepoFactory.TvDB_ImageFanart.GetBySeriesID(a.TvDBID)).ToList(); - images.Fanarts.AddRange(fanarts.Where(a => includeDisabled || a.Enabled != 0).Select(a => - { - var preferred = defaultFanart != null && defaultFanart.ImageParentID == a.TvDB_ImageFanartID; - return new Image(a.TvDB_ImageFanartID, ImageEntityType.TvDB_FanArt, preferred, a.Enabled == 0); - })); - - var defaultBanner = - RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, - ImageSizeType.WideBanner, ImageEntityType.TvDB_Banner); - var banners = tvdbIDs.SelectMany(a => RepoFactory.TvDB_ImageWideBanner.GetBySeriesID(a.TvDBID)).ToList(); - images.Banners.AddRange(banners.Where(a => includeDisabled || a.Enabled != 0).Select(a => - { - var preferred = defaultBanner != null && defaultBanner.ImageParentID == a.TvDB_ImageWideBannerID; - return new Image(a.TvDB_ImageWideBannerID, ImageEntityType.TvDB_Banner, preferred, a.Enabled == 0); - })); - - var defaultPoster = - RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, - ImageSizeType.Poster, ImageEntityType.TvDB_Cover); - var posters = tvdbIDs.SelectMany(a => RepoFactory.TvDB_ImagePoster.GetBySeriesID(a.TvDBID)).ToList(); - images.Posters.AddRange(posters.Where(a => includeDisabled || a.Enabled != 0).Select(a => - { - var preferred = defaultPoster != null && defaultPoster.ImageParentID == a.TvDB_ImagePosterID; - return new Image(a.TvDB_ImagePosterID, ImageEntityType.TvDB_Cover, preferred, a.Enabled == 0); - })); - } - - private static void AddMovieDBImages(HttpContext ctx, Images images, int animeID, bool includeDisabled = false) - { - var moviedb = RepoFactory.CrossRef_AniDB_Other.GetByAnimeIDAndType(animeID, CrossRefType.MovieDB); - - var moviedbPosters = moviedb == null - ? new List<MovieDB_Poster>() - : RepoFactory.MovieDB_Poster.GetByMovieID(int.Parse(moviedb.CrossRefID)); - var defaultPoster = - RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, - ImageSizeType.Poster, ImageEntityType.MovieDB_Poster); - images.Posters.AddRange(moviedbPosters.Where(a => includeDisabled || a.Enabled != 0).Select(a => - { - var preferred = defaultPoster != null && defaultPoster.ImageParentID == a.MovieDB_PosterID; - return new Image(a.MovieDB_PosterID, ImageEntityType.MovieDB_Poster, preferred, a.Enabled == 1); - })); - - var moviedbFanarts = moviedb == null - ? new List<MovieDB_Fanart>() - : RepoFactory.MovieDB_Fanart.GetByMovieID(int.Parse(moviedb.CrossRefID)); - var defaultFanart = - RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, - ImageSizeType.Fanart, ImageEntityType.MovieDB_FanArt); - images.Fanarts.AddRange(moviedbFanarts.Where(a => includeDisabled || a.Enabled != 0).Select(a => - { - var preferred = defaultFanart != null && defaultFanart.ImageParentID == a.MovieDB_FanartID; - return new Image(a.MovieDB_FanartID, ImageEntityType.MovieDB_FanArt, preferred, a.Enabled == 1); - })); - } - - #endregion - /// <summary> /// Auto-matching settings for the series. /// </summary> @@ -712,214 +173,6 @@ public AutoMatchSettings MergeWithExisting(SVR_AnimeSeries series) /// </summary> public class AniDB { - public AniDB() { } - - public AniDB(SVR_AniDB_Anime anime, bool includeTitles) : this(anime, null, includeTitles) { } - - public AniDB(SVR_AniDB_Anime anime, SVR_AnimeSeries series = null, bool includeTitles = true) - { - series ??= RepoFactory.AnimeSeries.GetByAnimeID(anime.AnimeID); - ID = anime.AnimeID; - ShokoID = series?.AnimeSeriesID; - Type = GetAniDBSeriesType(anime.AnimeType); - Title = series?.GetSeriesName() ?? anime.PreferredTitle; - Titles = includeTitles - ? anime.GetTitles().Select(title => new Title - { - Name = title.Title, - Language = title.LanguageCode, - Type = title.TitleType, - Default = string.Equals(title.Title, Title), - Source = "AniDB" - } - ).ToList() - : null; - Description = anime.Description; - Restricted = anime.Restricted == 1; - Poster = GetAniDBPoster(anime.AnimeID); - EpisodeCount = anime.EpisodeCountNormal; - Rating = new Rating { Source = "AniDB", Value = anime.Rating, MaxValue = 1000, Votes = anime.VoteCount }; - UserApproval = null; - Relation = null; - } - - public AniDB(ResponseAniDBTitles.Anime result, bool includeTitles) : this(result, null, includeTitles) { } - - public AniDB(ResponseAniDBTitles.Anime result, SVR_AnimeSeries series = null, bool includeTitles = false) - { - if (series == null) - { - series = RepoFactory.AnimeSeries.GetByAnimeID(result.AnimeID); - } - - var anime = series != null ? series.GetAnime() : RepoFactory.AniDB_Anime.GetByAnimeID(result.AnimeID); - - ID = result.AnimeID; - ShokoID = series?.AnimeSeriesID; - Type = GetAniDBSeriesType(anime?.AnimeType); - Title = series?.GetSeriesName() ?? anime?.PreferredTitle ?? result.MainTitle; - Titles = includeTitles - ? result.Titles.Select(title => new Title - { - Language = title.LanguageCode, - Name = title.Title, - Type = title.TitleType, - Default = string.Equals(title.Title, Title), - Source = "AniDB" - } - ).ToList() - : null; - Description = anime?.Description; - Restricted = anime is { Restricted: 1 }; - EpisodeCount = anime?.EpisodeCount; - Poster = GetAniDBPoster(result.AnimeID); - } - - public AniDB(SVR_AniDB_Anime_Relation relation, bool includeTitles) : this(relation, null, includeTitles) { } - - public AniDB(SVR_AniDB_Anime_Relation relation, SVR_AnimeSeries series = null, bool includeTitles = true) - { - series ??= RepoFactory.AnimeSeries.GetByAnimeID(relation.RelatedAnimeID); - ID = relation.RelatedAnimeID; - ShokoID = series?.AnimeSeriesID; - SetTitles(relation, series, includeTitles); - Poster = GetAniDBPoster(relation.RelatedAnimeID); - Rating = null; - UserApproval = null; - Relation = SeriesRelation.GetRelationTypeFromAnidbRelationType(relation.RelationType); - } - - private void SetTitles(AniDB_Anime_Relation relation, SVR_AnimeSeries series, bool includeTitles) - { - var anime = RepoFactory.AniDB_Anime.GetByAnimeID(relation.RelatedAnimeID); - if (anime is not null) - { - Type = GetAniDBSeriesType(anime.AnimeType); - Title = series?.GetSeriesName() ?? anime.PreferredTitle; - Titles = includeTitles - ? anime.GetTitles().Select( - title => new Title - { - Name = title.Title, - Language = title.LanguageCode, - Type = title.TitleType, - Default = string.Equals(title.Title, Title), - Source = "AniDB" - } - ).ToList() - : null; - Description = anime.Description; - Restricted = anime.Restricted == 1; - EpisodeCount = anime.EpisodeCountNormal; - return; - } - - var result = Utils.AniDBTitleHelper.SearchAnimeID(relation.RelatedAnimeID); - if (result != null) - { - Type = SeriesType.Unknown; - Title = result.PreferredTitle; - Titles = includeTitles - ? result.Titles.Select( - title => new Title - { - Language = title.LanguageCode, - Name = title.Title, - Type = title.TitleType, - Default = string.Equals(title.Title, Title), - Source = "AniDB" - } - ).ToList() - : null; - Description = null; - // If the other anime is present we assume they're of the same kind. Be it restricted or unrestricted. - anime = RepoFactory.AniDB_Anime.GetByAnimeID(relation.AnimeID); - Restricted = anime is not null && anime.Restricted == 1; - return; - } - - Type = SeriesType.Unknown; - Titles = includeTitles ? new List<Title>() : null; - Restricted = false; - } - - public AniDB(AniDB_Anime_Similar similar, bool includeTitles) : this(similar, null, includeTitles) { } - - public AniDB(AniDB_Anime_Similar similar, SVR_AnimeSeries series = null, bool includeTitles = true) - { - series ??= RepoFactory.AnimeSeries.GetByAnimeID(similar.SimilarAnimeID); - ID = similar.SimilarAnimeID; - ShokoID = series?.AnimeSeriesID; - SetTitles(similar, series, includeTitles); - Poster = GetAniDBPoster(similar.SimilarAnimeID); - Rating = null; - UserApproval = new Rating - { - Value = new Vote(similar.Approval, similar.Total).GetRating(100), - MaxValue = 100, - Votes = similar.Total, - Source = "AniDB", - Type = "User Approval" - }; - Relation = null; - Restricted = false; - } - - private void SetTitles(AniDB_Anime_Similar similar, SVR_AnimeSeries series, bool includeTitles) - { - var anime = RepoFactory.AniDB_Anime.GetByAnimeID(similar.SimilarAnimeID); - if (anime is not null) - { - Type = GetAniDBSeriesType(anime.AnimeType); - Title = series?.GetSeriesName() ?? anime.PreferredTitle; - Titles = includeTitles - ? anime.GetTitles().Select( - title => new Title - { - Name = title.Title, - Language = title.LanguageCode, - Type = title.TitleType, - Default = string.Equals(title.Title, Title), - Source = "AniDB" - } - ).ToList() - : null; - Description = anime.Description; - Restricted = anime.Restricted == 1; - return; - } - - var result = Utils.AniDBTitleHelper.SearchAnimeID(similar.SimilarAnimeID); - if (result != null) - { - Type = SeriesType.Unknown; - Title = result.PreferredTitle; - Titles = includeTitles - ? result.Titles.Select( - title => new Title - { - Language = title.LanguageCode, - Name = title.Title, - Type = title.TitleType, - Default = string.Equals(title.Title, Title), - Source = "AniDB" - } - ).ToList() - : null; - Description = null; - // If the other anime is present we assume they're of the same kind. Be it restricted or unrestricted. - anime = RepoFactory.AniDB_Anime.GetByAnimeID(similar.AnimeID); - Restricted = anime is not null && anime.Restricted == 1; - return; - } - - Type = SeriesType.Unknown; - Title = null; - Titles = includeTitles ? new List<Title>() : null; - Description = null; - Restricted = false; - } - /// <summary> /// AniDB ID /// </summary> @@ -958,6 +211,19 @@ private void SetTitles(AniDB_Anime_Similar similar, SVR_AnimeSeries series, bool [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string Description { get; set; } + /// <summary> + /// Air date (2013-02-27, shut up avael). Anything without an air date is going to be missing a lot of info. + /// </summary> + [Required] + [JsonConverter(typeof(DateFormatConverter), "yyyy-MM-dd")] + public DateTime? AirDate { get; set; } + + /// <summary> + /// End date, can be omitted. Omitted means that it's still airing (2013-02-27) + /// </summary> + [JsonConverter(typeof(DateFormatConverter), "yyyy-MM-dd")] + public DateTime? EndDate { get; set; } + /// <summary> /// Restricted content. Mainly porn. /// </summary> @@ -994,39 +260,6 @@ private void SetTitles(AniDB_Anime_Similar similar, SVR_AnimeSeries series, bool public RelationType? Relation { get; set; } } - /// <summary> - /// The AniDB Data model for series - /// </summary> - public class AniDBWithDate : AniDB - { - public AniDBWithDate(SVR_AniDB_Anime anime, SVR_AnimeSeries series = null) : base(anime, - series) - { - if (anime.AirDate.HasValue) - { - AirDate = anime.AirDate.Value; - } - - if (anime.EndDate.HasValue) - { - EndDate = anime.EndDate.Value; - } - } - - /// <summary> - /// Air date (2013-02-27, shut up avael). Anything without an air date is going to be missing a lot of info. - /// </summary> - [Required] - [JsonConverter(typeof(DateFormatConverter), "yyyy-MM-dd")] - public DateTime? AirDate { get; set; } - - /// <summary> - /// End date, can be omitted. Omitted means that it's still airing (2013-02-27) - /// </summary> - [JsonConverter(typeof(DateFormatConverter), "yyyy-MM-dd")] - public DateTime? EndDate { get; set; } - } - /// <summary> /// The result entries for the "Recommended For You" algorithm. /// </summary> @@ -1035,7 +268,7 @@ public class AniDBRecommendedForYou /// <summary> /// The recommended AniDB entry. /// </summary> - public AniDBWithDate Anime; + public AniDB Anime; /// <summary> /// Number of similar anime that resulted in this recommendation. @@ -1048,45 +281,6 @@ public class AniDBRecommendedForYou /// </summary> public class TvDB { - public TvDB(HttpContext ctx, TvDB_Series tbdbSeries, SVR_AnimeSeries series, - List<SVR_AnimeEpisode> episodeList = null) - { - if (episodeList == null) - { - episodeList = series.GetAnimeEpisodes(true); - } - - ID = tbdbSeries.SeriesID; - Description = tbdbSeries.Overview; - Title = tbdbSeries.SeriesName; - if (tbdbSeries.Rating != null) - { - Rating = new Rating { Source = "TvDB", Value = tbdbSeries.Rating.Value, MaxValue = 10 }; - } - - var images = new Images(); - AddTvDBImages(ctx, images, series.AniDB_ID); - Posters = images.Posters; - Fanarts = images.Fanarts; - Banners = images.Banners; - - // Aggregate stuff - var firstEp = episodeList.FirstOrDefault(a => - a.AniDB_Episode != null && a.AniDB_Episode.EpisodeType == (int)AniDBEpisodeType.Episode && - a.AniDB_Episode.EpisodeNumber == 1) - ?.TvDBEpisode; - - var lastEp = episodeList - .Where(a => a.AniDB_Episode != null && a.AniDB_Episode.EpisodeType == (int)AniDBEpisodeType.Episode) - .OrderBy(a => a.AniDB_Episode.EpisodeType) - .ThenBy(a => a.AniDB_Episode.EpisodeNumber).LastOrDefault() - ?.TvDBEpisode; - - Season = firstEp?.SeasonNumber; - AirDate = firstEp?.AirDate; - EndDate = lastEp?.AirDate; - } - /// <summary> /// TvDB ID /// </summary> @@ -1285,15 +479,6 @@ public class SeriesSearchResult : Series /// Contains the original matched substring from the original string. /// </summary> public string Match { get; set; } = string.Empty; - - public SeriesSearchResult(HttpContext ctx, SeriesSearch.SearchResult<SVR_AnimeSeries> result) : base(ctx, result.Result) - { - ExactMatch = result.ExactMatch; - Index = result.Index; - Distance = result.Distance; - LengthDifference = result.LengthDifference; - Match = result.Match; - } } public enum SeriesType diff --git a/Shoko.Server/API/v3/Models/Shoko/WebUI.cs b/Shoko.Server/API/v3/Models/Shoko/WebUI.cs index 3ac10133d..acaac74fa 100644 --- a/Shoko.Server/API/v3/Models/Shoko/WebUI.cs +++ b/Shoko.Server/API/v3/Models/Shoko/WebUI.cs @@ -94,35 +94,6 @@ public WebUITheme(WebUIThemeProvider.ThemeDefinition definition, bool withCSS = public class WebUIGroupExtra { - public WebUIGroupExtra(SVR_AnimeGroup group, SVR_AnimeSeries series, SVR_AniDB_Anime anime, - TagFilter.Filter filter = TagFilter.Filter.None, bool orderByName = false, int tagLimit = 30) - { - ID = group.AnimeGroupID; - Type = Series.GetAniDBSeriesType(anime.AnimeType); - Rating = new Rating { Source = "AniDB", Value = anime.Rating, MaxValue = 1000, Votes = anime.VoteCount }; - if (anime.AirDate != null) - { - var airdate = anime.AirDate.Value; - if (airdate != DateTime.MinValue) - { - AirDate = airdate; - } - } - - if (anime.EndDate != null) - { - var enddate = anime.EndDate.Value; - if (enddate != DateTime.MinValue) - { - EndDate = enddate; - } - } - - Tags = Series.GetTags(anime, filter, excludeDescriptions: true, orderByName) - .Take(tagLimit) - .ToList(); - } - /// <summary> /// Shoko Group ID. /// </summary> @@ -183,47 +154,6 @@ public class WebUISeriesExtra /// The inferred source material for the series. /// </summary> public string SourceMaterial { get; set; } - - public WebUISeriesExtra(HttpContext ctx, SVR_AnimeSeries series) - { - var anime = series.GetAnime(); - var cast = Series.GetCast(anime.AnimeID, new () { Role.CreatorRoleType.Studio, Role.CreatorRoleType.Producer }); - - FirstAirSeason = GetFirstAiringSeasonGroupFilter(ctx, anime); - Studios = cast - .Where(role => role.RoleName == Role.CreatorRoleType.Studio) - .Select(role => role.Staff) - .ToList(); - Producers = cast - .Where(role => role.RoleName == Role.CreatorRoleType.Producer) - .Select(role => role.Staff) - .ToList(); - SourceMaterial = Series.GetTags(anime, TagFilter.Filter.Invert | TagFilter.Filter.Source, excludeDescriptions: true) - .FirstOrDefault()?.Name ?? "Original Work"; - } - - private Filter GetFirstAiringSeasonGroupFilter(HttpContext ctx, SVR_AniDB_Anime anime) - { - var type = (AnimeType)anime.AnimeType; - if (type != AnimeType.TVSeries && type != AnimeType.Web) - return null; - - var (year, season) = anime.GetSeasons() - .FirstOrDefault(); - if (year == 0) - return null; - - var seasonName = $"{season} {year}"; - var seasonsFilterID = RepoFactory.Filter.GetTopLevel() - .FirstOrDefault(f => f.FilterType == (GroupFilterType.Directory | GroupFilterType.Season))?.FilterID; - if (seasonsFilterID == null) return null; - var firstAirSeason = RepoFactory.Filter.GetByParentID(seasonsFilterID.Value) - .FirstOrDefault(f => f.Name == seasonName); - if (firstAirSeason == null) - return null; - - return new Filter(ctx, firstAirSeason); - } } public class WebUISeriesFileSummary diff --git a/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs b/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs index 3d1c50377..8cfca825f 100644 --- a/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs +++ b/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs @@ -27,7 +27,7 @@ protected override void Process() if (GroupFilterID == 0) { RepoFactory.GroupFilter.CreateOrVerifyLockedFilters(); - RepoFactory.Filter.CreateOrVerifyLockedFilters(); + RepoFactory.FilterPreset.CreateOrVerifyLockedFilters(); return; } diff --git a/Shoko.Server/Databases/BaseDatabase.cs b/Shoko.Server/Databases/BaseDatabase.cs index 246da07c2..0425595d3 100644 --- a/Shoko.Server/Databases/BaseDatabase.cs +++ b/Shoko.Server/Databases/BaseDatabase.cs @@ -281,13 +281,13 @@ public void PopulateInitialData() public void CreateOrVerifyLockedFilters() { RepoFactory.GroupFilter.CreateOrVerifyLockedFilters(); - RepoFactory.Filter.CreateOrVerifyLockedFilters(); + RepoFactory.FilterPreset.CreateOrVerifyLockedFilters(); } private void CreateInitialGroupFilters() { RepoFactory.GroupFilter.CreateInitialGroupFilters(); - RepoFactory.Filter.CreateInitialFilters(); + RepoFactory.FilterPreset.CreateInitialFilters(); } private void CreateInitialUsers() diff --git a/Shoko.Server/Databases/MySQL.cs b/Shoko.Server/Databases/MySQL.cs index 1e1ea1211..e1f88f678 100644 --- a/Shoko.Server/Databases/MySQL.cs +++ b/Shoko.Server/Databases/MySQL.cs @@ -738,9 +738,9 @@ public class MySQL : BaseDatabase<MySqlConnection> new(118, 1, DatabaseFixes.FixAnimeSourceLinks), new(118, 2, DatabaseFixes.FixOrphanedShokoEpisodes), new DatabaseCommand(119, 1, - "CREATE TABLE Filter( FilterID INT NOT NULL AUTO_INCREMENT, ParentFilterID int, Name text NOT NULL, FilterType int NOT NULL, Locked bit NOT NULL, Hidden bit NOT NULL, ApplyAtSeriesLevel bit NOT NULL, Expression longtext, SortingExpression longtext, PRIMARY KEY (`FilterID`) ); "), + "CREATE TABLE FilterPreset( FilterPresetID INT NOT NULL AUTO_INCREMENT, ParentFilterPresetID int, Name text NOT NULL, FilterType int NOT NULL, Locked bit NOT NULL, Hidden bit NOT NULL, ApplyAtSeriesLevel bit NOT NULL, Expression longtext, SortingExpression longtext, PRIMARY KEY (`FilterID`) ); "), new DatabaseCommand(119, 2, - "ALTER TABLE Filter ADD INDEX IX_Filter_ParentFilterID (ParentFilterID); ALTER TABLE Filter ADD INDEX IX_Filter_Name (Name); ALTER TABLE Filter ADD INDEX IX_Filter_FilterType (FilterType); ALTER TABLE Filter ADD INDEX IX_Filter_LockedHidden (Locked, Hidden);"), + "ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_ParentFilterPresetID (ParentFilterPresetID); ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_Name (Name); ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_FilterType (FilterType); ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_LockedHidden (Locked, Hidden);"), }; private DatabaseCommand linuxTableVersionsFix = new("RENAME TABLE versions TO Versions;"); diff --git a/Shoko.Server/Databases/SQLServer.cs b/Shoko.Server/Databases/SQLServer.cs index 4944299e9..b7b997523 100644 --- a/Shoko.Server/Databases/SQLServer.cs +++ b/Shoko.Server/Databases/SQLServer.cs @@ -681,9 +681,9 @@ public override bool HasVersionsTable() new DatabaseCommand(111, 1, DatabaseFixes.FixAnimeSourceLinks), new DatabaseCommand(111, 2, DatabaseFixes.FixOrphanedShokoEpisodes), new DatabaseCommand(112, 1, - "CREATE TABLE Filter( FilterID INT IDENTITY(1,1), ParentFilterID int, Name nvarchar(max) NOT NULL, FilterType int NOT NULL, Locked bit NOT NULL, Hidden bit NOT NULL, ApplyAtSeriesLevel bit NOT NULL, Expression nvarchar(max), SortingExpression nvarchar(max) ); "), + "CREATE TABLE FilterPreset( FilterPresetID INT IDENTITY(1,1), ParentFilterPresetID int, Name nvarchar(max) NOT NULL, FilterType int NOT NULL, Locked bit NOT NULL, Hidden bit NOT NULL, ApplyAtSeriesLevel bit NOT NULL, Expression nvarchar(max), SortingExpression nvarchar(max) ); "), new DatabaseCommand(112, 2, - "CREATE INDEX IX_Filter_ParentFilterID ON Filter(ParentFilterID); CREATE INDEX IX_Filter_Name ON Filter(Name); CREATE INDEX IX_Filter_FilterType ON Filter(FilterType); CREATE INDEX IX_Filter_LockedHidden ON Filter(Locked, Hidden);"), + "CREATE INDEX IX_FilterPreset_ParentFilterPresetID ON FilterPreset(ParentFilterPresetID); CREATE INDEX IX_FilterPreset_Name ON FilterPreset(Name); CREATE INDEX IX_FilterPreset_FilterType ON FilterPreset(FilterType); CREATE INDEX IX_FilterPreset_LockedHidden ON FilterPreset(Locked, Hidden);"), }; private static Tuple<bool, string> DropDefaultsOnAnimeEpisode_User(object connection) diff --git a/Shoko.Server/Databases/SQLite.cs b/Shoko.Server/Databases/SQLite.cs index d4d60e426..a3081084e 100644 --- a/Shoko.Server/Databases/SQLite.cs +++ b/Shoko.Server/Databases/SQLite.cs @@ -672,9 +672,9 @@ public override void CreateDatabase() new(104, 1, DatabaseFixes.FixAnimeSourceLinks), new(104, 2, DatabaseFixes.FixOrphanedShokoEpisodes), new DatabaseCommand(105, 1, - "CREATE TABLE Filter( FilterID INTEGER PRIMARY KEY AUTOINCREMENT, ParentFilterID int, Name text NOT NULL, FilterType int NOT NULL, Locked int NOT NULL, Hidden int NOT NULL, ApplyAtSeriesLevel int NOT NULL, Expression text, SortingExpression text ); "), + "CREATE TABLE FilterPreset( FilterPresetID INTEGER PRIMARY KEY AUTOINCREMENT, ParentFilterPresetID int, Name text NOT NULL, FilterType int NOT NULL, Locked int NOT NULL, Hidden int NOT NULL, ApplyAtSeriesLevel int NOT NULL, Expression text, SortingExpression text ); "), new DatabaseCommand(105, 2, - "CREATE INDEX IX_Filter_ParentFilterID ON Filter(ParentFilterID); CREATE INDEX IX_Filter_Name ON Filter(Name); CREATE INDEX IX_Filter_FilterType ON Filter(FilterType); CREATE INDEX IX_Filter_LockedHidden ON Filter(Locked, Hidden);"), + "CREATE INDEX IX_FilterPreset_ParentFilterPresetID ON FilterPreset(ParentFilterPresetID); CREATE INDEX IX_FilterPreset_Name ON FilterPreset(Name); CREATE INDEX IX_FilterPreset_FilterType ON FilterPreset(FilterType); CREATE INDEX IX_FilterPreset_LockedHidden ON FilterPreset(Locked, Hidden);"), }; private static Tuple<bool, string> DropLanguage(object connection) diff --git a/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs b/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs index c81abf350..630830eef 100644 --- a/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs +++ b/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Files; -public class HasAudioLanguageExpression : FilterExpression<bool> +public class HasAudioLanguageExpression : FilterExpression<bool>, IWithStringParameter { public HasAudioLanguageExpression(string parameter) { diff --git a/Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs b/Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs index 760a95fe3..a793d5142 100644 --- a/Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs +++ b/Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Files; -public class HasSharedAudioLanguageExpression : FilterExpression<bool> +public class HasSharedAudioLanguageExpression : FilterExpression<bool>, IWithStringParameter { public HasSharedAudioLanguageExpression(string parameter) { diff --git a/Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs b/Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs index 42ea0eaee..788a91595 100644 --- a/Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs +++ b/Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Files; -public class HasSharedSubtitleLanguageExpression : FilterExpression<bool> +public class HasSharedSubtitleLanguageExpression : FilterExpression<bool>, IWithStringParameter { public HasSharedSubtitleLanguageExpression(string parameter) { diff --git a/Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs b/Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs index fb3f81f87..5c16cb0de 100644 --- a/Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs +++ b/Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Files; -public class HasSharedVideoSourceExpression : FilterExpression<bool> +public class HasSharedVideoSourceExpression : FilterExpression<bool>, IWithStringParameter { public HasSharedVideoSourceExpression(string parameter) { diff --git a/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs b/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs index 0a0e7457d..77c62643b 100644 --- a/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs +++ b/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Files; -public class HasSubtitleLanguageExpression : FilterExpression<bool> +public class HasSubtitleLanguageExpression : FilterExpression<bool>, IWithStringParameter { public HasSubtitleLanguageExpression(string parameter) { diff --git a/Shoko.Server/Filters/Files/HasVideoSourceExpression.cs b/Shoko.Server/Filters/Files/HasVideoSourceExpression.cs index a8a2041a3..9d7cfb3d5 100644 --- a/Shoko.Server/Filters/Files/HasVideoSourceExpression.cs +++ b/Shoko.Server/Filters/Files/HasVideoSourceExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Files; -public class HasVideoSourceExpression : FilterExpression<bool> +public class HasVideoSourceExpression : FilterExpression<bool>, IWithStringParameter { public HasVideoSourceExpression(string parameter) { diff --git a/Shoko.Server/Filters/FilterEvaluator.cs b/Shoko.Server/Filters/FilterEvaluator.cs index f7b766053..c5ad604e4 100644 --- a/Shoko.Server/Filters/FilterEvaluator.cs +++ b/Shoko.Server/Filters/FilterEvaluator.cs @@ -16,13 +16,13 @@ public class FilterEvaluator private readonly AnimeSeriesRepository _series = RepoFactory.AnimeSeries; /// <summary> - /// Evaluate the given filter, applying the necessary logic + /// Evaluate the given filter, applying the necessary logic /// </summary> /// <param name="filter"></param> /// <param name="userID"></param> - /// <returns>SeriesIDs, grouped by GroupID</returns> + /// <returns>SeriesIDs, grouped by the direct parent GroupID</returns> /// <exception cref="ArgumentNullException"></exception> - public IEnumerable<IGrouping<int, int>> EvaluateFilter(Filter filter, int? userID) + public IEnumerable<IGrouping<int, int>> EvaluateFilter(FilterPreset filter, int? userID) { ArgumentNullException.ThrowIfNull(filter); var user = filter.Expression?.UserDependent ?? false; @@ -54,7 +54,7 @@ public IEnumerable<IGrouping<int, int>> EvaluateFilter(Filter filter, int? userI return result; } - private static IOrderedEnumerable<FilterableWithID> OrderFilterables(Filter filter, IEnumerable<FilterableWithID> filtered) + private static IOrderedEnumerable<FilterableWithID> OrderFilterables(FilterPreset filter, IEnumerable<FilterableWithID> filtered) { var nameSorter = new NameSortingSelector(); var ordered = filter.SortingExpression == null ? filtered.OrderBy(a => nameSorter.Evaluate(a.Filterable)) : diff --git a/Shoko.Server/Filters/FilterExtensions.cs b/Shoko.Server/Filters/FilterExtensions.cs index 5a68d7b02..e3dea6db6 100644 --- a/Shoko.Server/Filters/FilterExtensions.cs +++ b/Shoko.Server/Filters/FilterExtensions.cs @@ -13,7 +13,7 @@ namespace Shoko.Server.Filters; public static class FilterExtensions { - public static bool IsDirectory(this Filter filter) => (filter.FilterType & GroupFilterType.Directory) != 0; + public static bool IsDirectory(this FilterPreset filter) => (filter.FilterType & GroupFilterType.Directory) != 0; public static Filterable ToFilterable(this SVR_AnimeSeries series) { diff --git a/Shoko.Server/Filters/Functions/DateAddFunction.cs b/Shoko.Server/Filters/Functions/DateAddFunction.cs index 30a02aae7..254014b38 100644 --- a/Shoko.Server/Filters/Functions/DateAddFunction.cs +++ b/Shoko.Server/Filters/Functions/DateAddFunction.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Functions; -public class DateAddFunction : FilterExpression<DateTime?> +public class DateAddFunction : FilterExpression<DateTime?>, IWithDateSelectorParameter, IWithTimeSpanParameter { public DateAddFunction() { @@ -20,6 +21,12 @@ public DateAddFunction(FilterExpression<DateTime?> selector, TimeSpan parameter) public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; + public FilterExpression<DateTime?> Left + { + get => Selector; + set => Selector = value; + } + public override DateTime? Evaluate(Filterable f) { return Selector.Evaluate(f) + Parameter; diff --git a/Shoko.Server/Filters/Functions/DateDiffFunction.cs b/Shoko.Server/Filters/Functions/DateDiffFunction.cs index 2132a6161..f67dab51a 100644 --- a/Shoko.Server/Filters/Functions/DateDiffFunction.cs +++ b/Shoko.Server/Filters/Functions/DateDiffFunction.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Functions; -public class DateDiffFunction : FilterExpression<DateTime?> +public class DateDiffFunction : FilterExpression<DateTime?>, IWithDateSelectorParameter, IWithTimeSpanParameter { public DateDiffFunction(FilterExpression<DateTime?> selector, TimeSpan parameter) { @@ -17,6 +18,12 @@ public DateDiffFunction() { } public override bool TimeDependent => Selector.TimeDependent; public override bool UserDependent => Selector.UserDependent; + public FilterExpression<DateTime?> Left + { + get => Selector; + set => Selector = value; + } + public override DateTime? Evaluate(Filterable f) { return Selector.Evaluate(f) - Parameter; diff --git a/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs b/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs index ed2733b1c..18e03668a 100644 --- a/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs +++ b/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Info; -public class HasAnimeTypeExpression : FilterExpression<bool> +public class HasAnimeTypeExpression : FilterExpression<bool>, IWithStringParameter { public HasAnimeTypeExpression(string parameter) { diff --git a/Shoko.Server/Filters/Info/HasCustomTagExpression.cs b/Shoko.Server/Filters/Info/HasCustomTagExpression.cs index 4a6bc9bd1..cf34e8156 100644 --- a/Shoko.Server/Filters/Info/HasCustomTagExpression.cs +++ b/Shoko.Server/Filters/Info/HasCustomTagExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Info; -public class HasCustomTagExpression : FilterExpression<bool> +public class HasCustomTagExpression : FilterExpression<bool>, IWithStringParameter { public HasCustomTagExpression(string parameter) { diff --git a/Shoko.Server/Filters/Info/HasNameExpression.cs b/Shoko.Server/Filters/Info/HasNameExpression.cs index 785e33258..8ba574a12 100644 --- a/Shoko.Server/Filters/Info/HasNameExpression.cs +++ b/Shoko.Server/Filters/Info/HasNameExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Info; -public class HasNameExpression : FilterExpression<bool> +public class HasNameExpression : FilterExpression<bool>, IWithStringParameter { public HasNameExpression(string parameter) { diff --git a/Shoko.Server/Filters/Info/HasTagExpression.cs b/Shoko.Server/Filters/Info/HasTagExpression.cs index d9a44e38c..835175898 100644 --- a/Shoko.Server/Filters/Info/HasTagExpression.cs +++ b/Shoko.Server/Filters/Info/HasTagExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Info; -public class HasTagExpression : FilterExpression<bool> +public class HasTagExpression : FilterExpression<bool>, IWithStringParameter { public HasTagExpression(string parameter) { diff --git a/Shoko.Server/Filters/Info/InSeasonExpression.cs b/Shoko.Server/Filters/Info/InSeasonExpression.cs index 847116580..58de3f1ec 100644 --- a/Shoko.Server/Filters/Info/InSeasonExpression.cs +++ b/Shoko.Server/Filters/Info/InSeasonExpression.cs @@ -1,9 +1,10 @@ using System; using Shoko.Models.Enums; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Info; -public class InSeasonExpression : FilterExpression<bool> +public class InSeasonExpression : FilterExpression<bool>, IWithNumberParameter, IWithSecondStringParameter { public InSeasonExpression(int year, AnimeSeason season) { @@ -16,6 +17,18 @@ public InSeasonExpression() { } public AnimeSeason Season { get; set; } public override bool TimeDependent => false; public override bool UserDependent => false; + + double? IWithNumberParameter.Parameter + { + get => Year; + set => Year = value.HasValue ? (int)value.Value : 0; + } + + string IWithSecondStringParameter.SecondParameter + { + get => Season.ToString(); + set => Season = Enum.Parse<AnimeSeason>(value); + } public override bool Evaluate(Filterable filterable) { diff --git a/Shoko.Server/Filters/Info/InYearExpression.cs b/Shoko.Server/Filters/Info/InYearExpression.cs index 054917fd2..1e0d1c2ff 100644 --- a/Shoko.Server/Filters/Info/InYearExpression.cs +++ b/Shoko.Server/Filters/Info/InYearExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Info; -public class InYearExpression : FilterExpression<bool> +public class InYearExpression : FilterExpression<bool>, IWithNumberParameter { public InYearExpression(int parameter) { @@ -14,6 +15,12 @@ public InYearExpression() { } public override bool TimeDependent => true; public override bool UserDependent => false; + double? IWithNumberParameter.Parameter + { + get => Parameter; + set => Parameter = value.HasValue ? (int)value.Value : 0; + } + public override bool Evaluate(Filterable filterable) { return filterable.Years.Contains(Parameter); diff --git a/Shoko.Server/Filters/Interfaces/IWithDateParameter.cs b/Shoko.Server/Filters/Interfaces/IWithDateParameter.cs new file mode 100644 index 000000000..a3ef416d3 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithDateParameter.cs @@ -0,0 +1,8 @@ +using System; + +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithDateParameter +{ + DateTime Parameter { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithDateSelectorParameter.cs b/Shoko.Server/Filters/Interfaces/IWithDateSelectorParameter.cs new file mode 100644 index 000000000..f011a3403 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithDateSelectorParameter.cs @@ -0,0 +1,8 @@ +using System; + +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithDateSelectorParameter +{ + FilterExpression<DateTime?> Left { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithExpressionParameter.cs b/Shoko.Server/Filters/Interfaces/IWithExpressionParameter.cs new file mode 100644 index 000000000..ef65c4405 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithExpressionParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithExpressionParameter +{ + FilterExpression<bool> Left { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithNumberParameter.cs b/Shoko.Server/Filters/Interfaces/IWithNumberParameter.cs new file mode 100644 index 000000000..d332703cb --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithNumberParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithNumberParameter +{ + double? Parameter { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithNumberSelectorParameter.cs b/Shoko.Server/Filters/Interfaces/IWithNumberSelectorParameter.cs new file mode 100644 index 000000000..684438346 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithNumberSelectorParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithNumberSelectorParameter +{ + FilterExpression<double> Left { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithSecondDateSelectorParameter.cs b/Shoko.Server/Filters/Interfaces/IWithSecondDateSelectorParameter.cs new file mode 100644 index 000000000..d8f4129f9 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithSecondDateSelectorParameter.cs @@ -0,0 +1,8 @@ +using System; + +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithSecondDateSelectorParameter +{ + FilterExpression<DateTime?> Right { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithSecondExpressionParameter.cs b/Shoko.Server/Filters/Interfaces/IWithSecondExpressionParameter.cs new file mode 100644 index 000000000..df13d480d --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithSecondExpressionParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithSecondExpressionParameter +{ + FilterExpression<bool> Right { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithSecondNumberSelectorParameter.cs b/Shoko.Server/Filters/Interfaces/IWithSecondNumberSelectorParameter.cs new file mode 100644 index 000000000..6aac4dc2c --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithSecondNumberSelectorParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithSecondNumberSelectorParameter +{ + FilterExpression<double> Right { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithSecondStringParameter.cs b/Shoko.Server/Filters/Interfaces/IWithSecondStringParameter.cs new file mode 100644 index 000000000..0cf944bdc --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithSecondStringParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithSecondStringParameter +{ + string SecondParameter { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithSecondStringSelectorParameter.cs b/Shoko.Server/Filters/Interfaces/IWithSecondStringSelectorParameter.cs new file mode 100644 index 000000000..fbe68ec85 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithSecondStringSelectorParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithSecondStringSelectorParameter +{ + FilterExpression<string> Right { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithStringParameter.cs b/Shoko.Server/Filters/Interfaces/IWithStringParameter.cs new file mode 100644 index 000000000..a879c64c7 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithStringParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithStringParameter +{ + string Parameter { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithStringSelectorParameter.cs b/Shoko.Server/Filters/Interfaces/IWithStringSelectorParameter.cs new file mode 100644 index 000000000..3d96d04ee --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithStringSelectorParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithStringSelectorParameter +{ + FilterExpression<string> Left { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithTimeSpanParameter.cs b/Shoko.Server/Filters/Interfaces/IWithTimeSpanParameter.cs new file mode 100644 index 000000000..08902b51d --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithTimeSpanParameter.cs @@ -0,0 +1,8 @@ +using System; + +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithTimeSpanParameter +{ + TimeSpan Parameter { get; set; } +} diff --git a/Shoko.Server/Filters/LegacyFilterConverter.cs b/Shoko.Server/Filters/LegacyFilterConverter.cs index b9c86a8ed..940c75853 100644 --- a/Shoko.Server/Filters/LegacyFilterConverter.cs +++ b/Shoko.Server/Filters/LegacyFilterConverter.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using Shoko.Models.Enums; @@ -13,20 +12,20 @@ namespace Shoko.Server.Filters; public class LegacyFilterConverter { - public List<GroupFilterCondition> GetConditions(Filter filter) + public List<GroupFilterCondition> GetConditions(FilterPreset filter) { // TODO traverse the tree and replace with pre-set mappings return new List<GroupFilterCondition>(); } - public List<GroupFilterSortingCriteria> GetSortingCriteria(Filter filter) + public List<GroupFilterSortingCriteria> GetSortingCriteria(FilterPreset filter) { // TODO traverse the tree and replace with pre-set mappings return new List<GroupFilterSortingCriteria> { new() { - GroupFilterID = filter.FilterID, SortType = GroupFilterSorting.SortName, SortDirection = GroupFilterSortDirection.Asc + GroupFilterID = filter.FilterPresetID, SortType = GroupFilterSorting.SortName, SortDirection = GroupFilterSortDirection.Asc } }; } diff --git a/Shoko.Server/Filters/LegacyMappings.cs b/Shoko.Server/Filters/LegacyMappings.cs index 8544dfd3e..58b62d26a 100644 --- a/Shoko.Server/Filters/LegacyMappings.cs +++ b/Shoko.Server/Filters/LegacyMappings.cs @@ -8,12 +8,6 @@ using Shoko.Server.Filters.Logic.DateTimes; using Shoko.Server.Filters.Selectors; -using DateGreaterEqual = Shoko.Server.Filters.Logic.DateTimes.GreaterThanEqualExpression; -using DateGreater = Shoko.Server.Filters.Logic.DateTimes.GreaterThanExpression; -using DateLess = Shoko.Server.Filters.Logic.DateTimes.LessThanExpression; -using NumberGreater = Shoko.Server.Filters.Logic.Numbers.GreaterThanExpression; -using NumberLess = Shoko.Server.Filters.Logic.Numbers.LessThanExpression; - namespace Shoko.Server.Filters; public class LegacyMappings @@ -109,7 +103,7 @@ private static FilterExpression<bool> GetDateExpression(FilterExpression<DateTim { if (!int.TryParse(parameter, out var lastX)) return suppressErrors ? null : throw new ArgumentException(@"Parameter is not a number", nameof(parameter)); - return new DateGreaterEqual(selector, + return new DateGreaterThanEqualsExpression(selector, new DateDiffFunction(new DateAddFunction(new TodayFunction(), TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1)), TimeSpan.FromDays(lastX))); } @@ -119,7 +113,7 @@ private static FilterExpression<bool> GetDateExpression(FilterExpression<DateTim return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} was not a date in format of yyyyMMdd", nameof(parameter)); - return new DateGreater(selector, date); + return new DateGreaterThanExpression(selector, date); } case GroupFilterOperator.LessThan: { @@ -127,7 +121,7 @@ private static FilterExpression<bool> GetDateExpression(FilterExpression<DateTim return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} was not a date in format of yyyyMMdd", nameof(parameter)); - return new DateGreater(selector, date); + return new DateGreaterThanExpression(selector, date); } default: return suppressErrors @@ -214,9 +208,9 @@ public static FilterExpression<bool> GetRatingExpression(GroupFilterOperator op, { // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand case GroupFilterOperator.GreaterThan: - return new NumberLess(new HighestAniDBRatingSelector(), rating); + return new NumberLessThanExpression(new HighestAniDBRatingSelector(), rating); case GroupFilterOperator.LessThan: - return new NumberGreater(new HighestAniDBRatingSelector(), rating); + return new NumberGreaterThanExpression(new HighestAniDBRatingSelector(), rating); default: return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Rating"); } @@ -230,9 +224,9 @@ public static FilterExpression<bool> GetUserRatingExpression(GroupFilterOperator { // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand case GroupFilterOperator.GreaterThan: - return new NumberLess(new HighestUserRatingSelector(), rating); + return new NumberLessThanExpression(new HighestUserRatingSelector(), rating); case GroupFilterOperator.LessThan: - return new NumberGreater(new HighestUserRatingSelector(), rating); + return new NumberGreaterThanExpression(new HighestUserRatingSelector(), rating); default: return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for User Rating"); } @@ -246,9 +240,9 @@ public static FilterExpression<bool> GetEpisodeCountExpression(GroupFilterOperat { // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand case GroupFilterOperator.GreaterThan: - return new NumberLess(new EpisodeCountSelector(), count); + return new NumberLessThanExpression(new EpisodeCountSelector(), count); case GroupFilterOperator.LessThan: - return new NumberGreater(new HighestUserRatingSelector(), count); + return new NumberGreaterThanExpression(new HighestUserRatingSelector(), count); default: return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Episode Count"); } diff --git a/Shoko.Server/Filters/Logic/AndExpression.cs b/Shoko.Server/Filters/Logic/AndExpression.cs index 9e52d9839..56d69ff66 100644 --- a/Shoko.Server/Filters/Logic/AndExpression.cs +++ b/Shoko.Server/Filters/Logic/AndExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic; -public class AndExpression : FilterExpression<bool> +public class AndExpression : FilterExpression<bool>, IWithExpressionParameter, IWithSecondExpressionParameter { public AndExpression(FilterExpression<bool> left, FilterExpression<bool> right) { diff --git a/Shoko.Server/Filters/Logic/DateTimes/EqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateEqualsExpression.cs similarity index 74% rename from Shoko.Server/Filters/Logic/DateTimes/EqualExpression.cs rename to Shoko.Server/Filters/Logic/DateTimes/DateEqualsExpression.cs index 403b92783..b20b42d05 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/EqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/DateEqualsExpression.cs @@ -1,20 +1,21 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.DateTimes; -public class EqualExpression : FilterExpression<bool> +public class DateEqualsExpression : FilterExpression<bool>, IWithDateSelectorParameter, IWithSecondDateSelectorParameter, IWithDateParameter { - public EqualExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) + public DateEqualsExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) { Left = left; Right = right; } - public EqualExpression(FilterExpression<DateTime?> left, DateTime parameter) + public DateEqualsExpression(FilterExpression<DateTime?> left, DateTime parameter) { Left = left; Parameter = parameter; } - public EqualExpression() { } + public DateEqualsExpression() { } public FilterExpression<DateTime?> Left { get; set; } public FilterExpression<DateTime?> Right { get; set; } @@ -46,7 +47,7 @@ public override bool Evaluate(Filterable filterable) return (date > operand ? date - operand : operand - date).Value.TotalDays < 1; } - protected bool Equals(EqualExpression other) + protected bool Equals(DateEqualsExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); } @@ -68,7 +69,7 @@ public override bool Equals(object obj) return false; } - return Equals((EqualExpression)obj); + return Equals((DateEqualsExpression)obj); } public override int GetHashCode() @@ -76,12 +77,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(EqualExpression left, EqualExpression right) + public static bool operator ==(DateEqualsExpression left, DateEqualsExpression right) { return Equals(left, right); } - public static bool operator !=(EqualExpression left, EqualExpression right) + public static bool operator !=(DateEqualsExpression left, DateEqualsExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanEqualsExpression.cs similarity index 71% rename from Shoko.Server/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs rename to Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanEqualsExpression.cs index ffd46db64..7e6287d58 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/GreaterThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanEqualsExpression.cs @@ -1,20 +1,21 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.DateTimes; -public class GreaterThanEqualExpression : FilterExpression<bool> +public class DateGreaterThanEqualsExpression : FilterExpression<bool>, IWithDateSelectorParameter, IWithSecondDateSelectorParameter, IWithDateParameter { - public GreaterThanEqualExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) + public DateGreaterThanEqualsExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) { Left = left; Right = right; } - public GreaterThanEqualExpression(FilterExpression<DateTime?> left, DateTime parameter) + public DateGreaterThanEqualsExpression(FilterExpression<DateTime?> left, DateTime parameter) { Left = left; Parameter = parameter; } - public GreaterThanEqualExpression() { } + public DateGreaterThanEqualsExpression() { } public FilterExpression<DateTime?> Left { get; set; } public FilterExpression<DateTime?> Right { get; set; } @@ -46,7 +47,7 @@ public override bool Evaluate(Filterable filterable) return date >= operand; } - protected bool Equals(GreaterThanEqualExpression other) + protected bool Equals(DateGreaterThanEqualsExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); } @@ -68,7 +69,7 @@ public override bool Equals(object obj) return false; } - return Equals((GreaterThanEqualExpression)obj); + return Equals((DateGreaterThanEqualsExpression)obj); } public override int GetHashCode() @@ -76,12 +77,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(GreaterThanEqualExpression left, GreaterThanEqualExpression right) + public static bool operator ==(DateGreaterThanEqualsExpression left, DateGreaterThanEqualsExpression right) { return Equals(left, right); } - public static bool operator !=(GreaterThanEqualExpression left, GreaterThanEqualExpression right) + public static bool operator !=(DateGreaterThanEqualsExpression left, DateGreaterThanEqualsExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/DateTimes/GreaterThanExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanExpression.cs similarity index 73% rename from Shoko.Server/Filters/Logic/DateTimes/GreaterThanExpression.cs rename to Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanExpression.cs index 3960332e8..198edc7b7 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/GreaterThanExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanExpression.cs @@ -1,20 +1,21 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.DateTimes; -public class GreaterThanExpression : FilterExpression<bool> +public class DateGreaterThanExpression : FilterExpression<bool>, IWithDateSelectorParameter, IWithSecondDateSelectorParameter, IWithDateParameter { - public GreaterThanExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) + public DateGreaterThanExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) { Left = left; Right = right; } - public GreaterThanExpression(FilterExpression<DateTime?> left, DateTime parameter) + public DateGreaterThanExpression(FilterExpression<DateTime?> left, DateTime parameter) { Left = left; Parameter = parameter; } - public GreaterThanExpression() { } + public DateGreaterThanExpression() { } public FilterExpression<DateTime?> Left { get; set; } public FilterExpression<DateTime?> Right { get; set; } @@ -46,7 +47,7 @@ public override bool Evaluate(Filterable filterable) return date > operand; } - protected bool Equals(GreaterThanExpression other) + protected bool Equals(DateGreaterThanExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); } @@ -68,7 +69,7 @@ public override bool Equals(object obj) return false; } - return Equals((GreaterThanExpression)obj); + return Equals((DateGreaterThanExpression)obj); } public override int GetHashCode() @@ -76,12 +77,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(GreaterThanExpression left, GreaterThanExpression right) + public static bool operator ==(DateGreaterThanExpression left, DateGreaterThanExpression right) { return Equals(left, right); } - public static bool operator !=(GreaterThanExpression left, GreaterThanExpression right) + public static bool operator !=(DateGreaterThanExpression left, DateGreaterThanExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/DateTimes/LessThanEqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateLessThanEqualsExpression.cs similarity index 70% rename from Shoko.Server/Filters/Logic/DateTimes/LessThanEqualExpression.cs rename to Shoko.Server/Filters/Logic/DateTimes/DateLessThanEqualsExpression.cs index c7514ce91..2f3ea9bda 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/LessThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/DateLessThanEqualsExpression.cs @@ -1,20 +1,21 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.DateTimes; -public class LessThanEqualExpression : FilterExpression<bool> +public class DateLessThanEqualsExpression : FilterExpression<bool>, IWithDateSelectorParameter, IWithSecondDateSelectorParameter, IWithDateParameter { - public LessThanEqualExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) + public DateLessThanEqualsExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) { Left = left; Right = right; } - public LessThanEqualExpression(FilterExpression<DateTime?> left, DateTime parameter) + public DateLessThanEqualsExpression(FilterExpression<DateTime?> left, DateTime parameter) { Left = left; Parameter = parameter; } - public LessThanEqualExpression() { } + public DateLessThanEqualsExpression() { } public FilterExpression<DateTime?> Left { get; set; } public FilterExpression<DateTime?> Right { get; set; } @@ -39,7 +40,7 @@ public override bool Evaluate(Filterable filterable) return date <= operand; } - protected bool Equals(LessThanEqualExpression other) + protected bool Equals(DateLessThanEqualsExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); } @@ -61,7 +62,7 @@ public override bool Equals(object obj) return false; } - return Equals((LessThanEqualExpression)obj); + return Equals((DateLessThanEqualsExpression)obj); } public override int GetHashCode() @@ -69,12 +70,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(LessThanEqualExpression left, LessThanEqualExpression right) + public static bool operator ==(DateLessThanEqualsExpression left, DateLessThanEqualsExpression right) { return Equals(left, right); } - public static bool operator !=(LessThanEqualExpression left, LessThanEqualExpression right) + public static bool operator !=(DateLessThanEqualsExpression left, DateLessThanEqualsExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/DateTimes/LessThanExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateLessThanExpression.cs similarity index 73% rename from Shoko.Server/Filters/Logic/DateTimes/LessThanExpression.cs rename to Shoko.Server/Filters/Logic/DateTimes/DateLessThanExpression.cs index 646eef952..0181f888c 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/LessThanExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/DateLessThanExpression.cs @@ -1,20 +1,21 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.DateTimes; -public class LessThanExpression : FilterExpression<bool> +public class DateLessThanExpression : FilterExpression<bool>, IWithDateSelectorParameter, IWithSecondDateSelectorParameter, IWithDateParameter { - public LessThanExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) + public DateLessThanExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) { Left = left; Right = right; } - public LessThanExpression(FilterExpression<DateTime?> left, DateTime parameter) + public DateLessThanExpression(FilterExpression<DateTime?> left, DateTime parameter) { Left = left; Parameter = parameter; } - public LessThanExpression() { } + public DateLessThanExpression() { } public FilterExpression<DateTime?> Left { get; set; } public FilterExpression<DateTime?> Right { get; set; } @@ -46,7 +47,7 @@ public override bool Evaluate(Filterable filterable) return date < operand; } - protected bool Equals(LessThanExpression other) + protected bool Equals(DateLessThanExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); } @@ -68,7 +69,7 @@ public override bool Equals(object obj) return false; } - return Equals((LessThanExpression)obj); + return Equals((DateLessThanExpression)obj); } public override int GetHashCode() @@ -76,12 +77,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(LessThanExpression left, LessThanExpression right) + public static bool operator ==(DateLessThanExpression left, DateLessThanExpression right) { return Equals(left, right); } - public static bool operator !=(LessThanExpression left, LessThanExpression right) + public static bool operator !=(DateLessThanExpression left, DateLessThanExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/DateTimes/NotEqualExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateNotEqualsExpression.cs similarity index 74% rename from Shoko.Server/Filters/Logic/DateTimes/NotEqualExpression.cs rename to Shoko.Server/Filters/Logic/DateTimes/DateNotEqualsExpression.cs index bd73a496c..fd7134539 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/NotEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/DateNotEqualsExpression.cs @@ -1,20 +1,21 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.DateTimes; -public class NotEqualExpression : FilterExpression<bool> +public class DateNotEqualsExpression : FilterExpression<bool>, IWithDateSelectorParameter, IWithSecondDateSelectorParameter, IWithDateParameter { - public NotEqualExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) + public DateNotEqualsExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) { Left = left; Right = right; } - public NotEqualExpression(FilterExpression<DateTime?> left, DateTime parameter) + public DateNotEqualsExpression(FilterExpression<DateTime?> left, DateTime parameter) { Left = left; Parameter = parameter; } - public NotEqualExpression() { } + public DateNotEqualsExpression() { } public FilterExpression<DateTime?> Left { get; set; } public FilterExpression<DateTime?> Right { get; set; } @@ -46,7 +47,7 @@ public override bool Evaluate(Filterable filterable) return (date > operand ? date - operand : operand - date).Value.TotalDays >= 1; } - protected bool Equals(NotEqualExpression other) + protected bool Equals(DateNotEqualsExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); } @@ -68,7 +69,7 @@ public override bool Equals(object obj) return false; } - return Equals((NotEqualExpression)obj); + return Equals((DateNotEqualsExpression)obj); } public override int GetHashCode() @@ -76,12 +77,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(NotEqualExpression left, NotEqualExpression right) + public static bool operator ==(DateNotEqualsExpression left, DateNotEqualsExpression right) { return Equals(left, right); } - public static bool operator !=(NotEqualExpression left, NotEqualExpression right) + public static bool operator !=(DateNotEqualsExpression left, DateNotEqualsExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/NotExpression.cs b/Shoko.Server/Filters/Logic/NotExpression.cs index b758f3be8..5b32571ae 100644 --- a/Shoko.Server/Filters/Logic/NotExpression.cs +++ b/Shoko.Server/Filters/Logic/NotExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic; -public class NotExpression : FilterExpression<bool> +public class NotExpression : FilterExpression<bool>, IWithExpressionParameter { public NotExpression(FilterExpression<bool> left) { diff --git a/Shoko.Server/Filters/Logic/Numbers/EqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberEqualsExpression.cs similarity index 67% rename from Shoko.Server/Filters/Logic/Numbers/EqualExpression.cs rename to Shoko.Server/Filters/Logic/Numbers/NumberEqualsExpression.cs index 47ab9b706..dbfd82688 100644 --- a/Shoko.Server/Filters/Logic/Numbers/EqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NumberEqualsExpression.cs @@ -1,20 +1,21 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Numbers; -public class EqualExpression : FilterExpression<bool> +public class NumberEqualsExpression : FilterExpression<bool>, IWithNumberSelectorParameter, IWithSecondNumberSelectorParameter, IWithNumberParameter { - public EqualExpression(FilterExpression<double> left, FilterExpression<double> right) + public NumberEqualsExpression(FilterExpression<double> left, FilterExpression<double> right) { Left = left; Right = right; } - public EqualExpression(FilterExpression<double> left, double parameter) + public NumberEqualsExpression(FilterExpression<double> left, double parameter) { Left = left; Parameter = parameter; } - public EqualExpression() { } + public NumberEqualsExpression() { } public FilterExpression<double> Left { get; set; } public FilterExpression<double> Right { get; set; } @@ -29,7 +30,7 @@ public override bool Evaluate(Filterable filterable) return Math.Abs(left - right) < 0.001D; } - protected bool Equals(EqualExpression other) + protected bool Equals(NumberEqualsExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); } @@ -51,7 +52,7 @@ public override bool Equals(object obj) return false; } - return Equals((EqualExpression)obj); + return Equals((NumberEqualsExpression)obj); } public override int GetHashCode() @@ -59,12 +60,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(EqualExpression left, EqualExpression right) + public static bool operator ==(NumberEqualsExpression left, NumberEqualsExpression right) { return Equals(left, right); } - public static bool operator !=(EqualExpression left, EqualExpression right) + public static bool operator !=(NumberEqualsExpression left, NumberEqualsExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/Numbers/GreaterThanEqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanEqualsExpression.cs similarity index 64% rename from Shoko.Server/Filters/Logic/Numbers/GreaterThanEqualExpression.cs rename to Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanEqualsExpression.cs index eb6c8e545..5f4cce9dc 100644 --- a/Shoko.Server/Filters/Logic/Numbers/GreaterThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanEqualsExpression.cs @@ -1,20 +1,21 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Numbers; -public class GreaterThanEqualExpression : FilterExpression<bool> +public class NumberGreaterThanEqualsExpression : FilterExpression<bool>, IWithNumberSelectorParameter, IWithSecondNumberSelectorParameter, IWithNumberParameter { - public GreaterThanEqualExpression(FilterExpression<double> left, FilterExpression<double> right) + public NumberGreaterThanEqualsExpression(FilterExpression<double> left, FilterExpression<double> right) { Left = left; Right = right; } - public GreaterThanEqualExpression(FilterExpression<double> left, double parameter) + public NumberGreaterThanEqualsExpression(FilterExpression<double> left, double parameter) { Left = left; Parameter = parameter; } - public GreaterThanEqualExpression() { } + public NumberGreaterThanEqualsExpression() { } public FilterExpression<double> Left { get; set; } public FilterExpression<double> Right { get; set; } @@ -29,7 +30,7 @@ public override bool Evaluate(Filterable filterable) return Math.Abs(left - right) < 0.001D || left > right; } - protected bool Equals(GreaterThanEqualExpression other) + protected bool Equals(NumberGreaterThanEqualsExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); } @@ -51,7 +52,7 @@ public override bool Equals(object obj) return false; } - return Equals((GreaterThanEqualExpression)obj); + return Equals((NumberGreaterThanEqualsExpression)obj); } public override int GetHashCode() @@ -59,12 +60,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(GreaterThanEqualExpression left, GreaterThanEqualExpression right) + public static bool operator ==(NumberGreaterThanEqualsExpression left, NumberGreaterThanEqualsExpression right) { return Equals(left, right); } - public static bool operator !=(GreaterThanEqualExpression left, GreaterThanEqualExpression right) + public static bool operator !=(NumberGreaterThanEqualsExpression left, NumberGreaterThanEqualsExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/Numbers/GreaterThanExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanExpression.cs similarity index 65% rename from Shoko.Server/Filters/Logic/Numbers/GreaterThanExpression.cs rename to Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanExpression.cs index efd836e86..9af00d204 100644 --- a/Shoko.Server/Filters/Logic/Numbers/GreaterThanExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanExpression.cs @@ -1,20 +1,21 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Numbers; -public class GreaterThanExpression : FilterExpression<bool> +public class NumberGreaterThanExpression : FilterExpression<bool>, IWithNumberSelectorParameter, IWithSecondNumberSelectorParameter, IWithNumberParameter { - public GreaterThanExpression(FilterExpression<double> left, FilterExpression<double> right) + public NumberGreaterThanExpression(FilterExpression<double> left, FilterExpression<double> right) { Left = left; Right = right; } - public GreaterThanExpression(FilterExpression<double> left, double parameter) + public NumberGreaterThanExpression(FilterExpression<double> left, double parameter) { Left = left; Parameter = parameter; } - public GreaterThanExpression() { } + public NumberGreaterThanExpression() { } public FilterExpression<double> Left { get; set; } public FilterExpression<double> Right { get; set; } @@ -29,7 +30,7 @@ public override bool Evaluate(Filterable filterable) return left > right; } - protected bool Equals(GreaterThanExpression other) + protected bool Equals(NumberGreaterThanExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); } @@ -51,7 +52,7 @@ public override bool Equals(object obj) return false; } - return Equals((GreaterThanExpression)obj); + return Equals((NumberGreaterThanExpression)obj); } public override int GetHashCode() @@ -59,12 +60,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(GreaterThanExpression left, GreaterThanExpression right) + public static bool operator ==(NumberGreaterThanExpression left, NumberGreaterThanExpression right) { return Equals(left, right); } - public static bool operator !=(GreaterThanExpression left, GreaterThanExpression right) + public static bool operator !=(NumberGreaterThanExpression left, NumberGreaterThanExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/Numbers/LessThanEqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberLessThanEqualsExpression.cs similarity index 65% rename from Shoko.Server/Filters/Logic/Numbers/LessThanEqualExpression.cs rename to Shoko.Server/Filters/Logic/Numbers/NumberLessThanEqualsExpression.cs index e198fd7b8..3284c96b2 100644 --- a/Shoko.Server/Filters/Logic/Numbers/LessThanEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NumberLessThanEqualsExpression.cs @@ -1,20 +1,21 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Numbers; -public class LessThanEqualExpression : FilterExpression<bool> +public class NumberLessThanEqualsExpression : FilterExpression<bool>, IWithNumberSelectorParameter, IWithSecondNumberSelectorParameter, IWithNumberParameter { - public LessThanEqualExpression(FilterExpression<double> left, FilterExpression<double> right) + public NumberLessThanEqualsExpression(FilterExpression<double> left, FilterExpression<double> right) { Left = left; Right = right; } - public LessThanEqualExpression(FilterExpression<double> left, double parameter) + public NumberLessThanEqualsExpression(FilterExpression<double> left, double parameter) { Left = left; Parameter = parameter; } - public LessThanEqualExpression() { } + public NumberLessThanEqualsExpression() { } public FilterExpression<double> Left { get; set; } public FilterExpression<double> Right { get; set; } @@ -29,7 +30,7 @@ public override bool Evaluate(Filterable filterable) return Math.Abs(left - right) < 0.001D || left < right; } - protected bool Equals(LessThanEqualExpression other) + protected bool Equals(NumberLessThanEqualsExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); } @@ -51,7 +52,7 @@ public override bool Equals(object obj) return false; } - return Equals((LessThanEqualExpression)obj); + return Equals((NumberLessThanEqualsExpression)obj); } public override int GetHashCode() @@ -59,12 +60,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(LessThanEqualExpression left, LessThanEqualExpression right) + public static bool operator ==(NumberLessThanEqualsExpression left, NumberLessThanEqualsExpression right) { return Equals(left, right); } - public static bool operator !=(LessThanEqualExpression left, LessThanEqualExpression right) + public static bool operator !=(NumberLessThanEqualsExpression left, NumberLessThanEqualsExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/Numbers/LessThanExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberLessThanExpression.cs similarity index 66% rename from Shoko.Server/Filters/Logic/Numbers/LessThanExpression.cs rename to Shoko.Server/Filters/Logic/Numbers/NumberLessThanExpression.cs index dfa234dfe..3144fd23e 100644 --- a/Shoko.Server/Filters/Logic/Numbers/LessThanExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NumberLessThanExpression.cs @@ -1,20 +1,21 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Numbers; -public class LessThanExpression : FilterExpression<bool> +public class NumberLessThanExpression : FilterExpression<bool>, IWithNumberSelectorParameter, IWithSecondNumberSelectorParameter, IWithNumberParameter { - public LessThanExpression(FilterExpression<double> left, FilterExpression<double> right) + public NumberLessThanExpression(FilterExpression<double> left, FilterExpression<double> right) { Left = left; Right = right; } - public LessThanExpression(FilterExpression<double> left, double parameter) + public NumberLessThanExpression(FilterExpression<double> left, double parameter) { Left = left; Parameter = parameter; } - public LessThanExpression() { } + public NumberLessThanExpression() { } public FilterExpression<double> Left { get; set; } public FilterExpression<double> Right { get; set; } @@ -29,7 +30,7 @@ public override bool Evaluate(Filterable filterable) return left < right; } - protected bool Equals(LessThanExpression other) + protected bool Equals(NumberLessThanExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); } @@ -51,7 +52,7 @@ public override bool Equals(object obj) return false; } - return Equals((LessThanExpression)obj); + return Equals((NumberLessThanExpression)obj); } public override int GetHashCode() @@ -59,12 +60,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(LessThanExpression left, LessThanExpression right) + public static bool operator ==(NumberLessThanExpression left, NumberLessThanExpression right) { return Equals(left, right); } - public static bool operator !=(LessThanExpression left, LessThanExpression right) + public static bool operator !=(NumberLessThanExpression left, NumberLessThanExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/Numbers/NotEqualExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberNotEqualsExpression.cs similarity index 66% rename from Shoko.Server/Filters/Logic/Numbers/NotEqualExpression.cs rename to Shoko.Server/Filters/Logic/Numbers/NumberNotEqualsExpression.cs index 81ba89cc5..7c0d3e99f 100644 --- a/Shoko.Server/Filters/Logic/Numbers/NotEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NumberNotEqualsExpression.cs @@ -1,20 +1,21 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Numbers; -public class NotEqualExpression : FilterExpression<bool> +public class NumberNotEqualsExpression : FilterExpression<bool>, IWithNumberSelectorParameter, IWithSecondNumberSelectorParameter, IWithNumberParameter { - public NotEqualExpression(FilterExpression<double> left, FilterExpression<double> right) + public NumberNotEqualsExpression(FilterExpression<double> left, FilterExpression<double> right) { Left = left; Right = right; } - public NotEqualExpression(FilterExpression<double> left, double parameter) + public NumberNotEqualsExpression(FilterExpression<double> left, double parameter) { Left = left; Parameter = parameter; } - public NotEqualExpression() { } + public NumberNotEqualsExpression() { } public FilterExpression<double> Left { get; set; } public FilterExpression<double> Right { get; set; } @@ -29,7 +30,7 @@ public override bool Evaluate(Filterable filterable) return Math.Abs(left - right) >= 0.001D; } - protected bool Equals(NotEqualExpression other) + protected bool Equals(NumberNotEqualsExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); } @@ -51,7 +52,7 @@ public override bool Equals(object obj) return false; } - return Equals((NotEqualExpression)obj); + return Equals((NumberNotEqualsExpression)obj); } public override int GetHashCode() @@ -59,12 +60,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(NotEqualExpression left, NotEqualExpression right) + public static bool operator ==(NumberNotEqualsExpression left, NumberNotEqualsExpression right) { return Equals(left, right); } - public static bool operator !=(NotEqualExpression left, NotEqualExpression right) + public static bool operator !=(NumberNotEqualsExpression left, NumberNotEqualsExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/OrExpression.cs b/Shoko.Server/Filters/Logic/OrExpression.cs index 257d7bc7c..ba047ec97 100644 --- a/Shoko.Server/Filters/Logic/OrExpression.cs +++ b/Shoko.Server/Filters/Logic/OrExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic; -public class OrExpression : FilterExpression<bool> +public class OrExpression : FilterExpression<bool>, IWithExpressionParameter, IWithSecondExpressionParameter { public OrExpression(FilterExpression<bool> left, FilterExpression<bool> right) { diff --git a/Shoko.Server/Filters/Logic/Strings/ContainsExpression.cs b/Shoko.Server/Filters/Logic/Strings/StringContainsExpression.cs similarity index 68% rename from Shoko.Server/Filters/Logic/Strings/ContainsExpression.cs rename to Shoko.Server/Filters/Logic/Strings/StringContainsExpression.cs index bee1a721c..ce2f6fbb0 100644 --- a/Shoko.Server/Filters/Logic/Strings/ContainsExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/StringContainsExpression.cs @@ -1,22 +1,23 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Strings; -public class ContainsExpression : FilterExpression<bool> +public class StringContainsExpression : FilterExpression<bool>, IWithStringSelectorParameter, IWithSecondStringSelectorParameter, IWithStringParameter { - public ContainsExpression(FilterExpression<string> left, FilterExpression<string> right) + public StringContainsExpression(FilterExpression<string> left, FilterExpression<string> right) { Left = left; Right = right; } - public ContainsExpression(FilterExpression<string> left, string parameter) + public StringContainsExpression(FilterExpression<string> left, string parameter) { Left = left; Parameter = parameter; } - public ContainsExpression() { } + public StringContainsExpression() { } public FilterExpression<string> Left { get; set; } public FilterExpression<string> Right { get; set; } @@ -36,7 +37,7 @@ public override bool Evaluate(Filterable filterable) return left.Contains(right, StringComparison.InvariantCultureIgnoreCase); } - protected bool Equals(ContainsExpression other) + protected bool Equals(StringContainsExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter == other.Parameter; } @@ -58,7 +59,7 @@ public override bool Equals(object obj) return false; } - return Equals((ContainsExpression)obj); + return Equals((StringContainsExpression)obj); } public override int GetHashCode() @@ -66,12 +67,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(ContainsExpression left, ContainsExpression right) + public static bool operator ==(StringContainsExpression left, StringContainsExpression right) { return Equals(left, right); } - public static bool operator !=(ContainsExpression left, ContainsExpression right) + public static bool operator !=(StringContainsExpression left, StringContainsExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/Strings/EqualExpression.cs b/Shoko.Server/Filters/Logic/Strings/StringEqualsExpression.cs similarity index 67% rename from Shoko.Server/Filters/Logic/Strings/EqualExpression.cs rename to Shoko.Server/Filters/Logic/Strings/StringEqualsExpression.cs index 23d4cd910..250063eb7 100644 --- a/Shoko.Server/Filters/Logic/Strings/EqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/StringEqualsExpression.cs @@ -1,20 +1,21 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Strings; -public class EqualExpression : FilterExpression<bool> +public class StringEqualsExpression : FilterExpression<bool>, IWithStringSelectorParameter, IWithSecondStringSelectorParameter, IWithStringParameter { - public EqualExpression(FilterExpression<string> left, FilterExpression<string> right) + public StringEqualsExpression(FilterExpression<string> left, FilterExpression<string> right) { Left = left; Right = right; } - public EqualExpression(FilterExpression<string> left, string parameter) + public StringEqualsExpression(FilterExpression<string> left, string parameter) { Left = left; Parameter = parameter; } - public EqualExpression() { } + public StringEqualsExpression() { } public FilterExpression<string> Left { get; set; } public FilterExpression<string> Right { get; set; } @@ -29,7 +30,7 @@ public override bool Evaluate(Filterable filterable) return string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase); } - protected bool Equals(EqualExpression other) + protected bool Equals(StringEqualsExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter == other.Parameter; } @@ -51,7 +52,7 @@ public override bool Equals(object obj) return false; } - return Equals((EqualExpression)obj); + return Equals((StringEqualsExpression)obj); } public override int GetHashCode() @@ -59,12 +60,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(EqualExpression left, EqualExpression right) + public static bool operator ==(StringEqualsExpression left, StringEqualsExpression right) { return Equals(left, right); } - public static bool operator !=(EqualExpression left, EqualExpression right) + public static bool operator !=(StringEqualsExpression left, StringEqualsExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/Strings/NotEqualExpression.cs b/Shoko.Server/Filters/Logic/Strings/StringNotEqualsExpression.cs similarity index 66% rename from Shoko.Server/Filters/Logic/Strings/NotEqualExpression.cs rename to Shoko.Server/Filters/Logic/Strings/StringNotEqualsExpression.cs index 91926a343..1f0e49dd0 100644 --- a/Shoko.Server/Filters/Logic/Strings/NotEqualExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/StringNotEqualsExpression.cs @@ -1,20 +1,21 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic.Strings; -public class NotEqualExpression : FilterExpression<bool> +public class StringNotEqualsExpression : FilterExpression<bool>, IWithStringSelectorParameter, IWithSecondStringSelectorParameter, IWithStringParameter { - public NotEqualExpression(FilterExpression<string> left, FilterExpression<string> right) + public StringNotEqualsExpression(FilterExpression<string> left, FilterExpression<string> right) { Left = left; Right = right; } - public NotEqualExpression(FilterExpression<string> left, string parameter) + public StringNotEqualsExpression(FilterExpression<string> left, string parameter) { Left = left; Parameter = parameter; } - public NotEqualExpression() { } + public StringNotEqualsExpression() { } public FilterExpression<string> Left { get; set; } public FilterExpression<string> Right { get; set; } @@ -29,7 +30,7 @@ public override bool Evaluate(Filterable filterable) return !string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase); } - protected bool Equals(NotEqualExpression other) + protected bool Equals(StringNotEqualsExpression other) { return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter == other.Parameter; } @@ -51,7 +52,7 @@ public override bool Equals(object obj) return false; } - return Equals((NotEqualExpression)obj); + return Equals((StringNotEqualsExpression)obj); } public override int GetHashCode() @@ -59,12 +60,12 @@ public override int GetHashCode() return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); } - public static bool operator ==(NotEqualExpression left, NotEqualExpression right) + public static bool operator ==(StringNotEqualsExpression left, StringNotEqualsExpression right) { return Equals(left, right); } - public static bool operator !=(NotEqualExpression left, NotEqualExpression right) + public static bool operator !=(StringNotEqualsExpression left, StringNotEqualsExpression right) { return !Equals(left, right); } diff --git a/Shoko.Server/Filters/Logic/XorExpression.cs b/Shoko.Server/Filters/Logic/XorExpression.cs index 69889f240..768f67535 100644 --- a/Shoko.Server/Filters/Logic/XorExpression.cs +++ b/Shoko.Server/Filters/Logic/XorExpression.cs @@ -1,8 +1,9 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Logic; -public class XorExpression : FilterExpression<bool> +public class XorExpression : FilterExpression<bool>, IWithExpressionParameter, IWithSecondExpressionParameter { public XorExpression(FilterExpression<bool> left, FilterExpression<bool> right) { diff --git a/Shoko.Server/Mappings/FilterMap.cs b/Shoko.Server/Mappings/FilterPresetMap.cs similarity index 78% rename from Shoko.Server/Mappings/FilterMap.cs rename to Shoko.Server/Mappings/FilterPresetMap.cs index 3573747af..eb6b5b896 100644 --- a/Shoko.Server/Mappings/FilterMap.cs +++ b/Shoko.Server/Mappings/FilterPresetMap.cs @@ -5,14 +5,14 @@ namespace Shoko.Server.Mappings; -public class FilterMap : ClassMap<Filter> +public class FilterPresetMap : ClassMap<FilterPreset> { - public FilterMap() + public FilterPresetMap() { - Table("Filter"); + Table("FilterPreset"); Not.LazyLoad(); - Id(x => x.FilterID); - Map(x => x.ParentFilterID).Nullable(); + Id(x => x.FilterPresetID); + Map(x => x.ParentFilterPresetID).Nullable(); //References(x => x.Parent).Nullable().PropertyRef(x => x.ParentFilterID); Map(x => x.Name).Not.Nullable(); Map(x => x.FilterType).Not.Nullable().CustomType<GroupFilterType>(); diff --git a/Shoko.Server/Models/Filter.cs b/Shoko.Server/Models/FilterPreset.cs similarity index 79% rename from Shoko.Server/Models/Filter.cs rename to Shoko.Server/Models/FilterPreset.cs index 2a36c2b6a..3a4794959 100644 --- a/Shoko.Server/Models/Filter.cs +++ b/Shoko.Server/Models/FilterPreset.cs @@ -3,11 +3,11 @@ namespace Shoko.Server.Models; -public class Filter +public class FilterPreset { - public int FilterID { get; set; } + public int FilterPresetID { get; set; } //public virtual Filter Parent { get; set; } - public int? ParentFilterID { get; set; } + public int? ParentFilterPresetID { get; set; } public string Name { get; set; } public bool ApplyAtSeriesLevel { get; set; } public bool Locked { get; set; } diff --git a/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs b/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs index 31067ab74..0a614acdf 100644 --- a/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs @@ -125,7 +125,7 @@ public void Save(SVR_AnimeGroup grp, bool updategrpcontractstats, bool recursive { RepoFactory.GroupFilter.CreateOrVerifyDirectoryFilters(false, grp.Contract?.Stat_AllTags, grp.Contract?.Stat_AllYears, grp.Contract?.Stat_AllSeasons); - RepoFactory.Filter.CreateOrVerifyDirectoryFilters(false, grp.Contract?.Stat_AllTags, + RepoFactory.FilterPreset.CreateOrVerifyDirectoryFilters(false, grp.Contract?.Stat_AllTags, grp.Contract?.Stat_AllYears, grp.Contract?.GetSeasons()); //This call will create extra years or tags if the Group have a new year or tag grp.UpdateGroupFilters(types); diff --git a/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs b/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs index c4ba9e0cb..903bc4f29 100644 --- a/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs @@ -285,7 +285,7 @@ private static void UpdateGroupFilters(SVR_AnimeSeries obj, Stopwatch sw, string $"Saving Series {animeID} | Updating Group Filters for Years ({string.Join(",", (IEnumerable<int>)allyears?.OrderBy(a => a) ?? Array.Empty<int>())}) and Seasons ({string.Join(",", (IEnumerable<string>)seasons ?? Array.Empty<string>())})"); RepoFactory.GroupFilter.CreateOrVerifyDirectoryFilters(false, obj.Contract?.AniDBAnime?.AniDBAnime?.GetAllTags(), allyears, seasons); - RepoFactory.Filter.CreateOrVerifyDirectoryFilters(false, + RepoFactory.FilterPreset.CreateOrVerifyDirectoryFilters(false, obj.Contract?.AniDBAnime?.AniDBAnime?.GetAllTags(), allyears, obj.Contract?.AniDBAnime?.AniDBAnime?.GetSeasons().ToHashSet()); // Update other existing filters diff --git a/Shoko.Server/Repositories/Cached/FilterRepository.cs b/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs similarity index 85% rename from Shoko.Server/Repositories/Cached/FilterRepository.cs rename to Shoko.Server/Repositories/Cached/FilterPresetRepository.cs index 44dc85134..abbc1bb53 100644 --- a/Shoko.Server/Repositories/Cached/FilterRepository.cs +++ b/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs @@ -23,32 +23,32 @@ namespace Shoko.Server.Repositories.Cached; -public class FilterRepository : BaseCachedRepository<Filter, int> +public class FilterPresetRepository : BaseCachedRepository<FilterPreset, int> { - private PocoIndex<int, Filter, int> Parents; + private PocoIndex<int, FilterPreset, int> Parents; private readonly ChangeTracker<int> Changes = new(); - public FilterRepository() + public FilterPresetRepository() { EndSaveCallback = obj => { - Changes.AddOrUpdate(obj.FilterID); + Changes.AddOrUpdate(obj.FilterPresetID); }; EndDeleteCallback = obj => { - Changes.Remove(obj.FilterID); + Changes.Remove(obj.FilterPresetID); }; } - protected override int SelectKey(Filter entity) + protected override int SelectKey(FilterPreset entity) { - return entity.FilterID; + return entity.FilterPresetID; } public override void PopulateIndexes() { Changes.AddOrUpdateRange(Cache.Keys); - Parents = Cache.CreateIndex(a => a.ParentFilterID ?? 0); + Parents = Cache.CreateIndex(a => a.ParentFilterPresetID ?? 0); } public override void RegenerateDb() { } @@ -63,7 +63,7 @@ public override void PostProcess() t, " " + Resources.GroupFilter_Cleanup); var all = GetAll(); - var set = new HashSet<Filter>(all); + var set = new HashSet<FilterPreset>(all); var notin = all.Except(set).ToList(); Delete(notin); } @@ -91,7 +91,7 @@ public void CreateOrVerifyLockedFilters() if (!lockedGFs.Any(a => a.Name == Constants.GroupFilterName.ContinueWatching)) { - var gf = new Filter + var gf = new FilterPreset { Name = Constants.GroupFilterName.ContinueWatching, Locked = true, @@ -106,7 +106,7 @@ public void CreateOrVerifyLockedFilters() //Create All filter if (!lockedGFs.Any(a => a.Name == Constants.GroupFilterName.All)) { - var gf = new Filter + var gf = new FilterPreset { Name = Constants.GroupFilterName.All, Locked = true, @@ -118,7 +118,7 @@ public void CreateOrVerifyLockedFilters() if (!lockedGFs.Any(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Tag))) { - var gf = new Filter + var gf = new FilterPreset { Name = Constants.GroupFilterName.Tags, FilterType = (GroupFilterType.Directory | GroupFilterType.Tag), @@ -129,7 +129,7 @@ public void CreateOrVerifyLockedFilters() if (!lockedGFs.Any(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Year))) { - var gf = new Filter + var gf = new FilterPreset { Name = Constants.GroupFilterName.Years, FilterType = (GroupFilterType.Directory | GroupFilterType.Year), @@ -140,7 +140,7 @@ public void CreateOrVerifyLockedFilters() if (!lockedGFs.Any(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Season))) { - var gf = new Filter + var gf = new FilterPreset { Name = Constants.GroupFilterName.Seasons, FilterType = (GroupFilterType.Directory | GroupFilterType.Season), @@ -196,9 +196,9 @@ public void CreateOrVerifyDirectoryFilters(bool frominit = false, ISet<string> t Resources.Filter_Filter + " " + cnt + "/" + max + " - " + s); } - var yf = new Filter + var yf = new FilterPreset { - ParentFilterID = tagsdirec.FilterID, + ParentFilterPresetID = tagsdirec.FilterPresetID, FilterType = GroupFilterType.Tag, ApplyAtSeriesLevel = true, Name = tinfo.ToTitleCase(s), @@ -249,9 +249,9 @@ public void CreateOrVerifyDirectoryFilters(bool frominit = false, ISet<string> t Resources.Filter_Filter + " " + cnt + "/" + max + " - " + s); } - var yf = new Filter + var yf = new FilterPreset { - ParentFilterID = yearsdirec.FilterID, + ParentFilterPresetID = yearsdirec.FilterPresetID, Name = s.ToString(), FilterType = GroupFilterType.Year, Locked = true, @@ -302,9 +302,9 @@ public void CreateOrVerifyDirectoryFilters(bool frominit = false, ISet<string> t Resources.Filter_Filter + " " + cnt + "/" + max + " - " + season); } - var yf = new Filter + var yf = new FilterPreset { - ParentFilterID = seasonsdirectory.FilterID, + ParentFilterPresetID = seasonsdirectory.FilterPresetID, Name = season.Season + " " + season.Year, Locked = true, FilterType = GroupFilterType.Season, @@ -328,7 +328,7 @@ public void CreateInitialFilters() if (GetTopLevel().Count > 6) return; // Favorites - var gf = new Filter + var gf = new FilterPreset { Name = Constants.GroupFilterName.Favorites, FilterType = GroupFilterType.UserDefined, @@ -338,7 +338,7 @@ public void CreateInitialFilters() Save(gf); // Missing Episodes - gf = new Filter + gf = new FilterPreset { Name = Constants.GroupFilterName.MissingEpisodes, FilterType = GroupFilterType.UserDefined, @@ -349,11 +349,11 @@ public void CreateInitialFilters() // Newly Added Series - gf = new Filter + gf = new FilterPreset { Name = Constants.GroupFilterName.NewlyAddedSeries, FilterType = GroupFilterType.UserDefined, - Expression = new GreaterThanEqualExpression + Expression = new DateGreaterThanEqualsExpression { Left = new DateAddFunction(new LastAddedDateSelector(), TimeSpan.FromDays(10)), Right = new TodayFunction() @@ -363,17 +363,17 @@ public void CreateInitialFilters() Save(gf); // Newly Airing Series - gf = new Filter + gf = new FilterPreset { Name = Constants.GroupFilterName.NewlyAiringSeries, FilterType = GroupFilterType.UserDefined, - Expression = new GreaterThanEqualExpression(new DateAddFunction(new LastAirDateSelector(), TimeSpan.FromDays(30)), new TodayFunction()), + Expression = new DateGreaterThanEqualsExpression(new DateAddFunction(new LastAirDateSelector(), TimeSpan.FromDays(30)), new TodayFunction()), SortingExpression = new LastAirDateSortingSelector { Descending = true } }; Save(gf); // Votes Needed - gf = new Filter + gf = new FilterPreset { Name = Constants.GroupFilterName.MissingVotes, ApplyAtSeriesLevel = true, @@ -405,12 +405,12 @@ public void CreateInitialFilters() Save(gf); // Recently Watched - gf = new Filter + gf = new FilterPreset { Name = Constants.GroupFilterName.RecentlyWatched, FilterType = GroupFilterType.UserDefined, Expression = new AndExpression(new HasWatchedEpisodesExpression(), new - GreaterThanEqualExpression(new DateAddFunction(new LastWatchedDateSelector(), TimeSpan.FromDays(10)), new TodayFunction())), + DateGreaterThanEqualsExpression(new DateAddFunction(new LastWatchedDateSelector(), TimeSpan.FromDays(10)), new TodayFunction())), SortingExpression = new LastWatchedDateSortingSelector { Descending = true @@ -419,7 +419,7 @@ public void CreateInitialFilters() Save(gf); // TvDB/MovieDB Link Missing - gf = new Filter + gf = new FilterPreset { Name = Constants.GroupFilterName.MissingLinks, ApplyAtSeriesLevel = true, @@ -430,12 +430,12 @@ public void CreateInitialFilters() Save(gf); } - public override void Save(Filter obj) + public override void Save(FilterPreset obj) { WriteLock(() => { base.Save(obj); }); } - public override void Save(IReadOnlyCollection<Filter> objs) + public override void Save(IReadOnlyCollection<FilterPreset> objs) { foreach (var obj in objs) { @@ -443,7 +443,7 @@ public override void Save(IReadOnlyCollection<Filter> objs) } } - public override void Delete(IReadOnlyCollection<Filter> objs) + public override void Delete(IReadOnlyCollection<FilterPreset> objs) { foreach (var cr in objs) { @@ -451,22 +451,38 @@ public override void Delete(IReadOnlyCollection<Filter> objs) } } - public List<Filter> GetByParentID(int parentid) + public List<FilterPreset> GetByParentID(int parentid) { return ReadLock(() => Parents.GetMultiple(parentid)); } - public List<Filter> GetTopLevel() + public FilterPreset GetTopLevelFilter(int filterID) + { + var parent = GetByID(filterID); + if (parent == null || parent.ParentFilterPresetID is null or 0) + return parent; + + while (true) + { + if (parent.ParentFilterPresetID is null or 0) return parent; + var next = GetByID(parent.ParentFilterPresetID.Value); + // should never happen, but it's not completely impossible + if (next == null) return parent; + parent = next; + } + } + + public List<FilterPreset> GetTopLevel() { return GetByParentID(0); } - public List<Filter> GetLockedGroupFilters() + public List<FilterPreset> GetLockedGroupFilters() { return ReadLock(() => Cache.Values.Where(a => a.Locked).ToList()); } - public List<Filter> GetTimeDependentFilters() + public List<FilterPreset> GetTimeDependentFilters() { return ReadLock(() => GetAll().Where(a => a.Expression.TimeDependent).ToList()); } diff --git a/Shoko.Server/Repositories/RepoFactory.cs b/Shoko.Server/Repositories/RepoFactory.cs index f8eee98e2..44cdb9360 100644 --- a/Shoko.Server/Repositories/RepoFactory.cs +++ b/Shoko.Server/Repositories/RepoFactory.cs @@ -94,7 +94,7 @@ public static class RepoFactory public static AnimeStaffRepository AnimeStaff { get; } = new(); public static CrossRef_Anime_StaffRepository CrossRef_Anime_Staff { get; } = new(); public static GroupFilterRepository GroupFilter { get; } = new(); - public static FilterRepository Filter { get; } = new(); + public static FilterPresetRepository FilterPreset { get; } = new(); /************** DEPRECATED **************/ /* We need to delete them at some point */ diff --git a/Shoko.Server/Server/Startup.cs b/Shoko.Server/Server/Startup.cs index d0526a0f2..69876c0f4 100644 --- a/Shoko.Server/Server/Startup.cs +++ b/Shoko.Server/Server/Startup.cs @@ -55,6 +55,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton<TvDBApiHelper>(); services.AddSingleton<MovieDBHelper>(); services.AddSingleton<FilterEvaluator>(); + services.AddSingleton<LegacyFilterConverter>(); services.AddScoped<CommonImplementation>(); services.AddSingleton<IShokoEventHandler>(ShokoEventHandler.Instance); services.AddSingleton<ICommandRequestFactory, CommandRequestFactory>(); diff --git a/Shoko.Server/Tasks/AnimeGroupCreator.cs b/Shoko.Server/Tasks/AnimeGroupCreator.cs index 5a4dfaba6..4f2c74f23 100644 --- a/Shoko.Server/Tasks/AnimeGroupCreator.cs +++ b/Shoko.Server/Tasks/AnimeGroupCreator.cs @@ -29,7 +29,7 @@ internal class AnimeGroupCreator private readonly AnimeGroupRepository _animeGroupRepo = RepoFactory.AnimeGroup; private readonly AnimeGroup_UserRepository _animeGroupUserRepo = RepoFactory.AnimeGroup_User; private readonly GroupFilterRepository _groupFilterRepo = RepoFactory.GroupFilter; - private readonly FilterRepository _filterRepo = RepoFactory.Filter; + private readonly FilterPresetRepository _filterRepo = RepoFactory.FilterPreset; private readonly JMMUserRepository _userRepo = RepoFactory.JMMUser; private readonly bool _autoGroupSeries; diff --git a/Shoko.Tests/Shoko.Tests/FilterTests.cs b/Shoko.Tests/Shoko.Tests/FilterTests.cs index 5a79366e5..ff623e277 100644 --- a/Shoko.Tests/Shoko.Tests/FilterTests.cs +++ b/Shoko.Tests/Shoko.Tests/FilterTests.cs @@ -15,6 +15,7 @@ namespace Shoko.Tests; public class FilterTests { + #region TestData private const string GroupFilterableString = "{\"MissingEpisodes\":0,\"MissingEpisodesCollecting\":0,\"Tags\":[\"Earth\",\"Japan\",\"Asia\",\"friendship\",\"daily life\",\"high school\",\"school life\",\"comedy\",\"Kyoto\",\"dynamic\",\"themes\",\"source material\",\"setting\",\"elements\",\"place\",\"manga\",\"funny expressions\",\"storytelling\",\"character driven\",\"facial distortion\",\"narration\",\"origin\",\"episodic\",\"Japanese production\"],\"CustomTags\":[],\"Years\":[2022],\"Seasons\":[{\"Item1\":2022,\"Item2\":1}],\"HasTvDBLink\":true,\"HasMissingTvDbLink\":false,\"HasTMDbLink\":true,\"HasMissingTMDbLink\":false,\"HasTraktLink\":false,\"HasMissingTraktLink\":true,\"IsFinished\":true,\"AirDate\":\"2022-04-06T20:00:00\",\"LastAirDate\":\"2022-06-22T20:00:00\",\"AddedDate\":\"2023-05-05T14:42:24.4477733\",\"LastAddedDate\":\"2023-05-05T13:37:50.32298\",\"EpisodeCount\":12,\"TotalEpisodeCount\":12,\"LowestAniDBRating\":7.4,\"HighestAniDBRating\":7.4,\"VideoSources\":[\"Web\"],\"AnimeTypes\":[\"TVSeries\"],\"AudioLanguages\":[\"japanese\"],\"SubtitleLanguages\":[\"english\"]}"; private const string GroupUserFilterableString = @@ -23,6 +24,9 @@ public class FilterTests "{\"MissingEpisodes\":0,\"MissingEpisodesCollecting\":0,\"Tags\":[\"high school\",\"dynamic\",\"themes\",\"source material\",\"setting\",\"elements\",\"place\",\"Earth\",\"Japan\",\"Kyoto\",\"manga\",\"Asia\",\"comedy\",\"friendship\",\"daily life\",\"school life\",\"funny expressions\",\"storytelling\",\"character driven\",\"facial distortion\",\"narration\",\"origin\",\"episodic\",\"Japanese production\"],\"CustomTags\":[],\"Years\":[2022],\"Seasons\":[{\"Item1\":2022,\"Item2\":1}],\"HasTvDBLink\":true,\"HasMissingTvDbLink\":false,\"HasTMDbLink\":false,\"HasMissingTMDbLink\":false,\"HasTraktLink\":false,\"HasMissingTraktLink\":true,\"IsFinished\":true,\"AirDate\":\"2022-04-06T20:00:00\",\"LastAirDate\":\"2022-06-22T20:00:00\",\"AddedDate\":\"2023-05-05T14:42:24.3131538\",\"LastAddedDate\":\"2023-05-05T13:37:50.32298\",\"EpisodeCount\":12,\"TotalEpisodeCount\":12,\"LowestAniDBRating\":7.4,\"HighestAniDBRating\":7.4,\"VideoSources\":[\"Web\"],\"AnimeTypes\":[\"TVSeries\"],\"AudioLanguages\":[\"japanese\"],\"SubtitleLanguages\":[\"english\"]}"; private const string SeriesUserFilterableString = "{\"IsFavorite\":false,\"WatchedEpisodes\":12,\"UnwatchedEpisodes\":0,\"HasVotes\":false,\"HasPermanentVotes\":false,\"MissingPermanentVotes\":false,\"WatchedDate\":\"2023-05-05T13:42:13.3933582\",\"LastWatchedDate\":\"2023-05-05T13:43:21.3729042\",\"LowestUserRating\":0.0,\"HighestUserRating\":0.0,\"MissingEpisodes\":0,\"MissingEpisodesCollecting\":0,\"Tags\":[\"high school\",\"dynamic\",\"themes\",\"source material\",\"setting\",\"elements\",\"place\",\"Earth\",\"Japan\",\"Kyoto\",\"manga\",\"Asia\",\"comedy\",\"friendship\",\"daily life\",\"school life\",\"funny expressions\",\"storytelling\",\"character driven\",\"facial distortion\",\"narration\",\"origin\",\"episodic\",\"Japanese production\"],\"CustomTags\":[],\"Years\":[2022],\"Seasons\":[{\"Item1\":2022,\"Item2\":1}],\"HasTvDBLink\":true,\"HasMissingTvDbLink\":false,\"HasTMDbLink\":false,\"HasMissingTMDbLink\":false,\"HasTraktLink\":false,\"HasMissingTraktLink\":true,\"IsFinished\":true,\"AirDate\":\"2022-04-06T20:00:00\",\"LastAirDate\":\"2022-06-22T20:00:00\",\"AddedDate\":\"2023-05-05T14:42:24.3131538\",\"LastAddedDate\":\"2023-05-05T13:37:50.32298\",\"EpisodeCount\":12,\"TotalEpisodeCount\":12,\"LowestAniDBRating\":7.4,\"HighestAniDBRating\":7.4,\"VideoSources\":[\"Web\"],\"AnimeTypes\":[\"TVSeries\"],\"AudioLanguages\":[\"japanese\"],\"SubtitleLanguages\":[\"english\"]}"; + private const string AllSeries = + ""; + #endregion public static readonly IEnumerable<object[]> GroupFilterable = new[] { new[] { JsonConvert.DeserializeObject<Filterable>(GroupFilterableString, new IReadOnlySetConverter()) }}; public static readonly IEnumerable<object[]> GroupUserFilterable = new[] { new[] { JsonConvert.DeserializeObject<UserDependentFilterable>(GroupUserFilterableString, new IReadOnlySetConverter()) }}; @@ -54,7 +58,7 @@ public void GroupFilterable_WithoutUserFilter_ExpectsTrue(Filterable group) public void GroupFilterable_WithDateFunctionFilter_ExpectsFalse(Filterable group) { var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), - new GreaterThanEqualExpression(new LastAddedDateSelector(), + new DateGreaterThanEqualsExpression(new LastAddedDateSelector(), new DateDiffFunction(new DateAddFunction(new TodayFunction(), TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1)), TimeSpan.FromDays(30)))); Assert.False(top.UserDependent); @@ -65,7 +69,7 @@ public void GroupFilterable_WithDateFunctionFilter_ExpectsFalse(Filterable group public void GroupFilterable_WithDateFunctionFilter_ExpectsTrue(Filterable group) { var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), - new GreaterThanEqualExpression(new LastAddedDateSelector(), new DateDiffFunction( + new DateGreaterThanEqualsExpression(new LastAddedDateSelector(), new DateDiffFunction( new DateAddFunction(new TodayFunction(), TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1)), TimeSpan.FromDays(120)))); Assert.False(top.UserDependent); From 1802b3df7d483d6fd8d099c32639e1f214db65e3 Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Sat, 16 Sep 2023 10:51:43 -0400 Subject: [PATCH 17/34] Start Removing GroupFilter. Massive Speedup of FilterEvaluator and Batch Evaluation --- Shoko.Server/API/AuthenticationController.cs | 2 +- .../ShokoServiceImplementation_Entities.cs | 4 +- Shoko.Server/API/v2/Modules/Common.cs | 4 - .../API/v3/Controllers/FilterController.cs | 15 +- .../API/v3/Controllers/UserController.cs | 2 +- Shoko.Server/API/v3/Helpers/FilterFactory.cs | 94 ++++++++++-- Shoko.Server/API/v3/Helpers/SeriesFactory.cs | 4 +- Shoko.Server/API/v3/Helpers/WebUIFactory.cs | 4 +- Shoko.Server/API/v3/Models/Shoko/Group.cs | 4 +- Shoko.Server/API/v3/Models/Shoko/User.cs | 2 +- .../CommandRequest_RefreshGroupFilter.cs | 17 +-- Shoko.Server/Databases/BaseDatabase.cs | 6 +- Shoko.Server/Filters/FilterEvaluator.cs | 52 ++++++- Shoko.Server/Filters/FilterExtensions.cs | 120 +++++++++------- Shoko.Server/Import/Importer.cs | 5 - Shoko.Server/Models/SVR_AniDB_Anime.cs | 2 +- Shoko.Server/Models/SVR_AnimeGroup.cs | 135 ++---------------- Shoko.Server/Models/SVR_AnimeGroup_User.cs | 18 --- Shoko.Server/Models/SVR_AnimeSeries.cs | 96 ++----------- Shoko.Server/Models/SVR_AnimeSeries_User.cs | 31 ---- Shoko.Server/Models/SVR_JMMUser.cs | 21 +-- Shoko.Server/Plex/PlexHelper.cs | 8 +- Shoko.Server/PlexAndKodi/Breadcrumbs.cs | 1 - .../Providers/TraktTV/TraktTVHelper.cs | 7 +- .../Cached/AniDB_AnimeRepository.cs | 36 +++-- .../Cached/AnimeGroupRepository.cs | 5 - .../Cached/AnimeGroup_UserRepository.cs | 40 ------ .../Cached/AnimeSeriesRepository.cs | 8 +- .../Cached/AnimeSeries_UserRepository.cs | 12 -- .../CrossRef_AniDB_TraktV2Repository.cs | 36 +---- .../Cached/FilterPresetRepository.cs | 23 +-- .../Repositories/Cached/JMMUserRepository.cs | 40 ------ ...CrossRef_Languages_AniDB_FileRepository.cs | 21 ++- ...CrossRef_Subtitles_AniDB_FileRepository.cs | 23 +-- Shoko.Server/Tasks/AnimeGroupCreator.cs | 5 - 35 files changed, 315 insertions(+), 588 deletions(-) diff --git a/Shoko.Server/API/AuthenticationController.cs b/Shoko.Server/API/AuthenticationController.cs index e327a7792..f77e32870 100644 --- a/Shoko.Server/API/AuthenticationController.cs +++ b/Shoko.Server/API/AuthenticationController.cs @@ -58,7 +58,7 @@ public ActionResult ChangePassword([FromBody] string newPassword) try { User.Password = Digest.Hash(newPassword.Trim()); - RepoFactory.JMMUser.Save(User, false); + RepoFactory.JMMUser.Save(User); RepoFactory.AuthTokens.DeleteAllWithUserID(User.JMMUserID); return Ok(); } diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs index 523a01d9b..b0e2ac3e5 100755 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs @@ -3672,7 +3672,7 @@ public string ChangePassword(int userID, string newPassword, bool revokeapikey) } jmmUser.Password = Digest.Hash(newPassword); - RepoFactory.JMMUser.Save(jmmUser, false); + RepoFactory.JMMUser.Save(jmmUser); if (revokeapikey) { RepoFactory.AuthTokens.DeleteAllWithUserID(jmmUser.JMMUserID); @@ -3780,7 +3780,7 @@ public string SaveUser(JMMUser user) } } - RepoFactory.JMMUser.Save(jmmUser, updateGf); + RepoFactory.JMMUser.Save(jmmUser); // update stats if (updateStats) diff --git a/Shoko.Server/API/v2/Modules/Common.cs b/Shoko.Server/API/v2/Modules/Common.cs index 454680d6a..2b4091ce4 100644 --- a/Shoko.Server/API/v2/Modules/Common.cs +++ b/Shoko.Server/API/v2/Modules/Common.cs @@ -2657,28 +2657,24 @@ internal ActionResult SerieVote(int id, int score, int uid) [HttpGet("cloud/list")] public ActionResult GetCloudAccounts() { - // TODO APIv2: Cloud return StatusCode(StatusCodes.Status501NotImplemented); } [HttpGet("cloud/count")] public ActionResult GetCloudAccountsCount() { - // TODO APIv2: Cloud return StatusCode(StatusCodes.Status501NotImplemented); } [HttpPost("cloud/add")] public ActionResult AddCloudAccount() { - // TODO APIv2: Cloud return StatusCode(StatusCodes.Status501NotImplemented); } [HttpPost("cloud/delete")] public ActionResult DeleteCloudAccount() { - // TODO APIv2: Cloud return StatusCode(StatusCodes.Status501NotImplemented); } diff --git a/Shoko.Server/API/v3/Controllers/FilterController.cs b/Shoko.Server/API/v3/Controllers/FilterController.cs index 2470e6639..1e8b9a961 100644 --- a/Shoko.Server/API/v3/Controllers/FilterController.cs +++ b/Shoko.Server/API/v3/Controllers/FilterController.cs @@ -52,19 +52,20 @@ public ActionResult<ListResult<Filter>> GetAllFilters([FromQuery] bool includeEm [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] bool withConditions = false) { var user = User; - return RepoFactory.FilterPreset.GetTopLevel() - .Where(filter => + + return _filterEvaluator.BatchEvaluateFilters(RepoFactory.FilterPreset.GetTopLevel(), user.JMMUserID) + .Where(kv => { + var filter = kv.Key; if (!showHidden && filter.Hidden) return false; - if (includeEmpty || (filter.IsDirectory() - ? RepoFactory.FilterPreset.GetByParentID(filter.FilterPresetID).Count > 0 - : _filterEvaluator.EvaluateFilter(filter, user.JMMUserID).Any())) + if (includeEmpty || (filter.IsDirectory() ? RepoFactory.FilterPreset.GetByParentID(filter.FilterPresetID).Count > 0 : kv.Value.Any())) return true; return false; }) + .Select(a => a.Key) .OrderBy(filter => filter.Name) .ToListResult(filter => _factory.GetFilter(filter, withConditions), page, pageSize); } @@ -239,7 +240,7 @@ internal static bool ResetPreviewFilterForUser(SVR_JMMUser user) if (!ModelState.IsValid) return ValidationProblem(ModelState); - var filter = _factory.MergeWithExisting(body, filterPreset, ModelState, true); + _factory.MergeWithExisting(body, filterPreset, ModelState, true); if (!ModelState.IsValid) return ValidationProblem(ModelState); @@ -255,7 +256,7 @@ internal static bool ResetPreviewFilterForUser(SVR_JMMUser user) public ActionResult<Filter.Input.CreateOrUpdateFilterBody> PutPreviewFilter([FromBody] Filter.Input.CreateOrUpdateFilterBody body) { var filterPreset = GetPreviewFilterForUser(User); - var filter = _factory.MergeWithExisting(body, filterPreset, ModelState, true); + _factory.MergeWithExisting(body, filterPreset, ModelState, true); if (!ModelState.IsValid) return ValidationProblem(ModelState); diff --git a/Shoko.Server/API/v3/Controllers/UserController.cs b/Shoko.Server/API/v3/Controllers/UserController.cs index 3b7f61074..2e38e4b09 100644 --- a/Shoko.Server/API/v3/Controllers/UserController.cs +++ b/Shoko.Server/API/v3/Controllers/UserController.cs @@ -230,7 +230,7 @@ private ActionResult ChangePassword(SVR_JMMUser user, User.Input.ChangePasswordB return Forbid("User must be admin to change other's password."); user.Password = string.IsNullOrEmpty(body.Password) ? "" : Digest.Hash(body.Password); - RepoFactory.JMMUser.Save(user, false); + RepoFactory.JMMUser.Save(user); if (body.RevokeAPIKeys) RepoFactory.AuthTokens.DeleteAllWithUserID(user.JMMUserID); diff --git a/Shoko.Server/API/v3/Helpers/FilterFactory.cs b/Shoko.Server/API/v3/Helpers/FilterFactory.cs index 2ad7bcf90..e85ab7808 100644 --- a/Shoko.Server/API/v3/Helpers/FilterFactory.cs +++ b/Shoko.Server/API/v3/Helpers/FilterFactory.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Globalization; using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -17,9 +18,9 @@ public class FilterFactory private readonly HttpContext _context; private readonly FilterEvaluator _evaluator; - public FilterFactory(HttpContext context, FilterEvaluator evaluator) + public FilterFactory(IHttpContextAccessor context, FilterEvaluator evaluator) { - _context = context; + _context = context.HttpContext; _evaluator = evaluator; } @@ -115,6 +116,69 @@ public static Filter.FilterCondition GetExpressionTree(FilterExpression expressi return result; } + + public FilterExpression<T> GetExpressionTree<T>(Filter.FilterCondition condition) + { + var type = Type.GetType(condition.Type + "Expression"); + if (type == null) throw new ArgumentException($"FilterCondition type {condition.Type}Expression was not found"); + var result = (FilterExpression<T>)Activator.CreateInstance(type); + + // Left/First + switch (result) + { + case IWithExpressionParameter left: + left.Left = GetExpressionTree<bool>(condition.Left); + break; + case IWithDateSelectorParameter left: + left.Left = GetExpressionTree<DateTime?>(condition.Left); + break; + case IWithNumberSelectorParameter left: + left.Left = GetExpressionTree<double>(condition.Left); + break; + case IWithStringSelectorParameter left: + left.Left = GetExpressionTree<string>(condition.Left); + break; + } + + // Parameters + switch (result) + { + case IWithStringParameter parameter: + parameter.Parameter = parameter.Parameter; + break; + case IWithNumberParameter parameter: + parameter.Parameter = double.Parse(condition.Parameter!); + break; + case IWithDateParameter parameter: + parameter.Parameter = DateTime.ParseExact(condition.Parameter!, "yyyy-MM-dd", CultureInfo.InvariantCulture.DateTimeFormat); + break; + case IWithTimeSpanParameter parameter: + parameter.Parameter = TimeSpan.ParseExact(condition.Parameter!, "G", CultureInfo.InvariantCulture.DateTimeFormat); + break; + } + + // Right/Second + switch (result) + { + case IWithSecondExpressionParameter right: + right.Right = GetExpressionTree<bool>(condition.Right); + break; + case IWithSecondDateSelectorParameter right: + right.Right = GetExpressionTree<DateTime?>(condition.Right); + break; + case IWithSecondStringSelectorParameter right: + right.Right = GetExpressionTree<string>(condition.Right); + break; + case IWithSecondNumberSelectorParameter right: + right.Right = GetExpressionTree<double>(condition.Right); + break; + case IWithSecondStringParameter right: + right.SecondParameter = condition.SecondParameter; + break; + } + + return result; + } public static Filter.SortingCriteria GetSortingCriteria(SortingExpression expression) { @@ -140,6 +204,18 @@ public static Filter.SortingCriteria GetSortingCriteria(SortingExpression expres return result; } + public static SortingExpression GetSortingCriteria(Filter.SortingCriteria criteria) + { + var type = Type.GetType(criteria.Type + "Selector"); + if (type == null) throw new ArgumentException($"SortingExpression type {criteria.Type}Selector was not found"); + var result = (SortingExpression)Activator.CreateInstance(type)!; + result.Descending = criteria.IsInverted; + + if (criteria.Next != null) result.Next = GetSortingCriteria(criteria.Next); + + return result; + } + public Filter.Input.CreateOrUpdateFilterBody CreateOrUpdateFilterBody(FilterPreset groupFilter) { var result = new Filter.Input.CreateOrUpdateFilterBody @@ -212,16 +288,8 @@ public Filter MergeWithExisting(Filter.Input.CreateOrUpdateFilterBody body, Filt groupFilter.ApplyAtSeriesLevel = body.ApplyAtSeriesLevel; if (!body.IsDirectory) { - if (body.Expression != null) - { - // TODO convert back from API model - //groupFilter.Expression = body.Expression - } - - if (body.Sorting != null) - { - // TODO Convert back from API model - } + if (body.Expression != null) groupFilter.Expression = GetExpressionTree<bool>(body.Expression); + if (body.Sorting != null) groupFilter.SortingExpression = GetSortingCriteria(body.Sorting); } // Skip saving if we're just going to preview a group filter. diff --git a/Shoko.Server/API/v3/Helpers/SeriesFactory.cs b/Shoko.Server/API/v3/Helpers/SeriesFactory.cs index 3f469d27a..74c040bf7 100644 --- a/Shoko.Server/API/v3/Helpers/SeriesFactory.cs +++ b/Shoko.Server/API/v3/Helpers/SeriesFactory.cs @@ -32,9 +32,9 @@ public class SeriesFactory private readonly AniDB_TagRepository _aniDBTagRepository; private readonly AniDB_Anime_TagRepository _aniDBAnimeTagRepository; - public SeriesFactory(HttpContext context) + public SeriesFactory(IHttpContextAccessor context) { - _context = context; + _context = context.HttpContext; _crossRefAnimeStaffRepository = RepoFactory.CrossRef_Anime_Staff; _animeCharacterRepository = RepoFactory.AnimeCharacter; _animeStaffRepository = RepoFactory.AnimeStaff; diff --git a/Shoko.Server/API/v3/Helpers/WebUIFactory.cs b/Shoko.Server/API/v3/Helpers/WebUIFactory.cs index e47654594..5e440f19b 100644 --- a/Shoko.Server/API/v3/Helpers/WebUIFactory.cs +++ b/Shoko.Server/API/v3/Helpers/WebUIFactory.cs @@ -12,9 +12,9 @@ public class WebUIFactory private readonly FilterFactory _filterFactory; private readonly SeriesFactory _seriesFactory; - public WebUIFactory(HttpContext context, FilterFactory filterFactory, SeriesFactory seriesFactory) + public WebUIFactory(IHttpContextAccessor context, FilterFactory filterFactory, SeriesFactory seriesFactory) { - _context = context; + _context = context.HttpContext; _filterFactory = filterFactory; _seriesFactory = seriesFactory; } diff --git a/Shoko.Server/API/v3/Models/Shoko/Group.cs b/Shoko.Server/API/v3/Models/Shoko/Group.cs index 00f553946..0e9c7c92a 100644 --- a/Shoko.Server/API/v3/Models/Shoko/Group.cs +++ b/Shoko.Server/API/v3/Models/Shoko/Group.cs @@ -4,10 +4,12 @@ using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.Models; using Shoko.Server.Repositories; +using Shoko.Server.Utilities; // ReSharper disable UnusedMember.Local // ReSharper disable UnusedAutoPropertyAccessor.Global @@ -97,7 +99,7 @@ public Group(HttpContext ctx, SVR_AnimeGroup group, bool randomiseImages = false HasCustomDescription = group.OverrideDescription == 1; // TODO make a factory for this file. Not feeling it rn - var factory = new SeriesFactory(ctx); + var factory = ctx.RequestServices.GetRequiredService<SeriesFactory>(); Images = mainSeries == null ? new Images() : factory.GetDefaultImages(mainSeries, randomiseImages); } diff --git a/Shoko.Server/API/v3/Models/Shoko/User.cs b/Shoko.Server/API/v3/Models/Shoko/User.cs index eb448bafc..c3dfef4b6 100644 --- a/Shoko.Server/API/v3/Models/Shoko/User.cs +++ b/Shoko.Server/API/v3/Models/Shoko/User.cs @@ -231,7 +231,7 @@ public CreateOrUpdateUserBody() { } } // Save the model now. - RepoFactory.JMMUser.Save(user, false); + RepoFactory.JMMUser.Save(user); return new User(user); } diff --git a/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs b/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs index 8cfca825f..3a1dc66bd 100644 --- a/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs +++ b/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs @@ -24,21 +24,8 @@ public class CommandRequest_RefreshGroupFilter : CommandRequestImplementation protected override void Process() { - if (GroupFilterID == 0) - { - RepoFactory.GroupFilter.CreateOrVerifyLockedFilters(); - RepoFactory.FilterPreset.CreateOrVerifyLockedFilters(); - return; - } - - var gf = RepoFactory.GroupFilter.GetByID(GroupFilterID); - if (gf == null) - { - return; - } - - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); + // TODO Remove this + RepoFactory.FilterPreset.CreateOrVerifyLockedFilters(); } public override void GenerateCommandID() diff --git a/Shoko.Server/Databases/BaseDatabase.cs b/Shoko.Server/Databases/BaseDatabase.cs index 0425595d3..f45d63a3f 100644 --- a/Shoko.Server/Databases/BaseDatabase.cs +++ b/Shoko.Server/Databases/BaseDatabase.cs @@ -280,13 +280,11 @@ public void PopulateInitialData() public void CreateOrVerifyLockedFilters() { - RepoFactory.GroupFilter.CreateOrVerifyLockedFilters(); RepoFactory.FilterPreset.CreateOrVerifyLockedFilters(); } private void CreateInitialGroupFilters() { - RepoFactory.GroupFilter.CreateInitialGroupFilters(); RepoFactory.FilterPreset.CreateInitialFilters(); } @@ -312,7 +310,7 @@ private void CreateInitialUsers() Password = defaultPassword, Username = settings.Database.DefaultUserUsername }; - RepoFactory.JMMUser.Save(defaultUser, true); + RepoFactory.JMMUser.Save(defaultUser); var familyUser = new SVR_JMMUser { @@ -324,7 +322,7 @@ private void CreateInitialUsers() Password = string.Empty, Username = "Family Friendly" }; - RepoFactory.JMMUser.Save(familyUser, true); + RepoFactory.JMMUser.Save(familyUser); } private void CreateInitialRenameScript() diff --git a/Shoko.Server/Filters/FilterEvaluator.cs b/Shoko.Server/Filters/FilterEvaluator.cs index c5ad604e4..a61fcc858 100644 --- a/Shoko.Server/Filters/FilterEvaluator.cs +++ b/Shoko.Server/Filters/FilterEvaluator.cs @@ -33,10 +33,10 @@ public IEnumerable<IGrouping<int, int>> EvaluateFilter(FilterPreset filter, int? var filterables = filter.ApplyAtSeriesLevel switch { - true when user => _series.GetAll().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), - true => _series.GetAll().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToFilterable())), - false when user => _groups.GetAll().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), - false => _groups.GetAll().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToFilterable())) + true when user => _series.GetAll().AsParallel().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), + true => _series.GetAll().AsParallel().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToFilterable())), + false when user => _groups.GetAll().AsParallel().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), + false => _groups.GetAll().AsParallel().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToFilterable())) }; // Filtering @@ -53,6 +53,50 @@ public IEnumerable<IGrouping<int, int>> EvaluateFilter(FilterPreset filter, int? return result; } + + /// <summary> + /// Evaluate the given filter, applying the necessary logic + /// </summary> + /// <param name="filters"></param> + /// <param name="userID"></param> + /// <returns>SeriesIDs, grouped by the direct parent GroupID</returns> + /// <exception cref="ArgumentNullException"></exception> + public Dictionary<FilterPreset, IEnumerable<IGrouping<int, int>>> BatchEvaluateFilters(List<FilterPreset> filters, int? userID) + { + ArgumentNullException.ThrowIfNull(filters); + if (!filters.Any()) return new Dictionary<FilterPreset, IEnumerable<IGrouping<int, int>>>(); + var user = filters.Any(a => a.Expression?.UserDependent ?? false); + if (user && userID == null) throw new ArgumentNullException(nameof(userID)); + + var filterables = filters.Any(a => a.ApplyAtSeriesLevel) switch + { + true when user => _series.GetAll().AsParallel().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), + true => _series.GetAll().AsParallel().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToFilterable())), + false when user => _groups.GetAll().AsParallel().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), + false => _groups.GetAll().AsParallel().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToFilterable())) + }; + + // Filtering + var filtered = filterables.SelectMany(a => filters.Select(f => (Filter: f, FilterableWithID: a))) + .Where(a => a.Filter.Expression?.Evaluate(a.FilterableWithID.Filterable) ?? true); + + // ordering + var grouped = filtered.GroupBy(a => a.Filter).ToDictionary(a => a.Key, f => + { + var ordered = OrderFilterables(f.Key, f.Select(a => a.FilterableWithID)); + + var result = ordered.GroupBy(a => a.GroupID, a => a.SeriesID); + if (!f.Key.ApplyAtSeriesLevel) + result = result.Select(a => new Grouping(a.Key, _series.GetByGroupID(a.Key).Select(ser => ser.AnimeSeriesID).ToArray())); + + return result; + }); + + foreach (var filter in filters.Where(filter => !grouped.ContainsKey(filter))) + grouped.Add(filter, Array.Empty<IGrouping<int, int>>()); + + return grouped; + } private static IOrderedEnumerable<FilterableWithID> OrderFilterables(FilterPreset filter, IEnumerable<FilterableWithID> filtered) { diff --git a/Shoko.Server/Filters/FilterExtensions.cs b/Shoko.Server/Filters/FilterExtensions.cs index e3dea6db6..b0f069633 100644 --- a/Shoko.Server/Filters/FilterExtensions.cs +++ b/Shoko.Server/Filters/FilterExtensions.cs @@ -19,6 +19,14 @@ public static Filterable ToFilterable(this SVR_AnimeSeries series) { var anime = series.GetAnime(); var name = series.GetSeriesName(); + + var audio = new HashSet<string>(); + var audioNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Languages.Select(b => b.LanguageName)); + if (audioNames.Any()) audio = audioNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + var subtitles = new HashSet<string>(); + var subtitleNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Subtitles.Select(b => b.LanguageName)); + if (subtitleNames.Any()) subtitles = subtitleNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + // TODO optimize this a bunch. Lots of duplicate calls. Contract should be severely trimmed var filterable = new Filterable { @@ -46,8 +54,8 @@ public static Filterable ToFilterable(this SVR_AnimeSeries series) LastAddedDate = series.GetVideoLocals().Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), EpisodeCount = anime?.EpisodeCountNormal ?? 0, TotalEpisodeCount = anime?.EpisodeCount ?? 0, - LowestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero), - HighestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero), + LowestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), + HighestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), AnimeTypes = anime == null ? new HashSet<string>() : new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) @@ -57,13 +65,9 @@ public static Filterable ToFilterable(this SVR_AnimeSeries series) VideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality ?? new HashSet<string>(), SharedVideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), AudioLanguages = series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet<string>(), - SharedAudioLanguages = - series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Languages.Select(b => b.LanguageName)) - .Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(), + SharedAudioLanguages = audio, SubtitleLanguages = series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet<string>(), - SharedSubtitleLanguages = - series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Subtitles.Select(b => b.LanguageName)) - .Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(), + SharedSubtitleLanguages = subtitles, }; return filterable; @@ -76,6 +80,14 @@ public static UserDependentFilterable ToUserDependentFilterable(this SVR_AnimeSe var vote = anime?.UserVote; var watchedDates = series.GetVideoLocals().Select(a => a.GetUserRecord(userID)?.WatchedDate).Where(a => a != null).OrderBy(a => a).ToList(); var name = series.GetSeriesName(); + + var audio = new HashSet<string>(); + var audioNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Languages.Select(b => b.LanguageName)); + if (audioNames.Any()) audio = audioNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + var subtitles = new HashSet<string>(); + var subtitleNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Subtitles.Select(b => b.LanguageName)); + if (subtitleNames.Any()) subtitles = subtitleNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + var filterable = new UserDependentFilterable { Name = name, @@ -102,8 +114,8 @@ public static UserDependentFilterable ToUserDependentFilterable(this SVR_AnimeSe LastAddedDate = series.GetVideoLocals().Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), EpisodeCount = anime?.EpisodeCountNormal ?? 0, TotalEpisodeCount = anime?.EpisodeCount ?? 0, - LowestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero), - HighestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero), + LowestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), + HighestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), AnimeTypes = anime == null ? new HashSet<string>() : new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) @@ -113,13 +125,9 @@ public static UserDependentFilterable ToUserDependentFilterable(this SVR_AnimeSe VideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality ?? new HashSet<string>(), SharedVideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), AudioLanguages = series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet<string>(), - SharedAudioLanguages = - series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Languages.Select(b => b.LanguageName)) - .Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(), + SharedAudioLanguages = audio, SubtitleLanguages = series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet<string>(), - SharedSubtitleLanguages = - series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Subtitles.Select(b => b.LanguageName)) - .Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(), + SharedSubtitleLanguages = subtitles, IsFavorite = false, WatchedEpisodes = user?.WatchedCount ?? 0, UnwatchedEpisodes = (anime?.EpisodeCount ?? 0) - (user?.WatchedCount ?? 0), @@ -211,8 +219,21 @@ private static bool HasMissingTvDBLink(SVR_AnimeSeries series) public static Filterable ToFilterable(this SVR_AnimeGroup group) { - var series = group.GetAllSeries(); - var hasTrakt = series.All(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()); + var series = group.GetAllSeries(true); + var anime = group.Anime; + var hasTrakt = series.Any(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()); + var audio = new HashSet<string>(); + var audioLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Languages.Select(b => b.LanguageName)); + if (audioLanguageNames.Any()) + audio = audioLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) + .ToHashSet(); + var subtitles = new HashSet<string>(); + var subtitleLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Subtitles.Select(b => b.LanguageName)); + if (subtitleLanguageNames.Any()) + subtitles = subtitleLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + // TODO optimize this a bunch. Lots of duplicate calls. Contract should be severely trimmed var filterable = new Filterable { @@ -232,7 +253,7 @@ public static Filterable ToFilterable(this SVR_AnimeGroup group) var parts = a.Split(' '); return (int.Parse(parts[1]), Enum.Parse<AnimeSeason>(parts[0])); }).ToHashSet(), - HasTvDBLink = series.All(a => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(a.AniDB_ID).Any()), + HasTvDBLink = series.Any(a => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(a.AniDB_ID).Any()), HasMissingTvDbLink = HasMissingTvDBLink(group), HasTMDbLink = group.Contract?.Stat_HasMovieDBLink ?? false, HasMissingTMDbLink = HasMissingTMDbLink(group), @@ -243,23 +264,15 @@ public static Filterable ToFilterable(this SVR_AnimeGroup group) LastAddedDate = series.SelectMany(a => a.GetVideoLocals()).Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), EpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCountNormal ?? 0), TotalEpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCount ?? 0), - LowestAniDBRating = - group.Anime.DefaultIfEmpty().Min(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), - HighestAniDBRating = - group.Anime.DefaultIfEmpty().Max(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), - AnimeTypes = new HashSet<string>(group.Anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), + LowestAniDBRating = anime.Select(a => decimal.Round(Convert.ToDecimal(a?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero)).DefaultIfEmpty().Min(), + HighestAniDBRating = anime.Select(a => decimal.Round(Convert.ToDecimal(a?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero)).DefaultIfEmpty().Max(), + AnimeTypes = new HashSet<string>(anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), VideoSources = group.Contract?.Stat_AllVideoQuality ?? new HashSet<string>(), SharedVideoSources = group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), AudioLanguages = group.Contract?.Stat_AudioLanguages ?? new HashSet<string>(), - SharedAudioLanguages = - series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) - .Select(a => a.Languages.Select(b => b.LanguageName)).Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) - .ToHashSet(), + SharedAudioLanguages = audio, SubtitleLanguages = group.Contract?.Stat_SubtitleLanguages ?? new HashSet<string>(), - SharedSubtitleLanguages = - series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) - .Select(a => a.Subtitles.Select(b => b.LanguageName)).Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) - .ToHashSet(), + SharedSubtitleLanguages = subtitles }; return filterable; @@ -268,15 +281,28 @@ public static Filterable ToFilterable(this SVR_AnimeGroup group) public static Filterable ToUserDependentFilterable(this SVR_AnimeGroup group, int userID) { var series = group.GetAllSeries(true); - var hasTrakt = series.All(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()); + var anime = group.Anime; + var hasTrakt = series.Any(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()); var user = group.GetUserRecord(userID); - var vote = group.Anime.Select(a => a.UserVote).Where(a => a is { VoteType: (int)VoteType.AnimePermanent or (int)VoteType.AnimeTemporary }) + var vote = anime.Select(a => a.UserVote).Where(a => a is { VoteType: (int)VoteType.AnimePermanent or (int)VoteType.AnimeTemporary }) .Select(a => a.VoteValue).OrderBy(a => a).ToList(); - var hasPermanent = group.Anime.Select(a => a.UserVote).Any(a => a is { VoteType: (int)VoteType.AnimePermanent }); - var missingPermanent = - group.Anime.Any(a => a.UserVote is not { VoteType: (int)VoteType.AnimePermanent } && a.EndDate != null && a.EndDate > DateTime.Now); + var hasPermanent = anime.Select(a => a.UserVote).Any(a => a is { VoteType: (int)VoteType.AnimePermanent }); + var missingPermanent = anime.Any(a => a.UserVote is not { VoteType: (int)VoteType.AnimePermanent } && a.EndDate != null && a.EndDate > DateTime.Now); var watchedDates = series.SelectMany(a => a.GetVideoLocals()).Select(a => a.GetUserRecord(userID)?.WatchedDate).Where(a => a != null).OrderBy(a => a) .ToList(); + + var audio = new HashSet<string>(); + var audioLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Languages.Select(b => b.LanguageName)); + if (audioLanguageNames.Any()) + audio = audioLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) + .ToHashSet(); + var subtitles = new HashSet<string>(); + var subtitleLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Subtitles.Select(b => b.LanguageName)); + if (subtitleLanguageNames.Any()) + subtitles = subtitleLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + // TODO optimize this a bunch. Lots of duplicate calls. Contract should be severely trimmed var filterable = new UserDependentFilterable { @@ -296,7 +322,7 @@ public static Filterable ToUserDependentFilterable(this SVR_AnimeGroup group, in var parts = a.Split(' '); return (int.Parse(parts[1]), Enum.Parse<AnimeSeason>(parts[0])); }).ToHashSet(), - HasTvDBLink = series.All(a => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(a.AniDB_ID).Any()), + HasTvDBLink = series.Any(a => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(a.AniDB_ID).Any()), HasMissingTvDbLink = HasMissingTvDBLink(group), HasTMDbLink = group.Contract?.Stat_HasMovieDBLink ?? false, HasMissingTMDbLink = HasMissingTMDbLink(group), @@ -308,22 +334,18 @@ public static Filterable ToUserDependentFilterable(this SVR_AnimeGroup group, in EpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCountNormal ?? 0), TotalEpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCount ?? 0), LowestAniDBRating = - group.Anime.DefaultIfEmpty().Min(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), + anime.Select(a => decimal.Round(Convert.ToDecimal(a?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero)).DefaultIfEmpty() + .Min(), HighestAniDBRating = - group.Anime.DefaultIfEmpty().Max(anime => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 00) / 100, 1, MidpointRounding.AwayFromZero)), - AnimeTypes = new HashSet<string>(group.Anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), + anime.Select(a => decimal.Round(Convert.ToDecimal(a?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero)).DefaultIfEmpty() + .Max(), + AnimeTypes = new HashSet<string>(anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), VideoSources = group.Contract?.Stat_AllVideoQuality ?? new HashSet<string>(), SharedVideoSources = group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), AudioLanguages = group.Contract?.Stat_AudioLanguages ?? new HashSet<string>(), - SharedAudioLanguages = - series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) - .Select(a => a.Languages.Select(b => b.LanguageName)).Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) - .ToHashSet(), + SharedAudioLanguages = audio, SubtitleLanguages = group.Contract?.Stat_SubtitleLanguages ?? new HashSet<string>(), - SharedSubtitleLanguages = - series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) - .Select(a => a.Subtitles.Select(b => b.LanguageName)).Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) - .ToHashSet(), + SharedSubtitleLanguages = subtitles, IsFavorite = user?.IsFave == 1, WatchedEpisodes = user?.WatchedCount ?? 0, UnwatchedEpisodes = user?.UnwatchedEpisodeCount ?? 0, diff --git a/Shoko.Server/Import/Importer.cs b/Shoko.Server/Import/Importer.cs index fe688b9b5..71f92b96c 100755 --- a/Shoko.Server/Import/Importer.cs +++ b/Shoko.Server/Import/Importer.cs @@ -1139,11 +1139,6 @@ public static void UpdateAllStats() ser.QueueUpdateStats(); } - foreach (var gf in RepoFactory.GroupFilter.GetAll()) - { - gf.QueueUpdate(); - } - commandFactory.CreateAndSave<CommandRequest_RefreshGroupFilter>(c => c.GroupFilterID = 0); } diff --git a/Shoko.Server/Models/SVR_AniDB_Anime.cs b/Shoko.Server/Models/SVR_AniDB_Anime.cs index ba4a88ba5..75ea3df4b 100644 --- a/Shoko.Server/Models/SVR_AniDB_Anime.cs +++ b/Shoko.Server/Models/SVR_AniDB_Anime.cs @@ -115,7 +115,7 @@ public List<CrossRef_AniDB_TraktV2> GetCrossRefTraktV2() public List<CrossRef_AniDB_TraktV2> GetCrossRefTraktV2(ISession session) { - return RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(session, AnimeID); + return RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(AnimeID); } public List<CrossRef_AniDB_MAL> GetCrossRefMAL() diff --git a/Shoko.Server/Models/SVR_AnimeGroup.cs b/Shoko.Server/Models/SVR_AnimeGroup.cs index 85bd00def..ae948c420 100644 --- a/Shoko.Server/Models/SVR_AnimeGroup.cs +++ b/Shoko.Server/Models/SVR_AnimeGroup.cs @@ -197,7 +197,7 @@ public static void RenameAllGroups() public List<SVR_AniDB_Anime> Anime => - GetSeries().Select(serie => serie.GetAnime()).Where(anime => anime != null).ToList(); + RepoFactory.AnimeSeries.GetByGroupID(AnimeGroupID).Select(s => s.GetAnime()).Where(anime => anime != null).ToList(); public decimal AniDBRating { @@ -283,9 +283,9 @@ public void SetMainSeries(SVR_AnimeSeries series) if (series == null && (IsManuallyNamed == 0 || OverrideDescription == 0)) series = GetMainSeries(); if (IsManuallyNamed == 0) - GroupName = SortName = series.GetSeriesName(); + GroupName = SortName = series!.GetSeriesName(); if (OverrideDescription == 0) - Description = series.GetAnime().Description; + Description = series!.GetAnime().Description; // Save the changes for this group only. DateTimeUpdated = DateTime.Now; @@ -294,29 +294,21 @@ public void SetMainSeries(SVR_AnimeSeries series) public List<SVR_AnimeSeries> GetSeries() { - var seriesList = RepoFactory.AnimeSeries.GetByGroupID(AnimeGroupID) - .OrderBy(a => a.AirDate) - .ToList(); + // Make sure the default/main series is the first, if it's directly within the group + if (DefaultAnimeSeriesID == null && MainAniDBAnimeID == null) + return RepoFactory.AnimeSeries.GetByGroupID(AnimeGroupID).OrderBy(a => a.AirDate).ToList(); - // Make sure the default/main series is the first, if it's directly - // within the group. - if (DefaultAnimeSeriesID.HasValue || MainAniDBAnimeID.HasValue) - { - SVR_AnimeSeries mainSeries = null; - if (DefaultAnimeSeriesID.HasValue) - mainSeries = seriesList.FirstOrDefault(ser => ser.AnimeSeriesID == DefaultAnimeSeriesID.Value); - - if (mainSeries == null && MainAniDBAnimeID.HasValue) - mainSeries = seriesList.FirstOrDefault(ser => ser.AniDB_ID == MainAniDBAnimeID.Value); + SVR_AnimeSeries mainSeries = null; + if (DefaultAnimeSeriesID.HasValue) mainSeries = RepoFactory.AnimeSeries.GetByID(DefaultAnimeSeriesID.Value); + if (mainSeries == null && MainAniDBAnimeID.HasValue) mainSeries = RepoFactory.AnimeSeries.GetByAnimeID(MainAniDBAnimeID.Value); - if (mainSeries != null) - { - seriesList.Remove(mainSeries); - seriesList.Insert(0, mainSeries); - } - } + var seriesList = RepoFactory.AnimeSeries.GetByGroupID(AnimeGroupID).OrderBy(a => a.AirDate).ToList(); + if (mainSeries == null) return seriesList; + seriesList.Remove(mainSeries); + seriesList.Insert(0, mainSeries); return seriesList; + } public List<SVR_AnimeSeries> GetAllSeries(bool skipSorting = false) @@ -1248,68 +1240,6 @@ private static ILookup<int, string> GetVideoQualities(IEnumerable<int> groupIds .Distinct().Select(a => (GroupID: id, Source: a))).ToLookup(a => a.GroupID, a => a.Source); } - public void DeleteFromFilters() - { - foreach (var gf in RepoFactory.GroupFilter.GetAll()) - { - var change = false; - foreach (var k in gf.GroupsIds.Keys) - { - if (gf.GroupsIds[k].Contains(AnimeGroupID)) - { - gf.GroupsIds[k].Remove(AnimeGroupID); - change = true; - } - } - - if (change) - { - RepoFactory.GroupFilter.Save(gf); - } - } - } - - public void UpdateGroupFilters(HashSet<GroupFilterConditionType> types, SVR_JMMUser user = null) - { - IReadOnlyList<SVR_JMMUser> users = new List<SVR_JMMUser> { user }; - if (user == null) - { - users = RepoFactory.JMMUser.GetAll(); - } - - var tosave = new List<SVR_GroupFilter>(); - - var n = new HashSet<GroupFilterConditionType>(types); - var gfs = RepoFactory.GroupFilter.GetWithConditionTypesAndAll(n); - logger.Trace($"Updating {gfs.Count} Group Filters from Group {GroupName}"); - foreach (var gf in gfs) - { - if (gf.UpdateGroupFilterFromGroup(Contract, null)) - { - if (!tosave.Contains(gf)) - { - tosave.Add(gf); - } - } - - foreach (var u in users) - { - var cgrp = GetUserContract(u.JMMUserID, n); - - if (gf.UpdateGroupFilterFromGroup(cgrp, u)) - { - if (!tosave.Contains(gf)) - { - tosave.Add(gf); - } - } - } - } - - RepoFactory.GroupFilter.Save(tosave); - } - - public static void GetAnimeGroupsRecursive(int animeGroupID, ref List<SVR_AnimeGroup> groupList) { var grp = RepoFactory.AnimeGroup.GetByID(animeGroupID); @@ -1353,43 +1283,6 @@ public void DeleteGroup(bool updateParent = true) subGroup.DeleteGroup(false); } - var gfs = - RepoFactory.GroupFilter.GetWithConditionsTypes(new HashSet<GroupFilterConditionType> - { - GroupFilterConditionType.AnimeGroup - }); - foreach (var gf in gfs) - { - var c = gf.Conditions.RemoveAll(a => - { - if (a.ConditionType != (int)GroupFilterConditionType.AnimeGroup) - { - return false; - } - - if (!int.TryParse(a.ConditionParameter, out var thisGrpID)) - { - return false; - } - - if (thisGrpID != AnimeGroupID) - { - return false; - } - - return true; - }); - if (gf.Conditions.Count <= 0) - { - RepoFactory.GroupFilter.Delete(gf.GroupFilterID); - } - else - { - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - } - } - RepoFactory.AnimeGroup.Delete(this); // finally update stats diff --git a/Shoko.Server/Models/SVR_AnimeGroup_User.cs b/Shoko.Server/Models/SVR_AnimeGroup_User.cs index 812e8e607..92e2b5863 100644 --- a/Shoko.Server/Models/SVR_AnimeGroup_User.cs +++ b/Shoko.Server/Models/SVR_AnimeGroup_User.cs @@ -60,24 +60,6 @@ public SVR_AnimeGroup_User(int userID, int groupID) StoppedCount = 0; } - - public void UpdateGroupFilters(HashSet<GroupFilterConditionType> types) - { - var grp = RepoFactory.AnimeGroup.GetByID(AnimeGroupID); - var usr = RepoFactory.JMMUser.GetByID(JMMUserID); - if (grp != null && usr != null) - { - grp.UpdateGroupFilters(types, usr); - } - } - - public void DeleteFromFilters() - { - var toSave = RepoFactory.GroupFilter.GetAll().AsParallel() - .Where(gf => gf.DeleteGroupFromFilters(JMMUserID, AnimeGroupID)).ToList(); - RepoFactory.GroupFilter.Save(toSave); - } - public void UpdatePlexKodiContracts() { var grp = RepoFactory.AnimeGroup.GetByID(AnimeGroupID); diff --git a/Shoko.Server/Models/SVR_AnimeSeries.cs b/Shoko.Server/Models/SVR_AnimeSeries.cs index 7d2bb0fe1..f92fa252d 100644 --- a/Shoko.Server/Models/SVR_AnimeSeries.cs +++ b/Shoko.Server/Models/SVR_AnimeSeries.cs @@ -377,7 +377,7 @@ public List<TvDB_Series> GetTvDBSeries() public List<CrossRef_AniDB_TraktV2> GetCrossRefTraktV2(ISession session) { - return RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(session, AniDB_ID); + return RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(AniDB_ID); } public List<Trakt_Show> GetTraktShow() @@ -707,44 +707,31 @@ public SVR_AniDB_Anime GetAnime() return RepoFactory.AniDB_Anime.GetByAnimeID(AniDB_ID); } + private DateTime? _airDate; public DateTime? AirDate { get { + if (_airDate != null) return _airDate; var anime = GetAnime(); if (anime?.AirDate != null) - { - return anime.AirDate.Value; - } + return _airDate = anime.AirDate.Value; // This will be slower, but hopefully more accurate - var ep = GetAnimeEpisodes() - .Select(a => a.AniDB_Episode) - .Where(a => (a.EpisodeType == (int)EpisodeType.Episode) && a.LengthSeconds > 0) - .Select(a => a.GetAirDateAsDate()) - .Where(a => a != null) - .OrderBy(a => a) - .FirstOrDefault(); - if (ep != null) - { - return ep.Value; - } - - return null; + var ep = RepoFactory.AniDB_Episode.GetByAnimeID(AniDB_ID) + .Where(a => a.EpisodeType == (int)EpisodeType.Episode && a.LengthSeconds > 0 && a.AirDate != 0) + .MinBy(a => a.AirDate); + return _airDate = ep?.GetAirDateAsDate(); } } + private DateTime? _endDate; public DateTime? EndDate { get { - var anime = GetAnime(); - if (anime?.EndDate != null) - { - return anime.EndDate.Value; - } - - return null; + if (_endDate != null) return _endDate; + return _endDate = GetAnime()?.EndDate; } } @@ -935,67 +922,6 @@ public List<SVR_AnimeGroup> AllGroupsAbove } } - public void UpdateGroupFilters(HashSet<GroupFilterConditionType> types, SVR_JMMUser user = null) - { - IReadOnlyList<SVR_JMMUser> users = new List<SVR_JMMUser> { user }; - if (user == null) - { - users = RepoFactory.JMMUser.GetAll(); - } - - var tosave = new List<SVR_GroupFilter>(); - - var n = new HashSet<GroupFilterConditionType>(types); - var gfs = RepoFactory.GroupFilter.GetWithConditionTypesAndAll(n); - logger.Trace($"Updating {gfs.Count} Group Filters from Series {GetAnime().MainTitle}"); - foreach (var gf in gfs) - { - if (gf.UpdateGroupFilterFromSeries(Contract, null)) - { - if (!tosave.Contains(gf)) - { - tosave.Add(gf); - } - } - - foreach (var u in users) - { - var cgrp = GetUserContract(u.JMMUserID, n); - - if (gf.UpdateGroupFilterFromSeries(cgrp, u)) - { - if (!tosave.Contains(gf)) - { - tosave.Add(gf); - } - } - } - } - - RepoFactory.GroupFilter.Save(tosave); - } - - public void DeleteFromFilters() - { - foreach (var gf in RepoFactory.GroupFilter.GetAll()) - { - var change = false; - foreach (var k in gf.SeriesIds.Keys) - { - if (gf.SeriesIds[k].Contains(AnimeSeriesID)) - { - gf.SeriesIds[k].Remove(AnimeSeriesID); - change = true; - } - } - - if (change) - { - RepoFactory.GroupFilter.Save(gf); - } - } - } - public static Dictionary<int, HashSet<GroupFilterConditionType>> BatchUpdateContracts(ISessionWrapper session, IReadOnlyCollection<SVR_AnimeSeries> seriesBatch, bool onlyStats = false) { diff --git a/Shoko.Server/Models/SVR_AnimeSeries_User.cs b/Shoko.Server/Models/SVR_AnimeSeries_User.cs index cc7ef9669..13490314b 100644 --- a/Shoko.Server/Models/SVR_AnimeSeries_User.cs +++ b/Shoko.Server/Models/SVR_AnimeSeries_User.cs @@ -82,35 +82,4 @@ public static HashSet<GroupFilterConditionType> GetConditionTypesChanged(SVR_Ani return h; } - - public void UpdateGroupFilter(HashSet<GroupFilterConditionType> types) - { - var ser = RepoFactory.AnimeSeries.GetByID(AnimeSeriesID); - var usr = RepoFactory.JMMUser.GetByID(JMMUserID); - if (ser != null && usr != null) - { - ser.UpdateGroupFilters(types, usr); - } - } - - public void DeleteFromFilters() - { - foreach (var gf in RepoFactory.GroupFilter.GetAll()) - { - var change = false; - if (gf.SeriesIds.ContainsKey(JMMUserID)) - { - if (gf.SeriesIds[JMMUserID].Contains(AnimeSeriesID)) - { - gf.SeriesIds[JMMUserID].Remove(AnimeSeriesID); - change = true; - } - } - - if (change) - { - RepoFactory.GroupFilter.Save(gf); - } - } - } } diff --git a/Shoko.Server/Models/SVR_JMMUser.cs b/Shoko.Server/Models/SVR_JMMUser.cs index 2a9583d35..f03590c70 100644 --- a/Shoko.Server/Models/SVR_JMMUser.cs +++ b/Shoko.Server/Models/SVR_JMMUser.cs @@ -89,7 +89,7 @@ public bool SetAvatarImage(byte[] byteArray, string contentType, string fieldNam } if (!skipSave) - RepoFactory.JMMUser.Save(this, false); + RepoFactory.JMMUser.Save(this); return true; } @@ -100,7 +100,7 @@ public void RemoveAvatarImage(bool skipSave = false) AvatarImageMetadata = null; if (!skipSave) - RepoFactory.JMMUser.Save(this, false); + RepoFactory.JMMUser.Save(this); } #endregion @@ -148,23 +148,6 @@ public static bool CompareUser(JMMUser olduser, JMMUser newuser) return false; } - public void UpdateGroupFilters() - { - IReadOnlyList<SVR_GroupFilter> gfs = RepoFactory.GroupFilter.GetAll(); - List<SVR_AnimeGroup> allGrps = RepoFactory.AnimeGroup.GetAllTopLevelGroups(); // No Need of subgroups - foreach (SVR_GroupFilter gf in gfs) - { - bool change = false; - foreach (SVR_AnimeGroup grp in allGrps) - { - CL_AnimeGroup_User cgrp = grp.GetUserContract(JMMUserID); - change |= gf.UpdateGroupFilterFromGroup(cgrp, this); - } - if (change) - RepoFactory.GroupFilter.Save(gf); - } - } - // IUserIdentity implementation public string UserName { diff --git a/Shoko.Server/Plex/PlexHelper.cs b/Shoko.Server/Plex/PlexHelper.cs index 09a591e82..0ec56144f 100644 --- a/Shoko.Server/Plex/PlexHelper.cs +++ b/Shoko.Server/Plex/PlexHelper.cs @@ -267,7 +267,6 @@ public void SaveUser(JMMUser user) { var existingUser = false; var updateStats = false; - var updateGf = false; SVR_JMMUser jmmUser = null; if (user.JMMUserID != 0) { @@ -283,7 +282,6 @@ public void SaveUser(JMMUser user) { jmmUser = new SVR_JMMUser(); updateStats = true; - updateGf = true; } if (existingUser && jmmUser.IsAniDBUser != user.IsAniDBUser) @@ -292,10 +290,6 @@ public void SaveUser(JMMUser user) } var hcat = string.Join(",", user.HideCategories); - if (jmmUser.HideCategories != hcat) - { - updateGf = true; - } jmmUser.HideCategories = hcat; jmmUser.IsAniDBUser = user.IsAniDBUser; @@ -348,7 +342,7 @@ public void SaveUser(JMMUser user) } } - RepoFactory.JMMUser.Save(jmmUser, updateGf); + RepoFactory.JMMUser.Save(jmmUser); // update stats if (!updateStats) diff --git a/Shoko.Server/PlexAndKodi/Breadcrumbs.cs b/Shoko.Server/PlexAndKodi/Breadcrumbs.cs index 7ca0a850b..a5176a329 100644 --- a/Shoko.Server/PlexAndKodi/Breadcrumbs.cs +++ b/Shoko.Server/PlexAndKodi/Breadcrumbs.cs @@ -26,7 +26,6 @@ public class BreadCrumbs public string ParentIndex { get; set; } private static Dictionary<string, BreadCrumbs> Cache = new(); - //TODO CACHE EVICTION? public BreadCrumbs Update(Video v, bool noart = false) { diff --git a/Shoko.Server/Providers/TraktTV/TraktTVHelper.cs b/Shoko.Server/Providers/TraktTV/TraktTVHelper.cs index fe65bc30d..3e27f47a9 100644 --- a/Shoko.Server/Providers/TraktTV/TraktTVHelper.cs +++ b/Shoko.Server/Providers/TraktTV/TraktTVHelper.cs @@ -429,10 +429,7 @@ public string LinkAniDBTrakt(int animeID, EpisodeType aniEpType, int aniEpNumber public string LinkAniDBTrakt(ISession session, int animeID, EpisodeType aniEpType, int aniEpNumber, string traktID, int seasonNumber, int traktEpNumber, bool excludeFromWebCache) { - var xrefTemps = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeIDEpTypeEpNumber( - session, animeID, - (int)aniEpType, - aniEpNumber); + var xrefTemps = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeIDEpTypeEpNumber(animeID, (int)aniEpType, aniEpNumber); if (xrefTemps is { Count: > 0 }) { foreach (var xrefTemp in xrefTemps) @@ -1299,7 +1296,7 @@ private List<TraktV2Comment> GetShowCommentsV2(ISession session, int animeID) } var traktXRefs = - RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(session, animeID); + RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(animeID); if (traktXRefs == null || traktXRefs.Count == 0) { return null; diff --git a/Shoko.Server/Repositories/Cached/AniDB_AnimeRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_AnimeRepository.cs index 6a2bb6d8c..27998b5b0 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_AnimeRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_AnimeRepository.cs @@ -146,11 +146,8 @@ public Dictionary<int, DefaultAnimeImages> GetDefaultImagesByAnime(ISessionWrapp } // treating cache as a global DB lock, as well - var results = Lock(() => - { - // TODO: Determine if joining on the correct columns - return session.CreateSQLQuery( - @"SELECT {defImg.*}, {tvWide.*}, {tvPoster.*}, {tvFanart.*}, {movPoster.*}, {movFanart.*} + var results = Lock(() => session.CreateSQLQuery( + @"SELECT {defImg.*}, {tvWide.*}, {tvPoster.*}, {tvFanart.*}, {movPoster.*}, {movFanart.*} FROM AniDB_Anime_DefaultImage defImg LEFT OUTER JOIN TvDB_ImageWideBanner AS tvWide ON tvWide.TvDB_ImageWideBannerID = defImg.ImageParentID AND defImg.ImageParentType = :tvdbBannerType @@ -163,21 +160,20 @@ LEFT OUTER JOIN MovieDB_Poster AS movPoster LEFT OUTER JOIN MovieDB_Fanart AS movFanart ON movFanart.MovieDB_FanartID = defImg.ImageParentID AND defImg.ImageParentType = :movdbFanartType WHERE defImg.AnimeID IN (:animeIds) AND defImg.ImageParentType IN (:tvdbBannerType, :tvdbCoverType, :tvdbFanartType, :movdbPosterType, :movdbFanartType)" - ) - .AddEntity("defImg", typeof(AniDB_Anime_DefaultImage)) - .AddEntity("tvWide", typeof(TvDB_ImageWideBanner)) - .AddEntity("tvPoster", typeof(TvDB_ImagePoster)) - .AddEntity("tvFanart", typeof(TvDB_ImageFanart)) - .AddEntity("movPoster", typeof(MovieDB_Poster)) - .AddEntity("movFanart", typeof(MovieDB_Fanart)) - .SetParameterList("animeIds", animeIds) - .SetInt32("tvdbBannerType", (int)ImageEntityType.TvDB_Banner) - .SetInt32("tvdbCoverType", (int)ImageEntityType.TvDB_Cover) - .SetInt32("tvdbFanartType", (int)ImageEntityType.TvDB_FanArt) - .SetInt32("movdbPosterType", (int)ImageEntityType.MovieDB_Poster) - .SetInt32("movdbFanartType", (int)ImageEntityType.MovieDB_FanArt) - .List<object[]>(); - }); + ) + .AddEntity("defImg", typeof(AniDB_Anime_DefaultImage)) + .AddEntity("tvWide", typeof(TvDB_ImageWideBanner)) + .AddEntity("tvPoster", typeof(TvDB_ImagePoster)) + .AddEntity("tvFanart", typeof(TvDB_ImageFanart)) + .AddEntity("movPoster", typeof(MovieDB_Poster)) + .AddEntity("movFanart", typeof(MovieDB_Fanart)) + .SetParameterList("animeIds", animeIds) + .SetInt32("tvdbBannerType", (int)ImageEntityType.TvDB_Banner) + .SetInt32("tvdbCoverType", (int)ImageEntityType.TvDB_Cover) + .SetInt32("tvdbFanartType", (int)ImageEntityType.TvDB_FanArt) + .SetInt32("movdbPosterType", (int)ImageEntityType.MovieDB_Poster) + .SetInt32("movdbFanartType", (int)ImageEntityType.MovieDB_FanArt) + .List<object[]>()); foreach (var result in results) { diff --git a/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs b/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs index 0a614acdf..2a8c52f07 100644 --- a/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs @@ -26,7 +26,6 @@ public AnimeGroupRepository() BeginDeleteCallback = cr => { RepoFactory.AnimeGroup_User.Delete(RepoFactory.AnimeGroup_User.GetByGroupID(cr.AnimeGroupID)); - cr.DeleteFromFilters(); }; EndDeleteCallback = cr => { @@ -123,12 +122,8 @@ public void Save(SVR_AnimeGroup grp, bool updategrpcontractstats, bool recursive if (verifylockedFilters) { - RepoFactory.GroupFilter.CreateOrVerifyDirectoryFilters(false, grp.Contract?.Stat_AllTags, - grp.Contract?.Stat_AllYears, grp.Contract?.Stat_AllSeasons); RepoFactory.FilterPreset.CreateOrVerifyDirectoryFilters(false, grp.Contract?.Stat_AllTags, grp.Contract?.Stat_AllYears, grp.Contract?.GetSeasons()); - //This call will create extra years or tags if the Group have a new year or tag - grp.UpdateGroupFilters(types); } if (grp.AnimeGroupParentID.HasValue && recursive) diff --git a/Shoko.Server/Repositories/Cached/AnimeGroup_UserRepository.cs b/Shoko.Server/Repositories/Cached/AnimeGroup_UserRepository.cs index 35c7d8ee7..4fc981965 100644 --- a/Shoko.Server/Repositories/Cached/AnimeGroup_UserRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeGroup_UserRepository.cs @@ -23,8 +23,6 @@ public AnimeGroup_UserRepository() { Changes.TryAdd(cr.JMMUserID, new ChangeTracker<int>()); Changes[cr.JMMUserID].Remove(cr.AnimeGroupID); - - cr.DeleteFromFilters(); }; } @@ -52,49 +50,11 @@ public override void RegenerateDb() public override void Save(SVR_AnimeGroup_User obj) { - // Get The previous AnimeGroup_User from db for comparison; - var old = Lock(() => - { - using var session = DatabaseFactory.SessionFactory.OpenSession(); - return session.Get<SVR_AnimeGroup_User>(obj.AnimeGroup_UserID); - }); - obj.UpdatePlexKodiContracts(); - var types = GetConditionTypesChanged(old, obj); base.Save(obj); Changes.TryAdd(obj.JMMUserID, new ChangeTracker<int>()); Changes[obj.JMMUserID].AddOrUpdate(obj.AnimeGroupID); - obj.UpdateGroupFilters(types); - } - - private static HashSet<GroupFilterConditionType> GetConditionTypesChanged(SVR_AnimeGroup_User oldcontract, - SVR_AnimeGroup_User newcontract) - { - var h = new HashSet<GroupFilterConditionType>(); - - if (oldcontract == null || - oldcontract.UnwatchedEpisodeCount > 0 != newcontract.UnwatchedEpisodeCount > 0) - { - h.Add(GroupFilterConditionType.HasUnwatchedEpisodes); - } - - if (oldcontract == null || oldcontract.IsFave != newcontract.IsFave) - { - h.Add(GroupFilterConditionType.Favourite); - } - - if (oldcontract == null || oldcontract.WatchedDate != newcontract.WatchedDate) - { - h.Add(GroupFilterConditionType.EpisodeWatchedDate); - } - - if (oldcontract == null || oldcontract.WatchedEpisodeCount > 0 != newcontract.WatchedEpisodeCount > 0) - { - h.Add(GroupFilterConditionType.HasWatchedEpisodes); - } - - return h; } /// <summary> diff --git a/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs b/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs index 903bc4f29..3809f50ce 100644 --- a/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs @@ -33,7 +33,6 @@ public AnimeSeriesRepository() }; EndDeleteCallback = cr => { - cr.DeleteFromFilters(); if (cr.AnimeGroupID <= 0) { return; @@ -212,7 +211,7 @@ public void Save(SVR_AnimeSeries obj, bool updateGroups, bool onlyupdatestats, b if (updateGroups && !isMigrating) UpdateGroups(obj, animeID, sw, oldGroup); - if (!skipgroupfilters && !isMigrating) UpdateGroupFilters(obj, sw, animeID, types); + if (false && !skipgroupfilters && !isMigrating) UpdateGroupFilters(obj, sw, animeID, types); Changes.AddOrUpdate(obj.AnimeSeriesID); @@ -283,13 +282,8 @@ private static void UpdateGroupFilters(SVR_AnimeSeries obj, Stopwatch sw, string //This call will create extra years or tags if the Group have a new year or tag logger.Trace( $"Saving Series {animeID} | Updating Group Filters for Years ({string.Join(",", (IEnumerable<int>)allyears?.OrderBy(a => a) ?? Array.Empty<int>())}) and Seasons ({string.Join(",", (IEnumerable<string>)seasons ?? Array.Empty<string>())})"); - RepoFactory.GroupFilter.CreateOrVerifyDirectoryFilters(false, - obj.Contract?.AniDBAnime?.AniDBAnime?.GetAllTags(), allyears, seasons); RepoFactory.FilterPreset.CreateOrVerifyDirectoryFilters(false, obj.Contract?.AniDBAnime?.AniDBAnime?.GetAllTags(), allyears, obj.Contract?.AniDBAnime?.AniDBAnime?.GetSeasons().ToHashSet()); - - // Update other existing filters - obj.UpdateGroupFilters(types); sw.Stop(); logger.Trace($"Saving Series {animeID} | Updated Group Filters in {sw.Elapsed.TotalSeconds:0.00###}s"); sw.Restart(); diff --git a/Shoko.Server/Repositories/Cached/AnimeSeries_UserRepository.cs b/Shoko.Server/Repositories/Cached/AnimeSeries_UserRepository.cs index eae1d810e..e5d3913c7 100644 --- a/Shoko.Server/Repositories/Cached/AnimeSeries_UserRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeSeries_UserRepository.cs @@ -21,8 +21,6 @@ public AnimeSeries_UserRepository() Changes.TryAdd(cr.JMMUserID, new ChangeTracker<int>()); Changes[cr.JMMUserID].Remove(cr.AnimeSeriesID); - - cr.DeleteFromFilters(); }; } @@ -46,19 +44,9 @@ public override void RegenerateDb() public override void Save(SVR_AnimeSeries_User obj) { UpdatePlexKodiContracts(obj); - SVR_AnimeSeries_User old; - old = Lock(() => - { - using var session = DatabaseFactory.SessionFactory.OpenSession(); - return session.Get<SVR_AnimeSeries_User>(obj.AnimeSeries_UserID); - }); - - var types = SVR_AnimeSeries_User.GetConditionTypesChanged(old, obj); base.Save(obj); Changes.TryAdd(obj.JMMUserID, new ChangeTracker<int>()); Changes[obj.JMMUserID].AddOrUpdate(obj.AnimeSeriesID); - - obj.UpdateGroupFilter(types); } private void UpdatePlexKodiContracts(SVR_AnimeSeries_User ugrp) diff --git a/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TraktV2Repository.cs b/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TraktV2Repository.cs index c430ff12d..a47e03572 100644 --- a/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TraktV2Repository.cs +++ b/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TraktV2Repository.cs @@ -16,42 +16,12 @@ public class CrossRef_AniDB_TraktV2Repository : BaseCachedRepository<CrossRef_An public List<CrossRef_AniDB_TraktV2> GetByAnimeID(int id) { - return Lock(() => - { - using var session = DatabaseFactory.SessionFactory.OpenSession(); - return GetByAnimeID(session, id); - }); - } - - public List<CrossRef_AniDB_TraktV2> GetByAnimeID(ISession session, int id) - { - return Lock(() => - { - var xrefs = session - .CreateCriteria(typeof(CrossRef_AniDB_TraktV2)) - .Add(Restrictions.Eq("AnimeID", id)) - .AddOrder(Order.Asc("AniDBStartEpisodeType")) - .AddOrder(Order.Asc("AniDBStartEpisodeNumber")) - .List<CrossRef_AniDB_TraktV2>(); - - return new List<CrossRef_AniDB_TraktV2>(xrefs); - }); + return AnimeIDs.GetMultiple(id).OrderBy(a => a.AniDBStartEpisodeType).ThenBy(a => a.AniDBStartEpisodeNumber).ToList(); } - public List<CrossRef_AniDB_TraktV2> GetByAnimeIDEpTypeEpNumber(ISession session, int id, int aniEpType, - int aniEpisodeNumber) + public List<CrossRef_AniDB_TraktV2> GetByAnimeIDEpTypeEpNumber(int id, int aniEpType, int aniEpisodeNumber) { - return Lock(() => - { - var xrefs = session - .CreateCriteria(typeof(CrossRef_AniDB_TraktV2)) - .Add(Restrictions.Eq("AnimeID", id)) - .Add(Restrictions.Eq("AniDBStartEpisodeType", aniEpType)) - .Add(Restrictions.Eq("AniDBStartEpisodeNumber", aniEpisodeNumber)) - .List<CrossRef_AniDB_TraktV2>(); - - return new List<CrossRef_AniDB_TraktV2>(xrefs); - }); + return AnimeIDs.GetMultiple(id).Where(a => a.AniDBStartEpisodeType == aniEpType && a.AniDBStartEpisodeNumber == aniEpisodeNumber).ToList(); } public CrossRef_AniDB_TraktV2 GetByTraktID(ISession session, string id, int season, int episodeNumber, diff --git a/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs b/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs index abbc1bb53..e1e8dd2ec 100644 --- a/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs +++ b/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs @@ -1,14 +1,13 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using NutzCode.InMemoryIndex; using Shoko.Commons.Extensions; using Shoko.Commons.Properties; using Shoko.Models.Enums; -using Shoko.Server.Extensions; +using Shoko.Server.Filters; using Shoko.Server.Filters.Functions; using Shoko.Server.Filters.Info; using Shoko.Server.Filters.Logic; @@ -17,8 +16,8 @@ using Shoko.Server.Filters.SortingSelectors; using Shoko.Server.Filters.User; using Shoko.Server.Models; -using Shoko.Server.Repositories.NHibernate; using Shoko.Server.Server; +using Shoko.Server.Utilities; using Constants = Shoko.Server.Server.Constants; namespace Shoko.Server.Repositories.Cached; @@ -70,13 +69,15 @@ public override void PostProcess() public void CleanUpEmptyDirectoryFilters() { - var toremove = GetAll().Where(a => (a.FilterType & GroupFilterType.Directory) != 0) - .Where(gf => // TODO evaluate - false).ToList(); - if (toremove.Count > 0) - { - Delete(toremove); - } + var evaluator = Utils.ServiceContainer.GetRequiredService<FilterEvaluator>(); + var users = RepoFactory.JMMUser.GetAll(); + var toRemove = GetAll().Where(a => (a.FilterType & GroupFilterType.Directory) != 0) + .Where(gf => gf.Expression?.UserDependent ?? false + ? !users.Any(u => evaluator.EvaluateFilter(gf, u.JMMUserID).Any()) + : !evaluator.EvaluateFilter(gf, null).Any()).ToList(); + if (toRemove.Count <= 0) return; + + Delete(toRemove); } public void CreateOrVerifyLockedFilters() diff --git a/Shoko.Server/Repositories/Cached/JMMUserRepository.cs b/Shoko.Server/Repositories/Cached/JMMUserRepository.cs index cced968ad..354dbef9c 100644 --- a/Shoko.Server/Repositories/Cached/JMMUserRepository.cs +++ b/Shoko.Server/Repositories/Cached/JMMUserRepository.cs @@ -25,43 +25,6 @@ public override void RegenerateDb() { } - public override void Save(SVR_JMMUser obj) - { - Save(obj, true); - } - - public void Save(SVR_JMMUser obj, bool updateGroupFilters) - { - var isNew = false; - if (obj.JMMUserID == 0) - { - isNew = true; - base.Save(obj); - } - - if (updateGroupFilters) - { - SVR_JMMUser old = null; - if (!isNew) - { - old = Lock(() => - { - using var session = DatabaseFactory.SessionFactory.OpenSession(); - return session.Get<SVR_JMMUser>(obj.JMMUserID); - }); - } - - updateGroupFilters = SVR_JMMUser.CompareUser(old, obj); - } - - base.Save(obj); - if (updateGroupFilters) - { - logger.Trace("Updating group filter stats by user from JMMUserRepository.Save: {0}", obj.JMMUserID); - obj.UpdateGroupFilters(); - } - } - public SVR_JMMUser GetByUsername(string username) { if (string.IsNullOrWhiteSpace(username)) @@ -102,9 +65,6 @@ public bool RemoveUser(int userID, bool skipValidation = false) } } - var toSave = RepoFactory.GroupFilter.GetAll().AsParallel().Where(a => a.RemoveUser(userID)).ToList(); - RepoFactory.GroupFilter.Save(toSave); - RepoFactory.AnimeSeries_User.Delete(RepoFactory.AnimeSeries_User.GetByUserID(userID)); RepoFactory.AnimeGroup_User.Delete(RepoFactory.AnimeGroup_User.GetByUserID(userID)); RepoFactory.AnimeEpisode_User.Delete(RepoFactory.AnimeEpisode_User.GetByUserID(userID)); diff --git a/Shoko.Server/Repositories/Direct/CrossRef_Languages_AniDB_FileRepository.cs b/Shoko.Server/Repositories/Direct/CrossRef_Languages_AniDB_FileRepository.cs index ce6e91d1f..b2747c595 100644 --- a/Shoko.Server/Repositories/Direct/CrossRef_Languages_AniDB_FileRepository.cs +++ b/Shoko.Server/Repositories/Direct/CrossRef_Languages_AniDB_FileRepository.cs @@ -2,21 +2,19 @@ using System.Collections.Generic; using System.Linq; using NHibernate; +using NutzCode.InMemoryIndex; using Shoko.Models.Server; using Shoko.Server.Databases; namespace Shoko.Server.Repositories.Direct; -public class CrossRef_Languages_AniDB_FileRepository : BaseDirectRepository<CrossRef_Languages_AniDB_File, int> +public class CrossRef_Languages_AniDB_FileRepository : BaseCachedRepository<CrossRef_Languages_AniDB_File, int> { + private PocoIndex<int, CrossRef_Languages_AniDB_File, int> FileIDs; + public List<CrossRef_Languages_AniDB_File> GetByFileID(int id) { - return Lock(() => - { - using var session = DatabaseFactory.SessionFactory.OpenSession(); - return session.Query<CrossRef_Languages_AniDB_File>() - .Where(a => a.FileID == id).ToList(); - }); + return FileIDs.GetMultiple(id); } public Dictionary<int, HashSet<string>> GetLanguagesByAnime(IEnumerable<int> animeIds) @@ -51,4 +49,13 @@ FROM CrossRef_File_Episode eps .List<string>().ToHashSet(StringComparer.InvariantCultureIgnoreCase); }); } + + public override void PopulateIndexes() + { + FileIDs = Cache.CreateIndex(a => a.FileID); + } + + public override void RegenerateDb() { } + + protected override int SelectKey(CrossRef_Languages_AniDB_File entity) => entity.CrossRef_Languages_AniDB_FileID; } diff --git a/Shoko.Server/Repositories/Direct/CrossRef_Subtitles_AniDB_FileRepository.cs b/Shoko.Server/Repositories/Direct/CrossRef_Subtitles_AniDB_FileRepository.cs index dbff47519..c4ddf1d9d 100644 --- a/Shoko.Server/Repositories/Direct/CrossRef_Subtitles_AniDB_FileRepository.cs +++ b/Shoko.Server/Repositories/Direct/CrossRef_Subtitles_AniDB_FileRepository.cs @@ -2,23 +2,19 @@ using System.Collections.Generic; using System.Linq; using NHibernate; +using NutzCode.InMemoryIndex; using Shoko.Models.Server; using Shoko.Server.Databases; namespace Shoko.Server.Repositories.Direct; -public class CrossRef_Subtitles_AniDB_FileRepository : BaseDirectRepository<CrossRef_Subtitles_AniDB_File, int> +public class CrossRef_Subtitles_AniDB_FileRepository : BaseCachedRepository<CrossRef_Subtitles_AniDB_File, int> { + private PocoIndex<int, CrossRef_Subtitles_AniDB_File, int> FileIDs; + public List<CrossRef_Subtitles_AniDB_File> GetByFileID(int id) { - return Lock(() => - { - using var session = DatabaseFactory.SessionFactory.OpenSession(); - return session - .Query<CrossRef_Subtitles_AniDB_File>() - .Where(a => a.FileID == id) - .ToList(); - }); + return FileIDs.GetMultiple(id); } public Dictionary<int, HashSet<string>> GetLanguagesByAnime(IEnumerable<int> animeIds) @@ -51,4 +47,13 @@ public HashSet<string> GetLanguagesForAnime(int animeID) .List<string>().ToHashSet(StringComparer.InvariantCultureIgnoreCase); }); } + + public override void PopulateIndexes() + { + FileIDs = Cache.CreateIndex(a => a.FileID); + } + + public override void RegenerateDb() { } + + protected override int SelectKey(CrossRef_Subtitles_AniDB_File entity) => entity.CrossRef_Subtitles_AniDB_FileID; } diff --git a/Shoko.Server/Tasks/AnimeGroupCreator.cs b/Shoko.Server/Tasks/AnimeGroupCreator.cs index 4f2c74f23..9daf32793 100644 --- a/Shoko.Server/Tasks/AnimeGroupCreator.cs +++ b/Shoko.Server/Tasks/AnimeGroupCreator.cs @@ -28,9 +28,7 @@ internal class AnimeGroupCreator private readonly AnimeSeriesRepository _animeSeriesRepo = RepoFactory.AnimeSeries; private readonly AnimeGroupRepository _animeGroupRepo = RepoFactory.AnimeGroup; private readonly AnimeGroup_UserRepository _animeGroupUserRepo = RepoFactory.AnimeGroup_User; - private readonly GroupFilterRepository _groupFilterRepo = RepoFactory.GroupFilter; private readonly FilterPresetRepository _filterRepo = RepoFactory.FilterPreset; - private readonly JMMUserRepository _userRepo = RepoFactory.JMMUser; private readonly bool _autoGroupSeries; /// <summary> @@ -108,7 +106,6 @@ private void ClearGroupsAndDependencies(ISessionWrapper session, int tempGroupId // We've deleted/modified all AnimeSeries/GroupFilter records, so update caches to reflect that _animeSeriesRepo.ClearCache(); - _groupFilterRepo.ClearCache(); _log.Info("AnimeGroups have been removed and GroupFilters have been reset"); } @@ -486,7 +483,6 @@ public void RecreateAllGroups(ISessionWrapper session) // (Otherwise updating Group Filters won't get the correct results) _animeGroupRepo.Populate(session, false); _animeGroupUserRepo.Populate(session, false); - _groupFilterRepo.Populate(session, false); UpdateGroupFilters(); @@ -501,7 +497,6 @@ public void RecreateAllGroups(ISessionWrapper session) // If an error occurs then chances are the caches are in an inconsistent state. So re-populate them _animeSeriesRepo.Populate(); _animeGroupRepo.Populate(); - _groupFilterRepo.Populate(); _animeGroupUserRepo.Populate(); } catch (Exception ie) From 1f98f8b9b5c0adc0714b63d6043871ce0878b219 Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Sat, 16 Sep 2023 11:53:15 -0400 Subject: [PATCH 18/34] Some cleanup to make startup faster --- .../Cached/FilterPresetRepository.cs | 18 +- .../Cached/GroupFilterRepository.cs | 579 +----------------- 2 files changed, 10 insertions(+), 587 deletions(-) diff --git a/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs b/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs index e1e8dd2ec..3af889a6d 100644 --- a/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs +++ b/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs @@ -55,7 +55,7 @@ public override void RegenerateDb() { } public override void PostProcess() { - const string t = "Filter"; + const string t = "FilterPreset"; // Clean up. This will populate empty conditions and remove duplicate filters ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, @@ -70,11 +70,9 @@ public override void PostProcess() public void CleanUpEmptyDirectoryFilters() { var evaluator = Utils.ServiceContainer.GetRequiredService<FilterEvaluator>(); - var users = RepoFactory.JMMUser.GetAll(); - var toRemove = GetAll().Where(a => (a.FilterType & GroupFilterType.Directory) != 0) - .Where(gf => gf.Expression?.UserDependent ?? false - ? !users.Any(u => evaluator.EvaluateFilter(gf, u.JMMUserID).Any()) - : !evaluator.EvaluateFilter(gf, null).Any()).ToList(); + var filters = GetAll().Where(a => (a.FilterType & GroupFilterType.Directory) != 0) + .Where(gf => gf.Expression is not null && gf.Expression.UserDependent).ToList(); + var toRemove = evaluator.BatchEvaluateFilters(filters, null).Where(a => !a.Value.Any()).Select(a => a.Key).ToList(); if (toRemove.Count <= 0) return; Delete(toRemove); @@ -82,7 +80,7 @@ public void CleanUpEmptyDirectoryFilters() public void CreateOrVerifyLockedFilters() { - const string t = "GroupFilter"; + const string t = "FilterPreset"; var lockedGFs = GetLockedGroupFilters(); @@ -156,11 +154,10 @@ public void CreateOrVerifyLockedFilters() public void CreateOrVerifyDirectoryFilters(bool frominit = false, ISet<string> tags = null, ISet<int> airdate = null, ISet<(int Year, AnimeSeason Season)> seasons = null) { - const string t = "GroupFilter"; + const string t = "FilterPreset"; var lockedGFs = GetLockedGroupFilters(); - var tagsdirec = lockedGFs.FirstOrDefault(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Tag)); if (tagsdirec != null) { @@ -275,8 +272,7 @@ public void CreateOrVerifyDirectoryFilters(bool frominit = false, ISet<string> t allseasons = new SortedSet<(int Year, AnimeSeason Season)>(); foreach (var ser in grps) { - var seriesSeasons = ser?.Contract?.AniDBAnime?.AniDBAnime?.GetSeasons().ToList(); - if ((seriesSeasons?.Count ?? 0) == 0) ser?.UpdateContract(); + var seriesSeasons = ser?.GetAnime()?.GetSeasons().ToList(); if ((seriesSeasons?.Count ?? 0) == 0) continue; allseasons.UnionWith(seriesSeasons); } diff --git a/Shoko.Server/Repositories/Cached/GroupFilterRepository.cs b/Shoko.Server/Repositories/Cached/GroupFilterRepository.cs index 565347aeb..d2b65629c 100644 --- a/Shoko.Server/Repositories/Cached/GroupFilterRepository.cs +++ b/Shoko.Server/Repositories/Cached/GroupFilterRepository.cs @@ -58,76 +58,10 @@ public override void PopulateIndexes() PostProcessFilters = new List<SVR_GroupFilter>(); } - public override void RegenerateDb() - { - foreach (var g in Cache.Values.Where(g => - (g.GroupFilterID != 0 && g.GroupsIdsVersion < SVR_GroupFilter.GROUPFILTER_VERSION) || - g.GroupConditionsVersion < SVR_GroupFilter.GROUPCONDITIONS_VERSION).ToList()) - { - if (g.GroupConditionsVersion == 0) - { - g.Conditions = RepoFactory.GroupFilterCondition.GetByGroupFilterID(g.GroupFilterID); - } - - Save(g, true); - PostProcessFilters.Add(g); - } - } - - - public override void PostProcess() - { - const string t = "GroupFilter"; - ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, - t, string.Empty); - foreach (var g in Cache.Values.ToList()) - { - if (g.GroupsIdsVersion < SVR_GroupFilter.GROUPFILTER_VERSION || - g.GroupConditionsVersion < SVR_GroupFilter.GROUPCONDITIONS_VERSION || - g.SeriesIdsVersion < SVR_GroupFilter.SERIEFILTER_VERSION) - { - if (!PostProcessFilters.Contains(g)) - { - PostProcessFilters.Add(g); - } - } - } - - var max = PostProcessFilters.Count; - var cnt = 0; - foreach (var gf in PostProcessFilters) - { - cnt++; - ServerState.Instance.ServerStartingStatus = string.Format( - Resources.Database_Validating, t, - Resources.Filter_Recalc + " " + cnt + "/" + max + " - " + - gf.GroupFilterName); - if (gf.GroupsIdsVersion < SVR_GroupFilter.GROUPFILTER_VERSION || - gf.GroupConditionsVersion < SVR_GroupFilter.GROUPCONDITIONS_VERSION || - gf.SeriesIdsVersion < SVR_GroupFilter.SERIEFILTER_VERSION) - { - gf.CalculateGroupsAndSeries(); - } + public override void RegenerateDb() { } - Save(gf); - } - // Clean up. This will populate empty conditions and remove duplicate filters - ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, - t, - " " + Resources.GroupFilter_Cleanup); - var all = GetAll(); - var set = new HashSet<SVR_GroupFilter>(all); - var notin = all.Except(set).ToList(); - Delete(notin); - - // Remove orphaned group filter conditions - var toremove = RepoFactory.GroupFilterCondition.GetAll().ToList() - .Where(condition => RepoFactory.GroupFilter.GetByID(condition.GroupFilterID) == null).ToList(); - RepoFactory.GroupFilterCondition.Delete(toremove); - - PostProcessFilters = null; - } + public override void PostProcess() { } public void CleanUpEmptyDirectoryFilters() { @@ -142,514 +76,7 @@ public void CleanUpEmptyDirectoryFilters() } } - public void CreateOrVerifyLockedFilters() - { - const string t = "GroupFilter"; - - var lockedGFs = GetLockedGroupFilters(); - - ServerState.Instance.ServerStartingStatus = string.Format( - Resources.Database_Validating, t, - " " + Resources.Filter_CreateContinueWatching); - - var cwatching = - lockedGFs.FirstOrDefault( - a => - a.FilterType == (int)GroupFilterType.ContinueWatching); - if (cwatching != null && cwatching.FilterType != (int)GroupFilterType.ContinueWatching) - { - cwatching.FilterType = (int)GroupFilterType.ContinueWatching; - Save(cwatching); - } - else if (cwatching == null) - { - var gf = new SVR_GroupFilter - { - GroupFilterName = Constants.GroupFilterName.ContinueWatching, - Locked = 1, - SortingCriteria = "4;2", // by last watched episode desc - ApplyToSeries = 0, - BaseCondition = 1, // all - FilterType = (int)GroupFilterType.ContinueWatching, - InvisibleInClients = 0, - Conditions = new List<GroupFilterCondition>() - }; - var gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.HasWatchedEpisodes, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = string.Empty, - GroupFilterID = gf.GroupFilterID - }; - gf.Conditions.Add(gfc); - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.HasUnwatchedEpisodes, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = string.Empty, - GroupFilterID = gf.GroupFilterID - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - Save(gf); //Get ID - } - - //Create All filter - var allfilter = lockedGFs.FirstOrDefault(a => a.FilterType == (int)GroupFilterType.All); - if (allfilter == null) - { - var gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_All, - Locked = 1, - InvisibleInClients = 0, - FilterType = (int)GroupFilterType.All, - BaseCondition = 1, - SortingCriteria = "5;1" - }; - gf.CalculateGroupsAndSeries(); - Save(gf); - } - - var tagsdirec = - lockedGFs.FirstOrDefault( - a => a.FilterType == (int)(GroupFilterType.Directory | GroupFilterType.Tag)); - if (tagsdirec == null) - { - tagsdirec = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Tags, - InvisibleInClients = 0, - FilterType = (int)(GroupFilterType.Directory | GroupFilterType.Tag), - BaseCondition = 1, - Locked = 1, - SortingCriteria = "13;1" - }; - Save(tagsdirec); - } - - var yearsdirec = - lockedGFs.FirstOrDefault( - a => a.FilterType == (int)(GroupFilterType.Directory | GroupFilterType.Year)); - if (yearsdirec == null) - { - yearsdirec = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Years, - InvisibleInClients = 0, - FilterType = (int)(GroupFilterType.Directory | GroupFilterType.Year), - BaseCondition = 1, - Locked = 1, - SortingCriteria = "13;1" - }; - Save(yearsdirec); - } - - var seasonsdirec = - lockedGFs.FirstOrDefault( - a => a.FilterType == (int)(GroupFilterType.Directory | GroupFilterType.Season)); - if (seasonsdirec == null) - { - seasonsdirec = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Seasons, - InvisibleInClients = 0, - FilterType = (int)(GroupFilterType.Directory | GroupFilterType.Season), - BaseCondition = 1, - Locked = 1, - SortingCriteria = "13;1" - }; - Save(seasonsdirec); - } - - CreateOrVerifyDirectoryFilters(true); - } - - public void CreateOrVerifyDirectoryFilters(bool frominit = false, HashSet<string> tags = null, - HashSet<int> airdate = null, SortedSet<string> seasons = null) - { - const string t = "GroupFilter"; - - var lockedGFs = GetLockedGroupFilters(); - - - var tagsdirec = lockedGFs.FirstOrDefault( - a => a.FilterType == (int)(GroupFilterType.Directory | GroupFilterType.Tag)); - if (tagsdirec != null) - { - HashSet<string> alltags; - if (tags == null) - { - alltags = new HashSet<string>( - RepoFactory.AniDB_Tag.GetAllForLocalSeries().Select(a => a.TagName.Replace('`', '\'')), - StringComparer.InvariantCultureIgnoreCase); - } - else - { - alltags = new HashSet<string>(tags, - StringComparer.InvariantCultureIgnoreCase); - } - - var existingTags = - new HashSet<string>( - lockedGFs.Where(a => a.FilterType == (int)GroupFilterType.Tag) - .Select(a => a.Conditions.FirstOrDefault()?.ConditionParameter), - StringComparer.InvariantCultureIgnoreCase); - alltags.ExceptWith(existingTags); - - var max = alltags.Count; - var cnt = 0; - //AniDB Tags are in english so we use en-us culture - var tinfo = new CultureInfo("en-US", false).TextInfo; - foreach (var s in alltags) - { - cnt++; - if (frominit) - { - ServerState.Instance.ServerStartingStatus = string.Format( - Resources.Database_Validating, t, - Resources.Filter_CreatingTag + " " + - Resources.Filter_Filter + " " + cnt + "/" + max + " - " + s); - } - - var yf = new SVR_GroupFilter - { - ParentGroupFilterID = tagsdirec.GroupFilterID, - InvisibleInClients = 0, - ApplyToSeries = 1, - GroupFilterName = tinfo.ToTitleCase(s), - BaseCondition = 1, - Locked = 1, - SortingCriteria = "5;1", - FilterType = (int)GroupFilterType.Tag - }; - var gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.Tag, - ConditionOperator = (int)GroupFilterOperator.In, - ConditionParameter = s, - GroupFilterID = yf.GroupFilterID - }; - yf.Conditions.Add(gfc); - yf.CalculateGroupsAndSeries(); - Save(yf); - } - } - - var yearsdirec = lockedGFs.FirstOrDefault( - a => a.FilterType == (int)(GroupFilterType.Directory | GroupFilterType.Year)); - if (yearsdirec != null) - { - HashSet<string> allyears; - if (airdate == null || airdate.Count == 0) - { - var grps = - RepoFactory.AnimeSeries.GetAll().Select(a => a.Contract).Where(a => a != null).ToList(); - - allyears = new HashSet<string>(StringComparer.Ordinal); - foreach (var ser in grps) - { - var endyear = ser.AniDBAnime.AniDBAnime.EndYear; - var startyear = ser.AniDBAnime.AniDBAnime.BeginYear; - if (endyear <= 0) - { - endyear = DateTime.Today.Year; - } - - if (endyear < startyear || endyear - startyear + 1 >= int.MaxValue) - { - endyear = startyear; - } - - if (startyear != 0) - { - allyears.UnionWith(Enumerable.Range(startyear, - endyear - startyear + 1) - .Select(a => a.ToString())); - } - } - } - else - { - allyears = new HashSet<string>(airdate.Select(a => a.ToString()), StringComparer.Ordinal); - } - - var notin = - new HashSet<string>( - lockedGFs.Where(a => a.FilterType == (int)GroupFilterType.Year) - .Select(a => a.Conditions.FirstOrDefault()?.ConditionParameter), - StringComparer.InvariantCultureIgnoreCase); - allyears.ExceptWith(notin); - var max = allyears.Count; - var cnt = 0; - foreach (var s in allyears) - { - cnt++; - if (frominit) - { - ServerState.Instance.ServerStartingStatus = string.Format( - Resources.Database_Validating, t, - Resources.Filter_CreatingYear + " " + - Resources.Filter_Filter + " " + cnt + "/" + max + " - " + s); - } - - var yf = new SVR_GroupFilter - { - ParentGroupFilterID = yearsdirec.GroupFilterID, - InvisibleInClients = 0, - GroupFilterName = s, - BaseCondition = 1, - Locked = 1, - SortingCriteria = "5;1", - FilterType = (int)GroupFilterType.Year, - ApplyToSeries = 1 - }; - var gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.Year, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = s, - GroupFilterID = yf.GroupFilterID - }; - yf.Conditions.Add(gfc); - yf.CalculateGroupsAndSeries(); - Save(yf); - } - } - - var seasonsdirectory = lockedGFs.FirstOrDefault( - a => a.FilterType == (int)(GroupFilterType.Directory | GroupFilterType.Season)); - if (seasonsdirectory != null) - { - SortedSet<string> allseasons; - if (seasons == null) - { - var grps = - RepoFactory.AnimeSeries.GetAll().ToList(); - - allseasons = new SortedSet<string>(new SeasonComparator()); - foreach (var ser in grps) - { - if ((ser?.Contract?.AniDBAnime?.Stat_AllSeasons?.Count ?? 0) == 0) - { - ser?.UpdateContract(); - } - - if ((ser?.Contract?.AniDBAnime?.Stat_AllSeasons?.Count ?? 0) == 0) - { - continue; - } - - allseasons.UnionWith(ser.Contract.AniDBAnime.Stat_AllSeasons); - } - } - else - { - allseasons = seasons; - } - - var notin = - new HashSet<string>( - lockedGFs.Where(a => a.FilterType == (int)GroupFilterType.Season) - .Select(a => a.Conditions.FirstOrDefault()?.ConditionParameter), - StringComparer.InvariantCultureIgnoreCase); - allseasons.ExceptWith(notin); - var max = allseasons.Count; - var cnt = 0; - foreach (var season in allseasons) - { - cnt++; - if (frominit) - { - ServerState.Instance.ServerStartingStatus = string.Format( - Resources.Database_Validating, t, - Resources.Filter_CreatingSeason + " " + - Resources.Filter_Filter + " " + cnt + "/" + max + " - " + season); - } - - var yf = new SVR_GroupFilter - { - ParentGroupFilterID = seasonsdirectory.GroupFilterID, - InvisibleInClients = 0, - GroupFilterName = season, - BaseCondition = 1, - Locked = 1, - SortingCriteria = "5;1", - FilterType = (int)GroupFilterType.Season, - ApplyToSeries = 1 - }; - var gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.Season, - ConditionOperator = (int)GroupFilterOperator.In, - ConditionParameter = season, - GroupFilterID = yf.GroupFilterID - }; - yf.Conditions.Add(gfc); - yf.CalculateGroupsAndSeries(); - Save(yf); - } - } - - CleanUpEmptyDirectoryFilters(); - } - - public void CreateInitialGroupFilters() - { - // group filters - // Do to DatabaseFixes, some filters may be made, namely directory filters - // All, Continue Watching, Years, Seasons, Tags... 6 seems to be enough to tell for now - // We can't just check the existence of anything specific, as the user can delete most of these - if (GetTopLevel().Count() > 6) return; - - // Favorites - var gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Favorites, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - var gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.Favourite, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - Save(gf); - - // Missing Episodes - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_MissingEpisodes, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.MissingEpisodesCollecting, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - Save(gf); - - - // Newly Added Series - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Added, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.SeriesCreatedDate, - ConditionOperator = (int)GroupFilterOperator.LastXDays, - ConditionParameter = "10" - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - Save(gf); - - // Newly Airing Series - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Airing, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.AirDate, - ConditionOperator = (int)GroupFilterOperator.LastXDays, - ConditionParameter = "30" - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - Save(gf); - - // Votes Needed - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Votes, - ApplyToSeries = 1, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.CompletedSeries, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.HasUnwatchedEpisodes, - ConditionOperator = (int)GroupFilterOperator.Exclude, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.UserVotedAny, - ConditionOperator = (int)GroupFilterOperator.Exclude, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - Save(gf); - - // Recently Watched - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_RecentlyWatched, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.EpisodeWatchedDate, - ConditionOperator = (int)GroupFilterOperator.LastXDays, - ConditionParameter = "10" - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - Save(gf); - - // TvDB/MovieDB Link Missing - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_LinkMissing, - ApplyToSeries = 1, // This makes far more sense as applied to series - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.AssignedTvDBOrMovieDBInfo, - ConditionOperator = (int)GroupFilterOperator.Exclude, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - Save(gf); - } + public void CreateOrVerifyLockedFilters() { } public override void Save(SVR_GroupFilter obj) { From b94c8dcfd2d79eb54fe99aaf89942163479da3c5 Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Sat, 16 Sep 2023 14:10:44 -0400 Subject: [PATCH 19/34] Improve Memory Bandwidth a bit --- Shoko.Commons | 2 +- Shoko.Server/Utilities/PocoCache.cs | 343 +++++++++++----------------- 2 files changed, 129 insertions(+), 216 deletions(-) diff --git a/Shoko.Commons b/Shoko.Commons index 9e08c74a3..6b800221d 160000 --- a/Shoko.Commons +++ b/Shoko.Commons @@ -1 +1 @@ -Subproject commit 9e08c74a395fb3cbf4b8d99c6a67f911b28b5a81 +Subproject commit 6b800221d38adb25ecd6b9890695719eff0bf63c diff --git a/Shoko.Server/Utilities/PocoCache.cs b/Shoko.Server/Utilities/PocoCache.cs index 7fe901a85..da0d4c626 100644 --- a/Shoko.Server/Utilities/PocoCache.cs +++ b/Shoko.Server/Utilities/PocoCache.cs @@ -33,33 +33,32 @@ namespace NutzCode.InMemoryIndex; public class PocoCache<T, S> where S : class { - private Dictionary<T, S> _dict; - private Func<S, T> _func; - private List<IPocoCacheObserver<T, S>> _observers = new(); + private readonly Dictionary<T, S> _dict; + private readonly Func<S, T> _func; + private readonly List<IPocoCacheObserver<T, S>> _observers = new(); public Dictionary<T, S>.KeyCollection Keys => _dict.Keys; public Dictionary<T, S>.ValueCollection Values => _dict.Values; - public PocoIndex<T, S, U> CreateIndex<U>(Func<S, U> paramfunc1) + public PocoIndex<T, S, U> CreateIndex<U>(Func<S, U> func1) { - return new PocoIndex<T, S, U>(this, paramfunc1); + return new PocoIndex<T, S, U>(this, func1); } - public PocoIndex<T, S, N, U> CreateIndex<N, U>(Func<S, N> paramfunc1, Func<S, U> paramfunc2) + public PocoIndex<T, S, N, U> CreateIndex<N, U>(Func<S, N> func1, Func<S, U> func2) { - return new PocoIndex<T, S, N, U>(this, paramfunc1, paramfunc2); + return new PocoIndex<T, S, N, U>(this, func1, func2); } - public PocoIndex<T, S, N, R, U> CreateIndex<N, R, U>(Func<S, N> paramfunc1, Func<S, R> paramfunc2, - Func<S, U> paramfunc3) + public PocoIndex<T, S, N, R, U> CreateIndex<N, R, U>(Func<S, N> func1, Func<S, R> func2, Func<S, U> func3) { - return new PocoIndex<T, S, N, R, U>(this, paramfunc1, paramfunc2, paramfunc3); + return new PocoIndex<T, S, N, R, U>(this, func1, func2, func3); } - public PocoCache(IEnumerable<S> objectlist, Func<S, T> keyfunc) + public PocoCache(IEnumerable<S> objectList, Func<S, T> keyFunc) { - _func = keyfunc; - _dict = objectlist.ToDictionary(keyfunc, a => a); + _func = keyFunc; + _dict = objectList.ToDictionary(keyFunc, a => a); } internal void AddChain(IPocoCacheObserver<T, S> observer) @@ -69,9 +68,7 @@ internal void AddChain(IPocoCacheObserver<T, S> observer) public S Get(T key) { - return _dict.ContainsKey(key) - ? _dict[key] - : null; + return _dict.TryGetValue(key, out var value) ? value : null; } public void Update(S obj) @@ -110,7 +107,7 @@ public void Clear() } } -public interface IPocoCacheObserver<TKey, TEntity> where TEntity : class +public interface IPocoCacheObserver<in TKey, in TEntity> where TEntity : class { void Update(TKey key, TEntity entity); @@ -122,39 +119,33 @@ public interface IPocoCacheObserver<TKey, TEntity> where TEntity : class public class PocoIndex<T, S, U> : IPocoCacheObserver<T, S> where S : class { - internal PocoCache<T, S> _cache; - internal BiDictionaryOneToMany<T, U> _dict; - internal Func<S, U> _func; + private static readonly List<S> EmptyList = new (); + private readonly PocoCache<T, S> _cache; + private readonly BiDictionaryOneToMany<T, U> _dict; + private readonly Func<S, U> _func; - internal PocoIndex(PocoCache<T, S> cache, Func<S, U> paramfunc1) + internal PocoIndex(PocoCache<T, S> cache, Func<S, U> func1) { _cache = cache; - _dict = new BiDictionaryOneToMany<T, U>(_cache.Keys.ToDictionary(a => a, a => paramfunc1(_cache.Get(a)))); - _func = paramfunc1; + _dict = new BiDictionaryOneToMany<T, U>(_cache.Keys.ToDictionary(a => a, a => func1(_cache.Get(a)))); + _func = func1; cache.AddChain(this); } public S GetOne(U key) { - if (_cache == null || !_dict.ContainsInverseKey(key)) - { + if (_cache == null || !_dict.TryGetInverse(key, out var results)) return null; - } - var hashes = _dict.FindInverse(key); - return hashes.Count == 0 - ? null - : _cache.Get(hashes.First()); + return results.Count == 0 ? null : _cache.Get(results.FirstOrDefault()); } public List<S> GetMultiple(U key) { - if (_cache == null || !_dict.ContainsInverseKey(key)) - { - return new List<S>(); - } + if (_cache == null || !_dict.TryGetInverse(key, out var results)) + return EmptyList; - return _dict.FindInverse(key).Select(a => _cache.Get(a)).ToList(); + return results.Select(a => _cache.Get(a)).ToList(); } void IPocoCacheObserver<T, S>.Update(T key, S obj) @@ -175,11 +166,11 @@ void IPocoCacheObserver<T, S>.Clear() public class PocoIndex<T, S, N, U> where S : class { - public PocoIndex<T, S, Tuple<N, U>> _index; + private readonly PocoIndex<T, S, Tuple<N, U>> _index; - internal PocoIndex(PocoCache<T, S> cache, Func<S, N> paramfunc1, Func<S, U> paramfunc2) + internal PocoIndex(PocoCache<T, S> cache, Func<S, N> func1, Func<S, U> func2) { - _index = new PocoIndex<T, S, Tuple<N, U>>(cache, a => new Tuple<N, U>(paramfunc1(a), paramfunc2(a))); + _index = new PocoIndex<T, S, Tuple<N, U>>(cache, a => new Tuple<N, U>(func1(a), func2(a))); } public S GetOne(N key1, U key2) @@ -197,12 +188,12 @@ public List<S> GetMultiple(N key1, U key2) public class PocoIndex<T, S, N, R, U> where S : class { - public PocoIndex<T, S, Tuple<N, R, U>> _index; + private readonly PocoIndex<T, S, Tuple<N, R, U>> _index; - internal PocoIndex(PocoCache<T, S> cache, Func<S, N> paramfunc1, Func<S, R> paramfunc2, Func<S, U> paramfunc3) + internal PocoIndex(PocoCache<T, S> cache, Func<S, N> func1, Func<S, R> func2, Func<S, U> func3) { _index = new PocoIndex<T, S, Tuple<N, R, U>>(cache, - a => new Tuple<N, R, U>(paramfunc1(a), paramfunc2(a), paramfunc3(a))); + a => new Tuple<N, R, U>(func1(a), func2(a), func3(a))); } public S GetOne(N key1, R key2, U key3) @@ -220,8 +211,8 @@ public List<S> GetMultiple(N key1, R key2, U key3) public class BiDictionaryOneToOne<T, S> { - private Dictionary<T, S> direct = new(); - private Dictionary<S, T> inverse = new(); + private readonly Dictionary<T, S> _direct = new(); + private readonly Dictionary<S, T> _inverse = new(); public BiDictionaryOneToOne() { @@ -229,223 +220,175 @@ public BiDictionaryOneToOne() public BiDictionaryOneToOne(Dictionary<T, S> input) { - direct = input; - inverse = direct.ToDictionary(a => a.Value, a => a.Key); + _direct = input; + _inverse = _direct.ToDictionary(a => a.Value, a => a.Key); } public S this[T key] { - get => direct[key]; + get => _direct[key]; set { - if (direct.ContainsKey(key)) - { - var oldvalue = direct[key]; - if (oldvalue.Equals(value)) - { - return; - } - - if (inverse.ContainsKey(oldvalue)) - { - inverse.Remove(oldvalue); - } - } - - if (!inverse.ContainsKey(value)) + if (_direct.TryGetValue(key, out var oldValue)) { - inverse.Add(value, key); + if (oldValue.Equals(value)) return; + if (_inverse.ContainsKey(oldValue)) _inverse.Remove(oldValue); } - direct[key] = value; + _inverse.TryAdd(value, key); + _direct[key] = value; } } public T FindInverse(S k) { - return inverse[k]; + return _inverse[k]; } public bool ContainsKey(T key) { - return direct.ContainsKey(key); + return _direct.ContainsKey(key); } public bool ContainsInverseKey(S key) { - return inverse.ContainsKey(key); + return _inverse.ContainsKey(key); } public void Remove(T value) { - if (direct.ContainsKey(value)) - { - var n = direct[value]; - inverse.Remove(n); - direct.Remove(value); - } + if (!_direct.TryGetValue(value, out var oldValue)) return; + _inverse.Remove(oldValue); + _direct.Remove(value); } public void Clear() { - direct.Clear(); - inverse.Clear(); + _direct.Clear(); + _inverse.Clear(); } } public class BiDictionaryOneToMany<T, S> { - private Dictionary<T, S> direct = new(); - private Dictionary<S, HashSet<T>> inverse = new(); + private readonly Dictionary<T, S> _direct = new(); + private readonly Dictionary<S, HashSet<T>> _inverse = new(); - private readonly bool valueIsNullable; + private readonly bool _valueIsNullable; - private HashSet<T> inverseNullValueSet; + private HashSet<T> _inverseNullValueSet; public BiDictionaryOneToMany() { - valueIsNullable = Nullable.GetUnderlyingType(typeof(S)) != null; + _valueIsNullable = Nullable.GetUnderlyingType(typeof(S)) != null; } public BiDictionaryOneToMany(Dictionary<T, S> input) { - valueIsNullable = Nullable.GetUnderlyingType(typeof(S)) != null; - direct = input; - if (valueIsNullable) + _valueIsNullable = Nullable.GetUnderlyingType(typeof(S)) != null; + _direct = input; + if (_valueIsNullable) { var hashSet = input.Where(a => a.Value == null).Select(a => a.Key).ToHashSet(); // Only set the hash-set if the input contained a null value. See `ContainsInverseKey` at L348 as to why. if (hashSet.Count > 0) { - inverseNullValueSet = hashSet; + _inverseNullValueSet = hashSet; } - inverse = input.Where(a => a.Value != null).GroupBy(a => a.Value) + _inverse = input.Where(a => a.Value != null).GroupBy(a => a.Value) .ToDictionary(a => a.Key, a => a.Select(b => b.Key).ToHashSet()); } else { - inverse = input.GroupBy(a => a.Value).ToDictionary(a => a.Key, a => a.Select(b => b.Key).ToHashSet()); + _inverse = input.GroupBy(a => a.Value).ToDictionary(a => a.Key, a => a.Select(b => b.Key).ToHashSet()); } } public S this[T key] { - get => direct[key]; + get => _direct[key]; set { - if (direct.ContainsKey(key)) + if (_direct.TryGetValue(key, out var oldValue)) { - var oldvalue = direct[key]; - if (oldvalue.Equals(value)) + if (oldValue.Equals(value)) { return; } - if (valueIsNullable && oldvalue == null) - { - if (inverseNullValueSet != null && inverseNullValueSet.Contains(key)) - { - inverseNullValueSet.Remove(key); - } - } - else - { - if (inverse.ContainsKey(oldvalue) && inverse[oldvalue].Contains(key)) - { - inverse[oldvalue].Remove(key); - } - } + if (_inverse.TryGetValue(oldValue, out var inverseValue) && inverseValue.Contains(key)) + inverseValue.Remove(key); } - if (valueIsNullable && value == null) + if (_valueIsNullable && value == null) { - if (inverseNullValueSet == null) - { - inverseNullValueSet = new HashSet<T>(); - } - - if (!inverseNullValueSet.Contains(key)) - { - inverseNullValueSet.Add(key); - } + _inverseNullValueSet ??= new HashSet<T>(); + _inverseNullValueSet.Add(key); } else { - if (!inverse.ContainsKey(value)) - { - inverse[value] = new HashSet<T>(); - } - - if (!inverse[value].Contains(key)) - { - inverse[value].Add(key); - } + if (_inverse.TryGetValue(value, out var set)) + set.Add(key); + else + _inverse.Add(value, new HashSet<T>{key}); } - direct[key] = value; + _direct[key] = value; } } public bool ContainsKey(T key) { - return direct.ContainsKey(key); + return _direct.ContainsKey(key); } public bool ContainsInverseKey(S key) { - return valueIsNullable && key == null ? inverseNullValueSet != null : inverse.ContainsKey(key); + return _valueIsNullable && key == null ? _inverseNullValueSet != null : _inverse.ContainsKey(key); } + public bool TryGetValue(T key, out S value) => _direct.TryGetValue(key, out value); + public bool TryGetInverse(S key, out HashSet<T> value) => _inverse.TryGetValue(key, out value); + public HashSet<T> FindInverse(S key) { - if (valueIsNullable && key == null) + if (_valueIsNullable && key == null) { - return inverseNullValueSet ?? new HashSet<T>(); + return _inverseNullValueSet ?? new HashSet<T>(); } - return inverse.ContainsKey(key) - ? inverse[key] - : new HashSet<T>(); + return _inverse.TryGetValue(key, out var value) ? value : new HashSet<T>(); } public void Remove(T key) { - if (direct.ContainsKey(key)) + if (!_direct.TryGetValue(key, out var oldValue)) return; + if (_valueIsNullable && oldValue == null) { - var oldvalue = direct[key]; - if (valueIsNullable && oldvalue == null) - { - if (inverseNullValueSet != null && inverseNullValueSet.Contains(key)) - { - inverseNullValueSet.Remove(key); - } - } - else - { - if (inverse.ContainsKey(oldvalue) && inverse[oldvalue].Contains(key)) - { - inverse[oldvalue].Remove(key); - } - } - - direct.Remove(key); + if (_inverseNullValueSet != null && _inverseNullValueSet.Contains(key)) _inverseNullValueSet.Remove(key); + } + else + { + if (_inverse.TryGetValue(oldValue, out var inverseValue) && inverseValue.Contains(key)) inverseValue.Remove(key); } + + _direct.Remove(key); } public void Clear() { - direct.Clear(); - inverse.Clear(); - inverseNullValueSet = null; + _direct.Clear(); + _inverse.Clear(); + _inverseNullValueSet = null; } } public class BiDictionaryManyToMany<T, S> { - private Dictionary<T, HashSet<S>> direct = new(); - private Dictionary<S, HashSet<T>> inverse = new(); + private readonly Dictionary<T, HashSet<S>> _direct = new(); + private readonly Dictionary<S, HashSet<T>> _inverse = new(); public BiDictionaryManyToMany() { @@ -453,112 +396,82 @@ public BiDictionaryManyToMany() public BiDictionaryManyToMany(Dictionary<T, HashSet<S>> input) { - direct = input; - inverse = new Dictionary<S, HashSet<T>>(); + _direct = input; + _inverse = new Dictionary<S, HashSet<T>>(); foreach (var t in input.Keys) { foreach (var s in input[t]) { - if (!inverse.ContainsKey(s)) - { - inverse[s] = new HashSet<T>(); - } - - inverse[s].Add(t); + if (_inverse.TryGetValue(s, out var inverseValue)) + inverseValue.Add(t); + else + _inverse.Add(s, new HashSet<T> { t }); } } } public HashSet<S> this[T key] { - get => direct[key]; + get => _direct[key]; set { - if (direct.ContainsKey(key)) + if (_direct.TryGetValue(key, out var oldValue)) { - var oldvalue = direct[key]; - if (oldvalue.SetEquals(value)) - { - return; - } + if (oldValue.SetEquals(value)) return; - foreach (var s in oldvalue.ToList()) + foreach (var s in oldValue) { - if (inverse.ContainsKey(s)) - { - if (inverse[s].Contains(key)) - { - inverse[s].Remove(key); - } - - if (inverse[s].Count == 0) - { - inverse.Remove(s); - } - } + if (!_inverse.TryGetValue(s, out var inverseValue)) continue; + if (inverseValue.Contains(key)) inverseValue.Remove(key); + if (inverseValue.Count == 0) _inverse.Remove(s); } } foreach (var s in value) { - if (!inverse.ContainsKey(s)) - { - inverse[s] = new HashSet<T>(); - } - - inverse[s].Add(key); + if (_inverse.TryGetValue(s, out var inverseValue)) + inverseValue.Add(key); + else + _inverse.Add(s, new HashSet<T> { key }); } - direct[key] = value; + _direct[key] = value; } } public bool ContainsKey(T key) { - return direct.ContainsKey(key); + return _direct.ContainsKey(key); } public bool ContainsInverseKey(S key) { - return inverse.ContainsKey(key); + return _inverse.ContainsKey(key); } public HashSet<T> FindInverse(S k) { - return inverse.ContainsKey(k) - ? inverse[k] - : new HashSet<T>(); + return _inverse.TryGetValue(k, out var value) ? value : new HashSet<T>(); } public void Remove(T key) { - if (direct.ContainsKey(key)) - { - var oldvalue = direct[key]; - foreach (var s in oldvalue.ToList()) - { - if (inverse.ContainsKey(s)) - { - if (inverse[s].Contains(key)) - { - inverse[s].Remove(key); - } - - if (inverse[s].Count == 0) - { - inverse.Remove(s); - } - } - } + if (!_direct.TryGetValue(key, out var oldValue)) return; - direct.Remove(key); + foreach (var s in oldValue) + { + if (!_inverse.TryGetValue(s, out var inverseValue)) continue; + if (inverseValue.Contains(key)) inverseValue.Remove(key); + if (inverseValue.Count == 0) _inverse.Remove(s); } + + _direct.Remove(key); } public void Clear() { - direct.Clear(); - inverse.Clear(); + _direct.Clear(); + _inverse.Clear(); } } @@ -566,6 +479,6 @@ public static class Extensions { public static bool HasItems<T>(this List<T> org) { - return org != null && org.Count > 0; + return org is { Count: > 0 }; } } From b3d1586750498fafdf762ce33bd331a9716d6c99 Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Sun, 17 Sep 2023 18:44:38 -0400 Subject: [PATCH 20/34] Filters: Lazy Loading Filterable APIv2 --- Shoko.Server/API/AuthenticationController.cs | 2 +- Shoko.Server/API/v2/Models/common/Filter.cs | 144 +++-- Shoko.Server/API/v2/Models/common/Filters.cs | 46 +- Shoko.Server/API/v2/Models/common/Group.cs | 176 +++---- Shoko.Server/API/v2/Modules/Common.cs | 29 +- .../API/v3/Controllers/FilterController.cs | 2 +- .../API/v3/Controllers/TreeController.cs | 10 +- Shoko.Server/API/v3/Helpers/FilterFactory.cs | 34 ++ Shoko.Server/Filters/FilterEvaluator.cs | 10 +- Shoko.Server/Filters/FilterExtensions.cs | 399 +++++++------- Shoko.Server/Filters/Filterable.cs | 497 ++++++++++++++++-- .../Filters/UserDependentFilterable.cs | 160 +++++- Shoko.Server/Models/SVR_AniDB_Anime.cs | 11 +- Shoko.Server/Models/SVR_AnimeSeries.cs | 16 +- .../Cached/FilterPresetRepository.cs | 2 +- Shoko.Server/Settings/ServerSettings.cs | 1 + Shoko.Server/Utilities/Languages.cs | 12 + 17 files changed, 1079 insertions(+), 472 deletions(-) diff --git a/Shoko.Server/API/AuthenticationController.cs b/Shoko.Server/API/AuthenticationController.cs index f77e32870..aab916a8e 100644 --- a/Shoko.Server/API/AuthenticationController.cs +++ b/Shoko.Server/API/AuthenticationController.cs @@ -24,7 +24,7 @@ public class AuthenticationController : BaseController [ProducesResponseType(400)] [ProducesResponseType(401)] [ProducesResponseType(200)] - public ActionResult<dynamic> Login(AuthUser auth) + public ActionResult<object> Login(AuthUser auth) { if (!ModelState.IsValid || string.IsNullOrEmpty(auth.user?.Trim())) { diff --git a/Shoko.Server/API/v2/Models/common/Filter.cs b/Shoko.Server/API/v2/Models/common/Filter.cs index 6b0db010c..431b6e77b 100644 --- a/Shoko.Server/API/v2/Models/common/Filter.cs +++ b/Shoko.Server/API/v2/Models/common/Filter.cs @@ -3,9 +3,10 @@ using System.Linq; using System.Runtime.Serialization; using Microsoft.AspNetCore.Http; -using Shoko.Models.Client; +using Microsoft.Extensions.DependencyInjection; using Shoko.Models.Enums; using Shoko.Models.Server; +using Shoko.Server.Filters; using Shoko.Server.Models; using Shoko.Server.Repositories; @@ -33,107 +34,88 @@ public Filter() groups = new List<Group>(); } - internal new static Filter GenerateFromGroupFilter(HttpContext ctx, SVR_GroupFilter gf, int uid, bool nocast, + internal static Filter GenerateFromGroupFilter(HttpContext ctx, FilterPreset gf, int uid, bool nocast, bool notag, int level, - bool all, bool allpic, int pic, TagFilter.Filter tagfilter) + bool all, bool allpic, int pic, TagFilter.Filter tagfilter, List<IGrouping<int, int>> evaluatedResults = null) { var groups = new List<Group>(); - var filter = new Filter { name = gf.GroupFilterName, id = gf.GroupFilterID, size = 0 }; - if (gf.GroupsIds.ContainsKey(uid)) + var filter = new Filter { name = gf.Name, id = gf.FilterPresetID, size = 0 }; + if (evaluatedResults == null) { - var groupsh = gf.GroupsIds[uid]; - if (groupsh.Count != 0) - { - filter.size = groupsh.Count; + var evaluator = ctx.RequestServices.GetRequiredService<FilterEvaluator>(); + evaluatedResults = evaluator.EvaluateFilter(gf, ctx.GetUser().JMMUserID).ToList(); + } + + if (evaluatedResults.Count != 0) + { + filter.size = evaluatedResults.Count; - // Populate Random Art - List<SVR_AnimeGroup> groupsList; + // Populate Random Art - List<SVR_AnimeSeries> arts = null; - if (gf.SeriesIds.ContainsKey(uid)) + List<SVR_AnimeSeries> arts = null; + var seriesList = evaluatedResults.SelectMany(a => a).Select(RepoFactory.AnimeSeries.GetByID).ToList(); + var groupsList = evaluatedResults.Select(r => RepoFactory.AnimeGroup.GetByID(r.Key)).ToList(); + if (pic == 1) + { + arts = seriesList.Where(SeriesHasCompleteArt).Where(a => a != null).ToList(); + if (arts.Count == 0) { - var seriesList = gf.SeriesIds[uid].Select(RepoFactory.AnimeSeries.GetByID).ToList(); - groupsList = seriesList.Select(a => a.AnimeGroupID).Distinct() - .Select(RepoFactory.AnimeGroup.GetByID).ToList(); - if (pic == 1) - { - arts = seriesList.Where(SeriesHasCompleteArt).Where(a => a != null).ToList(); - if (arts.Count == 0) - { - arts = seriesList.Where(SeriesHasMostlyCompleteArt).Where(a => a != null).ToList(); - } - - if (arts.Count == 0) - { - arts = seriesList; - } - } + arts = seriesList.Where(SeriesHasMostlyCompleteArt).Where(a => a != null).ToList(); } - else + + if (arts.Count == 0) { - groupsList = new List<SVR_AnimeGroup>(); + arts = seriesList; } + } - if (arts?.Count > 0) - { - var rand = new Random(); - var anime = arts[rand.Next(arts.Count)]; + if (arts?.Count > 0) + { + var rand = new Random(); + var anime = arts[rand.Next(arts.Count)]; - var fanarts = GetFanartFromSeries(anime); - if (fanarts.Any()) - { - var fanart = fanarts[rand.Next(fanarts.Count)]; - filter.art.fanart.Add(new Art - { - index = 0, - url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.TvDB_FanArt, - fanart.TvDB_ImageFanartID) - }); - } - - var banners = GetBannersFromSeries(anime); - if (banners.Any()) - { - var banner = banners[rand.Next(banners.Count)]; - filter.art.banner.Add(new Art - { - index = 0, - url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.TvDB_Banner, - banner.TvDB_ImageWideBannerID) - }); - } - - filter.art.thumb.Add(new Art + var fanarts = GetFanartFromSeries(anime); + if (fanarts.Any()) + { + var fanart = fanarts[rand.Next(fanarts.Count)]; + filter.art.fanart.Add(new Art { - url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.AniDB_Cover, - anime.AniDB_ID), - index = 0 + index = 0, + url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.TvDB_FanArt, + fanart.TvDB_ImageFanartID) }); } - var order = new Dictionary<CL_AnimeGroup_User, Group>(); - if (level > 0) + var banners = GetBannersFromSeries(anime); + if (banners.Any()) { - foreach (var ag in groupsList) + var banner = banners[rand.Next(banners.Count)]; + filter.art.banner.Add(new Art { - var group = - Group.GenerateFromAnimeGroup(ctx, ag, uid, nocast, notag, level - 1, all, - filter.id, allpic, pic, tagfilter); - groups.Add(group); - order.Add(ag.GetUserContract(uid), group); - } + index = 0, + url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.TvDB_Banner, + banner.TvDB_ImageWideBannerID) + }); } - if (groups.Count > 0) + filter.art.thumb.Add(new Art { - // Proper Sorting! - IEnumerable<CL_AnimeGroup_User> grps = order.Keys; - grps = gf.SortCriteriaList.Count != 0 - ? GroupFilterHelper.Sort(grps, gf) - : grps.OrderBy(a => a.GroupName); - groups = grps.Select(a => order[a]).ToList(); - filter.groups = groups; - } + url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.AniDB_Cover, + anime.AniDB_ID), + index = 0 + }); + } + + if (level > 0) + { + groups.AddRange(groupsList.Select(ag => + Group.GenerateFromAnimeGroup(ctx, ag, uid, nocast, notag, level - 1, all, filter.id, allpic, pic, tagfilter, + evaluatedResults.FirstOrDefault(a => a.Key == ag.AnimeGroupID)?.ToList()))); + } + + if (groups.Count > 0) + { + filter.groups = groups; } } diff --git a/Shoko.Server/API/v2/Models/common/Filters.cs b/Shoko.Server/API/v2/Models/common/Filters.cs index a10659efd..3bddab479 100644 --- a/Shoko.Server/API/v2/Models/common/Filters.cs +++ b/Shoko.Server/API/v2/Models/common/Filters.cs @@ -2,9 +2,11 @@ using System.Linq; using System.Runtime.Serialization; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Shoko.Commons.Extensions; using Shoko.Models; using Shoko.Models.Enums; +using Shoko.Server.Filters; using Shoko.Server.Models; using Shoko.Server.Repositories; @@ -24,43 +26,37 @@ public Filters() filters = new List<Filters>(); } - internal static Filters GenerateFromGroupFilter(HttpContext ctx, SVR_GroupFilter gf, int uid, bool nocast, + internal static Filters GenerateFromGroupFilter(HttpContext ctx, FilterPreset gf, int uid, bool nocast, bool notag, int level, - bool all, bool allpic, int pic, TagFilter.Filter tagfilter) + bool all, bool allpic, int pic, TagFilter.Filter tagfilter, Dictionary<FilterPreset, IEnumerable<IGrouping<int, int>>> evaluatedResults = null) { - var f = new Filters { id = gf.GroupFilterID, name = gf.GroupFilterName }; - - var _ = new List<string>(); - var gfs = RepoFactory.GroupFilter.GetByParentID(f.id).AsParallel() - // Not invisible in clients - .Where(a => !a.IsHidden && - // and Has groups or is a directory - ((a.GroupsIds.ContainsKey(uid) && a.GroupsIds[uid].Count > 0) || - a.IsDirectory) && - // and is not a blacklisted tag - !((a.FilterType & (int)GroupFilterType.Tag) != 0 && - TagFilter.IsTagBlackListed(a.GroupFilterName, tagfilter))); + var f = new Filters { id = gf.FilterPresetID, name = gf.Name }; + var hideCategories = ctx.GetUser().GetHideCategories(); + var gfs = RepoFactory.FilterPreset.GetByParentID(f.id).AsParallel().Where(a => + !a.Hidden && !((a.FilterType & GroupFilterType.Tag) != 0 && + (!hideCategories.Contains(a.Name) || TagFilter.IsTagBlackListed(a.Name, tagfilter)))) + .ToList(); if (level > 0) { - var filters = gfs.Select(cgf => - Filter.GenerateFromGroupFilter(ctx, cgf, uid, nocast, notag, level - 1, all, allpic, pic, - tagfilter)).ToList(); - - if (gf.FilterType == ((int)GroupFilterType.Season | (int)GroupFilterType.Directory)) - { - f.filters = filters.OrderBy(a => a.name, new SeasonComparator()).Cast<Filters>().ToList(); - } - else + if (evaluatedResults == null) { - f.filters = filters.OrderByNatural(a => a.name).Cast<Filters>().ToList(); + var evaluator = ctx.RequestServices.GetRequiredService<FilterEvaluator>(); + evaluatedResults = evaluator.BatchEvaluateFilters(gfs, ctx.GetUser().JMMUserID); } + var filters = gfs.Select(cgf => + Filter.GenerateFromGroupFilter(ctx, cgf, uid, nocast, notag, level - 1, all, allpic, pic, tagfilter, evaluatedResults[cgf].ToList())).ToList(); + + f.filters = gf.FilterType == (GroupFilterType.Season | GroupFilterType.Directory) + ? filters.OrderBy(a => a.name, new SeasonComparator()).Cast<Filters>().ToList() + : filters.OrderByNatural(a => a.name).Cast<Filters>().ToList(); + f.size = filters.Count; } else { - f.size = gfs.Count(); + f.size = gfs.Count; } f.url = APIV2Helper.ConstructFilterIdUrl(ctx, f.id); diff --git a/Shoko.Server/API/v2/Models/common/Group.cs b/Shoko.Server/API/v2/Models/common/Group.cs index 59eaefdb9..6e5fc74f2 100644 --- a/Shoko.Server/API/v2/Models/common/Group.cs +++ b/Shoko.Server/API/v2/Models/common/Group.cs @@ -4,8 +4,10 @@ using System.Linq; using System.Runtime.Serialization; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Shoko.Models.Enums; using Shoko.Models.PlexAndKodi; +using Shoko.Server.Filters; using Shoko.Server.Models; using Shoko.Server.PlexAndKodi; using Shoko.Server.Repositories; @@ -31,7 +33,7 @@ public Group() public static Group GenerateFromAnimeGroup(HttpContext ctx, SVR_AnimeGroup ag, int uid, bool nocast, bool notag, int level, - bool all, int filterid, bool allpic, int pic, TagFilter.Filter tagfilter) + bool all, int filterid, bool allpic, int pic, TagFilter.Filter tagfilter, List<int> evaluatedSeriesIDs = null) { var g = new Group { @@ -43,124 +45,106 @@ public static Group GenerateFromAnimeGroup(HttpContext ctx, SVR_AnimeGroup ag, i edited = ag.DateTimeUpdated }; - SVR_GroupFilter filter = null; - if (filterid > 0) + if (filterid > 0 && evaluatedSeriesIDs == null) { - filter = RepoFactory.GroupFilter.GetByID(filterid); - if (filter?.ApplyToSeries == 0) - { - filter = null; - } + var filter = RepoFactory.FilterPreset.GetByID(filterid); + var evaluator = ctx.RequestServices.GetRequiredService<FilterEvaluator>(); + evaluatedSeriesIDs = evaluator.EvaluateFilter(filter, ctx.GetUser().JMMUserID).FirstOrDefault(a => a.Key == ag.AnimeGroupID)?.ToList(); } - List<SVR_AniDB_Anime> animes; - if (filter != null) + var animes = evaluatedSeriesIDs != null + ? evaluatedSeriesIDs.Select(id => RepoFactory.AnimeSeries.GetByID(id)).Select(ser => ser.GetAnime()).Where(a => a != null).ToList() + : ag.Anime?.OrderBy(a => a.BeginYear).ThenBy(a => a.AirDate ?? DateTime.MaxValue).ToList(); + + if (animes is not { Count: > 0 }) return g; + + var anime = animes.FirstOrDefault(); + if (anime == null) return g; + + PopulateArtFromAniDBAnime(ctx, animes, g, allpic, pic); + + List<SVR_AnimeEpisode> ael; + if (evaluatedSeriesIDs != null) { - animes = filter.SeriesIds[uid].Select(id => RepoFactory.AnimeSeries.GetByID(id)) - .Where(ser => ser?.AnimeGroupID == ag.AnimeGroupID).Select(ser => ser.GetAnime()) - .Where(a => a != null).OrderBy(a => a.BeginYear).ThenBy(a => a.AirDate ?? DateTime.MaxValue) - .ToList(); + var series = evaluatedSeriesIDs.Select(id => RepoFactory.AnimeSeries.GetByID(id)).ToList(); + ael = series.SelectMany(ser => ser?.GetAnimeEpisodes()).Where(a => a != null).ToList(); + g.size = series.Count; } else { - animes = ag.Anime?.OrderBy(a => a.BeginYear).ThenBy(a => a.AirDate ?? DateTime.MaxValue).ToList(); + var series = ag.GetAllSeries(); + ael = series.SelectMany(a => a?.GetAnimeEpisodes()).Where(a => a != null).ToList(); + g.size = series.Count; } - if (animes != null && animes.Count > 0) - { - var anime = animes.FirstOrDefault(a => a != null); - if (anime == null) - { - return g; - } + GenerateSizes(g, ael, uid); - PopulateArtFromAniDBAnime(ctx, animes, g, allpic, pic); + g.air = anime.AirDate?.ToPlexDate() ?? string.Empty; - List<SVR_AnimeEpisode> ael; - if (filter != null && filter.SeriesIds.ContainsKey(uid)) - { - var series = filter.SeriesIds[uid].Select(id => RepoFactory.AnimeSeries.GetByID(id)) - .Where(ser => (ser?.AnimeGroupID ?? 0) == ag.AnimeGroupID).ToList(); - ael = series.SelectMany(ser => ser?.GetAnimeEpisodes()).Where(a => a != null) - .ToList(); - g.size = series.Count; - } - else - { - var series = ag.GetAllSeries(); - ael = series.SelectMany(a => a?.GetAnimeEpisodes()).Where(a => a != null).ToList(); - g.size = series.Count; - } - - GenerateSizes(g, ael, uid); - - g.air = anime.AirDate?.ToPlexDate() ?? string.Empty; - - g.rating = Math.Round(ag.AniDBRating / 100, 1).ToString(CultureInfo.InvariantCulture); - g.summary = anime.Description ?? string.Empty; - g.titles = anime.GetTitles().Select(s => new AnimeTitle - { - Type = s.TitleType.ToString().ToLower(), Language = s.LanguageCode, Title = s.Title - }).ToList(); - g.year = anime.BeginYear.ToString(); + g.rating = Math.Round(ag.AniDBRating / 100, 1).ToString(CultureInfo.InvariantCulture); + g.summary = anime.Description ?? string.Empty; + g.titles = anime.GetTitles().Select(s => new AnimeTitle + { + Type = s.TitleType.ToString().ToLower(), Language = s.LanguageCode, Title = s.Title + }).ToList(); + g.year = anime.BeginYear.ToString(); - if (!notag && ag.Contract.Stat_AllTags != null) - { - g.tags = TagFilter.String.ProcessTags(tagfilter, ag.Contract.Stat_AllTags.ToList()); - } + if (!notag && ag.Contract.Stat_AllTags != null) + { + g.tags = TagFilter.String.ProcessTags(tagfilter, ag.Contract.Stat_AllTags.ToList()); + } - if (!nocast) + if (!nocast) + { + var xref_animestaff = + RepoFactory.CrossRef_Anime_Staff.GetByAnimeIDAndRoleType(anime.AnimeID, StaffRoleType.Seiyuu); + foreach (var xref in xref_animestaff) { - var xref_animestaff = - RepoFactory.CrossRef_Anime_Staff.GetByAnimeIDAndRoleType(anime.AnimeID, StaffRoleType.Seiyuu); - foreach (var xref in xref_animestaff) + if (xref.RoleID == null) { - if (xref.RoleID == null) - { - continue; - } - - var character = RepoFactory.AnimeCharacter.GetByID(xref.RoleID.Value); - if (character == null) - { - continue; - } + continue; + } - var staff = RepoFactory.AnimeStaff.GetByID(xref.StaffID); - if (staff == null) - { - continue; - } + var character = RepoFactory.AnimeCharacter.GetByID(xref.RoleID.Value); + if (character == null) + { + continue; + } - var role = new Role - { - character = character.Name, - character_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.Character, - xref.RoleID.Value), - staff = staff.Name, - staff_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.Staff, - xref.StaffID), - role = xref.Role, - type = ((StaffRoleType)xref.RoleType).ToString() - }; - if (g.roles == null) - { - g.roles = new List<Role>(); - } + var staff = RepoFactory.AnimeStaff.GetByID(xref.StaffID); + if (staff == null) + { + continue; + } - g.roles.Add(role); + var role = new Role + { + character = character.Name, + character_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.Character, + xref.RoleID.Value), + staff = staff.Name, + staff_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.Staff, + xref.StaffID), + role = xref.Role, + type = ((StaffRoleType)xref.RoleType).ToString() + }; + if (g.roles == null) + { + g.roles = new List<Role>(); } + + g.roles.Add(role); } + } - if (level > 0) + if (level > 0) + { + foreach (var ada in animes.Select(a => RepoFactory.AnimeSeries.GetByAnimeID(a.AnimeID))) { - foreach (var ada in animes.Select(a => RepoFactory.AnimeSeries.GetByAnimeID(a.AnimeID))) - { - g.series.Add(Serie.GenerateFromAnimeSeries(ctx, ada, uid, nocast, notag, level - 1, all, allpic, - pic, tagfilter)); - } - // we already sorted animes, so no need to sort + g.series.Add(Serie.GenerateFromAnimeSeries(ctx, ada, uid, nocast, notag, level - 1, all, allpic, + pic, tagfilter)); } + // we already sorted animes, so no need to sort } return g; diff --git a/Shoko.Server/API/v2/Modules/Common.cs b/Shoko.Server/API/v2/Modules/Common.cs index 2b4091ce4..d720efb27 100644 --- a/Shoko.Server/API/v2/Modules/Common.cs +++ b/Shoko.Server/API/v2/Modules/Common.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using NLog; using Quartz; @@ -22,6 +23,7 @@ using Shoko.Server.Commands; using Shoko.Server.Commands.AniDB; using Shoko.Server.Extensions; +using Shoko.Server.Filters; using Shoko.Server.Models; using Shoko.Server.Repositories; using Shoko.Server.Scheduling.Jobs; @@ -2694,7 +2696,7 @@ public async Task<ActionResult> RunCloudImport() /// Handle /api/filter /// Using if without ?id consider using ?level as it will scan resursive for object from Filter to RawFile /// </summary> - /// <returns>Filter or List<Filter></returns> + /// <returns><see cref="Filter"/> or <see cref="List{Filter}"/></returns> [HttpGet("filter")] public object GetFilters([FromQuery] API_Call_Parameters para) { @@ -2727,25 +2729,26 @@ internal object GetAllFilters(int uid, bool nocast, bool notag, int level, bool { id = 0, name = "Filters", viewed = 0, url = APIV2Helper.ConstructFilterUrl(HttpContext) }; - var allGfs = RepoFactory.GroupFilter.GetTopLevel() - .Where(a => !a.IsHidden && - ((a.GroupsIds.ContainsKey(uid) && a.GroupsIds[uid].Count > 0) || - a.IsDirectory)) - .ToList(); + var allGfs = RepoFactory.FilterPreset.GetTopLevel().Where(a => !a.Hidden).ToList(); var _filters = new List<APIFilters>(); + var evaluator = HttpContext.RequestServices.GetRequiredService<FilterEvaluator>(); + var user = HttpContext.GetUser(); + var hideCategories = user.GetHideCategories(); + var filtersToEvaluate = level > 1 + ? RepoFactory.FilterPreset.GetAll().Where(a => (a.FilterType & GroupFilterType.Tag) == 0 || !hideCategories.Contains(a.Name)).ToList() + : allGfs; + var result = evaluator.BatchEvaluateFilters(filtersToEvaluate, user.JMMUserID); foreach (var gf in allGfs) { APIFilters filter; - if (!gf.IsDirectory) + if (!gf.IsDirectory()) { - filter = Filter.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, - tagfilter); + filter = Filter.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, tagfilter, result[gf].ToList()); } else { - filter = APIFilters.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, - tagfilter); + filter = APIFilters.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, tagfilter, result); } _filters.Add(filter); @@ -2787,9 +2790,9 @@ internal object GetAllFilters(int uid, bool nocast, bool notag, int level, bool internal object GetFilter(int id, int uid, bool nocast, bool notag, int level, bool all, bool allpic, int pic, TagFilter.Filter tagfilter) { - var gf = RepoFactory.GroupFilter.GetByID(id); + var gf = RepoFactory.FilterPreset.GetByID(id); - if (gf.IsDirectory) + if (gf.IsDirectory()) { // if it's a directory, it IS a filter-inception; var fgs = APIFilters.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, diff --git a/Shoko.Server/API/v3/Controllers/FilterController.cs b/Shoko.Server/API/v3/Controllers/FilterController.cs index 1e8b9a961..c5670629e 100644 --- a/Shoko.Server/API/v3/Controllers/FilterController.cs +++ b/Shoko.Server/API/v3/Controllers/FilterController.cs @@ -53,7 +53,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] bool withConditio { var user = User; - return _filterEvaluator.BatchEvaluateFilters(RepoFactory.FilterPreset.GetTopLevel(), user.JMMUserID) + return _filterEvaluator.BatchEvaluateFilters(RepoFactory.FilterPreset.GetTopLevel(), user.JMMUserID, true) .Where(kv => { var filter = kv.Key; diff --git a/Shoko.Server/API/v3/Controllers/TreeController.cs b/Shoko.Server/API/v3/Controllers/TreeController.cs index 83c1640fd..b6e1d9135 100644 --- a/Shoko.Server/API/v3/Controllers/TreeController.cs +++ b/Shoko.Server/API/v3/Controllers/TreeController.cs @@ -4,6 +4,7 @@ using System.Linq; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Shoko.Commons.Extensions; using Shoko.Models.Enums; using Shoko.Plugin.Abstractions.DataModels; using Shoko.Plugin.Abstractions.Extensions; @@ -94,10 +95,11 @@ [FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValu if (!filterPreset.IsDirectory()) return new ListResult<Filter>(); - return RepoFactory.FilterPreset.GetByParentID(filterID) - .Where(filter => showHidden || !filter.Hidden) - .OrderBy(filter => filter.Name) - .ToListResult(filter => _filterFactory.GetFilter(filter), page, pageSize); + var hideCategories = HttpContext.GetUser().GetHideCategories(); + + return _filterFactory.GetFilters(RepoFactory.FilterPreset.GetByParentID(filterID) + .Where(filter => (showHidden || !filter.Hidden) && !hideCategories.Contains(filter.Name)).OrderBy(a => a.Name).ToList()) + .ToListResult(page, pageSize); } /// <summary> diff --git a/Shoko.Server/API/v3/Helpers/FilterFactory.cs b/Shoko.Server/API/v3/Helpers/FilterFactory.cs index e85ab7808..c4611ad78 100644 --- a/Shoko.Server/API/v3/Helpers/FilterFactory.cs +++ b/Shoko.Server/API/v3/Helpers/FilterFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using Microsoft.AspNetCore.Http; @@ -52,6 +53,39 @@ public Filter GetFilter(FilterPreset groupFilter, bool fullModel = false) return filter; } + public IEnumerable<Filter> GetFilters(List<FilterPreset> groupFilters, bool fullModel = false) + { + var user = _context.GetUser(); + var evaluate = groupFilters.Any(a => !a.IsDirectory()); + var results = evaluate ? _evaluator.BatchEvaluateFilters(groupFilters, user.JMMUserID, true) : null; + var filters = groupFilters.Select(groupFilter => + { + var filter = new Filter + { + IDs = new Filter.FilterIDs + { + ID = groupFilter.FilterPresetID, ParentFilter = groupFilter.ParentFilterPresetID + }, + Name = groupFilter.Name, + IsLocked = groupFilter.Locked, + IsDirectory = groupFilter.IsDirectory(), + IsHidden = groupFilter.Hidden, + ApplyAtSeriesLevel = groupFilter.ApplyAtSeriesLevel, + }; + + if (fullModel) + { + filter.Expression = GetExpressionTree(groupFilter.Expression); + filter.Sorting = GetSortingCriteria(groupFilter.SortingExpression); + } + + filter.Size = filter.IsDirectory ? RepoFactory.FilterPreset.GetByParentID(groupFilter.FilterPresetID).Count : results?[groupFilter].Count() ?? 0; + return filter; + }); + + return filters; + } + public static Filter.FilterCondition GetExpressionTree(FilterExpression expression) { if (expression is null) return null; diff --git a/Shoko.Server/Filters/FilterEvaluator.cs b/Shoko.Server/Filters/FilterEvaluator.cs index a61fcc858..d64676681 100644 --- a/Shoko.Server/Filters/FilterEvaluator.cs +++ b/Shoko.Server/Filters/FilterEvaluator.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using Shoko.Models.Enums; using Shoko.Server.Filters.SortingSelectors; using Shoko.Server.Models; using Shoko.Server.Repositories; @@ -53,15 +54,16 @@ public IEnumerable<IGrouping<int, int>> EvaluateFilter(FilterPreset filter, int? return result; } - + /// <summary> /// Evaluate the given filter, applying the necessary logic /// </summary> /// <param name="filters"></param> /// <param name="userID"></param> + /// <param name="skipSorting"></param> /// <returns>SeriesIDs, grouped by the direct parent GroupID</returns> /// <exception cref="ArgumentNullException"></exception> - public Dictionary<FilterPreset, IEnumerable<IGrouping<int, int>>> BatchEvaluateFilters(List<FilterPreset> filters, int? userID) + public Dictionary<FilterPreset, IEnumerable<IGrouping<int, int>>> BatchEvaluateFilters(List<FilterPreset> filters, int? userID, bool skipSorting=false) { ArgumentNullException.ThrowIfNull(filters); if (!filters.Any()) return new Dictionary<FilterPreset, IEnumerable<IGrouping<int, int>>>(); @@ -78,12 +80,12 @@ public Dictionary<FilterPreset, IEnumerable<IGrouping<int, int>>> BatchEvaluateF // Filtering var filtered = filterables.SelectMany(a => filters.Select(f => (Filter: f, FilterableWithID: a))) - .Where(a => a.Filter.Expression?.Evaluate(a.FilterableWithID.Filterable) ?? true); + .Where(a => (a.Filter.FilterType & GroupFilterType.Directory) == 0 && (a.Filter.Expression?.Evaluate(a.FilterableWithID.Filterable) ?? true)); // ordering var grouped = filtered.GroupBy(a => a.Filter).ToDictionary(a => a.Key, f => { - var ordered = OrderFilterables(f.Key, f.Select(a => a.FilterableWithID)); + var ordered = skipSorting ? f.Select(a => a.FilterableWithID) : OrderFilterables(f.Key, f.Select(a => a.FilterableWithID)); var result = ordered.GroupBy(a => a.GroupID, a => a.SeriesID); if (!f.Key.ApplyAtSeriesLevel) diff --git a/Shoko.Server/Filters/FilterExtensions.cs b/Shoko.Server/Filters/FilterExtensions.cs index b0f069633..46a8081b4 100644 --- a/Shoko.Server/Filters/FilterExtensions.cs +++ b/Shoko.Server/Filters/FilterExtensions.cs @@ -18,56 +18,62 @@ public static class FilterExtensions public static Filterable ToFilterable(this SVR_AnimeSeries series) { var anime = series.GetAnime(); - var name = series.GetSeriesName(); - var audio = new HashSet<string>(); - var audioNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Languages.Select(b => b.LanguageName)); - if (audioNames.Any()) audio = audioNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); - var subtitles = new HashSet<string>(); - var subtitleNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Subtitles.Select(b => b.LanguageName)); - if (subtitleNames.Any()) subtitles = subtitleNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); - - // TODO optimize this a bunch. Lots of duplicate calls. Contract should be severely trimmed var filterable = new Filterable { - Name = name, - SortingName = name.GetSortName(), - SeriesCount = 1, - AirDate = anime?.AirDate, - MissingEpisodes = series.Contract?.MissingEpisodeCount ?? 0, - MissingEpisodesCollecting = series.Contract?.MissingEpisodeCountGroups ?? 0, - Tags = anime?.GetAllTags() ?? new HashSet<string>(), - CustomTags = + NameDelegate = series.GetSeriesName, + SortingNameDelegate = () => series.GetSeriesName().GetSortName(), + SeriesCountDelegate = () => 1, + AirDateDelegate = () => anime?.AirDate, + MissingEpisodesDelegate = () => series.Contract?.MissingEpisodeCount ?? 0, + MissingEpisodesCollectingDelegate = () => series.Contract?.MissingEpisodeCountGroups ?? 0, + TagsDelegate = () => anime?.GetAllTags() ?? new HashSet<string>(), + CustomTagsDelegate = () => series.Contract?.AniDBAnime?.CustomTags?.Select(a => a.TagName).ToHashSet(StringComparer.InvariantCultureIgnoreCase) ?? new HashSet<string>(), - Years = GetYears(series), - Seasons = anime.GetSeasons().ToHashSet(), - HasTvDBLink = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(), - HasMissingTvDbLink = HasMissingTvDBLink(series), - HasTMDbLink = series.Contract?.CrossRefAniDBMovieDB != null, - HasMissingTMDbLink = HasMissingTMDbLink(series), - HasTraktLink = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), - HasMissingTraktLink = !RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), - IsFinished = series.Contract?.AniDBAnime?.AniDBAnime?.EndDate != null && series.Contract.AniDBAnime.AniDBAnime.EndDate.Value < DateTime.Now, - LastAirDate = + YearsDelegate = () => GetYears(series), + SeasonsDelegate = () => anime.GetSeasons().ToHashSet(), + HasTvDBLinkDelegate = () => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(), + HasMissingTvDbLinkDelegate = () => HasMissingTvDBLink(series), + HasTMDbLinkDelegate = () => series.Contract?.CrossRefAniDBMovieDB != null, + HasMissingTMDbLinkDelegate = () => HasMissingTMDbLink(series), + HasTraktLinkDelegate = () => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), + HasMissingTraktLinkDelegate = () => !RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), + IsFinishedDelegate = + () => series.Contract?.AniDBAnime?.AniDBAnime?.EndDate != null && series.Contract.AniDBAnime.AniDBAnime.EndDate.Value < DateTime.Now, + LastAirDateDelegate = () => series.EndDate ?? series.GetAnimeEpisodes().Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), - AddedDate = series.DateTimeCreated, - LastAddedDate = series.GetVideoLocals().Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), - EpisodeCount = anime?.EpisodeCountNormal ?? 0, - TotalEpisodeCount = anime?.EpisodeCount ?? 0, - LowestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), - HighestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), - AnimeTypes = anime == null + AddedDateDelegate = () => series.DateTimeCreated, + LastAddedDateDelegate = () => series.GetVideoLocals().Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), + EpisodeCountDelegate = () => anime?.EpisodeCountNormal ?? 0, + TotalEpisodeCountDelegate = () => anime?.EpisodeCount ?? 0, + LowestAniDBRatingDelegate = () => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), + HighestAniDBRatingDelegate = () => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), + AnimeTypesDelegate = () => anime == null ? new HashSet<string>() : new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) { ((AnimeType)anime.AnimeType).ToString() }, - VideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality ?? new HashSet<string>(), - SharedVideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), - AudioLanguages = series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet<string>(), - SharedAudioLanguages = audio, - SubtitleLanguages = series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet<string>(), - SharedSubtitleLanguages = subtitles, + VideoSourcesDelegate = () => series.Contract?.AniDBAnime?.Stat_AllVideoQuality ?? new HashSet<string>(), + SharedVideoSourcesDelegate = () => series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), + AudioLanguagesDelegate = () => series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet<string>(), + SharedAudioLanguagesDelegate = () => + { + var audio = new HashSet<string>(); + var audioNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null) + .Select(a => a.Languages.Select(b => b.LanguageName)); + if (audioNames.Any()) audio = audioNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + return audio; + }, + SubtitleLanguagesDelegate = () => series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet<string>(), + SharedSubtitleLanguagesDelegate = () => + { + var subtitles = new HashSet<string>(); + var subtitleNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null) + .Select(a => a.Subtitles.Select(b => b.LanguageName)); + if (subtitleNames.Any()) subtitles = subtitleNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + return subtitles; + }, }; return filterable; @@ -79,65 +85,69 @@ public static UserDependentFilterable ToUserDependentFilterable(this SVR_AnimeSe var user = series.GetUserRecord(userID); var vote = anime?.UserVote; var watchedDates = series.GetVideoLocals().Select(a => a.GetUserRecord(userID)?.WatchedDate).Where(a => a != null).OrderBy(a => a).ToList(); - var name = series.GetSeriesName(); - - var audio = new HashSet<string>(); - var audioNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Languages.Select(b => b.LanguageName)); - if (audioNames.Any()) audio = audioNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); - var subtitles = new HashSet<string>(); - var subtitleNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Subtitles.Select(b => b.LanguageName)); - if (subtitleNames.Any()) subtitles = subtitleNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); var filterable = new UserDependentFilterable { - Name = name, - SortingName = name.GetSortName(), - SeriesCount = 1, - AirDate = anime?.AirDate, - MissingEpisodes = series.Contract?.MissingEpisodeCount ?? 0, - MissingEpisodesCollecting = series.Contract?.MissingEpisodeCountGroups ?? 0, - Tags = anime?.GetAllTags() ?? new HashSet<string>(), - CustomTags = + NameDelegate = series.GetSeriesName, + SortingNameDelegate = () => series.GetSeriesName().GetSortName(), + SeriesCountDelegate = () => 1, + AirDateDelegate = () => anime?.AirDate, + MissingEpisodesDelegate = () => series.Contract?.MissingEpisodeCount ?? 0, + MissingEpisodesCollectingDelegate = () => series.Contract?.MissingEpisodeCountGroups ?? 0, + TagsDelegate = () => anime?.GetAllTags() ?? new HashSet<string>(), + CustomTagsDelegate = () => series.Contract?.AniDBAnime?.CustomTags?.Select(a => a.TagName).ToHashSet(StringComparer.InvariantCultureIgnoreCase) ?? new HashSet<string>(), - Years = GetYears(series), - Seasons = anime?.GetSeasons().ToHashSet(), - HasTvDBLink = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(), - HasMissingTvDbLink = HasMissingTvDBLink(series), - HasTMDbLink = series.Contract?.CrossRefAniDBMovieDB != null, - HasMissingTMDbLink = HasMissingTMDbLink(series), - HasTraktLink = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), - HasMissingTraktLink = !RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), - IsFinished = series.Contract?.AniDBAnime?.AniDBAnime?.EndDate != null && series.Contract.AniDBAnime.AniDBAnime.EndDate.Value < DateTime.Now, - LastAirDate = + YearsDelegate = () => GetYears(series), + SeasonsDelegate = () => anime?.GetSeasons().ToHashSet(), + HasTvDBLinkDelegate = () => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(), + HasMissingTvDbLinkDelegate = () => HasMissingTvDBLink(series), + HasTMDbLinkDelegate = () => series.Contract?.CrossRefAniDBMovieDB != null, + HasMissingTMDbLinkDelegate = () => HasMissingTMDbLink(series), + HasTraktLinkDelegate = () => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), + HasMissingTraktLinkDelegate = () => !RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), + IsFinishedDelegate = () => series.Contract?.AniDBAnime?.AniDBAnime?.EndDate != null && series.Contract.AniDBAnime.AniDBAnime.EndDate.Value < DateTime.Now, + LastAirDateDelegate = () => series.EndDate ?? series.GetAnimeEpisodes().Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), - AddedDate = series.DateTimeCreated, - LastAddedDate = series.GetVideoLocals().Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), - EpisodeCount = anime?.EpisodeCountNormal ?? 0, - TotalEpisodeCount = anime?.EpisodeCount ?? 0, - LowestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), - HighestAniDBRating = decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), - AnimeTypes = anime == null + AddedDateDelegate = () => series.DateTimeCreated, + LastAddedDateDelegate = () => series.GetVideoLocals().Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), + EpisodeCountDelegate = () => anime?.EpisodeCountNormal ?? 0, + TotalEpisodeCountDelegate = () => anime?.EpisodeCount ?? 0, + LowestAniDBRatingDelegate = () => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), + HighestAniDBRatingDelegate = () => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), + AnimeTypesDelegate = () => anime == null ? new HashSet<string>() : new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) { ((AnimeType)anime.AnimeType).ToString() }, - VideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality ?? new HashSet<string>(), - SharedVideoSources = series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), - AudioLanguages = series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet<string>(), - SharedAudioLanguages = audio, - SubtitleLanguages = series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet<string>(), - SharedSubtitleLanguages = subtitles, - IsFavorite = false, - WatchedEpisodes = user?.WatchedCount ?? 0, - UnwatchedEpisodes = (anime?.EpisodeCount ?? 0) - (user?.WatchedCount ?? 0), - LowestUserRating = vote?.VoteValue ?? 0, - HighestUserRating = vote?.VoteValue ?? 0, - HasVotes = vote != null, - HasPermanentVotes = vote is { VoteType: (int)AniDBVoteType.Anime }, - MissingPermanentVotes = vote is not { VoteType: (int)AniDBVoteType.Anime } && anime?.EndDate != null && anime.EndDate > DateTime.Now, - WatchedDate = watchedDates.FirstOrDefault(), - LastWatchedDate = watchedDates.LastOrDefault() + VideoSourcesDelegate = () => series.Contract?.AniDBAnime?.Stat_AllVideoQuality ?? new HashSet<string>(), + SharedVideoSourcesDelegate = () => series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), + AudioLanguagesDelegate = () => series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet<string>(), + SharedAudioLanguagesDelegate = () => + { + var audio = new HashSet<string>(); + var audioNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Languages.Select(b => b.LanguageName)); + if (audioNames.Any()) audio = audioNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + return audio; + }, + SubtitleLanguagesDelegate = () => series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet<string>(), + SharedSubtitleLanguagesDelegate = () => + { + var subtitles = new HashSet<string>(); + var subtitleNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Subtitles.Select(b => b.LanguageName)); + if (subtitleNames.Any()) subtitles = subtitleNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + return subtitles; + }, + IsFavoriteDelegate = () => false, + WatchedEpisodesDelegate = () => user?.WatchedCount ?? 0, + UnwatchedEpisodesDelegate = () => (anime?.EpisodeCount ?? 0) - (user?.WatchedCount ?? 0), + LowestUserRatingDelegate = () => vote?.VoteValue ?? 0, + HighestUserRatingDelegate = () => vote?.VoteValue ?? 0, + HasVotesDelegate = () => vote != null, + HasPermanentVotesDelegate = () => vote is { VoteType: (int)AniDBVoteType.Anime }, + MissingPermanentVotesDelegate = () => vote is not { VoteType: (int)AniDBVoteType.Anime } && anime?.EndDate != null && anime.EndDate > DateTime.Now, + WatchedDateDelegate = () => watchedDates.FirstOrDefault(), + LastWatchedDateDelegate = () => watchedDates.LastOrDefault() }; return filterable; @@ -221,58 +231,62 @@ public static Filterable ToFilterable(this SVR_AnimeGroup group) { var series = group.GetAllSeries(true); var anime = group.Anime; - var hasTrakt = series.Any(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()); - var audio = new HashSet<string>(); - var audioLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) - .Select(a => a.Languages.Select(b => b.LanguageName)); - if (audioLanguageNames.Any()) - audio = audioLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) - .ToHashSet(); - var subtitles = new HashSet<string>(); - var subtitleLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) - .Select(a => a.Subtitles.Select(b => b.LanguageName)); - if (subtitleLanguageNames.Any()) - subtitles = subtitleLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); - - // TODO optimize this a bunch. Lots of duplicate calls. Contract should be severely trimmed + var filterable = new Filterable { - Name = group.GroupName, - SortingName = group.GroupName.GetSortName(), - SeriesCount = series.Count, - AirDate = group.Contract.Stat_AirDate_Min, - LastAirDate = group.Contract?.Stat_EndDate ?? group.GetAllSeries().SelectMany(a => a.GetAnimeEpisodes()).Select(a => + NameDelegate = () => group.GroupName, + SortingNameDelegate = () => group.GroupName.GetSortName(), + SeriesCountDelegate = () => series.Count, + AirDateDelegate = () => group.Contract.Stat_AirDate_Min, + LastAirDateDelegate = () => group.Contract?.Stat_EndDate ?? group.GetAllSeries().SelectMany(a => a.GetAnimeEpisodes()).Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), - MissingEpisodes = group.Contract?.MissingEpisodeCount ?? 0, - MissingEpisodesCollecting = group.Contract?.MissingEpisodeCountGroups ?? 0, - Tags = group.Contract?.Stat_AllTags ?? new HashSet<string>(), - CustomTags = group.Contract?.Stat_AllCustomTags ?? new HashSet<string>(), - Years = group.Contract?.Stat_AllYears ?? new HashSet<int>(), - Seasons = group.Contract?.Stat_AllSeasons.Select(a => + MissingEpisodesDelegate = () => group.Contract?.MissingEpisodeCount ?? 0, + MissingEpisodesCollectingDelegate = () => group.Contract?.MissingEpisodeCountGroups ?? 0, + TagsDelegate = () => group.Contract?.Stat_AllTags ?? new HashSet<string>(), + CustomTagsDelegate = () => group.Contract?.Stat_AllCustomTags ?? new HashSet<string>(), + YearsDelegate = () => group.Contract?.Stat_AllYears ?? new HashSet<int>(), + SeasonsDelegate = () => group.Contract?.Stat_AllSeasons.Select(a => { var parts = a.Split(' '); return (int.Parse(parts[1]), Enum.Parse<AnimeSeason>(parts[0])); }).ToHashSet(), - HasTvDBLink = series.Any(a => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(a.AniDB_ID).Any()), - HasMissingTvDbLink = HasMissingTvDBLink(group), - HasTMDbLink = group.Contract?.Stat_HasMovieDBLink ?? false, - HasMissingTMDbLink = HasMissingTMDbLink(group), - HasTraktLink = hasTrakt, - HasMissingTraktLink = !hasTrakt, - IsFinished = group.Contract?.Stat_HasFinishedAiring ?? false, - AddedDate = group.DateTimeCreated, - LastAddedDate = series.SelectMany(a => a.GetVideoLocals()).Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), - EpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCountNormal ?? 0), - TotalEpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCount ?? 0), - LowestAniDBRating = anime.Select(a => decimal.Round(Convert.ToDecimal(a?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero)).DefaultIfEmpty().Min(), - HighestAniDBRating = anime.Select(a => decimal.Round(Convert.ToDecimal(a?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero)).DefaultIfEmpty().Max(), - AnimeTypes = new HashSet<string>(anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), - VideoSources = group.Contract?.Stat_AllVideoQuality ?? new HashSet<string>(), - SharedVideoSources = group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), - AudioLanguages = group.Contract?.Stat_AudioLanguages ?? new HashSet<string>(), - SharedAudioLanguages = audio, - SubtitleLanguages = group.Contract?.Stat_SubtitleLanguages ?? new HashSet<string>(), - SharedSubtitleLanguages = subtitles + HasTvDBLinkDelegate = () => series.Any(a => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(a.AniDB_ID).Any()), + HasMissingTvDbLinkDelegate = () => HasMissingTvDBLink(group), + HasTMDbLinkDelegate = () => group.Contract?.Stat_HasMovieDBLink ?? false, + HasMissingTMDbLinkDelegate = () => HasMissingTMDbLink(group), + HasTraktLinkDelegate = () => series.Any(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()), + HasMissingTraktLinkDelegate = () => series.Any(a => !RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()), + IsFinishedDelegate = () => group.Contract?.Stat_HasFinishedAiring ?? false, + AddedDateDelegate = () => group.DateTimeCreated, + LastAddedDateDelegate = () => series.SelectMany(a => a.GetVideoLocals()).Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), + EpisodeCountDelegate = () => series.Sum(a => a.GetAnime()?.EpisodeCountNormal ?? 0), + TotalEpisodeCountDelegate = () => series.Sum(a => a.GetAnime()?.EpisodeCount ?? 0), + LowestAniDBRatingDelegate = () => anime.Select(a => decimal.Round(Convert.ToDecimal(a?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero)).DefaultIfEmpty().Min(), + HighestAniDBRatingDelegate = () => anime.Select(a => decimal.Round(Convert.ToDecimal(a?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero)).DefaultIfEmpty().Max(), + AnimeTypesDelegate = () => new HashSet<string>(anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), + VideoSourcesDelegate = () => group.Contract?.Stat_AllVideoQuality ?? new HashSet<string>(), + SharedVideoSourcesDelegate = () => group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), + AudioLanguagesDelegate = () => group.Contract?.Stat_AudioLanguages ?? new HashSet<string>(), + SharedAudioLanguagesDelegate = () => + { + var audio = new HashSet<string>(); + var audioLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Languages.Select(b => b.LanguageName)); + if (audioLanguageNames.Any()) + audio = audioLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) + .ToHashSet(); + return audio; + }, + SubtitleLanguagesDelegate = () => group.Contract?.Stat_SubtitleLanguages ?? new HashSet<string>(), + SharedSubtitleLanguagesDelegate = () => + { + var subtitles = new HashSet<string>(); + var subtitleLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Subtitles.Select(b => b.LanguageName)); + if (subtitleLanguageNames.Any()) + subtitles = subtitleLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + return subtitles; + } }; return filterable; @@ -282,80 +296,81 @@ public static Filterable ToUserDependentFilterable(this SVR_AnimeGroup group, in { var series = group.GetAllSeries(true); var anime = group.Anime; - var hasTrakt = series.Any(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()); var user = group.GetUserRecord(userID); var vote = anime.Select(a => a.UserVote).Where(a => a is { VoteType: (int)VoteType.AnimePermanent or (int)VoteType.AnimeTemporary }) .Select(a => a.VoteValue).OrderBy(a => a).ToList(); - var hasPermanent = anime.Select(a => a.UserVote).Any(a => a is { VoteType: (int)VoteType.AnimePermanent }); - var missingPermanent = anime.Any(a => a.UserVote is not { VoteType: (int)VoteType.AnimePermanent } && a.EndDate != null && a.EndDate > DateTime.Now); var watchedDates = series.SelectMany(a => a.GetVideoLocals()).Select(a => a.GetUserRecord(userID)?.WatchedDate).Where(a => a != null).OrderBy(a => a) .ToList(); - - var audio = new HashSet<string>(); - var audioLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) - .Select(a => a.Languages.Select(b => b.LanguageName)); - if (audioLanguageNames.Any()) - audio = audioLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) - .ToHashSet(); - var subtitles = new HashSet<string>(); - var subtitleLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) - .Select(a => a.Subtitles.Select(b => b.LanguageName)); - if (subtitleLanguageNames.Any()) - subtitles = subtitleLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); - - // TODO optimize this a bunch. Lots of duplicate calls. Contract should be severely trimmed + var filterable = new UserDependentFilterable { - Name = group.GroupName, - SortingName = group.GroupName.GetSortName(), - SeriesCount = series.Count, - AirDate = group.Contract.Stat_AirDate_Min, - LastAirDate = group.Contract?.Stat_EndDate ?? group.GetAllSeries().SelectMany(a => a.GetAnimeEpisodes()).Select(a => + NameDelegate = () => group.GroupName, + SortingNameDelegate = () => group.GroupName.GetSortName(), + SeriesCountDelegate = () => series.Count, + AirDateDelegate = () => group.Contract.Stat_AirDate_Min, + LastAirDateDelegate = () => group.Contract?.Stat_EndDate ?? group.GetAllSeries().SelectMany(a => a.GetAnimeEpisodes()).Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), - MissingEpisodes = group.Contract?.MissingEpisodeCount ?? 0, - MissingEpisodesCollecting = group.Contract?.MissingEpisodeCountGroups ?? 0, - Tags = group.Contract?.Stat_AllTags ?? new HashSet<string>(), - CustomTags = group.Contract?.Stat_AllCustomTags ?? new HashSet<string>(), - Years = group.Contract?.Stat_AllYears ?? new HashSet<int>(), - Seasons = group.Contract?.Stat_AllSeasons.Select(a => + MissingEpisodesDelegate = () => group.Contract?.MissingEpisodeCount ?? 0, + MissingEpisodesCollectingDelegate = () => group.Contract?.MissingEpisodeCountGroups ?? 0, + TagsDelegate = () => group.Contract?.Stat_AllTags ?? new HashSet<string>(), + CustomTagsDelegate = () => group.Contract?.Stat_AllCustomTags ?? new HashSet<string>(), + YearsDelegate = () => group.Contract?.Stat_AllYears ?? new HashSet<int>(), + SeasonsDelegate = () => group.Contract?.Stat_AllSeasons.Select(a => { var parts = a.Split(' '); return (int.Parse(parts[1]), Enum.Parse<AnimeSeason>(parts[0])); }).ToHashSet(), - HasTvDBLink = series.Any(a => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(a.AniDB_ID).Any()), - HasMissingTvDbLink = HasMissingTvDBLink(group), - HasTMDbLink = group.Contract?.Stat_HasMovieDBLink ?? false, - HasMissingTMDbLink = HasMissingTMDbLink(group), - HasTraktLink = hasTrakt, - HasMissingTraktLink = !hasTrakt, - IsFinished = group.Contract?.Stat_HasFinishedAiring ?? false, - AddedDate = group.DateTimeCreated, - LastAddedDate = series.SelectMany(a => a.GetVideoLocals()).Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), - EpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCountNormal ?? 0), - TotalEpisodeCount = series.Sum(a => a.GetAnime()?.EpisodeCount ?? 0), - LowestAniDBRating = + HasTvDBLinkDelegate = () => series.Any(a => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(a.AniDB_ID).Any()), + HasMissingTvDbLinkDelegate = () => HasMissingTvDBLink(group), + HasTMDbLinkDelegate = () => group.Contract?.Stat_HasMovieDBLink ?? false, + HasMissingTMDbLinkDelegate = () => HasMissingTMDbLink(group), + HasTraktLinkDelegate = () => series.Any(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()), + HasMissingTraktLinkDelegate = () => series.Any(a => !RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()), + IsFinishedDelegate = () => group.Contract?.Stat_HasFinishedAiring ?? false, + AddedDateDelegate = () => group.DateTimeCreated, + LastAddedDateDelegate = () => series.SelectMany(a => a.GetVideoLocals()).Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), + EpisodeCountDelegate = () => series.Sum(a => a.GetAnime()?.EpisodeCountNormal ?? 0), + TotalEpisodeCountDelegate = () => series.Sum(a => a.GetAnime()?.EpisodeCount ?? 0), + LowestAniDBRatingDelegate = () => anime.Select(a => decimal.Round(Convert.ToDecimal(a?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero)).DefaultIfEmpty() .Min(), - HighestAniDBRating = + HighestAniDBRatingDelegate = () => anime.Select(a => decimal.Round(Convert.ToDecimal(a?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero)).DefaultIfEmpty() .Max(), - AnimeTypes = new HashSet<string>(anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), - VideoSources = group.Contract?.Stat_AllVideoQuality ?? new HashSet<string>(), - SharedVideoSources = group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), - AudioLanguages = group.Contract?.Stat_AudioLanguages ?? new HashSet<string>(), - SharedAudioLanguages = audio, - SubtitleLanguages = group.Contract?.Stat_SubtitleLanguages ?? new HashSet<string>(), - SharedSubtitleLanguages = subtitles, - IsFavorite = user?.IsFave == 1, - WatchedEpisodes = user?.WatchedCount ?? 0, - UnwatchedEpisodes = user?.UnwatchedEpisodeCount ?? 0, - LowestUserRating = vote.FirstOrDefault(), - HighestUserRating = vote.LastOrDefault(), - HasVotes = vote.Any(), - HasPermanentVotes = hasPermanent, - MissingPermanentVotes = missingPermanent, - WatchedDate = watchedDates.FirstOrDefault(), - LastWatchedDate = watchedDates.LastOrDefault() + AnimeTypesDelegate = () => new HashSet<string>(anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), + VideoSourcesDelegate = () => group.Contract?.Stat_AllVideoQuality ?? new HashSet<string>(), + SharedVideoSourcesDelegate = () => group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), + AudioLanguagesDelegate = () => group.Contract?.Stat_AudioLanguages ?? new HashSet<string>(), + SharedAudioLanguagesDelegate = () => + { + var audio = new HashSet<string>(); + var audioLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Languages.Select(b => b.LanguageName)); + if (audioLanguageNames.Any()) + audio = audioLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) + .ToHashSet(); + return audio; + }, + SubtitleLanguagesDelegate = () => group.Contract?.Stat_SubtitleLanguages ?? new HashSet<string>(), + SharedSubtitleLanguagesDelegate = () => + { + var subtitles = new HashSet<string>(); + var subtitleLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Subtitles.Select(b => b.LanguageName)); + if (subtitleLanguageNames.Any()) + subtitles = subtitleLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + return subtitles; + }, + IsFavoriteDelegate = () => user?.IsFave == 1, + WatchedEpisodesDelegate = () => user?.WatchedCount ?? 0, + UnwatchedEpisodesDelegate = () => user?.UnwatchedEpisodeCount ?? 0, + LowestUserRatingDelegate = () => vote.FirstOrDefault(), + HighestUserRatingDelegate = () => vote.LastOrDefault(), + HasVotesDelegate = () => vote.Any(), + HasPermanentVotesDelegate = () => anime.Select(a => a.UserVote).Any(a => a is { VoteType: (int)VoteType.AnimePermanent }), + MissingPermanentVotesDelegate = () => anime.Any(a => a.UserVote is not { VoteType: (int)VoteType.AnimePermanent } && a.EndDate != null && a.EndDate > DateTime.Now), + WatchedDateDelegate = () => watchedDates.FirstOrDefault(), + LastWatchedDateDelegate = () => watchedDates.LastOrDefault() }; return filterable; diff --git a/Shoko.Server/Filters/Filterable.cs b/Shoko.Server/Filters/Filterable.cs index b0d2eef9e..cd6ca01db 100644 --- a/Shoko.Server/Filters/Filterable.cs +++ b/Shoko.Server/Filters/Filterable.cs @@ -1,133 +1,568 @@ using System; using System.Collections.Generic; +using System.Threading; using Shoko.Models.Enums; namespace Shoko.Server.Filters; public class Filterable { + + private readonly Lazy<DateTime> _addedDate; + private readonly Func<DateTime> _addedDateDelegate; + + private readonly Lazy<DateTime?> _airDate; + private readonly Func<DateTime?> _airDateDelegate; + + private readonly Lazy<IReadOnlySet<string>> _animeTypes; + private readonly Func<IReadOnlySet<string>> _animeTypesDelegate; + + private readonly Lazy<IReadOnlySet<string>> _audioLanguages; + private readonly Func<IReadOnlySet<string>> _audioLanguagesDelegate; + + private readonly Lazy<IReadOnlySet<string>> _customTags; + private readonly Func<IReadOnlySet<string>> _customTagsDelegate; + + private readonly Lazy<int> _episodeCount; + private readonly Func<int> _episodeCountDelegate; + + private readonly Lazy<bool> _hasMissingTMDbLink; + private readonly Func<bool> _hasMissingTmDbLinkDelegate; + + private readonly Lazy<bool> _hasMissingTraktLink; + private readonly Func<bool> _hasMissingTraktLinkDelegate; + + private readonly Lazy<bool> _hasMissingTvDBLink; + private readonly Func<bool> _hasMissingTvDbLinkDelegate; + + private readonly Lazy<bool> _hasTMDbLink; + private readonly Func<bool> _hasTmDbLinkDelegate; + + private readonly Lazy<bool> _hasTraktLink; + private readonly Func<bool> _hasTraktLinkDelegate; + + private readonly Lazy<bool> _hasTvDBLink; + private readonly Func<bool> _hasTvDBLinkDelegate; + + private readonly Lazy<decimal> _highestAniDBRating; + private readonly Func<decimal> _highestAniDBRatingDelegate; + + private readonly Lazy<bool> _isFinished; + private readonly Func<bool> _isFinishedDelegate; + + private readonly Lazy<DateTime> _lastAddedDate; + private readonly Func<DateTime> _lastAddedDateDelegate; + + private readonly Lazy<DateTime?> _lastAirDate; + private readonly Func<DateTime?> _lastAirDateDelegate; + + private readonly Lazy<decimal> _lowestAniDBRating; + private readonly Func<decimal> _lowestAniDBRatingDelegate; + + private readonly Lazy<int> _missingEpisodes; + + private readonly Lazy<int> _missingEpisodesCollecting; + private readonly Func<int> _missingEpisodesCollectingDelegate; + private readonly Func<int> _missingEpisodesDelegate; + + private readonly Lazy<string> _name; + private readonly Func<string> _nameDelegate; + + private readonly Lazy<IReadOnlySet<(int year, AnimeSeason season)>> _seasons; + private readonly Func<IReadOnlySet<(int year, AnimeSeason season)>> _seasonsDelegate; + + private readonly Lazy<int> _seriesCount; + private readonly Func<int> _seriesCountDelegate; + + private readonly Lazy<IReadOnlySet<string>> _sharedAudioLanguages; + private readonly Func<IReadOnlySet<string>> _sharedAudioLanguagesDelegate; + + private readonly Lazy<IReadOnlySet<string>> _sharedSubtitleLanguages; + private readonly Func<IReadOnlySet<string>> _sharedSubtitleLanguagesDelegate; + + private readonly Lazy<IReadOnlySet<string>> _sharedVideoSources; + private readonly Func<IReadOnlySet<string>> _sharedVideoSourcesDelegate; + + private readonly Lazy<string> _sortingName; + private readonly Func<string> _sortingNameDelegate; + + private readonly Lazy<IReadOnlySet<string>> _subtitleLanguages; + private readonly Func<IReadOnlySet<string>> _subtitleLanguagesDelegate; + + private readonly Lazy<IReadOnlySet<string>> _tags; + private readonly Func<IReadOnlySet<string>> _tagsDelegate; + + private readonly Lazy<int> _totalEpisodeCount; + private readonly Func<int> _totalEpisodeCountDelegate; + + private readonly Lazy<IReadOnlySet<string>> _videoSources; + private readonly Func<IReadOnlySet<string>> _videoSourcesDelegate; + + private readonly Lazy<IReadOnlySet<int>> _years; + private readonly Func<IReadOnlySet<int>> _yearsDelegate; + /// <summary> /// Name /// </summary> - public string Name { get; init; } + public string Name => _name.Value; + + public Func<string> NameDelegate + { + get => _nameDelegate; + init + { + _nameDelegate = value; + _name = new Lazy<string>(_nameDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Sorting Name /// </summary> - public string SortingName { get; init; } + public string SortingName => _sortingName.Value; + + public Func<string> SortingNameDelegate + { + get => _sortingNameDelegate; + init + { + _sortingNameDelegate = value; + _sortingName = new Lazy<string>(_sortingNameDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// The number of series in a group /// </summary> - public int SeriesCount { get; init; } + public int SeriesCount => _seriesCount.Value; + + public Func<int> SeriesCountDelegate + { + get => _seriesCountDelegate; + init + { + _seriesCountDelegate = value; + _seriesCount = new Lazy<int>(_seriesCountDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Number of Missing Episodes /// </summary> - public int MissingEpisodes { get; init; } + public int MissingEpisodes => _missingEpisodes.Value; + + public Func<int> MissingEpisodesDelegate + { + get => _missingEpisodesDelegate; + init + { + _missingEpisodesDelegate = value; + _missingEpisodes = new Lazy<int>(_missingEpisodesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Number of Missing Episodes from Groups that you have /// </summary> - public int MissingEpisodesCollecting { get; init; } + public int MissingEpisodesCollecting => _missingEpisodesCollecting.Value; + + public Func<int> MissingEpisodesCollectingDelegate + { + get => _missingEpisodesCollectingDelegate; + init + { + _missingEpisodesCollectingDelegate = value; + _missingEpisodesCollecting = new Lazy<int>(_missingEpisodesCollectingDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// All of the tags /// </summary> - public IReadOnlySet<string> Tags { get; init; } + public IReadOnlySet<string> Tags => _tags.Value; + + public Func<IReadOnlySet<string>> TagsDelegate + { + get => _tagsDelegate; + init + { + _tagsDelegate = value; + _tags = new Lazy<IReadOnlySet<string>>(_tagsDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// All of the custom tags /// </summary> - public IReadOnlySet<string> CustomTags { get; init; } + public IReadOnlySet<string> CustomTags => _customTags.Value; + + public Func<IReadOnlySet<string>> CustomTagsDelegate + { + get => _customTagsDelegate; + init + { + _customTagsDelegate = value; + _customTags = new Lazy<IReadOnlySet<string>>(_customTagsDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// The years this aired in /// </summary> - public IReadOnlySet<int> Years { get; init; } + public IReadOnlySet<int> Years => _years.Value; + + public Func<IReadOnlySet<int>> YearsDelegate + { + get => _yearsDelegate; + init + { + _yearsDelegate = value; + _years = new Lazy<IReadOnlySet<int>>(_yearsDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// The seasons this aired in /// </summary> - public IReadOnlySet<(int year, AnimeSeason season)> Seasons { get; init; } + public IReadOnlySet<(int year, AnimeSeason season)> Seasons => _seasons.Value; + + public Func<IReadOnlySet<(int year, AnimeSeason season)>> SeasonsDelegate + { + get => _seasonsDelegate; + init + { + _seasonsDelegate = value; + _seasons = new Lazy<IReadOnlySet<(int year, AnimeSeason season)>>(_seasonsDelegate); + } + } + /// <summary> /// Has at least one TvDB Link /// </summary> - public bool HasTvDBLink { get; init; } + public bool HasTvDBLink => _hasTvDBLink.Value; + + public Func<bool> HasTvDBLinkDelegate + { + get => _hasTvDBLinkDelegate; + init + { + _hasTvDBLinkDelegate = value; + _hasTvDBLink = new Lazy<bool>(_hasTvDBLinkDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Missing at least one TvDB Link /// </summary> - public bool HasMissingTvDbLink { get; init; } + public bool HasMissingTvDbLink => _hasMissingTvDBLink.Value; + + public Func<bool> HasMissingTvDbLinkDelegate + { + get => _hasMissingTvDbLinkDelegate; + init + { + _hasMissingTvDbLinkDelegate = value; + _hasMissingTvDBLink = new Lazy<bool>(_hasMissingTvDbLinkDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Has at least one TMDb Link /// </summary> - public bool HasTMDbLink { get; init; } + public bool HasTMDbLink => _hasTMDbLink.Value; + + public Func<bool> HasTMDbLinkDelegate + { + get => _hasTmDbLinkDelegate; + init + { + _hasTmDbLinkDelegate = value; + _hasTMDbLink = new Lazy<bool>(_hasTmDbLinkDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Missing at least one TMDb Link /// </summary> - public bool HasMissingTMDbLink { get; init; } + public bool HasMissingTMDbLink => _hasMissingTMDbLink.Value; + + public Func<bool> HasMissingTMDbLinkDelegate + { + get => _hasMissingTmDbLinkDelegate; + init + { + _hasMissingTmDbLinkDelegate = value; + _hasMissingTMDbLink = new Lazy<bool>(_hasMissingTmDbLinkDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Has at least one Trakt Link /// </summary> - public bool HasTraktLink { get; init; } + public bool HasTraktLink => _hasTraktLink.Value; + + public Func<bool> HasTraktLinkDelegate + { + get => _hasTraktLinkDelegate; + init + { + _hasTraktLinkDelegate = value; + _hasTraktLink = new Lazy<bool>(_hasTraktLinkDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Missing at least one Trakt Link /// </summary> - public bool HasMissingTraktLink { get; init; } + public bool HasMissingTraktLink => _hasMissingTraktLink.Value; + + public Func<bool> HasMissingTraktLinkDelegate + { + get => _hasMissingTraktLinkDelegate; + init + { + _hasMissingTraktLinkDelegate = value; + _hasMissingTraktLink = new Lazy<bool>(_hasMissingTraktLinkDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Has Finished airing /// </summary> - public bool IsFinished { get; init; } + public bool IsFinished => _isFinished.Value; + + public Func<bool> IsFinishedDelegate + { + get => _isFinishedDelegate; + init + { + _isFinishedDelegate = value; + _isFinished = new Lazy<bool>(_isFinishedDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// First Air Date /// </summary> - public DateTime? AirDate { get; init; } + public DateTime? AirDate => _airDate.Value; + + public Func<DateTime?> AirDateDelegate + { + get => _airDateDelegate; + init + { + _airDateDelegate = value; + _airDate = new Lazy<DateTime?>(_airDateDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Latest Air Date /// </summary> - public DateTime? LastAirDate { get; init; } + public DateTime? LastAirDate => _lastAirDate.Value; + + public Func<DateTime?> LastAirDateDelegate + { + get => _lastAirDateDelegate; + init + { + _lastAirDateDelegate = value; + _lastAirDate = new Lazy<DateTime?>(_lastAirDateDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// When it was first added to the collection /// </summary> - public DateTime AddedDate { get; init; } + public DateTime AddedDate => _addedDate.Value; + + public Func<DateTime> AddedDateDelegate + { + get => _addedDateDelegate; + init + { + _addedDateDelegate = value; + _addedDate = new Lazy<DateTime>(_addedDateDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// When it was most recently added to the collection /// </summary> - public DateTime LastAddedDate { get; init; } + public DateTime LastAddedDate => _lastAddedDate.Value; + + public Func<DateTime> LastAddedDateDelegate + { + get => _lastAddedDateDelegate; + init + { + _lastAddedDateDelegate = value; + _lastAddedDate = new Lazy<DateTime>(_lastAddedDateDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Highest Episode Count /// </summary> - public int EpisodeCount { get; init; } + public int EpisodeCount => _episodeCount.Value; + + public Func<int> EpisodeCountDelegate + { + get => _episodeCountDelegate; + init + { + _episodeCountDelegate = value; + _episodeCount = new Lazy<int>(_episodeCountDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Total Episode Count /// </summary> - public int TotalEpisodeCount { get; init; } + public int TotalEpisodeCount => _totalEpisodeCount.Value; + + public Func<int> TotalEpisodeCountDelegate + { + get => _totalEpisodeCountDelegate; + init + { + _totalEpisodeCountDelegate = value; + _totalEpisodeCount = new Lazy<int>(_totalEpisodeCountDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Lowest AniDB Rating (0-10) /// </summary> - public decimal LowestAniDBRating { get; init; } + public decimal LowestAniDBRating => _lowestAniDBRating.Value; + + public Func<decimal> LowestAniDBRatingDelegate + { + get => _lowestAniDBRatingDelegate; + init + { + _lowestAniDBRatingDelegate = value; + _lowestAniDBRating = new Lazy<decimal>(_lowestAniDBRatingDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Highest AniDB Rating (0-10) /// </summary> - public decimal HighestAniDBRating { get; init; } + public decimal HighestAniDBRating => _highestAniDBRating.Value; + + public Func<decimal> HighestAniDBRatingDelegate + { + get => _highestAniDBRatingDelegate; + init + { + _highestAniDBRatingDelegate = value; + _highestAniDBRating = new Lazy<decimal>(_highestAniDBRatingDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. /// </summary> - public IReadOnlySet<string> VideoSources { get; init; } + public IReadOnlySet<string> VideoSources => _videoSources.Value; + + public Func<IReadOnlySet<string>> VideoSourcesDelegate + { + get => _videoSourcesDelegate; + init + { + _videoSourcesDelegate = value; + _videoSources = new Lazy<IReadOnlySet<string>>(_videoSourcesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. (only sources that are in every file) /// </summary> - public IReadOnlySet<string> SharedVideoSources { get; init; } + public IReadOnlySet<string> SharedVideoSources => _sharedVideoSources.Value; + + public Func<IReadOnlySet<string>> SharedVideoSourcesDelegate + { + get => _sharedVideoSourcesDelegate; + init + { + _sharedVideoSourcesDelegate = value; + _sharedVideoSources = new Lazy<IReadOnlySet<string>>(_sharedVideoSourcesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// The anime types (movie, series, ova, etc) /// </summary> - public IReadOnlySet<string> AnimeTypes { get; init; } + public IReadOnlySet<string> AnimeTypes => _animeTypes.Value; + + public Func<IReadOnlySet<string>> AnimeTypesDelegate + { + get => _animeTypesDelegate; + init + { + _animeTypesDelegate = value; + _animeTypes = new Lazy<IReadOnlySet<string>>(_animeTypesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Audio Languages /// </summary> - public IReadOnlySet<string> AudioLanguages { get; init; } + public IReadOnlySet<string> AudioLanguages => _audioLanguages.Value; + + public Func<IReadOnlySet<string>> AudioLanguagesDelegate + { + get => _audioLanguagesDelegate; + init + { + _audioLanguagesDelegate = value; + _audioLanguages = new Lazy<IReadOnlySet<string>>(_audioLanguagesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Audio Languages (only languages that are in every file) /// </summary> - public IReadOnlySet<string> SharedAudioLanguages { get; init; } + public IReadOnlySet<string> SharedAudioLanguages => _sharedAudioLanguages.Value; + + public Func<IReadOnlySet<string>> SharedAudioLanguagesDelegate + { + get => _sharedAudioLanguagesDelegate; + init + { + _sharedAudioLanguagesDelegate = value; + _sharedAudioLanguages = new Lazy<IReadOnlySet<string>>(_sharedAudioLanguagesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Subtitle Languages /// </summary> - public IReadOnlySet<string> SubtitleLanguages { get; init; } + public IReadOnlySet<string> SubtitleLanguages => _subtitleLanguages.Value; + + public Func<IReadOnlySet<string>> SubtitleLanguagesDelegate + { + get => _subtitleLanguagesDelegate; + init + { + _subtitleLanguagesDelegate = value; + _subtitleLanguages = new Lazy<IReadOnlySet<string>>(_subtitleLanguagesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Subtitle Languages (only languages that are in every file) /// </summary> - public IReadOnlySet<string> SharedSubtitleLanguages { get; init; } + public IReadOnlySet<string> SharedSubtitleLanguages => _sharedSubtitleLanguages.Value; + + public Func<IReadOnlySet<string>> SharedSubtitleLanguagesDelegate + { + get => _sharedSubtitleLanguagesDelegate; + init + { + _sharedSubtitleLanguagesDelegate = value; + _sharedSubtitleLanguages = new Lazy<IReadOnlySet<string>>(_sharedSubtitleLanguagesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } } diff --git a/Shoko.Server/Filters/UserDependentFilterable.cs b/Shoko.Server/Filters/UserDependentFilterable.cs index ec8e61fd8..ac12809e3 100644 --- a/Shoko.Server/Filters/UserDependentFilterable.cs +++ b/Shoko.Server/Filters/UserDependentFilterable.cs @@ -1,47 +1,187 @@ using System; +using System.Threading; namespace Shoko.Server.Filters; public class UserDependentFilterable : Filterable { + private readonly Lazy<bool> _hasPermanentVotes; + private readonly Func<bool> _hasPermanentVotesDelegate; + + private readonly Lazy<bool> _hasVotes; + private readonly Func<bool> _hasVotesDelegate; + + private readonly Lazy<decimal> _highestUserRating; + private readonly Func<decimal> _highestUserRatingDelegate; + + private readonly Lazy<bool> _isFavorite; + private readonly Func<bool> _isFavoriteDelegate; + + private readonly Lazy<DateTime?> _lastWatchedDate; + private readonly Func<DateTime?> _lastWatchedDateDelegate; + + private readonly Lazy<decimal> _lowestUserRating; + private readonly Func<decimal> _lowestUserRatingDelegate; + + private readonly Lazy<bool> _missingPermanentVotes; + private readonly Func<bool> _missingPermanentVotesDelegate; + + private readonly Lazy<int> _unwatchedEpisodes; + private readonly Func<int> _unwatchedEpisodesDelegate; + + private readonly Lazy<DateTime?> _watchedDate; + private readonly Func<DateTime?> _watchedDateDelegate; + + private readonly Lazy<int> _watchedEpisodes; + private readonly Func<int> _watchedEpisodesDelegate; + /// <summary> /// Probably will be removed in the future. Custom Tags would handle this better /// </summary> - public bool IsFavorite { get; init; } + public bool IsFavorite => _isFavorite.Value; + + public Func<bool> IsFavoriteDelegate + { + get => _isFavoriteDelegate; + init + { + _isFavoriteDelegate = value; + _isFavorite = new Lazy<bool>(_isFavoriteDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// The number of episodes watched /// </summary> - public int WatchedEpisodes { get; init; } + public int WatchedEpisodes => _watchedEpisodes.Value; + + public Func<int> WatchedEpisodesDelegate + { + get => _watchedEpisodesDelegate; + init + { + _watchedEpisodesDelegate = value; + _watchedEpisodes = new Lazy<int>(_watchedEpisodesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// The number of episodes that have not been watched /// </summary> - public int UnwatchedEpisodes { get; init; } + public int UnwatchedEpisodes => _unwatchedEpisodes.Value; + + public Func<int> UnwatchedEpisodesDelegate + { + get => _unwatchedEpisodesDelegate; + init + { + _unwatchedEpisodesDelegate = value; + _unwatchedEpisodes = new Lazy<int>(_unwatchedEpisodesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Has any user votes /// </summary> - public bool HasVotes { get; init; } + public bool HasVotes => _hasVotes.Value; + + public Func<bool> HasVotesDelegate + { + get => _hasVotesDelegate; + init + { + _hasVotesDelegate = value; + _hasVotes = new Lazy<bool>(_hasVotesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Has permanent (after finishing) user votes /// </summary> - public bool HasPermanentVotes { get; init; } + public bool HasPermanentVotes => _hasPermanentVotes.Value; + + public Func<bool> HasPermanentVotesDelegate + { + get => _hasPermanentVotesDelegate; + init + { + _hasPermanentVotesDelegate = value; + _hasPermanentVotes = new Lazy<bool>(_hasPermanentVotesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Has permanent (after finishing) user votes /// </summary> - public bool MissingPermanentVotes { get; init; } + public bool MissingPermanentVotes => _missingPermanentVotes.Value; + + public Func<bool> MissingPermanentVotesDelegate + { + get => _missingPermanentVotesDelegate; + init + { + _missingPermanentVotesDelegate = value; + _missingPermanentVotes = new Lazy<bool>(_missingPermanentVotesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// First Watched Date /// </summary> - public DateTime? WatchedDate { get; init; } + public DateTime? WatchedDate => _watchedDate.Value; + + public Func<DateTime?> WatchedDateDelegate + { + get => _watchedDateDelegate; + init + { + _watchedDateDelegate = value; + _watchedDate = new Lazy<DateTime?>(_watchedDateDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Latest Watched Date /// </summary> - public DateTime? LastWatchedDate { get; init; } + public DateTime? LastWatchedDate => _lastWatchedDate.Value; + + public Func<DateTime?> LastWatchedDateDelegate + { + get => _lastWatchedDateDelegate; + init + { + _lastWatchedDateDelegate = value; + _lastWatchedDate = new Lazy<DateTime?>(_lastWatchedDateDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Lowest User Rating (0-10) /// </summary> - public decimal LowestUserRating { get; init; } + public decimal LowestUserRating => _lowestUserRating.Value; + + public Func<decimal> LowestUserRatingDelegate + { + get => _lowestUserRatingDelegate; + init + { + _lowestUserRatingDelegate = value; + _lowestUserRating = new Lazy<decimal>(_lowestUserRatingDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + /// <summary> /// Highest User Rating (0-10) /// </summary> - public decimal HighestUserRating { get; init; } + public decimal HighestUserRating => _highestUserRating.Value; + + public Func<decimal> HighestUserRatingDelegate + { + get => _highestUserRatingDelegate; + init + { + _highestUserRatingDelegate = value; + _highestUserRating = new Lazy<decimal>(_highestUserRatingDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } } diff --git a/Shoko.Server/Models/SVR_AniDB_Anime.cs b/Shoko.Server/Models/SVR_AniDB_Anime.cs index 75ea3df4b..d134619b1 100644 --- a/Shoko.Server/Models/SVR_AniDB_Anime.cs +++ b/Shoko.Server/Models/SVR_AniDB_Anime.cs @@ -598,20 +598,20 @@ private string GetFormattedTitle(List<SVR_AniDB_Anime_Title> titles = null) titles ??= GetTitles(); // Check each preferred language in order. - foreach (var thisLanguage in Languages.PreferredNamingLanguages.Select(a => a.Language)) + foreach (var thisLanguage in Languages.PreferredNamingLanguageNames) { // First check the main title. - var title = titles.FirstOrDefault(title => title.TitleType == TitleType.Main && title.Language == thisLanguage); + var title = titles.FirstOrDefault(t => t.TitleType == TitleType.Main && t.Language == thisLanguage); if (title != null) return title.Title; // Then check for an official title. - title = titles.FirstOrDefault(title => title.TitleType == TitleType.Official && title.Language == thisLanguage); + title = titles.FirstOrDefault(t => t.TitleType == TitleType.Official && t.Language == thisLanguage); if (title != null) return title.Title; // Then check for _any_ title at all, if there is no main or official title in the langugage. if (Utils.SettingsProvider.GetSettings().LanguageUseSynonyms) { - title = titles.FirstOrDefault(title => title.Language == thisLanguage); + title = titles.FirstOrDefault(t => t.Language == thisLanguage); if (title != null) return title.Title; } } @@ -637,7 +637,8 @@ public AniDB_Vote UserVote } } - public string PreferredTitle => GetFormattedTitle(); + private string _cachedTitle; + public string PreferredTitle => _cachedTitle == null ? (_cachedTitle = GetFormattedTitle()) : _cachedTitle; [XmlIgnore] public List<AniDB_Episode> AniDBEpisodes => RepoFactory.AniDB_Episode.GetByAnimeID(AnimeID); diff --git a/Shoko.Server/Models/SVR_AnimeSeries.cs b/Shoko.Server/Models/SVR_AnimeSeries.cs index f92fa252d..206a83c71 100644 --- a/Shoko.Server/Models/SVR_AnimeSeries.cs +++ b/Shoko.Server/Models/SVR_AnimeSeries.cs @@ -191,15 +191,15 @@ public string GetSeriesName() if (!string.IsNullOrEmpty(SeriesNameOverride)) return SeriesNameOverride; + if (Utils.SettingsProvider.GetSettings().SeriesNameSource == DataSourceType.AniDB) + return GetAnime().PreferredTitle; + // Try to find the TvDB title if we prefer TvDB titles. - if (Utils.SettingsProvider.GetSettings().SeriesNameSource == DataSourceType.TvDB) - { - var tvdbShows = GetTvDBSeries(); - var tvdbShowTitle = tvdbShows - .FirstOrDefault(show => show.SeriesName.Contains("**DUPLICATE", StringComparison.InvariantCultureIgnoreCase))?.SeriesName; - if (!string.IsNullOrEmpty(tvdbShowTitle)) - return tvdbShowTitle; - } + var tvdbShows = GetTvDBSeries(); + var tvdbShowTitle = tvdbShows + .FirstOrDefault(show => !show.SeriesName.Contains("**DUPLICATE", StringComparison.InvariantCultureIgnoreCase))?.SeriesName; + if (!string.IsNullOrEmpty(tvdbShowTitle)) + return tvdbShowTitle; // Otherwise just return the anidb title. return GetAnime().PreferredTitle; diff --git a/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs b/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs index 3af889a6d..ad1f38310 100644 --- a/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs +++ b/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs @@ -72,7 +72,7 @@ public void CleanUpEmptyDirectoryFilters() var evaluator = Utils.ServiceContainer.GetRequiredService<FilterEvaluator>(); var filters = GetAll().Where(a => (a.FilterType & GroupFilterType.Directory) != 0) .Where(gf => gf.Expression is not null && gf.Expression.UserDependent).ToList(); - var toRemove = evaluator.BatchEvaluateFilters(filters, null).Where(a => !a.Value.Any()).Select(a => a.Key).ToList(); + var toRemove = evaluator.BatchEvaluateFilters(filters, null, true).Where(a => !a.Value.Any()).Select(a => a.Key).ToList(); if (toRemove.Count <= 0) return; Delete(toRemove); diff --git a/Shoko.Server/Settings/ServerSettings.cs b/Shoko.Server/Settings/ServerSettings.cs index 8160e5d44..845cc034c 100644 --- a/Shoko.Server/Settings/ServerSettings.cs +++ b/Shoko.Server/Settings/ServerSettings.cs @@ -80,6 +80,7 @@ public List<string> LanguagePreference { _languagePreference = value; Languages.PreferredNamingLanguages = null; + Languages.PreferredNamingLanguageNames = null; } } diff --git a/Shoko.Server/Utilities/Languages.cs b/Shoko.Server/Utilities/Languages.cs index f9c88f69c..2a47ad0cf 100644 --- a/Shoko.Server/Utilities/Languages.cs +++ b/Shoko.Server/Utilities/Languages.cs @@ -29,6 +29,18 @@ public static List<NamingLanguage> PreferredNamingLanguages set => _preferredNamingLanguages = value; } + private static List<TitleLanguage> _preferredNamingLanguageNames; + public static List<TitleLanguage> PreferredNamingLanguageNames + { + get + { + if (_preferredNamingLanguages != null) return _preferredNamingLanguageNames; + _preferredNamingLanguageNames = PreferredNamingLanguages.Select(a => a.Language).ToList(); + return _preferredNamingLanguageNames; + } + set => _preferredNamingLanguageNames = value; + } + private static List<NamingLanguage> _preferredEpisodeNamingLanguages; public static List<NamingLanguage> PreferredEpisodeNamingLanguages From 6d843e90d358162753b021d840660a25c674e0c7 Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Sun, 17 Sep 2023 20:50:25 -0400 Subject: [PATCH 21/34] Cleanup (to avoid needing to update unused filter changes) --- .../ShokoServiceImplementationKodi.cs | 163 -- .../ShokoServiceImplementationMetro.cs | 35 +- .../ShokoServiceImplementationPlex.cs | 109 -- Shoko.Server/API/v1/Modules/ImageModule.cs | 14 - Shoko.Server/API/v1/Modules/KodiModule.cs | 15 - Shoko.Server/API/v1/Modules/MainModule.cs | 14 - Shoko.Server/API/v1/Modules/MetroModule.cs | 19 - Shoko.Server/API/v1/Modules/PlexModule.cs | 15 - Shoko.Server/API/v1/Modules/StreamModule.cs | 15 - Shoko.Server/API/v2/Modules/Core.cs | 16 +- Shoko.Server/Databases/DatabaseFixes.cs | 177 +- Shoko.Server/Import/Importer.cs | 46 - .../PlexAndKodi/CommonImplementation.cs | 1475 ----------------- Shoko.Server/PlexAndKodi/Helper.cs | 283 ---- Shoko.Server/PlexAndKodi/Kodi/KodiProvider.cs | 66 - Shoko.Server/Server/ShokoServer.cs | 1 - Shoko.Server/Server/Startup.cs | 1 - 17 files changed, 31 insertions(+), 2433 deletions(-) delete mode 100644 Shoko.Server/API/v1/Implementations/ShokoServiceImplementationKodi.cs delete mode 100644 Shoko.Server/API/v1/Implementations/ShokoServiceImplementationPlex.cs delete mode 100644 Shoko.Server/API/v1/Modules/ImageModule.cs delete mode 100644 Shoko.Server/API/v1/Modules/KodiModule.cs delete mode 100644 Shoko.Server/API/v1/Modules/MainModule.cs delete mode 100644 Shoko.Server/API/v1/Modules/MetroModule.cs delete mode 100644 Shoko.Server/API/v1/Modules/PlexModule.cs delete mode 100644 Shoko.Server/API/v1/Modules/StreamModule.cs delete mode 100644 Shoko.Server/PlexAndKodi/CommonImplementation.cs delete mode 100644 Shoko.Server/PlexAndKodi/Kodi/KodiProvider.cs diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationKodi.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationKodi.cs deleted file mode 100644 index b37f4b5cb..000000000 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationKodi.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System; -using System.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Quartz; -using Shoko.Models.Interfaces; -using Shoko.Models.PlexAndKodi; -using Shoko.Server.Commands; -using Shoko.Server.PlexAndKodi; -using Shoko.Server.PlexAndKodi.Kodi; -using Shoko.Server.Settings; -using Stream = System.IO.Stream; - -namespace Shoko.Server.API.v1.Implementations; - -[ApiController] -[Route("/api/Kodi")] -[ApiVersion("1.0", Deprecated = true)] -public class ShokoServiceImplementationKodi : IShokoServerKodi, IHttpContextAccessor -{ - private readonly ShokoServiceImplementation _service; - public HttpContext HttpContext { get; set; } - - private readonly CommonImplementation _impl; - - private readonly ILogger<ShokoServiceImplementationKodi> _logger; - private readonly ISettingsProvider _settingsProvider; - - public ShokoServiceImplementationKodi(ICommandRequestFactory commandFactory, - ILogger<ShokoServiceImplementationKodi> logger, ISchedulerFactory schedulerFactory, ISettingsProvider settingsProvider, CommonImplementation impl) - { - _settingsProvider = settingsProvider; - _service = new ShokoServiceImplementation(null, null, null, commandFactory, schedulerFactory, settingsProvider); - _logger = logger; - _impl = impl; - } - - - [HttpGet("Image/Support/{name}")] - public Stream GetSupportImage(string name) - { - return _impl.GetSupportImage(name); - } - - [HttpGet("Filters/{userId}")] - public MediaContainer GetFilters(string userId) - { - return _impl.GetFilters(new KodiProvider { HttpContext = HttpContext }, userId); - } - - [HttpGet("Metadata/{userId}/{type}/{id}/{filterid?}")] - public MediaContainer GetMetadata(string userId, int type, string id, int? filterid) - { - return _impl.GetMetadata(new KodiProvider { HttpContext = HttpContext }, userId, type, id, null, false, - filterid); - } - - [HttpGet("User")] - public PlexContract_Users GetUsers() - { - return _impl.GetUsers(new KodiProvider { HttpContext = HttpContext }); - } - - [HttpGet("Version")] - public Response Version() - { - return _impl.GetVersion(); - } - - [HttpGet("Search/{userId}/{limit}/{query}")] - public MediaContainer Search(string userId, int limit, string query) - { - return _impl.Search(new KodiProvider { HttpContext = HttpContext }, userId, limit, query, false); - } - - [HttpGet("SearchTag/{userId}/{limit}/{query}")] - public MediaContainer SearchTag(string userId, int limit, string query) - { - return _impl.Search(new KodiProvider { HttpContext = HttpContext }, userId, limit, query, true); - } - - [HttpGet("Group/Watch/{userId}/{groupid}/{status}")] - public Response ToggleWatchedStatusOnGroup(string userId, int groupid, bool status) - { - return _impl.ToggleWatchedStatusOnGroup(new KodiProvider { HttpContext = HttpContext }, userId, - groupid, status); - } - - [HttpGet("Serie/Watch/{userId}/{serieid}/{status}")] - public Response ToggleWatchedStatusOnSeries(string userId, int serieid, bool status) - { - return _impl.ToggleWatchedStatusOnSeries(new KodiProvider { HttpContext = HttpContext }, userId, - serieid, status); - } - - [HttpGet("Serie/Watch/{userId}/{epid}/{status}")] - public Response ToggleWatchedStatusOnEpisode(string userId, int epid, bool status) - { - return _impl.ToggleWatchedStatusOnEpisode(new KodiProvider { HttpContext = HttpContext }, userId, epid, - status); - } - - [HttpGet("Vote/{userId}/{id}/{votevalue}/{votetype}")] - public Response Vote(string userId, int id, float votevalue, int votetype) - { - return _impl.VoteAnime(new KodiProvider { HttpContext = HttpContext }, userId, id, votevalue, - votetype); - } - - [HttpGet("Trakt/Scrobble/{animeId}/{type}/{progress}/{status}")] - public Response TraktScrobble(string animeId, int type, float progress, int status) - { - return _impl.TraktScrobble(new KodiProvider { HttpContext = HttpContext }, animeId, type, progress, - status); - } - - [HttpGet("Video/Rescan/{vlid}")] - public Response Rescan(int vlid) - { - var r = new Response(); - try - { - var output = _service.RescanFile(vlid); - if (!string.IsNullOrEmpty(output)) - { - r.Code = HttpStatusCode.BadRequest.ToString(); - r.Message = output; - return r; - } - - r.Code = HttpStatusCode.OK.ToString(); - } - catch (Exception ex) - { - r.Code = "500"; - r.Message = "Internal Error : " + ex; - _logger.LogError(ex, "{Ex}", ex.ToString()); - } - - return r; - } - - - [HttpGet("Video/Rehash/{vlid}")] - public Response Rehash(int vlid) - { - var r = new Response(); - try - { - _service.RehashFile(vlid); - r.Code = HttpStatusCode.OK.ToString(); - } - catch (Exception ex) - { - r.Code = "500"; - r.Message = "Internal Error : " + ex; - _logger.LogError(ex, "{Ex}", ex.ToString()); - } - - return r; - } -} diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs index 2356115d8..d2129e0af 100644 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs @@ -17,6 +17,7 @@ using Shoko.Server.Commands; using Shoko.Server.Commands.AniDB; using Shoko.Server.Extensions; +using Shoko.Server.Filters; using Shoko.Server.Models; using Shoko.Server.Providers.AniDB.Interfaces; using Shoko.Server.Providers.TraktTV; @@ -518,43 +519,30 @@ public List<Metro_Anime_Summary> GetAnimeContinueWatching(int maxRecords, int jm } // find the locked Continue Watching Filter - SVR_GroupFilter gf = null; - var lockedGFs = RepoFactory.GroupFilter.GetLockedGroupFilters(); + FilterPreset gf = null; + var lockedGFs = RepoFactory.FilterPreset.GetLockedGroupFilters(); if (lockedGFs != null) { // if it already exists we can leave - foreach (var gfTemp in lockedGFs) + foreach (var gfTemp in lockedGFs.Where(gfTemp => gfTemp.FilterType == GroupFilterType.ContinueWatching)) { - if (gfTemp.FilterType == (int)GroupFilterType.ContinueWatching) - { - gf = gfTemp; - break; - } + gf = gfTemp; + break; } } - if (gf == null || !gf.GroupsIds.ContainsKey(jmmuserID)) - { - return retAnime; - } + if (gf == null) return retAnime; - var comboGroups = - gf.GroupsIds[jmmuserID] - .Select(a => RepoFactory.AnimeGroup.GetByID(a)) - .Where(a => a != null) - .Select(a => a.GetUserContract(jmmuserID)); + var evaluator = HttpContext.RequestServices.GetRequiredService<FilterEvaluator>(); + var results = evaluator.EvaluateFilter(gf, user.JMMUserID); - // apply sorting - comboGroups = GroupFilterHelper.Sort(comboGroups, gf); + var comboGroups = results.Select(a => RepoFactory.AnimeGroup.GetByID(a.Key)).Where(a => a != null).Select(a => a.GetUserContract(jmmuserID)); foreach (var grp in comboGroups) { foreach (var ser in RepoFactory.AnimeSeries.GetByGroupID(grp.AnimeGroupID)) { - if (!user.AllowedSeries(ser)) - { - continue; - } + if (!user.AllowedSeries(ser)) continue; var serUser = ser.GetUserRecord(jmmuserID); @@ -588,7 +576,6 @@ public List<Metro_Anime_Summary> GetAnimeContinueWatching(int maxRecords, int jm retAnime.Add(summ); - // Lets only return the specified amount if (retAnime.Count == maxRecords) { diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationPlex.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationPlex.cs deleted file mode 100644 index 07fe98de5..000000000 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationPlex.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System.Collections.Generic; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Shoko.Models.Interfaces; -using Shoko.Models.Plex.Connections; -using Shoko.Models.PlexAndKodi; -using Shoko.Server.PlexAndKodi; -using Shoko.Server.PlexAndKodi.Plex; -using Directory = Shoko.Models.Plex.Libraries.Directory; -using MediaContainer = Shoko.Models.PlexAndKodi.MediaContainer; -using Stream = System.IO.Stream; - -namespace Shoko.Server.API.v1.Implementations; - -[ApiController] -[Route("/api/Plex")] -[ApiVersion("1.0", Deprecated = true)] -public class ShokoServiceImplementationPlex : IShokoServerPlex, IHttpContextAccessor -{ - public HttpContext HttpContext { get; set; } - private readonly CommonImplementation _impl; - - public ShokoServiceImplementationPlex(CommonImplementation impl) - { - _impl = impl; - } - - [HttpGet("Image/Support/{name}")] - public Stream GetSupportImage(string name) - { - return _impl.GetSupportImage(name); - } - - [HttpGet("Filters/{userId}")] - public MediaContainer GetFilters(string userId) - { - return _impl.GetFilters(new PlexProvider { HttpContext = HttpContext }, userId); - } - - [HttpGet("Metadata/{userId}/{type}/{id}/{historyinfo}/{filterid?}")] - public MediaContainer GetMetadata(string userId, int type, string id, string historyinfo, int? filterid) - { - return _impl.GetMetadata(new PlexProvider { HttpContext = HttpContext }, userId, type, id, historyinfo, - false, filterid); - } - - [HttpGet("User")] - public PlexContract_Users GetUsers() - { - return _impl.GetUsers(new PlexProvider { HttpContext = HttpContext }); - } - - [HttpGet("Search/{userId}/{limit}/{query}")] - public MediaContainer Search(string userId, int limit, string query) - { - return _impl.Search(new PlexProvider { HttpContext = HttpContext }, userId, limit, query, false); - } - - [HttpGet("Serie/Watch/{userId}/{epid}/{status}")] - public Response ToggleWatchedStatusOnEpisode(string userId, int epid, bool status) - { - return _impl.ToggleWatchedStatusOnEpisode(new PlexProvider { HttpContext = HttpContext }, userId, epid, - status); - } - - [HttpGet("Vote/{userId}/{id}/{votevalue}/{votetype}")] - public Response Vote(string userId, int id, float votevalue, int votetype) - { - return _impl.VoteAnime(new PlexProvider { HttpContext = HttpContext }, userId, id, votevalue, - votetype); - } - - [HttpGet("Linking/Devices/Current/{userId}")] - public MediaDevice CurrentDevice(int userId) - { - return _impl.CurrentDevice(userId); - } - - [HttpPost("Linking/Directories/{userId}")] - public void UseDirectories(int userId, List<Directory> directories) - { - _impl.UseDirectories(userId, directories); - } - - [HttpGet("Linking/Directories/{userId}")] - public Directory[] Directories(int userId) - { - return _impl.Directories(userId); - } - - [HttpPost("Linking/Servers/{userId}")] - public void UseDevice(int userId, MediaDevice server) - { - _impl.UseDevice(userId, server); - } - - [HttpGet("Linking/Devices/{userId}")] - public MediaDevice[] AvailableDevices(int userId) - { - return _impl.AvailableDevices(userId) ?? new MediaDevice[0]; - } - - - [HttpGet("Metadata/{userId}/{type}/{id}")] - public MediaContainer GetMetadataWithoutHistory(string userId, int type, string id) - { - return GetMetadata(userId, type, id, null, null); - } -} diff --git a/Shoko.Server/API/v1/Modules/ImageModule.cs b/Shoko.Server/API/v1/Modules/ImageModule.cs deleted file mode 100644 index 714d07759..000000000 --- a/Shoko.Server/API/v1/Modules/ImageModule.cs +++ /dev/null @@ -1,14 +0,0 @@ -#if false -using Nancy.Rest.Module; - -namespace Shoko.Server.API.v1 -{ - public class ImageModule : RestModule - { - public ImageModule() - { - SetRestImplementation(new ShokoServiceImplementationImage()); - } - } -} -#endif diff --git a/Shoko.Server/API/v1/Modules/KodiModule.cs b/Shoko.Server/API/v1/Modules/KodiModule.cs deleted file mode 100644 index 182f070f6..000000000 --- a/Shoko.Server/API/v1/Modules/KodiModule.cs +++ /dev/null @@ -1,15 +0,0 @@ -#if false -using Nancy.Rest.Module; -using Shoko.Server.API.v1.Implementations; - -namespace Shoko.Server.API.v1.Modules -{ - public class KodiModule : RestModule - { - public KodiModule() - { - SetRestImplementation(new ShokoServiceImplementationKodi()); - } - } -} -#endif diff --git a/Shoko.Server/API/v1/Modules/MainModule.cs b/Shoko.Server/API/v1/Modules/MainModule.cs deleted file mode 100644 index d9f1c0707..000000000 --- a/Shoko.Server/API/v1/Modules/MainModule.cs +++ /dev/null @@ -1,14 +0,0 @@ -#if false -using Nancy.Rest.Module; - -namespace Shoko.Server.API.v1 -{ - public class MainModule : RestModule - { - public MainModule() - { - SetRestImplementation(new ShokoServiceImplementation()); - } - } -} -#endif diff --git a/Shoko.Server/API/v1/Modules/MetroModule.cs b/Shoko.Server/API/v1/Modules/MetroModule.cs deleted file mode 100644 index a73ecaef4..000000000 --- a/Shoko.Server/API/v1/Modules/MetroModule.cs +++ /dev/null @@ -1,19 +0,0 @@ -#if false -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Nancy.Rest.Module; - -namespace Shoko.Server.API.v1 -{ - public class MetroModule : RestModule - { - public MetroModule() - { - SetRestImplementation(new ShokoServiceImplementationMetro()); - } - } -} -#endif diff --git a/Shoko.Server/API/v1/Modules/PlexModule.cs b/Shoko.Server/API/v1/Modules/PlexModule.cs deleted file mode 100644 index 50f481de8..000000000 --- a/Shoko.Server/API/v1/Modules/PlexModule.cs +++ /dev/null @@ -1,15 +0,0 @@ -#if false -using Nancy.Rest.Module; -using Shoko.Server.API.v1.Implementations; - -namespace Shoko.Server.API.v1.Modules -{ - public class PlexModule : RestModule - { - public PlexModule() - { - SetRestImplementation(new ShokoServiceImplementationPlex()); - } - } -} -#endif diff --git a/Shoko.Server/API/v1/Modules/StreamModule.cs b/Shoko.Server/API/v1/Modules/StreamModule.cs deleted file mode 100644 index e2028b4e1..000000000 --- a/Shoko.Server/API/v1/Modules/StreamModule.cs +++ /dev/null @@ -1,15 +0,0 @@ -#if false -using Nancy.Rest.Module; -using Shoko.Server.API.v1.Implementations; - -namespace Shoko.Server.API.v1.Modules -{ - public class StreamModule : RestModule - { - public StreamModule() - { - SetRestImplementation(new ShokoServiceImplementationStream()); - } - } -} -#endif diff --git a/Shoko.Server/API/v2/Modules/Core.cs b/Shoko.Server/API/v2/Modules/Core.cs index 8716bda5c..bd0e64a50 100644 --- a/Shoko.Server/API/v2/Modules/Core.cs +++ b/Shoko.Server/API/v2/Modules/Core.cs @@ -610,8 +610,20 @@ public ActionResult ScanMovieDB() [HttpGet("user/list")] public ActionResult<Dictionary<int, string>> GetUsers() { - var common = HttpContext.RequestServices.GetRequiredService<CommonImplementation>(); - return common.GetUsers(); + var users = new Dictionary<int, string>(); + try + { + foreach (var us in RepoFactory.JMMUser.GetAll()) + { + users.Add(us.JMMUserID, us.Username); + } + + return users; + } + catch + { + return null; + } } /// <summary> diff --git a/Shoko.Server/Databases/DatabaseFixes.cs b/Shoko.Server/Databases/DatabaseFixes.cs index 2cec4674c..27f7e7114 100644 --- a/Shoko.Server/Databases/DatabaseFixes.cs +++ b/Shoko.Server/Databases/DatabaseFixes.cs @@ -121,26 +121,7 @@ public static void RemoveOldMovieDBImageRecords() } - public static void FixContinueWatchingGroupFilter_20160406() - { - // group filters - - // check if it already exists - var lockedGFs = RepoFactory.GroupFilter.GetLockedGroupFilters(); - - if (lockedGFs != null) - { - foreach (var gf in lockedGFs) - { - if (gf.GroupFilterName.Equals(Constants.GroupFilterName.ContinueWatching, - StringComparison.InvariantCultureIgnoreCase)) - { - gf.FilterType = (int)GroupFilterType.ContinueWatching; - RepoFactory.GroupFilter.Save(gf); - } - } - } - } + public static void FixContinueWatchingGroupFilter_20160406() { } public static void MigrateTraktLinks_V1_to_V2() { @@ -523,59 +504,9 @@ public static void MigrateAniDB_FileUpdates() RepoFactory.AniDB_FileUpdate.Save(tosave); } - public static void FixDuplicateTagFiltersAndUpdateSeasons() - { - var filters = RepoFactory.GroupFilter.GetAll(); - var seasons = filters.Where(a => a.FilterType == (int)GroupFilterType.Season).ToList(); - var tags = filters.Where(a => a.FilterType == (int)GroupFilterType.Tag).ToList(); - - var tagsGrouping = tags.GroupBy(a => a.GroupFilterName).SelectMany(a => a.Skip(1)).ToList(); - - tagsGrouping.ForEach(RepoFactory.GroupFilter.Delete); + public static void FixDuplicateTagFiltersAndUpdateSeasons() { } - tags = filters.Where(a => a.FilterType == (int)GroupFilterType.Tag).ToList(); - - foreach (var filter in tags.Where(a => a.GroupConditions.Contains("`"))) - { - filter.GroupConditions = filter.GroupConditions.Replace("`", "'"); - RepoFactory.GroupFilter.Save(filter); - } - - foreach (var seasonFilter in seasons) - { - seasonFilter.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(seasonFilter); - } - } - - public static void RecalculateYears() - { - try - { - var filters = RepoFactory.GroupFilter.GetAll(); - if (filters.Count == 0) - { - return; - } - - foreach (var gf in filters) - { - if (gf.FilterType != (int)GroupFilterType.Year) - { - continue; - } - - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - } - - RepoFactory.GroupFilter.CreateOrVerifyLockedFilters(); - } - catch (Exception e) - { - logger.Error(e); - } - } + public static void RecalculateYears() { } public static void PopulateResourceLinks() { @@ -598,107 +529,11 @@ public static void PopulateTagWeight() } } - public static void FixTagsWithInclude() - { - try - { - foreach (var gf in RepoFactory.GroupFilter.GetAll()) - { - if (gf.FilterType != (int)GroupFilterType.Tag) - { - continue; - } - - foreach (var gfc in gf.Conditions) - { - if (gfc.ConditionType != (int)GroupFilterConditionType.Tag) - { - continue; - } - - if (gfc.ConditionOperator == (int)GroupFilterOperator.Include) - { - gfc.ConditionOperator = (int)GroupFilterOperator.In; - RepoFactory.GroupFilterCondition.Save(gfc); - continue; - } - - if (gfc.ConditionOperator == (int)GroupFilterOperator.Exclude) - { - gfc.ConditionOperator = (int)GroupFilterOperator.NotIn; - RepoFactory.GroupFilterCondition.Save(gfc); - } - } - - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - } - } - catch (Exception e) - { - logger.Error(e); - } - } - - public static void MakeTagsApplyToSeries() - { - try - { - var filters = RepoFactory.GroupFilter.GetAll(); - if (filters.Count == 0) - { - return; - } + public static void FixTagsWithInclude() { } - foreach (var gf in filters) - { - if (gf.FilterType != (int)GroupFilterType.Tag) - { - continue; - } + public static void MakeTagsApplyToSeries() { } - gf.ApplyToSeries = 1; - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - } - - RepoFactory.GroupFilter.CreateOrVerifyLockedFilters(); - } - catch (Exception e) - { - logger.Error(e); - } - } - - public static void MakeYearsApplyToSeries() - { - try - { - var filters = RepoFactory.GroupFilter.GetAll(); - if (filters.Count == 0) - { - return; - } - - foreach (var gf in filters) - { - if (gf.FilterType != (int)GroupFilterType.Year) - { - continue; - } - - gf.ApplyToSeries = 1; - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - } - - RepoFactory.GroupFilter.CreateOrVerifyLockedFilters(); - } - catch (Exception e) - { - logger.Error(e); - } - } + public static void MakeYearsApplyToSeries() { } public static void UpdateAllTvDBSeries() { diff --git a/Shoko.Server/Import/Importer.cs b/Shoko.Server/Import/Importer.cs index 71f92b96c..212e48078 100755 --- a/Shoko.Server/Import/Importer.cs +++ b/Shoko.Server/Import/Importer.cs @@ -1194,52 +1194,6 @@ public static int UpdateAniDBFileData(bool missingInfo, bool outOfDate, bool dry return vidsToUpdate.Count; } - public static void CheckForDayFilters() - { - var sched = - RepoFactory.ScheduledUpdate.GetByUpdateType((int)ScheduledUpdateType.DayFiltersUpdate); - if (sched != null) - { - if (DateTime.Now.Day == sched.LastUpdate.Day) - { - return; - } - } - //Get GroupFiters that change daily - - var conditions = new HashSet<GroupFilterConditionType> - { - GroupFilterConditionType.AirDate, - GroupFilterConditionType.LatestEpisodeAirDate, - GroupFilterConditionType.SeriesCreatedDate, - GroupFilterConditionType.EpisodeWatchedDate, - GroupFilterConditionType.EpisodeAddedDate - }; - var evalfilters = RepoFactory.GroupFilter.GetWithConditionsTypes(conditions) - .Where( - a => a.Conditions.Any(b => conditions.Contains(b.GetConditionTypeEnum()) && - b.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays)) - .ToList(); - foreach (var g in evalfilters) - { - g.CalculateGroupsAndSeries(); - } - - RepoFactory.GroupFilter.Save(evalfilters); - - if (sched == null) - { - sched = new ScheduledUpdate - { - UpdateDetails = string.Empty, UpdateType = (int)ScheduledUpdateType.DayFiltersUpdate - }; - } - - sched.LastUpdate = DateTime.Now; - RepoFactory.ScheduledUpdate.Save(sched); - } - - public static void CheckForTvDBUpdates(bool forceRefresh) { var settings = Utils.SettingsProvider.GetSettings(); diff --git a/Shoko.Server/PlexAndKodi/CommonImplementation.cs b/Shoko.Server/PlexAndKodi/CommonImplementation.cs deleted file mode 100644 index 0f8f3aa51..000000000 --- a/Shoko.Server/PlexAndKodi/CommonImplementation.cs +++ /dev/null @@ -1,1475 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Shoko.Commons.Extensions; -using Shoko.Commons.Properties; -using Shoko.Models.Client; -using Shoko.Models.Enums; -using Shoko.Models.Plex.Connections; -using Shoko.Models.PlexAndKodi; -using Shoko.Models.Server; -using Shoko.Server.Commands; -using Shoko.Server.Commands.AniDB; -using Shoko.Server.Databases; -using Shoko.Server.Models; -using Shoko.Server.Plex; -using Shoko.Server.PlexAndKodi.Kodi; -using Shoko.Server.PlexAndKodi.Plex; -using Shoko.Server.Providers.TraktTV; -using Shoko.Server.Repositories; -using Shoko.Server.Repositories.NHibernate; -using Shoko.Server.Settings; -using Shoko.Server.Utilities; -using Directory = Shoko.Models.Plex.Libraries.Directory; -using MediaContainer = Shoko.Models.PlexAndKodi.MediaContainer; -using Stream = System.IO.Stream; - -// ReSharper disable FunctionComplexityOverflow - -namespace Shoko.Server.PlexAndKodi; - -public class CommonImplementation -{ - private readonly ILogger<CommonImplementation> _logger; - private readonly ISettingsProvider _settingsProvider; - - //private functions are use internal - - public CommonImplementation(ILogger<CommonImplementation> logger, ISettingsProvider settingsProvider) - { - _logger = logger; - _settingsProvider = settingsProvider; - } - - public Stream GetSupportImage(string name) - { - if (string.IsNullOrEmpty(name)) - { - return null; - } - - name = Path.GetFileNameWithoutExtension(name); - var man = Resources.ResourceManager; - var dta = (byte[])man.GetObject(name); - if (dta == null || dta.Length == 0) - { - return null; - } - - var ms = new MemoryStream(dta); - ms.Seek(0, SeekOrigin.Begin); - return ms; - } - - public MediaContainer GetFilters(IProvider prov, string uid) - { - int.TryParse(uid, out var t); - var user = t > 0 ? Helper.GetJMMUser(uid) : Helper.GetUser(uid); - if (user == null) - { - return new MediaContainer { ErrorString = "User not found" }; - } - - var userid = user.JMMUserID; - - var info = prov.UseBreadCrumbs - ? new BreadCrumbs { Key = prov.ConstructFiltersUrl(userid), Title = "Anime" } - : null; - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Show, "Anime", false, false, info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); //Normal OPTION VERB - } - - var dirs = new List<Video>(); - try - { - using (var session = DatabaseFactory.SessionFactory.OpenSession()) - { - var allGfs = RepoFactory.GroupFilter.GetTopLevel() - .Where(a => !a.IsHidden && - ( - (a.GroupsIds.ContainsKey(userid) && a.GroupsIds[userid].Count > 0) - || a.IsDirectory) - ) - .ToList(); - - - foreach (var gg in allGfs) - { - var pp = Helper.DirectoryFromFilter(prov, gg, userid); - if (pp != null) - { - dirs.Add(prov, pp, info); - } - } - - var vids = RepoFactory.VideoLocal.GetVideosWithoutEpisodeUnsorted(); - if (vids.Count > 0) - { - var pp = new Shoko.Models.PlexAndKodi.Directory { Type = "show" }; - pp.Key = prov.ShortUrl(prov.ConstructUnsortUrl(userid)); - pp.Title = "Unsort"; - pp.AnimeType = AnimeTypes.AnimeUnsort.ToString(); - pp.Thumb = prov.ConstructSupportImageLink("plex_unsort.png"); - pp.LeafCount = vids.Count; - pp.ViewedLeafCount = 0; - dirs.Add(prov, pp, info); - } - - var playlists = RepoFactory.Playlist.GetAll(); - if (playlists.Count > 0) - { - var pp = new Shoko.Models.PlexAndKodi.Directory { Type = "show" }; - pp.Key = prov.ShortUrl(prov.ConstructPlaylistUrl(userid)); - pp.Title = "Playlists"; - pp.AnimeType = AnimeTypes.AnimePlaylist.ToString(); - pp.Thumb = prov.ConstructSupportImageLink("plex_playlists.png"); - pp.LeafCount = playlists.Count; - pp.ViewedLeafCount = 0; - dirs.Add(prov, pp, info); - } - - dirs = dirs.OrderBy(a => a.Title).ToList(); - } - - ret.MediaContainer.RandomizeArt(prov, dirs); - if (prov.AddPlexPrefsItem) - { - var dir = new Shoko.Models.PlexAndKodi.Directory { Prompt = "Search" }; - dir.Thumb = dir.Art = "/:/plugins/com.plexapp.plugins.myanime/resources/Search.png"; - dir.Key = "/video/jmm/search"; - dir.Title = "Search"; - dir.Search = "1"; - dirs.Add(dir); - } - - if (prov.AddPlexSearchItem) - { - var dir = new Shoko.Models.PlexAndKodi.Directory(); - dir.Thumb = dir.Art = "/:/plugins/com.plexapp.plugins.myanime/resources/Gear.png"; - dir.Key = "/:/plugins/com.plexapp.plugins.myanime/prefs"; - dir.Title = "Preferences"; - dir.Settings = "1"; - dirs.Add(dir); - } - - ret.Childrens = dirs; - var dinfo = prov.GetPlexClient(); - if (dinfo != null) - { - _logger.LogInformation(dinfo.ToString()); - } - - return ret.GetStream(prov); - } - catch (Exception ex) - { - _logger.LogError(ex, ex.ToString()); - return new MediaContainer { ErrorString = "System Error, see JMMServer logs for more information" }; - } - } - - public MediaContainer GetMetadata(IProvider prov, string UserId, int type, string Id, string historyinfo, - bool nocast = false, int? filter = null) - { - try - { - var his = prov.UseBreadCrumbs ? BreadCrumbs.FromKey(historyinfo) : null; - var user = Helper.GetJMMUser(UserId); - - switch ((JMMType)type) - { - case JMMType.Group: - return GetItemsFromGroup(prov, user.JMMUserID, Id, his, nocast, filter); - case JMMType.GroupFilter: - return GetGroupsOrSubFiltersFromFilter(prov, user.JMMUserID, Id, his, nocast); - case JMMType.GroupUnsort: - return GetUnsort(prov, user.JMMUserID, his); - case JMMType.Serie: - return GetItemsFromSerie(prov, user.JMMUserID, Id, his, nocast); - case JMMType.Episode: - return GetFromEpisode(prov, user.JMMUserID, Id, his); - case JMMType.File: - return GetFromFile(prov, user.JMMUserID, Id, his); - case JMMType.Playlist: - return GetItemsFromPlaylist(prov, user.JMMUserID, Id, his); - case JMMType.FakeIosThumb: - return FakeParentForIOSThumbnail(prov, Id); - } - - return new MediaContainer { ErrorString = "Unsupported Type" }; - } - catch (Exception ex) - { - _logger.LogError(ex, ex.ToString()); - return new MediaContainer { ErrorString = "System Error, see JMMServer logs for more information" }; - } - } - - private MediaContainer GetItemsFromPlaylist(IProvider prov, int userid, string id, BreadCrumbs info) - { - var PlaylistID = -1; - int.TryParse(id, out PlaylistID); - - if (PlaylistID == 0) - { - using (var session = DatabaseFactory.SessionFactory.OpenSession()) - { - var ret = new BaseObject( - prov.NewMediaContainer(MediaContainerTypes.Show, "Playlists", true, true, info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); //Normal - } - - var retPlaylists = new List<Video>(); - var playlists = RepoFactory.Playlist.GetAll(); - var sessionWrapper = session.Wrap(); - - foreach (var playlist in playlists) - { - var dir = new Shoko.Models.PlexAndKodi.Directory - { - Key = prov.ShortUrl(prov.ConstructPlaylistIdUrl(userid, playlist.PlaylistID)), - Title = playlist.PlaylistName, - Id = playlist.PlaylistID, - AnimeType = AnimeTypes.AnimePlaylist.ToString() - }; - var episodeID = -1; - if (int.TryParse(playlist.PlaylistItems.Split('|')[0].Split(';')[1], out episodeID)) - { - var anime = RepoFactory.AnimeEpisode.GetByID(episodeID) - .GetAnimeSeries() - .GetAnime(); - dir.Thumb = anime?.GetDefaultPosterDetailsNoBlanks()?.GenPoster(prov); - dir.Art = anime?.GetDefaultFanartDetailsNoBlanks()?.GenArt(prov); - dir.Banner = anime?.GetDefaultWideBannerDetailsNoBlanks()?.GenArt(prov); - } - else - { - dir.Thumb = prov.ConstructSupportImageLink("plex_404V.png"); - } - - dir.LeafCount = playlist.PlaylistItems.Split('|').Count(); - dir.ViewedLeafCount = 0; - retPlaylists.Add(prov, dir, info); - } - - retPlaylists = retPlaylists.OrderBy(a => a.Title).ToList(); - ret.Childrens = retPlaylists; - return ret.GetStream(prov); - } - } - - if (PlaylistID > 0) - { - var playlist = RepoFactory.Playlist.GetByID(PlaylistID); - var playlistItems = playlist.PlaylistItems.Split('|'); - var vids = new List<Video>(); - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Episode, playlist.PlaylistName, true, - true, - info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); //Normal - } - - foreach (var item in playlistItems) - { - try - { - var episodeID = -1; - int.TryParse(item.Split(';')[1], out episodeID); - if (episodeID < 0) - { - return new MediaContainer { ErrorString = "Invalid Episode ID" }; - } - - var e = RepoFactory.AnimeEpisode.GetByID(episodeID); - if (e == null) - { - return new MediaContainer { ErrorString = "Invalid Episode" }; - } - - var ep = - new KeyValuePair<SVR_AnimeEpisode, CL_AnimeEpisode_User>(e, - e.GetUserContract(userid)); - if (ep.Value != null && ep.Value.LocalFileCount == 0) - { - continue; - } - - var ser = RepoFactory.AnimeSeries.GetByID(ep.Key.AnimeSeriesID); - if (ser == null) - { - return new MediaContainer { ErrorString = "Invalid Series" }; - } - - var con = ser.GetUserContract(userid); - if (con == null) - { - return new MediaContainer { ErrorString = "Invalid Series, Contract not found" }; - } - - var v = Helper.VideoFromAnimeEpisode(prov, con.CrossRefAniDBTvDBV2, ep, userid); - if (v != null && v.Medias != null && v.Medias.Count > 0) - { - Helper.AddInformationFromMasterSeries(v, con, ser.GetPlexContract(userid)); - v.Type = "episode"; - vids.Add(prov, v, info); - var dinfo = prov.GetPlexClient(); - if (prov.ConstructFakeIosParent && dinfo != null && dinfo.Client == PlexClient.IOS) - { - v.GrandparentKey = - prov.Proxyfy(prov.ConstructFakeIosThumb(userid, v.ParentThumb, - v.Art ?? v.ParentArt ?? v.GrandparentArt)); - } - - v.ParentKey = null; - } - } - catch - { - //Fast fix if file do not exist, and still is in db. (Xml Serialization of video info will fail on null) - } - } - - ret.MediaContainer.RandomizeArt(prov, vids); - ret.Childrens = vids; - return ret.GetStream(prov); - } - - return new MediaContainer { ErrorString = "Invalid Playlist" }; - } - - private MediaContainer GetUnsort(IProvider prov, int userid, BreadCrumbs info) - { - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Video, "Unsort", true, true, info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); - } - - var dirs = new List<Video>(); - var vids = RepoFactory.VideoLocal.GetVideosWithoutEpisode(); - foreach (var v in vids.OrderByDescending(a => a.DateTimeCreated)) - { - try - { - var m = Helper.VideoFromVideoLocal(prov, v, userid); - dirs.Add(prov, m, info); - m.Thumb = prov.ConstructSupportImageLink("plex_404.png"); - m.ParentThumb = prov.ConstructSupportImageLink("plex_unsort.png"); - m.ParentKey = null; - var dinfo = prov.GetPlexClient(); - if (prov.ConstructFakeIosParent && dinfo != null && dinfo.Client == PlexClient.IOS) - - { - m.GrandparentKey = - prov.Proxyfy(prov.ConstructFakeIosThumb(userid, m.ParentThumb, - m.Art ?? m.ParentArt ?? m.GrandparentArt)); - } - } - catch - { - //Fast fix if file do not exist, and still is in db. (Xml Serialization of video info will fail on null) - } - } - - ret.Childrens = dirs; - return ret.GetStream(prov); - } - - private MediaContainer GetFromFile(IProvider prov, int userid, string Id, BreadCrumbs info) - { - if (!int.TryParse(Id, out var id)) - { - return new MediaContainer { ErrorString = "Invalid File Id" }; - } - - var vi = RepoFactory.VideoLocal.GetByID(id); - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.File, - Path.GetFileNameWithoutExtension(vi.FileName ?? string.Empty), - true, false, info)); - var v2 = Helper.VideoFromVideoLocal(prov, vi, userid); - var dirs = new List<Video>(); - dirs.EppAdd(prov, v2, info, true); - v2.Thumb = prov.ConstructSupportImageLink("plex_404.png"); - v2.ParentThumb = prov.ConstructSupportImageLink("plex_unsort.png"); - var dinfo = prov.GetPlexClient(); - if (prov.ConstructFakeIosParent && dinfo != null && dinfo.Client == PlexClient.IOS) - - { - v2.GrandparentKey = - prov.Proxyfy(prov.ConstructFakeIosThumb(userid, v2.ParentThumb, - v2.Art ?? v2.ParentArt ?? v2.GrandparentArt)); - } - - v2.ParentKey = null; - if (prov.UseBreadCrumbs) - { - v2.Key = prov.ShortUrl(ret.MediaContainer.Key); - } - - ret.MediaContainer.Childrens = dirs; - return ret.GetStream(prov); - } - - private MediaContainer GetFromEpisode(IProvider prov, int userid, string Id, BreadCrumbs info) - { - if (!int.TryParse(Id, out var id)) - { - return new MediaContainer { ErrorString = "Invalid Episode Id" }; - } - - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Episode, "Episode", true, true, info)); - using (var session = DatabaseFactory.SessionFactory.OpenSession()) - { - var dirs = new List<Video>(); - var sessionWrapper = session.Wrap(); - - var e = RepoFactory.AnimeEpisode.GetByID(id); - if (e == null) - { - return new MediaContainer { ErrorString = "Invalid Episode Id" }; - } - - var ep = - new KeyValuePair<SVR_AnimeEpisode, CL_AnimeEpisode_User>(e, - e.GetUserContract(userid)); - if (ep.Value != null && ep.Value.LocalFileCount == 0) - { - return new MediaContainer { ErrorString = "Episode do not have videolocals" }; - } - - var aep = ep.Key.AniDB_Episode; - if (aep == null) - { - return new MediaContainer { ErrorString = "Invalid Episode AniDB link not found" }; - } - - var ser = RepoFactory.AnimeSeries.GetByID(ep.Key.AnimeSeriesID); - if (ser == null) - { - return new MediaContainer { ErrorString = "Invalid Serie" }; - } - - var anime = ser.GetAnime(); - var con = ser.GetUserContract(userid); - if (con == null) - { - return new MediaContainer { ErrorString = "Invalid Serie, Contract not found" }; - } - - try - { - var v = Helper.VideoFromAnimeEpisode(prov, con.CrossRefAniDBTvDBV2, ep, userid); - if (v != null) - { - var nv = ser.GetPlexContract(userid); - Helper.AddInformationFromMasterSeries(v, con, ser.GetPlexContract(userid), - prov is KodiProvider); - if (v.Medias != null && v.Medias.Count > 0) - { - v.Type = "episode"; - dirs.EppAdd(prov, v, info, true); - var dinfo = prov.GetPlexClient(); - if (prov.ConstructFakeIosParent && dinfo != null && dinfo.Client == PlexClient.IOS) - { - v.GrandparentKey = - prov.Proxyfy(prov.ConstructFakeIosThumb(userid, v.ParentThumb, - v.Art ?? v.ParentArt ?? v.GrandparentArt)); - } - - v.ParentKey = null; - } - - if (prov.UseBreadCrumbs) - { - v.Key = prov.ShortUrl(ret.MediaContainer.Key); - } - - ret.MediaContainer.Art = prov.ReplaceSchemeHost(nv.Art ?? nv.ParentArt ?? nv.GrandparentArt); - } - - ret.MediaContainer.Childrens = dirs; - return ret.GetStream(prov); - } - catch - { - //Fast fix if file do not exist, and still is in db. (Xml Serialization of video info will fail on null) - } - } - - return new MediaContainer { ErrorString = "Episode Not Found" }; - } - - public Dictionary<int, string> GetUsers() - { - var users = new Dictionary<int, string>(); - try - { - foreach (var us in RepoFactory.JMMUser.GetAll()) - { - users.Add(us.JMMUserID, us.Username); - } - - return users; - } - catch - { - return null; - } - } - - public PlexContract_Users GetUsers(IProvider prov) - { - var gfs = new PlexContract_Users(); - try - { - gfs.Users = new List<PlexContract_User>(); - foreach (var us in RepoFactory.JMMUser.GetAll()) - { - var p = new PlexContract_User { id = us.JMMUserID.ToString(), name = us.Username }; - gfs.Users.Add(p); - } - } - catch (Exception ex) - { - _logger.LogError(ex, ex.ToString()); - return new PlexContract_Users { ErrorString = "System Error, see JMMServer logs for more information" }; - } - - return gfs; - } - - public Response GetVersion() - { - var rsp = new Response(); - try - { - rsp.Code = "200"; - rsp.Message = Assembly.GetEntryAssembly().GetName().Version.ToString(); - return rsp; - } - catch (Exception e) - { - _logger.LogError(e, e.ToString()); - rsp.Code = "500"; - rsp.Message = "System Error, see JMMServer logs for more information"; - } - - return rsp; - } - - public MediaContainer Search(IProvider prov, string UserId, int lim, string query, bool searchTag, - bool nocast = false) - { - var info = prov.UseBreadCrumbs - ? new BreadCrumbs - { - Key = prov.ConstructSearchUrl(UserId, lim, query, searchTag), Title = "Search for '" + query + "'" - } - : null; - - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Show, "Search for '" + query + "'", true, - true, - info)); - if (lim == 0) - { - lim = 100; - } - - var user = Helper.GetUser(UserId); - if (user == null) - { - return new MediaContainer { ErrorString = "User Not Found" }; - } - - var ls = new List<Video>(); - var cnt = 0; - var series = searchTag - ? RepoFactory.AnimeSeries.GetAll() - .Where( - a => - a.Contract != null && a.Contract.AniDBAnime != null && - a.Contract.AniDBAnime.AniDBAnime != null && - (a.Contract.AniDBAnime.AniDBAnime.GetAllTags() - .Contains(query, - StringComparer.InvariantCultureIgnoreCase) || - a.Contract.AniDBAnime.CustomTags.Select(b => b.TagName) - .Contains(query, StringComparer.InvariantCultureIgnoreCase))) - : RepoFactory.AnimeSeries.GetAll() - .Where( - a => - a.Contract != null && a.Contract.AniDBAnime != null && - a.Contract.AniDBAnime.AniDBAnime != null && - string.Join(",", a.Contract.AniDBAnime.AniDBAnime.AllTitles) - .IndexOf(query, 0, StringComparison.InvariantCultureIgnoreCase) >= 0); - - //List<AniDB_Anime> animes = searchTag ? RepoFactory.AniDB_Anime.SearchByTag(query) : RepoFactory.AniDB_Anime.SearchByName(query); - foreach (var ser in series) - { - if (!user.AllowedSeries(ser)) - { - continue; - } - - Video v = ser.GetPlexContract(user.JMMUserID)?.Clone<Shoko.Models.PlexAndKodi.Directory>(prov); - if (v != null) - { - switch (ser.Contract.AniDBAnime.AniDBAnime.AnimeType) - { - case (int)AnimeType.Movie: - v.SourceTitle = "Anime Movies"; - break; - case (int)AnimeType.OVA: - v.SourceTitle = "Anime Ovas"; - break; - case (int)AnimeType.Other: - v.SourceTitle = "Anime Others"; - break; - case (int)AnimeType.TVSeries: - v.SourceTitle = "Anime Series"; - break; - case (int)AnimeType.TVSpecial: - v.SourceTitle = "Anime Specials"; - break; - case (int)AnimeType.Web: - v.SourceTitle = "Anime Web Clips"; - break; - } - - if (nocast) - { - v.Roles = null; - } - - v.GenerateKey(prov, user.JMMUserID); - ls.Add(prov, v, info); - } - - cnt++; - if (cnt == lim) - { - break; - } - } - - ret.MediaContainer.RandomizeArt(prov, ls); - ret.MediaContainer.Childrens = Helper.ConvertToDirectory(ls, prov); - return ret.GetStream(prov); - } - - public MediaContainer GetItemsFromGroup(IProvider prov, int userid, string GroupId, BreadCrumbs info, - bool nocast, int? filterID) - { - int.TryParse(GroupId, out var groupID); - if (groupID == -1) - { - return new MediaContainer { ErrorString = "Invalid Group Id" }; - } - - var retGroups = new List<Video>(); - var grp = RepoFactory.AnimeGroup.GetByID(groupID); - - if (grp == null) - { - return new MediaContainer { ErrorString = "Invalid Group" }; - } - - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Show, grp.GroupName, false, true, info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); - } - - var basegrp = grp?.GetUserContract(userid); - if (basegrp != null) - { - var seriesList = grp.GetSeries(); - if (filterID != null) - { - var filter = RepoFactory.GroupFilter.GetByID(filterID.Value); - if (filter != null) - { - if (filter.ApplyToSeries > 0) - { - if (filter.SeriesIds.ContainsKey(userid)) - { - seriesList = - seriesList.Where(a => filter.SeriesIds[userid].Contains(a.AnimeSeriesID)).ToList(); - } - } - } - } - - foreach (var grpChild in grp.GetChildGroups()) - { - var v = grpChild.GetPlexContract(userid); - if (v != null) - { - v.Type = "show"; - v.GenerateKey(prov, userid); - - v.Art = Helper.GetRandomFanartFromVideo(v, prov) ?? v.Art; - v.Banner = Helper.GetRandomBannerFromVideo(v, prov) ?? v.Banner; - - if (nocast) - { - v.Roles = null; - } - - retGroups.Add(prov, v, info); - v.ParentThumb = v.GrandparentThumb = null; - } - } - - foreach (var ser in seriesList) - { - var v = ser.GetPlexContract(userid)?.Clone<Shoko.Models.PlexAndKodi.Directory>(prov); - if (v != null) - { - v.AirDate = ser.AirDate ?? DateTime.MinValue; - v.Group = basegrp; - v.Type = "show"; - v.GenerateKey(prov, userid); - v.Art = Helper.GetRandomFanartFromVideo(v, prov) ?? v.Art; - v.Banner = Helper.GetRandomBannerFromVideo(v, prov) ?? v.Banner; - if (nocast) - { - v.Roles = null; - } - - retGroups.Add(prov, v, info); - v.ParentThumb = v.GrandparentThumb = null; - } - } - } - - ret.MediaContainer.RandomizeArt(prov, retGroups); - ret.Childrens = Helper.ConvertToDirectory(retGroups.OrderBy(a => a.AirDate).ToList(), prov); - FilterExtras(prov, ret.Childrens); - return ret.GetStream(prov); - } - - public Response ToggleWatchedStatusOnEpisode(IProvider prov, string userid, int aep, bool wstatus) - { - var rsp = new Response { Code = "400", Message = "Bad Request" }; - try - { - if (!int.TryParse(userid, out var usid)) - { - return rsp; - } - - var ep = RepoFactory.AnimeEpisode.GetByID(aep); - if (ep == null) - { - rsp.Code = "404"; - rsp.Message = "Episode Not Found"; - return rsp; - } - - ep.ToggleWatchedStatus(wstatus, true, DateTime.Now, false, usid, true); - var series = ep.GetAnimeSeries(); - series?.UpdateStats(true, false); - series?.AnimeGroup?.TopLevelAnimeGroup?.UpdateStatsFromTopLevel(true, true); - rsp.Code = "200"; - rsp.Message = null; - } - catch (Exception ex) - { - rsp.Code = "500"; - rsp.Message = "Internal Error : " + ex; - _logger.LogError(ex, ex.ToString()); - } - - return rsp; - } - - public Response ToggleWatchedStatusOnSeries(IProvider prov, string userid, int aep, - bool wstatus) - { - //prov.AddResponseHeaders(); - - var rsp = new Response { Code = "400", Message = "Bad Request" }; - try - { - if (!int.TryParse(userid, out var usid)) - { - return rsp; - } - - var series = RepoFactory.AnimeSeries.GetByID(aep); - if (series == null) - { - rsp.Code = "404"; - rsp.Message = "Episode Not Found"; - return rsp; - } - - var eps = series.GetAnimeEpisodes(); - foreach (var ep in eps) - { - if (ep.AniDB_Episode == null) - { - continue; - } - - if (ep.EpisodeTypeEnum == EpisodeType.Credits) - { - continue; - } - - if (ep.EpisodeTypeEnum == EpisodeType.Trailer) - { - continue; - } - - ep.ToggleWatchedStatus(wstatus, true, DateTime.Now, false, usid, true); - } - - series.UpdateStats(true, false); - series.AnimeGroup?.TopLevelAnimeGroup?.UpdateStatsFromTopLevel(true, true); - rsp.Code = "200"; - rsp.Message = null; - } - catch (Exception ex) - { - rsp.Code = "500"; - rsp.Message = "Internal Error : " + ex; - _logger.LogError(ex, ex.ToString()); - } - - return rsp; - } - - public Response ToggleWatchedStatusOnGroup(IProvider prov, string userid, int aep, - bool wstatus) - { - //prov.AddResponseHeaders(); - - var rsp = new Response { Code = "400", Message = "Bad Request" }; - try - { - if (!int.TryParse(userid, out var usid)) - { - return rsp; - } - - var group = RepoFactory.AnimeGroup.GetByID(aep); - if (group == null) - { - rsp.Code = "404"; - rsp.Message = "Episode Not Found"; - return rsp; - } - - foreach (var series in group.GetAllSeries()) - { - foreach (var ep in series.GetAnimeEpisodes()) - { - if (ep.AniDB_Episode == null) - { - continue; - } - - if (ep.EpisodeTypeEnum == EpisodeType.Credits) - { - continue; - } - - if (ep.EpisodeTypeEnum == EpisodeType.Trailer) - { - continue; - } - - ep.ToggleWatchedStatus(wstatus, true, DateTime.Now, false, usid, true); - } - - series.UpdateStats(true, false); - } - - group.TopLevelAnimeGroup.UpdateStatsFromTopLevel(true, false); - - rsp.Code = "200"; - rsp.Message = null; - } - catch (Exception ex) - { - rsp.Code = "500"; - rsp.Message = "Internal Error : " + ex; - _logger.LogError(ex, ex.ToString()); - } - - return rsp; - } - - public Response VoteAnime(IProvider prov, string userid, int objid, float vvalue, - int vt) - { - var rsp = new Response { Code = "400", Message = "Bad Request" }; - try - { - if (!int.TryParse(userid, out var usid)) - { - return rsp; - } - - var commandFactory = Utils.ServiceContainer.GetRequiredService<ICommandRequestFactory>(); - if (vt == (int)AniDBVoteType.Episode) - { - var ep = RepoFactory.AnimeEpisode.GetByID(objid); - if (ep == null) - { - rsp.Code = "404"; - rsp.Message = "Episode Not Found"; - return rsp; - } - - var anime = ep.GetAnimeSeries().GetAnime(); - if (anime == null) - { - rsp.Code = "404"; - rsp.Message = "Anime Not Found"; - return rsp; - } - - var msg = string.Format("Voting for anime episode: {0} - Value: {1}", ep.AnimeEpisodeID, - vvalue); - _logger.LogInformation(msg); - - // lets save to the database and assume it will work - var thisVote = RepoFactory.AniDB_Vote.GetByEntityAndType(ep.AnimeEpisodeID, AniDBVoteType.Episode); - - if (thisVote == null) - { - thisVote = new AniDB_Vote { EntityID = ep.AnimeEpisodeID }; - } - - thisVote.VoteType = vt; - - var iVoteValue = 0; - if (vvalue > 0) - { - iVoteValue = (int)(vvalue * 100); - } - else - { - iVoteValue = (int)vvalue; - } - - msg = string.Format("Voting for anime episode Formatted: {0} - Value: {1}", ep.AnimeEpisodeID, - iVoteValue); - _logger.LogInformation(msg); - thisVote.VoteValue = iVoteValue; - RepoFactory.AniDB_Vote.Save(thisVote); - - commandFactory.CreateAndSave<CommandRequest_VoteAnime>( - c => - { - c.AnimeID = anime.AnimeID; - c.VoteType = vt; - c.VoteValue = Convert.ToDecimal(vvalue); - } - ); - } - - if (vt == (int)AniDBVoteType.Anime) - { - var ser = RepoFactory.AnimeSeries.GetByID(objid); - var anime = ser.GetAnime(); - if (anime == null) - { - rsp.Code = "404"; - rsp.Message = "Anime Not Found"; - return rsp; - } - - var msg = string.Format("Voting for anime: {0} - Value: {1}", anime.AnimeID, vvalue); - _logger.LogInformation(msg); - - // lets save to the database and assume it will work - var thisVote = - RepoFactory.AniDB_Vote.GetByEntityAndType(anime.AnimeID, AniDBVoteType.AnimeTemp) ?? - RepoFactory.AniDB_Vote.GetByEntityAndType(anime.AnimeID, AniDBVoteType.Anime); - - if (thisVote == null) - { - thisVote = new AniDB_Vote { EntityID = anime.AnimeID }; - } - - thisVote.VoteType = vt; - - var iVoteValue = 0; - if (vvalue > 0) - { - iVoteValue = (int)(vvalue * 100); - } - else - { - iVoteValue = (int)vvalue; - } - - msg = string.Format("Voting for anime Formatted: {0} - Value: {1}", anime.AnimeID, iVoteValue); - _logger.LogInformation(msg); - thisVote.VoteValue = iVoteValue; - RepoFactory.AniDB_Vote.Save(thisVote); - commandFactory.CreateAndSave<CommandRequest_VoteAnime>( - c => - { - c.AnimeID = anime.AnimeID; - c.VoteType = vt; - c.VoteValue = Convert.ToDecimal(vvalue); - } - ); - } - - rsp.Code = "200"; - rsp.Message = null; - } - catch (Exception ex) - { - rsp.Code = "500"; - rsp.Message = "Internal Error : " + ex; - _logger.LogError(ex, ex.ToString()); - } - - return rsp; - } - - public Response TraktScrobble(IProvider prov, string animeId, int typeTrakt, float progressTrakt, int status) - { - var traktHelper = Utils.ServiceContainer.GetRequiredService<TraktTVHelper>(); - var rsp = new Response { Code = "400", Message = "Bad Request" }; - try - { - var statusTraktV2 = ScrobblePlayingStatus.Start; - switch (status) - { - case (int)ScrobblePlayingStatus.Start: - statusTraktV2 = ScrobblePlayingStatus.Start; - break; - case (int)ScrobblePlayingStatus.Pause: - statusTraktV2 = ScrobblePlayingStatus.Pause; - break; - case (int)ScrobblePlayingStatus.Stop: - statusTraktV2 = ScrobblePlayingStatus.Stop; - break; - } - - progressTrakt = progressTrakt / 10; - - switch (typeTrakt) - { - // Movie - case (int)ScrobblePlayingType.movie: - rsp.Code = traktHelper.Scrobble( - ScrobblePlayingType.movie, animeId, - statusTraktV2, progressTrakt) - .ToString(); - rsp.Message = "Movie Scrobbled"; - break; - // TV episode - case (int)ScrobblePlayingType.episode: - rsp.Code = - traktHelper.Scrobble(ScrobblePlayingType.episode, - animeId, - statusTraktV2, progressTrakt) - .ToString(); - rsp.Message = "Episode Scrobbled"; - break; - //error - } - } - catch (Exception ex) - { - rsp.Code = "500"; - rsp.Message = "Internal Error : " + ex; - _logger.LogError(ex, ex.ToString()); - } - - return rsp; - } - - private MediaContainer FakeParentForIOSThumbnail(IProvider prov, string base64) - { - var ret = new BaseObject(prov.NewMediaContainer(MediaContainerTypes.None, null, false)); - if (!ret.Init(prov)) - { - return new MediaContainer(); - } - - var urls = Helper.Base64DecodeUrl(base64).Split('|'); - var thumb = prov.ReplaceSchemeHost(urls[0]); - var art = prov.ReplaceSchemeHost(urls[1]); - var v = new Shoko.Models.PlexAndKodi.Directory - { - Thumb = thumb, - ParentThumb = thumb, - GrandparentThumb = thumb, - Art = art, - ParentArt = art, - GrandparentArt = art - }; - ret.MediaContainer.Thumb = ret.MediaContainer.ParentThumb = ret.MediaContainer.GrandparentThumb = thumb; - ret.MediaContainer.Art = ret.MediaContainer.ParentArt = ret.MediaContainer.GrandparentArt = art; - var vids = new List<Video> { v }; - ret.Childrens = vids; - return ret.GetStream(prov); - } - - private void FilterExtras(IProvider provider, List<Video> videos) - { - foreach (var v in videos) - { - if (!provider.EnableAnimeTitlesInLists) - { - v.Titles = null; - } - - if (!provider.EnableGenresInLists) - { - v.Genres = null; - } - - if (!provider.EnableRolesInLists) - { - v.Roles = null; - } - } - } - - public MediaContainer GetItemsFromSerie(IProvider prov, int userid, string SerieId, BreadCrumbs info, - bool nocast) - { - BaseObject ret = null; - EpisodeType? eptype = null; - int serieID; - if (SerieId.Contains("_")) - { - var ndata = SerieId.Split('_'); - if (!int.TryParse(ndata[0], out var ept)) - { - return new MediaContainer { ErrorString = "Invalid Serie Id" }; - } - - eptype = (EpisodeType)ept; - if (!int.TryParse(ndata[1], out serieID)) - { - return new MediaContainer { ErrorString = "Invalid Serie Id" }; - } - } - else - { - if (!int.TryParse(SerieId, out serieID)) - { - return new MediaContainer { ErrorString = "Invalid Serie Id" }; - } - } - - - using (var session = DatabaseFactory.SessionFactory.OpenSession()) - { - if (serieID == -1) - { - return new MediaContainer { ErrorString = "Invalid Serie Id" }; - } - - var sessionWrapper = session.Wrap(); - var ser = RepoFactory.AnimeSeries.GetByID(serieID); - if (ser == null) - { - return new MediaContainer { ErrorString = "Invalid Series" }; - } - - var cseries = ser.GetUserContract(userid); - if (cseries == null) - { - return new MediaContainer { ErrorString = "Invalid Series, Contract Not Found" }; - } - - var nv = ser.GetPlexContract(userid); - - - var episodes = ser.GetAnimeEpisodes() - .ToDictionary(a => a, a => a.GetUserContract(userid)); - episodes = episodes.Where(a => a.Value == null || a.Value.LocalFileCount > 0) - .ToDictionary(a => a.Key, a => a.Value); - if (eptype.HasValue) - { - episodes = episodes.Where(a => a.Key.AniDB_Episode != null && a.Key.EpisodeTypeEnum == eptype.Value) - .ToDictionary(a => a.Key, a => a.Value); - } - else - { - var types = episodes.Keys.Where(a => a.AniDB_Episode != null) - .Select(a => a.EpisodeTypeEnum).Distinct().ToList(); - if (types.Count > 1) - { - ret = new BaseObject( - prov.NewMediaContainer(MediaContainerTypes.Show, "Types", false, true, info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); - } - - ret.MediaContainer.Art = cseries.AniDBAnime?.AniDBAnime?.DefaultImageFanart.GenArt(prov); - ret.MediaContainer.LeafCount = - cseries.WatchedEpisodeCount + cseries.UnwatchedEpisodeCount; - ret.MediaContainer.ViewedLeafCount = cseries.WatchedEpisodeCount; - var eps = new List<PlexEpisodeType>(); - foreach (var ee in types) - { - var k2 = new PlexEpisodeType(); - PlexEpisodeType.EpisodeTypeTranslated(k2, ee, - (AnimeType)cseries.AniDBAnime.AniDBAnime.AnimeType, - episodes.Count(a => a.Key.EpisodeTypeEnum == ee)); - eps.Add(k2); - } - - var dirs = new List<Video>(); - //bool converttoseason = true; - - foreach (var ee in eps.OrderBy(a => a.Name)) - { - Video v = new Shoko.Models.PlexAndKodi.Directory - { - Art = nv.Art, Title = ee.Name, AnimeType = "AnimeType", LeafCount = ee.Count - }; - v.ChildCount = v.LeafCount; - v.ViewedLeafCount = 0; - v.Key = prov.ShortUrl(prov.ConstructSerieIdUrl(userid, ee.Type + "_" + ser.AnimeSeriesID)); - v.Thumb = prov.ConstructSupportImageLink(ee.Image); - if (ee.AnimeType == AnimeType.Movie || - ee.AnimeType == AnimeType.OVA) - { - v = Helper.MayReplaceVideo(v, ser, cseries, userid, false, nv); - } - - dirs.Add(prov, v, info, false, true); - } - - ret.Childrens = dirs; - return ret.GetStream(prov); - } - } - - ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Episode, ser.GetSeriesName(), true, - true, info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); - } - - ret.MediaContainer.Art = cseries.AniDBAnime?.AniDBAnime?.DefaultImageFanart.GenArt(prov); - ret.MediaContainer.LeafCount = - cseries.WatchedEpisodeCount + cseries.UnwatchedEpisodeCount; - ret.MediaContainer.ViewedLeafCount = cseries.WatchedEpisodeCount; - - // Here we are collapsing to episodes - - var vids = new List<Video>(); - - if (eptype.HasValue && info != null) - { - info.ParentKey = info.GrandParentKey; - } - - var hasRoles = false; - foreach (var ep in episodes) - { - try - { - var v = Helper.VideoFromAnimeEpisode(prov, cseries.CrossRefAniDBTvDBV2, ep, userid); - if (v != null && v.Medias != null && v.Medias.Count > 0) - { - if (nocast && !hasRoles) - { - hasRoles = true; - } - - Helper.AddInformationFromMasterSeries(v, cseries, nv, hasRoles); - v.Type = "episode"; - vids.Add(prov, v, info); - v.GrandparentThumb = v.ParentThumb; - var dinfo = prov.GetPlexClient(); - if (prov.ConstructFakeIosParent && dinfo != null && dinfo.Client == PlexClient.IOS) - { - v.GrandparentKey = - prov.Proxyfy(prov.ConstructFakeIosThumb(userid, v.ParentThumb, - v.Art ?? v.ParentArt ?? v.GrandparentArt)); - } - - v.ParentKey = null; - if (!hasRoles) - { - hasRoles = true; - } - } - } - catch - { - //Fast fix if file do not exist, and still is in db. (Xml Serialization of video info will fail on null) - } - } - - ret.Childrens = vids.OrderBy(a => a.EpisodeNumber).ToList(); - FilterExtras(prov, ret.Childrens); - return ret.GetStream(prov); - } - } - - private MediaContainer GetGroupsOrSubFiltersFromFilter(IProvider prov, int userid, string GroupFilterId, - BreadCrumbs info, bool nocast) - { - //List<Joint> retGroups = new List<Joint>(); - try - { - int.TryParse(GroupFilterId, out var groupFilterID); - using (var session = DatabaseFactory.SessionFactory.OpenSession()) - { - var retGroups = new List<Video>(); - if (groupFilterID == -1) - { - return new MediaContainer { ErrorString = "Invalid Group Filter" }; - } - - var start = DateTime.Now; - - SVR_GroupFilter gf; - gf = RepoFactory.GroupFilter.GetByID(groupFilterID); - if (gf == null) - { - return new MediaContainer { ErrorString = "Invalid Group Filter" }; - } - - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Show, gf.GroupFilterName, false, true, - info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); - } - - var allGfs = - RepoFactory.GroupFilter.GetByParentID(groupFilterID) - .Where(a => !a.IsHidden && - ( - (a.GroupsIds.ContainsKey(userid) && a.GroupsIds[userid].Count > 0) - || a.IsDirectory) - ) - .ToList(); - var dirs = new List<Shoko.Models.PlexAndKodi.Directory>(); - foreach (var gg in allGfs) - { - var pp = Helper.DirectoryFromFilter(prov, gg, userid); - if (pp != null) - { - dirs.Add(prov, pp, info); - } - } - - if (dirs.Count > 0) - { - ret.Childrens = dirs.OrderBy(a => a.Title).Cast<Video>().ToList(); - return ret.GetStream(prov); - } - - var order = new Dictionary<CL_AnimeGroup_User, Video>(); - if (gf.GroupsIds.ContainsKey(userid)) - { - // NOTE: The ToList() in the below foreach is required to prevent enumerable was modified exception - foreach (var grp in gf.GroupsIds[userid] - .ToList() - .Select(a => RepoFactory.AnimeGroup.GetByID(a)) - .Where(a => a != null)) - { - Video v = grp.GetPlexContract(userid)?.Clone<Shoko.Models.PlexAndKodi.Directory>(prov); - if (v != null) - { - if (v.Group == null) - { - v.Group = grp.GetUserContract(userid); - } - - v.GenerateKey(prov, userid); - v.Type = "show"; - v.Art = Helper.GetRandomFanartFromVideo(v, prov) ?? v.Art; - v.Banner = Helper.GetRandomBannerFromVideo(v, prov) ?? v.Banner; - if (nocast) - { - v.Roles = null; - } - - order.Add(v.Group, v); - retGroups.Add(prov, v, info); - v.ParentThumb = v.GrandparentThumb = null; - } - } - } - - ret.MediaContainer.RandomizeArt(prov, retGroups); - var grps = retGroups.Select(a => a.Group); - grps = gf.SortCriteriaList.Count != 0 - ? GroupFilterHelper.Sort(grps, gf) - : grps.OrderBy(a => a.GroupName); - ret.Childrens = grps.Select(a => order[a]).ToList(); - FilterExtras(prov, ret.Childrens); - return ret.GetStream(prov); - } - } - catch (Exception ex) - { - _logger.LogError(ex, ex.ToString()); - return new MediaContainer { ErrorString = "System Error, see JMMServer logs for more information" }; - } - } - - public void UseDirectories(int userId, List<Directory> directories) - { - var settings = _settingsProvider.GetSettings(); - if (directories == null) - { - settings.Plex.Libraries = new List<int>(); - return; - } - - settings.Plex.Libraries = directories.Select(s => s.Key).ToList(); - _settingsProvider.SaveSettings(); - } - - public Directory[] Directories(int userId) - { - return PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).GetDirectories(); - } - - public void UseDevice(int userId, MediaDevice server) - { - PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).UseServer(server); - } - - public MediaDevice[] AvailableDevices(int userId) - { - return PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).GetPlexServers().ToArray(); - } - - public MediaDevice CurrentDevice(int userId) - { - return PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).ServerCache; - } -} diff --git a/Shoko.Server/PlexAndKodi/Helper.cs b/Shoko.Server/PlexAndKodi/Helper.cs index 3226b5aa2..932bedb3d 100644 --- a/Shoko.Server/PlexAndKodi/Helper.cs +++ b/Shoko.Server/PlexAndKodi/Helper.cs @@ -10,9 +10,6 @@ using Shoko.Models.Client; using Shoko.Models.Enums; using Shoko.Models.PlexAndKodi; -using Shoko.Models.Server; -using Shoko.Server.Extensions; -using Shoko.Server.ImageDownload; using Shoko.Server.Models; using Shoko.Server.Repositories; using Shoko.Server.Server; @@ -127,36 +124,6 @@ public static string Base64EncodeUrl(string plainText) return Convert.ToBase64String(plainTextBytes).Replace("+", "-").Replace("/", "_").Replace("=", ","); } - public static string Base64DecodeUrl(string url) - { - var data = Convert.FromBase64String(url.Replace("-", "+").Replace("_", "/").Replace(",", "=")); - return Encoding.UTF8.GetString(data); - } - - public static SVR_JMMUser GetUser(string userid) - { - var allusers = RepoFactory.JMMUser.GetAll(); - foreach (var n in allusers) - { - if (userid.FindIn(n.GetPlexUsers())) - { - return n; - } - } - - return allusers.FirstOrDefault(a => a.IsAdmin == 1) ?? - allusers.FirstOrDefault(a => a.Username == "Default") ?? allusers.First(); - } - - public static SVR_JMMUser GetJMMUser(string userid) - { - var allusers = RepoFactory.JMMUser.GetAll(); - int.TryParse(userid, out var id); - return allusers.FirstOrDefault(a => a.JMMUserID == id) ?? - allusers.FirstOrDefault(a => a.IsAdmin == 1) ?? - allusers.FirstOrDefault(a => a.Username == "Default") ?? allusers.First(); - } - public static void AddLinksToAnimeEpisodeVideo(IProvider prov, Video v, int userid) { @@ -198,105 +165,6 @@ public static void AddLinksToAnimeEpisodeVideo(IProvider prov, Video v, int user } } - public static Video VideoFromVideoLocal(IProvider prov, SVR_VideoLocal v, int userid) - { - var l = new Video - { - AnimeType = AnimeTypes.AnimeFile.ToString(), - Id = v.VideoLocalID, - Type = "episode", - Summary = "Episode Overview Not Available", //TODO Internationalization - Title = Path.GetFileNameWithoutExtension(v.FileName), - AddedAt = v.DateTimeCreated.ToUnixTime(), - UpdatedAt = v.DateTimeUpdated.ToUnixTime(), - OriginallyAvailableAt = v.DateTimeCreated.ToPlexDate(), - Year = v.DateTimeCreated.Year, - Medias = new List<Media>() - }; - var vlr = v.GetUserRecord(userid); - if (vlr?.WatchedDate != null) - { - l.LastViewedAt = vlr.WatchedDate.Value.ToUnixTime(); - } - - if (vlr?.ResumePosition > 0) - { - l.ViewOffset = vlr.ResumePosition; - } - - if (v.Media != null) - { - var m = new Media(v.VideoLocalID, v.Media); - l.Medias.Add(m); - l.Duration = m.Duration; - } - - AddLinksToAnimeEpisodeVideo(prov, l, userid); - return l; - } - - - public static Video VideoFromAnimeEpisode(IProvider prov, List<CrossRef_AniDB_TvDBV2> cross, - KeyValuePair<SVR_AnimeEpisode, CL_AnimeEpisode_User> e, int userid) - { - var v = GenerateVideoFromAnimeEpisode(e.Key, e.Value.JMMUserID); - if (v?.Thumb != null) - { - v.Thumb = prov.ReplaceSchemeHost(v.Thumb); - } - - if (v != null) - { - if (e.Key.AniDB_Episode == null) - { - return v; - } - - if (e.Value != null) - { - v.ViewCount = e.Value.WatchedCount; - if (e.Value.WatchedDate.HasValue) - { - v.LastViewedAt = e.Value.WatchedDate.Value.ToUnixTime(); - } - } - - v.ParentIndex = 1; - if (e.Key.EpisodeTypeEnum != EpisodeType.Episode) - { - v.ParentIndex = 0; - } - - if (e.Key.EpisodeTypeEnum == EpisodeType.Episode) - { - var client = prov.GetPlexClient().Product; - if (client == "Plex for Windows" || client == "Plex Home Theater") - { - v.Title = $"{v.EpisodeNumber}. {v.Title}"; - } - } - - if (cross != null && cross.Count > 0) - { - var c2 = - cross.FirstOrDefault( - a => - a.AniDBStartEpisodeType == v.EpisodeType && - a.AniDBStartEpisodeNumber <= v.EpisodeNumber); - if (c2?.TvDBSeasonNumber > 0) - { - v.ParentIndex = c2.TvDBSeasonNumber; - } - } - - AddLinksToAnimeEpisodeVideo(prov, v, userid); - } - - v.AddResumePosition(prov, userid); - - return v; - } - private static readonly Regex UrlSafe = new("[ \\$^`:<>\\[\\]\\{\\}\"“\\+%@/;=\\?\\\\\\^\\|~‘,]", RegexOptions.Compiled); @@ -428,102 +296,6 @@ public static Video GenerateVideoFromAnimeEpisode(SVR_AnimeEpisode ep, int userI return l; } - private static void GetValidVideoRecursive(IProvider prov, SVR_GroupFilter f, int userid, Directory pp) - { - var gfs = RepoFactory.GroupFilter.GetByParentID(f.GroupFilterID) - .Where(a => a.GroupsIds.ContainsKey(userid) && a.GroupsIds[userid].Count > 0) - .ToList(); - - foreach (var gg in gfs.Where(a => !a.IsDirectory)) - { - if (gg.GroupsIds.ContainsKey(userid)) - { - var groups = gg.GroupsIds[userid]; - if (groups.Count != 0) - { - foreach (var grp in groups.Randomize(f.GroupFilterID)) - { - var ag = RepoFactory.AnimeGroup.GetByID(grp); - var v = ag.GetPlexContract(userid); - if (v?.Art == null || v.Thumb == null) - { - continue; - } - - pp.Art = prov.ReplaceSchemeHost(v.Art); - pp.Thumb = prov.ReplaceSchemeHost(v.Thumb); - break; - } - } - } - - if (pp.Art != null) - { - break; - } - } - - if (pp.Art == null) - { - foreach (var gg in gfs - .Where(a => a.IsDirectory && !a.IsHidden) - .Randomize(f.GroupFilterID)) - { - GetValidVideoRecursive(prov, gg, userid, pp); - if (pp.Art != null) - { - break; - } - } - } - - pp.LeafCount = gfs.Count; - pp.ViewedLeafCount = 0; - } - - public static Directory DirectoryFromFilter(IProvider prov, SVR_GroupFilter gg, - int userid) - { - var pp = new Directory { Type = "show" }; - pp.Key = prov.ConstructFilterIdUrl(userid, gg.GroupFilterID); - pp.Title = gg.GroupFilterName; - pp.Id = gg.GroupFilterID; - pp.AnimeType = AnimeTypes.AnimeGroupFilter.ToString(); - if (gg.IsDirectory) - { - GetValidVideoRecursive(prov, gg, userid, pp); - } - else if (gg.GroupsIds.ContainsKey(userid)) - { - var groups = gg.GroupsIds[userid]; - if (groups.Count == 0) - { - return pp; - } - - pp.LeafCount = groups.Count; - pp.ViewedLeafCount = 0; - foreach (var grp in groups.Randomize()) - { - var ag = RepoFactory.AnimeGroup.GetByID(grp); - var v = ag.GetPlexContract(userid); - if (v?.Art == null || v.Thumb == null) - { - continue; - } - - pp.Art = prov.ReplaceSchemeHost(v.Art); - pp.Thumb = prov.ReplaceSchemeHost(v.Thumb); - break; - } - - return pp; - } - - return pp; - } - - public static void AddInformationFromMasterSeries(Video v, CL_AnimeSeries_User cserie, Video nv, bool omitExtraData = false) { @@ -609,38 +381,6 @@ public static IEnumerable<T> Randomize<T>(this IEnumerable<T> source, int seed = return source.OrderBy(item => rnd.Next()); } - public static string GetRandomFanartFromVideo(Video v, IProvider prov) - { - return GetRandomArtFromList(v.Fanarts, prov); - } - - public static string GetRandomBannerFromVideo(Video v, IProvider prov) - { - return GetRandomArtFromList(v.Banners, prov); - } - - public static string GetRandomArtFromList(List<Contract_ImageDetails> list, IProvider prov) - { - if (list == null || list.Count == 0) - { - return null; - } - - Contract_ImageDetails art; - if (list.Count == 1) - { - art = list[0]; - } - else - { - var rand = new Random(); - art = list[rand.Next(0, list.Count)]; - } - - var details = new ImageDetails { ImageID = art.ImageID, ImageType = (ImageEntityType)art.ImageType }; - return details.GenArt(prov); - } - public static Video GenerateFromAnimeGroup(SVR_AnimeGroup grp, int userid, List<SVR_AnimeSeries> allSeries) { var cgrp = grp.GetUserContract(userid); @@ -760,29 +500,6 @@ public static Video GenerateFromAnimeGroup(SVR_AnimeGroup grp, int userid, List< } } - - public static List<Video> ConvertToDirectory(List<Video> n, IProvider prov) - { - var ks = new List<Video>(); - foreach (var n1 in n) - { - Video m; - if (n1 is Directory) - { - m = n1; - } - else - { - m = n1.Clone<Directory>(prov); - } - - m.ParentThumb = m.GrandparentThumb = null; - ks.Add(m); - } - - return ks; - } - public static Video MayReplaceVideo(Video v1, SVR_AnimeSeries ser, CL_AnimeSeries_User cserie, int userid, bool all = true, Video serie = null) { diff --git a/Shoko.Server/PlexAndKodi/Kodi/KodiProvider.cs b/Shoko.Server/PlexAndKodi/Kodi/KodiProvider.cs deleted file mode 100644 index 2dbd4dcc4..000000000 --- a/Shoko.Server/PlexAndKodi/Kodi/KodiProvider.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using Microsoft.AspNetCore.Http; -using Shoko.Models.PlexAndKodi; -using Shoko.Server.Server; -using Shoko.Server.Utilities; - -namespace Shoko.Server.PlexAndKodi.Kodi; - -public class KodiProvider : IProvider -{ - //public const string MediaTagVersion = "1420942002"; - - public string ServiceAddress => ShokoServer.PathAddressKodi; - public int ServicePort => Utils.SettingsProvider.GetSettings().ServerPort; - public bool UseBreadCrumbs => false; // turn off breadcrumbs navigation (plex) - public bool ConstructFakeIosParent => false; //turn off plex workaround for ios (plex) - public bool AutoWatch => false; //turn off marking watched on stream side (plex) - - public bool EnableRolesInLists { get; } = true; - public bool EnableAnimeTitlesInLists { get; } = true; - public bool EnableGenresInLists { get; } = true; - - - public string ExcludeTags => "Plex"; - - public string Proxyfy(string url) - { - return url; - } - - public string ShortUrl(string url) - { - // Faster and More accurate than regex - try - { - var uri = new Uri(url); - return uri.PathAndQuery; - } - catch - { - // if this fails, then there is a problem - return url; - } - } - - public MediaContainer NewMediaContainer(MediaContainerTypes type, string title = null, bool allowsync = false, - bool nocache = false, BreadCrumbs info = null) - { - var m = new MediaContainer(); - m.Title1 = m.Title2 = title; - // not needed - //m.AllowSync = allowsync ? "1" : "0"; - //m.NoCache = nocache ? "1" : "0"; - //m.ViewMode = "65592"; - //m.ViewGroup = "show"; - //m.MediaTagVersion = MediaTagVersion; - m.Identifier = "plugin.video.nakamori"; - return m; - } - - public bool AddPlexSearchItem { get; } = false; - public bool AddPlexPrefsItem { get; } = false; - public bool RemoveFileAttribute { get; } = false; - public bool AddEpisodeNumberToTitlesOnUnsupportedClients { get; } = false; - public HttpContext HttpContext { get; set; } -} diff --git a/Shoko.Server/Server/ShokoServer.cs b/Shoko.Server/Server/ShokoServer.cs index d56299756..580581f3d 100644 --- a/Shoko.Server/Server/ShokoServer.cs +++ b/Shoko.Server/Server/ShokoServer.cs @@ -545,7 +545,6 @@ private void ShutDown() private static void AutoUpdateTimer_Elapsed(object sender, ElapsedEventArgs e) { - Importer.CheckForDayFilters(); Importer.CheckForCalendarUpdate(false); Importer.CheckForAnimeUpdate(false); Importer.CheckForTvDBUpdates(false); diff --git a/Shoko.Server/Server/Startup.cs b/Shoko.Server/Server/Startup.cs index 69876c0f4..079c92c95 100644 --- a/Shoko.Server/Server/Startup.cs +++ b/Shoko.Server/Server/Startup.cs @@ -56,7 +56,6 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton<MovieDBHelper>(); services.AddSingleton<FilterEvaluator>(); services.AddSingleton<LegacyFilterConverter>(); - services.AddScoped<CommonImplementation>(); services.AddSingleton<IShokoEventHandler>(ShokoEventHandler.Instance); services.AddSingleton<ICommandRequestFactory, CommandRequestFactory>(); services.AddSingleton<IConnectivityMonitor, CloudFlareConnectivityMonitor>(); From 0070aea96ea8846f9d03c07553fdc8e9b736a97a Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Sat, 23 Sep 2023 13:00:41 -0400 Subject: [PATCH 22/34] Filters: Backwards conversion Remove Old Classes More Unit Tests --- .../ShokoServiceImplementation.cs | 18 +- .../ShokoServiceImplementation_Entities.cs | 310 ++- Shoko.Server/API/v2/APIV2Helper.cs | 179 -- .../Files/HasAudioLanguageExpression.cs | 2 +- .../Files/HasSharedAudioLanguageExpression.cs | 2 +- .../HasSharedSubtitleLanguageExpression.cs | 2 +- .../Files/HasSharedVideoSourceExpression.cs | 2 +- .../Files/HasSubtitleLanguageExpression.cs | 2 +- .../Filters/Files/HasVideoSourceExpression.cs | 2 +- Shoko.Server/Filters/FilterEvaluator.cs | 28 +- Shoko.Server/Filters/FilterExpression.cs | 7 +- Shoko.Server/Filters/Filterable.cs | 282 ++- .../Filters/Functions/DateAddFunction.cs | 7 +- .../Filters/Functions/DateDiffFunction.cs | 7 +- .../Filters/Functions/TodayFunction.cs | 3 +- .../Filters/Info/HasAnimeTypeExpression.cs | 2 +- .../Filters/Info/HasCustomTagExpression.cs | 2 +- .../HasMissingEpisodesCollectingExpression.cs | 4 +- .../Info/HasMissingEpisodesExpression.cs | 4 +- .../Filters/Info/HasNameExpression.cs | 2 +- .../Filters/Info/HasTMDbLinkExpression.cs | 4 +- Shoko.Server/Filters/Info/HasTagExpression.cs | 2 +- .../Filters/Info/HasTraktLinkExpression.cs | 4 +- .../Filters/Info/HasTvDBLinkExpression.cs | 4 +- .../Filters/Info/InSeasonExpression.cs | 6 +- Shoko.Server/Filters/Info/InYearExpression.cs | 6 +- .../Filters/Info/IsFinishedExpression.cs | 4 +- .../Filters/Info/MissingTMDbLinkExpression.cs | 4 +- .../Info/MissingTraktLinkExpression.cs | 4 +- .../Filters/Info/MissingTvDBLinkExpression.cs | 4 +- .../Filters/Interfaces/IFilterExpression.cs | 4 +- .../Filters/Interfaces/IFilterable.cs | 163 ++ .../Interfaces/IUserDependentFilterable.cs | 56 + .../Interfaces/IWithNumberParameter.cs | 2 +- .../Legacy}/GroupFilterSortingCriteria.cs | 2 +- .../Legacy/LegacyConditionConverter.cs | 927 +++++++ .../Filters/Legacy/LegacyFilterConverter.cs | 175 ++ Shoko.Server/Filters/Legacy/LegacyMappings.cs | 472 ++++ Shoko.Server/Filters/LegacyFilterConverter.cs | 121 - Shoko.Server/Filters/LegacyMappings.cs | 287 --- Shoko.Server/Filters/Logic/AndExpression.cs | 7 +- .../Logic/DateTimes/DateEqualsExpression.cs | 7 +- .../DateGreaterThanEqualsExpression.cs | 7 +- .../DateTimes/DateGreaterThanExpression.cs | 7 +- .../DateTimes/DateLessThanEqualsExpression.cs | 7 +- .../Logic/DateTimes/DateLessThanExpression.cs | 7 +- .../DateTimes/DateNotEqualsExpression.cs | 7 +- Shoko.Server/Filters/Logic/NotExpression.cs | 7 +- .../Logic/Numbers/NumberEqualsExpression.cs | 11 +- .../NumberGreaterThanEqualsExpression.cs | 11 +- .../Numbers/NumberGreaterThanExpression.cs | 11 +- .../Numbers/NumberLessThanEqualsExpression.cs | 11 +- .../Logic/Numbers/NumberLessThanExpression.cs | 11 +- .../Numbers/NumberNotEqualsExpression.cs | 11 +- Shoko.Server/Filters/Logic/OrExpression.cs | 7 +- .../Logic/Strings/StringContainsExpression.cs | 7 +- .../Logic/Strings/StringEqualsExpression.cs | 7 +- .../Strings/StringNotEqualsExpression.cs | 7 +- Shoko.Server/Filters/Logic/XorExpression.cs | 7 +- .../Filters/Selectors/AddedDateSelector.cs | 3 +- .../Filters/Selectors/AirDateSelector.cs | 3 +- .../Selectors/AudioLanguageCountSelector.cs | 4 +- .../Filters/Selectors/EpisodeCountSelector.cs | 4 +- .../Selectors/HighestAniDBRatingSelector.cs | 3 +- .../Selectors/HighestUserRatingSelector.cs | 3 +- .../Selectors/LastAddedDateSelector.cs | 3 +- .../Filters/Selectors/LastAirDateSelector.cs | 3 +- .../Selectors/LastWatchedDateSelector.cs | 3 +- .../Selectors/LowestAniDBRatingSelector.cs | 3 +- .../Selectors/LowestUserRatingSelector.cs | 3 +- .../Filters/Selectors/SeriesCountSelector.cs | 4 +- .../SubtitleLanguageCountSelector.cs | 4 +- .../Selectors/TotalEpisodeCountSelector.cs | 4 +- .../Filters/Selectors/WatchedDateSelector.cs | 3 +- .../AddedDateSortingSelector.cs | 4 +- .../AirDateSortingSelector.cs | 3 +- .../AudioLanguageCountSortingSelector.cs | 4 +- .../EpisodeCountSortingSelector.cs | 4 +- .../HighestAniDBRatingSortingSelector.cs | 3 +- .../HighestUserRatingSortingSelector.cs | 3 +- .../LastAddedDateSortingSelector.cs | 4 +- .../LastAirDateSortingSelector.cs | 3 +- .../LastWatchedDateSortingSelector.cs | 3 +- .../LowestAniDBRatingSortingSelector.cs | 3 +- .../LowestUserRatingSortingSelector.cs | 3 +- ...ngEpisodeCollectingCountSortingSelector.cs | 4 +- .../MissingEpisodeCountSortingSelector.cs | 4 +- .../SortingSelectors/NameSortingSelector.cs | 4 +- .../SeriesCountSortingSelector.cs | 14 + .../SortingNameSortingSelector.cs | 4 +- .../SubtitleLanguageCountSortingSelector.cs | 4 +- .../TotalEpisodeCountSortingSelector.cs | 4 +- .../UnwatchedCountSortingSelector.cs | 14 + .../WatchedCountSortingSelector.cs | 14 + .../WatchedDateSortingSelector.cs | 3 +- .../User/HasPermanentUserVotesExpression.cs | 4 +- .../User/HasUnwatchedEpisodesExpression.cs | 4 +- .../Filters/User/HasUserVotesExpression.cs | 4 +- .../User/HasWatchedEpisodesExpression.cs | 4 +- .../Filters/User/IsFavoriteExpression.cs | 4 +- .../MissingPermanentUserVotesExpression.cs | 4 +- .../Filters/UserDependentFilterExpression.cs | 8 +- .../Filters/UserDependentFilterable.cs | 93 +- .../Filters/UserDependentSortingExpression.cs | 8 +- .../Mappings/GroupFilterConditionMap.cs | 18 - Shoko.Server/Mappings/GroupFilterMap.cs | 30 - Shoko.Server/Models/SVR_GroupFilter.cs | 2231 ----------------- .../Cached/GroupFilterRepository.cs | 418 --- Shoko.Server/Repositories/RepoFactory.cs | 3 - Shoko.Server/Server/Startup.cs | 2 +- Shoko.Server/Utilities/GroupFilterHelper.cs | 326 --- Shoko.Tests/Shoko.Tests/FilterTests.cs | 25 +- Shoko.Tests/Shoko.Tests/LegacyFilterTests.cs | 139 + Shoko.Tests/Shoko.Tests/TestFilterable.cs | 41 + .../TestUserDependentFilterable.cs | 18 + 115 files changed, 2737 insertions(+), 4078 deletions(-) create mode 100644 Shoko.Server/Filters/Interfaces/IFilterable.cs create mode 100644 Shoko.Server/Filters/Interfaces/IUserDependentFilterable.cs rename Shoko.Server/{Utilities => Filters/Legacy}/GroupFilterSortingCriteria.cs (93%) create mode 100644 Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs create mode 100644 Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs create mode 100644 Shoko.Server/Filters/Legacy/LegacyMappings.cs delete mode 100644 Shoko.Server/Filters/LegacyFilterConverter.cs delete mode 100644 Shoko.Server/Filters/LegacyMappings.cs create mode 100644 Shoko.Server/Filters/SortingSelectors/SeriesCountSortingSelector.cs create mode 100644 Shoko.Server/Filters/SortingSelectors/UnwatchedCountSortingSelector.cs create mode 100644 Shoko.Server/Filters/SortingSelectors/WatchedCountSortingSelector.cs delete mode 100644 Shoko.Server/Mappings/GroupFilterConditionMap.cs delete mode 100644 Shoko.Server/Mappings/GroupFilterMap.cs delete mode 100644 Shoko.Server/Models/SVR_GroupFilter.cs delete mode 100644 Shoko.Server/Repositories/Cached/GroupFilterRepository.cs delete mode 100644 Shoko.Server/Utilities/GroupFilterHelper.cs create mode 100644 Shoko.Tests/Shoko.Tests/LegacyFilterTests.cs create mode 100644 Shoko.Tests/Shoko.Tests/TestFilterable.cs create mode 100644 Shoko.Tests/Shoko.Tests/TestUserDependentFilterable.cs diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs index 23fd7154e..7b05bcd9b 100644 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs @@ -18,6 +18,8 @@ using Shoko.Server.Commands; using Shoko.Server.Commands.AniDB; using Shoko.Server.Extensions; +using Shoko.Server.Filters; +using Shoko.Server.Filters.Legacy; using Shoko.Server.Models; using Shoko.Server.Plex; using Shoko.Server.Providers.AniDB.Interfaces; @@ -175,16 +177,17 @@ public CL_MainChanges GetAllChanges(DateTime date, int userID) var changes = ChangeTracker<int>.GetChainedChanges( new List<ChangeTracker<int>> { - RepoFactory.GroupFilter.GetChangeTracker(), + RepoFactory.FilterPreset.GetChangeTracker(), RepoFactory.AnimeGroup.GetChangeTracker(), RepoFactory.AnimeGroup_User.GetChangeTracker(userID), RepoFactory.AnimeSeries.GetChangeTracker(), RepoFactory.AnimeSeries_User.GetChangeTracker(userID) }, date); + var legacyConverter = HttpContext.RequestServices.GetRequiredService<LegacyFilterConverter>(); c.Filters = new CL_Changes<CL_GroupFilter> { - ChangedItems = changes[0] - .ChangedItems.Select(a => RepoFactory.GroupFilter.GetByID(a)?.ToClient()) + ChangedItems = legacyConverter.ToClient(changes[0].ChangedItems.Select(a => RepoFactory.FilterPreset.GetByID(a)).ToList()) + .Select(a => a.Value) .Where(a => a != null) .ToList(), RemovedItems = changes[0].RemovedItems.ToList(), @@ -203,8 +206,7 @@ public CL_MainChanges GetAllChanges(DateTime date, int userID) if (!c.Filters.ChangedItems.Any(a => a.GroupFilterID == ag.ParentGroupFilterID.Value)) { end = false; - var cag = RepoFactory.GroupFilter.GetByID(ag.ParentGroupFilterID.Value)? - .ToClient(); + var cag = legacyConverter.ToClient(RepoFactory.FilterPreset.GetByID(ag.ParentGroupFilterID.Value)); if (cag != null) { c.Filters.ChangedItems.Add(cag); @@ -270,8 +272,10 @@ public CL_Changes<CL_GroupFilter> GetGroupFilterChanges(DateTime date) var c = new CL_Changes<CL_GroupFilter>(); try { - var changes = RepoFactory.GroupFilter.GetChangeTracker().GetChanges(date); - c.ChangedItems = changes.ChangedItems.Select(a => RepoFactory.GroupFilter.GetByID(a).ToClient()) + var legacyConverter = HttpContext.RequestServices.GetRequiredService<LegacyFilterConverter>(); + var changes = RepoFactory.FilterPreset.GetChangeTracker().GetChanges(date); + c.ChangedItems = legacyConverter.ToClient(changes.ChangedItems.Select(a => RepoFactory.FilterPreset.GetByID(a)).ToList()) + .Select(a => a.Value) .Where(a => a != null) .ToList(); c.RemovedItems = changes.RemovedItems.ToList(); diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs index b0e2ac3e5..24d66cdef 100755 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Shoko.Commons.Extensions; using Shoko.Models.Client; using Shoko.Models.Enums; @@ -10,6 +11,8 @@ using Shoko.Server.Commands; using Shoko.Server.Commands.AniDB; using Shoko.Server.Extensions; +using Shoko.Server.Filters; +using Shoko.Server.Filters.Legacy; using Shoko.Server.Models; using Shoko.Server.Repositories; using Shoko.Server.Tasks; @@ -159,40 +162,17 @@ public List<CL_AnimeEpisode_User> GetContinueWatchingFilter(int userID, int maxR try { var user = RepoFactory.JMMUser.GetByID(userID); - if (user == null) - { - return retEps; - } + if (user == null) return retEps; // find the locked Continue Watching Filter - SVR_GroupFilter gf = null; - var lockedGFs = RepoFactory.GroupFilter.GetLockedGroupFilters(); - if (lockedGFs != null) - { - // if it already exists we can leave - foreach (var gfTemp in lockedGFs) - { - if (gfTemp.FilterType == (int)GroupFilterType.ContinueWatching) - { - gf = gfTemp; - break; - } - } - } + var lockedGFs = RepoFactory.FilterPreset.GetLockedGroupFilters(); + var gf = lockedGFs?.FirstOrDefault(a => a.FilterType == GroupFilterType.ContinueWatching); + if (gf == null) return retEps; - if (gf == null || !gf.GroupsIds.ContainsKey(userID)) - { - return retEps; - } - - var comboGroups = gf.GroupsIds[userID].Select(a => RepoFactory.AnimeGroup.GetByID(a)).Where(a => a != null) + var evaluator = HttpContext.RequestServices.GetRequiredService<FilterEvaluator>(); + var comboGroups = evaluator.EvaluateFilter(gf, userID).Select(a => RepoFactory.AnimeGroup.GetByID(a.Key)).Where(a => a != null) .Select(a => a.GetUserContract(userID)); - - // apply sorting - comboGroups = GroupFilterHelper.Sort(comboGroups, gf); - - foreach (var grp in comboGroups) { var sers = RepoFactory.AnimeSeries.GetByGroupID(grp.AnimeGroupID).OrderBy(a => a.AirDate).ToList(); @@ -201,51 +181,23 @@ public List<CL_AnimeEpisode_User> GetContinueWatchingFilter(int userID, int maxR foreach (var ser in sers) { - if (!user.AllowedSeries(ser)) - { - continue; - } - - var useSeries = true; - - if (seriesWatching.Count > 0) - { - if (ser.GetAnime().AnimeType == (int)AnimeType.TVSeries) - { - // make sure this series is not a sequel to an existing series we have already added - foreach (AniDB_Anime_Relation rel in ser.GetAnime().GetRelatedAnime()) - { - if (rel.RelationType.ToLower().Trim().Equals("sequel") || - rel.RelationType.ToLower().Trim().Equals("prequel")) - { - useSeries = false; - } - } - } - } + if (!user.AllowedSeries(ser)) continue; - if (!useSeries) - { - continue; - } + var anime = ser.GetAnime(); + var useSeries = seriesWatching.Count > 0 && anime.AnimeType == (int)AnimeType.TVSeries && !anime.GetRelatedAnime().Any(a => + a.RelationType.ToLower().Trim().Equals("sequel") || a.RelationType.ToLower().Trim().Equals("prequel")); + if (!useSeries) continue; var ep = GetNextUnwatchedEpisode(ser.AnimeSeriesID, userID); - if (ep != null) - { - retEps.Add(ep); + if (ep == null) continue; - // Lets only return the specified amount - if (retEps.Count == maxRecords) - { - return retEps; - } + retEps.Add(ep); - if (ser.GetAnime().AnimeType == (int)AnimeType.TVSeries) - { - seriesWatching.Add(ser.AniDB_ID); - } - } + // Lets only return the specified amount + if (retEps.Count == maxRecords) return retEps; + + if (anime.AnimeType == (int)AnimeType.TVSeries) seriesWatching.Add(ser.AniDB_ID); } } } @@ -2084,48 +2036,39 @@ public List<CL_AnimeGroup_User> GetAnimeGroupsForFilter(int groupFilterID, int u try { var user = RepoFactory.JMMUser.GetByID(userID); - if (user == null) - { - return retGroups; - } + if (user == null) return retGroups; + + var gf = RepoFactory.FilterPreset.GetByID(groupFilterID); - SVR_GroupFilter gf; - gf = RepoFactory.GroupFilter.GetByID(groupFilterID); - if (gf != null && gf.GroupsIds.ContainsKey(userID)) + if (gf != null) { - retGroups = gf.GroupsIds[userID].Select(a => RepoFactory.AnimeGroup.GetByID(a)) - .Where(a => a != null).Select(a => a.GetUserContract(userID)).ToList(); + var evaluator = HttpContext.RequestServices.GetRequiredService<FilterEvaluator>(); + var results = evaluator.EvaluateFilter(gf, userID); + retGroups = results.Select(a => RepoFactory.AnimeGroup.GetByID(a.Key)).Where(a => a != null).Select(a => a.GetUserContract(userID)).ToList(); } - if (getSingleSeriesGroups) + if (!getSingleSeriesGroups) return retGroups; + + var nGroups = new List<CL_AnimeGroup_User>(); + foreach (var cag in retGroups) { - var nGroups = new List<CL_AnimeGroup_User>(); - foreach (var cag in retGroups) + var ng = cag.DeepCopy(); + if (cag.Stat_SeriesCount == 1) { - var ng = cag.DeepCopy(); - if (cag.Stat_SeriesCount == 1) + if (cag.DefaultAnimeSeriesID.HasValue) { - if (cag.DefaultAnimeSeriesID.HasValue) - { - ng.SeriesForNameOverride = RepoFactory.AnimeSeries.GetByGroupID(ng.AnimeGroupID) - .FirstOrDefault(a => a.AnimeSeriesID == cag.DefaultAnimeSeriesID.Value) - ?.GetUserContract(userID); - } - - if (ng.SeriesForNameOverride == null) - { - ng.SeriesForNameOverride = RepoFactory.AnimeSeries.GetByGroupID(ng.AnimeGroupID) - .FirstOrDefault()?.GetUserContract(userID); - } + ng.SeriesForNameOverride = RepoFactory.AnimeSeries.GetByGroupID(ng.AnimeGroupID) + .FirstOrDefault(a => a.AnimeSeriesID == cag.DefaultAnimeSeriesID.Value) + ?.GetUserContract(userID); } - nGroups.Add(ng); + ng.SeriesForNameOverride ??= RepoFactory.AnimeSeries.GetByGroupID(ng.AnimeGroupID).FirstOrDefault()?.GetUserContract(userID); } - retGroups = nGroups; + nGroups.Add(ng); } - return retGroups; + return nGroups; } catch (Exception ex) { @@ -3091,12 +3034,11 @@ public CL_Response<CL_GroupFilter> SaveGroupFilter(CL_GroupFilter contract) { var response = new CL_Response<CL_GroupFilter> { ErrorMessage = string.Empty, Result = null }; - // Process the group - SVR_GroupFilter gf; + FilterPreset gf = null; if (contract.GroupFilterID != 0) { - gf = RepoFactory.GroupFilter.GetByID(contract.GroupFilterID); + gf = RepoFactory.FilterPreset.GetByID(contract.GroupFilterID); if (gf == null) { response.ErrorMessage = "Could not find existing Group Filter with ID: " + @@ -3105,11 +3047,24 @@ public CL_Response<CL_GroupFilter> SaveGroupFilter(CL_GroupFilter contract) } } - gf = SVR_GroupFilter.FromClient(contract); + var legacyConverter = HttpContext.RequestServices.GetRequiredService<LegacyFilterConverter>(); + var newFilter = legacyConverter.FromClient(contract); + if (gf == null) + { + gf = newFilter; + } + else + { + gf.Name = newFilter.Name; + gf.Hidden = newFilter.Hidden; + gf.ApplyAtSeriesLevel = newFilter.ApplyAtSeriesLevel; + gf.Expression = newFilter.Expression; + gf.SortingExpression = newFilter.SortingExpression; + } - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - response.Result = gf.ToClient(); + RepoFactory.FilterPreset.Save(gf); + + response.Result = legacyConverter.ToClient(gf); return response; } @@ -3118,13 +3073,10 @@ public string DeleteGroupFilter(int groupFilterID) { try { - var gf = RepoFactory.GroupFilter.GetByID(groupFilterID); - if (gf == null) - { - return "Group Filter not found"; - } + var gf = RepoFactory.FilterPreset.GetByID(groupFilterID); + if (gf == null) return "Group Filter not found"; - RepoFactory.GroupFilter.Delete(groupFilterID); + RepoFactory.FilterPreset.Delete(groupFilterID); return string.Empty; } @@ -3140,7 +3092,7 @@ public CL_GroupFilterExtended GetGroupFilterExtended(int groupFilterID, int user { try { - var gf = RepoFactory.GroupFilter.GetByID(groupFilterID); + var gf = RepoFactory.FilterPreset.GetByID(groupFilterID); if (gf == null) { return null; @@ -3152,9 +3104,14 @@ public CL_GroupFilterExtended GetGroupFilterExtended(int groupFilterID, int user return null; } - var contract = gf.ToClientExtended(user); - - return contract; + var legacyConverter = HttpContext.RequestServices.GetRequiredService<LegacyFilterConverter>(); + var model = legacyConverter.ToClient(gf); + return new CL_GroupFilterExtended + { + GroupFilter = model, + GroupCount = model.Groups[userID].Count, + SeriesCount = model.Series[userID].Count + }; } catch (Exception ex) { @@ -3176,21 +3133,12 @@ public List<CL_GroupFilterExtended> GetAllGroupFiltersExtended(int userID) return gfs; } - var allGfs = RepoFactory.GroupFilter.GetAll(); - foreach (var gf in allGfs) + var allGfs = RepoFactory.FilterPreset.GetAll(); + var legacyConverter = HttpContext.RequestServices.GetRequiredService<LegacyFilterConverter>(); + gfs = legacyConverter.ToClient(allGfs).Select(a => new CL_GroupFilterExtended { - var gfContract = gf.ToClient(); - var gfeContract = new CL_GroupFilterExtended - { - GroupFilter = gfContract, GroupCount = 0, SeriesCount = 0 - }; - if (gf.GroupsIds.ContainsKey(user.JMMUserID)) - { - gfeContract.GroupCount = gf.GroupsIds.Count; - } - - gfs.Add(gfeContract); - } + GroupFilter = a.Value, GroupCount = a.Value.Groups[userID].Count + }).ToList(); } catch (Exception ex) { @@ -3213,22 +3161,13 @@ public List<CL_GroupFilterExtended> GetGroupFiltersExtended(int userID, int gfpa } var allGfs = gfparentid == 0 - ? RepoFactory.GroupFilter.GetTopLevel() - : RepoFactory.GroupFilter.GetByParentID(gfparentid); - foreach (var gf in allGfs) + ? RepoFactory.FilterPreset.GetTopLevel() + : RepoFactory.FilterPreset.GetByParentID(gfparentid); + var legacyConverter = HttpContext.RequestServices.GetRequiredService<LegacyFilterConverter>(); + gfs = legacyConverter.ToClient(allGfs).Select(a => new CL_GroupFilterExtended { - var gfContract = gf.ToClient(); - var gfeContract = new CL_GroupFilterExtended - { - GroupFilter = gfContract, GroupCount = 0, SeriesCount = 0 - }; - if (gf.GroupsIds.ContainsKey(user.JMMUserID)) - { - gfeContract.GroupCount = gf.GroupsIds.Count; - } - - gfs.Add(gfeContract); - } + GroupFilter = a.Value, GroupCount = a.Value.Groups.FirstOrDefault().Value.Count + }).ToList(); } catch (Exception ex) { @@ -3246,15 +3185,15 @@ public List<CL_GroupFilter> GetAllGroupFilters() { var start = DateTime.Now; - var allGfs = RepoFactory.GroupFilter.GetAll(); + var allGfs = RepoFactory.FilterPreset.GetAll(); var ts = DateTime.Now - start; logger.Info("GetAllGroupFilters (Database) in {0} ms", ts.TotalMilliseconds); - start = DateTime.Now; - foreach (var gf in allGfs) - { - gfs.Add(gf.ToClient()); - } + var legacyConverter = HttpContext.RequestServices.GetRequiredService<LegacyFilterConverter>(); + gfs = legacyConverter.ToClient(allGfs) + .Select(a => a.Value) + .Where(a => a != null) + .ToList(); } catch (Exception ex) { @@ -3272,17 +3211,14 @@ public List<CL_GroupFilter> GetGroupFilters(int gfparentid = 0) { var start = DateTime.Now; - var allGfs = gfparentid == 0 - ? RepoFactory.GroupFilter.GetTopLevel() - : RepoFactory.GroupFilter.GetByParentID(gfparentid); + var allGfs = gfparentid == 0 ? RepoFactory.FilterPreset.GetTopLevel() : RepoFactory.FilterPreset.GetByParentID(gfparentid); var ts = DateTime.Now - start; logger.Info("GetAllGroupFilters (Database) in {0} ms", ts.TotalMilliseconds); - - start = DateTime.Now; - foreach (var gf in allGfs) - { - gfs.Add(gf.ToClient()); - } + var legacyConverter = HttpContext.RequestServices.GetRequiredService<LegacyFilterConverter>(); + gfs = legacyConverter.ToClient(allGfs) + .Select(a => a.Value) + .Where(a => a != null) + .ToList(); } catch (Exception ex) { @@ -3297,7 +3233,8 @@ public CL_GroupFilter GetGroupFilter(int gf) { try { - return RepoFactory.GroupFilter.GetByID(gf)?.ToClient(); + var legacyConverter = HttpContext.RequestServices.GetRequiredService<LegacyFilterConverter>(); + return legacyConverter.ToClient(RepoFactory.FilterPreset.GetByID(gf)); } catch (Exception ex) { @@ -3312,7 +3249,60 @@ public CL_GroupFilter EvaluateGroupFilter(CL_GroupFilter contract) { try { - return SVR_GroupFilter.EvaluateContract(contract); + var legacyConverter = HttpContext.RequestServices.GetRequiredService<LegacyFilterConverter>(); + var expression = LegacyConditionConverter.GetExpression(contract); + + var filter = new FilterPreset + { + Expression = expression, + ApplyAtSeriesLevel = contract.ApplyToSeries == 1, + Name = contract.GroupFilterName, + SortingExpression = LegacyConditionConverter.GetSortingExpression(contract.SortingCriteria) + }; + + var evaluator = HttpContext.RequestServices.GetRequiredService<FilterEvaluator>(); + var groupIds = new Dictionary<int, HashSet<int>>(); + var seriesIds = new Dictionary<int, HashSet<int>>(); + if ((filter.Expression?.UserDependent ?? false) || (filter.SortingExpression?.UserDependent ?? false)) + { + foreach (var userID in RepoFactory.JMMUser.GetAll().Select(a => a.JMMUserID)) + { + var results = evaluator.EvaluateFilter(filter, userID).ToList(); + groupIds[userID] = results.Select(a => a.Key).ToHashSet(); + seriesIds[userID] = results.SelectMany(a => a).ToHashSet(); + } + } + else + { + var results = evaluator.EvaluateFilter(filter, null).ToList(); + var groupIdSet = results.Select(a => a.Key).ToHashSet(); + var seriesIdSet = results.SelectMany(a => a).ToHashSet(); + foreach (var userID in RepoFactory.JMMUser.GetAll().Select(a => a.JMMUserID)) + { + groupIds[userID] = groupIdSet; + seriesIds[userID] = seriesIdSet; + } + } + + var model = new CL_GroupFilter + { + GroupFilterID = filter.FilterPresetID, + GroupFilterName = filter.Name, + ApplyToSeries = filter.ApplyAtSeriesLevel ? 1 : 0, + Locked = filter.Locked ? 1 : 0, + FilterType = (int)filter.FilterType, + ParentGroupFilterID = filter.ParentFilterPresetID, + InvisibleInClients = filter.Hidden ? 1 : 0, + BaseCondition = 0, + FilterConditions = null, + SortingCriteria = LegacyConditionConverter.GetSortingCriteria(filter), + Groups = groupIds, + Series = seriesIds, + Childs = filter.FilterPresetID == 0 + ? new HashSet<int>() + : RepoFactory.FilterPreset.GetByParentID(filter.FilterPresetID).Select(a => a.FilterPresetID).ToHashSet() + }; + return model; } catch (Exception ex) { diff --git a/Shoko.Server/API/v2/APIV2Helper.cs b/Shoko.Server/API/v2/APIV2Helper.cs index 45cd90160..8fefe4356 100644 --- a/Shoko.Server/API/v2/APIV2Helper.cs +++ b/Shoko.Server/API/v2/APIV2Helper.cs @@ -1,41 +1,14 @@ -using System; -using System.Net; using Microsoft.AspNetCore.Http; -using Shoko.Models.PlexAndKodi; -using Shoko.Server.API.v2.Models.common; -using Shoko.Server.Models; -using Shoko.Server.PlexAndKodi; -using Shoko.Server.Repositories; namespace Shoko.Server.API.v2; public class APIV2Helper { - #region Contructors - public static string ConstructUnsortUrl(HttpContext ctx, bool short_url = false) { return APIHelper.ProperURL(ctx, "/api/file/unsort", short_url); } - [Obsolete] - public static string ConstructGroupIdUrl(HttpContext ctx, string gid, bool short_url = false) - { - return APIHelper.ProperURL(ctx, "__TEST__" + (int)JMMType.Group + "/" + gid, short_url); - } - - [Obsolete] - public static string ConstructSerieIdUrl(HttpContext ctx, string sid, bool short_url = false) - { - return APIHelper.ProperURL(ctx, "__TEST__" + (int)JMMType.Serie + " / " + sid, short_url); - } - - [Obsolete] - public static string ConstructVideoUrl(HttpContext ctx, string vid, JMMType type, bool short_url = false) - { - return APIHelper.ProperURL(ctx, "__TEST__" + (int)type + "/" + vid, short_url); - } - public static string ConstructFilterIdUrl(HttpContext ctx, int groupfilter_id, bool short_url = false) { return APIHelper.ProperURL(ctx, "/api/filter?id=" + groupfilter_id, short_url); @@ -46,39 +19,6 @@ public static string ConstructFilterUrl(HttpContext ctx, bool short_url = false) return APIHelper.ProperURL(ctx, "/api/filter", short_url); } - [Obsolete] - public static string ConstructFiltersUrl(HttpContext ctx, bool short_url = false) - { - return APIHelper.ProperURL(ctx, "__TEST__", short_url); - } - - [Obsolete] - public static string ConstructSearchUrl(HttpContext ctx, string limit, string query, bool searchTag, - bool short_url = false) - { - if (searchTag) - { - return APIHelper.ProperURL(ctx, "/api/searchTag/" + limit + "/" + WebUtility.UrlEncode(query), - short_url); - } - - return APIHelper.ProperURL(ctx, "/api/search/" + limit + "/" + WebUtility.UrlEncode(query), - short_url); - } - - [Obsolete] - public static string ConstructPlaylistUrl(HttpContext ctx, bool short_url = false) - { - return APIHelper.ProperURL(ctx, "/api/metadata/" + (int)JMMType.Playlist + "/0", short_url); - } - - [Obsolete] - public static string ConstructPlaylistIdUrl(HttpContext ctx, int pid, bool short_url = false) - { - return APIHelper.ProperURL(ctx, "/api/metadata/" + (int)JMMType.Playlist + "/" + pid, short_url); - } - - public static string ConstructVideoLocalStream(HttpContext ctx, int userid, string vid, string name, bool autowatch) { return APIHelper.ProperURL(ctx, "/Stream/" + vid + "/" + userid + "/" + autowatch + "/" + name); @@ -88,123 +28,4 @@ public static string ConstructSupportImageLink(HttpContext ctx, string name, boo { return APIHelper.ProperURL(ctx, "/api/v2/image/support/" + name, short_url); } - - public static string ConstructImageLinkFromRest(HttpContext ctx, string path, bool short_url = true) - { - return ConvertRestImageToNonRestUrl(ctx, path, short_url); - } - - private static string ConvertRestImageToNonRestUrl(HttpContext ctx, string url, bool short_url) - { - // Rest URLs should always end in either type/id or type/id/ratio - // Regardless of ',' or '.', ratio will not parse as int - if (string.IsNullOrEmpty(url)) - { - return null; - } - - var link = url.ToLower(); - var split = link.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - int type; - if (int.TryParse(split[split.Length - 1], out var id)) // no ratio - { - if (int.TryParse(split[split.Length - 2], out type)) - { - return APIHelper.ConstructImageLinkFromTypeAndId(ctx, type, id, short_url); - } - } - else if (int.TryParse(split[split.Length - 2], out id)) // ratio - { - if (int.TryParse(split[split.Length - 3], out type)) - { - return APIHelper.ConstructImageLinkFromTypeAndId(ctx, type, id, short_url); - } - } - - return null; // invalid url, which did not end in type/id[/ratio] - } - - #endregion - - public static Filter FilterFromGroupFilter(HttpContext ctx, SVR_GroupFilter gg, int uid) - { - var ob = new Filter - { - name = gg.GroupFilterName, id = gg.GroupFilterID, url = ConstructFilterIdUrl(ctx, gg.GroupFilterID) - }; - if (gg.GroupsIds.ContainsKey(uid)) - { - var groups = gg.GroupsIds[uid]; - if (groups.Count != 0) - { - ob.size = groups.Count; - ob.viewed = 0; - - foreach (var grp in groups) - { - var ag = RepoFactory.AnimeGroup.GetByID(grp); - var v = ag.GetPlexContract(uid); - if (v?.Art != null && v.Thumb != null) - { - ob.art.fanart.Add(new Art { url = ConstructImageLinkFromRest(ctx, v.Art), index = 0 }); - ob.art.thumb.Add(new Art { url = ConstructImageLinkFromRest(ctx, v.Thumb), index = 0 }); - break; - } - } - } - } - - return ob; - } - - public static Filter FilterFromAnimeGroup(HttpContext ctx, SVR_AnimeGroup grp, int uid) - { - var ob = new Filter - { - name = grp.GroupName, - id = grp.AnimeGroupID, - url = ConstructFilterIdUrl(ctx, grp.AnimeGroupID), - size = -1, - viewed = -1 - }; - foreach (var ser in grp.GetSeries().Randomize()) - { - var anim = ser.GetAnime(); - if (anim != null) - { - var fanart = anim.GetDefaultFanartDetailsNoBlanks(); - var banner = anim.GetDefaultWideBannerDetailsNoBlanks(); - - if (fanart != null) - { - ob.art.fanart.Add(new Art - { - url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)fanart.ImageType, fanart.ImageID), - index = ob.art.fanart.Count - }); - ob.art.thumb.Add(new Art - { - url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)fanart.ImageType, fanart.ImageID), - index = ob.art.thumb.Count - }); - } - - if (banner != null) - { - ob.art.banner.Add(new Art - { - url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)banner.ImageType, banner.ImageID), - index = ob.art.banner.Count - }); - } - - if (ob.art.fanart.Count > 0) - { - break; - } - } - } - - return ob; - } } diff --git a/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs b/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs index 630830eef..d4fdbba87 100644 --- a/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs +++ b/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs @@ -15,7 +15,7 @@ public HasAudioLanguageExpression() { } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.AudioLanguages.Contains(Parameter); } diff --git a/Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs b/Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs index a793d5142..d20510e8d 100644 --- a/Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs +++ b/Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs @@ -15,7 +15,7 @@ public HasSharedAudioLanguageExpression() { } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.SharedAudioLanguages.Contains(Parameter); } diff --git a/Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs b/Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs index 788a91595..a117d77d2 100644 --- a/Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs +++ b/Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs @@ -15,7 +15,7 @@ public HasSharedSubtitleLanguageExpression() { } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.SharedSubtitleLanguages.Contains(Parameter); } diff --git a/Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs b/Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs index 5c16cb0de..6e1f67ea2 100644 --- a/Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs +++ b/Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs @@ -15,7 +15,7 @@ public HasSharedVideoSourceExpression() { } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.SharedVideoSources.Contains(Parameter); } diff --git a/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs b/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs index 77c62643b..25ada250f 100644 --- a/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs +++ b/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs @@ -15,7 +15,7 @@ public HasSubtitleLanguageExpression() { } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.SubtitleLanguages.Contains(Parameter); } diff --git a/Shoko.Server/Filters/Files/HasVideoSourceExpression.cs b/Shoko.Server/Filters/Files/HasVideoSourceExpression.cs index 9d7cfb3d5..73ad0ba5c 100644 --- a/Shoko.Server/Filters/Files/HasVideoSourceExpression.cs +++ b/Shoko.Server/Filters/Files/HasVideoSourceExpression.cs @@ -15,7 +15,7 @@ public HasVideoSourceExpression() { } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.VideoSources.Contains(Parameter); } diff --git a/Shoko.Server/Filters/FilterEvaluator.cs b/Shoko.Server/Filters/FilterEvaluator.cs index d64676681..4029bc331 100644 --- a/Shoko.Server/Filters/FilterEvaluator.cs +++ b/Shoko.Server/Filters/FilterEvaluator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Shoko.Models.Enums; +using Shoko.Server.Filters.Interfaces; using Shoko.Server.Filters.SortingSelectors; using Shoko.Server.Models; using Shoko.Server.Repositories; @@ -12,9 +13,21 @@ namespace Shoko.Server.Filters; public class FilterEvaluator { - private readonly AnimeGroupRepository _groups = RepoFactory.AnimeGroup; + private readonly AnimeGroupRepository _groups; - private readonly AnimeSeriesRepository _series = RepoFactory.AnimeSeries; + private readonly AnimeSeriesRepository _series; + + public FilterEvaluator() + { + _series = RepoFactory.AnimeSeries; + _groups = RepoFactory.AnimeGroup; + } + + public FilterEvaluator(AnimeGroupRepository groups, AnimeSeriesRepository series) + { + _groups = groups; + _series = series; + } /// <summary> /// Evaluate the given filter, applying the necessary logic @@ -34,10 +47,10 @@ public IEnumerable<IGrouping<int, int>> EvaluateFilter(FilterPreset filter, int? var filterables = filter.ApplyAtSeriesLevel switch { - true when user => _series.GetAll().AsParallel().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), - true => _series.GetAll().AsParallel().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToFilterable())), - false when user => _groups.GetAll().AsParallel().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), - false => _groups.GetAll().AsParallel().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToFilterable())) + true when user => _series?.GetAll().AsParallel().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))) ?? Array.Empty<FilterableWithID>().AsParallel(), + true => _series?.GetAll().AsParallel().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToFilterable())) ?? Array.Empty<FilterableWithID>().AsParallel(), + false when user => _groups?.GetAll().AsParallel().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))) ?? Array.Empty<FilterableWithID>().AsParallel(), + false => _groups?.GetAll().AsParallel().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToFilterable())) ?? Array.Empty<FilterableWithID>().AsParallel() }; // Filtering @@ -118,7 +131,8 @@ private static IOrderedEnumerable<FilterableWithID> OrderFilterables(FilterPrese return ordered; } - private record FilterableWithID(int SeriesID, int GroupID, Filterable Filterable); + private record FilterableWithID(int SeriesID, int GroupID, IFilterable Filterable); + private record UserFilterableWithID(int UserID, int SeriesID, int GroupID, IFilterable Filterable) : FilterableWithID(SeriesID, GroupID, Filterable); private record Grouping(int GroupID, int[] SeriesIDs) : IGrouping<int, int> { diff --git a/Shoko.Server/Filters/FilterExpression.cs b/Shoko.Server/Filters/FilterExpression.cs index 85a84b537..1d63f926c 100644 --- a/Shoko.Server/Filters/FilterExpression.cs +++ b/Shoko.Server/Filters/FilterExpression.cs @@ -48,9 +48,14 @@ public override int GetHashCode() { return !Equals(left, right); } + + public virtual bool IsType(FilterExpression expression) + { + return expression.GetType() == GetType(); + } } public abstract class FilterExpression<T> : FilterExpression, IFilterExpression<T> { - public abstract T Evaluate(Filterable f); + public abstract T Evaluate(IFilterable f); } diff --git a/Shoko.Server/Filters/Filterable.cs b/Shoko.Server/Filters/Filterable.cs index cd6ca01db..cd5cf9f27 100644 --- a/Shoko.Server/Filters/Filterable.cs +++ b/Shoko.Server/Filters/Filterable.cs @@ -2,10 +2,11 @@ using System.Collections.Generic; using System.Threading; using Shoko.Models.Enums; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters; -public class Filterable +public class Filterable : IFilterable { private readonly Lazy<DateTime> _addedDate; @@ -101,10 +102,11 @@ public class Filterable private readonly Lazy<IReadOnlySet<int>> _years; private readonly Func<IReadOnlySet<int>> _yearsDelegate; - /// <summary> - /// Name - /// </summary> - public string Name => _name.Value; + public string Name + { + get => _name.Value; + init => throw new NotSupportedException(); + } public Func<string> NameDelegate { @@ -116,10 +118,11 @@ public Func<string> NameDelegate } } - /// <summary> - /// Sorting Name - /// </summary> - public string SortingName => _sortingName.Value; + public string SortingName + { + get => _sortingName.Value; + init => throw new NotSupportedException(); + } public Func<string> SortingNameDelegate { @@ -131,10 +134,11 @@ public Func<string> SortingNameDelegate } } - /// <summary> - /// The number of series in a group - /// </summary> - public int SeriesCount => _seriesCount.Value; + public int SeriesCount + { + get => _seriesCount.Value; + init => throw new NotSupportedException(); + } public Func<int> SeriesCountDelegate { @@ -146,10 +150,11 @@ public Func<int> SeriesCountDelegate } } - /// <summary> - /// Number of Missing Episodes - /// </summary> - public int MissingEpisodes => _missingEpisodes.Value; + public int MissingEpisodes + { + get => _missingEpisodes.Value; + init => throw new NotSupportedException(); + } public Func<int> MissingEpisodesDelegate { @@ -161,10 +166,11 @@ public Func<int> MissingEpisodesDelegate } } - /// <summary> - /// Number of Missing Episodes from Groups that you have - /// </summary> - public int MissingEpisodesCollecting => _missingEpisodesCollecting.Value; + public int MissingEpisodesCollecting + { + get => _missingEpisodesCollecting.Value; + init => throw new NotSupportedException(); + } public Func<int> MissingEpisodesCollectingDelegate { @@ -176,10 +182,11 @@ public Func<int> MissingEpisodesCollectingDelegate } } - /// <summary> - /// All of the tags - /// </summary> - public IReadOnlySet<string> Tags => _tags.Value; + public IReadOnlySet<string> Tags + { + get => _tags.Value; + init => throw new NotSupportedException(); + } public Func<IReadOnlySet<string>> TagsDelegate { @@ -191,10 +198,11 @@ public Func<IReadOnlySet<string>> TagsDelegate } } - /// <summary> - /// All of the custom tags - /// </summary> - public IReadOnlySet<string> CustomTags => _customTags.Value; + public IReadOnlySet<string> CustomTags + { + get => _customTags.Value; + init => throw new NotSupportedException(); + } public Func<IReadOnlySet<string>> CustomTagsDelegate { @@ -206,10 +214,11 @@ public Func<IReadOnlySet<string>> CustomTagsDelegate } } - /// <summary> - /// The years this aired in - /// </summary> - public IReadOnlySet<int> Years => _years.Value; + public IReadOnlySet<int> Years + { + get => _years.Value; + init => throw new NotSupportedException(); + } public Func<IReadOnlySet<int>> YearsDelegate { @@ -221,10 +230,11 @@ public Func<IReadOnlySet<int>> YearsDelegate } } - /// <summary> - /// The seasons this aired in - /// </summary> - public IReadOnlySet<(int year, AnimeSeason season)> Seasons => _seasons.Value; + public IReadOnlySet<(int year, AnimeSeason season)> Seasons + { + get => _seasons.Value; + init => throw new NotSupportedException(); + } public Func<IReadOnlySet<(int year, AnimeSeason season)>> SeasonsDelegate { @@ -236,10 +246,11 @@ public Func<IReadOnlySet<int>> YearsDelegate } } - /// <summary> - /// Has at least one TvDB Link - /// </summary> - public bool HasTvDBLink => _hasTvDBLink.Value; + public bool HasTvDBLink + { + get => _hasTvDBLink.Value; + init => throw new NotSupportedException(); + } public Func<bool> HasTvDBLinkDelegate { @@ -251,10 +262,11 @@ public Func<bool> HasTvDBLinkDelegate } } - /// <summary> - /// Missing at least one TvDB Link - /// </summary> - public bool HasMissingTvDbLink => _hasMissingTvDBLink.Value; + public bool HasMissingTvDbLink + { + get => _hasMissingTvDBLink.Value; + init => throw new NotSupportedException(); + } public Func<bool> HasMissingTvDbLinkDelegate { @@ -266,10 +278,11 @@ public Func<bool> HasMissingTvDbLinkDelegate } } - /// <summary> - /// Has at least one TMDb Link - /// </summary> - public bool HasTMDbLink => _hasTMDbLink.Value; + public bool HasTMDbLink + { + get => _hasTMDbLink.Value; + init => throw new NotSupportedException(); + } public Func<bool> HasTMDbLinkDelegate { @@ -281,10 +294,11 @@ public Func<bool> HasTMDbLinkDelegate } } - /// <summary> - /// Missing at least one TMDb Link - /// </summary> - public bool HasMissingTMDbLink => _hasMissingTMDbLink.Value; + public bool HasMissingTMDbLink + { + get => _hasMissingTMDbLink.Value; + init => throw new NotSupportedException(); + } public Func<bool> HasMissingTMDbLinkDelegate { @@ -296,10 +310,11 @@ public Func<bool> HasMissingTMDbLinkDelegate } } - /// <summary> - /// Has at least one Trakt Link - /// </summary> - public bool HasTraktLink => _hasTraktLink.Value; + public bool HasTraktLink + { + get => _hasTraktLink.Value; + init => throw new NotSupportedException(); + } public Func<bool> HasTraktLinkDelegate { @@ -311,10 +326,11 @@ public Func<bool> HasTraktLinkDelegate } } - /// <summary> - /// Missing at least one Trakt Link - /// </summary> - public bool HasMissingTraktLink => _hasMissingTraktLink.Value; + public bool HasMissingTraktLink + { + get => _hasMissingTraktLink.Value; + init => throw new NotSupportedException(); + } public Func<bool> HasMissingTraktLinkDelegate { @@ -326,10 +342,11 @@ public Func<bool> HasMissingTraktLinkDelegate } } - /// <summary> - /// Has Finished airing - /// </summary> - public bool IsFinished => _isFinished.Value; + public bool IsFinished + { + get => _isFinished.Value; + init => throw new NotSupportedException(); + } public Func<bool> IsFinishedDelegate { @@ -341,10 +358,11 @@ public Func<bool> IsFinishedDelegate } } - /// <summary> - /// First Air Date - /// </summary> - public DateTime? AirDate => _airDate.Value; + public DateTime? AirDate + { + get => _airDate.Value; + init => throw new NotSupportedException(); + } public Func<DateTime?> AirDateDelegate { @@ -356,10 +374,11 @@ public Func<DateTime?> AirDateDelegate } } - /// <summary> - /// Latest Air Date - /// </summary> - public DateTime? LastAirDate => _lastAirDate.Value; + public DateTime? LastAirDate + { + get => _lastAirDate.Value; + init => throw new NotSupportedException(); + } public Func<DateTime?> LastAirDateDelegate { @@ -371,10 +390,11 @@ public Func<DateTime?> LastAirDateDelegate } } - /// <summary> - /// When it was first added to the collection - /// </summary> - public DateTime AddedDate => _addedDate.Value; + public DateTime AddedDate + { + get => _addedDate.Value; + init => throw new NotSupportedException(); + } public Func<DateTime> AddedDateDelegate { @@ -386,10 +406,11 @@ public Func<DateTime> AddedDateDelegate } } - /// <summary> - /// When it was most recently added to the collection - /// </summary> - public DateTime LastAddedDate => _lastAddedDate.Value; + public DateTime LastAddedDate + { + get => _lastAddedDate.Value; + init => throw new NotSupportedException(); + } public Func<DateTime> LastAddedDateDelegate { @@ -401,10 +422,11 @@ public Func<DateTime> LastAddedDateDelegate } } - /// <summary> - /// Highest Episode Count - /// </summary> - public int EpisodeCount => _episodeCount.Value; + public int EpisodeCount + { + get => _episodeCount.Value; + init => throw new NotSupportedException(); + } public Func<int> EpisodeCountDelegate { @@ -416,10 +438,11 @@ public Func<int> EpisodeCountDelegate } } - /// <summary> - /// Total Episode Count - /// </summary> - public int TotalEpisodeCount => _totalEpisodeCount.Value; + public int TotalEpisodeCount + { + get => _totalEpisodeCount.Value; + init => throw new NotSupportedException(); + } public Func<int> TotalEpisodeCountDelegate { @@ -431,10 +454,11 @@ public Func<int> TotalEpisodeCountDelegate } } - /// <summary> - /// Lowest AniDB Rating (0-10) - /// </summary> - public decimal LowestAniDBRating => _lowestAniDBRating.Value; + public decimal LowestAniDBRating + { + get => _lowestAniDBRating.Value; + init => throw new NotSupportedException(); + } public Func<decimal> LowestAniDBRatingDelegate { @@ -446,10 +470,11 @@ public Func<decimal> LowestAniDBRatingDelegate } } - /// <summary> - /// Highest AniDB Rating (0-10) - /// </summary> - public decimal HighestAniDBRating => _highestAniDBRating.Value; + public decimal HighestAniDBRating + { + get => _highestAniDBRating.Value; + init => throw new NotSupportedException(); + } public Func<decimal> HighestAniDBRatingDelegate { @@ -461,10 +486,11 @@ public Func<decimal> HighestAniDBRatingDelegate } } - /// <summary> - /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. - /// </summary> - public IReadOnlySet<string> VideoSources => _videoSources.Value; + public IReadOnlySet<string> VideoSources + { + get => _videoSources.Value; + init => throw new NotSupportedException(); + } public Func<IReadOnlySet<string>> VideoSourcesDelegate { @@ -476,10 +502,11 @@ public Func<IReadOnlySet<string>> VideoSourcesDelegate } } - /// <summary> - /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. (only sources that are in every file) - /// </summary> - public IReadOnlySet<string> SharedVideoSources => _sharedVideoSources.Value; + public IReadOnlySet<string> SharedVideoSources + { + get => _sharedVideoSources.Value; + init => throw new NotSupportedException(); + } public Func<IReadOnlySet<string>> SharedVideoSourcesDelegate { @@ -491,10 +518,11 @@ public Func<IReadOnlySet<string>> SharedVideoSourcesDelegate } } - /// <summary> - /// The anime types (movie, series, ova, etc) - /// </summary> - public IReadOnlySet<string> AnimeTypes => _animeTypes.Value; + public IReadOnlySet<string> AnimeTypes + { + get => _animeTypes.Value; + init => throw new NotSupportedException(); + } public Func<IReadOnlySet<string>> AnimeTypesDelegate { @@ -506,10 +534,11 @@ public Func<IReadOnlySet<string>> AnimeTypesDelegate } } - /// <summary> - /// Audio Languages - /// </summary> - public IReadOnlySet<string> AudioLanguages => _audioLanguages.Value; + public IReadOnlySet<string> AudioLanguages + { + get => _audioLanguages.Value; + init => throw new NotSupportedException(); + } public Func<IReadOnlySet<string>> AudioLanguagesDelegate { @@ -521,10 +550,11 @@ public Func<IReadOnlySet<string>> AudioLanguagesDelegate } } - /// <summary> - /// Audio Languages (only languages that are in every file) - /// </summary> - public IReadOnlySet<string> SharedAudioLanguages => _sharedAudioLanguages.Value; + public IReadOnlySet<string> SharedAudioLanguages + { + get => _sharedAudioLanguages.Value; + init => throw new NotSupportedException(); + } public Func<IReadOnlySet<string>> SharedAudioLanguagesDelegate { @@ -536,10 +566,11 @@ public Func<IReadOnlySet<string>> SharedAudioLanguagesDelegate } } - /// <summary> - /// Subtitle Languages - /// </summary> - public IReadOnlySet<string> SubtitleLanguages => _subtitleLanguages.Value; + public IReadOnlySet<string> SubtitleLanguages + { + get => _subtitleLanguages.Value; + init => throw new NotSupportedException(); + } public Func<IReadOnlySet<string>> SubtitleLanguagesDelegate { @@ -551,10 +582,11 @@ public Func<IReadOnlySet<string>> SubtitleLanguagesDelegate } } - /// <summary> - /// Subtitle Languages (only languages that are in every file) - /// </summary> - public IReadOnlySet<string> SharedSubtitleLanguages => _sharedSubtitleLanguages.Value; + public IReadOnlySet<string> SharedSubtitleLanguages + { + get => _sharedSubtitleLanguages.Value; + init => throw new NotSupportedException(); + } public Func<IReadOnlySet<string>> SharedSubtitleLanguagesDelegate { diff --git a/Shoko.Server/Filters/Functions/DateAddFunction.cs b/Shoko.Server/Filters/Functions/DateAddFunction.cs index 254014b38..fb0be3cc4 100644 --- a/Shoko.Server/Filters/Functions/DateAddFunction.cs +++ b/Shoko.Server/Filters/Functions/DateAddFunction.cs @@ -27,7 +27,7 @@ public FilterExpression<DateTime?> Left set => Selector = value; } - public override DateTime? Evaluate(Filterable f) + public override DateTime? Evaluate(IFilterable f) { return Selector.Evaluate(f) + Parameter; } @@ -71,4 +71,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is DateAddFunction exp && Left.IsType(exp.Left) && Selector.IsType(exp.Selector); + } } diff --git a/Shoko.Server/Filters/Functions/DateDiffFunction.cs b/Shoko.Server/Filters/Functions/DateDiffFunction.cs index f67dab51a..669f3bf35 100644 --- a/Shoko.Server/Filters/Functions/DateDiffFunction.cs +++ b/Shoko.Server/Filters/Functions/DateDiffFunction.cs @@ -24,7 +24,7 @@ public FilterExpression<DateTime?> Left set => Selector = value; } - public override DateTime? Evaluate(Filterable f) + public override DateTime? Evaluate(IFilterable f) { return Selector.Evaluate(f) - Parameter; } @@ -68,4 +68,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is DateDiffFunction exp && Left.IsType(exp.Left) && Selector.IsType(exp.Selector); + } } diff --git a/Shoko.Server/Filters/Functions/TodayFunction.cs b/Shoko.Server/Filters/Functions/TodayFunction.cs index d31926140..615266491 100644 --- a/Shoko.Server/Filters/Functions/TodayFunction.cs +++ b/Shoko.Server/Filters/Functions/TodayFunction.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Functions; @@ -7,7 +8,7 @@ public class TodayFunction : FilterExpression<DateTime?> public override bool TimeDependent => true; public override bool UserDependent => false; - public override DateTime? Evaluate(Filterable f) + public override DateTime? Evaluate(IFilterable f) { return DateTime.Today; } diff --git a/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs b/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs index 18e03668a..3f10e1adb 100644 --- a/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs +++ b/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs @@ -15,7 +15,7 @@ public HasAnimeTypeExpression() { } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.AnimeTypes.Contains(Parameter); } diff --git a/Shoko.Server/Filters/Info/HasCustomTagExpression.cs b/Shoko.Server/Filters/Info/HasCustomTagExpression.cs index cf34e8156..5d7690dcf 100644 --- a/Shoko.Server/Filters/Info/HasCustomTagExpression.cs +++ b/Shoko.Server/Filters/Info/HasCustomTagExpression.cs @@ -15,7 +15,7 @@ public HasCustomTagExpression() { } public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.CustomTags.Contains(Parameter); } diff --git a/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs b/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs index 11b129dc3..0265ad948 100644 --- a/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs +++ b/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.Info; public class HasMissingEpisodesCollectingExpression : FilterExpression<bool> @@ -5,7 +7,7 @@ public class HasMissingEpisodesCollectingExpression : FilterExpression<bool> public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.MissingEpisodesCollecting > 0; } diff --git a/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs b/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs index 01e723b46..8ac6091c4 100644 --- a/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs +++ b/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.Info; public class HasMissingEpisodesExpression : FilterExpression<bool> @@ -5,7 +7,7 @@ public class HasMissingEpisodesExpression : FilterExpression<bool> public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.MissingEpisodes > 0; } diff --git a/Shoko.Server/Filters/Info/HasNameExpression.cs b/Shoko.Server/Filters/Info/HasNameExpression.cs index 8ba574a12..89eee6721 100644 --- a/Shoko.Server/Filters/Info/HasNameExpression.cs +++ b/Shoko.Server/Filters/Info/HasNameExpression.cs @@ -15,7 +15,7 @@ public HasNameExpression() { } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.Name.Equals(Parameter, StringComparison.InvariantCultureIgnoreCase); } diff --git a/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs b/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs index de9580fb0..8839975d7 100644 --- a/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs +++ b/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.Info; public class HasTMDbLinkExpression : FilterExpression<bool> @@ -5,7 +7,7 @@ public class HasTMDbLinkExpression : FilterExpression<bool> public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.HasTMDbLink; } diff --git a/Shoko.Server/Filters/Info/HasTagExpression.cs b/Shoko.Server/Filters/Info/HasTagExpression.cs index 835175898..34cbeb4b6 100644 --- a/Shoko.Server/Filters/Info/HasTagExpression.cs +++ b/Shoko.Server/Filters/Info/HasTagExpression.cs @@ -15,7 +15,7 @@ public HasTagExpression() { } public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.Tags.Contains(Parameter); } diff --git a/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs b/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs index b912a8903..171d08bb3 100644 --- a/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs +++ b/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.Info; public class HasTraktLinkExpression : FilterExpression<bool> @@ -5,7 +7,7 @@ public class HasTraktLinkExpression : FilterExpression<bool> public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.HasTraktLink; } diff --git a/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs b/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs index f3fbecbe1..9c819f591 100644 --- a/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs +++ b/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.Info; public class HasTvDBLinkExpression : FilterExpression<bool> @@ -5,7 +7,7 @@ public class HasTvDBLinkExpression : FilterExpression<bool> public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.HasTvDBLink; } diff --git a/Shoko.Server/Filters/Info/InSeasonExpression.cs b/Shoko.Server/Filters/Info/InSeasonExpression.cs index 58de3f1ec..c0c90522c 100644 --- a/Shoko.Server/Filters/Info/InSeasonExpression.cs +++ b/Shoko.Server/Filters/Info/InSeasonExpression.cs @@ -18,10 +18,10 @@ public InSeasonExpression() { } public override bool TimeDependent => false; public override bool UserDependent => false; - double? IWithNumberParameter.Parameter + double IWithNumberParameter.Parameter { get => Year; - set => Year = value.HasValue ? (int)value.Value : 0; + set => Year = (int)value; } string IWithSecondStringParameter.SecondParameter @@ -30,7 +30,7 @@ string IWithSecondStringParameter.SecondParameter set => Season = Enum.Parse<AnimeSeason>(value); } - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.Seasons.Contains((Year, Season)); } diff --git a/Shoko.Server/Filters/Info/InYearExpression.cs b/Shoko.Server/Filters/Info/InYearExpression.cs index 1e0d1c2ff..96194b880 100644 --- a/Shoko.Server/Filters/Info/InYearExpression.cs +++ b/Shoko.Server/Filters/Info/InYearExpression.cs @@ -15,13 +15,13 @@ public InYearExpression() { } public override bool TimeDependent => true; public override bool UserDependent => false; - double? IWithNumberParameter.Parameter + double IWithNumberParameter.Parameter { get => Parameter; - set => Parameter = value.HasValue ? (int)value.Value : 0; + set => Parameter = (int)value; } - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.Years.Contains(Parameter); } diff --git a/Shoko.Server/Filters/Info/IsFinishedExpression.cs b/Shoko.Server/Filters/Info/IsFinishedExpression.cs index 60df07970..d2711fcaa 100644 --- a/Shoko.Server/Filters/Info/IsFinishedExpression.cs +++ b/Shoko.Server/Filters/Info/IsFinishedExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.Info; public class IsFinishedExpression : FilterExpression<bool> @@ -5,7 +7,7 @@ public class IsFinishedExpression : FilterExpression<bool> public override bool TimeDependent => true; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.IsFinished; } diff --git a/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs index 4b97b9e55..9fd79cf7f 100644 --- a/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs +++ b/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.Info; /// <summary> @@ -8,7 +10,7 @@ public class MissingTMDbLinkExpression : FilterExpression<bool> public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.HasMissingTMDbLink; } diff --git a/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs index c5d9a344a..7e1efc6bf 100644 --- a/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs +++ b/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.Info; /// <summary> @@ -8,7 +10,7 @@ public class MissingTraktLinkExpression : FilterExpression<bool> public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.HasMissingTraktLink; } diff --git a/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs index aa794eab1..8a3c804a5 100644 --- a/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs +++ b/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.Info; /// <summary> @@ -8,7 +10,7 @@ public class MissingTvDBLinkExpression : FilterExpression<bool> public override bool TimeDependent => false; public override bool UserDependent => false; - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return filterable.HasMissingTvDbLink; } diff --git a/Shoko.Server/Filters/Interfaces/IFilterExpression.cs b/Shoko.Server/Filters/Interfaces/IFilterExpression.cs index 041465f97..07341ed18 100644 --- a/Shoko.Server/Filters/Interfaces/IFilterExpression.cs +++ b/Shoko.Server/Filters/Interfaces/IFilterExpression.cs @@ -8,10 +8,10 @@ public interface IFilterExpression public interface IFilterExpression<out T> { - T Evaluate(Filterable f); + T Evaluate(IFilterable f); } public interface IUserDependentFilterExpression<out T> { - T Evaluate(UserDependentFilterable f); + T Evaluate(IUserDependentFilterable f); } diff --git a/Shoko.Server/Filters/Interfaces/IFilterable.cs b/Shoko.Server/Filters/Interfaces/IFilterable.cs new file mode 100644 index 000000000..ae3b87e41 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IFilterable.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using Shoko.Models.Enums; + +namespace Shoko.Server.Filters.Interfaces; + +public interface IFilterable +{ + /// <summary> + /// Name + /// </summary> + string Name { get; init; } + + /// <summary> + /// Sorting Name + /// </summary> + string SortingName { get; init; } + + /// <summary> + /// The number of series in a group + /// </summary> + int SeriesCount { get; init; } + + /// <summary> + /// Number of Missing Episodes + /// </summary> + int MissingEpisodes { get; init; } + + /// <summary> + /// Number of Missing Episodes from Groups that you have + /// </summary> + int MissingEpisodesCollecting { get; init; } + + /// <summary> + /// All of the tags + /// </summary> + IReadOnlySet<string> Tags { get; init; } + + /// <summary> + /// All of the custom tags + /// </summary> + IReadOnlySet<string> CustomTags { get; init; } + + /// <summary> + /// The years this aired in + /// </summary> + IReadOnlySet<int> Years { get; init; } + + /// <summary> + /// The seasons this aired in + /// </summary> + IReadOnlySet<(int year, AnimeSeason season)> Seasons { get; init; } + + /// <summary> + /// Has at least one TvDB Link + /// </summary> + bool HasTvDBLink { get; init; } + + /// <summary> + /// Missing at least one TvDB Link + /// </summary> + bool HasMissingTvDbLink { get; init; } + + /// <summary> + /// Has at least one TMDb Link + /// </summary> + bool HasTMDbLink { get; init; } + + /// <summary> + /// Missing at least one TMDb Link + /// </summary> + bool HasMissingTMDbLink { get; init; } + + /// <summary> + /// Has at least one Trakt Link + /// </summary> + bool HasTraktLink { get; init; } + + /// <summary> + /// Missing at least one Trakt Link + /// </summary> + bool HasMissingTraktLink { get; init; } + + /// <summary> + /// Has Finished airing + /// </summary> + bool IsFinished { get; init; } + + /// <summary> + /// First Air Date + /// </summary> + DateTime? AirDate { get; init; } + + /// <summary> + /// Latest Air Date + /// </summary> + DateTime? LastAirDate { get; init; } + + /// <summary> + /// When it was first added to the collection + /// </summary> + DateTime AddedDate { get; init; } + + /// <summary> + /// When it was most recently added to the collection + /// </summary> + DateTime LastAddedDate { get; init; } + + /// <summary> + /// Highest Episode Count + /// </summary> + int EpisodeCount { get; init; } + + /// <summary> + /// Total Episode Count + /// </summary> + int TotalEpisodeCount { get; init; } + + /// <summary> + /// Lowest AniDB Rating (0-10) + /// </summary> + decimal LowestAniDBRating { get; init; } + + /// <summary> + /// Highest AniDB Rating (0-10) + /// </summary> + decimal HighestAniDBRating { get; init; } + + /// <summary> + /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. + /// </summary> + IReadOnlySet<string> VideoSources { get; init; } + + /// <summary> + /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. (only sources that are in every file) + /// </summary> + IReadOnlySet<string> SharedVideoSources { get; init; } + + /// <summary> + /// The anime types (movie, series, ova, etc) + /// </summary> + IReadOnlySet<string> AnimeTypes { get; init; } + + /// <summary> + /// Audio Languages + /// </summary> + IReadOnlySet<string> AudioLanguages { get; init; } + + /// <summary> + /// Audio Languages (only languages that are in every file) + /// </summary> + IReadOnlySet<string> SharedAudioLanguages { get; init; } + + /// <summary> + /// Subtitle Languages + /// </summary> + IReadOnlySet<string> SubtitleLanguages { get; init; } + + /// <summary> + /// Subtitle Languages (only languages that are in every file) + /// </summary> + IReadOnlySet<string> SharedSubtitleLanguages { get; init; } +} diff --git a/Shoko.Server/Filters/Interfaces/IUserDependentFilterable.cs b/Shoko.Server/Filters/Interfaces/IUserDependentFilterable.cs new file mode 100644 index 000000000..79468552b --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IUserDependentFilterable.cs @@ -0,0 +1,56 @@ +using System; + +namespace Shoko.Server.Filters.Interfaces; + +public interface IUserDependentFilterable : IFilterable +{ + /// <summary> + /// Probably will be removed in the future. Custom Tags would handle this better + /// </summary> + bool IsFavorite { get; init; } + + /// <summary> + /// The number of episodes watched + /// </summary> + int WatchedEpisodes { get; init; } + + /// <summary> + /// The number of episodes that have not been watched + /// </summary> + int UnwatchedEpisodes { get; init; } + + /// <summary> + /// Has any user votes + /// </summary> + bool HasVotes { get; init; } + + /// <summary> + /// Has permanent (after finishing) user votes + /// </summary> + bool HasPermanentVotes { get; init; } + + /// <summary> + /// Has permanent (after finishing) user votes + /// </summary> + bool MissingPermanentVotes { get; init; } + + /// <summary> + /// First Watched Date + /// </summary> + DateTime? WatchedDate { get; init; } + + /// <summary> + /// Latest Watched Date + /// </summary> + DateTime? LastWatchedDate { get; init; } + + /// <summary> + /// Lowest User Rating (0-10) + /// </summary> + decimal LowestUserRating { get; init; } + + /// <summary> + /// Highest User Rating (0-10) + /// </summary> + public decimal HighestUserRating { get; init; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithNumberParameter.cs b/Shoko.Server/Filters/Interfaces/IWithNumberParameter.cs index d332703cb..63be734a8 100644 --- a/Shoko.Server/Filters/Interfaces/IWithNumberParameter.cs +++ b/Shoko.Server/Filters/Interfaces/IWithNumberParameter.cs @@ -2,5 +2,5 @@ namespace Shoko.Server.Filters.Interfaces; public interface IWithNumberParameter { - double? Parameter { get; set; } + double Parameter { get; set; } } diff --git a/Shoko.Server/Utilities/GroupFilterSortingCriteria.cs b/Shoko.Server/Filters/Legacy/GroupFilterSortingCriteria.cs similarity index 93% rename from Shoko.Server/Utilities/GroupFilterSortingCriteria.cs rename to Shoko.Server/Filters/Legacy/GroupFilterSortingCriteria.cs index cd8e5fc51..0202dfb1c 100644 --- a/Shoko.Server/Utilities/GroupFilterSortingCriteria.cs +++ b/Shoko.Server/Filters/Legacy/GroupFilterSortingCriteria.cs @@ -1,6 +1,6 @@ using Shoko.Models.Enums; -namespace Shoko.Server; +namespace Shoko.Server.Filters.Legacy; public class GroupFilterSortingCriteria { diff --git a/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs b/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs new file mode 100644 index 000000000..d86e1fe88 --- /dev/null +++ b/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs @@ -0,0 +1,927 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Shoko.Models.Client; +using Shoko.Models.Enums; +using Shoko.Models.Server; +using Shoko.Server.Filters.Files; +using Shoko.Server.Filters.Functions; +using Shoko.Server.Filters.Info; +using Shoko.Server.Filters.Interfaces; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Filters.Logic.DateTimes; +using Shoko.Server.Filters.Logic.Numbers; +using Shoko.Server.Filters.Selectors; +using Shoko.Server.Filters.SortingSelectors; +using Shoko.Server.Filters.User; +using Shoko.Server.Models; + +namespace Shoko.Server.Filters.Legacy; + +public static class LegacyConditionConverter +{ + public static bool TryConvertToConditions(FilterPreset filter, out List<GroupFilterCondition> conditions, out GroupFilterBaseCondition baseCondition) + { + // The allowed conversions are: + // Not(...) -> BaseCondition Inverted + // And(And(And(...))) -> Chains of And become the list + // a single condition + + var expression = filter.Expression; + // treat null expression similar to All + if (expression == null) + { + conditions = new List<GroupFilterCondition>(); + baseCondition = GroupFilterBaseCondition.Include; + return true; + } + + conditions = null; + var results = new List<GroupFilterCondition>(); + if (expression is NotExpression not) + { + baseCondition = GroupFilterBaseCondition.Exclude; + if (!TryGetConditionsRecursive(not.Left, results)) return false; + + conditions = results; + return true; + } + + baseCondition = GroupFilterBaseCondition.Include; + if (!TryGetConditionsRecursive(expression, results)) return false; + conditions = results; + return true; + } + + public static bool TryGetConditionsRecursive(FilterExpression expression, List<GroupFilterCondition> conditions) + { + if (expression is AndExpression and) return TryGetConditionsRecursive(and.Left, conditions) && TryGetConditionsRecursive(and.Right, conditions); + + if (!TryGetCondition(expression, out var condition)) return false; + conditions.Add(condition); + return true; + } + + private static bool TryGetCondition(FilterExpression expression, out GroupFilterCondition condition) + { + if (expression is NotExpression not && TryGetIncludeCondition(not.Left, out condition)) + { + condition.ConditionOperator = (int)GroupFilterOperator.Exclude; + return true; + } + + if (TryGetIncludeCondition(expression, out condition)) return true; + if (TryGetInCondition(expression, out condition)) return true; + if (TryGetComparatorCondition(expression, out condition)) return true; + return false; + } + + private static bool TryGetIncludeCondition(FilterExpression expression, out GroupFilterCondition condition) + { + condition = null; + var type = expression.GetType(); + if (type == typeof(HasMissingEpisodesExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.MissingEpisodes + }; + return true; + } + + if (type == typeof(HasMissingEpisodesCollectingExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.MissingEpisodesCollecting + }; + return true; + } + + if (type == typeof(HasUnwatchedEpisodesExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.HasUnwatchedEpisodes + }; + return true; + } + + if (type == typeof(HasWatchedEpisodesExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.HasWatchedEpisodes + }; + return true; + } + + if (type == typeof(HasPermanentUserVotesExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.UserVoted + }; + return true; + } + + if (type == typeof(HasUserVotesExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.UserVotedAny + }; + return true; + } + + if (type == typeof(HasTvDBLinkExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.AssignedTvDBInfo + }; + return true; + } + + if (type == typeof(HasTMDbLinkExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.AssignedMovieDBInfo + }; + return true; + } + + if (type == typeof(HasTraktLinkExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.AssignedTraktInfo + }; + return true; + } + + if (type == typeof(IsFavoriteExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.Favourite + }; + return true; + } + + if (type == typeof(IsFinishedExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.FinishedAiring + }; + return true; + } + + if (type == typeof(HasTMDbLinkExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.AssignedMovieDBInfo + }; + return true; + } + + if (expression == LegacyMappings.GetCompletedExpression()) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.CompletedSeries + }; + return true; + } + + if (expression == new OrExpression(new HasTvDBLinkExpression(), new HasTMDbLinkExpression())) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.AssignedTvDBOrMovieDBInfo + }; + return true; + } + + return false; + } + + private static bool TryGetInCondition(FilterExpression expression, out GroupFilterCondition condition) + { + condition = null; + + if (IsInTag(expression, out var tags, out var inverted)) + { + condition = new GroupFilterCondition + { + ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = string.Join(",", tags) + }; + return true; + } + + if (IsInCustomTag(expression, out var customTags, out inverted)) + { + condition = new GroupFilterCondition + { + ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.CustomTags, + ConditionParameter = string.Join(",", customTags) + }; + return true; + } + + if (IsInAnimeType(expression, out var animeType, out inverted)) + { + condition = new GroupFilterCondition + { + ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.AnimeType, + ConditionParameter = string.Join(",", animeType) + }; + return true; + } + + if (IsInVideoQuality(expression, out var videoQualities, out inverted)) + { + condition = new GroupFilterCondition + { + ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.VideoQuality, + ConditionParameter = string.Join(",", videoQualities) + }; + return true; + } + + if (IsInGroup(expression, out var groups, out inverted)) + { + condition = new GroupFilterCondition + { + ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.AnimeGroup, + ConditionParameter = string.Join(",", groups) + }; + return true; + } + + if (IsInYear(expression, out var years, out inverted)) + { + condition = new GroupFilterCondition + { + ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Year, + ConditionParameter = string.Join(",", years) + }; + return true; + } + + if (IsInSeason(expression, out var seasons, out inverted)) + { + condition = new GroupFilterCondition + { + ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Season, + ConditionParameter = string.Join(",", seasons.Select(a => a.Season + " " + a.Year)) + }; + return true; + } + + if (IsInAudioLanguage(expression, out var aLanguages, out inverted)) + { + condition = new GroupFilterCondition + { + ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.AudioLanguage, + ConditionParameter = string.Join(",", aLanguages) + }; + return true; + } + + if (IsInSubtitleLanguage(expression, out var sLanguages, out inverted)) + { + condition = new GroupFilterCondition + { + ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.SubtitleLanguage, + ConditionParameter = string.Join(",", sLanguages) + }; + return true; + } + + if (IsInSharedVideoQuality(expression, out var sVideoQuality, out inverted)) + { + condition = new GroupFilterCondition + { + ConditionOperator = inverted ? (int)GroupFilterOperator.NotInAllEpisodes : (int)GroupFilterOperator.InAllEpisodes, + ConditionType = (int)GroupFilterConditionType.VideoQuality, + ConditionParameter = string.Join(",", sVideoQuality) + }; + return true; + } + + if (IsInSharedAudioLanguage(expression, out var sALanguages, out inverted)) + { + condition = new GroupFilterCondition + { + ConditionOperator = inverted ? (int)GroupFilterOperator.NotInAllEpisodes : (int)GroupFilterOperator.InAllEpisodes, + ConditionType = (int)GroupFilterConditionType.AudioLanguage, + ConditionParameter = string.Join(",", sALanguages) + }; + return true; + } + + if (IsInSharedSubtitleLanguage(expression, out var sSLanguages, out inverted)) + { + condition = new GroupFilterCondition + { + ConditionOperator = inverted ? (int)GroupFilterOperator.NotInAllEpisodes : (int)GroupFilterOperator.InAllEpisodes, + ConditionType = (int)GroupFilterConditionType.SubtitleLanguage, + ConditionParameter = string.Join(",", sSLanguages) + }; + return true; + } + + return false; + } + + private static bool TryGetComparatorCondition(FilterExpression expression, out GroupFilterCondition condition) + { + condition = null; + if (IsAirDate(expression, out var airDatePara, out var airDateOperator)) + { + var para = airDatePara is DateTime date ? date.ToString("yyyyMMdd") : airDatePara.ToString(); + condition = new GroupFilterCondition + { + ConditionOperator = (int)airDateOperator, + ConditionType = (int)GroupFilterConditionType.AirDate, + ConditionParameter = para + }; + return true; + } + + if (IsLatestAirDate(expression, out var lastAirDatePara, out var lastAirDateOperator)) + { + var para = lastAirDatePara is DateTime date ? date.ToString("yyyyMMdd") : lastAirDatePara.ToString(); + condition = new GroupFilterCondition + { + ConditionOperator = (int)lastAirDateOperator, + ConditionType = (int)GroupFilterConditionType.LatestEpisodeAirDate, + ConditionParameter = para + }; + return true; + } + + if (IsSeriesCreatedDate(expression, out var seriesCreatedDatePara, out var seriesCreatedDateOperator)) + { + var para = seriesCreatedDatePara is DateTime date ? date.ToString("yyyyMMdd") : seriesCreatedDatePara.ToString(); + condition = new GroupFilterCondition + { + ConditionOperator = (int)seriesCreatedDateOperator, + ConditionType = (int)GroupFilterConditionType.SeriesCreatedDate, + ConditionParameter = para + }; + return true; + } + + if (IsEpisodeAddedDate(expression, out var episodeAddedDatePara, out var episodeAddedDateOperator)) + { + var para = episodeAddedDatePara is DateTime date ? date.ToString("yyyyMMdd") : episodeAddedDatePara.ToString(); + condition = new GroupFilterCondition + { + ConditionOperator = (int)episodeAddedDateOperator, + ConditionType = (int)GroupFilterConditionType.EpisodeAddedDate, + ConditionParameter = para + }; + return true; + } + + if (IsEpisodeWatchedDate(expression, out var episodeWatchedDatePara, out var episodeWatchedDateOperator)) + { + var para = episodeWatchedDatePara is DateTime date ? date.ToString("yyyyMMdd") : episodeWatchedDatePara.ToString(); + condition = new GroupFilterCondition + { + ConditionOperator = (int)episodeWatchedDateOperator, + ConditionType = (int)GroupFilterConditionType.EpisodeWatchedDate, + ConditionParameter = para + }; + return true; + } + + if (IsAniDBRating(expression, out var aniDBRatingPara, out var aniDBRatingOperator)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)aniDBRatingOperator, + ConditionType = (int)GroupFilterConditionType.AniDBRating, + ConditionParameter = aniDBRatingPara.ToString() + }; + return true; + } + + if (IsUserRating(expression, out var userRatingPara, out var userRatingOperator)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)userRatingOperator, + ConditionType = (int)GroupFilterConditionType.UserRating, + ConditionParameter = userRatingPara.ToString() + }; + return true; + } + + if (IsEpisodeCount(expression, out var episodeCountPara, out var episodeCountOperator)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)episodeCountOperator, + ConditionType = (int)GroupFilterConditionType.EpisodeCount, + ConditionParameter = episodeCountPara.ToString() + }; + return true; + } + + return false; + } + + public static bool IsInTag(FilterExpression expression, out List<string> parameters, out bool inverted) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasTagExpression), parameters, out inverted); + } + + public static bool IsInCustomTag(FilterExpression expression, out List<string> parameters, out bool inverted) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasTagExpression), parameters, out inverted); + } + + public static bool IsInAnimeType(FilterExpression expression, out List<string> parameters, out bool inverted) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasAnimeTypeExpression), parameters, out inverted); + } + + public static bool IsInVideoQuality(FilterExpression expression, out List<string> parameters, out bool inverted) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasVideoSourceExpression), parameters, out inverted); + } + + public static bool IsInSharedVideoQuality(FilterExpression expression, out List<string> parameters, out bool inverted) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasSharedVideoSourceExpression), parameters, out inverted); + } + + public static bool IsInGroup(FilterExpression expression, out List<string> parameters, out bool inverted) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasNameExpression), parameters, out inverted); + } + + public static bool IsInYear(FilterExpression expression, out List<int> parameters, out bool inverted) + { + parameters = new List<int>(); + return TryParseIn(expression, typeof(InYearExpression), parameters, out inverted); + } + + public static bool IsInSeason(FilterExpression expression, out List<(int Year, string Season)> parameters, out bool inverted) + { + parameters = new List<(int, string)>(); + return TryParseIn(expression, typeof(InSeasonExpression), parameters, out inverted); + } + + public static bool IsInAudioLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasAudioLanguageExpression), parameters, out inverted); + } + + public static bool IsInSubtitleLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasSubtitleLanguageExpression), parameters, out inverted); + } + + public static bool IsInSharedAudioLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasSharedAudioLanguageExpression), parameters, out inverted); + } + + public static bool IsInSharedSubtitleLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasSharedSubtitleLanguageExpression), parameters, out inverted); + } + + public static bool IsAirDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(AirDateSelector), out parameter, out gfOperator); + } + + public static bool IsLatestAirDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(LastAirDateSelector), out parameter, out gfOperator); + } + + public static bool IsSeriesCreatedDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(AddedDateSelector), out parameter, out gfOperator); + } + + public static bool IsEpisodeAddedDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(LastAddedDateSelector), out parameter, out gfOperator); + } + + public static bool IsEpisodeWatchedDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(LastWatchedDateSelector), out parameter, out gfOperator); + } + + public static bool IsAniDBRating(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(HighestAniDBRatingSelector), out parameter, out gfOperator); + } + + public static bool IsUserRating(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(HighestUserRatingSelector), out parameter, out gfOperator); + } + + public static bool IsEpisodeCount(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(EpisodeCountSelector), out parameter, out gfOperator); + } + + private static bool TryParseIn<T>(FilterExpression expression, Type type, List<T> parameters, out bool inverted) + { + inverted = false; + if (expression is NotExpression not) + { + inverted = true; + expression = not.Left; + } + + if (expression is OrExpression or) return TryParseIn(or.Left, type, parameters, out _) && TryParseIn(or.Right, type, parameters, out _); + if (expression.GetType() != type) return false; + + if (typeof(T) == typeof(string) && expression is IWithStringParameter withString) parameters.Add((T)(object)withString.Parameter); + else if (typeof(T) == typeof(DateTime) && expression is IWithDateParameter withDate) parameters.Add((T)(object)withDate.Parameter); + else if (typeof(T) == typeof(double) && expression is IWithNumberParameter withNumber) parameters.Add((T)(object)withNumber.Parameter); + else if (typeof(T) == typeof(TimeSpan) && expression is IWithTimeSpanParameter withTimeSpan) parameters.Add((T)(object)withTimeSpan.Parameter); + + return true; + + } + + private static bool TryParseIn<T,T1>(FilterExpression expression, Type type, List<(T, T1)> parameters, out bool inverted) + { + inverted = false; + if (expression is NotExpression not) + { + inverted = true; + expression = not.Left; + } + + if (expression is OrExpression or) return TryParseIn(or.Left, type, parameters, out _) && TryParseIn(or.Right, type, parameters, out _); + if (expression.GetType() != type) return false; + + T first = default; + T1 second = default; + if (typeof(T) == typeof(string) && expression is IWithStringParameter withString) first = (T)(object)withString.Parameter; + else if (typeof(T) == typeof(DateTime) && expression is IWithDateParameter withDate) first = (T)(object)withDate.Parameter; + else if (typeof(T) == typeof(int) && expression is IWithNumberParameter withInt) first = (T)(object)Convert.ToInt32(withInt.Parameter); + else if (typeof(T) == typeof(double) && expression is IWithNumberParameter withNumber) first = (T)(object)withNumber.Parameter; + else if (typeof(T) == typeof(TimeSpan) && expression is IWithTimeSpanParameter withTimeSpan) first = (T)(object)withTimeSpan.Parameter; + if (typeof(T1) == typeof(string) && expression is IWithSecondStringParameter withSecondString) second = (T1)(object)withSecondString.SecondParameter; + if (!EqualityComparer<T>.Default.Equals(first, default) && !EqualityComparer<T1>.Default.Equals(second, default)) parameters.Add((first, second)); + + return true; + + } + + private static bool TryParseComparator(FilterExpression expression, Type type, out object parameter, out GroupFilterOperator gfOperator) + { + // comparators share a similar format: + // Expression(selector, selector) or Expression(selector, parameter) + gfOperator = 0; + parameter = null; + switch (expression) + { + case DateGreaterThanExpression dateGreater when dateGreater.Left?.GetType() != type: + return false; + case DateGreaterThanExpression dateGreater: + gfOperator = GroupFilterOperator.GreaterThan; + parameter = dateGreater.Parameter; + return true; + case DateGreaterThanEqualsExpression dateGreaterEquals when dateGreaterEquals.Left?.GetType() != type: + return false; + case DateGreaterThanEqualsExpression dateGreaterEquals: + { + if (dateGreaterEquals.Right is not DateDiffFunction f) return false; + if (f.Left is not DateAddFunction add) return false; + if (add.Left is not TodayFunction) return false; + if (add.Parameter != TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1)) return false; + gfOperator = GroupFilterOperator.LastXDays; + parameter = f.Parameter.TotalDays; + return true; + } + case NumberGreaterThanExpression numberGreater when numberGreater.Left?.GetType() != type: + return false; + case NumberGreaterThanExpression numberGreater: + gfOperator = GroupFilterOperator.GreaterThan; + parameter = numberGreater.Parameter; + return true; + case DateLessThanExpression dateLess when dateLess.Left?.GetType() != type: + return false; + case DateLessThanExpression dateLess: + gfOperator = GroupFilterOperator.LessThan; + parameter = dateLess.Parameter; + return true; + case NumberLessThanExpression numberLess when numberLess.Left?.GetType() != type: + return false; + case NumberLessThanExpression numberLess: + gfOperator = GroupFilterOperator.LessThan; + parameter = numberLess.Parameter; + return true; + default: + return false; + } + } + + public static string GetSortingCriteria(FilterPreset filterPreset) + { + return string.Join("|", GetSortingCriteriaList(filterPreset).Select(a => (int)a.SortType + ";" + (int)a.SortDirection)); + } + + public static List<GroupFilterSortingCriteria> GetSortingCriteriaList(FilterPreset filter) + { + var results = new List<GroupFilterSortingCriteria>(); + var expression = filter.SortingExpression; + if (expression == null) + { + results.Add(new GroupFilterSortingCriteria + { + GroupFilterID = filter.FilterPresetID, + SortType = GroupFilterSorting.GroupName + }); + return results; + } + + var current = expression; + while (current != null) + { + var type = current.GetType(); + GroupFilterSorting sortType = 0; + if (type == typeof(AddedDateSortingSelector)) + sortType = GroupFilterSorting.SeriesAddedDate; + else if (type == typeof(LastAddedDateSortingSelector)) + sortType = GroupFilterSorting.EpisodeAddedDate; + else if (type == typeof(LastAirDateSortingSelector)) + sortType = GroupFilterSorting.EpisodeAirDate; + else if (type == typeof(LastWatchedDateSortingSelector)) + sortType = GroupFilterSorting.EpisodeWatchedDate; + else if (type == typeof(NameSortingSelector)) + sortType = GroupFilterSorting.GroupName; + else if (type == typeof(AirDateSortingSelector)) + sortType = GroupFilterSorting.Year; + else if (type == typeof(SeriesCountSortingSelector)) + sortType = GroupFilterSorting.SeriesCount; + else if (type == typeof(UnwatchedCountSortingSelector)) + sortType = GroupFilterSorting.UnwatchedEpisodeCount; + else if (type == typeof(MissingEpisodeCountSortingSelector)) + sortType = GroupFilterSorting.MissingEpisodeCount; + else if (type == typeof(HighestUserRatingSortingSelector)) + sortType = GroupFilterSorting.UserRating; + else if (type == typeof(HighestAniDBRatingSortingSelector)) + sortType = GroupFilterSorting.AniDBRating; + else if (type == typeof(SortingNameSortingSelector)) + sortType = GroupFilterSorting.SortName; + + if (sortType != 0) + { + results.Add(new GroupFilterSortingCriteria + { + GroupFilterID = filter.FilterPresetID, + SortType = sortType, + SortDirection = current.Descending ? GroupFilterSortDirection.Desc : GroupFilterSortDirection.Asc + }); + } + current = current.Next; + } + + return results; + } + + public static FilterExpression<bool> GetExpression(CL_GroupFilter groupFilter, bool suppressErrors = false) + { + var conditions = groupFilter.FilterConditions; + // forward compatibility is easier. Just map the old conditions to an expression + if (conditions == null || conditions.Count < 1) return null; + var first = conditions.Select((a, index) => new {Expression= GetExpression(a, suppressErrors), Index=index}).FirstOrDefault(a => a.Expression != null); + if (first == null) return null; + var condition = conditions.Count == 1 ? first.Expression : conditions.Skip(first.Index + 1).Aggregate(first.Expression, (a, b) => + { + var result = GetExpression(b, suppressErrors); + return result == null ? a : new AndExpression(a, result); + }); + return groupFilter.BaseCondition == (int)GroupFilterBaseCondition.Exclude ? new NotExpression(condition) : condition; + } + + private static FilterExpression<bool> GetExpression(GroupFilterCondition condition, bool suppressErrors = false) + { + var op = (GroupFilterOperator)condition.ConditionOperator; + var parameter = condition.ConditionParameter; + switch ((GroupFilterConditionType)condition.ConditionType) + { + case GroupFilterConditionType.CompletedSeries: + if (op == GroupFilterOperator.Include) + return LegacyMappings.GetCompletedExpression(); + return new NotExpression(LegacyMappings.GetCompletedExpression()); + case GroupFilterConditionType.MissingEpisodes: + if (op == GroupFilterOperator.Include) + return new HasMissingEpisodesExpression(); + return new NotExpression(new HasMissingEpisodesExpression()); + case GroupFilterConditionType.MissingEpisodesCollecting: + if (op == GroupFilterOperator.Include) + return new HasMissingEpisodesCollectingExpression(); + return new NotExpression(new HasMissingEpisodesCollectingExpression()); + case GroupFilterConditionType.HasUnwatchedEpisodes: + if (op == GroupFilterOperator.Include) + return new HasUnwatchedEpisodesExpression(); + return new NotExpression(new HasUnwatchedEpisodesExpression()); + case GroupFilterConditionType.HasWatchedEpisodes: + if (op == GroupFilterOperator.Include) + return new HasWatchedEpisodesExpression(); + return new NotExpression(new HasWatchedEpisodesExpression()); + case GroupFilterConditionType.UserVoted: + if (op == GroupFilterOperator.Include) + return new HasPermanentUserVotesExpression(); + return new NotExpression(new HasPermanentUserVotesExpression()); + case GroupFilterConditionType.UserVotedAny: + if (op == GroupFilterOperator.Include) + return new HasUserVotesExpression(); + return new NotExpression(new HasUserVotesExpression()); + case GroupFilterConditionType.Favourite: + if (op == GroupFilterOperator.Include) + return new IsFavoriteExpression(); + return new NotExpression(new IsFavoriteExpression()); + case GroupFilterConditionType.AssignedTvDBInfo: + if (op == GroupFilterOperator.Include) + return new HasTvDBLinkExpression(); + return new NotExpression(new HasTvDBLinkExpression()); + case GroupFilterConditionType.AssignedTraktInfo: + if (op == GroupFilterOperator.Include) + return new HasTraktLinkExpression(); + return new NotExpression(new HasTraktLinkExpression()); + case GroupFilterConditionType.Tag: + return LegacyMappings.GetTagExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.CustomTags: + return LegacyMappings.GetCustomTagExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AirDate: + return LegacyMappings.GetAirDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.LatestEpisodeAirDate: + return LegacyMappings.GetLastAirDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.SeriesCreatedDate: + return LegacyMappings.GetAddedDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.EpisodeAddedDate: + return LegacyMappings.GetLastAddedDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.EpisodeWatchedDate: + return LegacyMappings.GetWatchedDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AnimeType: + return LegacyMappings.GetAnimeTypeExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.VideoQuality: + return LegacyMappings.GetVideoQualityExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AudioLanguage: + return LegacyMappings.GetAudioLanguageExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.SubtitleLanguage: + return LegacyMappings.GetSubtitleLanguageExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AnimeGroup: + return LegacyMappings.GetGroupExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AniDBRating: + return LegacyMappings.GetRatingExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.UserRating: + return LegacyMappings.GetUserRatingExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AssignedMALInfo: + return suppressErrors ? null : throw new NotSupportedException("MAL is Deprecated"); + case GroupFilterConditionType.EpisodeCount: + return LegacyMappings.GetEpisodeCountExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.Year: + return LegacyMappings.GetYearExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.Season: + return LegacyMappings.GetSeasonExpression(op, parameter, suppressErrors); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(condition), $@"ConditionType {condition.ConditionType} is not valid"); + } + } + + public static SortingExpression GetSortingExpression(List<GroupFilterSortingCriteria> sorting) + { + SortingExpression expression = null; + SortingExpression result = null; + foreach (var criteria in sorting) + { + SortingExpression expr = criteria.SortType switch + { + GroupFilterSorting.SeriesAddedDate => new AddedDateSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.EpisodeAddedDate => new LastAddedDateSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.EpisodeAirDate => new LastAirDateSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.EpisodeWatchedDate => new LastWatchedDateSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.GroupName => new NameSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.Year => new AirDateSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.SeriesCount => new SeriesCountSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.UnwatchedEpisodeCount => new UnwatchedCountSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.MissingEpisodeCount => new MissingEpisodeCountSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.UserRating => new HighestUserRatingSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.AniDBRating => new HighestAniDBRatingSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.SortName => new SortingNameSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.GroupFilterName => new NameSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + _ => new NameSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + } + }; + if (expression == null) result = expression = expr; + else expression.Next = expr; + expression = expression.Next; + } + + return result ?? new NameSortingSelector(); + } + + public static SortingExpression GetSortingExpression(string sorting) + { + if (string.IsNullOrEmpty(sorting)) return new NameSortingSelector(); + var sortCriteriaList = new List<GroupFilterSortingCriteria>(); + var scrit = sorting.Split('|'); + foreach (var sortpair in scrit) + { + var spair = sortpair.Split(';'); + if (spair.Length != 2) + { + continue; + } + + int.TryParse(spair[0], out var stype); + int.TryParse(spair[1], out var sdir); + + if (stype > 0 && sdir > 0) + { + var gfsc = new GroupFilterSortingCriteria + { + GroupFilterID = 0, + SortType = (GroupFilterSorting)stype, + SortDirection = (GroupFilterSortDirection)sdir + }; + sortCriteriaList.Add(gfsc); + } + } + + return GetSortingExpression(sortCriteriaList); + } +} diff --git a/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs b/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs new file mode 100644 index 000000000..470705278 --- /dev/null +++ b/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs @@ -0,0 +1,175 @@ +using System.Collections.Generic; +using System.Linq; +using Shoko.Models.Client; +using Shoko.Models.Enums; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Models; +using Shoko.Server.Repositories; + +namespace Shoko.Server.Filters.Legacy; + +public class LegacyFilterConverter +{ + private readonly FilterEvaluator _evaluator; + + public LegacyFilterConverter(FilterEvaluator evaluator) + { + _evaluator = evaluator; + } + + public FilterPreset FromClient(CL_GroupFilter model) + { + var expression = LegacyConditionConverter.GetExpression(model); + var filter = new FilterPreset + { + FilterPresetID = model.GroupFilterID, + ParentFilterPresetID = model.ParentGroupFilterID, + Name = model.GroupFilterName, + FilterType = (GroupFilterType)model.FilterType, + Hidden = model.InvisibleInClients == 1, + Locked = model.Locked == 1, + ApplyAtSeriesLevel = model.ApplyToSeries == 1, + SortingExpression = LegacyConditionConverter.GetSortingExpression(model.SortingCriteria), + Expression = expression == null ? null : model.BaseCondition == (int)GroupFilterBaseCondition.Exclude ? new NotExpression(expression) : expression + }; + return filter; + } + + public CL_GroupFilter ToClient(FilterPreset filter) + { + if (filter == null) return null; + var groupIds = new Dictionary<int, HashSet<int>>(); + var seriesIds = new Dictionary<int, HashSet<int>>(); + if ((filter.Expression?.UserDependent ?? false) || (filter.SortingExpression?.UserDependent ?? false)) + { + foreach (var userID in RepoFactory.JMMUser.GetAll().Select(a => a.JMMUserID)) + { + var results = _evaluator.EvaluateFilter(filter, userID).ToList(); + groupIds[userID] = results.Select(a => a.Key).ToHashSet(); + seriesIds[userID] = results.SelectMany(a => a).ToHashSet(); + } + } + else + { + var results = _evaluator.EvaluateFilter(filter, null).ToList(); + var groupIdSet = results.Select(a => a.Key).ToHashSet(); + var seriesIdSet = results.SelectMany(a => a).ToHashSet(); + foreach (var userID in RepoFactory.JMMUser.GetAll().Select(a => a.JMMUserID)) + { + groupIds[userID] = groupIdSet; + seriesIds[userID] = seriesIdSet; + } + } + + LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var contract = new CL_GroupFilter + { + GroupFilterID = filter.FilterPresetID, + GroupFilterName = filter.Name, + ApplyToSeries = filter.ApplyAtSeriesLevel ? 1 : 0, + Locked = filter.Locked ? 1 : 0, + FilterType = (int)filter.FilterType, + ParentGroupFilterID = filter.ParentFilterPresetID, + InvisibleInClients = filter.Hidden ? 1 : 0, + BaseCondition = (int)baseCondition, + FilterConditions = conditions, + SortingCriteria = LegacyConditionConverter.GetSortingCriteria(filter), + Groups = groupIds, + Series = seriesIds, + Childs = filter.FilterPresetID == 0 + ? new HashSet<int>() + : RepoFactory.FilterPreset.GetByParentID(filter.FilterPresetID).Select(a => a.FilterPresetID).ToHashSet() + }; + return contract; + } + + public Dictionary<FilterPreset, CL_GroupFilter> ToClient(IReadOnlyList<FilterPreset> filters) + { + var result = new Dictionary<FilterPreset, CL_GroupFilter>(); + var userFilters = filters.Where(a => (a?.Expression?.UserDependent ?? false) || (a?.SortingExpression?.UserDependent ?? false)).ToList(); + var otherFilters = filters.Except(userFilters).ToList(); + + // batch evaluate each list, then build the mappings + foreach (var userID in RepoFactory.JMMUser.GetAll().Select(a => a.JMMUserID)) + { + var results = _evaluator.BatchEvaluateFilters(userFilters, userID); + var models = results.Select(kv => + { + var filter = kv.Key; + var groupIds = new Dictionary<int, HashSet<int>>(); + var seriesIds = new Dictionary<int, HashSet<int>>(); + groupIds[userID] = kv.Value.Select(a => a.Key).ToHashSet(); + seriesIds[userID] = kv.Value.SelectMany(a => a).ToHashSet(); + LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + return (Filter: filter, new CL_GroupFilter + { + GroupFilterID = filter.FilterPresetID, + GroupFilterName = filter.Name, + ApplyToSeries = filter.ApplyAtSeriesLevel ? 1 : 0, + Locked = filter.Locked ? 1 : 0, + FilterType = (int)filter.FilterType, + ParentGroupFilterID = filter.ParentFilterPresetID, + InvisibleInClients = filter.Hidden ? 1 : 0, + BaseCondition = (int)baseCondition, + FilterConditions = conditions, + SortingCriteria = LegacyConditionConverter.GetSortingCriteria(filter), + Groups = groupIds, + Series = seriesIds, + Childs = filter.FilterPresetID == 0 + ? new HashSet<int>() + : RepoFactory.FilterPreset.GetByParentID(filter.FilterPresetID).Select(a => a.FilterPresetID).ToHashSet() + }); + }); + + foreach (var (filter, model) in models) + { + result[filter] = model; + } + } + + if (otherFilters.Count > 0) + { + var results = _evaluator.BatchEvaluateFilters(userFilters, null); + var models = results.Select(kv => + { + var filter = kv.Key; + var groupIds = new Dictionary<int, HashSet<int>>(); + var seriesIds = new Dictionary<int, HashSet<int>>(); + var groupIdSet = kv.Value.Select(a => a.Key).ToHashSet(); + var seriesIdSet = kv.Value.SelectMany(a => a).ToHashSet(); + foreach (var userID in RepoFactory.JMMUser.GetAll().Select(a => a.JMMUserID)) + { + groupIds[userID] = groupIdSet; + seriesIds[userID] = seriesIdSet; + } + + LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + return (Filter: filter, new CL_GroupFilter + { + GroupFilterID = filter.FilterPresetID, + GroupFilterName = filter.Name, + ApplyToSeries = filter.ApplyAtSeriesLevel ? 1 : 0, + Locked = filter.Locked ? 1 : 0, + FilterType = (int)filter.FilterType, + ParentGroupFilterID = filter.ParentFilterPresetID, + InvisibleInClients = filter.Hidden ? 1 : 0, + BaseCondition = (int)baseCondition, + FilterConditions = conditions, + SortingCriteria = LegacyConditionConverter.GetSortingCriteria(filter), + Groups = groupIds, + Series = seriesIds, + Childs = filter.FilterPresetID == 0 + ? new HashSet<int>() + : RepoFactory.FilterPreset.GetByParentID(filter.FilterPresetID).Select(a => a.FilterPresetID).ToHashSet() + }); + }); + + foreach (var (filter, model) in models) + { + result[filter] = model; + } + } + + return result; + } +} diff --git a/Shoko.Server/Filters/Legacy/LegacyMappings.cs b/Shoko.Server/Filters/Legacy/LegacyMappings.cs new file mode 100644 index 000000000..09a439e8e --- /dev/null +++ b/Shoko.Server/Filters/Legacy/LegacyMappings.cs @@ -0,0 +1,472 @@ +using System; +using System.Linq; +using Shoko.Models.Enums; +using Shoko.Server.Filters.Files; +using Shoko.Server.Filters.Functions; +using Shoko.Server.Filters.Info; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Filters.Logic.DateTimes; +using Shoko.Server.Filters.Logic.Numbers; +using Shoko.Server.Filters.Selectors; +using Shoko.Server.Filters.User; + +namespace Shoko.Server.Filters.Legacy; + +public class LegacyMappings +{ + public static bool TryParseDate(string sDate, out DateTime result) + { + // yyyyMMdd or yyyy-MM-dd + result = DateTime.Today; + if (sDate.Length != 8 && sDate.Length != 10) return false; + if (!int.TryParse(sDate[..4], out var year)) return false; + int month; + int day; + if (sDate.Length == 8) + { + if (!int.TryParse(sDate[4..6], out month)) return false; + if (!int.TryParse(sDate[6..8], out day)) return false; + } + else + { + if (!int.TryParse(sDate[5..7], out month)) return false; + if (!int.TryParse(sDate[8..10], out day)) return false; + } + + result = new DateTime(year, month, day); + return true; + } + + public static AndExpression GetCompletedExpression() + { + return new AndExpression( + new AndExpression(new NotExpression(new HasUnwatchedEpisodesExpression()), new NotExpression(new HasMissingEpisodesExpression())), + new IsFinishedExpression()); + } + + public static FilterExpression<bool> GetTagExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.Include: + case GroupFilterOperator.In: + { + if (tags.Length <= 1) return new HasTagExpression(tags[0]); + + FilterExpression<bool> first = new HasTagExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasTagExpression(b))); + } + case GroupFilterOperator.Exclude: + case GroupFilterOperator.NotIn: + { + if (tags.Length <= 1) return new NotExpression(new HasTagExpression(tags[0])); + + FilterExpression<bool> first = new HasTagExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasTagExpression(b)))); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), + $@"ConditionOperator {op} not applicable for Tags"); + } + } + + public static FilterExpression<bool> GetCustomTagExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.Include: + case GroupFilterOperator.In: + { + if (tags.Length <= 1) return new HasCustomTagExpression(tags[0]); + + FilterExpression<bool> first = new HasCustomTagExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasCustomTagExpression(b))); + } + case GroupFilterOperator.Exclude: + case GroupFilterOperator.NotIn: + { + if (tags.Length <= 1) return new NotExpression(new HasCustomTagExpression(tags[0])); + + FilterExpression<bool> first = new HasCustomTagExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasCustomTagExpression(b)))); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), + $@"ConditionOperator {op} not applicable for Tags"); + } + } + + public static FilterExpression<bool> GetAirDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new AirDateSelector(), op, parameter, suppressErrors); + } + + public static FilterExpression<bool> GetLastAirDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new LastAirDateSelector(), op, parameter, suppressErrors); + } + + public static FilterExpression<bool> GetAddedDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new AddedDateSelector(), op, parameter, suppressErrors); + } + + public static FilterExpression<bool> GetLastAddedDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new LastAddedDateSelector(), op, parameter, suppressErrors); + } + + public static FilterExpression<bool> GetWatchedDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new LastWatchedDateSelector(), op, parameter, suppressErrors); + } + + private static FilterExpression<bool> GetDateExpression(FilterExpression<DateTime?> selector, GroupFilterOperator op, string parameter, bool suppressErrors) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + switch (op) + { + case GroupFilterOperator.LastXDays: + { + if (!int.TryParse(parameter, out var lastX)) + return suppressErrors ? null : throw new ArgumentException(@"Parameter is not a number", nameof(parameter)); + return new DateGreaterThanEqualsExpression(selector, + new DateDiffFunction(new DateAddFunction(new TodayFunction(), TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1)), + TimeSpan.FromDays(lastX))); + } + case GroupFilterOperator.GreaterThan: + { + if (!TryParseDate(parameter, out var date)) + return suppressErrors + ? null + : throw new ArgumentException($@"Parameter {parameter} was not a date in format of yyyyMMdd", nameof(parameter)); + return new DateGreaterThanExpression(selector, date); + } + case GroupFilterOperator.LessThan: + { + if (!TryParseDate(parameter, out var date)) + return suppressErrors + ? null + : throw new ArgumentException($@"Parameter {parameter} was not a date in format of yyyyMMdd", nameof(parameter)); + return new DateGreaterThanExpression(selector, date); + } + default: + return suppressErrors + ? null + : throw new ArgumentOutOfRangeException(nameof(op), + $@"ConditionOperator {op} not applicable for Date filters"); + } + } + + public static FilterExpression<bool> GetVideoQualityExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.Include: + case GroupFilterOperator.In: + { + if (tags.Length <= 1) return new HasVideoSourceExpression(tags[0]); + + FilterExpression<bool> first = new HasVideoSourceExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasVideoSourceExpression(b))); + } + case GroupFilterOperator.Exclude: + case GroupFilterOperator.NotIn: + { + if (tags.Length <= 1) return new NotExpression(new HasVideoSourceExpression(tags[0])); + + FilterExpression<bool> first = new HasVideoSourceExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasVideoSourceExpression(b)))); + } + case GroupFilterOperator.InAllEpisodes: + { + if (tags.Length <= 1) return new HasSharedVideoSourceExpression(tags[0]); + + FilterExpression<bool> first = new HasSharedVideoSourceExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasSharedVideoSourceExpression(b))); + } + case GroupFilterOperator.NotInAllEpisodes: + { + if (tags.Length <= 1) return new NotExpression(new HasSharedVideoSourceExpression(tags[0])); + + FilterExpression<bool> first = new HasSharedVideoSourceExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasSharedVideoSourceExpression(b)))); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Video Quality"); + } + } + + public static FilterExpression<bool> GetAudioLanguageExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.In: + { + if (tags.Length <= 1) return new HasAudioLanguageExpression(tags[0]); + + FilterExpression<bool> first = new HasAudioLanguageExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasAudioLanguageExpression(b))); + } + case GroupFilterOperator.NotIn: + { + if (tags.Length <= 1) return new NotExpression(new HasAudioLanguageExpression(tags[0])); + + FilterExpression<bool> first = new HasAudioLanguageExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasAudioLanguageExpression(b)))); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Audio Languages"); + } + } + + public static FilterExpression<bool> GetSubtitleLanguageExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.In: + { + if (tags.Length <= 1) return new HasSubtitleLanguageExpression(tags[0]); + + FilterExpression<bool> first = new HasSubtitleLanguageExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasSubtitleLanguageExpression(b))); + } + case GroupFilterOperator.NotIn: + { + if (tags.Length <= 1) return new NotExpression(new HasSubtitleLanguageExpression(tags[0])); + + FilterExpression<bool> first = new HasSubtitleLanguageExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasSubtitleLanguageExpression(b)))); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Subtitle Languages"); + } + } + + public static FilterExpression<bool> GetAnimeTypeExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.In: + { + if (tags.Length <= 1) return new HasAnimeTypeExpression(tags[0]); + + FilterExpression<bool> first = new HasAnimeTypeExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasAnimeTypeExpression(b))); + } + case GroupFilterOperator.NotIn: + { + if (tags.Length <= 1) return new NotExpression(new HasAnimeTypeExpression(tags[0])); + + FilterExpression<bool> first = new HasAnimeTypeExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasAnimeTypeExpression(b)))); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Anime Type"); + } + } + + public static FilterExpression<bool> GetGroupExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.In: + { + if (tags.Length <= 1) return new HasNameExpression(tags[0]); + + FilterExpression<bool> first = new HasNameExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasNameExpression(b))); + } + case GroupFilterOperator.NotIn: + { + if (tags.Length <= 1) return new NotExpression(new HasNameExpression(tags[0])); + + FilterExpression<bool> first = new HasNameExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasNameExpression(b)))); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Group Name"); + } + } + + public static FilterExpression<bool> GetRatingExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (!double.TryParse(parameter, out var rating)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + switch (op) + { + // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand + case GroupFilterOperator.GreaterThan: + return new NumberLessThanExpression(new HighestAniDBRatingSelector(), rating); + case GroupFilterOperator.LessThan: + return new NumberGreaterThanExpression(new HighestAniDBRatingSelector(), rating); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Rating"); + } + } + + public static FilterExpression<bool> GetUserRatingExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (!double.TryParse(parameter, out var rating)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + switch (op) + { + // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand + case GroupFilterOperator.GreaterThan: + return new NumberLessThanExpression(new HighestUserRatingSelector(), rating); + case GroupFilterOperator.LessThan: + return new NumberGreaterThanExpression(new HighestUserRatingSelector(), rating); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for User Rating"); + } + } + + public static FilterExpression<bool> GetEpisodeCountExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (!int.TryParse(parameter, out var count)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + switch (op) + { + // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand + case GroupFilterOperator.GreaterThan: + return new NumberLessThanExpression(new EpisodeCountSelector(), count); + case GroupFilterOperator.LessThan: + return new NumberGreaterThanExpression(new HighestUserRatingSelector(), count); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Episode Count"); + } + } + + public static FilterExpression<bool> GetYearExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.In: + { + if (!int.TryParse(tags[0], out var firstYear)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + if (tags.Length <= 1) return new InYearExpression(firstYear); + + FilterExpression<bool> first = new InYearExpression(firstYear); + return tags.Skip(1).Aggregate(first, (a, b) => + { + if (!int.TryParse(b, out var year)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + return new OrExpression(a, new InYearExpression(year)); + }); + } + case GroupFilterOperator.NotIn: + { + if (!int.TryParse(tags[0], out var firstYear)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + if (tags.Length <= 1) return new NotExpression(new InYearExpression(firstYear)); + + FilterExpression<bool> first = new InYearExpression(firstYear); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => + { + if (!int.TryParse(b, out var year)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + return new OrExpression(a, new InYearExpression(year)); + })); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Years"); + } + } + + public static FilterExpression<bool> GetSeasonExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.Include: + case GroupFilterOperator.In: + { + var firstParts = tags[0].Split(' '); + if (!int.TryParse(firstParts[1], out var firstYear)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid year", nameof(parameter)); + if (!Enum.TryParse<AnimeSeason>(firstParts[0], out var firstSeason)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid season", nameof(parameter)); + if (tags.Length <= 1) return new InSeasonExpression(firstYear, firstSeason); + + FilterExpression<bool> first = new InYearExpression(firstYear); + return tags.Skip(1).Aggregate(first, (a, b) => + { + var parts = b.Split(' '); + if (!int.TryParse(parts[1], out var year)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid year", nameof(parameter)); + if (!Enum.TryParse<AnimeSeason>(parts[0], out var season)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid season", nameof(parameter)); + return new OrExpression(a, new InSeasonExpression(year, season)); + }); + } + case GroupFilterOperator.Exclude: + case GroupFilterOperator.NotIn: + { + var firstParts = tags[0].Split(' '); + if (!int.TryParse(firstParts[1], out var firstYear)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid year", nameof(parameter)); + if (!Enum.TryParse<AnimeSeason>(firstParts[0], out var firstSeason)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid season", nameof(parameter)); + if (tags.Length <= 1) return new NotExpression(new InSeasonExpression(firstYear, firstSeason)); + + FilterExpression<bool> first = new InYearExpression(firstYear); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => + { + var parts = b.Split(' '); + if (!int.TryParse(parts[1], out var year)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid year", nameof(parameter)); + if (!Enum.TryParse<AnimeSeason>(parts[0], out var season)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid season", nameof(parameter)); + return new OrExpression(a, new InSeasonExpression(year, season)); + })); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Seasons"); + } + } +} diff --git a/Shoko.Server/Filters/LegacyFilterConverter.cs b/Shoko.Server/Filters/LegacyFilterConverter.cs deleted file mode 100644 index 940c75853..000000000 --- a/Shoko.Server/Filters/LegacyFilterConverter.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Shoko.Models.Enums; -using Shoko.Models.Server; -using Shoko.Server.Filters.Info; -using Shoko.Server.Filters.Logic; -using Shoko.Server.Filters.User; -using Shoko.Server.Models; - -namespace Shoko.Server.Filters; - -public class LegacyFilterConverter -{ - public List<GroupFilterCondition> GetConditions(FilterPreset filter) - { - // TODO traverse the tree and replace with pre-set mappings - return new List<GroupFilterCondition>(); - } - - public List<GroupFilterSortingCriteria> GetSortingCriteria(FilterPreset filter) - { - // TODO traverse the tree and replace with pre-set mappings - return new List<GroupFilterSortingCriteria> - { - new() - { - GroupFilterID = filter.FilterPresetID, SortType = GroupFilterSorting.SortName, SortDirection = GroupFilterSortDirection.Asc - } - }; - } - - public FilterExpression<bool> GetExpression(List<GroupFilterCondition> conditions, bool suppressErrors = false) - { - // forward compatibility is easier. Just map the old conditions to an expression - if (conditions == null || conditions.Count < 1) return null; - var first = conditions.Select((a, index) => new {Expression=GetExpression(a, suppressErrors), Index=index}).FirstOrDefault(a => a.Expression != null); - if (first == null) return null; - return conditions.Count == 1 ? first.Expression : conditions.Skip(first.Index + 1).Aggregate(first.Expression, (a, b) => - { - var result = GetExpression(b, suppressErrors); - return result == null ? a : new AndExpression(a, result); - }); - } - - private FilterExpression<bool> GetExpression(GroupFilterCondition condition, bool suppressErrors = false) - { - var op = (GroupFilterOperator)condition.ConditionOperator; - var parameter = condition.ConditionParameter; - switch ((GroupFilterConditionType)condition.ConditionType) - { - case GroupFilterConditionType.CompletedSeries: - return new AndExpression( - new AndExpression(new NotExpression(new HasUnwatchedEpisodesExpression()), new NotExpression(new HasMissingEpisodesExpression())), - new IsFinishedExpression()); - case GroupFilterConditionType.MissingEpisodes: - return new HasMissingEpisodesExpression(); - case GroupFilterConditionType.MissingEpisodesCollecting: - return new HasMissingEpisodesCollectingExpression(); - case GroupFilterConditionType.HasUnwatchedEpisodes: - return new HasUnwatchedEpisodesExpression(); - case GroupFilterConditionType.HasWatchedEpisodes: - return new HasWatchedEpisodesExpression(); - case GroupFilterConditionType.UserVoted: - return new HasPermanentUserVotesExpression(); - case GroupFilterConditionType.UserVotedAny: - return new HasUserVotesExpression(); - case GroupFilterConditionType.Tag: - return LegacyMappings.GetTagExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.CustomTags: - return LegacyMappings.GetCustomTagExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.AirDate: - return LegacyMappings.GetAirDateExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.LatestEpisodeAirDate: - return LegacyMappings.GetLastAirDateExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.SeriesCreatedDate: - return LegacyMappings.GetAddedDateExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.EpisodeAddedDate: - return LegacyMappings.GetLastAddedDateExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.EpisodeWatchedDate: - return LegacyMappings.GetWatchedDateExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.AssignedTvDBInfo: - return new HasTvDBLinkExpression(); - case GroupFilterConditionType.AnimeType: - return LegacyMappings.GetAnimeTypeExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.VideoQuality: - return LegacyMappings.GetVideoQualityExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.AudioLanguage: - return LegacyMappings.GetAudioLanguageExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.SubtitleLanguage: - return LegacyMappings.GetSubtitleLanguageExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.Favourite: - return new IsFavoriteExpression(); - case GroupFilterConditionType.AnimeGroup: - return LegacyMappings.GetGroupExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.AniDBRating: - return LegacyMappings.GetRatingExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.UserRating: - return LegacyMappings.GetUserRatingExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.FinishedAiring: - return new IsFinishedExpression(); - case GroupFilterConditionType.AssignedTvDBOrMovieDBInfo: - return new OrExpression(new HasTvDBLinkExpression(), new HasTMDbLinkExpression()); - case GroupFilterConditionType.AssignedMovieDBInfo: - return new HasTMDbLinkExpression(); - case GroupFilterConditionType.AssignedMALInfo: - return suppressErrors ? null : throw new NotSupportedException("MAL is Deprecated"); - case GroupFilterConditionType.EpisodeCount: - return LegacyMappings.GetEpisodeCountExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.Year: - return LegacyMappings.GetYearExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.Season: - return LegacyMappings.GetSeasonExpression(op, parameter, suppressErrors); - case GroupFilterConditionType.AssignedTraktInfo: - return new HasTraktLinkExpression(); - default: - return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(condition), $@"ConditionType {condition.ConditionType} is not valid"); - } - } - -} diff --git a/Shoko.Server/Filters/LegacyMappings.cs b/Shoko.Server/Filters/LegacyMappings.cs deleted file mode 100644 index 58b62d26a..000000000 --- a/Shoko.Server/Filters/LegacyMappings.cs +++ /dev/null @@ -1,287 +0,0 @@ -using System; -using Shoko.Models.Enums; -using Shoko.Server.Filters.Files; -using Shoko.Server.Filters.Functions; -using Shoko.Server.Filters.Info; -using Shoko.Server.Filters.Logic; -using Shoko.Server.Filters.Logic.Numbers; -using Shoko.Server.Filters.Logic.DateTimes; -using Shoko.Server.Filters.Selectors; - -namespace Shoko.Server.Filters; - -public class LegacyMappings -{ - public static bool TryParseDate(string sDate, out DateTime result) - { - // yyyyMMdd or yyyy-MM-dd - result = DateTime.Today; - if (sDate.Length != 8 && sDate.Length != 10) return false; - if (!int.TryParse(sDate[..4], out var year)) return false; - int month; - int day; - if (sDate.Length == 8) - { - if (!int.TryParse(sDate[4..6], out month)) return false; - if (!int.TryParse(sDate[6..8], out day)) return false; - } - else - { - if (!int.TryParse(sDate[5..7], out month)) return false; - if (!int.TryParse(sDate[8..10], out day)) return false; - } - - result = new DateTime(year, month, day); - return true; - } - - public static FilterExpression<bool> GetTagExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) - { - if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); - switch (op) - { - case GroupFilterOperator.Include: - case GroupFilterOperator.In: - return new HasTagExpression(parameter); - case GroupFilterOperator.Exclude: - case GroupFilterOperator.NotIn: - return new NotExpression(new HasTagExpression(parameter)); - default: - return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), - $@"ConditionOperator {op} not applicable for Tags"); - } - } - - public static FilterExpression<bool> GetCustomTagExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) - { - if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); - switch (op) - { - case GroupFilterOperator.Include: - case GroupFilterOperator.In: - return new HasCustomTagExpression(parameter); - case GroupFilterOperator.Exclude: - case GroupFilterOperator.NotIn: - return new NotExpression(new HasCustomTagExpression(parameter)); - default: - return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), - $@"ConditionOperator {op} not applicable for Tags"); - } - } - - public static FilterExpression<bool> GetAirDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) - { - return GetDateExpression(new AirDateSelector(), op, parameter, suppressErrors); - } - - public static FilterExpression<bool> GetLastAirDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) - { - return GetDateExpression(new LastAirDateSelector(), op, parameter, suppressErrors); - } - - public static FilterExpression<bool> GetAddedDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) - { - return GetDateExpression(new AddedDateSelector(), op, parameter, suppressErrors); - } - - public static FilterExpression<bool> GetLastAddedDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) - { - return GetDateExpression(new LastAddedDateSelector(), op, parameter, suppressErrors); - } - - public static FilterExpression<bool> GetWatchedDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) - { - return GetDateExpression(new LastWatchedDateSelector(), op, parameter, suppressErrors); - } - - private static FilterExpression<bool> GetDateExpression(FilterExpression<DateTime?> selector, GroupFilterOperator op, string parameter, bool suppressErrors) - { - if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); - switch (op) - { - case GroupFilterOperator.LastXDays: - { - if (!int.TryParse(parameter, out var lastX)) - return suppressErrors ? null : throw new ArgumentException(@"Parameter is not a number", nameof(parameter)); - return new DateGreaterThanEqualsExpression(selector, - new DateDiffFunction(new DateAddFunction(new TodayFunction(), TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1)), - TimeSpan.FromDays(lastX))); - } - case GroupFilterOperator.GreaterThan: - { - if (!TryParseDate(parameter, out var date)) - return suppressErrors - ? null - : throw new ArgumentException($@"Parameter {parameter} was not a date in format of yyyyMMdd", nameof(parameter)); - return new DateGreaterThanExpression(selector, date); - } - case GroupFilterOperator.LessThan: - { - if (!TryParseDate(parameter, out var date)) - return suppressErrors - ? null - : throw new ArgumentException($@"Parameter {parameter} was not a date in format of yyyyMMdd", nameof(parameter)); - return new DateGreaterThanExpression(selector, date); - } - default: - return suppressErrors - ? null - : throw new ArgumentOutOfRangeException(nameof(op), - $@"ConditionOperator {op} not applicable for Date filters"); - } - } - - public static FilterExpression<bool> GetVideoQualityExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) - { - switch (op) - { - case GroupFilterOperator.In: - return new HasVideoSourceExpression(parameter); - case GroupFilterOperator.InAllEpisodes: - return new HasSharedVideoSourceExpression(parameter); - case GroupFilterOperator.NotIn: - return new NotExpression(new HasVideoSourceExpression(parameter)); - case GroupFilterOperator.NotInAllEpisodes: - return new NotExpression(new HasSharedVideoSourceExpression(parameter)); - default: - return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Video Quality"); - } - } - - public static FilterExpression<bool> GetAudioLanguageExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) - { - switch (op) - { - case GroupFilterOperator.In: - return new HasAudioLanguageExpression(parameter); - case GroupFilterOperator.NotIn: - return new NotExpression(new HasAudioLanguageExpression(parameter)); - default: - return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Audio Languages"); - } - } - - public static FilterExpression<bool> GetSubtitleLanguageExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) - { - switch (op) - { - case GroupFilterOperator.In: - return new HasSubtitleLanguageExpression(parameter); - case GroupFilterOperator.NotIn: - return new NotExpression(new HasSubtitleLanguageExpression(parameter)); - default: - return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Subtitle Languages"); - } - } - - public static FilterExpression<bool> GetAnimeTypeExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) - { - switch (op) - { - case GroupFilterOperator.In: - return new HasAnimeTypeExpression(parameter); - case GroupFilterOperator.NotIn: - return new NotExpression(new HasAnimeTypeExpression(parameter)); - default: - return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Anime Type"); - } - } - - public static FilterExpression<bool> GetGroupExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) - { - switch (op) - { - case GroupFilterOperator.In: - return new HasNameExpression(parameter); - case GroupFilterOperator.NotIn: - return new NotExpression(new HasNameExpression(parameter)); - default: - return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Group Name"); - } - } - - public static FilterExpression<bool> GetRatingExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) - { - if (!double.TryParse(parameter, out var rating)) - return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); - switch (op) - { - // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand - case GroupFilterOperator.GreaterThan: - return new NumberLessThanExpression(new HighestAniDBRatingSelector(), rating); - case GroupFilterOperator.LessThan: - return new NumberGreaterThanExpression(new HighestAniDBRatingSelector(), rating); - default: - return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Rating"); - } - } - - public static FilterExpression<bool> GetUserRatingExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) - { - if (!double.TryParse(parameter, out var rating)) - return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); - switch (op) - { - // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand - case GroupFilterOperator.GreaterThan: - return new NumberLessThanExpression(new HighestUserRatingSelector(), rating); - case GroupFilterOperator.LessThan: - return new NumberGreaterThanExpression(new HighestUserRatingSelector(), rating); - default: - return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for User Rating"); - } - } - - public static FilterExpression<bool> GetEpisodeCountExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) - { - if (!int.TryParse(parameter, out var count)) - return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); - switch (op) - { - // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand - case GroupFilterOperator.GreaterThan: - return new NumberLessThanExpression(new EpisodeCountSelector(), count); - case GroupFilterOperator.LessThan: - return new NumberGreaterThanExpression(new HighestUserRatingSelector(), count); - default: - return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Episode Count"); - } - } - - public static FilterExpression<bool> GetYearExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) - { - if (!int.TryParse(parameter, out var year)) - return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); - switch (op) - { - case GroupFilterOperator.In: - case GroupFilterOperator.Include: - return new InYearExpression(year); - case GroupFilterOperator.NotIn: - case GroupFilterOperator.Exclude: - return new NotExpression(new InYearExpression(year)); - default: - return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Years"); - } - } - - public static FilterExpression<bool> GetSeasonExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) - { - var parts = parameter.Split(' '); - if (!int.TryParse(parts[1], out var year)) - return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid year", nameof(parameter)); - if (!Enum.TryParse<AnimeSeason>(parts[0], out var season)) - return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid season", nameof(parameter)); - switch (op) - { - case GroupFilterOperator.In: - case GroupFilterOperator.Include: - return new InSeasonExpression(year, season); - case GroupFilterOperator.NotIn: - case GroupFilterOperator.Exclude: - return new NotExpression(new InSeasonExpression(year, season)); - default: - return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Seasons"); - } - } -} diff --git a/Shoko.Server/Filters/Logic/AndExpression.cs b/Shoko.Server/Filters/Logic/AndExpression.cs index 56d69ff66..7180a7429 100644 --- a/Shoko.Server/Filters/Logic/AndExpression.cs +++ b/Shoko.Server/Filters/Logic/AndExpression.cs @@ -19,7 +19,7 @@ public AndExpression() { } public FilterExpression<bool> Left { get; set; } public FilterExpression<bool> Right { get; set; } - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return Left.Evaluate(filterable) && Right.Evaluate(filterable); } @@ -63,4 +63,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is AndExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/DateEqualsExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateEqualsExpression.cs index b20b42d05..1405c7ef3 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/DateEqualsExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/DateEqualsExpression.cs @@ -23,7 +23,7 @@ public DateEqualsExpression() { } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var date = Left.Evaluate(filterable); var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; @@ -86,4 +86,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is DateEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanEqualsExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanEqualsExpression.cs index 7e6287d58..0b1e64ea0 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanEqualsExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanEqualsExpression.cs @@ -23,7 +23,7 @@ public DateGreaterThanEqualsExpression() { } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var date = Left.Evaluate(filterable); var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; @@ -86,4 +86,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is DateGreaterThanEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanExpression.cs index 198edc7b7..1c10fa2be 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanExpression.cs @@ -23,7 +23,7 @@ public DateGreaterThanExpression() { } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var date = Left.Evaluate(filterable); var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; @@ -86,4 +86,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is DateGreaterThanExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/DateLessThanEqualsExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateLessThanEqualsExpression.cs index 2f3ea9bda..c4419741b 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/DateLessThanEqualsExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/DateLessThanEqualsExpression.cs @@ -23,7 +23,7 @@ public DateLessThanEqualsExpression() { } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var date = Left.Evaluate(filterable); if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) @@ -79,4 +79,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is DateLessThanEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/DateLessThanExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateLessThanExpression.cs index 0181f888c..55f01d196 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/DateLessThanExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/DateLessThanExpression.cs @@ -23,7 +23,7 @@ public DateLessThanExpression() { } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var date = Left.Evaluate(filterable); var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; @@ -86,4 +86,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is DateLessThanExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/DateTimes/DateNotEqualsExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateNotEqualsExpression.cs index fd7134539..00b0b1c49 100644 --- a/Shoko.Server/Filters/Logic/DateTimes/DateNotEqualsExpression.cs +++ b/Shoko.Server/Filters/Logic/DateTimes/DateNotEqualsExpression.cs @@ -23,7 +23,7 @@ public DateNotEqualsExpression() { } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var date = Left.Evaluate(filterable); var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; @@ -86,4 +86,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is DateNotEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/NotExpression.cs b/Shoko.Server/Filters/Logic/NotExpression.cs index 5b32571ae..9686bbc37 100644 --- a/Shoko.Server/Filters/Logic/NotExpression.cs +++ b/Shoko.Server/Filters/Logic/NotExpression.cs @@ -16,7 +16,7 @@ public NotExpression() { } public FilterExpression<bool> Left { get; set; } - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return !Left.Evaluate(filterable); } @@ -60,4 +60,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is NotExpression exp && Left.IsType(exp.Left); + } } diff --git a/Shoko.Server/Filters/Logic/Numbers/NumberEqualsExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberEqualsExpression.cs index dbfd82688..1fba5ac52 100644 --- a/Shoko.Server/Filters/Logic/Numbers/NumberEqualsExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NumberEqualsExpression.cs @@ -19,14 +19,14 @@ public NumberEqualsExpression() { } public FilterExpression<double> Left { get; set; } public FilterExpression<double> Right { get; set; } - public double? Parameter { get; set; } + public double Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var left = Left.Evaluate(filterable); - var right = Parameter ?? Right.Evaluate(filterable); + var right = Right?.Evaluate(filterable) ?? Parameter; return Math.Abs(left - right) < 0.001D; } @@ -69,4 +69,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is NumberEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanEqualsExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanEqualsExpression.cs index 5f4cce9dc..762aa1cda 100644 --- a/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanEqualsExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanEqualsExpression.cs @@ -19,14 +19,14 @@ public NumberGreaterThanEqualsExpression() { } public FilterExpression<double> Left { get; set; } public FilterExpression<double> Right { get; set; } - public double? Parameter { get; set; } + public double Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var left = Left.Evaluate(filterable); - var right = Parameter ?? Right.Evaluate(filterable); + var right = Right?.Evaluate(filterable) ?? Parameter; return Math.Abs(left - right) < 0.001D || left > right; } @@ -69,4 +69,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is NumberGreaterThanEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanExpression.cs index 9af00d204..da2d67873 100644 --- a/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanExpression.cs @@ -19,14 +19,14 @@ public NumberGreaterThanExpression() { } public FilterExpression<double> Left { get; set; } public FilterExpression<double> Right { get; set; } - public double? Parameter { get; set; } + public double Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var left = Left.Evaluate(filterable); - var right = Parameter ?? Right.Evaluate(filterable); + var right = Right?.Evaluate(filterable) ?? Parameter; return left > right; } @@ -69,4 +69,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is NumberGreaterThanExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/Numbers/NumberLessThanEqualsExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberLessThanEqualsExpression.cs index 3284c96b2..6b72be3dc 100644 --- a/Shoko.Server/Filters/Logic/Numbers/NumberLessThanEqualsExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NumberLessThanEqualsExpression.cs @@ -19,14 +19,14 @@ public NumberLessThanEqualsExpression() { } public FilterExpression<double> Left { get; set; } public FilterExpression<double> Right { get; set; } - public double? Parameter { get; set; } + public double Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var left = Left.Evaluate(filterable); - var right = Parameter ?? Right.Evaluate(filterable); + var right = Right?.Evaluate(filterable) ?? Parameter; return Math.Abs(left - right) < 0.001D || left < right; } @@ -69,4 +69,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is NumberLessThanEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/Numbers/NumberLessThanExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberLessThanExpression.cs index 3144fd23e..b53da6fa8 100644 --- a/Shoko.Server/Filters/Logic/Numbers/NumberLessThanExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NumberLessThanExpression.cs @@ -19,14 +19,14 @@ public NumberLessThanExpression() { } public FilterExpression<double> Left { get; set; } public FilterExpression<double> Right { get; set; } - public double? Parameter { get; set; } + public double Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var left = Left.Evaluate(filterable); - var right = Parameter ?? Right.Evaluate(filterable); + var right = Right?.Evaluate(filterable) ?? Parameter; return left < right; } @@ -69,4 +69,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is NumberLessThanExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/Numbers/NumberNotEqualsExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberNotEqualsExpression.cs index 7c0d3e99f..23f07d9a6 100644 --- a/Shoko.Server/Filters/Logic/Numbers/NumberNotEqualsExpression.cs +++ b/Shoko.Server/Filters/Logic/Numbers/NumberNotEqualsExpression.cs @@ -19,14 +19,14 @@ public NumberNotEqualsExpression() { } public FilterExpression<double> Left { get; set; } public FilterExpression<double> Right { get; set; } - public double? Parameter { get; set; } + public double Parameter { get; set; } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var left = Left.Evaluate(filterable); - var right = Parameter ?? Right.Evaluate(filterable); + var right = Right?.Evaluate(filterable) ?? Parameter; return Math.Abs(left - right) >= 0.001D; } @@ -69,4 +69,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is NumberNotEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/OrExpression.cs b/Shoko.Server/Filters/Logic/OrExpression.cs index ba047ec97..1ea617c9b 100644 --- a/Shoko.Server/Filters/Logic/OrExpression.cs +++ b/Shoko.Server/Filters/Logic/OrExpression.cs @@ -19,7 +19,7 @@ public OrExpression() { } public FilterExpression<bool> Left { get; set; } public FilterExpression<bool> Right { get; set; } - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return Left.Evaluate(filterable) || Right.Evaluate(filterable); } @@ -63,4 +63,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is OrExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/Strings/StringContainsExpression.cs b/Shoko.Server/Filters/Logic/Strings/StringContainsExpression.cs index ce2f6fbb0..42f59c9d0 100644 --- a/Shoko.Server/Filters/Logic/Strings/StringContainsExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/StringContainsExpression.cs @@ -25,7 +25,7 @@ public StringContainsExpression() { } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var left = Left.Evaluate(filterable); var right = Parameter ?? Right?.Evaluate(filterable); @@ -76,4 +76,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is StringContainsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/Strings/StringEqualsExpression.cs b/Shoko.Server/Filters/Logic/Strings/StringEqualsExpression.cs index 250063eb7..2e3e1bfcd 100644 --- a/Shoko.Server/Filters/Logic/Strings/StringEqualsExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/StringEqualsExpression.cs @@ -23,7 +23,7 @@ public StringEqualsExpression() { } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var left = Left.Evaluate(filterable); var right = Parameter ?? Right?.Evaluate(filterable); @@ -69,4 +69,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is StringEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/Strings/StringNotEqualsExpression.cs b/Shoko.Server/Filters/Logic/Strings/StringNotEqualsExpression.cs index 1f0e49dd0..a578807eb 100644 --- a/Shoko.Server/Filters/Logic/Strings/StringNotEqualsExpression.cs +++ b/Shoko.Server/Filters/Logic/Strings/StringNotEqualsExpression.cs @@ -23,7 +23,7 @@ public StringNotEqualsExpression() { } public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { var left = Left.Evaluate(filterable); var right = Parameter ?? Right?.Evaluate(filterable); @@ -69,4 +69,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is StringNotEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Logic/XorExpression.cs b/Shoko.Server/Filters/Logic/XorExpression.cs index 768f67535..50e014b78 100644 --- a/Shoko.Server/Filters/Logic/XorExpression.cs +++ b/Shoko.Server/Filters/Logic/XorExpression.cs @@ -19,7 +19,7 @@ public XorExpression() { } public FilterExpression<bool> Left { get; set; } public FilterExpression<bool> Right { get; set; } - public override bool Evaluate(Filterable filterable) + public override bool Evaluate(IFilterable filterable) { return Left.Evaluate(filterable) ^ Right.Evaluate(filterable); } @@ -63,4 +63,9 @@ public override int GetHashCode() { return !Equals(left, right); } + + public override bool IsType(FilterExpression expression) + { + return expression is XorExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } } diff --git a/Shoko.Server/Filters/Selectors/AddedDateSelector.cs b/Shoko.Server/Filters/Selectors/AddedDateSelector.cs index 5024f9323..b52a97085 100644 --- a/Shoko.Server/Filters/Selectors/AddedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/AddedDateSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,7 +8,7 @@ public class AddedDateSelector : FilterExpression<DateTime?> public override bool TimeDependent => false; public override bool UserDependent => false; - public override DateTime? Evaluate(Filterable f) + public override DateTime? Evaluate(IFilterable f) { return f.AddedDate; } diff --git a/Shoko.Server/Filters/Selectors/AirDateSelector.cs b/Shoko.Server/Filters/Selectors/AirDateSelector.cs index 9f08e5d79..cd92486c3 100644 --- a/Shoko.Server/Filters/Selectors/AirDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/AirDateSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,7 +8,7 @@ public class AirDateSelector : FilterExpression<DateTime?> public override bool TimeDependent => false; public override bool UserDependent => false; - public override DateTime? Evaluate(Filterable f) + public override DateTime? Evaluate(IFilterable f) { return f.AirDate; } diff --git a/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs b/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs index f40eb07c6..b1440d9e9 100644 --- a/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.Selectors; public class AudioLanguageCountSelector : FilterExpression<double> @@ -5,7 +7,7 @@ public class AudioLanguageCountSelector : FilterExpression<double> public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(Filterable f) + public override double Evaluate(IFilterable f) { return f.AudioLanguages.Count; } diff --git a/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs b/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs index 15eadfc94..103942d2a 100644 --- a/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.Selectors; public class EpisodeCountSelector : FilterExpression<double> @@ -5,7 +7,7 @@ public class EpisodeCountSelector : FilterExpression<double> public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(Filterable f) + public override double Evaluate(IFilterable f) { return f.EpisodeCount; } diff --git a/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs b/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs index fc39fe9c8..195059410 100644 --- a/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,7 +8,7 @@ public class HighestAniDBRatingSelector : FilterExpression<double> public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(Filterable f) + public override double Evaluate(IFilterable f) { return Convert.ToDouble(f.HighestAniDBRating); } diff --git a/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs b/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs index aead73c55..5fdc87660 100644 --- a/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,7 +8,7 @@ public class HighestUserRatingSelector : UserDependentFilterExpression<double> public override bool TimeDependent => false; public override bool UserDependent => true; - public override double Evaluate(UserDependentFilterable f) + public override double Evaluate(IUserDependentFilterable f) { return Convert.ToDouble(f.HighestUserRating); } diff --git a/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs b/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs index 2fb1fdf46..982378940 100644 --- a/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,7 +8,7 @@ public class LastAddedDateSelector : FilterExpression<DateTime?> public override bool TimeDependent => false; public override bool UserDependent => false; - public override DateTime? Evaluate(Filterable f) + public override DateTime? Evaluate(IFilterable f) { return f.LastAddedDate; } diff --git a/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs b/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs index 7ca6c358b..ca6ae648b 100644 --- a/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,7 +8,7 @@ public class LastAirDateSelector : FilterExpression<DateTime?> public override bool TimeDependent => false; public override bool UserDependent => false; - public override DateTime? Evaluate(Filterable f) + public override DateTime? Evaluate(IFilterable f) { return f.LastAirDate; } diff --git a/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs b/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs index 7b5dc3c5d..e1054fca4 100644 --- a/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,7 +8,7 @@ public class LastWatchedDateSelector : UserDependentFilterExpression<DateTime?> public override bool TimeDependent => false; public override bool UserDependent => true; - public override DateTime? Evaluate(UserDependentFilterable f) + public override DateTime? Evaluate(IUserDependentFilterable f) { return f.LastWatchedDate; } diff --git a/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs b/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs index 55a5ff498..40db18d91 100644 --- a/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,7 +8,7 @@ public class LowestAniDBRatingSelector : FilterExpression<double> public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(Filterable f) + public override double Evaluate(IFilterable f) { return Convert.ToDouble(f.LowestAniDBRating); } diff --git a/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs b/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs index 407c78057..6026592e6 100644 --- a/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs +++ b/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,7 +8,7 @@ public class LowestUserRatingSelector : UserDependentFilterExpression<double> public override bool TimeDependent => false; public override bool UserDependent => true; - public override double Evaluate(UserDependentFilterable f) + public override double Evaluate(IUserDependentFilterable f) { return Convert.ToDouble(f.LowestUserRating); } diff --git a/Shoko.Server/Filters/Selectors/SeriesCountSelector.cs b/Shoko.Server/Filters/Selectors/SeriesCountSelector.cs index e136338ca..c1b78cad4 100644 --- a/Shoko.Server/Filters/Selectors/SeriesCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/SeriesCountSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.Selectors; public class SeriesCountSelector : FilterExpression<double> @@ -5,7 +7,7 @@ public class SeriesCountSelector : FilterExpression<double> public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(Filterable f) + public override double Evaluate(IFilterable f) { return f.SeriesCount; } diff --git a/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs b/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs index 929d3937f..07f6b6f0c 100644 --- a/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.Selectors; public class SubtitleLanguageCountSelector : FilterExpression<double> @@ -5,7 +7,7 @@ public class SubtitleLanguageCountSelector : FilterExpression<double> public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(Filterable f) + public override double Evaluate(IFilterable f) { return f.SubtitleLanguages.Count; } diff --git a/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs b/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs index c9ce31e67..60560f0b9 100644 --- a/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs +++ b/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.Selectors; public class TotalEpisodeCountSelector : FilterExpression<double> @@ -5,7 +7,7 @@ public class TotalEpisodeCountSelector : FilterExpression<double> public override bool TimeDependent => false; public override bool UserDependent => false; - public override double Evaluate(Filterable f) + public override double Evaluate(IFilterable f) { return f.TotalEpisodeCount; } diff --git a/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs b/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs index ec3aa7c9e..20cbcd23b 100644 --- a/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs +++ b/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.Selectors; @@ -7,7 +8,7 @@ public class WatchedDateSelector : UserDependentFilterExpression<DateTime?> public override bool TimeDependent => false; public override bool UserDependent => true; - public override DateTime? Evaluate(UserDependentFilterable f) + public override DateTime? Evaluate(IUserDependentFilterable f) { return f.WatchedDate; } diff --git a/Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs index 37fd984d0..248eec808 100644 --- a/Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.SortingSelectors; public class AddedDateSortingSelector : SortingExpression @@ -5,7 +7,7 @@ public class AddedDateSortingSelector : SortingExpression public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(Filterable f) + public override object Evaluate(IFilterable f) { return f.AddedDate; } diff --git a/Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs index 0fb63fc7d..b999b0786 100644 --- a/Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -8,7 +9,7 @@ public class AirDateSortingSelector : SortingExpression public override bool UserDependent => false; public DateTime DefaultValue { get; set; } - public override object Evaluate(Filterable f) + public override object Evaluate(IFilterable f) { return f.AirDate ?? DefaultValue; } diff --git a/Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs index 59bf2a82a..f29c4951a 100644 --- a/Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.SortingSelectors; public class AudioLanguageCountSortingSelector : SortingExpression @@ -5,7 +7,7 @@ public class AudioLanguageCountSortingSelector : SortingExpression public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(Filterable f) + public override object Evaluate(IFilterable f) { return f.AudioLanguages.Count; } diff --git a/Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs index 1a42fef55..63fd55819 100644 --- a/Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.SortingSelectors; public class EpisodeCountSortingSelector : SortingExpression @@ -5,7 +7,7 @@ public class EpisodeCountSortingSelector : SortingExpression public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(Filterable f) + public override object Evaluate(IFilterable f) { return f.EpisodeCount; } diff --git a/Shoko.Server/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs index c98d4bfb6..6f49b0387 100644 --- a/Shoko.Server/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -7,7 +8,7 @@ public class HighestAniDBRatingSortingSelector : SortingExpression public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(Filterable f) + public override object Evaluate(IFilterable f) { return Convert.ToDouble(f.HighestAniDBRating); } diff --git a/Shoko.Server/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs index b56db0f15..0656e729d 100644 --- a/Shoko.Server/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -7,7 +8,7 @@ public class HighestUserRatingSortingSelector : UserDependentSortingExpression public override bool TimeDependent => false; public override bool UserDependent => true; - public override object Evaluate(UserDependentFilterable f) + public override object Evaluate(IUserDependentFilterable f) { return Convert.ToDouble(f.HighestUserRating); } diff --git a/Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs index 9e780b9de..5c38f6364 100644 --- a/Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.SortingSelectors; public class LastAddedDateSortingSelector : SortingExpression @@ -5,7 +7,7 @@ public class LastAddedDateSortingSelector : SortingExpression public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(Filterable f) + public override object Evaluate(IFilterable f) { return f.LastAddedDate; } diff --git a/Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs index f4065f0aa..3d8d49317 100644 --- a/Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -8,7 +9,7 @@ public class LastAirDateSortingSelector : SortingExpression public override bool UserDependent => false; public DateTime DefaultValue { get; set; } - public override object Evaluate(Filterable f) + public override object Evaluate(IFilterable f) { return f.LastAirDate ?? DefaultValue; } diff --git a/Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs index 907cc37a9..b14ae0eaf 100644 --- a/Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -8,7 +9,7 @@ public class LastWatchedDateSortingSelector : UserDependentSortingExpression public override bool UserDependent => true; public DateTime DefaultValue { get; set; } - public override object Evaluate(UserDependentFilterable f) + public override object Evaluate(IUserDependentFilterable f) { return f.LastWatchedDate ?? DefaultValue; } diff --git a/Shoko.Server/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs index 2c836a42c..d8f479cb2 100644 --- a/Shoko.Server/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -7,7 +8,7 @@ public class LowestAniDBRatingSortingSelector : SortingExpression public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(Filterable f) + public override object Evaluate(IFilterable f) { return Convert.ToDouble(f.LowestAniDBRating); } diff --git a/Shoko.Server/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs index 256642d2b..6fe4db933 100644 --- a/Shoko.Server/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -7,7 +8,7 @@ public class LowestUserRatingSortingSelector : UserDependentSortingExpression public override bool TimeDependent => false; public override bool UserDependent => true; - public override object Evaluate(UserDependentFilterable f) + public override object Evaluate(IUserDependentFilterable f) { return Convert.ToDouble(f.LowestUserRating); } diff --git a/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs index d5924c616..dee2b5ca5 100644 --- a/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.SortingSelectors; public class MissingEpisodeCollectingCountSortingSelector : SortingExpression @@ -5,7 +7,7 @@ public class MissingEpisodeCollectingCountSortingSelector : SortingExpression public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(Filterable f) + public override object Evaluate(IFilterable f) { return f.MissingEpisodesCollecting; } diff --git a/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs index 387647486..02a079504 100644 --- a/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.SortingSelectors; public class MissingEpisodeCountSortingSelector : SortingExpression @@ -5,7 +7,7 @@ public class MissingEpisodeCountSortingSelector : SortingExpression public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(Filterable f) + public override object Evaluate(IFilterable f) { return f.MissingEpisodes; } diff --git a/Shoko.Server/Filters/SortingSelectors/NameSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/NameSortingSelector.cs index b347d49b8..19cbb8ccf 100644 --- a/Shoko.Server/Filters/SortingSelectors/NameSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/NameSortingSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.SortingSelectors; public class NameSortingSelector : SortingExpression @@ -5,7 +7,7 @@ public class NameSortingSelector : SortingExpression public override bool TimeDependent => false; public override bool UserDependent => false; - public override string Evaluate(Filterable f) + public override string Evaluate(IFilterable f) { return f.Name; } diff --git a/Shoko.Server/Filters/SortingSelectors/SeriesCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/SeriesCountSortingSelector.cs new file mode 100644 index 000000000..73e91c6f4 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/SeriesCountSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class SeriesCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override object Evaluate(IFilterable f) + { + return f.SeriesCount; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs index 00572f570..eb9bcef22 100644 --- a/Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.SortingSelectors; public class SortingNameSortingSelector : SortingExpression @@ -5,7 +7,7 @@ public class SortingNameSortingSelector : SortingExpression public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(Filterable f) + public override object Evaluate(IFilterable f) { return f.SortingName; } diff --git a/Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs index 8f9948080..72e327c89 100644 --- a/Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.SortingSelectors; public class SubtitleLanguageCountSortingSelector : SortingExpression @@ -5,7 +7,7 @@ public class SubtitleLanguageCountSortingSelector : SortingExpression public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(Filterable f) + public override object Evaluate(IFilterable f) { return f.SubtitleLanguages.Count; } diff --git a/Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs index 6e4a80a28..c5d32dde6 100644 --- a/Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.SortingSelectors; public class TotalEpisodeCountSortingSelector : SortingExpression @@ -5,7 +7,7 @@ public class TotalEpisodeCountSortingSelector : SortingExpression public override bool TimeDependent => false; public override bool UserDependent => false; - public override object Evaluate(Filterable f) + public override object Evaluate(IFilterable f) { return f.TotalEpisodeCount; } diff --git a/Shoko.Server/Filters/SortingSelectors/UnwatchedCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/UnwatchedCountSortingSelector.cs new file mode 100644 index 000000000..fa1fa1207 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/UnwatchedCountSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class UnwatchedCountSortingSelector : UserDependentSortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override object Evaluate(IUserDependentFilterable f) + { + return f.UnwatchedEpisodes; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/WatchedCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/WatchedCountSortingSelector.cs new file mode 100644 index 000000000..ffae07bfe --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/WatchedCountSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class WatchedCountSortingSelector : UserDependentSortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override object Evaluate(IUserDependentFilterable f) + { + return f.WatchedEpisodes; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/WatchedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/WatchedDateSortingSelector.cs index a7a7efbd4..84c6670a4 100644 --- a/Shoko.Server/Filters/SortingSelectors/WatchedDateSortingSelector.cs +++ b/Shoko.Server/Filters/SortingSelectors/WatchedDateSortingSelector.cs @@ -1,4 +1,5 @@ using System; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters.SortingSelectors; @@ -8,7 +9,7 @@ public class WatchedDateSortingSelector : UserDependentSortingExpression public override bool UserDependent => true; public DateTime DefaultValue { get; set; } - public override object Evaluate(UserDependentFilterable f) + public override object Evaluate(IUserDependentFilterable f) { return f.WatchedDate ?? DefaultValue; } diff --git a/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs b/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs index 65d0bb58a..7306d4061 100644 --- a/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs +++ b/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.User; public class HasPermanentUserVotesExpression : UserDependentFilterExpression<bool> @@ -5,7 +7,7 @@ public class HasPermanentUserVotesExpression : UserDependentFilterExpression<boo public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(UserDependentFilterable filterable) + public override bool Evaluate(IUserDependentFilterable filterable) { return filterable.HasPermanentVotes; } diff --git a/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs b/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs index 8cc24c350..f3bdda9b4 100644 --- a/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs +++ b/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.User; public class HasUnwatchedEpisodesExpression : UserDependentFilterExpression<bool> @@ -5,7 +7,7 @@ public class HasUnwatchedEpisodesExpression : UserDependentFilterExpression<bool public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(UserDependentFilterable filterable) + public override bool Evaluate(IUserDependentFilterable filterable) { return filterable.UnwatchedEpisodes > 0; } diff --git a/Shoko.Server/Filters/User/HasUserVotesExpression.cs b/Shoko.Server/Filters/User/HasUserVotesExpression.cs index f2e99db19..436cbb519 100644 --- a/Shoko.Server/Filters/User/HasUserVotesExpression.cs +++ b/Shoko.Server/Filters/User/HasUserVotesExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.User; public class HasUserVotesExpression : UserDependentFilterExpression<bool> @@ -5,7 +7,7 @@ public class HasUserVotesExpression : UserDependentFilterExpression<bool> public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(UserDependentFilterable filterable) + public override bool Evaluate(IUserDependentFilterable filterable) { return filterable.HasVotes; } diff --git a/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs b/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs index 95b63c644..328b9690f 100644 --- a/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs +++ b/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.User; public class HasWatchedEpisodesExpression : UserDependentFilterExpression<bool> @@ -5,7 +7,7 @@ public class HasWatchedEpisodesExpression : UserDependentFilterExpression<bool> public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(UserDependentFilterable filterable) + public override bool Evaluate(IUserDependentFilterable filterable) { return filterable.WatchedEpisodes > 0; } diff --git a/Shoko.Server/Filters/User/IsFavoriteExpression.cs b/Shoko.Server/Filters/User/IsFavoriteExpression.cs index 36ac614d9..38a585196 100644 --- a/Shoko.Server/Filters/User/IsFavoriteExpression.cs +++ b/Shoko.Server/Filters/User/IsFavoriteExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.User; public class IsFavoriteExpression : UserDependentFilterExpression<bool> @@ -5,7 +7,7 @@ public class IsFavoriteExpression : UserDependentFilterExpression<bool> public override bool TimeDependent => false; public override bool UserDependent => true; - public override bool Evaluate(UserDependentFilterable filterable) + public override bool Evaluate(IUserDependentFilterable filterable) { return filterable.IsFavorite; } diff --git a/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs b/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs index 677dfb972..8d149e981 100644 --- a/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs +++ b/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs @@ -1,3 +1,5 @@ +using Shoko.Server.Filters.Interfaces; + namespace Shoko.Server.Filters.User; public class MissingPermanentUserVotesExpression : UserDependentFilterExpression<bool> @@ -5,7 +7,7 @@ public class MissingPermanentUserVotesExpression : UserDependentFilterExpression public override bool TimeDependent => true; public override bool UserDependent => true; - public override bool Evaluate(UserDependentFilterable filterable) + public override bool Evaluate(IUserDependentFilterable filterable) { return filterable.MissingPermanentVotes; } diff --git a/Shoko.Server/Filters/UserDependentFilterExpression.cs b/Shoko.Server/Filters/UserDependentFilterExpression.cs index 272d928eb..e90b02217 100644 --- a/Shoko.Server/Filters/UserDependentFilterExpression.cs +++ b/Shoko.Server/Filters/UserDependentFilterExpression.cs @@ -6,15 +6,15 @@ namespace Shoko.Server.Filters; public abstract class UserDependentFilterExpression<T> : FilterExpression<T>, IUserDependentFilterExpression<T> { - public abstract T Evaluate(UserDependentFilterable f); + public abstract T Evaluate(IUserDependentFilterable f); - public override T Evaluate(Filterable f) + public override T Evaluate(IFilterable f) { - if (UserDependent && f is not UserDependentFilterable) + if (UserDependent && f is not IUserDependentFilterable) { throw new ArgumentException("User Dependent Filter was given an Filterable, rather than an UserDependentFilterable"); } - return Evaluate((UserDependentFilterable)f); + return Evaluate((IUserDependentFilterable)f); } } diff --git a/Shoko.Server/Filters/UserDependentFilterable.cs b/Shoko.Server/Filters/UserDependentFilterable.cs index ac12809e3..19b34918d 100644 --- a/Shoko.Server/Filters/UserDependentFilterable.cs +++ b/Shoko.Server/Filters/UserDependentFilterable.cs @@ -1,9 +1,10 @@ using System; using System.Threading; +using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters; -public class UserDependentFilterable : Filterable +public class UserDependentFilterable : Filterable, IUserDependentFilterable { private readonly Lazy<bool> _hasPermanentVotes; private readonly Func<bool> _hasPermanentVotesDelegate; @@ -35,10 +36,11 @@ public class UserDependentFilterable : Filterable private readonly Lazy<int> _watchedEpisodes; private readonly Func<int> _watchedEpisodesDelegate; - /// <summary> - /// Probably will be removed in the future. Custom Tags would handle this better - /// </summary> - public bool IsFavorite => _isFavorite.Value; + public bool IsFavorite + { + get => _isFavorite.Value; + init => throw new NotSupportedException(); + } public Func<bool> IsFavoriteDelegate { @@ -50,10 +52,11 @@ public Func<bool> IsFavoriteDelegate } } - /// <summary> - /// The number of episodes watched - /// </summary> - public int WatchedEpisodes => _watchedEpisodes.Value; + public int WatchedEpisodes + { + get => _watchedEpisodes.Value; + init => throw new NotSupportedException(); + } public Func<int> WatchedEpisodesDelegate { @@ -65,10 +68,11 @@ public Func<int> WatchedEpisodesDelegate } } - /// <summary> - /// The number of episodes that have not been watched - /// </summary> - public int UnwatchedEpisodes => _unwatchedEpisodes.Value; + public int UnwatchedEpisodes + { + get => _unwatchedEpisodes.Value; + init => throw new NotSupportedException(); + } public Func<int> UnwatchedEpisodesDelegate { @@ -80,10 +84,11 @@ public Func<int> UnwatchedEpisodesDelegate } } - /// <summary> - /// Has any user votes - /// </summary> - public bool HasVotes => _hasVotes.Value; + public bool HasVotes + { + get => _hasVotes.Value; + init => throw new NotSupportedException(); + } public Func<bool> HasVotesDelegate { @@ -95,10 +100,11 @@ public Func<bool> HasVotesDelegate } } - /// <summary> - /// Has permanent (after finishing) user votes - /// </summary> - public bool HasPermanentVotes => _hasPermanentVotes.Value; + public bool HasPermanentVotes + { + get => _hasPermanentVotes.Value; + init => throw new NotSupportedException(); + } public Func<bool> HasPermanentVotesDelegate { @@ -110,10 +116,11 @@ public Func<bool> HasPermanentVotesDelegate } } - /// <summary> - /// Has permanent (after finishing) user votes - /// </summary> - public bool MissingPermanentVotes => _missingPermanentVotes.Value; + public bool MissingPermanentVotes + { + get => _missingPermanentVotes.Value; + init => throw new NotSupportedException(); + } public Func<bool> MissingPermanentVotesDelegate { @@ -125,10 +132,11 @@ public Func<bool> MissingPermanentVotesDelegate } } - /// <summary> - /// First Watched Date - /// </summary> - public DateTime? WatchedDate => _watchedDate.Value; + public DateTime? WatchedDate + { + get => _watchedDate.Value; + init => throw new NotSupportedException(); + } public Func<DateTime?> WatchedDateDelegate { @@ -140,10 +148,11 @@ public Func<DateTime?> WatchedDateDelegate } } - /// <summary> - /// Latest Watched Date - /// </summary> - public DateTime? LastWatchedDate => _lastWatchedDate.Value; + public DateTime? LastWatchedDate + { + get => _lastWatchedDate.Value; + init => throw new NotSupportedException(); + } public Func<DateTime?> LastWatchedDateDelegate { @@ -155,10 +164,11 @@ public Func<DateTime?> LastWatchedDateDelegate } } - /// <summary> - /// Lowest User Rating (0-10) - /// </summary> - public decimal LowestUserRating => _lowestUserRating.Value; + public decimal LowestUserRating + { + get => _lowestUserRating.Value; + init => throw new NotSupportedException(); + } public Func<decimal> LowestUserRatingDelegate { @@ -170,10 +180,11 @@ public Func<decimal> LowestUserRatingDelegate } } - /// <summary> - /// Highest User Rating (0-10) - /// </summary> - public decimal HighestUserRating => _highestUserRating.Value; + public decimal HighestUserRating + { + get => _highestUserRating.Value; + init => throw new NotSupportedException(); + } public Func<decimal> HighestUserRatingDelegate { diff --git a/Shoko.Server/Filters/UserDependentSortingExpression.cs b/Shoko.Server/Filters/UserDependentSortingExpression.cs index 32b6170e8..869fdbfd0 100644 --- a/Shoko.Server/Filters/UserDependentSortingExpression.cs +++ b/Shoko.Server/Filters/UserDependentSortingExpression.cs @@ -5,15 +5,15 @@ namespace Shoko.Server.Filters; public abstract class UserDependentSortingExpression : SortingExpression, IUserDependentSortingExpression { - public override object Evaluate(Filterable f) + public override object Evaluate(IFilterable f) { - if (UserDependent && f is not UserDependentFilterable) + if (UserDependent && f is not IUserDependentFilterable) { throw new ArgumentException("User Dependent Filter was given an Filterable, rather than an UserDependentFilterable"); } - return Evaluate((UserDependentFilterable)f); + return Evaluate((IUserDependentFilterable)f); } - public abstract object Evaluate(UserDependentFilterable f); + public abstract object Evaluate(IUserDependentFilterable f); } diff --git a/Shoko.Server/Mappings/GroupFilterConditionMap.cs b/Shoko.Server/Mappings/GroupFilterConditionMap.cs deleted file mode 100644 index 214063a54..000000000 --- a/Shoko.Server/Mappings/GroupFilterConditionMap.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FluentNHibernate.Mapping; -using Shoko.Models.Server; - -namespace Shoko.Server.Mappings; - -public class GroupFilterConditionMap : ClassMap<GroupFilterCondition> -{ - public GroupFilterConditionMap() - { - Not.LazyLoad(); - Id(x => x.GroupFilterConditionID); - - Map(x => x.ConditionOperator).Not.Nullable(); - Map(x => x.ConditionParameter); - Map(x => x.ConditionType).Not.Nullable(); - Map(x => x.GroupFilterID).Not.Nullable(); - } -} diff --git a/Shoko.Server/Mappings/GroupFilterMap.cs b/Shoko.Server/Mappings/GroupFilterMap.cs deleted file mode 100644 index 784ea41ac..000000000 --- a/Shoko.Server/Mappings/GroupFilterMap.cs +++ /dev/null @@ -1,30 +0,0 @@ -using FluentNHibernate.Mapping; -using Shoko.Server.Models; - -namespace Shoko.Server.Mappings; - -public class GroupFilterMap : ClassMap<SVR_GroupFilter> -{ - public GroupFilterMap() - { - Table("GroupFilter"); - - Not.LazyLoad(); - Id(x => x.GroupFilterID); - - Map(x => x.GroupFilterName); - Map(x => x.ApplyToSeries).Not.Nullable(); - Map(x => x.BaseCondition).Not.Nullable(); - Map(x => x.SortingCriteria); - Map(x => x.Locked); - Map(x => x.FilterType); - Map(x => x.GroupsIdsVersion).Not.Nullable(); - Map(x => x.GroupsIdsString).Nullable().CustomType("StringClob"); - Map(x => x.SeriesIdsVersion).Not.Nullable(); - Map(x => x.SeriesIdsString).Nullable().CustomType("StringClob"); - Map(x => x.GroupConditionsVersion).Not.Nullable(); - Map(x => x.GroupConditions).Nullable().CustomType("StringClob"); - Map(x => x.ParentGroupFilterID); - Map(x => x.InvisibleInClients); - } -} diff --git a/Shoko.Server/Models/SVR_GroupFilter.cs b/Shoko.Server/Models/SVR_GroupFilter.cs deleted file mode 100644 index 20bc9e8a6..000000000 --- a/Shoko.Server/Models/SVR_GroupFilter.cs +++ /dev/null @@ -1,2231 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using FluentNHibernate.MappingModel; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using NLog; -using Shoko.Commons.Extensions; -using Shoko.Models.Client; -using Shoko.Models.Enums; -using Shoko.Models.Server; -using Shoko.Server.Commands; -using Shoko.Server.Extensions; -using Shoko.Server.Repositories; -using Shoko.Server.Server; -using Shoko.Server.Utilities; - -namespace Shoko.Server.Models; - -public class SVR_GroupFilter : GroupFilter -{ - private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); - public int GroupsIdsVersion { get; set; } - public string GroupsIdsString { get; set; } - - public int GroupConditionsVersion { get; set; } - public string GroupConditions { get; set; } - - public int SeriesIdsVersion { get; set; } - public string SeriesIdsString { get; set; } - - public const int GROUPFILTER_VERSION = 3; - public const int GROUPCONDITIONS_VERSION = 1; - public const int SERIEFILTER_VERSION = 2; - - - internal Dictionary<int, HashSet<int>> _groupsId = new(); - internal Dictionary<int, HashSet<int>> _seriesId = new(); - internal List<GroupFilterCondition> _conditions = new(); - - public SVR_GroupFilter Parent => - ParentGroupFilterID.HasValue ? RepoFactory.GroupFilter.GetByID(ParentGroupFilterID.Value) : null; - - public SVR_GroupFilter TopLevelGroupFilter - { - get - { - var parent = Parent; - if (parent == null) - { - return this; - } - - while (true) - { - var next = parent.Parent; - if (next == null) - { - return parent; - } - - parent = next; - } - } - } - - /// <summary> - /// It is considered locked if it's not user defined or if the locked flag - /// is set. - /// </summary> - public bool IsLocked - { - get => !((GroupFilterType)FilterType).HasFlag(GroupFilterType.UserDefined) || (Locked.HasValue && Locked.Value == 1); - set => Locked = value ? 1 : 0; - } - - /// <summary> - /// Check if the group filter can contain sub-filters. - /// </summary> - public bool IsDirectory - { - get => ((GroupFilterType)FilterType).HasFlag(GroupFilterType.Directory); - set => FilterType = value ? FilterType | (int)GroupFilterType.Directory : FilterType & ~(int)GroupFilterType.Directory; - } - - /// <summary> - /// Indicates the group filter should be hidden unless explictly requested - /// for. - /// </summary> - public bool IsHidden - { - get => InvisibleInClients == 1; - set => InvisibleInClients = value ? 1 : 0; - } - - public virtual HashSet<GroupFilterConditionType> Types => - new HashSet<GroupFilterConditionType>( - Conditions.Select(a => a.ConditionType).Cast<GroupFilterConditionType>()); - - public virtual Dictionary<int, HashSet<int>> GroupsIds - { - get - { - if (_groupsId.Count != 0 || GroupsIdsVersion != GROUPFILTER_VERSION) - { - return _groupsId; - } - - var vals = JsonConvert.DeserializeObject<Dictionary<int, List<int>>>(GroupsIdsString); - if (vals == null) - { - return _groupsId; - } - - _groupsId = vals.ToDictionary(a => a.Key, a => new HashSet<int>(a.Value)); - return _groupsId; - } - set => _groupsId = value; - } - - public virtual Dictionary<int, HashSet<int>> SeriesIds - { - get - { - if (_seriesId.Count != 0 || SeriesIdsVersion != SERIEFILTER_VERSION) - { - return _seriesId; - } - - var vals = JsonConvert.DeserializeObject<Dictionary<int, List<int>>>(SeriesIdsString); - if (vals == null) - { - return _seriesId; - } - - _seriesId = vals.ToDictionary(a => a.Key, a => new HashSet<int>(a.Value)); - - return _seriesId; - } - set => _seriesId = value; - } - - public virtual List<GroupFilterCondition> Conditions - { - get - { - if (_conditions.Count == 0 && !string.IsNullOrEmpty(GroupConditions)) - { - _conditions = JsonConvert.DeserializeObject<List<GroupFilterCondition>>(GroupConditions); - } - - return _conditions; - } - set - { - if (value != null) - { - _conditions = value; - GroupConditions = JsonConvert.SerializeObject(_conditions); - } - } - } - - public override string ToString() - { - return $"{GroupFilterID} - {GroupFilterName}"; - } - - public List<GroupFilterSortingCriteria> SortCriteriaList - { - get - { - var sortCriteriaList = new List<GroupFilterSortingCriteria>(); - - if (!string.IsNullOrEmpty(SortingCriteria)) - { - var scrit = SortingCriteria.Split('|'); - foreach (var sortpair in scrit) - { - var spair = sortpair.Split(';'); - if (spair.Length != 2) - { - continue; - } - - int.TryParse(spair[0], out var stype); - int.TryParse(spair[1], out var sdir); - - if (stype > 0 && sdir > 0) - { - var gfsc = new GroupFilterSortingCriteria - { - GroupFilterID = GroupFilterID, - SortType = (GroupFilterSorting)stype, - SortDirection = (GroupFilterSortDirection)sdir - }; - sortCriteriaList.Add(gfsc); - } - } - } - - return sortCriteriaList; - } - - set - { - var crts = value.Select(a => $"{(int)a.SortType};{(int)a.SortDirection}"); - SortingCriteria = string.Join("|", crts); - } - } - - public bool DeleteGroupFromFilters(int userID, int groupID) - { - return WriteLock( - () => - { - if (!GroupsIds.ContainsKey(userID)) - { - return false; - } - - if (!GroupsIds[userID].Contains(groupID)) - { - return false; - } - - GroupsIds[userID].Remove(groupID); - return true; - } - ); - } - - public bool RemoveUser(int userID) - { - var changed = WriteLock( - () => - { - var changed = false; - if (GroupsIds.ContainsKey(userID)) - { - GroupsIds.Remove(userID); - changed = true; - } - - if (SeriesIds.ContainsKey(userID)) - { - SeriesIds.Remove(userID); - changed = true; - } - - return changed; - } - ); - - if (!changed) - { - return false; - } - - UpdateEntityReferenceStrings(); - return true; - } - - public CL_GroupFilter ToClient() - { - if (Conditions.FirstOrDefault(a => a.GroupFilterID == 0) != null) - { - Conditions.ForEach(a => a.GroupFilterID = GroupFilterID); - RepoFactory.GroupFilter.Save(this); - } - - var contract = new CL_GroupFilter - { - GroupFilterID = GroupFilterID, - GroupFilterName = GroupFilterName, - ApplyToSeries = ApplyToSeries, - BaseCondition = BaseCondition, - SortingCriteria = SortingCriteria, - Locked = Locked, - FilterType = FilterType, - ParentGroupFilterID = ParentGroupFilterID, - InvisibleInClients = InvisibleInClients, - FilterConditions = Conditions, - Groups = GroupsIds, - Series = SeriesIds, - Childs = GroupFilterID == 0 - ? new HashSet<int>() - : RepoFactory.GroupFilter.GetByParentID(GroupFilterID).Select(a => a.GroupFilterID).ToHashSet() - }; - return contract; - } - - public static SVR_GroupFilter FromClient(CL_GroupFilter gfc) - { - var gf = new SVR_GroupFilter - { - GroupFilterID = gfc.GroupFilterID, - GroupFilterName = gfc.GroupFilterName, - ApplyToSeries = gfc.ApplyToSeries, - BaseCondition = gfc.BaseCondition, - SortingCriteria = gfc.SortingCriteria, - Locked = gfc.Locked, - InvisibleInClients = gfc.InvisibleInClients, - ParentGroupFilterID = gfc.ParentGroupFilterID, - FilterType = gfc.FilterType, - Conditions = gfc.FilterConditions, - GroupsIds = gfc.Groups ?? new Dictionary<int, HashSet<int>>(), - SeriesIds = gfc.Series ?? new Dictionary<int, HashSet<int>>() - }; - if (gf.GroupFilterID != 0) - { - gf.Conditions.ForEach(a => a.GroupFilterID = gf.GroupFilterID); - } - - return gf; - } - - public CL_GroupFilterExtended ToClientExtended(SVR_JMMUser user) - { - var contract = new CL_GroupFilterExtended { GroupFilter = ToClient(), GroupCount = 0, SeriesCount = 0 }; - - ReadLock( - () => - { - if (GroupsIds.ContainsKey(user.JMMUserID)) - { - contract.GroupCount = GroupsIds[user.JMMUserID].Count; - } - - if (SeriesIds.ContainsKey(user.JMMUserID)) - { - contract.SeriesCount = SeriesIds[user.JMMUserID].Count; - } - } - ); - - return contract; - } - - public bool UpdateGroupFilterFromSeries(CL_AnimeSeries_User ser, JMMUser user) - { - if (ser == null) - { - return false; - } - - bool result; - if (ApplyToSeries == 1) - { - result = CalculateGroupFilterSeries(RepoFactory.AnimeSeries.GetAll().Select(a => a.AnimeSeriesID).ToHashSet(), ser, user); - if (!result) - { - return false; - } - - WriteLock( - () => - { - GroupsIds[user?.JMMUserID ?? 0] = SeriesIds[user?.JMMUserID ?? 0] - .Select(a => RepoFactory.AnimeSeries.GetByID(a)?.TopLevelAnimeGroup?.AnimeGroupID ?? -1) - .Where(a => a != -1).ToHashSet(); - } - ); - } - else - { - result = false; - // Top Level Group - int? groupID = ser.AnimeGroupID; - while (true) - { - if (groupID == null) - { - break; - } - - var grp = RepoFactory.AnimeGroup.GetByID(groupID.Value); - if (grp != null) - { - groupID = grp.AnimeGroupParentID; - } - else - { - break; - } - } - - if (groupID == null) - { - return false; - } - - var group = RepoFactory.AnimeGroup.GetByID(groupID.Value); - - var contract = group?.Contract; - if (user != null) - { - contract = group?.GetUserContract(user.JMMUserID); - } - - if (contract == null) - { - return false; - } - - result |= CalculateGroupFilterGroups(RepoFactory.AnimeGroup.GetAll().Select(a => a.AnimeGroupID).ToHashSet(), contract, user); - if (!result) - { - return false; - } - - WriteLock( - () => - { - SeriesIds[user?.JMMUserID ?? 0] = GroupsIds[user?.JMMUserID ?? 0] - .SelectMany( - a => - RepoFactory.AnimeGroup.GetByID(a)?.GetAllSeries()?.Select(b => b?.AnimeSeriesID ?? -1) - ) - .Where(a => a != -1).ToHashSet(); - } - ); - } - - return true; - } - - public bool UpdateGroupFilterFromGroup(CL_AnimeGroup_User grp, JMMUser user) - { - if (grp == null) - { - return false; - } - - bool result; - if (ApplyToSeries == 1) - { - result = false; - var sers = new List<SVR_AnimeSeries>(); - SVR_AnimeGroup.GetAnimeSeriesRecursive(grp.AnimeGroupID, ref sers); - - foreach (var ser in sers) - { - var contract = ser.Contract; - if (user != null) - { - contract = ser.GetUserContract(user.JMMUserID); - } - - result |= CalculateGroupFilterSeries(RepoFactory.AnimeSeries.GetAll().Select(a => a.AnimeSeriesID).ToHashSet(), contract, user); - } - - if (!result) - { - return false; - } - - WriteLock( - () => - { - GroupsIds[user?.JMMUserID ?? 0] = SeriesIds[user?.JMMUserID ?? 0] - .Select(a => RepoFactory.AnimeSeries.GetByID(a)?.TopLevelAnimeGroup?.AnimeGroupID ?? -1) - .Where(a => a != -1).ToHashSet(); - } - ); - } - else - { - result = CalculateGroupFilterGroups(RepoFactory.AnimeGroup.GetAll().Select(a => a.AnimeGroupID).ToHashSet(), grp, user); - if (!result) - { - return false; - } - - WriteLock( - () => - { - SeriesIds[user?.JMMUserID ?? 0] = GroupsIds[user?.JMMUserID ?? 0].SelectMany( - a => RepoFactory.AnimeGroup.GetByID(a) - ?.GetAllSeries() - ?.Select(b => b?.AnimeSeriesID ?? -1) - ) - .Where(a => a != -1) - .ToHashSet(); - } - ); - } - - return true; - } - - - private bool CalculateGroupFilterSeries(HashSet<int> allSeriesIds, CL_AnimeSeries_User ser, JMMUser user) - { - if (ser == null) - { - return false; - } - - var seriesIds = ReadLock(() => SeriesIds.TryGetValue(user?.JMMUserID ?? 0, out var seriesIds) ? seriesIds : null); - - var change = false; - if (seriesIds == null) - seriesIds = new HashSet<int>(); - else - change = seriesIds.RemoveWhere(a => !allSeriesIds.Contains(a)) > 0; - - if (EvaluateGroupFilter(ser, user)) - { - change |= seriesIds.Add(ser.AnimeSeriesID); - } - else - { - change |= seriesIds.Remove(ser.AnimeSeriesID); - } - - WriteLock(() => SeriesIds[user?.JMMUserID ?? 0] = seriesIds); - - return change; - } - - private bool CalculateGroupFilterGroups(HashSet<int> allGroupIds, CL_AnimeGroup_User grp, JMMUser user) - { - if (grp == null) return false; - - var groupIds = ReadLock(() => - GroupsIds.TryGetValue(user?.JMMUserID ?? 0, out var groupIds) ? new HashSet<int>(groupIds) : null); - - var change = false; - if (groupIds == null) - groupIds = new HashSet<int>(); - else - change = groupIds.RemoveWhere(a => !allGroupIds.Contains(a)) > 0; - - if (EvaluateGroupFilter(grp, user)) - { - change |= groupIds.Add(grp.AnimeGroupID); - } - else - { - change |= groupIds.Remove(grp.AnimeGroupID); - } - - WriteLock(() => GroupsIds[user?.JMMUserID ?? 0] = groupIds); - - return change; - } - - public void CalculateGroupsAndSeries() - { - if (ApplyToSeries == 1) - { - EvaluateAnimeSeries(); - - var erroredSeries = new HashSet<int>(); - WriteLock( - () => - { - foreach (var user in SeriesIds.Keys) - { - GroupsIds[user] = SeriesIds[user].Select( - a => - { - var id = RepoFactory.AnimeSeries.GetByID(a)?.TopLevelAnimeGroup?.AnimeGroupID ?? -1; - if (id == -1) - { - erroredSeries.Add(a); - } - - return id; - } - ).Where(a => a != -1) - .ToHashSet(); - } - } - ); - foreach (var id in erroredSeries.OrderBy(a => a).ToList()) - { - var ser = RepoFactory.AnimeSeries.GetByID(id); - LogManager.GetCurrentClassLogger() - .Error("While calculating group filters, an AnimeSeries without a group was found: " + - (ser?.GetSeriesName() ?? id.ToString())); - } - } - else - { - EvaluateAnimeGroups(); - - WriteLock( - () => - { - foreach (var user in GroupsIds.Keys) - { - var ids = GroupsIds[user]; - SeriesIds[user] = ids.SelectMany( - a => - RepoFactory.AnimeGroup.GetByID(a)?.GetAllSeries() - ?.Select(b => b?.AnimeSeriesID ?? -1) - ) - .Where(a => a != -1).ToHashSet(); - } - } - ); - } - - if (((GroupFilterType)FilterType).HasFlag(GroupFilterType.Tag)) - { - GroupFilterName = GroupFilterName.Replace('`', '\''); - } - } - - private void EvaluateAnimeGroups() - { - var users = RepoFactory.JMMUser.GetAll(); - // make sure the user has not filtered this out - var allGroupsIds = RepoFactory.AnimeGroup.GetAll().Select(a => a.AnimeGroupID).ToHashSet(); - foreach (var grp in RepoFactory.AnimeGroup.GetAllTopLevelGroups()) - { - foreach (var user in users) - { - CalculateGroupFilterGroups(allGroupsIds, grp.GetUserContract(user.JMMUserID, cloned: false), user); - } - } - } - - private void EvaluateAnimeSeries() - { - var users = RepoFactory.JMMUser.GetAll(); - var allSeries = RepoFactory.AnimeSeries.GetAll(); - var allSeriesIds = allSeries.Select(a => a.AnimeSeriesID).ToHashSet(); - foreach (var ser in allSeries) - { - if (ser.Contract == null) ser.UpdateContract(); - - if (ser.Contract == null) continue; - - CalculateGroupFilterSeries(allSeriesIds, ser.Contract, null); //Default no filter for JMM Client - foreach (var user in users) - { - CalculateGroupFilterSeries(allSeriesIds, ser.GetUserContract(user.JMMUserID, cloned: false), user); - } - } - } - - public static CL_GroupFilter EvaluateContract(CL_GroupFilter gfc) - { - var gf = FromClient(gfc); - if (gf.ApplyToSeries == 1) - { - gf.EvaluateAnimeSeries(); - - gf.WriteLock( - () => - { - foreach (var user in gf.SeriesIds.Keys) - { - gf.GroupsIds[user] = gf.SeriesIds[user] - .Select(a => RepoFactory.AnimeSeries.GetByID(a)?.TopLevelAnimeGroup?.AnimeGroupID ?? -1) - .Where(a => a != -1).ToHashSet(); - } - } - ); - } - else - { - gf.EvaluateAnimeGroups(); - - gf.WriteLock( - () => - { - foreach (var user in gf.GroupsIds.Keys) - { - gf.SeriesIds[user] = gf.GroupsIds[user] - .SelectMany(a => - RepoFactory.AnimeGroup.GetByID(a)?.GetAllSeries()?.Select(b => b?.AnimeSeriesID ?? -1)) - .Where(a => a != -1).ToHashSet(); - } - } - ); - } - - return gf.ToClient(); - } - - - public bool EvaluateGroupFilter(CL_AnimeGroup_User contractGroup, JMMUser curUser) - { - //Directories don't count - if (IsDirectory) - { - return false; - } - - if (contractGroup?.Stat_AllTags == null) - { - return false; - } - - if (curUser?.GetHideCategories().FindInEnumerable(contractGroup.Stat_AllTags) ?? false) - { - return false; - } - - // sub groups don't count - if (contractGroup.AnimeGroupParentID.HasValue) - { - return false; - } - - // first check for anime groups which are included exluded every time - foreach (var gfc in Conditions) - { - if (gfc.GetConditionTypeEnum() != GroupFilterConditionType.AnimeGroup) - { - continue; - } - - int.TryParse(gfc.ConditionParameter, out var groupID); - if (groupID == 0) - { - break; - } - - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Equals && - groupID == contractGroup.AnimeGroupID) - { - return true; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotEquals && - groupID != contractGroup.AnimeGroupID) - { - return true; - } - - return false; - } - - var exclude = BaseCondition == (int)GroupFilterBaseCondition.Exclude; - - return Conditions.All(gfc => exclude ^ EvaluateConditions(contractGroup, gfc)); - } - - private bool EvaluateConditions(CL_AnimeGroup_User contractGroup, GroupFilterCondition gfc) - { - var style = NumberStyles.Number; - var culture = CultureInfo.InvariantCulture; - switch (gfc.GetConditionTypeEnum()) - { - case GroupFilterConditionType.Favourite: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && contractGroup.IsFave == 0) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && contractGroup.IsFave == 1) - { - return false; - } - - break; - - case GroupFilterConditionType.MissingEpisodes: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - (contractGroup.MissingEpisodeCount > 0 || contractGroup.MissingEpisodeCountGroups > 0) == - false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - (contractGroup.MissingEpisodeCount > 0 || contractGroup.MissingEpisodeCountGroups > 0)) - { - return false; - } - - break; - - case GroupFilterConditionType.MissingEpisodesCollecting: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.MissingEpisodeCountGroups > 0 == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.MissingEpisodeCountGroups > 0) - { - return false; - } - - break; - case GroupFilterConditionType.Tag: - var tags = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .Where(a => !string.IsNullOrWhiteSpace(a)) - .ToList(); - var tagsFound = - tags.Any( - a => contractGroup.Stat_AllTags.Contains(a)); - if ((gfc.GetConditionOperatorEnum() == GroupFilterOperator.In || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include) && !tagsFound) - { - return false; - } - - if ((gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude) && tagsFound) - { - return false; - } - - break; - case GroupFilterConditionType.Year: - var years = new HashSet<int>(); - var parameterStrings = gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var yearString in parameterStrings) - { - if (int.TryParse(yearString.Trim(), out var year)) - { - years.Add(year); - } - } - - if (years.Count <= 0) - { - return false; - } - - if ((gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.In) && - !contractGroup.Stat_AllYears.FindInEnumerable(years)) - { - return false; - } - - if ((gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn) && - contractGroup.Stat_AllYears.FindInEnumerable(years)) - { - return false; - } - - break; - case GroupFilterConditionType.Season: - var paramStrings = gfc.ConditionParameter.Trim().Split(','); - - switch (gfc.GetConditionOperatorEnum()) - { - case GroupFilterOperator.Include: - case GroupFilterOperator.In: - return paramStrings.FindInEnumerable(contractGroup.Stat_AllSeasons); - case GroupFilterOperator.Exclude: - case GroupFilterOperator.NotIn: - return !paramStrings.FindInEnumerable(contractGroup.Stat_AllSeasons); - } - - break; - case GroupFilterConditionType.HasWatchedEpisodes: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.WatchedEpisodeCount > 0 == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.WatchedEpisodeCount > 0) - { - return false; - } - - break; - - case GroupFilterConditionType.HasUnwatchedEpisodes: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.UnwatchedEpisodeCount > 0 == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.UnwatchedEpisodeCount > 0) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedTvDBInfo: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_HasTvDBLink == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_HasTvDBLink) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedTraktInfo: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_HasTraktLink == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_HasTraktLink) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedMALInfo: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_HasMALLink == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_HasMALLink) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedMovieDBInfo: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_HasMovieDBLink == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_HasMovieDBLink) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedTvDBOrMovieDBInfo: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - !contractGroup.Stat_HasMovieDBOrTvDBLink) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_HasMovieDBOrTvDBLink) - { - return false; - } - - break; - - case GroupFilterConditionType.CompletedSeries: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_IsComplete == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_IsComplete) - { - return false; - } - - break; - - case GroupFilterConditionType.FinishedAiring: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_HasFinishedAiring == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_HasFinishedAiring) - { - return false; - } - - break; - - case GroupFilterConditionType.UserVoted: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_UserVotePermanent.HasValue == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_UserVotePermanent.HasValue) - { - return false; - } - - break; - - case GroupFilterConditionType.UserVotedAny: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_UserVoteOverall.HasValue == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_UserVoteOverall.HasValue) - { - return false; - } - - break; - - case GroupFilterConditionType.AirDate: - DateTime filterDate; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDate = DateTime.Today.AddDays(0 - days); - } - else - { - filterDate = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractGroup.Stat_AirDate_Min.HasValue || !contractGroup.Stat_AirDate_Max.HasValue) - { - return false; - } - - if (contractGroup.Stat_AirDate_Max.Value < filterDate) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (!contractGroup.Stat_AirDate_Min.HasValue || !contractGroup.Stat_AirDate_Max.HasValue) - { - return false; - } - - if (contractGroup.Stat_AirDate_Min.Value > filterDate) - { - return false; - } - } - - break; - case GroupFilterConditionType.LatestEpisodeAirDate: - DateTime filterDateEpisodeLastAired; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateEpisodeLastAired = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateEpisodeLastAired = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractGroup.LatestEpisodeAirDate.HasValue) - { - return false; - } - - if (contractGroup.LatestEpisodeAirDate.Value < filterDateEpisodeLastAired) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (!contractGroup.LatestEpisodeAirDate.HasValue) - { - return false; - } - - if (contractGroup.LatestEpisodeAirDate.Value > filterDateEpisodeLastAired) - { - return false; - } - } - - break; - case GroupFilterConditionType.SeriesCreatedDate: - DateTime filterDateSeries; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateSeries = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateSeries = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractGroup.Stat_SeriesCreatedDate.HasValue) - { - return false; - } - - if (contractGroup.Stat_SeriesCreatedDate.Value < filterDateSeries) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (!contractGroup.Stat_SeriesCreatedDate.HasValue) - { - return false; - } - - if (contractGroup.Stat_SeriesCreatedDate.Value > filterDateSeries) - { - return false; - } - } - - break; - - case GroupFilterConditionType.EpisodeWatchedDate: - DateTime filterDateEpsiodeWatched; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateEpsiodeWatched = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateEpsiodeWatched = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractGroup.WatchedDate.HasValue) - { - return false; - } - - if (contractGroup.WatchedDate.Value < filterDateEpsiodeWatched) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (contractGroup?.WatchedDate == null) - { - return false; - } - - if (contractGroup.WatchedDate.Value > filterDateEpsiodeWatched) - { - return false; - } - } - - break; - - case GroupFilterConditionType.EpisodeAddedDate: - DateTime filterDateEpisodeAdded; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateEpisodeAdded = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateEpisodeAdded = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractGroup.EpisodeAddedDate.HasValue) - { - return false; - } - - if (contractGroup.EpisodeAddedDate.Value < filterDateEpisodeAdded) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (!contractGroup.EpisodeAddedDate.HasValue) - { - return false; - } - - if (contractGroup.EpisodeAddedDate.Value > filterDateEpisodeAdded) - { - return false; - } - } - - break; - - case GroupFilterConditionType.EpisodeCount: - int.TryParse(gfc.ConditionParameter, out var epCount); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan && - contractGroup.Stat_EpisodeCount < epCount) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan && - contractGroup.Stat_EpisodeCount > epCount) - { - return false; - } - - break; - - case GroupFilterConditionType.AniDBRating: - decimal.TryParse(gfc.ConditionParameter, style, culture, out var dRating); - var thisRating = contractGroup.Stat_AniDBRating / 100; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan && thisRating < dRating) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan && thisRating > dRating) - { - return false; - } - - break; - - case GroupFilterConditionType.UserRating: - if (!contractGroup.Stat_UserVoteOverall.HasValue) - { - return false; - } - - decimal.TryParse(gfc.ConditionParameter, style, culture, out var dUserRating); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan && - contractGroup.Stat_UserVoteOverall.Value < dUserRating) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan && - contractGroup.Stat_UserVoteOverall.Value > dUserRating) - { - return false; - } - - break; - - case GroupFilterConditionType.CustomTags: - var ctags = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundTag = ctags.FindInEnumerable(contractGroup.Stat_AllCustomTags); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundTag) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundTag) - { - return false; - } - - break; - - case GroupFilterConditionType.AnimeType: - var ctypes = - gfc.ConditionParameter - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => ((int)Commons.Extensions.Models.RawToType(a)).ToString()) - .ToList(); - var foundAnimeType = ctypes.FindInEnumerable(contractGroup.Stat_AnimeTypes); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundAnimeType) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundAnimeType) - { - return false; - } - - break; - - case GroupFilterConditionType.VideoQuality: - var vqs = - gfc.ConditionParameter - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundVid = vqs.FindInEnumerable(contractGroup.Stat_AllVideoQuality); - var foundVidAllEps = vqs.FindInEnumerable(contractGroup.Stat_AllVideoQuality_Episodes); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundVid) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundVid) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.InAllEpisodes && !foundVidAllEps) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotInAllEpisodes && foundVidAllEps) - { - return false; - } - - break; - - case GroupFilterConditionType.AudioLanguage: - var als = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundLang = als.FindInEnumerable(contractGroup.Stat_AudioLanguages); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundLang) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundLang) - { - return false; - } - - break; - - case GroupFilterConditionType.SubtitleLanguage: - var ass = - gfc.ConditionParameter - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundSub = ass.FindInEnumerable(contractGroup.Stat_SubtitleLanguages); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundSub) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundSub) - { - return false; - } - - break; - } - - return true; - } - - public bool EvaluateGroupFilter(CL_AnimeSeries_User contractSerie, JMMUser curUser) - { - //Directories don't count - if (IsDirectory) - { - return false; - } - - if (contractSerie?.AniDBAnime?.AniDBAnime == null) - { - return false; - } - - if (curUser?.GetHideCategories().FindInEnumerable(contractSerie.AniDBAnime.AniDBAnime.GetAllTags()) ?? - false) - { - return false; - } - - var exclude = BaseCondition == (int)GroupFilterBaseCondition.Exclude; - - return Conditions.All(gfc => exclude ^ EvaluateConditions(contractSerie, gfc)); - } - - private bool EvaluateConditions(CL_AnimeSeries_User contractSerie, GroupFilterCondition gfc) - { - var style = NumberStyles.Number; - var culture = CultureInfo.InvariantCulture; - - switch (gfc.GetConditionTypeEnum()) - { - case GroupFilterConditionType.MissingEpisodes: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - (contractSerie.MissingEpisodeCount > 0 || contractSerie.MissingEpisodeCountGroups > 0) == - false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - (contractSerie.MissingEpisodeCount > 0 || contractSerie.MissingEpisodeCountGroups > 0)) - { - return false; - } - - break; - - case GroupFilterConditionType.MissingEpisodesCollecting: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractSerie.MissingEpisodeCountGroups > 0 == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractSerie.MissingEpisodeCountGroups > 0) - { - return false; - } - - break; - case GroupFilterConditionType.Tag: - var tags = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .Where(a => !string.IsNullOrWhiteSpace(a)) - .ToList(); - var tagsFound = - tags.Any(a => contractSerie.AniDBAnime.AniDBAnime.GetAllTags().Contains(a)); - if ((gfc.GetConditionOperatorEnum() == GroupFilterOperator.In || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include) && !tagsFound) - { - return false; - } - - if ((gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude) && tagsFound) - { - return false; - } - - break; - case GroupFilterConditionType.Year: - var years = new HashSet<int>(); - var parameterStrings = gfc.ConditionParameter.Trim().Split(','); - foreach (var yearString in parameterStrings) - { - if (int.TryParse(yearString.Trim(), out var paramYear)) - { - years.Add(paramYear); - } - } - - if (years.Count <= 0) - { - return false; - } - - switch (gfc.GetConditionOperatorEnum()) - { - case GroupFilterOperator.Include: - case GroupFilterOperator.In: - if (years.Any(year => contractSerie.AniDBAnime.IsInYear(year))) - { - return true; - } - - return false; - case GroupFilterOperator.Exclude: - case GroupFilterOperator.NotIn: - if (years.Any(year => contractSerie.AniDBAnime.IsInYear(year))) - { - return false; - } - - return true; - } - - break; - case GroupFilterConditionType.Season: - var paramStrings = gfc.ConditionParameter.Trim().Split(',').Select(a => - { - var b = a.Trim().Split(' '); - if (!Enum.TryParse(b[0], out AnimeSeason season)) - { - return null; - } - - if (!int.TryParse(b[1], out var year)) - { - return null; - } - - return Tuple.Create(season, year); - }).Where(a => a != null).ToArray(); - - switch (gfc.GetConditionOperatorEnum()) - { - case GroupFilterOperator.Include: - case GroupFilterOperator.In: - return paramStrings.Any(a => contractSerie?.AniDBAnime?.IsInSeason(a.Item1, a.Item2) ?? false); - case GroupFilterOperator.Exclude: - case GroupFilterOperator.NotIn: - return !paramStrings.Any(a => contractSerie?.AniDBAnime?.IsInSeason(a.Item1, a.Item2) ?? false); - } - - break; - case GroupFilterConditionType.HasWatchedEpisodes: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractSerie.WatchedEpisodeCount > 0 == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractSerie.WatchedEpisodeCount > 0) - { - return false; - } - - break; - - case GroupFilterConditionType.HasUnwatchedEpisodes: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractSerie.UnwatchedEpisodeCount > 0 == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractSerie.UnwatchedEpisodeCount > 0) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedTvDBInfo: - if (contractSerie.AniDBAnime.AniDBAnime.AnimeType == (int)AnimeType.Movie || - contractSerie.AniDBAnime.AniDBAnime.Restricted > 0) - { - return false; - } - - var tvDBInfoMissing = !RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(contractSerie.AniDB_ID).Any(); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && tvDBInfoMissing) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && !tvDBInfoMissing) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedMALInfo: - var malMissing = contractSerie.CrossRefAniDBMAL == null || - contractSerie.CrossRefAniDBMAL.Count == 0; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && malMissing) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && !malMissing) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedMovieDBInfo: - if (contractSerie.AniDBAnime.AniDBAnime.AnimeType != (int)AnimeType.Movie || - contractSerie.AniDBAnime.AniDBAnime.Restricted > 0) - { - return false; - } - - var movieMissing = contractSerie.CrossRefAniDBMovieDB == null; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && movieMissing) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && !movieMissing) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedTvDBOrMovieDBInfo: - // return true if excluding - if (contractSerie.AniDBAnime.AniDBAnime.Restricted > 0) - { - return false; - } - - var movieLinkMissing = contractSerie.CrossRefAniDBMovieDB == null; - var tvlinkMissing = !RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(contractSerie.AniDB_ID).Any(); - var bothMissing = movieLinkMissing && tvlinkMissing; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && bothMissing) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && !bothMissing) - { - return false; - } - - break; - - case GroupFilterConditionType.CompletedSeries: - var completed = contractSerie.AniDBAnime.AniDBAnime.EndDate.HasValue && - contractSerie.AniDBAnime.AniDBAnime.EndDate.Value < DateTime.Now && - !(contractSerie.MissingEpisodeCount > 0 || - contractSerie.MissingEpisodeCountGroups > 0); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && !completed) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && completed) - { - return false; - } - - break; - - case GroupFilterConditionType.FinishedAiring: - var finished = contractSerie.AniDBAnime.AniDBAnime.EndDate.HasValue && - contractSerie.AniDBAnime.AniDBAnime.EndDate.Value < DateTime.Now; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && !finished) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && finished) - { - return false; - } - - break; - - case GroupFilterConditionType.UserVoted: - var voted = contractSerie.AniDBAnime.UserVote is { VoteType: (int)AniDBVoteType.Anime }; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && !voted) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && voted) - { - return false; - } - - break; - - case GroupFilterConditionType.UserVotedAny: - var votedany = contractSerie.AniDBAnime.UserVote != null; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && !votedany) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && votedany) - { - return false; - } - - break; - - case GroupFilterConditionType.AirDate: - DateTime filterDate; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDate = DateTime.Today.AddDays(0 - days); - } - else - { - filterDate = GroupFilterHelper.GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractSerie.AniDBAnime.AniDBAnime.AirDate.HasValue || - contractSerie.AniDBAnime.AniDBAnime.AirDate.Value < filterDate) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (!contractSerie.AniDBAnime.AniDBAnime.AirDate.HasValue || - contractSerie.AniDBAnime.AniDBAnime.AirDate.Value > filterDate) - { - return false; - } - } - - break; - case GroupFilterConditionType.LatestEpisodeAirDate: - DateTime filterDateEpisodeLastAired; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateEpisodeLastAired = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateEpisodeLastAired = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractSerie.LatestEpisodeAirDate.HasValue) - { - return false; - } - - if (contractSerie.LatestEpisodeAirDate.Value < filterDateEpisodeLastAired) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (!contractSerie.LatestEpisodeAirDate.HasValue) - { - return false; - } - - if (contractSerie.LatestEpisodeAirDate.Value > filterDateEpisodeLastAired) - { - return false; - } - } - - break; - case GroupFilterConditionType.SeriesCreatedDate: - DateTime filterDateSeries; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateSeries = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateSeries = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (contractSerie.DateTimeCreated < filterDateSeries) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (contractSerie.DateTimeCreated > filterDateSeries) - { - return false; - } - } - - break; - - case GroupFilterConditionType.EpisodeWatchedDate: - DateTime filterDateEpsiodeWatched; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateEpsiodeWatched = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateEpsiodeWatched = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractSerie.WatchedDate.HasValue) - { - return false; - } - - if (contractSerie.WatchedDate.Value < filterDateEpsiodeWatched) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (contractSerie?.WatchedDate == null) - { - return false; - } - - if (contractSerie.WatchedDate.Value > filterDateEpsiodeWatched) - { - return false; - } - } - - break; - - case GroupFilterConditionType.EpisodeAddedDate: - DateTime filterDateEpisodeAdded; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateEpisodeAdded = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateEpisodeAdded = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractSerie.EpisodeAddedDate.HasValue) - { - return false; - } - - if (contractSerie.EpisodeAddedDate.Value < filterDateEpisodeAdded) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (!contractSerie.EpisodeAddedDate.HasValue) - { - return false; - } - - if (contractSerie.EpisodeAddedDate.Value > filterDateEpisodeAdded) - { - return false; - } - } - - break; - - case GroupFilterConditionType.EpisodeCount: - int.TryParse(gfc.ConditionParameter, out var epCount); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan && - contractSerie.AniDBAnime.AniDBAnime.EpisodeCount < epCount) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan && - contractSerie.AniDBAnime.AniDBAnime.EpisodeCount > epCount) - { - return false; - } - - break; - - case GroupFilterConditionType.AniDBRating: - decimal.TryParse(gfc.ConditionParameter, style, culture, out var dRating); - var totalVotes = contractSerie.AniDBAnime.AniDBAnime.VoteCount + - contractSerie.AniDBAnime.AniDBAnime.TempVoteCount; - decimal totalRating = contractSerie.AniDBAnime.AniDBAnime.Rating * - contractSerie.AniDBAnime.AniDBAnime.VoteCount + - contractSerie.AniDBAnime.AniDBAnime.TempRating * - contractSerie.AniDBAnime.AniDBAnime.TempVoteCount; - var thisRating = totalVotes == 0 ? 0 : totalRating / totalVotes / 100; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan && thisRating < dRating) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan && thisRating > dRating) - { - return false; - } - - break; - - case GroupFilterConditionType.UserRating: - decimal.TryParse(gfc.ConditionParameter, style, culture, out var dUserRating); - decimal val = contractSerie.AniDBAnime.UserVote?.VoteValue ?? 0; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan && val < dUserRating) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan && val > dUserRating) - { - return false; - } - - break; - - - case GroupFilterConditionType.CustomTags: - var ctags = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundTag = - ctags.FindInEnumerable(contractSerie.AniDBAnime.CustomTags.Select(a => a.TagName)); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundTag) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundTag) - { - return false; - } - - break; - - case GroupFilterConditionType.AnimeType: - var ctypes = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select( - a => ((int)Commons.Extensions.Models.RawToType(a.ToLowerInvariant())).ToString()) - .ToList(); - var foundAnimeType = ctypes.Contains(contractSerie.AniDBAnime.AniDBAnime.AnimeType.ToString()); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundAnimeType) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundAnimeType) - { - return false; - } - - break; - - case GroupFilterConditionType.VideoQuality: - var vqs = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundVid = vqs.FindInEnumerable(contractSerie.AniDBAnime.Stat_AllVideoQuality); - var foundVidAllEps = - vqs.FindInEnumerable(contractSerie.AniDBAnime.Stat_AllVideoQuality_Episodes); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundVid) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundVid) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.InAllEpisodes && !foundVidAllEps) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotInAllEpisodes && foundVidAllEps) - { - return false; - } - - break; - - case GroupFilterConditionType.AudioLanguage: - var als = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundLang = als.FindInEnumerable(contractSerie.AniDBAnime.Stat_AudioLanguages); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundLang) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundLang) - { - return false; - } - - break; - - case GroupFilterConditionType.SubtitleLanguage: - var ass = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundSub = ass.FindInEnumerable(contractSerie.AniDBAnime.Stat_AudioLanguages); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundSub) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundSub) - { - return false; - } - - break; - } - - return true; - } - - public static DateTime GetDateFromString(string sDate) - { - try - { - var year = int.Parse(sDate.Substring(0, 4)); - var month = int.Parse(sDate.Substring(4, 2)); - var day = int.Parse(sDate.Substring(6, 2)); - - return new DateTime(year, month, day); - } - catch - { - return DateTime.Today; - } - } - - /// <summary> - /// Updates the <see cref="GroupsIdsString"/> and/or <see cref="SeriesIdsString"/> properties - /// based on the current contents of <see cref="GroupsIds"/> and <see cref="SeriesIds"/>. - /// </summary> - /// <param name="updateGroups"><c>true</c> to update <see cref="GroupsIdsString"/>; otherwise, <c>false</c>.</param> - /// <param name="updateSeries"><c>true</c> to update <see cref="SeriesIds"/>; otherwise, <c>false</c>.</param> - public void UpdateEntityReferenceStrings(bool updateGroups = true, bool updateSeries = true) - { - WriteLock( - () => - { - if (updateGroups) - { - GroupsIdsString = JsonConvert.SerializeObject(GroupsIds); - GroupsIdsVersion = GROUPFILTER_VERSION; - } - - if (updateSeries) - { - SeriesIdsString = JsonConvert.SerializeObject(SeriesIds); - SeriesIdsVersion = SERIEFILTER_VERSION; - } - } - ); - } - - public void QueueUpdate() - { - var commandFactory = Utils.ServiceContainer.GetRequiredService<ICommandRequestFactory>(); - commandFactory.CreateAndSave<CommandRequest_RefreshGroupFilter>(c => c.GroupFilterID = GroupFilterID); - } - - public override bool Equals(object obj) - { - var other = obj as SVR_GroupFilter; - if (other?.ApplyToSeries != ApplyToSeries) - { - return false; - } - - if (other.BaseCondition != BaseCondition) - { - return false; - } - - if (other.FilterType != FilterType) - { - return false; - } - - if (other.InvisibleInClients != InvisibleInClients) - { - return false; - } - - if (other.Locked != Locked) - { - return false; - } - - if (other.ParentGroupFilterID != ParentGroupFilterID) - { - return false; - } - - if (other.GroupFilterName != GroupFilterName) - { - return false; - } - - if (other.SortingCriteria != SortingCriteria) - { - return false; - } - - if (Conditions == null || Conditions.Count == 0) - { - Conditions = RepoFactory.GroupFilterCondition.GetByGroupFilterID(GroupFilterID); - RepoFactory.GroupFilter.Save(this); - } - - if (other.Conditions == null || other.Conditions.Count == 0) - { - other.Conditions = RepoFactory.GroupFilterCondition.GetByGroupFilterID(other.GroupFilterID); - RepoFactory.GroupFilter.Save(other); - } - - if (Conditions != null && other.Conditions != null) - { - if (!Conditions.ContentEquals(other.Conditions)) - { - return false; - } - } - - return true; - } - - public override int GetHashCode() - { - return 0; // Always use equals - } - - private void ReadLock(Action action) - { - _lock.EnterReadLock(); - try - { - action.Invoke(); - } - finally - { - _lock.ExitReadLock(); - } - } - - private T ReadLock<T>(Func<T> action) - { - _lock.EnterReadLock(); - try - { - return action.Invoke(); - } - finally - { - _lock.ExitReadLock(); - } - } - - private void WriteLock(Action action) - { - _lock.EnterWriteLock(); - try - { - action.Invoke(); - } - finally - { - _lock.ExitWriteLock(); - } - } - - private T WriteLock<T>(Func<T> action) - { - _lock.EnterWriteLock(); - try - { - return action.Invoke(); - } - finally - { - _lock.ExitWriteLock(); - } - } -} diff --git a/Shoko.Server/Repositories/Cached/GroupFilterRepository.cs b/Shoko.Server/Repositories/Cached/GroupFilterRepository.cs deleted file mode 100644 index d2b65629c..000000000 --- a/Shoko.Server/Repositories/Cached/GroupFilterRepository.cs +++ /dev/null @@ -1,418 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using Newtonsoft.Json; -using NutzCode.InMemoryIndex; -using Shoko.Commons.Extensions; -using Shoko.Commons.Properties; -using Shoko.Models; -using Shoko.Models.Enums; -using Shoko.Models.Server; -using Shoko.Server.Extensions; -using Shoko.Server.Models; -using Shoko.Server.Repositories.NHibernate; -using Shoko.Server.Server; -using Constants = Shoko.Server.Server.Constants; - -namespace Shoko.Server.Repositories.Cached; - -public class GroupFilterRepository : BaseCachedRepository<SVR_GroupFilter, int> -{ - private PocoIndex<int, SVR_GroupFilter, int> Parents; - - private BiDictionaryManyToMany<int, GroupFilterConditionType> Types; - - private readonly ChangeTracker<int> Changes = new(); - - public List<SVR_GroupFilter> PostProcessFilters { get; set; } - - public GroupFilterRepository() - { - EndSaveCallback = obj => - { - Types[obj.GroupFilterID] = obj.Types; - Changes.AddOrUpdate(obj.GroupFilterID); - }; - EndDeleteCallback = obj => - { - Types.Remove(obj.GroupFilterID); - Changes.Remove(obj.GroupFilterID); - }; - } - - protected override int SelectKey(SVR_GroupFilter entity) - { - return entity.GroupFilterID; - } - - public override void PopulateIndexes() - { - Changes.AddOrUpdateRange(Cache.Keys); - Parents = Cache.CreateIndex(a => a.ParentGroupFilterID ?? 0); - Types = - new BiDictionaryManyToMany<int, GroupFilterConditionType>( - Cache.Values.ToDictionary(a => a.GroupFilterID, a => a.Types)); - PostProcessFilters = new List<SVR_GroupFilter>(); - } - - public override void RegenerateDb() { } - - - public override void PostProcess() { } - - public void CleanUpEmptyDirectoryFilters() - { - var toremove = GetAll().Where(a => (a.FilterType & (int)GroupFilterType.Directory) != 0) - .Where(gf => gf.GroupsIdsVersion == SVR_GroupFilter.GROUPFILTER_VERSION && - gf.SeriesIdsVersion == SVR_GroupFilter.SERIEFILTER_VERSION && gf.GroupsIds.Count == 0 && - string.IsNullOrEmpty(gf.GroupsIdsString) && gf.SeriesIds.Count == 0 && - string.IsNullOrEmpty(gf.SeriesIdsString)).ToList(); - if (toremove.Count > 0) - { - Delete(toremove); - } - } - - public void CreateOrVerifyLockedFilters() { } - - public override void Save(SVR_GroupFilter obj) - { - Save(obj, false); - } - - public void Save(SVR_GroupFilter obj, bool onlyconditions) - { - WriteLock( - () => - { - if (!onlyconditions) - { - obj.UpdateEntityReferenceStrings(); - } - - var resaveConditions = obj.GroupFilterID == 0; - obj.GroupConditions = JsonConvert.SerializeObject(obj._conditions); - obj.GroupConditionsVersion = SVR_GroupFilter.GROUPCONDITIONS_VERSION; - base.Save(obj); - if (resaveConditions) - { - obj.Conditions.ForEach(a => a.GroupFilterID = obj.GroupFilterID); - Save(obj, true); - } - } - ); - } - - public override void Save(IReadOnlyCollection<SVR_GroupFilter> objs) - { - foreach (var obj in objs) - { - Save(obj); - } - } - - public override void Delete(IReadOnlyCollection<SVR_GroupFilter> objs) - { - foreach (var cr in objs) - { - base.Delete(cr); - } - } - - /// <summary> - /// Updates a batch of <see cref="SVR_GroupFilter"/>s. - /// </summary> - /// <remarks> - /// This method ONLY updates existing <see cref="SVR_GroupFilter"/>s. It will not insert any that don't already exist. - /// </remarks> - /// <param name="session">The NHibernate session.</param> - /// <param name="groupFilters">The batch of <see cref="SVR_GroupFilter"/>s to update.</param> - /// <exception cref="ArgumentNullException"><paramref name="session"/> or <paramref name="groupFilters"/> is <c>null</c>.</exception> - public void BatchUpdate(ISessionWrapper session, IEnumerable<SVR_GroupFilter> groupFilters) - { - if (session == null) - { - throw new ArgumentNullException(nameof(session)); - } - - if (groupFilters == null) - { - throw new ArgumentNullException(nameof(groupFilters)); - } - - foreach (var groupFilter in groupFilters) - { - session.Update(groupFilter); - UpdateCache(groupFilter); - Changes.AddOrUpdate(groupFilter.GroupFilterID); - } - } - - public List<SVR_GroupFilter> GetByParentID(int parentid) - { - return ReadLock(() => Parents.GetMultiple(parentid)); - } - - public List<SVR_GroupFilter> GetTopLevel() - { - return GetByParentID(0); - } - - /// <summary> - /// Calculates what groups should belong to tag related group filters. - /// </summary> - /// <param name="session">The NHibernate session.</param> - /// <returns>A <see cref="ILookup{TKey,TElement}"/> that maps group filter ID to anime group IDs.</returns> - /// <exception cref="ArgumentNullException"><paramref name="session"/> is <c>null</c>.</exception> - public void CalculateAnimeSeriesPerTagGroupFilter(ISessionWrapper session) - { - if (session == null) - { - throw new ArgumentNullException(nameof(session)); - } - - var tagsdirec = GetAll(session).FirstOrDefault( - a => a.FilterType == (int)(GroupFilterType.Directory | GroupFilterType.Tag)); - - DropAllTagFilters(session); - - // user -> tag -> series - var somethingDictionary = - new ConcurrentDictionary<int, ConcurrentDictionary<string, HashSet<int>>>(); - var users = new List<SVR_JMMUser> { null }; - users.AddRange(RepoFactory.JMMUser.GetAll()); - var tags = RepoFactory.AniDB_Tag.GetAll().ToLookup(a => a?.TagName?.ToLowerInvariant()); - - Parallel.ForEach(tags, tag => - { - foreach (var series in tag.ToList().SelectMany(a => RepoFactory.AniDB_Anime_Tag.GetAnimeWithTag(a.TagID))) - { - var seriesTags = series.GetAnime()?.GetAllTags(); - foreach (var user in users) - { - if (user?.GetHideCategories().FindInEnumerable(seriesTags) ?? false) - { - continue; - } - - if (somethingDictionary.ContainsKey(user?.JMMUserID ?? 0)) - { - if (somethingDictionary[user?.JMMUserID ?? 0].ContainsKey(tag.Key)) - { - somethingDictionary[user?.JMMUserID ?? 0][tag.Key] - .Add(series.AnimeSeriesID); - } - else - { - somethingDictionary[user?.JMMUserID ?? 0].AddOrUpdate(tag.Key, - new HashSet<int> { series.AnimeSeriesID }, (oldTag, oldIDs) => - { - lock (oldIDs) - { - oldIDs.Add(series.AnimeSeriesID); - } - - return oldIDs; - }); - } - } - else - { - somethingDictionary.AddOrUpdate(user?.JMMUserID ?? 0, id => - { - var newdict = new ConcurrentDictionary<string, HashSet<int>>(); - newdict.AddOrUpdate(tag.Key, new HashSet<int> { series.AnimeSeriesID }, - (oldTag, oldIDs) => - { - lock (oldIDs) - { - oldIDs.Add(series.AnimeSeriesID); - } - - return oldIDs; - }); - return newdict; - }, - (i, value) => - { - if (value.ContainsKey(tag.Key)) - { - value[tag.Key] - .Add(series.AnimeSeriesID); - } - else - { - value.AddOrUpdate(tag.Key, - new HashSet<int> { series.AnimeSeriesID }, (oldTag, oldIDs) => - { - lock (oldIDs) - { - oldIDs.Add(series.AnimeSeriesID); - } - - return oldIDs; - }); - } - - return value; - }); - } - } - } - }); - - var lookup = somethingDictionary.Keys.Where(a => somethingDictionary[a] != null).ToDictionary(key => key, key => - somethingDictionary[key].Where(a => a.Value != null) - .SelectMany(p => p.Value.Select(a => Tuple.Create(p.Key, a))) - .ToLookup(p => p.Item1, p => p.Item2)); - - CreateAllTagFilters(session, tagsdirec, lookup); - } - - private void DropAllTagFilters(ISessionWrapper session) - { - ClearCache(); - Lock(() => - { - using var trans = session.BeginTransaction(); - session.CreateQuery($"DELETE FROM {nameof(SVR_GroupFilter)} WHERE FilterType = {(int)GroupFilterType.Tag};") - .ExecuteUpdate(); - trans.Commit(); - }); - } - - private void CreateAllTagFilters(ISessionWrapper session, SVR_GroupFilter tagsdirec, - Dictionary<int, ILookup<string, int>> lookup) - { - if (tagsdirec == null) - { - return; - } - - var alltags = new HashSet<string>( - RepoFactory.AniDB_Tag.GetAllForLocalSeries().Select(a => a.TagName.Replace('`', '\'')), - StringComparer.InvariantCultureIgnoreCase); - var toAdd = new List<SVR_GroupFilter>(alltags.Count); - - var users = RepoFactory.JMMUser.GetAll().ToList(); - - //AniDB Tags are in english so we use en-us culture - var tinfo = new CultureInfo("en-US", false).TextInfo; - foreach (var s in alltags) - { - // this is creating new tag filters, so locking isn't completely necessary. - // Ideally, we would create a blank filter to ensure an ID exists, but then it would be empty, - // and would have data inconsistencies, anyway - var yf = new SVR_GroupFilter - { - ParentGroupFilterID = tagsdirec.GroupFilterID, - InvisibleInClients = 0, - ApplyToSeries = 1, - GroupFilterName = tinfo.ToTitleCase(s), - BaseCondition = 1, - Locked = 1, - SortingCriteria = "5;1", - FilterType = (int)GroupFilterType.Tag - }; - yf.SeriesIds[0] = lookup[0][s.ToLowerInvariant()].ToHashSet(); - yf.GroupsIds[0] = yf.SeriesIds[0] - .Select(id => RepoFactory.AnimeSeries.GetByID(id).TopLevelAnimeGroup?.AnimeGroupID ?? -1) - .Where(id => id != -1).ToHashSet(); - foreach (var user in users) - { - var key = s.ToLowerInvariant(); - // this will happen if you only have porn or none the series you have are allowed by a user - if (!lookup.ContainsKey(user.JMMUserID) || !lookup[user.JMMUserID].Contains(key)) - { - continue; - } - - yf.SeriesIds[user.JMMUserID] = lookup[user.JMMUserID][key].ToHashSet(); - yf.GroupsIds[user.JMMUserID] = yf.SeriesIds[user.JMMUserID] - .Select(id => RepoFactory.AnimeSeries.GetByID(id).TopLevelAnimeGroup?.AnimeGroupID ?? -1) - .Where(id => id != -1).ToHashSet(); - } - - Lock(() => - { - using var trans = session.BeginTransaction(); - // get an ID - session.Insert(yf); - trans.Commit(); - }); - - var gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.Tag, - ConditionOperator = (int)GroupFilterOperator.In, - ConditionParameter = s, - GroupFilterID = yf.GroupFilterID - }; - yf.Conditions.Add(gfc); - yf.UpdateEntityReferenceStrings(); - yf.GroupConditions = JsonConvert.SerializeObject(yf._conditions); - yf.GroupConditionsVersion = SVR_GroupFilter.GROUPCONDITIONS_VERSION; - toAdd.Add(yf); - } - - Populate(session, false); - foreach (var filters in toAdd.Batch(50)) - { - Lock(() => - { - using var trans = session.BeginTransaction(); - BatchUpdate(session, filters); - trans.Commit(); - }); - } - } - - public List<SVR_GroupFilter> GetLockedGroupFilters() - { - return ReadLock(() => Cache.Values.Where(a => a.Locked == 1).ToList()); - } - - public List<SVR_GroupFilter> GetWithConditionTypesAndAll(HashSet<GroupFilterConditionType> types) - { - return ReadLock( - () => - { - var filters = new HashSet<int>( - Cache.Values - .Where(a => a.FilterType == (int)GroupFilterType.All) - .Select(a => a.GroupFilterID) - ); - foreach (var t in types) - { - filters.UnionWith(Types.FindInverse(t)); - } - - return filters.Select(a => Cache.Get(a)).ToList(); - } - ); - } - - public List<SVR_GroupFilter> GetWithConditionsTypes(HashSet<GroupFilterConditionType> types) - { - return ReadLock( - () => - { - var filters = new HashSet<int>(); - foreach (var t in types) - { - filters.UnionWith(Types.FindInverse(t)); - } - - return filters.Select(a => Cache.Get(a)).ToList(); - } - ); - } - - public ChangeTracker<int> GetChangeTracker() - { - return Changes; - } -} diff --git a/Shoko.Server/Repositories/RepoFactory.cs b/Shoko.Server/Repositories/RepoFactory.cs index 44cdb9360..99fb55c61 100644 --- a/Shoko.Server/Repositories/RepoFactory.cs +++ b/Shoko.Server/Repositories/RepoFactory.cs @@ -1,14 +1,12 @@ using System; using System.Collections.Generic; using System.Runtime; -using System.Threading.Tasks; using NLog; using Shoko.Commons.Extensions; using Shoko.Commons.Properties; using Shoko.Server.Repositories.Cached; using Shoko.Server.Repositories.Direct; using Shoko.Server.Server; -using Shoko.Server.Settings; // ReSharper disable InconsistentNaming @@ -93,7 +91,6 @@ public static class RepoFactory public static AnimeCharacterRepository AnimeCharacter { get; } = new(); public static AnimeStaffRepository AnimeStaff { get; } = new(); public static CrossRef_Anime_StaffRepository CrossRef_Anime_Staff { get; } = new(); - public static GroupFilterRepository GroupFilter { get; } = new(); public static FilterPresetRepository FilterPreset { get; } = new(); /************** DEPRECATED **************/ diff --git a/Shoko.Server/Server/Startup.cs b/Shoko.Server/Server/Startup.cs index 079c92c95..4b09a6c38 100644 --- a/Shoko.Server/Server/Startup.cs +++ b/Shoko.Server/Server/Startup.cs @@ -14,7 +14,7 @@ using Shoko.Server.API; using Shoko.Server.Commands; using Shoko.Server.Filters; -using Shoko.Server.PlexAndKodi; +using Shoko.Server.Filters.Legacy; using Shoko.Server.Plugin; using Shoko.Server.Providers.AniDB; using Shoko.Server.Providers.MovieDB; diff --git a/Shoko.Server/Utilities/GroupFilterHelper.cs b/Shoko.Server/Utilities/GroupFilterHelper.cs deleted file mode 100644 index e8f61a349..000000000 --- a/Shoko.Server/Utilities/GroupFilterHelper.cs +++ /dev/null @@ -1,326 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Shoko.Models.Client; -using Shoko.Models.Enums; -using Shoko.Server.Models; - -namespace Shoko.Server; - -public class GroupFilterHelper -{ - public static string GetTextForEnum_Sorting(GroupFilterSorting sort) - { - switch (sort) - { - case GroupFilterSorting.AniDBRating: - return "AniDB Rating"; - case GroupFilterSorting.EpisodeAddedDate: - return "Episode Added Date"; - case GroupFilterSorting.EpisodeAirDate: - return "Episode Air Date"; - case GroupFilterSorting.EpisodeWatchedDate: - return "Episode Watched Date"; - case GroupFilterSorting.GroupName: - return "Group Name"; - case GroupFilterSorting.SortName: - return "Sort Name"; - case GroupFilterSorting.MissingEpisodeCount: - return "Missing Episode Count"; - case GroupFilterSorting.SeriesAddedDate: - return "Series Added Date"; - case GroupFilterSorting.SeriesCount: - return "Series Count"; - case GroupFilterSorting.UnwatchedEpisodeCount: - return "Unwatched Episode Count"; - case GroupFilterSorting.UserRating: - return "User Rating"; - case GroupFilterSorting.Year: - return "Year"; - default: - return "AniDB Rating"; - } - } - - public static GroupFilterSorting GetEnumForText_Sorting(string enumDesc) - { - switch (enumDesc) - { - case "AniDB Rating": - return GroupFilterSorting.AniDBRating; - case "Episode Added Date": - return GroupFilterSorting.EpisodeAddedDate; - case "Episode Air Date": - return GroupFilterSorting.EpisodeAirDate; - case "Episode Watched Date": - return GroupFilterSorting.EpisodeWatchedDate; - case "Group Name": - return GroupFilterSorting.GroupName; - case "Sort Name": - return GroupFilterSorting.SortName; - case "Missing Episode Count": - return GroupFilterSorting.MissingEpisodeCount; - case "Series Added Date": - return GroupFilterSorting.SeriesAddedDate; - case "Series Count": - return GroupFilterSorting.SeriesCount; - case "Unwatched Episode Count": - return GroupFilterSorting.UnwatchedEpisodeCount; - case "User Rating": - return GroupFilterSorting.UserRating; - case "Year": - return GroupFilterSorting.Year; - default: - return GroupFilterSorting.AniDBRating; - } - } - - public static string GetTextForEnum_SortDirection(GroupFilterSortDirection sort) - { - switch (sort) - { - case GroupFilterSortDirection.Asc: - return "Asc"; - case GroupFilterSortDirection.Desc: - return "Desc"; - default: - return "Asc"; - } - } - - public static GroupFilterSortDirection GetEnumForText_SortDirection(string enumDesc) - { - switch (enumDesc) - { - case "Asc": - return GroupFilterSortDirection.Asc; - case "Desc": - return GroupFilterSortDirection.Desc; - default: - return GroupFilterSortDirection.Asc; - } - } - - public static List<string> GetAllSortTypes() - { - var cons = new List<string> - { - GetTextForEnum_Sorting(GroupFilterSorting.AniDBRating), - GetTextForEnum_Sorting(GroupFilterSorting.EpisodeAddedDate), - GetTextForEnum_Sorting(GroupFilterSorting.EpisodeAirDate), - GetTextForEnum_Sorting(GroupFilterSorting.EpisodeWatchedDate), - GetTextForEnum_Sorting(GroupFilterSorting.GroupName), - GetTextForEnum_Sorting(GroupFilterSorting.MissingEpisodeCount), - GetTextForEnum_Sorting(GroupFilterSorting.SeriesAddedDate), - GetTextForEnum_Sorting(GroupFilterSorting.SeriesCount), - GetTextForEnum_Sorting(GroupFilterSorting.SortName), - GetTextForEnum_Sorting(GroupFilterSorting.UnwatchedEpisodeCount), - GetTextForEnum_Sorting(GroupFilterSorting.UserRating), - GetTextForEnum_Sorting(GroupFilterSorting.Year) - }; - cons.Sort(); - - return cons; - } - - public static List<string> GetQuickSortTypes() - { - var cons = new List<string> - { - //GetTextForEnum_Sorting(GroupFilterSorting.AniDBRating); removed for performance reasons - GetTextForEnum_Sorting(GroupFilterSorting.EpisodeAddedDate), - //GetTextForEnum_Sorting(GroupFilterSorting.EpisodeAirDate); - GetTextForEnum_Sorting(GroupFilterSorting.EpisodeWatchedDate), - GetTextForEnum_Sorting(GroupFilterSorting.GroupName), - GetTextForEnum_Sorting(GroupFilterSorting.MissingEpisodeCount), - GetTextForEnum_Sorting(GroupFilterSorting.SeriesAddedDate), - GetTextForEnum_Sorting(GroupFilterSorting.SeriesCount), - GetTextForEnum_Sorting(GroupFilterSorting.SortName), - GetTextForEnum_Sorting(GroupFilterSorting.UnwatchedEpisodeCount), - GetTextForEnum_Sorting(GroupFilterSorting.UserRating), - GetTextForEnum_Sorting(GroupFilterSorting.Year) - }; - cons.Sort(); - - return cons; - } - - - public static string GetDateAsString(DateTime aDate) - { - return aDate.Year.ToString().PadLeft(4, '0') + - aDate.Month.ToString().PadLeft(2, '0') + - aDate.Day.ToString().PadLeft(2, '0'); - } - - public static DateTime GetDateFromString(string sDate) - { - try - { - var year = int.Parse(sDate.Substring(0, 4)); - var month = int.Parse(sDate.Substring(4, 2)); - var day = int.Parse(sDate.Substring(6, 2)); - - return new DateTime(year, month, day); - } - catch - { - return DateTime.Today; - } - } - - public static string GetDateAsFriendlyString(DateTime aDate) - { - return aDate.ToString("dd MMM yyyy", CultureInfo.CurrentCulture); - } - - public static IEnumerable<CL_AnimeGroup_User> Sort(IEnumerable<CL_AnimeGroup_User> groups, SVR_GroupFilter gf) - { - var isfirst = true; - var query = groups; - foreach (var gfsc in gf.SortCriteriaList) - { - query = Order(query, gfsc, isfirst); - isfirst = false; - } - - return query; - } - - public static IOrderedEnumerable<CL_AnimeGroup_User> Order(IEnumerable<CL_AnimeGroup_User> groups, - GroupFilterSortingCriteria gfsc, bool isfirst) - { - switch (gfsc.SortType) - { - case GroupFilterSorting.Year: - if (gfsc.SortDirection == GroupFilterSortDirection.Asc) - { - return Order(groups, a => a.Stat_AirDate_Min, gfsc.SortDirection, isfirst); - } - - return Order(groups, a => a.Stat_AirDate_Max, gfsc.SortDirection, isfirst); - case GroupFilterSorting.AniDBRating: - return Order(groups, a => a.Stat_AniDBRating, gfsc.SortDirection, isfirst); - case GroupFilterSorting.EpisodeAddedDate: - return Order(groups, a => a.EpisodeAddedDate, gfsc.SortDirection, isfirst); - case GroupFilterSorting.EpisodeAirDate: - return Order(groups, a => a.LatestEpisodeAirDate, gfsc.SortDirection, isfirst); - case GroupFilterSorting.EpisodeWatchedDate: - return Order(groups, a => a.WatchedDate, gfsc.SortDirection, isfirst); - case GroupFilterSorting.MissingEpisodeCount: - return Order(groups, a => a.MissingEpisodeCount, gfsc.SortDirection, isfirst); - case GroupFilterSorting.SeriesAddedDate: - return Order(groups, a => a.Stat_SeriesCreatedDate, gfsc.SortDirection, isfirst); - case GroupFilterSorting.SeriesCount: - return Order(groups, a => a.Stat_SeriesCount, gfsc.SortDirection, isfirst); - case GroupFilterSorting.SortName: - return Order(groups, a => a.SortName, gfsc.SortDirection, isfirst); - case GroupFilterSorting.UnwatchedEpisodeCount: - return Order(groups, a => a.UnwatchedEpisodeCount, gfsc.SortDirection, isfirst); - case GroupFilterSorting.UserRating: - return Order(groups, a => a.Stat_UserVoteOverall, gfsc.SortDirection, isfirst); - case GroupFilterSorting.GroupName: - case GroupFilterSorting.GroupFilterName: - return Order(groups, a => a.GroupName, gfsc.SortDirection, isfirst); - default: - return Order(groups, a => a.GroupName, gfsc.SortDirection, isfirst); - } - } - - private static IOrderedEnumerable<CL_AnimeGroup_User> Order<T>(IEnumerable<CL_AnimeGroup_User> groups, - Func<CL_AnimeGroup_User, T> o, - GroupFilterSortDirection direc, bool isfirst) - { - if (isfirst) - { - return direc == GroupFilterSortDirection.Asc - ? groups.OrderBy(o) - : groups.OrderByDescending(o); - } - - return direc == GroupFilterSortDirection.Asc - ? ((IOrderedEnumerable<CL_AnimeGroup_User>)groups).ThenBy(o) - : ((IOrderedEnumerable<CL_AnimeGroup_User>)groups).ThenByDescending(o); - } - - /* - public static List<SortPropOrFieldAndDirection> GetSortDescriptions(GroupFilter gf) - { - List<SortPropOrFieldAndDirection> sortlist = new List<SortPropOrFieldAndDirection>(); - - return sortlist; - } - - public static SortPropOrFieldAndDirection GetSortDescription(GroupFilterSorting sortType, - GroupFilterSortDirection sortDirection) - { - string sortColumn = string.Empty; - bool sortDescending = sortDirection == GroupFilterSortDirection.Desc; - SortType sortFieldType = SortType.eString; - - switch (sortType) - { - case GroupFilterSorting.AniDBRating: - sortColumn = "AniDBRating"; - sortFieldType = SortType.eDoubleOrFloat; - break; - case GroupFilterSorting.EpisodeAddedDate: - sortColumn = "EpisodeAddedDate"; - sortFieldType = SortType.eDateTime; - break; - case GroupFilterSorting.EpisodeAirDate: - sortColumn = "LatestEpisodeAirDate"; - sortFieldType = SortType.eDateTime; - break; - case GroupFilterSorting.EpisodeWatchedDate: - sortColumn = "WatchedDate"; - sortFieldType = SortType.eDateTime; - break; - case GroupFilterSorting.GroupName: - sortColumn = "GroupName"; - sortFieldType = SortType.eString; - break; - case GroupFilterSorting.SortName: - sortColumn = "SortName"; - sortFieldType = SortType.eString; - break; - case GroupFilterSorting.MissingEpisodeCount: - sortColumn = "MissingEpisodeCount"; - sortFieldType = SortType.eInteger; - break; - case GroupFilterSorting.SeriesAddedDate: - sortColumn = "Stat_SeriesCreatedDate"; - sortFieldType = SortType.eDateTime; - break; - case GroupFilterSorting.SeriesCount: - sortColumn = "AllSeriesCount"; - sortFieldType = SortType.eInteger; - break; - case GroupFilterSorting.UnwatchedEpisodeCount: - sortColumn = "UnwatchedEpisodeCount"; - sortFieldType = SortType.eInteger; - break; - case GroupFilterSorting.UserRating: - sortColumn = "Stat_UserVoteOverall"; - sortFieldType = SortType.eDoubleOrFloat; - break; - case GroupFilterSorting.Year: - if (sortDirection == GroupFilterSortDirection.Asc) - sortColumn = "Stat_AirDate_Min"; - else - sortColumn = "Stat_AirDate_Max"; - - sortFieldType = SortType.eDateTime; - break; - default: - sortColumn = "GroupName"; - sortFieldType = SortType.eString; - break; - } - - - return new SortPropOrFieldAndDirection(sortColumn, sortDescending, sortFieldType); - } - */ -} diff --git a/Shoko.Tests/Shoko.Tests/FilterTests.cs b/Shoko.Tests/Shoko.Tests/FilterTests.cs index ff623e277..227c93644 100644 --- a/Shoko.Tests/Shoko.Tests/FilterTests.cs +++ b/Shoko.Tests/Shoko.Tests/FilterTests.cs @@ -28,13 +28,13 @@ public class FilterTests ""; #endregion - public static readonly IEnumerable<object[]> GroupFilterable = new[] { new[] { JsonConvert.DeserializeObject<Filterable>(GroupFilterableString, new IReadOnlySetConverter()) }}; - public static readonly IEnumerable<object[]> GroupUserFilterable = new[] { new[] { JsonConvert.DeserializeObject<UserDependentFilterable>(GroupUserFilterableString, new IReadOnlySetConverter()) }}; - public static readonly IEnumerable<object[]> SeriesFilterable = new[] { new[] { JsonConvert.DeserializeObject<Filterable>(SeriesFilterableString, new IReadOnlySetConverter()) }}; - public static readonly IEnumerable<object[]> SeriesUserFilterable = new[] { new[] { JsonConvert.DeserializeObject<UserDependentFilterable>(SeriesUserFilterableString, new IReadOnlySetConverter()) }}; + public static readonly IEnumerable<object[]> GroupFilterable = new[] { new[] { JsonConvert.DeserializeObject<TestFilterable>(GroupFilterableString, new IReadOnlySetConverter()) }}; + public static readonly IEnumerable<object[]> GroupUserFilterable = new[] { new[] { JsonConvert.DeserializeObject<TestUserDependentFilterable>(GroupUserFilterableString, new IReadOnlySetConverter()) }}; + public static readonly IEnumerable<object[]> SeriesFilterable = new[] { new[] { JsonConvert.DeserializeObject<TestFilterable>(SeriesFilterableString, new IReadOnlySetConverter()) }}; + public static readonly IEnumerable<object[]> SeriesUserFilterable = new[] { new[] { JsonConvert.DeserializeObject<TestUserDependentFilterable>(SeriesUserFilterableString, new IReadOnlySetConverter()) }}; [Theory, MemberData(nameof(GroupFilterable))] - public void GroupFilterable_WithUserFilter_ExpectsException(Filterable group) + public void GroupFilterable_WithUserFilter_ExpectsException(TestFilterable group) { var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), @@ -45,7 +45,7 @@ public void GroupFilterable_WithUserFilter_ExpectsException(Filterable group) } [Theory, MemberData(nameof(GroupFilterable))] - public void GroupFilterable_WithoutUserFilter_ExpectsTrue(Filterable group) + public void GroupFilterable_WithoutUserFilter_ExpectsTrue(TestFilterable group) { var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), new HasVideoSourceExpression("Web")); @@ -55,7 +55,7 @@ public void GroupFilterable_WithoutUserFilter_ExpectsTrue(Filterable group) } [Theory, MemberData(nameof(GroupFilterable))] - public void GroupFilterable_WithDateFunctionFilter_ExpectsFalse(Filterable group) + public void GroupFilterable_WithDateFunctionFilter_ExpectsFalse(TestFilterable group) { var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), new DateGreaterThanEqualsExpression(new LastAddedDateSelector(), @@ -66,18 +66,17 @@ public void GroupFilterable_WithDateFunctionFilter_ExpectsFalse(Filterable group } [Theory, MemberData(nameof(GroupFilterable))] - public void GroupFilterable_WithDateFunctionFilter_ExpectsTrue(Filterable group) + public void GroupFilterable_WithDateFunctionFilter_ExpectsTrue(TestFilterable group) { var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), - new DateGreaterThanEqualsExpression(new LastAddedDateSelector(), new DateDiffFunction( - new DateAddFunction(new TodayFunction(), TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1)), TimeSpan.FromDays(120)))); + new DateGreaterThanEqualsExpression(new LastAddedDateSelector(), DateTime.Parse("2023-4-15"))); Assert.False(top.UserDependent); Assert.True(top.Evaluate(group)); } [Theory, MemberData(nameof(GroupUserFilterable))] - public void GroupUserFilterable_WithUserFilter_ExpectsTrue(UserDependentFilterable group) + public void GroupUserFilterable_WithUserFilter_ExpectsTrue(TestUserDependentFilterable group) { var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), new HasWatchedEpisodesExpression()); @@ -87,7 +86,7 @@ public void GroupUserFilterable_WithUserFilter_ExpectsTrue(UserDependentFilterab } [Theory, MemberData(nameof(SeriesFilterable))] - public void SeriesFilterable_WithUserFilter_ExpectsException(Filterable series) + public void SeriesFilterable_WithUserFilter_ExpectsException(TestFilterable series) { var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), new HasWatchedEpisodesExpression()); @@ -97,7 +96,7 @@ public void SeriesFilterable_WithUserFilter_ExpectsException(Filterable series) } [Theory, MemberData(nameof(SeriesUserFilterable))] - public void SeriesUserFilterable_WithUserFilter_ExpectsTrue(UserDependentFilterable series) + public void SeriesUserFilterable_WithUserFilter_ExpectsTrue(TestUserDependentFilterable series) { var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), new HasWatchedEpisodesExpression()); diff --git a/Shoko.Tests/Shoko.Tests/LegacyFilterTests.cs b/Shoko.Tests/Shoko.Tests/LegacyFilterTests.cs new file mode 100644 index 000000000..e65f9a6bb --- /dev/null +++ b/Shoko.Tests/Shoko.Tests/LegacyFilterTests.cs @@ -0,0 +1,139 @@ +using System.Collections.Generic; +using Shoko.Models.Enums; +using Shoko.Models.Server; +using Shoko.Server.Filters; +using Shoko.Server.Filters.Info; +using Shoko.Server.Filters.Legacy; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Filters.User; +using Shoko.Server.Models; +using Xunit; + +namespace Shoko.Tests; + +public class LegacyFilterTests +{ + [Fact] + public void TryConvertToConditions_InvalidFilter_ExpectsNull() + { + var top = new OrExpression(new AndExpression(new HasTagExpression("comedy"), + new NotExpression(new HasTagExpression("18 restricted"))), + new HasWatchedEpisodesExpression()); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + Assert.False(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Null(conditions); + } + + [Fact] + public void TryConvertToConditions_FilterWithTagIncludeAndExclude() + { + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), + new NotExpression(new HasTagExpression("18 restricted"))), + new HasWatchedEpisodesExpression()); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy" + }, + new() + { + ConditionOperator = (int)GroupFilterOperator.NotIn, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "18 restricted" + }, + new() + { + ConditionOperator = (int)GroupFilterOperator.Include, + ConditionType = (int)GroupFilterConditionType.HasWatchedEpisodes, + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void TryConvertToConditions_FilterWithTagInOperator_IncludeBaseCondition() + { + var top = new OrExpression(new OrExpression(new HasTagExpression("comedy"), new HasTagExpression("shounen")), new HasTagExpression("action")); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy,shounen,action" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void TryConvertToConditions_FilterWithTagInOperator_ExcludeBaseCondition() + { + var top = new NotExpression(new OrExpression(new OrExpression(new HasTagExpression("comedy"), new HasTagExpression("shounen")), + new HasTagExpression("action"))); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy,shounen,action" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Exclude, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void TryConvertToConditions_FilterWithMultipleConditions_IncludeBaseCondition() + { + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new InSeasonExpression(2023, AnimeSeason.Winter)), + new IsFinishedExpression()); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy" + }, + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Season, + ConditionParameter = "Winter 2023" + }, + new() + { + ConditionOperator = (int)GroupFilterOperator.Include, + ConditionType = (int)GroupFilterConditionType.FinishedAiring + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } +} diff --git a/Shoko.Tests/Shoko.Tests/TestFilterable.cs b/Shoko.Tests/Shoko.Tests/TestFilterable.cs new file mode 100644 index 000000000..5bb856f8c --- /dev/null +++ b/Shoko.Tests/Shoko.Tests/TestFilterable.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using Shoko.Models.Enums; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Tests; + +public class TestFilterable : IFilterable +{ + public string Name { get; init; } + public string SortingName { get; init; } + public int SeriesCount { get; init; } + public int MissingEpisodes { get; init; } + public int MissingEpisodesCollecting { get; init; } + public IReadOnlySet<string> Tags { get; init; } + public IReadOnlySet<string> CustomTags { get; init; } + public IReadOnlySet<int> Years { get; init; } + public IReadOnlySet<(int year, AnimeSeason season)> Seasons { get; init; } + public bool HasTvDBLink { get; init; } + public bool HasMissingTvDbLink { get; init; } + public bool HasTMDbLink { get; init; } + public bool HasMissingTMDbLink { get; init; } + public bool HasTraktLink { get; init; } + public bool HasMissingTraktLink { get; init; } + public bool IsFinished { get; init; } + public DateTime? AirDate { get; init; } + public DateTime? LastAirDate { get; init; } + public DateTime AddedDate { get; init; } + public DateTime LastAddedDate { get; init; } + public int EpisodeCount { get; init; } + public int TotalEpisodeCount { get; init; } + public decimal LowestAniDBRating { get; init; } + public decimal HighestAniDBRating { get; init; } + public IReadOnlySet<string> VideoSources { get; init; } + public IReadOnlySet<string> SharedVideoSources { get; init; } + public IReadOnlySet<string> AnimeTypes { get; init; } + public IReadOnlySet<string> AudioLanguages { get; init; } + public IReadOnlySet<string> SharedAudioLanguages { get; init; } + public IReadOnlySet<string> SubtitleLanguages { get; init; } + public IReadOnlySet<string> SharedSubtitleLanguages { get; init; } +} diff --git a/Shoko.Tests/Shoko.Tests/TestUserDependentFilterable.cs b/Shoko.Tests/Shoko.Tests/TestUserDependentFilterable.cs new file mode 100644 index 000000000..9a2c11df7 --- /dev/null +++ b/Shoko.Tests/Shoko.Tests/TestUserDependentFilterable.cs @@ -0,0 +1,18 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Tests; + +public class TestUserDependentFilterable : TestFilterable, IUserDependentFilterable +{ + public bool IsFavorite { get; init; } + public int WatchedEpisodes { get; init; } + public int UnwatchedEpisodes { get; init; } + public bool HasVotes { get; init; } + public bool HasPermanentVotes { get; init; } + public bool MissingPermanentVotes { get; init; } + public DateTime? WatchedDate { get; init; } + public DateTime? LastWatchedDate { get; init; } + public decimal LowestUserRating { get; init; } + public decimal HighestUserRating { get; init; } +} From 6e84f4d84ba7b7022d655adec9315b6a3d343faa Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Sat, 23 Sep 2023 13:14:21 -0400 Subject: [PATCH 23/34] Cleanup Some TODOs --- Shoko.CLI/Program.cs | 32 ------------------- Shoko.Server/API/v2/Modules/Core.cs | 2 +- .../API/v3/Controllers/FilterController.cs | 2 -- .../API/v3/Controllers/GroupController.cs | 2 +- .../API/v3/Controllers/SeriesController.cs | 2 +- Shoko.Server/API/v3/Helpers/WebUIFactory.cs | 6 +--- Shoko.Server/Models/SVR_AnimeSeries.cs | 2 -- 7 files changed, 4 insertions(+), 44 deletions(-) diff --git a/Shoko.CLI/Program.cs b/Shoko.CLI/Program.cs index f5d95e65b..d345b54cd 100644 --- a/Shoko.CLI/Program.cs +++ b/Shoko.CLI/Program.cs @@ -42,8 +42,6 @@ public static void Main() var startup = new Startup(logFactory.CreateLogger<Startup>(), settingsProvider); startup.Start(); AddEventHandlers(); - // TODO Remove this after filter merge - //Utils.ShokoServer.DBSetupCompleted += OnShokoServerOnDBSetupCompleted; startup.WaitForShutdown(); } catch (Exception e) @@ -52,36 +50,6 @@ public static void Main() } } - private static void OnShokoServerOnDBSetupCompleted(object? o, EventArgs eventArgs) - { - - var filterEvaluator = Utils.ServiceContainer.GetRequiredService<FilterEvaluator>(); - var s = Stopwatch.StartNew(); - RepoFactory.FilterPreset.CreateOrVerifyDirectoryFilters(); - s.Stop(); - _logger.LogInformation("Generating Directories took {Time}ms", s.ElapsedMilliseconds); - var comedyFilter = RepoFactory.FilterPreset.GetAll().FirstOrDefault(a => a.Name.Equals("comedy", StringComparison.InvariantCultureIgnoreCase)); - if (comedyFilter != null) - { - s.Restart(); - var result = filterEvaluator.EvaluateFilter(comedyFilter, null); - s.Stop(); - _logger.LogInformation("Filtering took {Time}ms", s.ElapsedMilliseconds); - s.Restart(); - var groups = result.SelectMany(a => a.Select(b => new - { - Group = RepoFactory.AnimeGroup.GetByID(a.Key), Series = RepoFactory.AnimeSeries.GetByID(b) - })) - .GroupBy(a => a.Group, a => a.Series) - .ToDictionary(a => a.Key, a => a.ToList()); - s.Stop(); - _logger.LogInformation("Projecting results took {Time}ms", s.ElapsedMilliseconds); - } - - _logger.LogInformation("Finished"); - } - - private static void AddEventHandlers() { Utils.YesNoRequired += OnUtilsOnYesNoRequired; diff --git a/Shoko.Server/API/v2/Modules/Core.cs b/Shoko.Server/API/v2/Modules/Core.cs index bd0e64a50..8778ff8e3 100644 --- a/Shoko.Server/API/v2/Modules/Core.cs +++ b/Shoko.Server/API/v2/Modules/Core.cs @@ -241,7 +241,7 @@ public Credentials GetAniDB() [HttpGet("anidb/votes/sync")] public ActionResult SyncAniDBVotes() { - //TODO APIv2: Command should be split into AniDb/MAL sepereate + //TODO APIv2: Command should be split into AniDb/MAL separate _commandFactory.CreateAndSave<CommandRequest_SyncMyVotes>(); return APIStatus.OK(); } diff --git a/Shoko.Server/API/v3/Controllers/FilterController.cs b/Shoko.Server/API/v3/Controllers/FilterController.cs index c5670629e..842b6b2f9 100644 --- a/Shoko.Server/API/v3/Controllers/FilterController.cs +++ b/Shoko.Server/API/v3/Controllers/FilterController.cs @@ -184,8 +184,6 @@ internal static FilterPreset GetDefaultFilterForUser(SVR_JMMUser user) Name = "Live Filtering", }; - // TODO: Update default filter for user here. - return filterPreset; } diff --git a/Shoko.Server/API/v3/Controllers/GroupController.cs b/Shoko.Server/API/v3/Controllers/GroupController.cs index d711392c3..c9564d3ae 100644 --- a/Shoko.Server/API/v3/Controllers/GroupController.cs +++ b/Shoko.Server/API/v3/Controllers/GroupController.cs @@ -280,7 +280,7 @@ public ActionResult<List<SeriesRelation>> GetShokoRelationsBySeriesID([FromRoute .Select(series => series.AniDB_ID) .ToHashSet(); - // TODO: Replace with a more generic implementation capable of suplying relations from more than just AniDB. + // TODO: Replace with a more generic implementation capable of supplying relations from more than just AniDB. return RepoFactory.AniDB_Anime_Relation.GetByAnimeID(animeIds) .Select(relation => (relation, relatedSeries: RepoFactory.AnimeSeries.GetByAnimeID(relation.RelatedAnimeID))) diff --git a/Shoko.Server/API/v3/Controllers/SeriesController.cs b/Shoko.Server/API/v3/Controllers/SeriesController.cs index 0ffca178a..258f8400a 100644 --- a/Shoko.Server/API/v3/Controllers/SeriesController.cs +++ b/Shoko.Server/API/v3/Controllers/SeriesController.cs @@ -235,7 +235,7 @@ public ActionResult<List<SeriesRelation>> GetShokoRelationsBySeriesID([FromRoute return Forbid(SeriesForbiddenForUser); } - // TODO: Replace with a more generic implementation capable of suplying relations from more than just AniDB. + // TODO: Replace with a more generic implementation capable of supplying relations from more than just AniDB. return RepoFactory.AniDB_Anime_Relation.GetByAnimeID(series.AniDB_ID) .Select(relation => (relation, relatedSeries: RepoFactory.AnimeSeries.GetByAnimeID(relation.RelatedAnimeID))) diff --git a/Shoko.Server/API/v3/Helpers/WebUIFactory.cs b/Shoko.Server/API/v3/Helpers/WebUIFactory.cs index 5e440f19b..39e890e5d 100644 --- a/Shoko.Server/API/v3/Helpers/WebUIFactory.cs +++ b/Shoko.Server/API/v3/Helpers/WebUIFactory.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using Microsoft.AspNetCore.Http; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.Models; @@ -8,18 +7,15 @@ namespace Shoko.Server.API.v3.Helpers; public class WebUIFactory { - private readonly HttpContext _context; private readonly FilterFactory _filterFactory; private readonly SeriesFactory _seriesFactory; - public WebUIFactory(IHttpContextAccessor context, FilterFactory filterFactory, SeriesFactory seriesFactory) + public WebUIFactory(FilterFactory filterFactory, SeriesFactory seriesFactory) { - _context = context.HttpContext; _filterFactory = filterFactory; _seriesFactory = seriesFactory; } - // TODO the rest of this public Models.Shoko.WebUI.WebUISeriesExtra GetWebUISeriesExtra(SVR_AnimeSeries series) { var anime = series.GetAnime(); diff --git a/Shoko.Server/Models/SVR_AnimeSeries.cs b/Shoko.Server/Models/SVR_AnimeSeries.cs index 206a83c71..e23ffda00 100644 --- a/Shoko.Server/Models/SVR_AnimeSeries.cs +++ b/Shoko.Server/Models/SVR_AnimeSeries.cs @@ -278,7 +278,6 @@ public List<SVR_AnimeEpisode> GetAnimeEpisodes(bool orderList = false, bool incl { if (orderList) { - // TODO: Convert to a LINQ query once we've switched to EF Core. return RepoFactory.AnimeEpisode.GetBySeriesID(AnimeSeriesID) .Where(episode => includeHidden || !episode.IsHidden) .Select(episode => (episode, anidbEpisode: episode.AniDB_Episode)) @@ -289,7 +288,6 @@ public List<SVR_AnimeEpisode> GetAnimeEpisodes(bool orderList = false, bool incl } if (!includeHidden) { - // TODO: Convert to a LINQ query once we've switched to EF Core. return RepoFactory.AnimeEpisode.GetBySeriesID(AnimeSeriesID) .Where(episode => !episode.IsHidden) .ToList(); From 7d399dc54e133ae2d83aa8f817e3eb0ab45b0ead Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Mon, 25 Sep 2023 13:47:48 -0400 Subject: [PATCH 24/34] Filter Migrations. Fix Accidentally Deleted Plex Endpoints --- Shoko.Commons | 2 +- .../ShokoServiceImplementation_Entities.cs | 3 +- .../ShokoServiceImplementationPlex.cs | 81 +++++++++++++++++++ Shoko.Server/Commands/CommandStartup.cs | 1 + Shoko.Server/Databases/DatabaseFixes.cs | 75 +++++++++++++++++ Shoko.Server/Databases/MySQL.cs | 4 + .../FilterExpressionConverter.cs | 10 +-- .../NHibernateDependencyInjector.cs | 2 +- .../TitleLanguageConverter.cs | 6 +- .../TitleTypeConverter.cs | 6 +- Shoko.Server/Databases/SQLServer.cs | 4 + Shoko.Server/Databases/SQLite.cs | 4 + Shoko.Server/Filters/FilterEvaluator.cs | 3 +- Shoko.Server/Filters/FilterExpression.cs | 1 - .../Legacy/LegacyConditionConverter.cs | 60 ++++++++------ .../Filters/Legacy/LegacyFilterConverter.cs | 21 ++++- Shoko.Server/Filters/Legacy/LegacyMappings.cs | 15 +++- Shoko.Server/Mappings/AniDB_Anime_TitleMap.cs | 2 +- .../Mappings/AniDB_Episode_TitleMap.cs | 2 +- Shoko.Server/Mappings/FilterPresetMap.cs | 5 +- Shoko.Server/Models/FilterPreset.cs | 4 +- .../Cached/FilterPresetRepository.cs | 2 +- 22 files changed, 260 insertions(+), 53 deletions(-) create mode 100644 Shoko.Server/API/v1/Implementations/ShokoServiceImplementationPlex.cs rename Shoko.Server/Databases/{TypeConverters => NHIbernate}/FilterExpressionConverter.cs (98%) rename Shoko.Server/Databases/{ => NHIbernate}/NHibernateDependencyInjector.cs (98%) rename Shoko.Server/Databases/{TypeConverters => NHIbernate}/TitleLanguageConverter.cs (99%) rename Shoko.Server/Databases/{TypeConverters => NHIbernate}/TitleTypeConverter.cs (99%) diff --git a/Shoko.Commons b/Shoko.Commons index 6b800221d..4b37ffeb0 160000 --- a/Shoko.Commons +++ b/Shoko.Commons @@ -1 +1 @@ -Subproject commit 6b800221d38adb25ecd6b9890695719eff0bf63c +Subproject commit 4b37ffeb03613b6e3fc132b37505acfb02c4a3cb diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs index 24d66cdef..2beab5b4b 100755 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs @@ -3249,8 +3249,7 @@ public CL_GroupFilter EvaluateGroupFilter(CL_GroupFilter contract) { try { - var legacyConverter = HttpContext.RequestServices.GetRequiredService<LegacyFilterConverter>(); - var expression = LegacyConditionConverter.GetExpression(contract); + var expression = LegacyConditionConverter.GetExpression(contract.FilterConditions, (GroupFilterBaseCondition)contract.BaseCondition); var filter = new FilterPreset { diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationPlex.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationPlex.cs new file mode 100644 index 000000000..86eca15b5 --- /dev/null +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationPlex.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Shoko.Models.Interfaces; +using Shoko.Models.Plex.Connections; +using Shoko.Models.PlexAndKodi; +using Shoko.Server.Plex; +using Shoko.Server.Repositories; +using Shoko.Server.Settings; +using Directory = Shoko.Models.Plex.Libraries.Directory; + +namespace Shoko.Server.API.v1.Implementations; + +[ApiController] +[Route("/api/Plex")] +[ApiVersion("1.0", Deprecated = true)] +public class ShokoServiceImplementationPlex : IShokoServerPlex, IHttpContextAccessor +{ + public HttpContext HttpContext { get; set; } + private readonly ISettingsProvider _settingsProvider; + + public ShokoServiceImplementationPlex(ISettingsProvider settingsProvider) + { + _settingsProvider = settingsProvider; + } + + [HttpGet("User")] + public PlexContract_Users GetUsers() + { + var gfs = new PlexContract_Users + { + Users = new List<PlexContract_User>() + }; + foreach (var us in RepoFactory.JMMUser.GetAll()) + { + var p = new PlexContract_User { id = us.JMMUserID.ToString(), name = us.Username }; + gfs.Users.Add(p); + } + + return gfs; + } + + [HttpGet("Linking/Devices/Current/{userId}")] + public MediaDevice CurrentDevice(int userId) + { + return PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).ServerCache; + } + + [HttpPost("Linking/Directories/{userId}")] + public void UseDirectories(int userId, List<Directory> directories) + { + var settings = _settingsProvider.GetSettings(); + if (directories == null) + { + settings.Plex.Libraries = new List<int>(); + return; + } + + settings.Plex.Libraries = directories.Select(s => s.Key).ToList(); + _settingsProvider.SaveSettings(); + } + + [HttpGet("Linking/Directories/{userId}")] + public Directory[] Directories(int userId) + { + return PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).GetDirectories(); + } + + [HttpPost("Linking/Servers/{userId}")] + public void UseDevice(int userId, MediaDevice server) + { + PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).UseServer(server); + } + + [HttpGet("Linking/Devices/{userId}")] + public MediaDevice[] AvailableDevices(int userId) + { + return PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).GetPlexServers().ToArray(); + } +} diff --git a/Shoko.Server/Commands/CommandStartup.cs b/Shoko.Server/Commands/CommandStartup.cs index 093f3c68e..5d4cf1833 100644 --- a/Shoko.Server/Commands/CommandStartup.cs +++ b/Shoko.Server/Commands/CommandStartup.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using NLog.Extensions.Logging; using Shoko.Server.Databases; +using Shoko.Server.Databases.NHIbernate; using Shoko.Server.Models; namespace Shoko.Server.Commands; diff --git a/Shoko.Server/Databases/DatabaseFixes.cs b/Shoko.Server/Databases/DatabaseFixes.cs index 27f7e7114..aca351d2e 100644 --- a/Shoko.Server/Databases/DatabaseFixes.cs +++ b/Shoko.Server/Databases/DatabaseFixes.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; using NHibernate; using NLog; using Shoko.Commons.Properties; @@ -12,6 +13,7 @@ using Shoko.Server.Commands; using Shoko.Server.Commands.AniDB; using Shoko.Server.Extensions; +using Shoko.Server.Filters.Legacy; using Shoko.Server.ImageDownload; using Shoko.Server.Models; using Shoko.Server.Providers.AniDB; @@ -27,6 +29,79 @@ public class DatabaseFixes { private static Logger logger = LogManager.GetCurrentClassLogger(); + public static void MigrateGroupFilterToFilterPreset() + { + var legacyConverter = Utils.ServiceContainer.GetRequiredService<LegacyFilterConverter>(); + using var session = DatabaseFactory.SessionFactory.OpenSession(); + var groupFilters = session.CreateSQLQuery( + "SELECT GroupFilterID, " + + "ParentGroupFilterID, " + + "GroupFilterName, " + + "ApplyToSeries, " + + "BaseCondition, " + + "Locked, " + + "FilterType, " + + "InvisibleInClients, " + + "GroupConditions, " + + "SortingCriteria " + + "FROM GroupFilter") + .AddScalar("GroupFilterID", NHibernateUtil.Int32) + .AddScalar("ParentGroupFilterID", NHibernateUtil.Int32) + .AddScalar("GroupFilterName", NHibernateUtil.String) + .AddScalar("ApplyToSeries", NHibernateUtil.Int32) + .AddScalar("BaseCondition", NHibernateUtil.Int32) + .AddScalar("Locked", NHibernateUtil.Int32) + .AddScalar("FilterType", NHibernateUtil.Int32) + .AddScalar("InvisibleInClients", NHibernateUtil.Int32) + .AddScalar("GroupConditions", NHibernateUtil.StringClob) + .AddScalar("SortingCriteria", NHibernateUtil.String) + .List(); + + var filters = new Dictionary<GroupFilter, List<GroupFilterCondition>>(); + foreach (var item in groupFilters) + { + var fields = (object[])item; + var filter = new GroupFilter + { + GroupFilterID = (int)fields[0], + ParentGroupFilterID = (int?)fields[1], + GroupFilterName = (string)fields[2], + ApplyToSeries = (int)fields[3], + BaseCondition = (int)fields[4], + Locked = (int)fields[5], + FilterType = (int)fields[6], + InvisibleInClients = (int)fields[7] + }; + var conditions = JsonConvert.DeserializeObject<List<GroupFilterCondition>>((string)fields[8]); + filters[filter] = conditions; + } + + var idMappings = new Dictionary<int, int>(); + // first, do the ones with no parent + foreach (var key in filters.Keys.Where(a => a.ParentGroupFilterID == null).OrderBy(a => a.GroupFilterID)) + { + var filter = legacyConverter.FromLegacy(key, filters[key]); + RepoFactory.FilterPreset.Save(filter); + idMappings[key.GroupFilterID] = filter.FilterPresetID; + } + + var filtersToProcess = filters.Keys.Where(a => !idMappings.ContainsKey(a.GroupFilterID) && idMappings.ContainsKey(a.ParentGroupFilterID.Value)) + .ToList(); + while (filtersToProcess.Count > 0) + { + foreach (var key in filtersToProcess) + { + var filter = legacyConverter.FromLegacy(key, filters[key]); + filter.ParentFilterPresetID = idMappings[key.ParentGroupFilterID.Value]; + RepoFactory.FilterPreset.Save(filter); + idMappings[key.GroupFilterID] = filter.FilterPresetID; + } + + filtersToProcess = filters.Keys.Where(a => !idMappings.ContainsKey(a.GroupFilterID) && idMappings.ContainsKey(a.ParentGroupFilterID.Value)) + .ToList(); + } + } + public static void MigrateAniDBToNet() { var settings = Utils.SettingsProvider.GetSettings(); diff --git a/Shoko.Server/Databases/MySQL.cs b/Shoko.Server/Databases/MySQL.cs index e1f88f678..c75ae0450 100644 --- a/Shoko.Server/Databases/MySQL.cs +++ b/Shoko.Server/Databases/MySQL.cs @@ -8,6 +8,7 @@ using NHibernate; using NHibernate.Driver.MySqlConnector; using Shoko.Commons.Properties; +using Shoko.Server.Databases.NHIbernate; using Shoko.Server.Repositories; using Shoko.Server.Server; using Shoko.Server.Utilities; @@ -741,6 +742,9 @@ public class MySQL : BaseDatabase<MySqlConnection> "CREATE TABLE FilterPreset( FilterPresetID INT NOT NULL AUTO_INCREMENT, ParentFilterPresetID int, Name text NOT NULL, FilterType int NOT NULL, Locked bit NOT NULL, Hidden bit NOT NULL, ApplyAtSeriesLevel bit NOT NULL, Expression longtext, SortingExpression longtext, PRIMARY KEY (`FilterID`) ); "), new DatabaseCommand(119, 2, "ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_ParentFilterPresetID (ParentFilterPresetID); ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_Name (Name); ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_FilterType (FilterType); ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_LockedHidden (Locked, Hidden);"), + new DatabaseCommand(119, 3, "DELETE FROM GroupFilter WHERE FilterType = 2"), + new DatabaseCommand(119, 4, DatabaseFixes.MigrateGroupFilterToFilterPreset), + new DatabaseCommand(119, 5, "DROP TABLE GroupFilter; DROP TABLE GroupFilterCondition"), }; private DatabaseCommand linuxTableVersionsFix = new("RENAME TABLE versions TO Versions;"); diff --git a/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs b/Shoko.Server/Databases/NHIbernate/FilterExpressionConverter.cs similarity index 98% rename from Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs rename to Shoko.Server/Databases/NHIbernate/FilterExpressionConverter.cs index 25b3576ce..f55c027c4 100644 --- a/Shoko.Server/Databases/TypeConverters/FilterExpressionConverter.cs +++ b/Shoko.Server/Databases/NHIbernate/FilterExpressionConverter.cs @@ -1,18 +1,16 @@ using System; -using System.Collections.Generic; using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using NHibernate.SqlTypes; -using NHibernate.UserTypes; using System.Data; using System.Data.Common; using Newtonsoft.Json; using NHibernate; using NHibernate.Engine; +using NHibernate.SqlTypes; +using NHibernate.UserTypes; using NLog; using Shoko.Server.Filters; -namespace Shoko.Server.Databases.TypeConverters; +namespace Shoko.Server.Databases.NHIbernate; public class FilterExpressionConverter : TypeConverter, IUserType { @@ -34,7 +32,7 @@ public override object ConvertFrom(ITypeDescriptorContext context, System.Global { MissingMemberHandling = MissingMemberHandling.Ignore, TypeNameHandling = TypeNameHandling.Objects, - Error = (sender, args) => + Error = (_, args) => { LogManager.GetCurrentClassLogger().Error(args.ErrorContext.Error); args.ErrorContext.Handled = true; diff --git a/Shoko.Server/Databases/NHibernateDependencyInjector.cs b/Shoko.Server/Databases/NHIbernate/NHibernateDependencyInjector.cs similarity index 98% rename from Shoko.Server/Databases/NHibernateDependencyInjector.cs rename to Shoko.Server/Databases/NHIbernate/NHibernateDependencyInjector.cs index f943cc235..b31accfe3 100644 --- a/Shoko.Server/Databases/NHibernateDependencyInjector.cs +++ b/Shoko.Server/Databases/NHIbernate/NHibernateDependencyInjector.cs @@ -5,7 +5,7 @@ using NHibernate; using NHibernate.Type; -namespace Shoko.Server.Databases; +namespace Shoko.Server.Databases.NHIbernate; public class NHibernateDependencyInjector : EmptyInterceptor { diff --git a/Shoko.Server/Databases/TypeConverters/TitleLanguageConverter.cs b/Shoko.Server/Databases/NHIbernate/TitleLanguageConverter.cs similarity index 99% rename from Shoko.Server/Databases/TypeConverters/TitleLanguageConverter.cs rename to Shoko.Server/Databases/NHIbernate/TitleLanguageConverter.cs index f439e7abd..30ae41ef1 100644 --- a/Shoko.Server/Databases/TypeConverters/TitleLanguageConverter.cs +++ b/Shoko.Server/Databases/NHIbernate/TitleLanguageConverter.cs @@ -1,15 +1,15 @@ using System; using System.ComponentModel; -using NHibernate.SqlTypes; -using NHibernate.UserTypes; using System.Data; using System.Data.Common; using NHibernate; using NHibernate.Engine; +using NHibernate.SqlTypes; +using NHibernate.UserTypes; using Shoko.Plugin.Abstractions.DataModels; using Shoko.Plugin.Abstractions.Extensions; -namespace Shoko.Server.Databases.TypeConverters; +namespace Shoko.Server.Databases.NHIbernate; public class TitleLanguageConverter : TypeConverter, IUserType { diff --git a/Shoko.Server/Databases/TypeConverters/TitleTypeConverter.cs b/Shoko.Server/Databases/NHIbernate/TitleTypeConverter.cs similarity index 99% rename from Shoko.Server/Databases/TypeConverters/TitleTypeConverter.cs rename to Shoko.Server/Databases/NHIbernate/TitleTypeConverter.cs index 6e389e249..3a04954e0 100644 --- a/Shoko.Server/Databases/TypeConverters/TitleTypeConverter.cs +++ b/Shoko.Server/Databases/NHIbernate/TitleTypeConverter.cs @@ -1,15 +1,15 @@ using System; using System.ComponentModel; -using NHibernate.SqlTypes; -using NHibernate.UserTypes; using System.Data; using System.Data.Common; using NHibernate; using NHibernate.Engine; +using NHibernate.SqlTypes; +using NHibernate.UserTypes; using Shoko.Plugin.Abstractions.DataModels; using Shoko.Plugin.Abstractions.Extensions; -namespace Shoko.Server.Databases.TypeConverters; +namespace Shoko.Server.Databases.NHIbernate; public class TitleTypeConverter : TypeConverter, IUserType { diff --git a/Shoko.Server/Databases/SQLServer.cs b/Shoko.Server/Databases/SQLServer.cs index b7b997523..966212f07 100644 --- a/Shoko.Server/Databases/SQLServer.cs +++ b/Shoko.Server/Databases/SQLServer.cs @@ -10,6 +10,7 @@ using NHibernate.AdoNet; using Shoko.Commons.Extensions; using Shoko.Commons.Properties; +using Shoko.Server.Databases.NHIbernate; using Shoko.Server.Repositories; using Shoko.Server.Server; using Shoko.Server.Settings; @@ -684,6 +685,9 @@ public override bool HasVersionsTable() "CREATE TABLE FilterPreset( FilterPresetID INT IDENTITY(1,1), ParentFilterPresetID int, Name nvarchar(max) NOT NULL, FilterType int NOT NULL, Locked bit NOT NULL, Hidden bit NOT NULL, ApplyAtSeriesLevel bit NOT NULL, Expression nvarchar(max), SortingExpression nvarchar(max) ); "), new DatabaseCommand(112, 2, "CREATE INDEX IX_FilterPreset_ParentFilterPresetID ON FilterPreset(ParentFilterPresetID); CREATE INDEX IX_FilterPreset_Name ON FilterPreset(Name); CREATE INDEX IX_FilterPreset_FilterType ON FilterPreset(FilterType); CREATE INDEX IX_FilterPreset_LockedHidden ON FilterPreset(Locked, Hidden);"), + new DatabaseCommand(112, 3, "DELETE FROM GroupFilter WHERE FilterType = 2"), + new DatabaseCommand(112, 4, DatabaseFixes.MigrateGroupFilterToFilterPreset), + new DatabaseCommand(112, 5, "DROP TABLE GroupFilter; DROP TABLE GroupFilterCondition"), }; private static Tuple<bool, string> DropDefaultsOnAnimeEpisode_User(object connection) diff --git a/Shoko.Server/Databases/SQLite.cs b/Shoko.Server/Databases/SQLite.cs index a3081084e..a8cab9398 100644 --- a/Shoko.Server/Databases/SQLite.cs +++ b/Shoko.Server/Databases/SQLite.cs @@ -8,6 +8,7 @@ using FluentNHibernate.Cfg.Db; using NHibernate; using Shoko.Commons.Properties; +using Shoko.Server.Databases.NHIbernate; using Shoko.Server.Databases.SqliteFixes; using Shoko.Server.Repositories; using Shoko.Server.Server; @@ -675,6 +676,9 @@ public override void CreateDatabase() "CREATE TABLE FilterPreset( FilterPresetID INTEGER PRIMARY KEY AUTOINCREMENT, ParentFilterPresetID int, Name text NOT NULL, FilterType int NOT NULL, Locked int NOT NULL, Hidden int NOT NULL, ApplyAtSeriesLevel int NOT NULL, Expression text, SortingExpression text ); "), new DatabaseCommand(105, 2, "CREATE INDEX IX_FilterPreset_ParentFilterPresetID ON FilterPreset(ParentFilterPresetID); CREATE INDEX IX_FilterPreset_Name ON FilterPreset(Name); CREATE INDEX IX_FilterPreset_FilterType ON FilterPreset(FilterType); CREATE INDEX IX_FilterPreset_LockedHidden ON FilterPreset(Locked, Hidden);"), + new DatabaseCommand(105, 3, "DELETE FROM GroupFilter WHERE FilterType = 2"), + new DatabaseCommand(105, 4, DatabaseFixes.MigrateGroupFilterToFilterPreset), + new DatabaseCommand(105, 5, "DROP TABLE GroupFilter; DROP TABLE GroupFilterCondition"), }; private static Tuple<bool, string> DropLanguage(object connection) diff --git a/Shoko.Server/Filters/FilterEvaluator.cs b/Shoko.Server/Filters/FilterEvaluator.cs index 4029bc331..8bb2b2098 100644 --- a/Shoko.Server/Filters/FilterEvaluator.cs +++ b/Shoko.Server/Filters/FilterEvaluator.cs @@ -80,7 +80,8 @@ public Dictionary<FilterPreset, IEnumerable<IGrouping<int, int>>> BatchEvaluateF { ArgumentNullException.ThrowIfNull(filters); if (!filters.Any()) return new Dictionary<FilterPreset, IEnumerable<IGrouping<int, int>>>(); - var user = filters.Any(a => a.Expression?.UserDependent ?? false); + // count it as a user filter if it needs to sort using a user-dependent expression + var user = filters.Any(a => (a?.Expression?.UserDependent ?? false) || skipSorting && (a?.SortingExpression?.UserDependent ?? false)); if (user && userID == null) throw new ArgumentNullException(nameof(userID)); var filterables = filters.Any(a => a.ApplyAtSeriesLevel) switch diff --git a/Shoko.Server/Filters/FilterExpression.cs b/Shoko.Server/Filters/FilterExpression.cs index 1d63f926c..11805c9ff 100644 --- a/Shoko.Server/Filters/FilterExpression.cs +++ b/Shoko.Server/Filters/FilterExpression.cs @@ -5,7 +5,6 @@ namespace Shoko.Server.Filters; public class FilterExpression : IFilterExpression { - public int FilterExpressionID { get; set; } [IgnoreDataMember] public virtual bool TimeDependent => false; [IgnoreDataMember] public virtual bool UserDependent => false; diff --git a/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs b/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs index d86e1fe88..978c38048 100644 --- a/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs +++ b/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Shoko.Models.Client; using Shoko.Models.Enums; using Shoko.Models.Server; using Shoko.Server.Filters.Files; @@ -447,114 +446,114 @@ private static bool TryGetComparatorCondition(FilterExpression expression, out G return false; } - public static bool IsInTag(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInTag(FilterExpression expression, out List<string> parameters, out bool inverted) { parameters = new List<string>(); return TryParseIn(expression, typeof(HasTagExpression), parameters, out inverted); } - public static bool IsInCustomTag(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInCustomTag(FilterExpression expression, out List<string> parameters, out bool inverted) { parameters = new List<string>(); return TryParseIn(expression, typeof(HasTagExpression), parameters, out inverted); } - public static bool IsInAnimeType(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInAnimeType(FilterExpression expression, out List<string> parameters, out bool inverted) { parameters = new List<string>(); return TryParseIn(expression, typeof(HasAnimeTypeExpression), parameters, out inverted); } - public static bool IsInVideoQuality(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInVideoQuality(FilterExpression expression, out List<string> parameters, out bool inverted) { parameters = new List<string>(); return TryParseIn(expression, typeof(HasVideoSourceExpression), parameters, out inverted); } - public static bool IsInSharedVideoQuality(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInSharedVideoQuality(FilterExpression expression, out List<string> parameters, out bool inverted) { parameters = new List<string>(); return TryParseIn(expression, typeof(HasSharedVideoSourceExpression), parameters, out inverted); } - public static bool IsInGroup(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInGroup(FilterExpression expression, out List<string> parameters, out bool inverted) { parameters = new List<string>(); return TryParseIn(expression, typeof(HasNameExpression), parameters, out inverted); } - public static bool IsInYear(FilterExpression expression, out List<int> parameters, out bool inverted) + private static bool IsInYear(FilterExpression expression, out List<int> parameters, out bool inverted) { parameters = new List<int>(); return TryParseIn(expression, typeof(InYearExpression), parameters, out inverted); } - public static bool IsInSeason(FilterExpression expression, out List<(int Year, string Season)> parameters, out bool inverted) + private static bool IsInSeason(FilterExpression expression, out List<(int Year, string Season)> parameters, out bool inverted) { parameters = new List<(int, string)>(); return TryParseIn(expression, typeof(InSeasonExpression), parameters, out inverted); } - public static bool IsInAudioLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInAudioLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) { parameters = new List<string>(); return TryParseIn(expression, typeof(HasAudioLanguageExpression), parameters, out inverted); } - public static bool IsInSubtitleLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInSubtitleLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) { parameters = new List<string>(); return TryParseIn(expression, typeof(HasSubtitleLanguageExpression), parameters, out inverted); } - public static bool IsInSharedAudioLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInSharedAudioLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) { parameters = new List<string>(); return TryParseIn(expression, typeof(HasSharedAudioLanguageExpression), parameters, out inverted); } - public static bool IsInSharedSubtitleLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInSharedSubtitleLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) { parameters = new List<string>(); return TryParseIn(expression, typeof(HasSharedSubtitleLanguageExpression), parameters, out inverted); } - public static bool IsAirDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + private static bool IsAirDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) { return TryParseComparator(expression, typeof(AirDateSelector), out parameter, out gfOperator); } - public static bool IsLatestAirDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + private static bool IsLatestAirDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) { return TryParseComparator(expression, typeof(LastAirDateSelector), out parameter, out gfOperator); } - public static bool IsSeriesCreatedDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + private static bool IsSeriesCreatedDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) { return TryParseComparator(expression, typeof(AddedDateSelector), out parameter, out gfOperator); } - public static bool IsEpisodeAddedDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + private static bool IsEpisodeAddedDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) { return TryParseComparator(expression, typeof(LastAddedDateSelector), out parameter, out gfOperator); } - public static bool IsEpisodeWatchedDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + private static bool IsEpisodeWatchedDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) { return TryParseComparator(expression, typeof(LastWatchedDateSelector), out parameter, out gfOperator); } - public static bool IsAniDBRating(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + private static bool IsAniDBRating(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) { return TryParseComparator(expression, typeof(HighestAniDBRatingSelector), out parameter, out gfOperator); } - public static bool IsUserRating(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + private static bool IsUserRating(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) { return TryParseComparator(expression, typeof(HighestUserRatingSelector), out parameter, out gfOperator); } - public static bool IsEpisodeCount(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + private static bool IsEpisodeCount(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) { return TryParseComparator(expression, typeof(EpisodeCountSelector), out parameter, out gfOperator); } @@ -719,9 +718,8 @@ public static List<GroupFilterSortingCriteria> GetSortingCriteriaList(FilterPres return results; } - public static FilterExpression<bool> GetExpression(CL_GroupFilter groupFilter, bool suppressErrors = false) + public static FilterExpression<bool> GetExpression(List<GroupFilterCondition> conditions, GroupFilterBaseCondition baseCondition, bool suppressErrors = false) { - var conditions = groupFilter.FilterConditions; // forward compatibility is easier. Just map the old conditions to an expression if (conditions == null || conditions.Count < 1) return null; var first = conditions.Select((a, index) => new {Expression= GetExpression(a, suppressErrors), Index=index}).FirstOrDefault(a => a.Expression != null); @@ -731,7 +729,7 @@ public static FilterExpression<bool> GetExpression(CL_GroupFilter groupFilter, b var result = GetExpression(b, suppressErrors); return result == null ? a : new AndExpression(a, result); }); - return groupFilter.BaseCondition == (int)GroupFilterBaseCondition.Exclude ? new NotExpression(condition) : condition; + return baseCondition == GroupFilterBaseCondition.Exclude ? new NotExpression(condition) : condition; } private static FilterExpression<bool> GetExpression(GroupFilterCondition condition, bool suppressErrors = false) @@ -744,6 +742,10 @@ private static FilterExpression<bool> GetExpression(GroupFilterCondition conditi if (op == GroupFilterOperator.Include) return LegacyMappings.GetCompletedExpression(); return new NotExpression(LegacyMappings.GetCompletedExpression()); + case GroupFilterConditionType.FinishedAiring: + if (op == GroupFilterOperator.Include) + return new IsFinishedExpression(); + return new NotExpression(new IsFinishedExpression()); case GroupFilterConditionType.MissingEpisodes: if (op == GroupFilterOperator.Include) return new HasMissingEpisodesExpression(); @@ -776,6 +778,14 @@ private static FilterExpression<bool> GetExpression(GroupFilterCondition conditi if (op == GroupFilterOperator.Include) return new HasTvDBLinkExpression(); return new NotExpression(new HasTvDBLinkExpression()); + case GroupFilterConditionType.AssignedMovieDBInfo: + if (op == GroupFilterOperator.Include) + return new HasTMDbLinkExpression(); + return new NotExpression(new HasTMDbLinkExpression()); + case GroupFilterConditionType.AssignedTvDBOrMovieDBInfo: + if (op == GroupFilterOperator.Include) + return new OrExpression(new HasTvDBLinkExpression(), new HasTMDbLinkExpression()); + return new NotExpression(new OrExpression(new HasTvDBLinkExpression(), new HasTMDbLinkExpression())); case GroupFilterConditionType.AssignedTraktInfo: if (op == GroupFilterOperator.Include) return new HasTraktLinkExpression(); @@ -817,7 +827,7 @@ private static FilterExpression<bool> GetExpression(GroupFilterCondition conditi case GroupFilterConditionType.Season: return LegacyMappings.GetSeasonExpression(op, parameter, suppressErrors); default: - return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(condition), $@"ConditionType {condition.ConditionType} is not valid"); + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(condition), $@"ConditionType {(GroupFilterConditionType)condition.ConditionType} is not valid"); } } diff --git a/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs b/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs index 470705278..33e30f0e3 100644 --- a/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs +++ b/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs @@ -2,6 +2,7 @@ using System.Linq; using Shoko.Models.Client; using Shoko.Models.Enums; +using Shoko.Models.Server; using Shoko.Server.Filters.Logic; using Shoko.Server.Models; using Shoko.Server.Repositories; @@ -19,7 +20,7 @@ public LegacyFilterConverter(FilterEvaluator evaluator) public FilterPreset FromClient(CL_GroupFilter model) { - var expression = LegacyConditionConverter.GetExpression(model); + var expression = LegacyConditionConverter.GetExpression(model.FilterConditions, (GroupFilterBaseCondition)model.BaseCondition); var filter = new FilterPreset { FilterPresetID = model.GroupFilterID, @@ -35,6 +36,22 @@ public FilterPreset FromClient(CL_GroupFilter model) return filter; } + public FilterPreset FromLegacy(GroupFilter model, List<GroupFilterCondition> conditions) + { + var expression = LegacyConditionConverter.GetExpression(conditions, (GroupFilterBaseCondition)model.BaseCondition); + var filter = new FilterPreset + { + Name = model.GroupFilterName, + FilterType = (GroupFilterType)model.FilterType, + Hidden = model.InvisibleInClients == 1, + Locked = model.Locked == 1, + ApplyAtSeriesLevel = model.ApplyToSeries == 1, + SortingExpression = LegacyConditionConverter.GetSortingExpression(model.SortingCriteria), + Expression = expression == null ? null : model.BaseCondition == (int)GroupFilterBaseCondition.Exclude ? new NotExpression(expression) : expression + }; + return filter; + } + public CL_GroupFilter ToClient(FilterPreset filter) { if (filter == null) return null; @@ -129,7 +146,7 @@ public Dictionary<FilterPreset, CL_GroupFilter> ToClient(IReadOnlyList<FilterPre if (otherFilters.Count > 0) { - var results = _evaluator.BatchEvaluateFilters(userFilters, null); + var results = _evaluator.BatchEvaluateFilters(otherFilters, null); var models = results.Select(kv => { var filter = kv.Key; diff --git a/Shoko.Server/Filters/Legacy/LegacyMappings.cs b/Shoko.Server/Filters/Legacy/LegacyMappings.cs index 09a439e8e..c5b410833 100644 --- a/Shoko.Server/Filters/Legacy/LegacyMappings.cs +++ b/Shoko.Server/Filters/Legacy/LegacyMappings.cs @@ -51,12 +51,13 @@ public static FilterExpression<bool> GetTagExpression(GroupFilterOperator op, st { '|', ',' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (tags.Length == 0) return suppressErrors ? null : throw new ArgumentException(@$"Parameter {parameter} was invalid", nameof(parameter)); switch (op) { case GroupFilterOperator.Include: case GroupFilterOperator.In: { - if (tags.Length <= 1) return new HasTagExpression(tags[0]); + if (tags.Length == 1) return new HasTagExpression(tags[0]); FilterExpression<bool> first = new HasTagExpression(tags[0]); return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasTagExpression(b))); @@ -64,7 +65,7 @@ public static FilterExpression<bool> GetTagExpression(GroupFilterOperator op, st case GroupFilterOperator.Exclude: case GroupFilterOperator.NotIn: { - if (tags.Length <= 1) return new NotExpression(new HasTagExpression(tags[0])); + if (tags.Length == 1) return new NotExpression(new HasTagExpression(tags[0])); FilterExpression<bool> first = new HasTagExpression(tags[0]); return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasTagExpression(b)))); @@ -222,6 +223,7 @@ public static FilterExpression<bool> GetAudioLanguageExpression(GroupFilterOpera switch (op) { case GroupFilterOperator.In: + case GroupFilterOperator.Include: { if (tags.Length <= 1) return new HasAudioLanguageExpression(tags[0]); @@ -229,6 +231,7 @@ public static FilterExpression<bool> GetAudioLanguageExpression(GroupFilterOpera return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasAudioLanguageExpression(b))); } case GroupFilterOperator.NotIn: + case GroupFilterOperator.Exclude: { if (tags.Length <= 1) return new NotExpression(new HasAudioLanguageExpression(tags[0])); @@ -250,6 +253,7 @@ public static FilterExpression<bool> GetSubtitleLanguageExpression(GroupFilterOp switch (op) { case GroupFilterOperator.In: + case GroupFilterOperator.Include: { if (tags.Length <= 1) return new HasSubtitleLanguageExpression(tags[0]); @@ -257,6 +261,7 @@ public static FilterExpression<bool> GetSubtitleLanguageExpression(GroupFilterOp return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasSubtitleLanguageExpression(b))); } case GroupFilterOperator.NotIn: + case GroupFilterOperator.Exclude: { if (tags.Length <= 1) return new NotExpression(new HasSubtitleLanguageExpression(tags[0])); @@ -278,6 +283,7 @@ public static FilterExpression<bool> GetAnimeTypeExpression(GroupFilterOperator switch (op) { case GroupFilterOperator.In: + case GroupFilterOperator.Include: { if (tags.Length <= 1) return new HasAnimeTypeExpression(tags[0]); @@ -285,6 +291,7 @@ public static FilterExpression<bool> GetAnimeTypeExpression(GroupFilterOperator return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasAnimeTypeExpression(b))); } case GroupFilterOperator.NotIn: + case GroupFilterOperator.Exclude: { if (tags.Length <= 1) return new NotExpression(new HasAnimeTypeExpression(tags[0])); @@ -306,6 +313,7 @@ public static FilterExpression<bool> GetGroupExpression(GroupFilterOperator op, switch (op) { case GroupFilterOperator.In: + case GroupFilterOperator.Include: { if (tags.Length <= 1) return new HasNameExpression(tags[0]); @@ -313,6 +321,7 @@ public static FilterExpression<bool> GetGroupExpression(GroupFilterOperator op, return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasNameExpression(b))); } case GroupFilterOperator.NotIn: + case GroupFilterOperator.Exclude: { if (tags.Length <= 1) return new NotExpression(new HasNameExpression(tags[0])); @@ -382,6 +391,7 @@ public static FilterExpression<bool> GetYearExpression(GroupFilterOperator op, s switch (op) { case GroupFilterOperator.In: + case GroupFilterOperator.Include: { if (!int.TryParse(tags[0], out var firstYear)) return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); @@ -396,6 +406,7 @@ public static FilterExpression<bool> GetYearExpression(GroupFilterOperator op, s }); } case GroupFilterOperator.NotIn: + case GroupFilterOperator.Exclude: { if (!int.TryParse(tags[0], out var firstYear)) return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); diff --git a/Shoko.Server/Mappings/AniDB_Anime_TitleMap.cs b/Shoko.Server/Mappings/AniDB_Anime_TitleMap.cs index 808d3f0de..39b11be4e 100644 --- a/Shoko.Server/Mappings/AniDB_Anime_TitleMap.cs +++ b/Shoko.Server/Mappings/AniDB_Anime_TitleMap.cs @@ -1,6 +1,6 @@ using FluentNHibernate.Mapping; +using Shoko.Server.Databases.NHIbernate; using Shoko.Server.Models; -using Shoko.Server.Databases.TypeConverters; namespace Shoko.Server.Mappings; diff --git a/Shoko.Server/Mappings/AniDB_Episode_TitleMap.cs b/Shoko.Server/Mappings/AniDB_Episode_TitleMap.cs index bd4a7c6bf..cb00e192c 100644 --- a/Shoko.Server/Mappings/AniDB_Episode_TitleMap.cs +++ b/Shoko.Server/Mappings/AniDB_Episode_TitleMap.cs @@ -1,5 +1,5 @@ using FluentNHibernate.Mapping; -using Shoko.Server.Databases.TypeConverters; +using Shoko.Server.Databases.NHIbernate; using Shoko.Server.Models; namespace Shoko.Server.Mappings; diff --git a/Shoko.Server/Mappings/FilterPresetMap.cs b/Shoko.Server/Mappings/FilterPresetMap.cs index eb6b5b896..4c62d66ff 100644 --- a/Shoko.Server/Mappings/FilterPresetMap.cs +++ b/Shoko.Server/Mappings/FilterPresetMap.cs @@ -1,6 +1,6 @@ using FluentNHibernate.Mapping; using Shoko.Models.Enums; -using Shoko.Server.Databases.TypeConverters; +using Shoko.Server.Databases.NHIbernate; using Shoko.Server.Models; namespace Shoko.Server.Mappings; @@ -13,7 +13,8 @@ public FilterPresetMap() Not.LazyLoad(); Id(x => x.FilterPresetID); Map(x => x.ParentFilterPresetID).Nullable(); - //References(x => x.Parent).Nullable().PropertyRef(x => x.ParentFilterID); + References(a => a.Parent).Column("ParentFilterPresetID").ReadOnly(); + HasMany(x => x.Children).Fetch.Join().KeyColumn("ParentFilterPresetID").ReadOnly(); Map(x => x.Name).Not.Nullable(); Map(x => x.FilterType).Not.Nullable().CustomType<GroupFilterType>(); Map(x => x.Locked).Not.Nullable(); diff --git a/Shoko.Server/Models/FilterPreset.cs b/Shoko.Server/Models/FilterPreset.cs index 3a4794959..bac634d51 100644 --- a/Shoko.Server/Models/FilterPreset.cs +++ b/Shoko.Server/Models/FilterPreset.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Shoko.Models.Enums; using Shoko.Server.Filters; @@ -6,7 +7,8 @@ namespace Shoko.Server.Models; public class FilterPreset { public int FilterPresetID { get; set; } - //public virtual Filter Parent { get; set; } + public virtual FilterPreset Parent { get; set; } + public virtual IEnumerable<FilterPreset> Children { get; set; } public int? ParentFilterPresetID { get; set; } public string Name { get; set; } public bool ApplyAtSeriesLevel { get; set; } diff --git a/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs b/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs index ad1f38310..34cdaa743 100644 --- a/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs +++ b/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs @@ -71,7 +71,7 @@ public void CleanUpEmptyDirectoryFilters() { var evaluator = Utils.ServiceContainer.GetRequiredService<FilterEvaluator>(); var filters = GetAll().Where(a => (a.FilterType & GroupFilterType.Directory) != 0) - .Where(gf => gf.Expression is not null && gf.Expression.UserDependent).ToList(); + .Where(gf => gf.Expression is not null && !gf.Expression.UserDependent).ToList(); var toRemove = evaluator.BatchEvaluateFilters(filters, null, true).Where(a => !a.Value.Any()).Select(a => a.Key).ToList(); if (toRemove.Count <= 0) return; From c3175bdf81397bf105b3ed00f700ae3e4504a9fb Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Mon, 25 Sep 2023 14:47:59 -0400 Subject: [PATCH 25/34] Fix Merge Conflict --- Shoko.Server/API/v3/Helpers/SeriesFactory.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Shoko.Server/API/v3/Helpers/SeriesFactory.cs b/Shoko.Server/API/v3/Helpers/SeriesFactory.cs index 74c040bf7..176abb37d 100644 --- a/Shoko.Server/API/v3/Helpers/SeriesFactory.cs +++ b/Shoko.Server/API/v3/Helpers/SeriesFactory.cs @@ -5,6 +5,7 @@ using Shoko.Commons.Extensions; using Shoko.Models.Enums; using Shoko.Models.Server; +using Shoko.Plugin.Abstractions.DataModels; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.API.v3.Models.Shoko; using Shoko.Server.Commands; @@ -545,7 +546,7 @@ public Series.AniDB GetAniDB(SVR_AniDB_Anime_Relation relation, SVR_AnimeSeries Poster = GetAniDBPoster(relation.RelatedAnimeID), Rating = null, UserApproval = null, - Relation = SeriesRelation.GetRelationTypeFromAnidbRelationType(relation.RelationType), + Relation = ((IRelatedAnime)relation).RelationType, }; SetAniDBTitles(result, relation, series, includeTitles); return result; From f0a1bd58f94d61d20907bdb4118b831e413289cc Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Mon, 25 Sep 2023 19:56:54 -0400 Subject: [PATCH 26/34] Fix Migrations --- Shoko.Server/Databases/DatabaseFixes.cs | 6 ++++++ Shoko.Server/Databases/MySQL.cs | 2 +- Shoko.Server/Databases/SQLServer.cs | 4 ++-- Shoko.Server/Databases/SQLite.cs | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Shoko.Server/Databases/DatabaseFixes.cs b/Shoko.Server/Databases/DatabaseFixes.cs index aca351d2e..eb50ce61c 100644 --- a/Shoko.Server/Databases/DatabaseFixes.cs +++ b/Shoko.Server/Databases/DatabaseFixes.cs @@ -101,6 +101,12 @@ public static void MigrateGroupFilterToFilterPreset() .ToList(); } } + + public static void DropGroupFilter() + { + using var session = DatabaseFactory.SessionFactory.OpenSession(); + session.CreateSQLQuery("DROP TABLE GroupFilter; DROP TABLE GroupFilterCondition").ExecuteUpdate(); + } public static void MigrateAniDBToNet() { diff --git a/Shoko.Server/Databases/MySQL.cs b/Shoko.Server/Databases/MySQL.cs index c75ae0450..24054f5a2 100644 --- a/Shoko.Server/Databases/MySQL.cs +++ b/Shoko.Server/Databases/MySQL.cs @@ -744,7 +744,7 @@ public class MySQL : BaseDatabase<MySqlConnection> "ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_ParentFilterPresetID (ParentFilterPresetID); ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_Name (Name); ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_FilterType (FilterType); ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_LockedHidden (Locked, Hidden);"), new DatabaseCommand(119, 3, "DELETE FROM GroupFilter WHERE FilterType = 2"), new DatabaseCommand(119, 4, DatabaseFixes.MigrateGroupFilterToFilterPreset), - new DatabaseCommand(119, 5, "DROP TABLE GroupFilter; DROP TABLE GroupFilterCondition"), + new DatabaseCommand(119, 5, DatabaseFixes.DropGroupFilter), }; private DatabaseCommand linuxTableVersionsFix = new("RENAME TABLE versions TO Versions;"); diff --git a/Shoko.Server/Databases/SQLServer.cs b/Shoko.Server/Databases/SQLServer.cs index 966212f07..4d1bde9d7 100644 --- a/Shoko.Server/Databases/SQLServer.cs +++ b/Shoko.Server/Databases/SQLServer.cs @@ -682,12 +682,12 @@ public override bool HasVersionsTable() new DatabaseCommand(111, 1, DatabaseFixes.FixAnimeSourceLinks), new DatabaseCommand(111, 2, DatabaseFixes.FixOrphanedShokoEpisodes), new DatabaseCommand(112, 1, - "CREATE TABLE FilterPreset( FilterPresetID INT IDENTITY(1,1), ParentFilterPresetID int, Name nvarchar(max) NOT NULL, FilterType int NOT NULL, Locked bit NOT NULL, Hidden bit NOT NULL, ApplyAtSeriesLevel bit NOT NULL, Expression nvarchar(max), SortingExpression nvarchar(max) ); "), + "CREATE TABLE FilterPreset( FilterPresetID INT IDENTITY(1,1), ParentFilterPresetID int, Name nvarchar(250) NOT NULL, FilterType int NOT NULL, Locked bit NOT NULL, Hidden bit NOT NULL, ApplyAtSeriesLevel bit NOT NULL, Expression nvarchar(max), SortingExpression nvarchar(max) ); "), new DatabaseCommand(112, 2, "CREATE INDEX IX_FilterPreset_ParentFilterPresetID ON FilterPreset(ParentFilterPresetID); CREATE INDEX IX_FilterPreset_Name ON FilterPreset(Name); CREATE INDEX IX_FilterPreset_FilterType ON FilterPreset(FilterType); CREATE INDEX IX_FilterPreset_LockedHidden ON FilterPreset(Locked, Hidden);"), new DatabaseCommand(112, 3, "DELETE FROM GroupFilter WHERE FilterType = 2"), new DatabaseCommand(112, 4, DatabaseFixes.MigrateGroupFilterToFilterPreset), - new DatabaseCommand(112, 5, "DROP TABLE GroupFilter; DROP TABLE GroupFilterCondition"), + new DatabaseCommand(112, 5, DatabaseFixes.DropGroupFilter), }; private static Tuple<bool, string> DropDefaultsOnAnimeEpisode_User(object connection) diff --git a/Shoko.Server/Databases/SQLite.cs b/Shoko.Server/Databases/SQLite.cs index a8cab9398..c82f63a49 100644 --- a/Shoko.Server/Databases/SQLite.cs +++ b/Shoko.Server/Databases/SQLite.cs @@ -678,7 +678,7 @@ public override void CreateDatabase() "CREATE INDEX IX_FilterPreset_ParentFilterPresetID ON FilterPreset(ParentFilterPresetID); CREATE INDEX IX_FilterPreset_Name ON FilterPreset(Name); CREATE INDEX IX_FilterPreset_FilterType ON FilterPreset(FilterType); CREATE INDEX IX_FilterPreset_LockedHidden ON FilterPreset(Locked, Hidden);"), new DatabaseCommand(105, 3, "DELETE FROM GroupFilter WHERE FilterType = 2"), new DatabaseCommand(105, 4, DatabaseFixes.MigrateGroupFilterToFilterPreset), - new DatabaseCommand(105, 5, "DROP TABLE GroupFilter; DROP TABLE GroupFilterCondition"), + new DatabaseCommand(105, 5, DatabaseFixes.DropGroupFilter), }; private static Tuple<bool, string> DropLanguage(object connection) From a124a49171299e69f039bb4ecd9a1a8b15ff18cf Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Mon, 25 Sep 2023 20:56:08 -0400 Subject: [PATCH 27/34] Perform user filtering in the Filter Evaluator --- .../API/v3/Controllers/FilterController.cs | 2 +- .../API/v3/Controllers/TreeController.cs | 2 +- Shoko.Server/Filters/FilterEvaluator.cs | 33 ++++++++++++------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Shoko.Server/API/v3/Controllers/FilterController.cs b/Shoko.Server/API/v3/Controllers/FilterController.cs index 842b6b2f9..4470a99c1 100644 --- a/Shoko.Server/API/v3/Controllers/FilterController.cs +++ b/Shoko.Server/API/v3/Controllers/FilterController.cs @@ -373,7 +373,7 @@ [FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValu // We don't need separate logic for ApplyAtSeriesLevel, as the FilterEvaluator handles that return results.SelectMany(a => a.Select(id => RepoFactory.AnimeSeries.GetByID(id))) - .Where(series => series != null && user.AllowedSeries(series) && (includeMissing || series.GetVideoLocals().Count > 0)) + .Where(series => series != null && (includeMissing || series.GetVideoLocals().Count > 0)) .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) .ToListResult(series => _seriesFactory.GetSeries(series, randomImages), page, pageSize); } diff --git a/Shoko.Server/API/v3/Controllers/TreeController.cs b/Shoko.Server/API/v3/Controllers/TreeController.cs index b6e1d9135..f89fddb52 100644 --- a/Shoko.Server/API/v3/Controllers/TreeController.cs +++ b/Shoko.Server/API/v3/Controllers/TreeController.cs @@ -279,7 +279,7 @@ [FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValu // We don't need separate logic for ApplyAtSeriesLevel, as the FilterEvaluator handles that return results.SelectMany(a => a.Select(id => RepoFactory.AnimeSeries.GetByID(id))) - .Where(series => series != null && user.AllowedSeries(series) && (includeMissing || series.GetVideoLocals().Count > 0)) + .Where(series => series != null && (includeMissing || series.GetVideoLocals().Count > 0)) .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) .ToListResult(series => _seriesFactory.GetSeries(series, randomImages), page, pageSize); } diff --git a/Shoko.Server/Filters/FilterEvaluator.cs b/Shoko.Server/Filters/FilterEvaluator.cs index 8bb2b2098..8f8102afa 100644 --- a/Shoko.Server/Filters/FilterEvaluator.cs +++ b/Shoko.Server/Filters/FilterEvaluator.cs @@ -39,18 +39,26 @@ public FilterEvaluator(AnimeGroupRepository groups, AnimeSeriesRepository series public IEnumerable<IGrouping<int, int>> EvaluateFilter(FilterPreset filter, int? userID) { ArgumentNullException.ThrowIfNull(filter); - var user = filter.Expression?.UserDependent ?? false; - if (user && userID == null) + var needsUser = filter.Expression?.UserDependent ?? false; + if (needsUser && userID == null) { throw new ArgumentNullException(nameof(userID)); } + var user = userID != null ? RepoFactory.JMMUser.GetByID(userID.Value) : null; + var filterables = filter.ApplyAtSeriesLevel switch { - true when user => _series?.GetAll().AsParallel().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))) ?? Array.Empty<FilterableWithID>().AsParallel(), - true => _series?.GetAll().AsParallel().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToFilterable())) ?? Array.Empty<FilterableWithID>().AsParallel(), - false when user => _groups?.GetAll().AsParallel().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))) ?? Array.Empty<FilterableWithID>().AsParallel(), - false => _groups?.GetAll().AsParallel().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToFilterable())) ?? Array.Empty<FilterableWithID>().AsParallel() + true when needsUser => _series?.GetAll().AsParallel().Where(a => user?.AllowedSeries(a) ?? true).Select(a => + new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))) ?? + Array.Empty<FilterableWithID>().AsParallel(), + true => _series?.GetAll().AsParallel().Where(a => user?.AllowedSeries(a) ?? true) + .Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToFilterable())) ?? Array.Empty<FilterableWithID>().AsParallel(), + false when needsUser => _groups?.GetAll().AsParallel().Where(a => user?.AllowedGroup(a) ?? true) + .Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))) ?? + Array.Empty<FilterableWithID>().AsParallel(), + false => _groups?.GetAll().AsParallel().Where(a => user?.AllowedGroup(a) ?? true) + .Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToFilterable())) ?? Array.Empty<FilterableWithID>().AsParallel() }; // Filtering @@ -81,15 +89,16 @@ public Dictionary<FilterPreset, IEnumerable<IGrouping<int, int>>> BatchEvaluateF ArgumentNullException.ThrowIfNull(filters); if (!filters.Any()) return new Dictionary<FilterPreset, IEnumerable<IGrouping<int, int>>>(); // count it as a user filter if it needs to sort using a user-dependent expression - var user = filters.Any(a => (a?.Expression?.UserDependent ?? false) || skipSorting && (a?.SortingExpression?.UserDependent ?? false)); - if (user && userID == null) throw new ArgumentNullException(nameof(userID)); + var needsUser = filters.Any(a => (a?.Expression?.UserDependent ?? false) || skipSorting && (a?.SortingExpression?.UserDependent ?? false)); + if (needsUser && userID == null) throw new ArgumentNullException(nameof(userID)); + var user = userID != null ? RepoFactory.JMMUser.GetByID(userID.Value) : null; var filterables = filters.Any(a => a.ApplyAtSeriesLevel) switch { - true when user => _series.GetAll().AsParallel().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), - true => _series.GetAll().AsParallel().Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToFilterable())), - false when user => _groups.GetAll().AsParallel().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), - false => _groups.GetAll().AsParallel().Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToFilterable())) + true when needsUser => _series.GetAll().AsParallel().Where(a => user?.AllowedSeries(a) ?? true).Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), + true => _series.GetAll().AsParallel().Where(a => user?.AllowedSeries(a) ?? true).Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToFilterable())), + false when needsUser => _groups.GetAll().AsParallel().Where(a => user?.AllowedGroup(a) ?? true).Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), + false => _groups.GetAll().AsParallel().Where(a => user?.AllowedGroup(a) ?? true).Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToFilterable())) }; // Filtering From d2f2db5b3605786c69a0c51803238736f18b2415 Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Mon, 25 Sep 2023 21:41:14 -0400 Subject: [PATCH 28/34] Some work on Filter API --- .../ShokoServiceImplementation_Entities.cs | 6 +++--- .../ShokoServiceImplementationMetro.cs | 2 +- Shoko.Server/API/v2/Models/common/Filters.cs | 13 +++++++------ Shoko.Server/API/v2/Modules/Common.cs | 1 + 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs index 2beab5b4b..e52b95f63 100755 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs @@ -166,7 +166,7 @@ public List<CL_AnimeEpisode_User> GetContinueWatchingFilter(int userID, int maxR // find the locked Continue Watching Filter var lockedGFs = RepoFactory.FilterPreset.GetLockedGroupFilters(); - var gf = lockedGFs?.FirstOrDefault(a => a.FilterType == GroupFilterType.ContinueWatching); + var gf = lockedGFs?.FirstOrDefault(a => a.Name == "Continue Watching"); if (gf == null) return retEps; var evaluator = HttpContext.RequestServices.GetRequiredService<FilterEvaluator>(); @@ -3293,8 +3293,8 @@ public CL_GroupFilter EvaluateGroupFilter(CL_GroupFilter contract) ParentGroupFilterID = filter.ParentFilterPresetID, InvisibleInClients = filter.Hidden ? 1 : 0, BaseCondition = 0, - FilterConditions = null, - SortingCriteria = LegacyConditionConverter.GetSortingCriteria(filter), + FilterConditions = contract.FilterConditions, + SortingCriteria = contract.SortingCriteria, Groups = groupIds, Series = seriesIds, Childs = filter.FilterPresetID == 0 diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs index d2129e0af..4ed4df31d 100644 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs @@ -524,7 +524,7 @@ public List<Metro_Anime_Summary> GetAnimeContinueWatching(int maxRecords, int jm if (lockedGFs != null) { // if it already exists we can leave - foreach (var gfTemp in lockedGFs.Where(gfTemp => gfTemp.FilterType == GroupFilterType.ContinueWatching)) + foreach (var gfTemp in lockedGFs.Where(gfTemp => gfTemp.Name == "Continue Watching")) { gf = gfTemp; break; diff --git a/Shoko.Server/API/v2/Models/common/Filters.cs b/Shoko.Server/API/v2/Models/common/Filters.cs index 3bddab479..d6aee9315 100644 --- a/Shoko.Server/API/v2/Models/common/Filters.cs +++ b/Shoko.Server/API/v2/Models/common/Filters.cs @@ -37,14 +37,15 @@ internal static Filters GenerateFromGroupFilter(HttpContext ctx, FilterPreset gf (!hideCategories.Contains(a.Name) || TagFilter.IsTagBlackListed(a.Name, tagfilter)))) .ToList(); - if (level > 0) + if (evaluatedResults == null) { - if (evaluatedResults == null) - { - var evaluator = ctx.RequestServices.GetRequiredService<FilterEvaluator>(); - evaluatedResults = evaluator.BatchEvaluateFilters(gfs, ctx.GetUser().JMMUserID); - } + var evaluator = ctx.RequestServices.GetRequiredService<FilterEvaluator>(); + evaluatedResults = evaluator.BatchEvaluateFilters(gfs, ctx.GetUser().JMMUserID); + gfs = gfs.Where(a => evaluatedResults[a].Any()).ToList(); + } + if (level > 0) + { var filters = gfs.Select(cgf => Filter.GenerateFromGroupFilter(ctx, cgf, uid, nocast, notag, level - 1, all, allpic, pic, tagfilter, evaluatedResults[cgf].ToList())).ToList(); diff --git a/Shoko.Server/API/v2/Modules/Common.cs b/Shoko.Server/API/v2/Modules/Common.cs index d720efb27..2305aaba6 100644 --- a/Shoko.Server/API/v2/Modules/Common.cs +++ b/Shoko.Server/API/v2/Modules/Common.cs @@ -2738,6 +2738,7 @@ internal object GetAllFilters(int uid, bool nocast, bool notag, int level, bool ? RepoFactory.FilterPreset.GetAll().Where(a => (a.FilterType & GroupFilterType.Tag) == 0 || !hideCategories.Contains(a.Name)).ToList() : allGfs; var result = evaluator.BatchEvaluateFilters(filtersToEvaluate, user.JMMUserID); + allGfs = allGfs.Where(a => result[a].Any()).ToList(); foreach (var gf in allGfs) { From 339b40103fe954d8e8ef6b58a354780e22fccaaf Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Tue, 26 Sep 2023 00:30:59 -0400 Subject: [PATCH 29/34] Some Desktop and V2 Compatibility Improvements --- .../ShokoServiceImplementation_Entities.cs | 27 +++---- Shoko.Server/Filters/FilterExtensions.cs | 6 +- .../Legacy/LegacyConditionConverter.cs | 11 +-- .../Filters/Legacy/LegacyFilterConverter.cs | 70 ++++++++++--------- 4 files changed, 53 insertions(+), 61 deletions(-) diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs index e52b95f63..b7d0974bb 100755 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs @@ -184,11 +184,10 @@ public List<CL_AnimeEpisode_User> GetContinueWatchingFilter(int userID, int maxR if (!user.AllowedSeries(ser)) continue; var anime = ser.GetAnime(); - var useSeries = seriesWatching.Count > 0 && anime.AnimeType == (int)AnimeType.TVSeries && !anime.GetRelatedAnime().Any(a => + var useSeries = seriesWatching.Count == 0 || anime.AnimeType != (int)AnimeType.TVSeries || !anime.GetRelatedAnime().Any(a => a.RelationType.ToLower().Trim().Equals("sequel") || a.RelationType.ToLower().Trim().Equals("prequel")); if (!useSeries) continue; - var ep = GetNextUnwatchedEpisode(ser.AnimeSeriesID, userID); if (ep == null) continue; @@ -349,30 +348,20 @@ public List<CL_AnimeEpisode_User> GetEpisodesRecentlyAdded(int maxRecords, int u } // We will deal with a large list, don't perform ops on the whole thing! - var vids = RepoFactory.VideoLocal.GetMostRecentlyAdded(maxRecords, userID); + var vids = RepoFactory.VideoLocal.GetMostRecentlyAdded(maxRecords*5, userID); foreach (var vid in vids) { - if (string.IsNullOrEmpty(vid.Hash)) - { - continue; - } + if (string.IsNullOrEmpty(vid.Hash)) continue; foreach (var ep in vid.GetAnimeEpisodes()) { var epContract = ep.GetUserContract(userID); - if (user.AllowedSeries(ep.GetAnimeSeries())) - { - if (epContract != null) - { - retEps.Add(epContract); + if (!user.AllowedSeries(ep.GetAnimeSeries()) || epContract == null) continue; + retEps.Add(epContract); - // Lets only return the specified amount - if (retEps.Count >= maxRecords) - { - return retEps; - } - } - } + // Lets only return the specified amount + if (retEps.Count < maxRecords) continue; + return retEps; } } } diff --git a/Shoko.Server/Filters/FilterExtensions.cs b/Shoko.Server/Filters/FilterExtensions.cs index 46a8081b4..2fb756452 100644 --- a/Shoko.Server/Filters/FilterExtensions.cs +++ b/Shoko.Server/Filters/FilterExtensions.cs @@ -139,8 +139,8 @@ public static UserDependentFilterable ToUserDependentFilterable(this SVR_AnimeSe return subtitles; }, IsFavoriteDelegate = () => false, - WatchedEpisodesDelegate = () => user?.WatchedCount ?? 0, - UnwatchedEpisodesDelegate = () => (anime?.EpisodeCount ?? 0) - (user?.WatchedCount ?? 0), + WatchedEpisodesDelegate = () => user?.WatchedEpisodeCount ?? 0, + UnwatchedEpisodesDelegate = () => user?.UnwatchedEpisodeCount ?? 0, LowestUserRatingDelegate = () => vote?.VoteValue ?? 0, HighestUserRatingDelegate = () => vote?.VoteValue ?? 0, HasVotesDelegate = () => vote != null, @@ -362,7 +362,7 @@ public static Filterable ToUserDependentFilterable(this SVR_AnimeGroup group, in return subtitles; }, IsFavoriteDelegate = () => user?.IsFave == 1, - WatchedEpisodesDelegate = () => user?.WatchedCount ?? 0, + WatchedEpisodesDelegate = () => user?.WatchedEpisodeCount ?? 0, UnwatchedEpisodesDelegate = () => user?.UnwatchedEpisodeCount ?? 0, LowestUserRatingDelegate = () => vote.FirstOrDefault(), HighestUserRatingDelegate = () => vote.LastOrDefault(), diff --git a/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs b/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs index 978c38048..d313d9868 100644 --- a/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs +++ b/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs @@ -240,7 +240,7 @@ private static bool TryGetInCondition(FilterExpression expression, out GroupFilt { ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, ConditionType = (int)GroupFilterConditionType.AnimeType, - ConditionParameter = string.Join(",", animeType) + ConditionParameter = string.Join(",", animeType).Replace(" ", "") }; return true; } @@ -613,10 +613,11 @@ private static bool TryParseComparator(FilterExpression expression, Type type, o parameter = null; switch (expression) { + // These are inverted because the parameter is second, as compared to the Legacy method, which evaluated as parameter operator selector case DateGreaterThanExpression dateGreater when dateGreater.Left?.GetType() != type: return false; case DateGreaterThanExpression dateGreater: - gfOperator = GroupFilterOperator.GreaterThan; + gfOperator = GroupFilterOperator.LessThan; parameter = dateGreater.Parameter; return true; case DateGreaterThanEqualsExpression dateGreaterEquals when dateGreaterEquals.Left?.GetType() != type: @@ -634,19 +635,19 @@ private static bool TryParseComparator(FilterExpression expression, Type type, o case NumberGreaterThanExpression numberGreater when numberGreater.Left?.GetType() != type: return false; case NumberGreaterThanExpression numberGreater: - gfOperator = GroupFilterOperator.GreaterThan; + gfOperator = GroupFilterOperator.LessThan; parameter = numberGreater.Parameter; return true; case DateLessThanExpression dateLess when dateLess.Left?.GetType() != type: return false; case DateLessThanExpression dateLess: - gfOperator = GroupFilterOperator.LessThan; + gfOperator = GroupFilterOperator.GreaterThan; parameter = dateLess.Parameter; return true; case NumberLessThanExpression numberLess when numberLess.Left?.GetType() != type: return false; case NumberLessThanExpression numberLess: - gfOperator = GroupFilterOperator.LessThan; + gfOperator = GroupFilterOperator.GreaterThan; parameter = numberLess.Parameter; return true; default: diff --git a/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs b/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs index 33e30f0e3..7ab8956c1 100644 --- a/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs +++ b/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs @@ -107,41 +107,43 @@ public Dictionary<FilterPreset, CL_GroupFilter> ToClient(IReadOnlyList<FilterPre var otherFilters = filters.Except(userFilters).ToList(); // batch evaluate each list, then build the mappings - foreach (var userID in RepoFactory.JMMUser.GetAll().Select(a => a.JMMUserID)) + var userResults = RepoFactory.JMMUser.GetAll() + .SelectMany(user => _evaluator.BatchEvaluateFilters(userFilters, user.JMMUserID).Select(a => (a.Key, user.JMMUserID, a.Value))) + .GroupBy(a => a.Key, a => (a.JMMUserID, a.Value)); + var userModels = userResults.Select(group => { - var results = _evaluator.BatchEvaluateFilters(userFilters, userID); - var models = results.Select(kv => + var filter = group.Key; + var groupIds = new Dictionary<int, HashSet<int>>(); + var seriesIds = new Dictionary<int, HashSet<int>>(); + foreach (var kv in group) { - var filter = kv.Key; - var groupIds = new Dictionary<int, HashSet<int>>(); - var seriesIds = new Dictionary<int, HashSet<int>>(); - groupIds[userID] = kv.Value.Select(a => a.Key).ToHashSet(); - seriesIds[userID] = kv.Value.SelectMany(a => a).ToHashSet(); - LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); - return (Filter: filter, new CL_GroupFilter - { - GroupFilterID = filter.FilterPresetID, - GroupFilterName = filter.Name, - ApplyToSeries = filter.ApplyAtSeriesLevel ? 1 : 0, - Locked = filter.Locked ? 1 : 0, - FilterType = (int)filter.FilterType, - ParentGroupFilterID = filter.ParentFilterPresetID, - InvisibleInClients = filter.Hidden ? 1 : 0, - BaseCondition = (int)baseCondition, - FilterConditions = conditions, - SortingCriteria = LegacyConditionConverter.GetSortingCriteria(filter), - Groups = groupIds, - Series = seriesIds, - Childs = filter.FilterPresetID == 0 - ? new HashSet<int>() - : RepoFactory.FilterPreset.GetByParentID(filter.FilterPresetID).Select(a => a.FilterPresetID).ToHashSet() - }); + groupIds[kv.JMMUserID] = kv.Value.Select(a => a.Key).ToHashSet(); + seriesIds[kv.JMMUserID] = kv.Value.SelectMany(a => a).ToHashSet(); + } + LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + return (Filter: filter, new CL_GroupFilter + { + GroupFilterID = filter.FilterPresetID, + GroupFilterName = filter.Name, + ApplyToSeries = filter.ApplyAtSeriesLevel ? 1 : 0, + Locked = filter.Locked ? 1 : 0, + FilterType = (int)filter.FilterType, + ParentGroupFilterID = filter.ParentFilterPresetID, + InvisibleInClients = filter.Hidden ? 1 : 0, + BaseCondition = (int)baseCondition, + FilterConditions = conditions, + SortingCriteria = LegacyConditionConverter.GetSortingCriteria(filter), + Groups = groupIds, + Series = seriesIds, + Childs = filter.FilterPresetID == 0 + ? new HashSet<int>() + : RepoFactory.FilterPreset.GetByParentID(filter.FilterPresetID).Select(a => a.FilterPresetID).ToHashSet() }); + }); - foreach (var (filter, model) in models) - { - result[filter] = model; - } + foreach (var (filter, model) in userModels) + { + result[filter] = model; } if (otherFilters.Count > 0) @@ -154,10 +156,10 @@ public Dictionary<FilterPreset, CL_GroupFilter> ToClient(IReadOnlyList<FilterPre var seriesIds = new Dictionary<int, HashSet<int>>(); var groupIdSet = kv.Value.Select(a => a.Key).ToHashSet(); var seriesIdSet = kv.Value.SelectMany(a => a).ToHashSet(); - foreach (var userID in RepoFactory.JMMUser.GetAll().Select(a => a.JMMUserID)) + foreach (var user in RepoFactory.JMMUser.GetAll()) { - groupIds[userID] = groupIdSet; - seriesIds[userID] = seriesIdSet; + groupIds[user.JMMUserID] = groupIdSet.Where(a => user.AllowedGroup(RepoFactory.AnimeGroup.GetByID(a))).ToHashSet(); + seriesIds[user.JMMUserID] = seriesIdSet.Where(a => user.AllowedSeries(RepoFactory.AnimeSeries.GetByID(a))).ToHashSet(); } LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); From 04f3f7d64046caae98bb7354f8a9b3afc9fc341e Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Tue, 26 Sep 2023 21:40:26 -0400 Subject: [PATCH 30/34] Fix some logic issues in filter conversion --- .../ShokoServiceImplementation_Entities.cs | 55 +------------------ Shoko.Server/Filters/FilterExpression.cs | 5 +- .../Legacy/LegacyConditionConverter.cs | 25 +++++++-- .../Filters/Legacy/LegacyFilterConverter.cs | 8 ++- Shoko.Server/Filters/Legacy/LegacyMappings.cs | 12 ++-- 5 files changed, 37 insertions(+), 68 deletions(-) diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs index b7d0974bb..400d6bd48 100755 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs @@ -3238,58 +3238,9 @@ public CL_GroupFilter EvaluateGroupFilter(CL_GroupFilter contract) { try { - var expression = LegacyConditionConverter.GetExpression(contract.FilterConditions, (GroupFilterBaseCondition)contract.BaseCondition); - - var filter = new FilterPreset - { - Expression = expression, - ApplyAtSeriesLevel = contract.ApplyToSeries == 1, - Name = contract.GroupFilterName, - SortingExpression = LegacyConditionConverter.GetSortingExpression(contract.SortingCriteria) - }; - - var evaluator = HttpContext.RequestServices.GetRequiredService<FilterEvaluator>(); - var groupIds = new Dictionary<int, HashSet<int>>(); - var seriesIds = new Dictionary<int, HashSet<int>>(); - if ((filter.Expression?.UserDependent ?? false) || (filter.SortingExpression?.UserDependent ?? false)) - { - foreach (var userID in RepoFactory.JMMUser.GetAll().Select(a => a.JMMUserID)) - { - var results = evaluator.EvaluateFilter(filter, userID).ToList(); - groupIds[userID] = results.Select(a => a.Key).ToHashSet(); - seriesIds[userID] = results.SelectMany(a => a).ToHashSet(); - } - } - else - { - var results = evaluator.EvaluateFilter(filter, null).ToList(); - var groupIdSet = results.Select(a => a.Key).ToHashSet(); - var seriesIdSet = results.SelectMany(a => a).ToHashSet(); - foreach (var userID in RepoFactory.JMMUser.GetAll().Select(a => a.JMMUserID)) - { - groupIds[userID] = groupIdSet; - seriesIds[userID] = seriesIdSet; - } - } - - var model = new CL_GroupFilter - { - GroupFilterID = filter.FilterPresetID, - GroupFilterName = filter.Name, - ApplyToSeries = filter.ApplyAtSeriesLevel ? 1 : 0, - Locked = filter.Locked ? 1 : 0, - FilterType = (int)filter.FilterType, - ParentGroupFilterID = filter.ParentFilterPresetID, - InvisibleInClients = filter.Hidden ? 1 : 0, - BaseCondition = 0, - FilterConditions = contract.FilterConditions, - SortingCriteria = contract.SortingCriteria, - Groups = groupIds, - Series = seriesIds, - Childs = filter.FilterPresetID == 0 - ? new HashSet<int>() - : RepoFactory.FilterPreset.GetByParentID(filter.FilterPresetID).Select(a => a.FilterPresetID).ToHashSet() - }; + var legacyConverter = HttpContext.RequestServices.GetRequiredService<LegacyFilterConverter>(); + var filter = legacyConverter.FromClient(contract); + var model = legacyConverter.ToClient(filter); return model; } catch (Exception ex) diff --git a/Shoko.Server/Filters/FilterExpression.cs b/Shoko.Server/Filters/FilterExpression.cs index 11805c9ff..6453ccb89 100644 --- a/Shoko.Server/Filters/FilterExpression.cs +++ b/Shoko.Server/Filters/FilterExpression.cs @@ -1,12 +1,13 @@ using System.Runtime.Serialization; +using Newtonsoft.Json; using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters; public class FilterExpression : IFilterExpression { - [IgnoreDataMember] public virtual bool TimeDependent => false; - [IgnoreDataMember] public virtual bool UserDependent => false; + [IgnoreDataMember][JsonIgnore] public virtual bool TimeDependent => false; + [IgnoreDataMember][JsonIgnore] public virtual bool UserDependent => false; protected virtual bool Equals(FilterExpression other) { diff --git a/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs b/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs index d313d9868..fb678fd00 100644 --- a/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs +++ b/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs @@ -37,6 +37,14 @@ public static bool TryConvertToConditions(FilterPreset filter, out List<GroupFil conditions = null; var results = new List<GroupFilterCondition>(); + // single condition + if (TryGetIncludeCondition(expression, out var condition)) + { + results.Add(condition); + baseCondition = GroupFilterBaseCondition.Include; + return true; + } + if (expression is NotExpression not) { baseCondition = GroupFilterBaseCondition.Exclude; @@ -52,11 +60,18 @@ public static bool TryConvertToConditions(FilterPreset filter, out List<GroupFil return true; } - public static bool TryGetConditionsRecursive(FilterExpression expression, List<GroupFilterCondition> conditions) + private static bool TryGetConditionsRecursive(FilterExpression expression, List<GroupFilterCondition> conditions) { + // Do this first, as compound expressions can throw off the following logic + if (TryGetIncludeCondition(expression, out var condition)) + { + conditions.Add(condition); + return true; + } + if (expression is AndExpression and) return TryGetConditionsRecursive(and.Left, conditions) && TryGetConditionsRecursive(and.Right, conditions); - if (!TryGetCondition(expression, out var condition)) return false; + if (!TryGetCondition(expression, out condition)) return false; conditions.Add(condition); return true; } @@ -69,7 +84,7 @@ private static bool TryGetCondition(FilterExpression expression, out GroupFilter return true; } - if (TryGetIncludeCondition(expression, out condition)) return true; + if (TryGetInCondition(expression, out condition)) return true; if (TryGetComparatorCondition(expression, out condition)) return true; return false; @@ -240,7 +255,7 @@ private static bool TryGetInCondition(FilterExpression expression, out GroupFilt { ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, ConditionType = (int)GroupFilterConditionType.AnimeType, - ConditionParameter = string.Join(",", animeType).Replace(" ", "") + ConditionParameter = string.Join(",", animeType) }; return true; } @@ -455,7 +470,7 @@ private static bool IsInTag(FilterExpression expression, out List<string> parame private static bool IsInCustomTag(FilterExpression expression, out List<string> parameters, out bool inverted) { parameters = new List<string>(); - return TryParseIn(expression, typeof(HasTagExpression), parameters, out inverted); + return TryParseIn(expression, typeof(HasCustomTagExpression), parameters, out inverted); } private static bool IsInAnimeType(FilterExpression expression, out List<string> parameters, out bool inverted) diff --git a/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs b/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs index 7ab8956c1..ff8d84f35 100644 --- a/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs +++ b/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs @@ -3,7 +3,6 @@ using Shoko.Models.Client; using Shoko.Models.Enums; using Shoko.Models.Server; -using Shoko.Server.Filters.Logic; using Shoko.Server.Models; using Shoko.Server.Repositories; @@ -31,7 +30,7 @@ public FilterPreset FromClient(CL_GroupFilter model) Locked = model.Locked == 1, ApplyAtSeriesLevel = model.ApplyToSeries == 1, SortingExpression = LegacyConditionConverter.GetSortingExpression(model.SortingCriteria), - Expression = expression == null ? null : model.BaseCondition == (int)GroupFilterBaseCondition.Exclude ? new NotExpression(expression) : expression + Expression = expression }; return filter; } @@ -47,7 +46,7 @@ public FilterPreset FromLegacy(GroupFilter model, List<GroupFilterCondition> con Locked = model.Locked == 1, ApplyAtSeriesLevel = model.ApplyToSeries == 1, SortingExpression = LegacyConditionConverter.GetSortingExpression(model.SortingCriteria), - Expression = expression == null ? null : model.BaseCondition == (int)GroupFilterBaseCondition.Exclude ? new NotExpression(expression) : expression + Expression = expression }; return filter; } @@ -79,6 +78,7 @@ public CL_GroupFilter ToClient(FilterPreset filter) } LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + foreach (var condition in conditions) condition.GroupFilterID = filter.FilterPresetID; var contract = new CL_GroupFilter { GroupFilterID = filter.FilterPresetID, @@ -121,6 +121,7 @@ public Dictionary<FilterPreset, CL_GroupFilter> ToClient(IReadOnlyList<FilterPre seriesIds[kv.JMMUserID] = kv.Value.SelectMany(a => a).ToHashSet(); } LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + foreach (var condition in conditions) condition.GroupFilterID = filter.FilterPresetID; return (Filter: filter, new CL_GroupFilter { GroupFilterID = filter.FilterPresetID, @@ -163,6 +164,7 @@ public Dictionary<FilterPreset, CL_GroupFilter> ToClient(IReadOnlyList<FilterPre } LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + foreach (var condition in conditions) condition.GroupFilterID = filter.FilterPresetID; return (Filter: filter, new CL_GroupFilter { GroupFilterID = filter.FilterPresetID, diff --git a/Shoko.Server/Filters/Legacy/LegacyMappings.cs b/Shoko.Server/Filters/Legacy/LegacyMappings.cs index c5b410833..f3108241b 100644 --- a/Shoko.Server/Filters/Legacy/LegacyMappings.cs +++ b/Shoko.Server/Filters/Legacy/LegacyMappings.cs @@ -285,18 +285,18 @@ public static FilterExpression<bool> GetAnimeTypeExpression(GroupFilterOperator case GroupFilterOperator.In: case GroupFilterOperator.Include: { - if (tags.Length <= 1) return new HasAnimeTypeExpression(tags[0]); + if (tags.Length <= 1) return new HasAnimeTypeExpression(tags[0].Replace(" ", "")); - FilterExpression<bool> first = new HasAnimeTypeExpression(tags[0]); - return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasAnimeTypeExpression(b))); + FilterExpression<bool> first = new HasAnimeTypeExpression(tags[0].Replace(" ", "")); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasAnimeTypeExpression(b.Replace(" ", "")))); } case GroupFilterOperator.NotIn: case GroupFilterOperator.Exclude: { - if (tags.Length <= 1) return new NotExpression(new HasAnimeTypeExpression(tags[0])); + if (tags.Length <= 1) return new NotExpression(new HasAnimeTypeExpression(tags[0].Replace(" ", ""))); - FilterExpression<bool> first = new HasAnimeTypeExpression(tags[0]); - return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasAnimeTypeExpression(b)))); + FilterExpression<bool> first = new HasAnimeTypeExpression(tags[0].Replace(" ", "")); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasAnimeTypeExpression(b.Replace(" ", ""))))); } default: return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Anime Type"); From bc1aa5d7eaaa7896b8a6f80d673841a9d2881f83 Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Tue, 26 Sep 2023 22:09:21 -0400 Subject: [PATCH 31/34] Missed a Null Check --- Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs b/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs index ff8d84f35..4b95a1100 100644 --- a/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs +++ b/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs @@ -78,7 +78,7 @@ public CL_GroupFilter ToClient(FilterPreset filter) } LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); - foreach (var condition in conditions) condition.GroupFilterID = filter.FilterPresetID; + conditions?.ForEach(condition => condition.GroupFilterID = filter.FilterPresetID); var contract = new CL_GroupFilter { GroupFilterID = filter.FilterPresetID, @@ -121,7 +121,7 @@ public Dictionary<FilterPreset, CL_GroupFilter> ToClient(IReadOnlyList<FilterPre seriesIds[kv.JMMUserID] = kv.Value.SelectMany(a => a).ToHashSet(); } LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); - foreach (var condition in conditions) condition.GroupFilterID = filter.FilterPresetID; + conditions?.ForEach(condition => condition.GroupFilterID = filter.FilterPresetID); return (Filter: filter, new CL_GroupFilter { GroupFilterID = filter.FilterPresetID, @@ -164,7 +164,7 @@ public Dictionary<FilterPreset, CL_GroupFilter> ToClient(IReadOnlyList<FilterPre } LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); - foreach (var condition in conditions) condition.GroupFilterID = filter.FilterPresetID; + conditions?.ForEach(condition => condition.GroupFilterID = filter.FilterPresetID); return (Filter: filter, new CL_GroupFilter { GroupFilterID = filter.FilterPresetID, From d23a01a3993a7ac0af0d411919ce12cc351123f8 Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Wed, 27 Sep 2023 11:23:23 -0400 Subject: [PATCH 32/34] Update Filters readme.md --- Shoko.Server/Filters/readme.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Shoko.Server/Filters/readme.md b/Shoko.Server/Filters/readme.md index f630ef53c..47e41abf2 100644 --- a/Shoko.Server/Filters/readme.md +++ b/Shoko.Server/Filters/readme.md @@ -1,6 +1,5 @@ An Expression is anything that transforms data: a method that takes zero or more arguments and returns a result. -Expressions are stored with TPH discriminated on Type. Arguments should be mapped to Type + "Argument" + Index ( -StringArgument1) in the mapping. Expressions should not have more than 5 Arguments of each type. If it would, then it +Expressions are stored with TPH discriminated on Type. Expressions should not have more than 5 Arguments of each type. If it would, then it should be redesigned. For example, `And(HasTag("comedy"),HasTag("action"))`. This is to keep the database schema reasonable and expressions simple. I considered a one-to-many map for arguments, and making that automatic for navigation properties seemed like a lot of work when we can design around it. @@ -11,10 +10,6 @@ Acceptable database types (can be mapped to other CLR types like enums) for Argu - Double (integers should be coerced to double for simplicity) - DateTime -Default CLR Argument mappings: - -- FilterExpression via foreign key - FilterExpression is a single Expression, whether that's something like "Or" or "HasTag" in `And(Or(HasTag('comedy'), HasTag('action')), Not(HasTag('18 restricted')))`. Expressions should be the least amount of work possible. NAND can be expressed as `Not(And())`. Exceptions can be made if it would take more than 2 or 3 @@ -23,4 +18,6 @@ people would expect those to exist. This is all a design for the back end. Expressions can be communicated in many ways. For example, the above could be written `tag: 'comedy' or tag: 'action' and not tag: '18 restricted'` -or `HasAnyTags('comedy', 'action') && HasNoTags('18 restriced')`. It just depends on how you map the input. \ No newline at end of file +or `HasAnyTags('comedy', 'action') && HasNoTags('18 restriced')`. It just depends on how you map the input. + +See https://en.m.wikipedia.org/wiki/Binary_expression_tree From d47ee19171124313adfda310dfa251a64862cf25 Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Wed, 27 Sep 2023 20:32:35 -0400 Subject: [PATCH 33/34] Update Logic for Legacy Filter Conversion --- Shoko.Commons | 2 +- .../Legacy/LegacyConditionConverter.cs | 214 +++++++++--------- .../Shoko.Tests/LegacyFilterConditionTests.cs | 194 ++++++++++++++++ Shoko.Tests/Shoko.Tests/LegacyFilterTests.cs | 46 +++- 4 files changed, 341 insertions(+), 115 deletions(-) create mode 100644 Shoko.Tests/Shoko.Tests/LegacyFilterConditionTests.cs diff --git a/Shoko.Commons b/Shoko.Commons index 4b37ffeb0..dbfe0d9f5 160000 --- a/Shoko.Commons +++ b/Shoko.Commons @@ -1 +1 @@ -Subproject commit 4b37ffeb03613b6e3fc132b37505acfb02c4a3cb +Subproject commit dbfe0d9f5b00b421349c1177f4ad37ead5c4c846 diff --git a/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs b/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs index fb678fd00..bbe4f3137 100644 --- a/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs +++ b/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs @@ -35,70 +35,72 @@ public static bool TryConvertToConditions(FilterPreset filter, out List<GroupFil return true; } - conditions = null; - var results = new List<GroupFilterCondition>(); - // single condition - if (TryGetIncludeCondition(expression, out var condition)) + if (TryGetSingleCondition(expression, out var condition)) { - results.Add(condition); baseCondition = GroupFilterBaseCondition.Include; + conditions = new List<GroupFilterCondition> { condition }; return true; } + var results = new List<GroupFilterCondition>(); if (expression is NotExpression not) { baseCondition = GroupFilterBaseCondition.Exclude; - if (!TryGetConditionsRecursive(not.Left, results)) return false; - - conditions = results; - return true; + if (TryGetConditionsRecursive<OrExpression>(not.Left, results)) + { + conditions = results; + return true; + } } baseCondition = GroupFilterBaseCondition.Include; - if (!TryGetConditionsRecursive(expression, results)) return false; - conditions = results; - return true; - } - - private static bool TryGetConditionsRecursive(FilterExpression expression, List<GroupFilterCondition> conditions) - { - // Do this first, as compound expressions can throw off the following logic - if (TryGetIncludeCondition(expression, out var condition)) + if (TryGetConditionsRecursive<AndExpression>(expression, results)) { - conditions.Add(condition); + conditions = results; return true; } - if (expression is AndExpression and) return TryGetConditionsRecursive(and.Left, conditions) && TryGetConditionsRecursive(and.Right, conditions); + conditions = null; + return false; + } - if (!TryGetCondition(expression, out condition)) return false; - conditions.Add(condition); - return true; + private static bool TryGetSingleCondition(FilterExpression expression, out GroupFilterCondition condition) + { + if (TryGetIncludeCondition(expression, out condition)) return true; + if (TryGetInCondition(expression, out condition)) return true; + return TryGetComparatorCondition(expression, out condition); } - private static bool TryGetCondition(FilterExpression expression, out GroupFilterCondition condition) + + private static bool TryGetConditionsRecursive<T>(FilterExpression expression, List<GroupFilterCondition> conditions) where T : IWithExpressionParameter, IWithSecondExpressionParameter { - if (expression is NotExpression not && TryGetIncludeCondition(not.Left, out condition)) + // Do this first, as compound expressions can throw off the following logic + if (TryGetSingleCondition(expression, out var condition)) { - condition.ConditionOperator = (int)GroupFilterOperator.Exclude; + conditions.Add(condition); return true; } - - if (TryGetInCondition(expression, out condition)) return true; - if (TryGetComparatorCondition(expression, out condition)) return true; + if (expression is T and) return TryGetConditionsRecursive<T>(and.Left, conditions) && TryGetConditionsRecursive<T>(and.Right, conditions); return false; } private static bool TryGetIncludeCondition(FilterExpression expression, out GroupFilterCondition condition) { + var conditionOperator = GroupFilterOperator.Include; + if (expression is NotExpression not) + { + conditionOperator = GroupFilterOperator.Exclude; + expression = not.Left; + } + condition = null; var type = expression.GetType(); if (type == typeof(HasMissingEpisodesExpression)) { condition = new GroupFilterCondition { - ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.MissingEpisodes + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.MissingEpisodes }; return true; } @@ -107,7 +109,7 @@ private static bool TryGetIncludeCondition(FilterExpression expression, out Grou { condition = new GroupFilterCondition { - ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.MissingEpisodesCollecting + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.MissingEpisodesCollecting }; return true; } @@ -116,7 +118,7 @@ private static bool TryGetIncludeCondition(FilterExpression expression, out Grou { condition = new GroupFilterCondition { - ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.HasUnwatchedEpisodes + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.HasUnwatchedEpisodes }; return true; } @@ -125,7 +127,7 @@ private static bool TryGetIncludeCondition(FilterExpression expression, out Grou { condition = new GroupFilterCondition { - ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.HasWatchedEpisodes + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.HasWatchedEpisodes }; return true; } @@ -134,7 +136,7 @@ private static bool TryGetIncludeCondition(FilterExpression expression, out Grou { condition = new GroupFilterCondition { - ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.UserVoted + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.UserVoted }; return true; } @@ -143,7 +145,7 @@ private static bool TryGetIncludeCondition(FilterExpression expression, out Grou { condition = new GroupFilterCondition { - ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.UserVotedAny + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.UserVotedAny }; return true; } @@ -152,7 +154,7 @@ private static bool TryGetIncludeCondition(FilterExpression expression, out Grou { condition = new GroupFilterCondition { - ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.AssignedTvDBInfo + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.AssignedTvDBInfo }; return true; } @@ -161,7 +163,7 @@ private static bool TryGetIncludeCondition(FilterExpression expression, out Grou { condition = new GroupFilterCondition { - ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.AssignedMovieDBInfo + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.AssignedMovieDBInfo }; return true; } @@ -170,7 +172,7 @@ private static bool TryGetIncludeCondition(FilterExpression expression, out Grou { condition = new GroupFilterCondition { - ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.AssignedTraktInfo + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.AssignedTraktInfo }; return true; } @@ -179,7 +181,7 @@ private static bool TryGetIncludeCondition(FilterExpression expression, out Grou { condition = new GroupFilterCondition { - ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.Favourite + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.Favourite }; return true; } @@ -188,7 +190,7 @@ private static bool TryGetIncludeCondition(FilterExpression expression, out Grou { condition = new GroupFilterCondition { - ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.FinishedAiring + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.FinishedAiring }; return true; } @@ -197,7 +199,7 @@ private static bool TryGetIncludeCondition(FilterExpression expression, out Grou { condition = new GroupFilterCondition { - ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.AssignedMovieDBInfo + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.AssignedMovieDBInfo }; return true; } @@ -206,7 +208,7 @@ private static bool TryGetIncludeCondition(FilterExpression expression, out Grou { condition = new GroupFilterCondition { - ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.CompletedSeries + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.CompletedSeries }; return true; } @@ -215,7 +217,7 @@ private static bool TryGetIncludeCondition(FilterExpression expression, out Grou { condition = new GroupFilterCondition { - ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.AssignedTvDBOrMovieDBInfo + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.AssignedTvDBOrMovieDBInfo }; return true; } @@ -225,140 +227,146 @@ private static bool TryGetIncludeCondition(FilterExpression expression, out Grou private static bool TryGetInCondition(FilterExpression expression, out GroupFilterCondition condition) { - condition = null; + var conditionOperator = GroupFilterOperator.In; + if (expression is NotExpression not) + { + conditionOperator = GroupFilterOperator.NotIn; + expression = not.Left; + } - if (IsInTag(expression, out var tags, out var inverted)) + if (IsInTag(expression, out var tags)) { condition = new GroupFilterCondition { - ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.Tag, ConditionParameter = string.Join(",", tags) }; return true; } - if (IsInCustomTag(expression, out var customTags, out inverted)) + if (IsInCustomTag(expression, out var customTags)) { condition = new GroupFilterCondition { - ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.CustomTags, ConditionParameter = string.Join(",", customTags) }; return true; } - if (IsInAnimeType(expression, out var animeType, out inverted)) + if (IsInAnimeType(expression, out var animeType)) { condition = new GroupFilterCondition { - ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.AnimeType, ConditionParameter = string.Join(",", animeType) }; return true; } - if (IsInVideoQuality(expression, out var videoQualities, out inverted)) + if (IsInVideoQuality(expression, out var videoQualities)) { condition = new GroupFilterCondition { - ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.VideoQuality, ConditionParameter = string.Join(",", videoQualities) }; return true; } - if (IsInGroup(expression, out var groups, out inverted)) + if (IsInGroup(expression, out var groups)) { condition = new GroupFilterCondition { - ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.AnimeGroup, ConditionParameter = string.Join(",", groups) }; return true; } - if (IsInYear(expression, out var years, out inverted)) + if (IsInYear(expression, out var years)) { condition = new GroupFilterCondition { - ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.Year, ConditionParameter = string.Join(",", years) }; return true; } - if (IsInSeason(expression, out var seasons, out inverted)) + if (IsInSeason(expression, out var seasons)) { condition = new GroupFilterCondition { - ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.Season, ConditionParameter = string.Join(",", seasons.Select(a => a.Season + " " + a.Year)) }; return true; } - if (IsInAudioLanguage(expression, out var aLanguages, out inverted)) + if (IsInAudioLanguage(expression, out var aLanguages)) { condition = new GroupFilterCondition { - ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.AudioLanguage, ConditionParameter = string.Join(",", aLanguages) }; return true; } - if (IsInSubtitleLanguage(expression, out var sLanguages, out inverted)) + if (IsInSubtitleLanguage(expression, out var sLanguages)) { condition = new GroupFilterCondition { - ConditionOperator = inverted ? (int)GroupFilterOperator.NotIn : (int)GroupFilterOperator.In, + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.SubtitleLanguage, ConditionParameter = string.Join(",", sLanguages) }; return true; } - if (IsInSharedVideoQuality(expression, out var sVideoQuality, out inverted)) + if (IsInSharedVideoQuality(expression, out var sVideoQuality)) { condition = new GroupFilterCondition { - ConditionOperator = inverted ? (int)GroupFilterOperator.NotInAllEpisodes : (int)GroupFilterOperator.InAllEpisodes, + ConditionOperator = conditionOperator == GroupFilterOperator.NotIn ? (int)GroupFilterOperator.NotInAllEpisodes : (int)GroupFilterOperator.InAllEpisodes, ConditionType = (int)GroupFilterConditionType.VideoQuality, ConditionParameter = string.Join(",", sVideoQuality) }; return true; } - if (IsInSharedAudioLanguage(expression, out var sALanguages, out inverted)) + if (IsInSharedAudioLanguage(expression, out var sALanguages)) { condition = new GroupFilterCondition { - ConditionOperator = inverted ? (int)GroupFilterOperator.NotInAllEpisodes : (int)GroupFilterOperator.InAllEpisodes, + ConditionOperator = conditionOperator == GroupFilterOperator.NotIn ? (int)GroupFilterOperator.NotInAllEpisodes : (int)GroupFilterOperator.InAllEpisodes, ConditionType = (int)GroupFilterConditionType.AudioLanguage, ConditionParameter = string.Join(",", sALanguages) }; return true; } - if (IsInSharedSubtitleLanguage(expression, out var sSLanguages, out inverted)) + if (IsInSharedSubtitleLanguage(expression, out var sSLanguages)) { condition = new GroupFilterCondition { - ConditionOperator = inverted ? (int)GroupFilterOperator.NotInAllEpisodes : (int)GroupFilterOperator.InAllEpisodes, + ConditionOperator = conditionOperator == GroupFilterOperator.NotIn ? (int)GroupFilterOperator.NotInAllEpisodes : (int)GroupFilterOperator.InAllEpisodes, ConditionType = (int)GroupFilterConditionType.SubtitleLanguage, ConditionParameter = string.Join(",", sSLanguages) }; return true; } + condition = null; return false; } @@ -461,76 +469,76 @@ private static bool TryGetComparatorCondition(FilterExpression expression, out G return false; } - private static bool IsInTag(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInTag(FilterExpression expression, out List<string> parameters) { parameters = new List<string>(); - return TryParseIn(expression, typeof(HasTagExpression), parameters, out inverted); + return TryParseIn(expression, typeof(HasTagExpression), parameters); } - private static bool IsInCustomTag(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInCustomTag(FilterExpression expression, out List<string> parameters) { parameters = new List<string>(); - return TryParseIn(expression, typeof(HasCustomTagExpression), parameters, out inverted); + return TryParseIn(expression, typeof(HasCustomTagExpression), parameters); } - private static bool IsInAnimeType(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInAnimeType(FilterExpression expression, out List<string> parameters) { parameters = new List<string>(); - return TryParseIn(expression, typeof(HasAnimeTypeExpression), parameters, out inverted); + return TryParseIn(expression, typeof(HasAnimeTypeExpression), parameters); } - private static bool IsInVideoQuality(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInVideoQuality(FilterExpression expression, out List<string> parameters) { parameters = new List<string>(); - return TryParseIn(expression, typeof(HasVideoSourceExpression), parameters, out inverted); + return TryParseIn(expression, typeof(HasVideoSourceExpression), parameters); } - private static bool IsInSharedVideoQuality(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInSharedVideoQuality(FilterExpression expression, out List<string> parameters) { parameters = new List<string>(); - return TryParseIn(expression, typeof(HasSharedVideoSourceExpression), parameters, out inverted); + return TryParseIn(expression, typeof(HasSharedVideoSourceExpression), parameters); } - private static bool IsInGroup(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInGroup(FilterExpression expression, out List<string> parameters) { parameters = new List<string>(); - return TryParseIn(expression, typeof(HasNameExpression), parameters, out inverted); + return TryParseIn(expression, typeof(HasNameExpression), parameters); } - private static bool IsInYear(FilterExpression expression, out List<int> parameters, out bool inverted) + private static bool IsInYear(FilterExpression expression, out List<int> parameters) { parameters = new List<int>(); - return TryParseIn(expression, typeof(InYearExpression), parameters, out inverted); + return TryParseIn(expression, typeof(InYearExpression), parameters); } - private static bool IsInSeason(FilterExpression expression, out List<(int Year, string Season)> parameters, out bool inverted) + private static bool IsInSeason(FilterExpression expression, out List<(int Year, string Season)> parameters) { parameters = new List<(int, string)>(); - return TryParseIn(expression, typeof(InSeasonExpression), parameters, out inverted); + return TryParseIn(expression, typeof(InSeasonExpression), parameters); } - private static bool IsInAudioLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInAudioLanguage(FilterExpression expression, out List<string> parameters) { parameters = new List<string>(); - return TryParseIn(expression, typeof(HasAudioLanguageExpression), parameters, out inverted); + return TryParseIn(expression, typeof(HasAudioLanguageExpression), parameters); } - private static bool IsInSubtitleLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInSubtitleLanguage(FilterExpression expression, out List<string> parameters) { parameters = new List<string>(); - return TryParseIn(expression, typeof(HasSubtitleLanguageExpression), parameters, out inverted); + return TryParseIn(expression, typeof(HasSubtitleLanguageExpression), parameters); } - private static bool IsInSharedAudioLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInSharedAudioLanguage(FilterExpression expression, out List<string> parameters) { parameters = new List<string>(); - return TryParseIn(expression, typeof(HasSharedAudioLanguageExpression), parameters, out inverted); + return TryParseIn(expression, typeof(HasSharedAudioLanguageExpression), parameters); } - private static bool IsInSharedSubtitleLanguage(FilterExpression expression, out List<string> parameters, out bool inverted) + private static bool IsInSharedSubtitleLanguage(FilterExpression expression, out List<string> parameters) { parameters = new List<string>(); - return TryParseIn(expression, typeof(HasSharedSubtitleLanguageExpression), parameters, out inverted); + return TryParseIn(expression, typeof(HasSharedSubtitleLanguageExpression), parameters); } private static bool IsAirDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) @@ -573,16 +581,9 @@ private static bool IsEpisodeCount(FilterExpression expression, out object param return TryParseComparator(expression, typeof(EpisodeCountSelector), out parameter, out gfOperator); } - private static bool TryParseIn<T>(FilterExpression expression, Type type, List<T> parameters, out bool inverted) + private static bool TryParseIn<T>(FilterExpression expression, Type type, List<T> parameters) { - inverted = false; - if (expression is NotExpression not) - { - inverted = true; - expression = not.Left; - } - - if (expression is OrExpression or) return TryParseIn(or.Left, type, parameters, out _) && TryParseIn(or.Right, type, parameters, out _); + if (expression is OrExpression or) return TryParseIn(or.Left, type, parameters) && TryParseIn(or.Right, type, parameters); if (expression.GetType() != type) return false; if (typeof(T) == typeof(string) && expression is IWithStringParameter withString) parameters.Add((T)(object)withString.Parameter); @@ -594,16 +595,9 @@ private static bool TryParseIn<T>(FilterExpression expression, Type type, List<T } - private static bool TryParseIn<T,T1>(FilterExpression expression, Type type, List<(T, T1)> parameters, out bool inverted) + private static bool TryParseIn<T,T1>(FilterExpression expression, Type type, List<(T, T1)> parameters) { - inverted = false; - if (expression is NotExpression not) - { - inverted = true; - expression = not.Left; - } - - if (expression is OrExpression or) return TryParseIn(or.Left, type, parameters, out _) && TryParseIn(or.Right, type, parameters, out _); + if (expression is OrExpression or) return TryParseIn(or.Left, type, parameters) && TryParseIn(or.Right, type, parameters); if (expression.GetType() != type) return false; T first = default; diff --git a/Shoko.Tests/Shoko.Tests/LegacyFilterConditionTests.cs b/Shoko.Tests/Shoko.Tests/LegacyFilterConditionTests.cs new file mode 100644 index 000000000..4a31130e1 --- /dev/null +++ b/Shoko.Tests/Shoko.Tests/LegacyFilterConditionTests.cs @@ -0,0 +1,194 @@ +using System.Collections.Generic; +using Shoko.Models.Enums; +using Shoko.Models.Server; +using Shoko.Server.Filters.Info; +using Shoko.Server.Filters.Legacy; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Models; +using Xunit; + +namespace Shoko.Tests; + +public class LegacyFilterConditionTests +{ + [Fact] + public void ToConditions_Tags_NotIn_Single() + { + var top = new NotExpression(new HasTagExpression("comedy")); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.NotIn, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void ToConditions_Tags_In_Multiple() + { + var top = new OrExpression(new OrExpression(new HasTagExpression("comedy"), new HasTagExpression("shounen")), new HasTagExpression("action")); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy,shounen,action" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void ToConditions_Tags_NotIn_Multiple() + { + var top = new NotExpression(new OrExpression(new OrExpression(new HasTagExpression("comedy"), new HasTagExpression("shounen")), new HasTagExpression("action"))); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.NotIn, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy,shounen,action" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void ToConditions_AnimeType_In_Single() + { + var top = new HasAnimeTypeExpression("TVSeries"); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.AnimeType, + ConditionParameter = "TVSeries" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void ToConditions_AnimeType_NotIn_Single() + { + var top = new NotExpression(new HasAnimeTypeExpression("TVSeries")); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.NotIn, + ConditionType = (int)GroupFilterConditionType.AnimeType, + ConditionParameter = "TVSeries" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void ToConditions_AnimeType_In_Multiple() + { + var top = new OrExpression(new OrExpression(new HasAnimeTypeExpression("TVSeries"), new HasAnimeTypeExpression("Movie")), new HasAnimeTypeExpression("OVA")); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.AnimeType, + ConditionParameter = "TVSeries,Movie,OVA" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void ToConditions_AnimeType_NotIn_Multiple() + { + var top = new NotExpression(new OrExpression(new OrExpression(new HasAnimeTypeExpression("TVSeries"), new HasAnimeTypeExpression("Movie")), new HasAnimeTypeExpression("OVA"))); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.NotIn, + ConditionType = (int)GroupFilterConditionType.AnimeType, + ConditionParameter = "TVSeries,Movie,OVA" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void ToExpression_Tags_In_Single() + { + var conditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy" + } + }; + var expression = LegacyConditionConverter.GetExpression(conditions, GroupFilterBaseCondition.Include); + var expected = new HasTagExpression("comedy"); + Assert.Equivalent(expected, expression); + } + + [Fact] + public void ToExpression_Tags_NotIn_Single() + { + var conditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.NotIn, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy" + } + }; + var expression = LegacyConditionConverter.GetExpression(conditions, GroupFilterBaseCondition.Include); + var expected = new NotExpression(new HasTagExpression("comedy")); + Assert.Equivalent(expected, expression); + } +} diff --git a/Shoko.Tests/Shoko.Tests/LegacyFilterTests.cs b/Shoko.Tests/Shoko.Tests/LegacyFilterTests.cs index e65f9a6bb..1a2af710a 100644 --- a/Shoko.Tests/Shoko.Tests/LegacyFilterTests.cs +++ b/Shoko.Tests/Shoko.Tests/LegacyFilterTests.cs @@ -30,9 +30,9 @@ public void TryConvertToConditions_InvalidFilter_ExpectsNull() [Fact] public void TryConvertToConditions_FilterWithTagIncludeAndExclude() { - var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), + var top = new AndExpression(new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), - new HasWatchedEpisodesExpression()); + new HasWatchedEpisodesExpression()), new NotExpression(new HasUnwatchedEpisodesExpression())); var filter = new FilterPreset { Name = "Test", Expression = top }; var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); @@ -54,6 +54,11 @@ public void TryConvertToConditions_FilterWithTagIncludeAndExclude() { ConditionOperator = (int)GroupFilterOperator.Include, ConditionType = (int)GroupFilterConditionType.HasWatchedEpisodes, + }, + new() + { + ConditionOperator = (int)GroupFilterOperator.Exclude, + ConditionType = (int)GroupFilterConditionType.HasUnwatchedEpisodes, } }; Assert.True(success); @@ -94,13 +99,13 @@ public void TryConvertToConditions_FilterWithTagInOperator_ExcludeBaseCondition( { new() { - ConditionOperator = (int)GroupFilterOperator.In, + ConditionOperator = (int)GroupFilterOperator.NotIn, ConditionType = (int)GroupFilterConditionType.Tag, ConditionParameter = "comedy,shounen,action" } }; Assert.True(success); - Assert.Equal(GroupFilterBaseCondition.Exclude, baseCondition); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); Assert.Equivalent(expectedConditions, conditions); } @@ -136,4 +141,37 @@ public void TryConvertToConditions_FilterWithMultipleConditions_IncludeBaseCondi Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); Assert.Equivalent(expectedConditions, conditions); } + + [Fact] + public void TryConvertToConditions_FilterWithMultipleConditions_ExcludeBaseCondition() + { + var top = new NotExpression(new OrExpression(new OrExpression(new HasTagExpression("comedy"), new InSeasonExpression(2023, AnimeSeason.Winter)), + new IsFinishedExpression())); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy" + }, + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Season, + ConditionParameter = "Winter 2023" + }, + new() + { + ConditionOperator = (int)GroupFilterOperator.Include, + ConditionType = (int)GroupFilterConditionType.FinishedAiring + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Exclude, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } } From a1ffefa53add6d585b79a3ffc01ba8c28412fda2 Mon Sep 17 00:00:00 2001 From: da3dsoul <da3dsoul@gmail.com> Date: Wed, 27 Sep 2023 22:14:43 -0400 Subject: [PATCH 34/34] Fix a Legacy Filter Conversion for Exclude All --- .../Filters/Legacy/LegacyConditionConverter.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs b/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs index bbe4f3137..19719c461 100644 --- a/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs +++ b/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs @@ -734,12 +734,20 @@ public static FilterExpression<bool> GetExpression(List<GroupFilterCondition> co if (conditions == null || conditions.Count < 1) return null; var first = conditions.Select((a, index) => new {Expression= GetExpression(a, suppressErrors), Index=index}).FirstOrDefault(a => a.Expression != null); if (first == null) return null; - var condition = conditions.Count == 1 ? first.Expression : conditions.Skip(first.Index + 1).Aggregate(first.Expression, (a, b) => + if (baseCondition == GroupFilterBaseCondition.Exclude) + { + return new NotExpression(conditions.Count == 1 ? first.Expression : conditions.Skip(first.Index + 1).Aggregate(first.Expression, (a, b) => + { + var result = GetExpression(b, suppressErrors); + return result == null ? a : new OrExpression(a, result); + })); + } + + return conditions.Count == 1 ? first.Expression : conditions.Skip(first.Index + 1).Aggregate(first.Expression, (a, b) => { var result = GetExpression(b, suppressErrors); return result == null ? a : new AndExpression(a, result); }); - return baseCondition == GroupFilterBaseCondition.Exclude ? new NotExpression(condition) : condition; } private static FilterExpression<bool> GetExpression(GroupFilterCondition condition, bool suppressErrors = false)