diff --git a/Shoko.Server/API/v3/Controllers/TreeController.cs b/Shoko.Server/API/v3/Controllers/TreeController.cs index 280e50b63..0ea57f6c2 100644 --- a/Shoko.Server/API/v3/Controllers/TreeController.cs +++ b/Shoko.Server/API/v3/Controllers/TreeController.cs @@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Shoko.Models.Enums; +using Shoko.Plugin.Abstractions.DataModels; +using Shoko.Plugin.Abstractions.Extensions; using Shoko.Server.API.Annotations; using Shoko.Server.API.ModelBinders; using Shoko.Server.API.v3.Helpers; @@ -13,6 +15,7 @@ using Shoko.Server.Models; using Shoko.Server.Repositories; using Shoko.Server.Settings; +using Shoko.Server.Utilities; using EpisodeType = Shoko.Server.API.v3.Models.Shoko.EpisodeType; using AniDBEpisodeType = Shoko.Models.Enums.EpisodeType; @@ -538,6 +541,7 @@ public ActionResult GetMainSeriesInGroup([FromRoute] int groupID, [FromQ /// Include data from selected s. /// Include watched episodes in the list. /// Filter episodes by the specified s. + /// An optional search query to filter episodes based on their titles. /// A list of episodes based on the specified filters. [HttpGet("Series/{seriesID}/Episode")] public ActionResult> GetEpisodes([FromRoute] int seriesID, @@ -545,7 +549,8 @@ [FromQuery] [Range(0, 1000)] int pageSize = 20, [FromQuery] [Range(1, int.MaxVal [FromQuery] IncludeOnlyFilter includeMissing = IncludeOnlyFilter.False, [FromQuery] IncludeOnlyFilter includeHidden = IncludeOnlyFilter.False, [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet includeDataFrom = null, [FromQuery] IncludeOnlyFilter includeWatched = IncludeOnlyFilter.True, - [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet type = null) + [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet type = null, + [FromQuery] string search = null) { var series = RepoFactory.AnimeSeries.GetByID(seriesID); if (series == null) @@ -558,7 +563,25 @@ [FromQuery] [Range(0, 1000)] int pageSize = 20, [FromQuery] [Range(1, int.MaxVal return Forbid(SeriesController.SeriesForbiddenForUser); } - return series.GetAnimeEpisodes(orderList: true, includeHidden: includeHidden != IncludeOnlyFilter.False) + IEnumerable episodes = series.GetAnimeEpisodes(orderList: true, includeHidden: includeHidden != IncludeOnlyFilter.False); + if (!string.IsNullOrEmpty(search)) + { + var languages = SettingsProvider.GetSettings() + .LanguagePreference + .Select(lang => lang.GetTitleLanguage()) + .Concat(new TitleLanguage[] { TitleLanguage.English, TitleLanguage.Romaji }) + .ToHashSet(); + episodes = episodes.FuzzySearch( + "", + ep => RepoFactory.AniDB_Episode_Title.GetByEpisodeID(ep.AniDB_EpisodeID) + .Where(title => title != null && languages.Contains(title.Language)) + .Select(title => title.Title) + .ToList() + ) + .Select(a => a.Result); + } + + return episodes .Where(a => { // Filter by hidden state, if spesified diff --git a/Shoko.Server/Utilities/SeriesSearch.cs b/Shoko.Server/Utilities/SeriesSearch.cs index 2e9e3d658..4d28a9464 100644 --- a/Shoko.Server/Utilities/SeriesSearch.cs +++ b/Shoko.Server/Utilities/SeriesSearch.cs @@ -141,54 +141,75 @@ private static SearchResult> CheckTitlesIndexOf(IGrouping< return dist; } - public static List> SearchCollection(string query, IEnumerable list, + /// + /// Searches a collection of items based on a search query using fuzzy search. + /// + /// The type of the items in the collection. + /// The search query used to filter the collection. + /// The collection of items to be searched. + /// A function that takes an item of type T and returns a list of strings that represent searchable properties of the item. + /// A list of search results containing the matched items and their search-related information, such as index, distance, and exact match status. + public static IOrderedEnumerable> SearchCollection(string query, IEnumerable list, Func> selector) { - var parallelList = list.ToList().AsParallel(); - var results = parallelList.Select(a => - { - var titles = selector(a); - SearchResult dist = null; - foreach (var title in titles) + return list.ToList().AsParallel() + .Select(a => { - if (string.IsNullOrEmpty(title)) + var titles = selector(a); + SearchResult dist = null; + foreach (var title in titles) { - continue; - } + if (string.IsNullOrEmpty(title)) + { + continue; + } - var k = Math.Max(Math.Min((int)(title.Length / 6D), (int)(query.Length / 6D)), 1); - if (query.Length <= 4 || title.Length <= 4) - { - k = 0; - } + var k = Math.Max(Math.Min((int)(title.Length / 6D), (int)(query.Length / 6D)), 1); + if (query.Length <= 4 || title.Length <= 4) + { + k = 0; + } - var result = - Misc.DiceFuzzySearch(title, query, k, a); - if (result.Index == -1) - { - continue; - } + var result = + Misc.DiceFuzzySearch(title, query, k, a); + if (result.Index == -1) + { + continue; + } - var searchGrouping = new SearchResult - { - Distance = result.Distance, - Index = result.Index, - ExactMatch = result.ExactMatch, - Match = title, - Result = a - }; - if (result.Distance < (dist?.Distance ?? int.MaxValue)) - { - dist = searchGrouping; + var searchGrouping = new SearchResult + { + Distance = result.Distance, + Index = result.Index, + ExactMatch = result.ExactMatch, + Match = title, + Result = a + }; + if (result.Distance < (dist?.Distance ?? int.MaxValue)) + { + dist = searchGrouping; + } } - } - - return dist; - }).Where(a => a != null && a.Index != -1).ToList().OrderBy(a => a.Index).ThenBy(a => a.Distance).ToList(); - return results; + return dist; + }) + .Where(a => a != null && a.Index != -1) + .ToList() + .OrderBy(a => a.Index) + .ThenBy(a => a.Distance); } + /// + /// Performs a fuzzy search on an enumerable collection. + /// + /// The type of the items in the enumerable collection. + /// The enumerable collection to be searched. + /// The search query used to filter the collection. + /// A function that takes an item of type T and returns a list of strings that represent searchable properties of the item. + /// An ordered enumerable of search results containing the matched items and their search-related information + public static IOrderedEnumerable> FuzzySearch(this IEnumerable enumerable, string query, Func> selector) + => SearchCollection(query, enumerable, selector); + /// /// Search for series with given query in name or tag ///