diff --git a/Shoko.Server/Filters/FilterExtensions.cs b/Shoko.Server/Filters/FilterExtensions.cs index a9f679c23..fe49da093 100644 --- a/Shoko.Server/Filters/FilterExtensions.cs +++ b/Shoko.Server/Filters/FilterExtensions.cs @@ -58,6 +58,10 @@ public static Filterable ToFilterable(this SVR_AnimeSeries series) series.Years, SeasonsDelegate = () => series.AniDB_Anime?.Seasons.ToHashSet() ?? [], + AvailableImageTypesDelegate = () => + series.GetAvailableImageTypes(), + PreferredImageTypesDelegate = () => + series.GetPreferredImageTypes(), HasTmdbLinkDelegate = () => series.TmdbShowCrossReferences.Count is > 0 || series.TmdbMovieCrossReferences.Count is > 0, HasMissingTmdbLinkDelegate = () => @@ -199,9 +203,13 @@ public static Filterable ToFilterable(this SVR_AnimeGroup group) CustomTagsDelegate = () => group.CustomTags.Select(a => a.TagName).ToHashSet(), YearsDelegate = () => - group.Years.ToHashSet(), + group.Years, SeasonsDelegate = () => - group.Seasons.ToHashSet(), + group.Seasons, + AvailableImageTypesDelegate = () => + group.AvailableImageTypes, + PreferredImageTypesDelegate = () => + group.PreferredImageTypes, 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 a9f758fdb..03fe902da 100644 --- a/Shoko.Server/Filters/Filterable.cs +++ b/Shoko.Server/Filters/Filterable.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Shoko.Models.Enums; +using Shoko.Plugin.Abstractions.Enums; using Shoko.Server.Filters.Interfaces; namespace Shoko.Server.Filters; @@ -45,6 +46,8 @@ public class Filterable : IFilterable private readonly Lazy _totalEpisodeCount; private readonly Lazy> _videoSources; private readonly Lazy> _years; + private readonly Lazy> _availableImageTypes; + private readonly Lazy> _preferredImageTypes; public string Name => _name.Value; @@ -123,6 +126,20 @@ public Func> YearsDelegate init => _seasons = new Lazy>(value); } + public IReadOnlySet AvailableImageTypes => _availableImageTypes.Value; + + public Func> AvailableImageTypesDelegate + { + init => _availableImageTypes = new Lazy>(value); + } + + public IReadOnlySet PreferredImageTypes => _preferredImageTypes.Value; + + public Func> PreferredImageTypesDelegate + { + init => _preferredImageTypes = new Lazy>(value); + } + public bool HasTmdbLink => _hasTmdbLink.Value; public Func HasTmdbLinkDelegate diff --git a/Shoko.Server/Filters/Info/HasAvailableImageExpression.cs b/Shoko.Server/Filters/Info/HasAvailableImageExpression.cs new file mode 100644 index 000000000..42b9b178b --- /dev/null +++ b/Shoko.Server/Filters/Info/HasAvailableImageExpression.cs @@ -0,0 +1,81 @@ +using System; +using System.Linq; +using Shoko.Plugin.Abstractions.Enums; +using Shoko.Server.Filters.Interfaces; +using Shoko.Server.Repositories; + +namespace Shoko.Server.Filters.Info; + +public class HasAvailableImageExpression : FilterExpression, IWithStringParameter +{ + public HasAvailableImageExpression(string parameter) + { + if (Enum.TryParse(parameter, out var imageEntityType)) + imageEntityType = ImageEntityType.None; + Parameter = imageEntityType; + } + + public HasAvailableImageExpression() { } + + public ImageEntityType Parameter { get; set; } + public override bool TimeDependent => true; + public override bool UserDependent => false; + public override string HelpDescription => "This condition passes if any of the anime has the available image type."; + public override string[] HelpPossibleParameters => RepoFactory.AnimeSeries.GetAllImageTypes().Select(a => a.ToString()).ToArray(); + + string IWithStringParameter.Parameter + { + get => Parameter.ToString(); + set + { + if (Enum.TryParse(value, out var imageEntityType)) + imageEntityType = ImageEntityType.None; + Parameter = imageEntityType; + } + } + + public override bool Evaluate(IFilterable filterable, IFilterableUserInfo userInfo) + { + return filterable.AvailableImageTypes.Contains(Parameter); + } + + protected bool Equals(HasAvailableImageExpression 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((HasAvailableImageExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasAvailableImageExpression left, HasAvailableImageExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasAvailableImageExpression left, HasAvailableImageExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/HasPreferredImageExpression.cs b/Shoko.Server/Filters/Info/HasPreferredImageExpression.cs new file mode 100644 index 000000000..51e057ddc --- /dev/null +++ b/Shoko.Server/Filters/Info/HasPreferredImageExpression.cs @@ -0,0 +1,81 @@ +using System; +using System.Linq; +using Shoko.Plugin.Abstractions.Enums; +using Shoko.Server.Filters.Interfaces; +using Shoko.Server.Repositories; + +namespace Shoko.Server.Filters.Info; + +public class HasPreferredImageExpression : FilterExpression, IWithStringParameter +{ + public HasPreferredImageExpression(string parameter) + { + if (Enum.TryParse(parameter, out var imageEntityType)) + imageEntityType = ImageEntityType.None; + Parameter = imageEntityType; + } + + public HasPreferredImageExpression() { } + + public ImageEntityType Parameter { get; set; } + public override bool TimeDependent => true; + public override bool UserDependent => false; + public override string HelpDescription => "This condition passes if any of the anime has the preferred image type."; + public override string[] HelpPossibleParameters => RepoFactory.AnimeSeries.GetAllImageTypes().Select(a => a.ToString()).ToArray(); + + string IWithStringParameter.Parameter + { + get => Parameter.ToString(); + set + { + if (Enum.TryParse(value, out var imageEntityType)) + imageEntityType = ImageEntityType.None; + Parameter = imageEntityType; + } + } + + public override bool Evaluate(IFilterable filterable, IFilterableUserInfo userInfo) + { + return filterable.PreferredImageTypes.Contains(Parameter); + } + + protected bool Equals(HasPreferredImageExpression 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((HasPreferredImageExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasPreferredImageExpression left, HasPreferredImageExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasPreferredImageExpression left, HasPreferredImageExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Interfaces/IFilterable.cs b/Shoko.Server/Filters/Interfaces/IFilterable.cs index cdf92a5a4..7201b1802 100644 --- a/Shoko.Server/Filters/Interfaces/IFilterable.cs +++ b/Shoko.Server/Filters/Interfaces/IFilterable.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Shoko.Models.Enums; +using Shoko.Plugin.Abstractions.Enums; namespace Shoko.Server.Filters.Interfaces; @@ -61,6 +62,16 @@ public interface IFilterable /// IReadOnlySet<(int year, AnimeSeason season)> Seasons { get; } + /// + /// Available image types. + /// + IReadOnlySet AvailableImageTypes { get; } + + /// + /// Preferred image types. + /// + IReadOnlySet PreferredImageTypes { get; } + /// /// Has at least one TMDB Link /// diff --git a/Shoko.Server/Filters/Selectors/StringSetSelectors/AvailableImageTypesSelector.cs b/Shoko.Server/Filters/Selectors/StringSetSelectors/AvailableImageTypesSelector.cs new file mode 100644 index 000000000..87657e948 --- /dev/null +++ b/Shoko.Server/Filters/Selectors/StringSetSelectors/AvailableImageTypesSelector.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Linq; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors.StringSetSelectors; + +public class AvailableImageTypesSelector : FilterExpression> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override string HelpDescription => "This returns a set of all the available image types in a filterable."; + public override FilterExpressionGroup Group => FilterExpressionGroup.Selector; + + public override IReadOnlySet Evaluate(IFilterable filterable, IFilterableUserInfo userInfo) + { + return filterable.AvailableImageTypes.Select(t => t.ToString()).ToHashSet(); + } + + protected bool Equals(AvailableImageTypesSelector 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((AvailableImageTypesSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(AvailableImageTypesSelector left, AvailableImageTypesSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(AvailableImageTypesSelector left, AvailableImageTypesSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/StringSetSelectors/PreferredImageTypesSelector.cs b/Shoko.Server/Filters/Selectors/StringSetSelectors/PreferredImageTypesSelector.cs new file mode 100644 index 000000000..22e18aaa1 --- /dev/null +++ b/Shoko.Server/Filters/Selectors/StringSetSelectors/PreferredImageTypesSelector.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Linq; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors.StringSetSelectors; + +public class PreferredImageTypesSelector : FilterExpression> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public override string HelpDescription => "This returns a set of all the preferred image types in a filterable."; + public override FilterExpressionGroup Group => FilterExpressionGroup.Selector; + + public override IReadOnlySet Evaluate(IFilterable filterable, IFilterableUserInfo userInfo) + { + return filterable.PreferredImageTypes.Select(t => t.ToString()).ToHashSet(); + } + + protected bool Equals(PreferredImageTypesSelector 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((PreferredImageTypesSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(PreferredImageTypesSelector left, PreferredImageTypesSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(PreferredImageTypesSelector left, PreferredImageTypesSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Models/SVR_AnimeGroup.cs b/Shoko.Server/Models/SVR_AnimeGroup.cs index 018945d44..6109dd13c 100644 --- a/Shoko.Server/Models/SVR_AnimeGroup.cs +++ b/Shoko.Server/Models/SVR_AnimeGroup.cs @@ -193,6 +193,14 @@ public List AllSeries public HashSet<(int Year, AnimeSeason Season)> Seasons => AllSeries.SelectMany(a => a.AniDB_Anime?.Seasons ?? []).ToHashSet(); + public HashSet AvailableImageTypes => AllSeries + .SelectMany(ser => ser.GetAvailableImageTypes()) + .ToHashSet(); + + public HashSet PreferredImageTypes => AllSeries + .SelectMany(ser => ser.GetPreferredImageTypes()) + .ToHashSet(); + public List Titles => AllSeries .SelectMany(ser => ser.AniDB_Anime?.Titles ?? []) .DistinctBy(tit => tit.AniDB_Anime_TitleID) diff --git a/Shoko.Server/Models/SVR_AnimeSeries.cs b/Shoko.Server/Models/SVR_AnimeSeries.cs index 77af63393..10f754791 100644 --- a/Shoko.Server/Models/SVR_AnimeSeries.cs +++ b/Shoko.Server/Models/SVR_AnimeSeries.cs @@ -329,6 +329,35 @@ IReadOnlyList GetTmdbOverviews() #region Images + public HashSet GetAvailableImageTypes() + { + var images = new List(); + var poster = AniDB_Anime?.GetImageMetadata(false); + if (poster is not null) + images.Add(poster); + foreach (var xref in TmdbShowCrossReferences) + images.AddRange(xref.GetImages()); + foreach (var xref in TmdbSeasonCrossReferences) + images.AddRange(xref.GetImages()); + foreach (var xref in TmdbMovieCrossReferences.DistinctBy(xref => xref.TmdbMovieID)) + images.AddRange(xref.GetImages()); + return images + .DistinctBy(image => image.ImageType) + .Select(image => image.ImageType) + .ToHashSet(); + } + + public HashSet GetPreferredImageTypes() + { + return RepoFactory.AniDB_Anime_PreferredImage.GetByAnimeID(AniDB_ID) + .WhereNotNull() + .Select(preferredImage => preferredImage.GetImageMetadata()) + .WhereNotNull() + .DistinctBy(image => image.ImageType) + .Select(image => image.ImageType) + .ToHashSet(); + } + public IImageMetadata? GetPreferredImageForType(ImageEntityType entityType) => RepoFactory.AniDB_Anime_PreferredImage.GetByAnidbAnimeIDAndType(AniDB_ID, entityType)?.GetImageMetadata(); diff --git a/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs b/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs index 724f78fbb..cebc9ed50 100644 --- a/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs @@ -11,6 +11,7 @@ using Shoko.Commons.Properties; using Shoko.Models.Enums; using Shoko.Models.Server; +using Shoko.Plugin.Abstractions.Enums; using Shoko.Server.Databases; using Shoko.Server.Models; using Shoko.Server.Repositories.NHibernate; @@ -18,6 +19,7 @@ using Shoko.Server.Tasks; using Shoko.Server.Utilities; +#pragma warning disable CA1822 #nullable enable namespace Shoko.Server.Repositories.Cached; @@ -341,6 +343,9 @@ public List GetWithMultipleReleases(bool ignoreVariations) .ToList(); } + public ImageEntityType[] GetAllImageTypes() + => [ImageEntityType.Backdrop, ImageEntityType.Banner, ImageEntityType.Logo, ImageEntityType.Poster]; + public IEnumerable GetAllYears() { var anime = RepoFactory.AnimeSeries.GetAll().Select(a => RepoFactory.AniDB_Anime.GetByAnimeID(a.AniDB_ID)).Where(a => a?.AirDate != null).ToList();