diff --git a/Shoko.Server/API/ModelBinders/CommaDelimitedModelBinder.cs b/Shoko.Server/API/ModelBinders/CommaDelimitedModelBinder.cs index 3f5f2f2d1..5fb5d440f 100644 --- a/Shoko.Server/API/ModelBinders/CommaDelimitedModelBinder.cs +++ b/Shoko.Server/API/ModelBinders/CommaDelimitedModelBinder.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; @@ -11,6 +13,7 @@ namespace Shoko.Server.API.ModelBinders; public class CommaDelimitedModelBinder : IModelBinder { private readonly ILogger _logger; + private static readonly Dictionary AddCache = new(); public CommaDelimitedModelBinder(ILogger logger) { @@ -24,26 +27,46 @@ public Task BindModelAsync(ModelBindingContext bindingContext) var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0]; var converter = TypeDescriptor.GetConverter(elementType); - // HashSet makes things really hard, as it needs compile time types - var result = Activator.CreateInstance(bindingContext.ModelType); - var addMethod = result?.GetType().GetMethod("Add"); - if (addMethod == null) + var items = valueProviderResult + .SelectMany(value => value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)).ToArray(); + object? result; + + if (bindingContext.ModelType.IsArray) { - _logger.LogDebug("Could not get Add method for {Type}", bindingContext.ModelType.FullName); - return Task.CompletedTask; + var array = Array.CreateInstance(elementType, items.Length); + Array.Copy(items.Select(a => converter.ConvertFromString(a)).ToArray(), array, items.Length); + result = array; } - - foreach (var item in valueProviderResult - .SelectMany(value => value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))) + else { - try + // HashSet makes things really hard, as it needs compile time types + result = Activator.CreateInstance(bindingContext.ModelType); + if (!AddCache.TryGetValue(bindingContext.ModelType, out var addMethod)) + { + addMethod = bindingContext.ModelType.GetMethod("Add"); + if (addMethod != null) AddCache[bindingContext.ModelType] = addMethod; + } + + if (addMethod == null) { - var value = converter.ConvertFromString(item); - addMethod.Invoke(result, new[] { value }); + _logger.LogDebug("Could not get Add method for {Type}", bindingContext.ModelType.FullName); + return Task.CompletedTask; } - catch (Exception e) + + foreach (var item in items) { - _logger.LogDebug(e, "Error converting value to {Name}", elementType.FullName); + try + { + var value = converter.ConvertFromString(item); + addMethod.Invoke(result, new[] + { + value + }); + } + catch (Exception e) + { + _logger.LogDebug(e, "Error converting value to {Name}", elementType.FullName); + } } } diff --git a/Shoko.Server/API/v3/Controllers/FileController.cs b/Shoko.Server/API/v3/Controllers/FileController.cs index fab957b56..c9a0e6546 100644 --- a/Shoko.Server/API/v3/Controllers/FileController.cs +++ b/Shoko.Server/API/v3/Controllers/FileController.cs @@ -61,19 +61,8 @@ public FileController(TraktTVHelper traktHelper, ICommandRequestFactory commandF /// /// Limits the number of results per page. Set to 0 to disable the limit. /// Page number. - /// Include missing files among the results. - /// Include ignored files among the results. - /// Include files marked as a variation among the results. - /// Include files with multiple locations (and thus have duplicates) among the results. - /// Include unrecognized files among the results. - /// Include manually linked files among the results. - /// Include previously viewed files among the results. - /// Include previously watched files among the results /// Sort ordering. Attach '-' at the start to reverse the order of the criteria. /// Include data from selected s. - /// Include media info data. - /// Include absolute paths for the file locations. - /// Include series and episode cross-references. /// Filter the search to only files for a given shoko series. /// Filter the search to only files for a given shoko episode. /// Filter the search to only files for a given anidb series. @@ -85,19 +74,11 @@ public FileController(TraktTVHelper traktHelper, ICommandRequestFactory commandF public ActionResult> GetFiles( [FromQuery, Range(0, 1000)] int pageSize = 100, [FromQuery, Range(1, int.MaxValue)] int page = 1, - [FromQuery] IncludeOnlyFilter includeMissing = IncludeOnlyFilter.True, - [FromQuery] IncludeOnlyFilter includeIgnored = IncludeOnlyFilter.False, - [FromQuery] IncludeOnlyFilter includeVariations = IncludeOnlyFilter.True, - [FromQuery] IncludeOnlyFilter includeDuplicates = IncludeOnlyFilter.True, - [FromQuery] IncludeOnlyFilter includeUnrecognized = IncludeOnlyFilter.True, - [FromQuery] IncludeOnlyFilter includeLinked = IncludeOnlyFilter.True, - [FromQuery] IncludeOnlyFilter includeViewed = IncludeOnlyFilter.True, - [FromQuery] IncludeOnlyFilter includeWatched = IncludeOnlyFilter.True, + [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] FileNonDefaultIncludeType[] include = default, + [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] FileExcludeTypes[] exclude = default, + [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] FileIncludeOnlyType[] include_only = default, [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] List sortOrder = null, [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet includeDataFrom = null, - [FromQuery] bool includeMediaInfo = false, - [FromQuery] bool includeAbsolutePaths = false, - [FromQuery] bool includeXRefs = false, [FromQuery] int? seriesID = null, [FromQuery] int? episodeID = null, [FromQuery] int? anidbSeriesID = null, @@ -105,6 +86,10 @@ public ActionResult> GetFiles( [FromQuery] string search = null, [FromQuery] bool fuzzy = true) { + include ??= Array.Empty(); + exclude ??= Array.Empty(); + include_only ??= Array.Empty(); + // Map shoko series id to anidb series id and check if the series // exists. if (seriesID.HasValue) @@ -151,16 +136,14 @@ public ActionResult> GetFiles( } // Filtering. var user = User; - var includeLocations = includeDuplicates != IncludeOnlyFilter.True || - !string.IsNullOrEmpty(search) || + var includeLocations = exclude.Contains(FileExcludeTypes.Duplicates) || !string.IsNullOrEmpty(search) || (sortOrder?.Any(criteria => criteria.Contains(FileSortCriteria.DuplicateCount.ToString())) ?? false); - var includeUserRecord = includeViewed != IncludeOnlyFilter.True || - includeWatched != IncludeOnlyFilter.True || - (sortOrder?.Any(criteria => criteria.Contains(FileSortCriteria.ViewedAt.ToString()) || criteria.Contains(FileSortCriteria.WatchedAt.ToString())) ?? false); + var includeUserRecord = exclude.Contains(FileExcludeTypes.Watched) || (sortOrder?.Any(criteria => + criteria.Contains(FileSortCriteria.ViewedAt.ToString()) || criteria.Contains(FileSortCriteria.WatchedAt.ToString())) ?? false); var enumerable = RepoFactory.VideoLocal.GetAll() .Select(video => ( Video: video, - BestLocation: video.GetBestVideoLocalPlace(includeMissing != IncludeOnlyFilter.True), + BestLocation: video.GetBestVideoLocalPlace(), Locations: includeLocations ? video.Places : null, UserRecord: includeUserRecord ? video.GetUserRecord(user.JMMUserID) : null )) @@ -194,67 +177,20 @@ public ActionResult> GetFiles( return false; } - if (includeMissing != IncludeOnlyFilter.True) - { - var shouldHideMissing = includeMissing == IncludeOnlyFilter.False; - var fileIsMissing = bestLocation == null; - if (shouldHideMissing == fileIsMissing) - return false; - } - - if (includeIgnored != IncludeOnlyFilter.True) - { - var shouldHideIgnored = includeIgnored == IncludeOnlyFilter.False; - if (shouldHideIgnored == video.IsIgnored) - return false; - } - - if (includeVariations != IncludeOnlyFilter.True) - { - var shouldHideVariation = includeVariations == IncludeOnlyFilter.False; - if (shouldHideVariation == video.IsVariation) - return false; - } - - if (includeDuplicates != IncludeOnlyFilter.True) - { - var shouldHideDuplicate = includeDuplicates == IncludeOnlyFilter.False; - var hasDuplicates = locations.Count > 1; - if (shouldHideDuplicate == hasDuplicates) - return false; - } + if (!include.Contains(FileNonDefaultIncludeType.Ignored) && video.IsIgnored) return false; + if (include_only.Contains(FileIncludeOnlyType.Ignored) && !video.IsIgnored) return false; - if (includeUnrecognized != IncludeOnlyFilter.True) - { - var shouldHideUnrecognized = includeUnrecognized == IncludeOnlyFilter.False; - var fileIsUnrecognized = xrefs.Count == 0; - if (shouldHideUnrecognized == fileIsUnrecognized) - return false; - } + if (exclude.Contains(FileExcludeTypes.Duplicates) && locations.Count > 1) return false; + if (include_only.Contains(FileIncludeOnlyType.Duplicates) && locations.Count <= 1) return false; - if (includeLinked != IncludeOnlyFilter.True) - { - var shouldHideLinked = includeLinked == IncludeOnlyFilter.False; - var fileIsLinked = xrefs.Count > 0 && xrefs.Any(xref => xref.CrossRefSource != (int)CrossRefSource.AniDB); - if (shouldHideLinked == fileIsLinked) - return false; - } + if (exclude.Contains(FileExcludeTypes.Unrecognized) && xrefs.Count == 0) return false; + if (include_only.Contains(FileIncludeOnlyType.Unrecognized) && xrefs.Count > 0) return false; - if (includeViewed != IncludeOnlyFilter.True) - { - var shouldHideViewed = includeViewed == IncludeOnlyFilter.False; - var fileIsViewed = userRecord != null; - if (shouldHideViewed == fileIsViewed) - return false; - } + if (exclude.Contains(FileExcludeTypes.ManualLinks) && xrefs.Count > 0 && xrefs.Any(xref => xref.CrossRefSource != (int)CrossRefSource.AniDB)) return false; + if (include_only.Contains(FileIncludeOnlyType.ManualLinks) && xrefs.Count == 0 || xrefs.Any(xref => xref.CrossRefSource == (int)CrossRefSource.AniDB)) return false; - if (includeWatched != IncludeOnlyFilter.True) - { - var shouldHideWatched = includeWatched == IncludeOnlyFilter.False; - var fileIsWatched = userRecord?.WatchedDate != null; - if (shouldHideWatched == fileIsWatched) - return false; - } + if (exclude.Contains(FileExcludeTypes.Watched) && userRecord?.WatchedDate != null) return false; + if (include_only.Contains(FileIncludeOnlyType.Watched) && userRecord?.WatchedDate == null) return false; return true; }); @@ -278,8 +214,9 @@ public ActionResult> GetFiles( }); // Skip and limit. - return enumerable - .ToListResult(tuple => new File(tuple.UserRecord, tuple.Video, includeXRefs, includeDataFrom, includeMediaInfo, includeAbsolutePaths), page, pageSize); + return enumerable.ToListResult( + tuple => new File(tuple.UserRecord, tuple.Video, include.Contains(FileNonDefaultIncludeType.XRefs), includeDataFrom, + include.Contains(FileNonDefaultIncludeType.MediaInfo), include.Contains(FileNonDefaultIncludeType.AbsolutePaths)), page, pageSize); } /// diff --git a/Shoko.Server/API/v3/Models/Common/DataIncludeContext.cs b/Shoko.Server/API/v3/Models/Common/DataIncludeContext.cs new file mode 100644 index 000000000..f6fbecb69 --- /dev/null +++ b/Shoko.Server/API/v3/Models/Common/DataIncludeContext.cs @@ -0,0 +1,45 @@ +namespace Shoko.Server.API.v3.Models.Common; + +public class DataIncludeContext +{ + /// + /// Include ignored files among the results + /// + public IncludeOnlyFilter IncludeIgnored { get; set; } = IncludeOnlyFilter.False; + /// + /// Include files marked as a variation among the results + /// + public IncludeOnlyFilter IncludeVariations { get; set; } = IncludeOnlyFilter.True; + /// + /// Include files with multiple locations (and thus have duplicates) among the results + /// + public IncludeOnlyFilter IncludeDuplicates { get; set; } = IncludeOnlyFilter.True; + /// + /// Include unrecognized files among the results + /// + public IncludeOnlyFilter IncludeUnrecognized { get; set; } = IncludeOnlyFilter.True; + /// + /// Include manually linked files among the results + /// + public IncludeOnlyFilter IncludeManuallyLinked { get; set; } = IncludeOnlyFilter.True; + /// + /// Include previously viewed files among the results + /// + public IncludeOnlyFilter IncludeViewed { get; set; } = IncludeOnlyFilter.True; + /// + /// Include previously watched files among the results + /// + public IncludeOnlyFilter IncludeWatched { get; set; } = IncludeOnlyFilter.True; + /// + /// Include media info data + /// + public IncludeOnlyFilter IncludeMediaInfo { get; set; } = IncludeOnlyFilter.False; + /// + /// Include absolute paths for the file locations + /// + public IncludeOnlyFilter IncludeAbsolutePaths { get; set; } = IncludeOnlyFilter.False; + /// + /// Include series and episode cross-references + /// + public IncludeOnlyFilter IncludeXRefs { get; set; } = IncludeOnlyFilter.False; +} diff --git a/Shoko.Server/API/v3/Models/Common/FileIncludeTypes.cs b/Shoko.Server/API/v3/Models/Common/FileIncludeTypes.cs new file mode 100644 index 000000000..bf263b420 --- /dev/null +++ b/Shoko.Server/API/v3/Models/Common/FileIncludeTypes.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Shoko.Server.API.v3.Models.Common; + +[JsonConverter(typeof(StringEnumConverter))] +public enum FileExcludeTypes +{ + Watched, + Variations, + Duplicates, + Unrecognized, + ManualLinks, +} + +[JsonConverter(typeof(StringEnumConverter))] +public enum FileNonDefaultIncludeType +{ + Ignored, + MediaInfo, + XRefs, + AbsolutePaths +} + +[JsonConverter(typeof(StringEnumConverter))] +public enum FileIncludeOnlyType +{ + Watched, + Variations, + Duplicates, + Unrecognized, + ManualLinks, + Ignored +}