diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs
index 1f4508e6c..6a9e48f8d 100644
--- a/Jellyfin.Api/BaseJellyfinApiController.cs
+++ b/Jellyfin.Api/BaseJellyfinApiController.cs
@@ -1,3 +1,4 @@
+using System;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api
@@ -9,5 +10,23 @@ namespace Jellyfin.Api
[Route("[controller]")]
public class BaseJellyfinApiController : ControllerBase
{
+ ///
+ /// Splits a string at a seperating character into an array of substrings.
+ ///
+ /// The string to split.
+ /// The char that seperates the substrings.
+ /// Option to remove empty substrings from the array.
+ /// An array of the substrings.
+ internal static string[] Split(string value, char separator, bool removeEmpty)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return Array.Empty();
+ }
+
+ return removeEmpty
+ ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
+ : value.Split(separator);
+ }
}
}
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
new file mode 100644
index 000000000..15a650bf9
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -0,0 +1,266 @@
+using System;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Linq;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Search;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ ///
+ /// Search controller.
+ ///
+ [Route("/Search/Hints")]
+ [Authenticated]
+ public class SearchController : BaseJellyfinApiController
+ {
+ private readonly ISearchEngine _searchEngine;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly IImageProcessor _imageProcessor;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
+ public SearchController(
+ ISearchEngine searchEngine,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ IImageProcessor imageProcessor)
+ {
+ _searchEngine = searchEngine;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _imageProcessor = imageProcessor;
+ }
+
+ ///
+ /// Gets the search hint result.
+ ///
+ /// The record index to start at. All items with a lower index will be dropped from the results.
+ /// The maximum number of records to return.
+ /// Supply a user id to search within a user's library or omit to search all.
+ /// The search term to filter on.
+ /// Optional filter whether to include people.
+ /// Optional filter whether to include media.
+ /// Optional filter whether to include genres.
+ /// Optional filter whether to include studios.
+ /// Optional filter whether to include artists.
+ /// If specified, only results with the specified item types are returned. This allows multiple, comma delimeted.
+ /// If specified, results with these item types are filtered out. This allows multiple, comma delimeted.
+ /// If specified, only results with the specified media types are returned. This allows multiple, comma delimeted.
+ /// If specified, only children of the parent are returned.
+ /// Optional filter for movies.
+ /// Optional filter for series.
+ /// Optional filter for news.
+ /// Optional filter for kids.
+ /// Optional filter for sports.
+ /// An with the results of the search.
+ [HttpGet]
+ [Description("Gets search hints based on a search term")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult Get(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] Guid userId,
+ [FromQuery, Required] string searchTerm,
+ [FromQuery] bool includePeople,
+ [FromQuery] bool includeMedia,
+ [FromQuery] bool includeGenres,
+ [FromQuery] bool includeStudios,
+ [FromQuery] bool includeArtists,
+ [FromQuery] string includeItemTypes,
+ [FromQuery] string excludeItemTypes,
+ [FromQuery] string mediaTypes,
+ [FromQuery] string parentId,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports)
+ {
+ var result = _searchEngine.GetSearchHints(new SearchQuery
+ {
+ Limit = limit,
+ SearchTerm = searchTerm,
+ IncludeArtists = includeArtists,
+ IncludeGenres = includeGenres,
+ IncludeMedia = includeMedia,
+ IncludePeople = includePeople,
+ IncludeStudios = includeStudios,
+ StartIndex = startIndex,
+ UserId = userId,
+ IncludeItemTypes = Split(includeItemTypes, ',', true),
+ ExcludeItemTypes = Split(excludeItemTypes, ',', true),
+ MediaTypes = Split(mediaTypes, ',', true),
+ ParentId = parentId,
+
+ IsKids = isKids,
+ IsMovie = isMovie,
+ IsNews = isNews,
+ IsSeries = isSeries,
+ IsSports = isSports
+ });
+
+ return Ok(new SearchHintResult
+ {
+ TotalRecordCount = result.TotalRecordCount,
+ SearchHints = result.Items.Select(GetSearchHintResult).ToArray()
+ });
+ }
+
+ ///
+ /// Gets the search hint result.
+ ///
+ /// The hint info.
+ /// SearchHintResult.
+ private SearchHint GetSearchHintResult(SearchHintInfo hintInfo)
+ {
+ var item = hintInfo.Item;
+
+ var result = new SearchHint
+ {
+ Name = item.Name,
+ IndexNumber = item.IndexNumber,
+ ParentIndexNumber = item.ParentIndexNumber,
+ Id = item.Id,
+ Type = item.GetClientTypeName(),
+ MediaType = item.MediaType,
+ MatchedTerm = hintInfo.MatchedTerm,
+ RunTimeTicks = item.RunTimeTicks,
+ ProductionYear = item.ProductionYear,
+ ChannelId = item.ChannelId,
+ EndDate = item.EndDate
+ };
+
+ // legacy
+ result.ItemId = result.Id;
+
+ if (item.IsFolder)
+ {
+ result.IsFolder = true;
+ }
+
+ var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary);
+
+ if (primaryImageTag != null)
+ {
+ result.PrimaryImageTag = primaryImageTag;
+ result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item);
+ }
+
+ SetThumbImageInfo(result, item);
+ SetBackdropImageInfo(result, item);
+
+ switch (item)
+ {
+ case IHasSeries hasSeries:
+ result.Series = hasSeries.SeriesName;
+ break;
+ case LiveTvProgram program:
+ result.StartDate = program.StartDate;
+ break;
+ case Series series:
+ if (series.Status.HasValue)
+ {
+ result.Status = series.Status.Value.ToString();
+ }
+
+ break;
+ case MusicAlbum album:
+ result.Artists = album.Artists;
+ result.AlbumArtist = album.AlbumArtist;
+ break;
+ case Audio song:
+ result.AlbumArtist = song.AlbumArtists?[0];
+ result.Artists = song.Artists;
+
+ MusicAlbum musicAlbum = song.AlbumEntity;
+
+ if (musicAlbum != null)
+ {
+ result.Album = musicAlbum.Name;
+ result.AlbumId = musicAlbum.Id;
+ }
+ else
+ {
+ result.Album = song.Album;
+ }
+
+ break;
+ }
+
+ if (!item.ChannelId.Equals(Guid.Empty))
+ {
+ var channel = _libraryManager.GetItemById(item.ChannelId);
+ result.ChannelName = channel?.Name;
+ }
+
+ return result;
+ }
+
+ private void SetThumbImageInfo(SearchHint hint, BaseItem item)
+ {
+ var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null;
+
+ if (itemWithImage == null && item is Episode)
+ {
+ itemWithImage = GetParentWithImage(item, ImageType.Thumb);
+ }
+
+ if (itemWithImage == null)
+ {
+ itemWithImage = GetParentWithImage(item, ImageType.Thumb);
+ }
+
+ if (itemWithImage != null)
+ {
+ var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb);
+
+ if (tag != null)
+ {
+ hint.ThumbImageTag = tag;
+ hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
+ }
+ }
+ }
+
+ private void SetBackdropImageInfo(SearchHint hint, BaseItem item)
+ {
+ var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null)
+ ?? GetParentWithImage(item, ImageType.Backdrop);
+
+ if (itemWithImage != null)
+ {
+ var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop);
+
+ if (tag != null)
+ {
+ hint.BackdropImageTag = tag;
+ hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
+ }
+ }
+ }
+
+ private T GetParentWithImage(BaseItem item, ImageType type)
+ where T : BaseItem
+ {
+ return item.GetParents().OfType().FirstOrDefault(i => i.HasImage(type));
+ }
+ }
+}
diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs
deleted file mode 100644
index e9d339c6e..000000000
--- a/MediaBrowser.Api/SearchService.cs
+++ /dev/null
@@ -1,333 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Search;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- ///
- /// Class GetSearchHints
- ///
- [Route("/Search/Hints", "GET", Summary = "Gets search hints based on a search term")]
- public class GetSearchHints : IReturn
- {
- ///
- /// Skips over a given number of items within the results. Use for paging.
- ///
- /// The start index.
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- ///
- /// The maximum number of items to return
- ///
- /// The limit.
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- ///
- /// Gets or sets the user id.
- ///
- /// The user id.
- [ApiMember(Name = "UserId", Description = "Optional. Supply a user id to search within a user's library or omit to search all.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- ///
- /// Search characters used to find items
- ///
- /// The index by.
- [ApiMember(Name = "SearchTerm", Description = "The search term to filter on", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string SearchTerm { get; set; }
-
-
- [ApiMember(Name = "IncludePeople", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool IncludePeople { get; set; }
-
- [ApiMember(Name = "IncludeMedia", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool IncludeMedia { get; set; }
-
- [ApiMember(Name = "IncludeGenres", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool IncludeGenres { get; set; }
-
- [ApiMember(Name = "IncludeStudios", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool IncludeStudios { get; set; }
-
- [ApiMember(Name = "IncludeArtists", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool IncludeArtists { get; set; }
-
- [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string IncludeItemTypes { get; set; }
-
- [ApiMember(Name = "ExcludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string ExcludeItemTypes { get; set; }
-
- [ApiMember(Name = "MediaTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string MediaTypes { get; set; }
-
- public string ParentId { get; set; }
-
- [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsMovie { get; set; }
-
- [ApiMember(Name = "IsSeries", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsSeries { get; set; }
-
- [ApiMember(Name = "IsNews", Description = "Optional filter for news.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsNews { get; set; }
-
- [ApiMember(Name = "IsKids", Description = "Optional filter for kids.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsKids { get; set; }
-
- [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsSports { get; set; }
-
- public GetSearchHints()
- {
- IncludeArtists = true;
- IncludeGenres = true;
- IncludeMedia = true;
- IncludePeople = true;
- IncludeStudios = true;
- }
- }
-
- ///
- /// Class SearchService
- ///
- [Authenticated]
- public class SearchService : BaseApiService
- {
- ///
- /// The _search engine
- ///
- private readonly ISearchEngine _searchEngine;
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly IImageProcessor _imageProcessor;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The search engine.
- /// The library manager.
- /// The dto service.
- /// The image processor.
- public SearchService(
- ILogger logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ISearchEngine searchEngine,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- IImageProcessor imageProcessor)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _searchEngine = searchEngine;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _imageProcessor = imageProcessor;
- }
-
- ///
- /// Gets the specified request.
- ///
- /// The request.
- /// System.Object.
- public object Get(GetSearchHints request)
- {
- var result = GetSearchHintsAsync(request);
-
- return ToOptimizedResult(result);
- }
-
- ///
- /// Gets the search hints async.
- ///
- /// The request.
- /// Task{IEnumerable{SearchHintResult}}.
- private SearchHintResult GetSearchHintsAsync(GetSearchHints request)
- {
- var result = _searchEngine.GetSearchHints(new SearchQuery
- {
- Limit = request.Limit,
- SearchTerm = request.SearchTerm,
- IncludeArtists = request.IncludeArtists,
- IncludeGenres = request.IncludeGenres,
- IncludeMedia = request.IncludeMedia,
- IncludePeople = request.IncludePeople,
- IncludeStudios = request.IncludeStudios,
- StartIndex = request.StartIndex,
- UserId = request.UserId,
- IncludeItemTypes = ApiEntryPoint.Split(request.IncludeItemTypes, ',', true),
- ExcludeItemTypes = ApiEntryPoint.Split(request.ExcludeItemTypes, ',', true),
- MediaTypes = ApiEntryPoint.Split(request.MediaTypes, ',', true),
- ParentId = request.ParentId,
-
- IsKids = request.IsKids,
- IsMovie = request.IsMovie,
- IsNews = request.IsNews,
- IsSeries = request.IsSeries,
- IsSports = request.IsSports
-
- });
-
- return new SearchHintResult
- {
- TotalRecordCount = result.TotalRecordCount,
-
- SearchHints = result.Items.Select(GetSearchHintResult).ToArray()
- };
- }
-
- ///
- /// Gets the search hint result.
- ///
- /// The hint info.
- /// SearchHintResult.
- private SearchHint GetSearchHintResult(SearchHintInfo hintInfo)
- {
- var item = hintInfo.Item;
-
- var result = new SearchHint
- {
- Name = item.Name,
- IndexNumber = item.IndexNumber,
- ParentIndexNumber = item.ParentIndexNumber,
- Id = item.Id,
- Type = item.GetClientTypeName(),
- MediaType = item.MediaType,
- MatchedTerm = hintInfo.MatchedTerm,
- RunTimeTicks = item.RunTimeTicks,
- ProductionYear = item.ProductionYear,
- ChannelId = item.ChannelId,
- EndDate = item.EndDate
- };
-
- // legacy
- result.ItemId = result.Id;
-
- if (item.IsFolder)
- {
- result.IsFolder = true;
- }
-
- var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary);
-
- if (primaryImageTag != null)
- {
- result.PrimaryImageTag = primaryImageTag;
- result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item);
- }
-
- SetThumbImageInfo(result, item);
- SetBackdropImageInfo(result, item);
-
- switch (item)
- {
- case IHasSeries hasSeries:
- result.Series = hasSeries.SeriesName;
- break;
- case LiveTvProgram program:
- result.StartDate = program.StartDate;
- break;
- case Series series:
- if (series.Status.HasValue)
- {
- result.Status = series.Status.Value.ToString();
- }
-
- break;
- case MusicAlbum album:
- result.Artists = album.Artists;
- result.AlbumArtist = album.AlbumArtist;
- break;
- case Audio song:
- result.AlbumArtist = song.AlbumArtists.FirstOrDefault();
- result.Artists = song.Artists;
-
- MusicAlbum musicAlbum = song.AlbumEntity;
-
- if (musicAlbum != null)
- {
- result.Album = musicAlbum.Name;
- result.AlbumId = musicAlbum.Id;
- }
- else
- {
- result.Album = song.Album;
- }
-
- break;
- }
-
- if (!item.ChannelId.Equals(Guid.Empty))
- {
- var channel = _libraryManager.GetItemById(item.ChannelId);
- result.ChannelName = channel?.Name;
- }
-
- return result;
- }
-
- private void SetThumbImageInfo(SearchHint hint, BaseItem item)
- {
- var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null;
-
- if (itemWithImage == null && item is Episode)
- {
- itemWithImage = GetParentWithImage(item, ImageType.Thumb);
- }
-
- if (itemWithImage == null)
- {
- itemWithImage = GetParentWithImage(item, ImageType.Thumb);
- }
-
- if (itemWithImage != null)
- {
- var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb);
-
- if (tag != null)
- {
- hint.ThumbImageTag = tag;
- hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
- }
- }
- }
-
- private void SetBackdropImageInfo(SearchHint hint, BaseItem item)
- {
- var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null)
- ?? GetParentWithImage(item, ImageType.Backdrop);
-
- if (itemWithImage != null)
- {
- var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop);
-
- if (tag != null)
- {
- hint.BackdropImageTag = tag;
- hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
- }
- }
- }
-
- private T GetParentWithImage(BaseItem item, ImageType type)
- where T : BaseItem
- {
- return item.GetParents().OfType().FirstOrDefault(i => i.HasImage(type));
- }
- }
-}