diff --git a/Shoko.Server/API/v3/Controllers/FilterController.cs b/Shoko.Server/API/v3/Controllers/FilterController.cs index dde469488..45c850967 100644 --- a/Shoko.Server/API/v3/Controllers/FilterController.cs +++ b/Shoko.Server/API/v3/Controllers/FilterController.cs @@ -142,6 +142,7 @@ public ActionResult AddNewFilter(Filter.Input.CreateOrUpdateFilterBody b IWithDateParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.Date, IWithNumberParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.Number, IWithStringParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.String, + IWithStringSetParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.StringSet, IWithTimeSpanParameter => Filter.FilterExpressionHelp.FilterExpressionParameterType.TimeSpan, _ => null }; diff --git a/Shoko.Server/API/v3/Models/Shoko/Filter.cs b/Shoko.Server/API/v3/Models/Shoko/Filter.cs index 219ebaf4c..6727017f8 100644 --- a/Shoko.Server/API/v3/Models/Shoko/Filter.cs +++ b/Shoko.Server/API/v3/Models/Shoko/Filter.cs @@ -230,6 +230,7 @@ public enum FilterExpressionParameterType String, TimeSpan, Bool, + StringSet, } } diff --git a/Shoko.Server/Filters/FilterExtensions.cs b/Shoko.Server/Filters/FilterExtensions.cs index fe26a76d5..d4efd4655 100644 --- a/Shoko.Server/Filters/FilterExtensions.cs +++ b/Shoko.Server/Filters/FilterExtensions.cs @@ -4,10 +4,10 @@ using Shoko.Commons.Extensions; using Shoko.Models.Enums; using Shoko.Models.MediaInfo; -using Shoko.Plugin.Abstractions.DataModels; using Shoko.Server.Models; using Shoko.Server.Providers.AniDB; using Shoko.Server.Repositories; +using Shoko.Server.Server; using AnimeType = Shoko.Models.Enums.AnimeType; using EpisodeType = Shoko.Models.Enums.EpisodeType; @@ -63,6 +63,23 @@ public static Filterable ToFilterable(this SVR_AnimeSeries series) series.GetAvailableImageTypes(), PreferredImageTypesDelegate = () => series.GetPreferredImageTypes(), + CharacterAppearancesDelegate = () => + RepoFactory.AniDB_Anime_Character.GetByAnimeID(series.AniDB_ID) + .GroupBy(a => a.Appearance) + .ToDictionary(a => a.Key, a => a.Select(b => b.CharacterID.ToString()).ToHashSet()) as IReadOnlyDictionary>, + CharacterIDsDelegate = () => + RepoFactory.AniDB_Anime_Character.GetByAnimeID(series.AniDB_ID) + .Select(a => a.CharacterID.ToString()) + .ToHashSet(), + CreatorIDsDelegate = () => + RepoFactory.AniDB_Anime_Character_Creator.GetByAnimeID(series.AniDB_ID).Select(a => a.CreatorID.ToString()) + .Concat(RepoFactory.AniDB_Anime_Staff.GetByAnimeID(series.AniDB_ID).Select(a => a.CreatorID.ToString())) + .ToHashSet(), + CreatorRolesDelegate = () => + RepoFactory.AniDB_Anime_Staff.GetByAnimeID(series.AniDB_ID).Select(a => (a.RoleType, a.CreatorID)) + .Concat(RepoFactory.AniDB_Anime_Character_Creator.GetByAnimeID(series.AniDB_ID).Select(a => (RoleType: CreatorRoleType.Actor, a.CreatorID))) + .GroupBy(a => a.RoleType) + .ToDictionary(a => a.Key, a => a.Select(b => b.CreatorID.ToString()).ToHashSet()) as IReadOnlyDictionary>, HasTmdbLinkDelegate = () => series.TmdbShowCrossReferences.Count is > 0 || series.TmdbMovieCrossReferences.Count is > 0, HasMissingTmdbLinkDelegate = () => @@ -215,6 +232,32 @@ public static Filterable ToFilterable(this SVR_AnimeGroup group) group.AvailableImageTypes, PreferredImageTypesDelegate = () => group.PreferredImageTypes, + CharacterIDsDelegate = () => + series.SelectMany(ser => RepoFactory.AniDB_Anime_Character.GetByAnimeID(ser.AniDB_ID)) + .Select(a => a.CharacterID.ToString()) + .ToHashSet(), + CharacterAppearancesDelegate = () => + series.SelectMany(ser => RepoFactory.AniDB_Anime_Character.GetByAnimeID(ser.AniDB_ID)) + .DistinctBy(a => (a.Appearance, a.CharacterID)) + .Select(a => (a.Appearance, a.CharacterID)) + .GroupBy(a => a.Appearance) + .ToDictionary(a => a.Key, a => a.Select(b => b.CharacterID.ToString()).ToHashSet()) as IReadOnlyDictionary>, + CreatorIDsDelegate = () => + series.SelectMany(ser => RepoFactory.AniDB_Anime_Character_Creator.GetByAnimeID(ser.AniDB_ID)) + .Select(a => a.CreatorID.ToString()) + .Concat(series.SelectMany(ser => RepoFactory.AniDB_Anime_Staff.GetByAnimeID(ser.AniDB_ID).Select(a => a.CreatorID.ToString()))) + .ToHashSet(), + CreatorRolesDelegate = () => + series.SelectMany(ser => RepoFactory.AniDB_Anime_Staff.GetByAnimeID(ser.AniDB_ID)) + .Select(a => (a.RoleType, a.CreatorID)) + .DistinctBy(a => (a.RoleType, a.CreatorID)) + .Concat( + series.SelectMany(ser => RepoFactory.AniDB_Anime_Character_Creator.GetByAnimeID(ser.AniDB_ID) + .DistinctBy(a => a.CreatorID) + .Select(a => (RoleType: CreatorRoleType.Actor, a.CreatorID))) + ) + .GroupBy(a => a.RoleType) + .ToDictionary(a => a.Key, a => a.Select(b => b.CreatorID.ToString()).ToHashSet()) as IReadOnlyDictionary>, HasTmdbLinkDelegate = () => series.Any(a => a.TmdbShowCrossReferences.Count is > 0 || a.TmdbMovieCrossReferences.Count is > 0), HasMissingTmdbLinkDelegate = () => diff --git a/Shoko.Server/Filters/Filterable.cs b/Shoko.Server/Filters/Filterable.cs index e4bb11677..972eadc6b 100644 --- a/Shoko.Server/Filters/Filterable.cs +++ b/Shoko.Server/Filters/Filterable.cs @@ -3,6 +3,7 @@ using Shoko.Models.Enums; using Shoko.Plugin.Abstractions.Enums; using Shoko.Server.Filters.Interfaces; +using Shoko.Server.Server; namespace Shoko.Server.Filters; @@ -49,268 +50,300 @@ public class Filterable : IFilterable private readonly Lazy> _years; private readonly Lazy> _availableImageTypes; private readonly Lazy> _preferredImageTypes; + private readonly Lazy> _characterIDs; + private readonly Lazy>> _characterAppearances; + private readonly Lazy> _creatorIDs; + private readonly Lazy>> _creatorRoles; public string Name => _name.Value; - public Func NameDelegate + public required Func NameDelegate { init => _name = new Lazy(value); } public IReadOnlySet Names => _names.Value; - public Func> NamesDelegate + public required Func> NamesDelegate { init => _names = new Lazy>(value); } public IReadOnlySet AniDBIDs => _aniDbIds.Value; - public Func> AniDBIDsDelegate + public required Func> AniDBIDsDelegate { init => _aniDbIds = new Lazy>(value); } public string SortingName => _sortingName.Value; - public Func SortingNameDelegate + public required Func SortingNameDelegate { init => _sortingName = new Lazy(value); } public int SeriesCount => _seriesCount.Value; - public Func SeriesCountDelegate + public required Func SeriesCountDelegate { init => _seriesCount = new Lazy(value); } public int MissingEpisodes => _missingEpisodes.Value; - public Func MissingEpisodesDelegate + public required Func MissingEpisodesDelegate { init => _missingEpisodes = new Lazy(value); } public int MissingEpisodesCollecting => _missingEpisodesCollecting.Value; - public Func MissingEpisodesCollectingDelegate + public required Func MissingEpisodesCollectingDelegate { init => _missingEpisodesCollecting = new Lazy(value); } public int VideoFiles => _videoFiles.Value; - public Func VideoFilesDelegate + public required Func VideoFilesDelegate { init => _videoFiles = new Lazy(value); } public IReadOnlySet Tags => _tags.Value; - public Func> TagsDelegate + public required Func> TagsDelegate { init => _tags = new Lazy>(value); } public IReadOnlySet CustomTags => _customTags.Value; - public Func> CustomTagsDelegate + public required Func> CustomTagsDelegate { init => _customTags = new Lazy>(value); } public IReadOnlySet Years => _years.Value; - public Func> YearsDelegate + public required Func> YearsDelegate { init => _years = new Lazy>(value); } public IReadOnlySet<(int year, AnimeSeason season)> Seasons => _seasons.Value; - public Func> SeasonsDelegate + public required Func> SeasonsDelegate { init => _seasons = new Lazy>(value); } public IReadOnlySet AvailableImageTypes => _availableImageTypes.Value; - public Func> AvailableImageTypesDelegate + public required Func> AvailableImageTypesDelegate { init => _availableImageTypes = new Lazy>(value); } public IReadOnlySet PreferredImageTypes => _preferredImageTypes.Value; - public Func> PreferredImageTypesDelegate + public required Func> PreferredImageTypesDelegate { init => _preferredImageTypes = new Lazy>(value); } + public IReadOnlySet CharacterIDs => _characterIDs.Value; + + public required Func> CharacterIDsDelegate + { + init => _characterIDs = new Lazy>(value); + } + + public IReadOnlyDictionary> CharacterAppearances => _characterAppearances.Value; + + public required Func>> CharacterAppearancesDelegate + { + init => _characterAppearances = new Lazy>>(value); + } + + public IReadOnlySet CreatorIDs => _creatorIDs.Value; + + public required Func> CreatorIDsDelegate + { + init => _creatorIDs = new Lazy>(value); + } + + public IReadOnlyDictionary> CreatorRoles => _creatorRoles.Value; + + public required Func>> CreatorRolesDelegate + { + init => _creatorRoles = new Lazy>>(value); + } + public bool HasTmdbLink => _hasTmdbLink.Value; - public Func HasTmdbLinkDelegate + public required Func HasTmdbLinkDelegate { init => _hasTmdbLink = new Lazy(value); } public bool HasMissingTmdbLink => _hasMissingTmdbLink.Value; - public Func HasMissingTmdbLinkDelegate + public required Func HasMissingTmdbLinkDelegate { init => _hasMissingTmdbLink = new Lazy(value); } public int AutomaticTmdbEpisodeLinks => _automaticTmdbEpisodeLinks.Value; - public Func AutomaticTmdbEpisodeLinksDelegate + public required Func AutomaticTmdbEpisodeLinksDelegate { init => _automaticTmdbEpisodeLinks = new Lazy(value); } public int UserVerifiedTmdbEpisodeLinks => _userVerifiedTmdbEpisodeLinks.Value; - public Func UserVerifiedTmdbEpisodeLinksDelegate + public required Func UserVerifiedTmdbEpisodeLinksDelegate { init => _userVerifiedTmdbEpisodeLinks = new Lazy(value); } public bool HasTraktLink => _hasTraktLink.Value; - public Func HasTraktLinkDelegate + public required Func HasTraktLinkDelegate { init => _hasTraktLink = new Lazy(value); } public bool HasMissingTraktLink => _hasMissingTraktLink.Value; - public Func HasMissingTraktLinkDelegate + public required Func HasMissingTraktLinkDelegate { init => _hasMissingTraktLink = new Lazy(value); } public bool IsFinished => _isFinished.Value; - public Func IsFinishedDelegate + public required Func IsFinishedDelegate { init => _isFinished = new Lazy(value); } public DateTime? AirDate => _airDate.Value; - public Func AirDateDelegate + public required Func AirDateDelegate { init => _airDate = new Lazy(value); } public DateTime? LastAirDate => _lastAirDate.Value; - public Func LastAirDateDelegate + public required Func LastAirDateDelegate { init => _lastAirDate = new Lazy(value); } public DateTime AddedDate => _addedDate.Value; - public Func AddedDateDelegate + public required Func AddedDateDelegate { init => _addedDate = new Lazy(value); } public DateTime LastAddedDate => _lastAddedDate.Value; - public Func LastAddedDateDelegate + public required Func LastAddedDateDelegate { init => _lastAddedDate = new Lazy(value); } public int EpisodeCount => _episodeCount.Value; - public Func EpisodeCountDelegate + public required Func EpisodeCountDelegate { init => _episodeCount = new Lazy(value); } public int TotalEpisodeCount => _totalEpisodeCount.Value; - public Func TotalEpisodeCountDelegate + public required Func TotalEpisodeCountDelegate { init => _totalEpisodeCount = new Lazy(value); } public decimal LowestAniDBRating => _lowestAniDBRating.Value; - public Func LowestAniDBRatingDelegate + public required Func LowestAniDBRatingDelegate { init => _lowestAniDBRating = new Lazy(value); } public decimal HighestAniDBRating => _highestAniDBRating.Value; - public Func HighestAniDBRatingDelegate + public required Func HighestAniDBRatingDelegate { init => _highestAniDBRating = new Lazy(value); } public decimal AverageAniDBRating => _averageAniDBRating.Value; - public Func AverageAniDBRatingDelegate + public required Func AverageAniDBRatingDelegate { init => _averageAniDBRating = new Lazy(value); } public IReadOnlySet VideoSources => _videoSources.Value; - public Func> VideoSourcesDelegate + public required Func> VideoSourcesDelegate { init => _videoSources = new Lazy>(value); } public IReadOnlySet SharedVideoSources => _sharedVideoSources.Value; - public Func> SharedVideoSourcesDelegate + public required Func> SharedVideoSourcesDelegate { init => _sharedVideoSources = new Lazy>(value); } public IReadOnlySet AnimeTypes => _animeTypes.Value; - public Func> AnimeTypesDelegate + public required Func> AnimeTypesDelegate { init => _animeTypes = new Lazy>(value); } public IReadOnlySet AudioLanguages => _audioLanguages.Value; - public Func> AudioLanguagesDelegate + public required Func> AudioLanguagesDelegate { init => _audioLanguages = new Lazy>(value); } public IReadOnlySet SharedAudioLanguages => _sharedAudioLanguages.Value; - public Func> SharedAudioLanguagesDelegate + public required Func> SharedAudioLanguagesDelegate { init => _sharedAudioLanguages = new Lazy>(value); } public IReadOnlySet SubtitleLanguages => _subtitleLanguages.Value; - public Func> SubtitleLanguagesDelegate + public required Func> SubtitleLanguagesDelegate { init => _subtitleLanguages = new Lazy>(value); } public IReadOnlySet SharedSubtitleLanguages => _sharedSubtitleLanguages.Value; - public Func> SharedSubtitleLanguagesDelegate + public required Func> SharedSubtitleLanguagesDelegate { init => _sharedSubtitleLanguages = new Lazy>(value); } public IReadOnlySet Resolutions => _resolutions.Value; - public Func> ResolutionsDelegate + public required Func> ResolutionsDelegate { init { @@ -320,21 +353,21 @@ public Func> ResolutionsDelegate public IReadOnlySet ImportFolderIDs => _importFolderIDs.Value; - public Func> ImportFolderIDsDelegate + public required Func> ImportFolderIDsDelegate { init => _importFolderIDs = new Lazy>(value); } public IReadOnlySet ImportFolderNames => _importFolderNames.Value; - public Func> ImportFolderNamesDelegate + public required Func> ImportFolderNamesDelegate { init => _importFolderNames = new Lazy>(value); } public IReadOnlySet FilePaths => _filePaths.Value; - public Func> FilePathsDelegate + public required Func> FilePathsDelegate { init => _filePaths = new Lazy>(value); } diff --git a/Shoko.Server/Filters/Info/HasCharacterExpression.cs b/Shoko.Server/Filters/Info/HasCharacterExpression.cs new file mode 100644 index 000000000..b0dcbd166 --- /dev/null +++ b/Shoko.Server/Filters/Info/HasCharacterExpression.cs @@ -0,0 +1,70 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +public class HasCharacterExpression : FilterExpression, IWithStringParameter +{ + public HasCharacterExpression(string characterID) + { + CharacterID = characterID; + } + + public HasCharacterExpression() { } + + public string CharacterID { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override string HelpDescription => "This condition passes if the filterable has a character."; + + string IWithStringParameter.Parameter + { + get => CharacterID; + set => CharacterID = value; + } + + public override bool Evaluate(IFilterable filterable, IFilterableUserInfo userInfo) + { + return filterable.CharacterIDs.Contains(CharacterID); + } + + protected bool Equals(HasCharacterExpression other) + { + return base.Equals(other) && CharacterID == other.CharacterID; + } + + 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((HasCharacterExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), CharacterID); + } + + public static bool operator ==(HasCharacterExpression left, HasCharacterExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasCharacterExpression left, HasCharacterExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/HasCharacterWithAppearanceExpression.cs b/Shoko.Server/Filters/Info/HasCharacterWithAppearanceExpression.cs new file mode 100644 index 000000000..26abb3203 --- /dev/null +++ b/Shoko.Server/Filters/Info/HasCharacterWithAppearanceExpression.cs @@ -0,0 +1,79 @@ +using System; +using Shoko.Server.Filters.Interfaces; +using Shoko.Server.Server; + +namespace Shoko.Server.Filters.Info; + +public class HasCharacterWithAppearanceExpression : FilterExpression, IWithStringParameter, IWithSecondStringParameter +{ + public HasCharacterWithAppearanceExpression(string characterID, CharacterAppearanceType appearance) + { + CharacterID = characterID; + Appearance = appearance; + } + + public HasCharacterWithAppearanceExpression() { } + + public string CharacterID { get; set; } + public CharacterAppearanceType Appearance { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override string HelpDescription => "This condition passes if the filterable has a character with the specified appearance."; + + string IWithStringParameter.Parameter + { + get => CharacterID; + set => CharacterID = value; + } + + string IWithSecondStringParameter.SecondParameter + { + get => Appearance.ToString(); + set => Appearance = Enum.Parse(value); + } + + public override bool Evaluate(IFilterable filterable, IFilterableUserInfo userInfo) + { + return filterable.CharacterAppearances.TryGetValue(Appearance, out var appearances) && appearances.Contains(CharacterID); + } + + protected bool Equals(HasCharacterWithAppearanceExpression other) + { + return base.Equals(other) && CharacterID == other.CharacterID && Appearance == other.Appearance; + } + + 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((HasCharacterWithAppearanceExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), CharacterID, (int)Appearance); + } + + public static bool operator ==(HasCharacterWithAppearanceExpression left, HasCharacterWithAppearanceExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasCharacterWithAppearanceExpression left, HasCharacterWithAppearanceExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/HasCreatorExpression.cs b/Shoko.Server/Filters/Info/HasCreatorExpression.cs new file mode 100644 index 000000000..1e39a196c --- /dev/null +++ b/Shoko.Server/Filters/Info/HasCreatorExpression.cs @@ -0,0 +1,70 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +public class HasCreatorExpression : FilterExpression, IWithStringParameter +{ + public HasCreatorExpression(string creatorID) + { + CreatorID = creatorID; + } + + public HasCreatorExpression() { } + + public string CreatorID { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override string HelpDescription => "This condition passes if the filterable has a creator."; + + string IWithStringParameter.Parameter + { + get => CreatorID; + set => CreatorID = value; + } + + public override bool Evaluate(IFilterable filterable, IFilterableUserInfo userInfo) + { + return filterable.CreatorIDs.Contains(CreatorID); + } + + protected bool Equals(HasCreatorExpression other) + { + return base.Equals(other) && CreatorID == other.CreatorID; + } + + 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((HasCreatorExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), CreatorID); + } + + public static bool operator ==(HasCreatorExpression left, HasCreatorExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasCreatorExpression left, HasCreatorExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/HasCreatorWithRoleExpression.cs b/Shoko.Server/Filters/Info/HasCreatorWithRoleExpression.cs new file mode 100644 index 000000000..8f835a528 --- /dev/null +++ b/Shoko.Server/Filters/Info/HasCreatorWithRoleExpression.cs @@ -0,0 +1,79 @@ +using System; +using Shoko.Server.Filters.Interfaces; +using Shoko.Server.Server; + +namespace Shoko.Server.Filters.Info; + +public class HasCreatorWithRoleExpression : FilterExpression, IWithStringParameter, IWithSecondStringParameter +{ + public HasCreatorWithRoleExpression(string creatorID, CreatorRoleType role) + { + CreatorID = creatorID; + Role = role; + } + + public HasCreatorWithRoleExpression() { } + + public string CreatorID { get; set; } + public CreatorRoleType Role { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override string HelpDescription => "This condition passes if the filterable has a creator with the specified role."; + + string IWithStringParameter.Parameter + { + get => CreatorID; + set => CreatorID = value; + } + + string IWithSecondStringParameter.SecondParameter + { + get => Role.ToString(); + set => Role = Enum.Parse(value); + } + + public override bool Evaluate(IFilterable filterable, IFilterableUserInfo userInfo) + { + return filterable.CreatorRoles.TryGetValue(Role, out var roles) && roles.Contains(CreatorID); + } + + protected bool Equals(HasCreatorWithRoleExpression other) + { + return base.Equals(other) && CreatorID == other.CreatorID && Role == other.Role; + } + + 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((HasCreatorWithRoleExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), CreatorID, (int)Role); + } + + public static bool operator ==(HasCreatorWithRoleExpression left, HasCreatorWithRoleExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasCreatorWithRoleExpression left, HasCreatorWithRoleExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Interfaces/IFilterable.cs b/Shoko.Server/Filters/Interfaces/IFilterable.cs index 03380def5..931ca21db 100644 --- a/Shoko.Server/Filters/Interfaces/IFilterable.cs +++ b/Shoko.Server/Filters/Interfaces/IFilterable.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Shoko.Models.Enums; using Shoko.Plugin.Abstractions.Enums; +using Shoko.Server.Server; namespace Shoko.Server.Filters.Interfaces; @@ -211,4 +212,24 @@ public interface IFilterable /// Relative File Paths /// IReadOnlySet FilePaths { get; } + + /// + /// Character IDs + /// + IReadOnlySet CharacterIDs { get; } + + /// + /// Character Appearance Types + /// + IReadOnlyDictionary> CharacterAppearances { get; } + + /// + /// Creator IDs + /// + IReadOnlySet CreatorIDs { get; } + + /// + /// Creator Roles + /// + IReadOnlyDictionary> CreatorRoles { get; } } diff --git a/Shoko.Server/Filters/Interfaces/IWithStringSetParameter.cs b/Shoko.Server/Filters/Interfaces/IWithStringSetParameter.cs new file mode 100644 index 000000000..a11eb798d --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithStringSetParameter.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithStringSetParameter +{ + IReadOnlySet Parameter { get; set; } +} diff --git a/Shoko.Server/Filters/Logic/StringSets/SetOverlapsExpression.cs b/Shoko.Server/Filters/Logic/StringSets/SetOverlapsExpression.cs new file mode 100644 index 000000000..19e7ee432 --- /dev/null +++ b/Shoko.Server/Filters/Logic/StringSets/SetOverlapsExpression.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.StringSets; + +public class SetOverlapsExpression : FilterExpression, IWithStringSetSelectorParameter, IWithStringSetParameter +{ + public SetOverlapsExpression(FilterExpression> left, IReadOnlySet parameter) + { + Left = left; + Parameter = parameter; + } + + public SetOverlapsExpression() { } + + public FilterExpression> Left { get; set; } + public IReadOnlySet Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent; + public override bool UserDependent => Left.UserDependent; + public override string HelpDescription => "This condition passes if any of the values in the left selector overlaps with the parameter"; + public override FilterExpressionGroup Group => FilterExpressionGroup.Logic; + + public override bool Evaluate(IFilterable filterable, IFilterableUserInfo userInfo) + { + var left = Left.Evaluate(filterable, userInfo); + var right = Parameter; + return left.Overlaps(right); + } + + protected bool Equals(SetOverlapsExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Parameter.SetEquals(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((SetOverlapsExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Parameter); + } + + public static bool operator ==(SetOverlapsExpression left, SetOverlapsExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(SetOverlapsExpression left, SetOverlapsExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is SetOverlapsExpression exp && Left.IsType(exp.Left); + } +} diff --git a/Shoko.Server/Filters/Logic/Strings/StringInExpression.cs b/Shoko.Server/Filters/Logic/Strings/StringInExpression.cs new file mode 100644 index 000000000..5841f41db --- /dev/null +++ b/Shoko.Server/Filters/Logic/Strings/StringInExpression.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.StringSets; + +public class StringInExpression : FilterExpression, IWithStringSelectorParameter, IWithStringSetParameter +{ + public StringInExpression(FilterExpression left, IReadOnlySet parameter) + { + Left = left; + Parameter = parameter; + } + + public StringInExpression() { } + + public FilterExpression Left { get; set; } + public IReadOnlySet Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent; + public override bool UserDependent => Left.UserDependent; + public override string HelpDescription => "This condition passes if any of the value from the left selector is contained within the parameter set"; + public override FilterExpressionGroup Group => FilterExpressionGroup.Logic; + + public override bool Evaluate(IFilterable filterable, IFilterableUserInfo userInfo) + { + var left = Left.Evaluate(filterable, userInfo); + var right = Parameter; + return right.Contains(left); + } + + protected bool Equals(StringInExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Parameter.SetEquals(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((StringInExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Parameter); + } + + public static bool operator ==(StringInExpression left, StringInExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(StringInExpression left, StringInExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is StringInExpression exp && Left.IsType(exp.Left); + } +} diff --git a/Shoko.Server/Filters/Selectors/StringSetSelectors/CharacterIDsSelector.cs b/Shoko.Server/Filters/Selectors/StringSetSelectors/CharacterIDsSelector.cs new file mode 100644 index 000000000..92d34cd86 --- /dev/null +++ b/Shoko.Server/Filters/Selectors/StringSetSelectors/CharacterIDsSelector.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors.StringSetSelectors; + +public class CharacterIDsSelector : FilterExpression> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override string HelpDescription => "This returns a set of all the character IDs in a filterable."; + public override FilterExpressionGroup Group => FilterExpressionGroup.Selector; + + public override IReadOnlySet Evaluate(IFilterable filterable, IFilterableUserInfo userInfo) + { + return filterable.CharacterIDs; + } + + protected bool Equals(CharacterIDsSelector 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((CharacterIDsSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(CharacterIDsSelector left, CharacterIDsSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(CharacterIDsSelector left, CharacterIDsSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/StringSetSelectors/CreatorIDsSelector.cs b/Shoko.Server/Filters/Selectors/StringSetSelectors/CreatorIDsSelector.cs new file mode 100644 index 000000000..e541fec0a --- /dev/null +++ b/Shoko.Server/Filters/Selectors/StringSetSelectors/CreatorIDsSelector.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors.StringSetSelectors; + +public class CreatorIDsSelector : FilterExpression> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override string HelpDescription => "This returns a set of all the creator IDs in a filterable."; + public override FilterExpressionGroup Group => FilterExpressionGroup.Selector; + + public override IReadOnlySet Evaluate(IFilterable filterable, IFilterableUserInfo userInfo) + { + return filterable.CreatorIDs; + } + + protected bool Equals(CreatorIDsSelector 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((CreatorIDsSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(CreatorIDsSelector left, CreatorIDsSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(CreatorIDsSelector left, CreatorIDsSelector right) + { + return !Equals(left, right); + } +}