diff options
| author | Eric Reed <ebr@mediabrowser3.com> | 2013-04-26 18:04:26 -0400 |
|---|---|---|
| committer | Eric Reed <ebr@mediabrowser3.com> | 2013-04-26 18:04:26 -0400 |
| commit | 6e7684ae0167efed8d3f64a9a49e6d1cc27abee8 (patch) | |
| tree | e5e40e8cbfb0ae721ce9ccd011c4380b64aedd78 | |
| parent | e25de8ec03525de1bc58bd0b48620db0ab645aed (diff) | |
| parent | 3eaf25132c65bf287f2206e85a8a1eaa5670ac15 (diff) | |
Merge branch 'master' of https://github.com/MediaBrowser/MediaBrowser
| -rw-r--r-- | MediaBrowser.Api/MediaBrowser.Api.csproj | 1 | ||||
| -rw-r--r-- | MediaBrowser.Api/SearchService.cs | 192 | ||||
| -rw-r--r-- | MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs | 1 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Library/ILibrarySearchEngine.cs | 9 | ||||
| -rw-r--r-- | MediaBrowser.Model/MediaBrowser.Model.csproj | 1 | ||||
| -rw-r--r-- | MediaBrowser.Model/Search/SearchHintResult.cs | 76 | ||||
| -rw-r--r-- | MediaBrowser.Server.Implementations/Library/LuceneSearchEngine.cs | 163 | ||||
| -rw-r--r-- | MediaBrowser.ServerApplication/ApplicationHost.cs | 2 | ||||
| -rw-r--r-- | MediaBrowser.WebDashboard/Api/DashboardService.cs | 2 | ||||
| -rw-r--r-- | MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj | 9 |
10 files changed, 446 insertions, 10 deletions
diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 32c1bfaf5..5f846a417 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -86,6 +86,7 @@ <Compile Include="ScheduledTasks\ScheduledTaskService.cs" /> <Compile Include="ScheduledTasks\ScheduledTasksWebSocketListener.cs" /> <Compile Include="ApiEntryPoint.cs" /> + <Compile Include="SearchService.cs" /> <Compile Include="SystemService.cs" /> <Compile Include="UserLibrary\ArtistsService.cs" /> <Compile Include="UserLibrary\BaseItemsByNameService.cs" /> diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs new file mode 100644 index 000000000..a3d228529 --- /dev/null +++ b/MediaBrowser.Api/SearchService.cs @@ -0,0 +1,192 @@ +using System.Collections; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Search; +using ServiceStack.ServiceHost; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; + +namespace MediaBrowser.Api +{ + /// <summary> + /// Class GetSearchHints + /// </summary> + [Route("/Search/Hints", "GET")] + [Api(Description = "Gets search hints based on a search term")] + public class GetSearchHints : IReturn<List<SearchHintResult>> + { + /// <summary> + /// Skips over a given number of items within the results. Use for paging. + /// </summary> + /// <value>The start index.</value> + [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; } + + /// <summary> + /// The maximum number of items to return + /// </summary> + /// <value>The limit.</value> + [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; } + + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [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; } + + /// <summary> + /// Search characters used to find items + /// </summary> + /// <value>The index by.</value> + [ApiMember(Name = "SearchTerm", Description = "The search term to filter on", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] + public string SearchTerm { get; set; } + } + + /// <summary> + /// Class SearchService + /// </summary> + public class SearchService : BaseApiService + { + /// <summary> + /// The _user manager + /// </summary> + private readonly IUserManager _userManager; + /// <summary> + /// The _search engine + /// </summary> + private readonly ILibrarySearchEngine _searchEngine; + private readonly ILibraryManager _libraryManager; + + /// <summary> + /// Initializes a new instance of the <see cref="SearchService" /> class. + /// </summary> + /// <param name="userManager">The user manager.</param> + /// <param name="searchEngine">The search engine.</param> + /// <param name="libraryManager">The library manager.</param> + public SearchService(IUserManager userManager, ILibrarySearchEngine searchEngine, ILibraryManager libraryManager) + { + _userManager = userManager; + _searchEngine = searchEngine; + _libraryManager = libraryManager; + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetSearchHints request) + { + var result = GetSearchHintsAsync(request).Result; + + return ToOptimizedResult(result); + } + + /// <summary> + /// Gets the search hints async. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>Task{IEnumerable{SearchHintResult}}.</returns> + private async Task<IEnumerable<SearchHintResult>> GetSearchHintsAsync(GetSearchHints request) + { + IEnumerable<BaseItem> inputItems; + + if (request.UserId.HasValue) + { + var user = _userManager.GetUserById(request.UserId.Value); + + inputItems = user.RootFolder.GetRecursiveChildren(user); + } + else + { + inputItems = _libraryManager.RootFolder.RecursiveChildren; + } + + var results = await _searchEngine.GetSearchHints(inputItems, request.SearchTerm).ConfigureAwait(false); + + if (request.StartIndex.HasValue) + { + results = results.Skip(request.StartIndex.Value); + } + + if (request.Limit.HasValue) + { + results = results.Take(request.Limit.Value); + } + + return results.Select(GetSearchHintResult); + } + + /// <summary> + /// Gets the search hint result. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>SearchHintResult.</returns> + private SearchHintResult GetSearchHintResult(BaseItem item) + { + var result = new SearchHintResult + { + Name = item.Name, + IndexNumber = item.IndexNumber, + ParentIndexNumber = item.ParentIndexNumber, + ItemId = DtoBuilder.GetClientItemId(item), + Type = item.GetType().Name, + MediaType = item.MediaType + }; + + if (item.HasImage(ImageType.Primary)) + { + result.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Primary, item.GetImage(ImageType.Primary)); + } + + var episode = item as Episode; + + if (episode != null) + { + result.Series = episode.Series.Name; + } + + var season = item as Season; + + if (season != null) + { + result.Series = season.Series.Name; + } + + var album = item as MusicAlbum; + + if (album != null) + { + var songs = album.Children.OfType<Audio>().ToList(); + + result.Artists = songs + .Select(i => i.Artist) + .Where(i => !string.IsNullOrEmpty(i)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + result.AlbumArtist = songs.Select(i => i.AlbumArtist).FirstOrDefault(i => !string.IsNullOrEmpty(i)); + } + + var song = item as Audio; + + if (song != null) + { + result.Album = song.Album; + result.AlbumArtist = song.AlbumArtist; + result.Artists = !string.IsNullOrEmpty(song.Artist) ? new[] { song.Artist } : new string[] { }; + } + + return result; + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs index 27f124e47..a69fedf25 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs @@ -79,7 +79,6 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "MediaTypes", Description = "Optional filter by MediaType. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] public string MediaTypes { get; set; } - /// <summary> /// Gets the filters. /// </summary> diff --git a/MediaBrowser.Controller/Library/ILibrarySearchEngine.cs b/MediaBrowser.Controller/Library/ILibrarySearchEngine.cs index 0aff451d9..315e75208 100644 --- a/MediaBrowser.Controller/Library/ILibrarySearchEngine.cs +++ b/MediaBrowser.Controller/Library/ILibrarySearchEngine.cs @@ -1,5 +1,6 @@ using MediaBrowser.Controller.Entities; using System.Collections.Generic; +using System.Threading.Tasks; namespace MediaBrowser.Controller.Library { @@ -16,5 +17,13 @@ namespace MediaBrowser.Controller.Library /// <returns>IEnumerable{BaseItem}.</returns> /// <exception cref="System.ArgumentNullException">searchTerm</exception> IEnumerable<BaseItem> Search(IEnumerable<BaseItem> items, string searchTerm); + + /// <summary> + /// Gets the search hints. + /// </summary> + /// <param name="inputItems">The input items.</param> + /// <param name="searchTerm">The search term.</param> + /// <returns>Task{IEnumerable{BaseItem}}.</returns> + Task<IEnumerable<BaseItem>> GetSearchHints(IEnumerable<BaseItem> inputItems, string searchTerm); } } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 99e34aaf6..cd54ec7c8 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -86,6 +86,7 @@ <Compile Include="Net\NetworkShareType.cs" /> <Compile Include="Querying\PersonsQuery.cs" /> <Compile Include="Querying\ThemeSongsResult.cs" /> + <Compile Include="Search\SearchHintResult.cs" /> <Compile Include="Serialization\IJsonSerializer.cs" /> <Compile Include="Serialization\IXmlSerializer.cs" /> <Compile Include="Updates\CheckForUpdateResult.cs" /> diff --git a/MediaBrowser.Model/Search/SearchHintResult.cs b/MediaBrowser.Model/Search/SearchHintResult.cs new file mode 100644 index 000000000..2142ac3f7 --- /dev/null +++ b/MediaBrowser.Model/Search/SearchHintResult.cs @@ -0,0 +1,76 @@ +using System; + +namespace MediaBrowser.Model.Search +{ + /// <summary> + /// Class SearchHintResult + /// </summary> + public class SearchHintResult + { + /// <summary> + /// Gets or sets the item id. + /// </summary> + /// <value>The item id.</value> + public string ItemId { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the index number. + /// </summary> + /// <value>The index number.</value> + public int? IndexNumber { get; set; } + + /// <summary> + /// Gets or sets the parent index number. + /// </summary> + /// <value>The parent index number.</value> + public int? ParentIndexNumber { get; set; } + + /// <summary> + /// Gets or sets the image tag. + /// </summary> + /// <value>The image tag.</value> + public Guid? PrimaryImageTag { get; set; } + + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public string Type { get; set; } + + /// <summary> + /// Gets or sets the type of the media. + /// </summary> + /// <value>The type of the media.</value> + public string MediaType { get; set; } + + /// <summary> + /// Gets or sets the series. + /// </summary> + /// <value>The series.</value> + public string Series { get; set; } + + /// <summary> + /// Gets or sets the album. + /// </summary> + /// <value>The album.</value> + public string Album { get; set; } + + /// <summary> + /// Gets or sets the album artist. + /// </summary> + /// <value>The album artist.</value> + public string AlbumArtist { get; set; } + + /// <summary> + /// Gets or sets the artists. + /// </summary> + /// <value>The artists.</value> + public string[] Artists { get; set; } + } +} diff --git a/MediaBrowser.Server.Implementations/Library/LuceneSearchEngine.cs b/MediaBrowser.Server.Implementations/Library/LuceneSearchEngine.cs index ed085c777..d5675578d 100644 --- a/MediaBrowser.Server.Implementations/Library/LuceneSearchEngine.cs +++ b/MediaBrowser.Server.Implementations/Library/LuceneSearchEngine.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Lucene.Net.Analysis.Standard; +using Lucene.Net.Analysis.Standard; using Lucene.Net.Documents; using Lucene.Net.Index; using Lucene.Net.QueryParsers; @@ -11,8 +6,13 @@ using Lucene.Net.Search; using Lucene.Net.Store; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Library { @@ -22,15 +22,22 @@ namespace MediaBrowser.Server.Implementations.Library /// </summary> public class LuceneSearchEngine : ILibrarySearchEngine, IDisposable { - public LuceneSearchEngine(IServerApplicationPaths serverPaths, ILogManager logManager) + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + + public LuceneSearchEngine(IServerApplicationPaths serverPaths, ILogManager logManager, ILibraryManager libraryManager) { + _libraryManager = libraryManager; + + _logger = logManager.GetLogger("Lucene"); + //string luceneDbPath = serverPaths.DataPath + "\\SearchIndexDB"; //if (!System.IO.Directory.Exists(luceneDbPath)) // System.IO.Directory.CreateDirectory(luceneDbPath); //else if(File.Exists(luceneDbPath + "\\write.lock")) // File.Delete(luceneDbPath + "\\write.lock"); - //LuceneSearch.Init(luceneDbPath, logManager.GetLogger("Lucene")); + //LuceneSearch.Init(luceneDbPath, _logger); //BaseItem.LibraryManager.LibraryChanged += LibraryChanged; } @@ -82,6 +89,146 @@ namespace MediaBrowser.Server.Implementations.Library //LuceneSearch.CloseAll(); } + + /// <summary> + /// Gets the search hints. + /// </summary> + /// <param name="inputItems">The input items.</param> + /// <param name="searchTerm">The search term.</param> + /// <returns>IEnumerable{SearchHintResult}.</returns> + /// <exception cref="System.ArgumentNullException">searchTerm</exception> + public async Task<IEnumerable<BaseItem>> GetSearchHints(IEnumerable<BaseItem> inputItems, string searchTerm) + { + if (string.IsNullOrEmpty(searchTerm)) + { + throw new ArgumentNullException("searchTerm"); + } + + var hints = new List<Tuple<BaseItem, int>>(); + + var items = inputItems.Where(i => !(i is MusicArtist)).ToList(); + + foreach (var item in items) + { + var index = IndexOf(item.Name, searchTerm); + + if (index != -1) + { + hints.Add(new Tuple<BaseItem, int>(item, index)); + } + } + + // Find artists + var artists = items.OfType<Audio>() + .SelectMany(i => new[] { i.Artist, i.AlbumArtist }) + .Where(i => !string.IsNullOrEmpty(i)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var item in artists) + { + var index = IndexOf(item, searchTerm); + + if (index != -1) + { + var artist = await _libraryManager.GetArtist(item).ConfigureAwait(false); + + hints.Add(new Tuple<BaseItem, int>(artist, index)); + } + } + + // Find genres + var genres = items.SelectMany(i => i.Genres) + .Where(i => !string.IsNullOrEmpty(i)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var item in genres) + { + var index = IndexOf(item, searchTerm); + + if (index != -1) + { + var genre = await _libraryManager.GetGenre(item).ConfigureAwait(false); + + hints.Add(new Tuple<BaseItem, int>(genre, index)); + } + } + + // Find studios + var studios = items.SelectMany(i => i.Studios) + .Where(i => !string.IsNullOrEmpty(i)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var item in studios) + { + var index = IndexOf(item, searchTerm); + + if (index != -1) + { + var studio = await _libraryManager.GetStudio(item).ConfigureAwait(false); + + hints.Add(new Tuple<BaseItem, int>(studio, index)); + } + } + + // Find persons + var persons = items.SelectMany(i => i.People) + .Select(i => i.Name) + .Where(i => !string.IsNullOrEmpty(i)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var item in persons) + { + var index = IndexOf(item, searchTerm); + + if (index != -1) + { + var person = await _libraryManager.GetPerson(item).ConfigureAwait(false); + + hints.Add(new Tuple<BaseItem, int>(person, index)); + } + } + + return hints.OrderBy(i => i.Item2).Select(i => i.Item1); + } + + /// <summary> + /// Gets the words. + /// </summary> + /// <param name="term">The term.</param> + /// <returns>System.String[][].</returns> + private string[] GetWords(string term) + { + // TODO: Improve this to be more accurate and respect culture + var words = term.Split(' '); + + return words; + } + + /// <summary> + /// Indexes the of. + /// </summary> + /// <param name="input">The input.</param> + /// <param name="term">The term.</param> + /// <returns>System.Int32.</returns> + private int IndexOf(string input, string term) + { + var index = 0; + + foreach (var word in GetWords(input)) + { + if (word.IndexOf(term, StringComparison.OrdinalIgnoreCase) != -1) + { + return index; + } + + index++; + } + return -1; + } } public static class LuceneSearch diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index f857a876e..c3d4d3094 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -269,7 +269,7 @@ namespace MediaBrowser.ServerApplication DisplayPreferencesManager = new DisplayPreferencesManager(LogManager.GetLogger("DisplayPreferencesManager")); RegisterSingleInstance(DisplayPreferencesManager); - RegisterSingleInstance<ILibrarySearchEngine>(() => new LuceneSearchEngine(ApplicationPaths, LogManager)); + RegisterSingleInstance<ILibrarySearchEngine>(() => new LuceneSearchEngine(ApplicationPaths, LogManager, LibraryManager)); MediaEncoder = new MediaEncoder(LogManager.GetLogger("MediaEncoder"), ZipClient, ApplicationPaths, JsonSerializer); RegisterSingleInstance(MediaEncoder); diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index df3e78e71..7aaa4d173 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -485,6 +485,7 @@ namespace MediaBrowser.WebDashboard.Api "pluginupdatespage.js", "scheduledtaskpage.js", "scheduledtaskspage.js", + "search.js", "songs.js", "supporterkeypage.js", "supporterpage.js", @@ -528,6 +529,7 @@ namespace MediaBrowser.WebDashboard.Api "librarybrowser.css", "detailtable.css", "posteritem.css", + "search.css", "pluginupdates.css", "userimage.css" }; diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 28f65da0f..e0e4f8c9c 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -93,6 +93,9 @@ <Content Include="dashboard-ui\css\images\bgflip.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\css\images\searchbutton.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\css\images\currentuserdefaultblack.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -183,6 +186,9 @@ <Content Include="dashboard-ui\css\posteritem.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\css\search.css">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\css\userimage.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -252,6 +258,9 @@ <Content Include="dashboard-ui\scripts\musicrecommended.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\scripts\search.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\songs.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
|
