Преглед на файлове

Merge remote-tracking branch 'upstream/master' into ribbons

Tim Hobbs преди 11 години
родител
ревизия
9976857e78
променени са 100 файла, в които са добавени 1811 реда и са изтрити 648 реда
  1. 4 1
      MediaBrowser.Api/BaseApiService.cs
  2. 4 2
      MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs
  3. 1 1
      MediaBrowser.Api/GamesService.cs
  4. 8 8
      MediaBrowser.Api/ItemUpdateService.cs
  5. 5 4
      MediaBrowser.Api/MediaBrowser.Api.csproj
  6. 80 0
      MediaBrowser.Api/Movies/CollectionService.cs
  7. 324 0
      MediaBrowser.Api/Movies/MoviesService.cs
  8. 1 1
      MediaBrowser.Api/Movies/TrailersService.cs
  9. 0 82
      MediaBrowser.Api/MoviesService.cs
  10. 1 1
      MediaBrowser.Api/Music/AlbumsService.cs
  11. 1 1
      MediaBrowser.Api/Music/InstantMixService.cs
  12. 75 53
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  13. 1 1
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  14. 1 1
      MediaBrowser.Api/Playback/Progressive/AudioService.cs
  15. 7 3
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  16. 1 1
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  17. 2 0
      MediaBrowser.Api/Playback/StreamRequest.cs
  18. 17 1
      MediaBrowser.Api/Playback/StreamState.cs
  19. 9 3
      MediaBrowser.Api/SearchService.cs
  20. 2 2
      MediaBrowser.Api/SessionsService.cs
  21. 6 6
      MediaBrowser.Api/SimilarItemsHelper.cs
  22. 87 17
      MediaBrowser.Api/TvShowsService.cs
  23. 6 8
      MediaBrowser.Api/UserLibrary/ArtistsService.cs
  24. 25 18
      MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
  25. 2 7
      MediaBrowser.Api/UserLibrary/GameGenresService.cs
  26. 2 7
      MediaBrowser.Api/UserLibrary/GenresService.cs
  27. 15 0
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  28. 2 7
      MediaBrowser.Api/UserLibrary/MusicGenresService.cs
  29. 2 7
      MediaBrowser.Api/UserLibrary/PersonsService.cs
  30. 2 7
      MediaBrowser.Api/UserLibrary/StudiosService.cs
  31. 37 5
      MediaBrowser.Api/UserLibrary/UserLibraryService.cs
  32. 2 17
      MediaBrowser.Api/UserLibrary/YearsService.cs
  33. 67 0
      MediaBrowser.Controller/Channels/ChannelItemInfo.cs
  34. 59 0
      MediaBrowser.Controller/Channels/IChannel.cs
  35. 12 0
      MediaBrowser.Controller/Channels/IChannelManager.cs
  36. 22 0
      MediaBrowser.Controller/Collections/CollectionCreationOptions.cs
  37. 32 0
      MediaBrowser.Controller/Collections/ICollectionManager.cs
  38. 21 0
      MediaBrowser.Controller/Dto/IDtoService.cs
  39. 18 0
      MediaBrowser.Controller/Entities/Audio/Audio.cs
  40. 5 6
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  41. 7 11
      MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
  42. 11 2
      MediaBrowser.Controller/Entities/BaseItem.cs
  43. 1 14
      MediaBrowser.Controller/Entities/BasePluginFolder.cs
  44. 52 18
      MediaBrowser.Controller/Entities/Folder.cs
  45. 7 10
      MediaBrowser.Controller/Entities/GameGenre.cs
  46. 8 10
      MediaBrowser.Controller/Entities/Genre.cs
  47. 3 3
      MediaBrowser.Controller/Entities/IHasImages.cs
  48. 7 31
      MediaBrowser.Controller/Entities/IItemByName.cs
  49. 7 3
      MediaBrowser.Controller/Entities/LinkedChild.cs
  50. 7 10
      MediaBrowser.Controller/Entities/Person.cs
  51. 7 11
      MediaBrowser.Controller/Entities/Studio.cs
  52. 2 1
      MediaBrowser.Controller/Entities/UserRootFolder.cs
  53. 17 12
      MediaBrowser.Controller/Entities/Year.cs
  54. 0 13
      MediaBrowser.Controller/Library/ILibraryManager.cs
  55. 1 2
      MediaBrowser.Controller/Library/IUserManager.cs
  56. 80 80
      MediaBrowser.Controller/Library/TVUtils.cs
  57. 0 7
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  58. 5 10
      MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
  59. 5 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  60. 12 0
      MediaBrowser.Controller/Net/IHttpResultFactory.cs
  61. 7 23
      MediaBrowser.Controller/Providers/DirectoryService.cs
  62. 1 1
      MediaBrowser.Controller/Providers/ILocalImageProvider.cs
  63. 7 0
      MediaBrowser.Controller/Session/ISessionManager.cs
  64. 1 1
      MediaBrowser.Dlna/MediaBrowser.Dlna.csproj
  65. 18 3
      MediaBrowser.Dlna/PlayTo/Configuration/PlayToConfiguration.cs
  66. 12 3
      MediaBrowser.Dlna/PlayTo/Configuration/TranscodeSetting.cs
  67. 68 28
      MediaBrowser.Dlna/PlayTo/Device.cs
  68. 10 1
      MediaBrowser.Dlna/PlayTo/DlnaController.cs
  69. 79 12
      MediaBrowser.Dlna/PlayTo/PlayToServerEntryPoint.cs
  70. 14 1
      MediaBrowser.Dlna/PlayTo/PlaylistItem.cs
  71. 9 5
      MediaBrowser.Dlna/PlayTo/StreamHelper.cs
  72. 6 0
      MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
  73. 6 0
      MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
  74. 80 10
      MediaBrowser.Model/ApiClient/IApiClient.cs
  75. 4 0
      MediaBrowser.Model/Configuration/AutoOrganize.cs
  76. 8 0
      MediaBrowser.Model/Configuration/DlnaOptions.cs
  77. 4 0
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  78. 29 0
      MediaBrowser.Model/Dto/RecommendationDto.cs
  79. 24 0
      MediaBrowser.Model/Entities/BaseItemInfo.cs
  80. 2 0
      MediaBrowser.Model/MediaBrowser.Model.csproj
  81. 12 0
      MediaBrowser.Model/Querying/ItemQuery.cs
  82. 3 7
      MediaBrowser.Model/Querying/ItemSortBy.cs
  83. 4 0
      MediaBrowser.Model/Querying/ItemsByNameQuery.cs
  84. 28 0
      MediaBrowser.Model/Querying/NextUpQuery.cs
  85. 4 0
      MediaBrowser.Model/Search/SearchQuery.cs
  86. 2 2
      MediaBrowser.Model/Session/BrowseRequest.cs
  87. 2 2
      MediaBrowser.Model/Session/MessageCommand.cs
  88. 1 1
      MediaBrowser.Model/Session/PlayRequest.cs
  89. 1 1
      MediaBrowser.Model/Session/PlaystateCommand.cs
  90. 1 1
      MediaBrowser.Providers/AdultVideos/AdultVideoXmlProvider.cs
  91. 1 1
      MediaBrowser.Providers/All/LocalImageProvider.cs
  92. 1 1
      MediaBrowser.Providers/BaseXmlProvider.cs
  93. 10 0
      MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
  94. 129 0
      MediaBrowser.Providers/BoxSets/BoxSetXmlParser.cs
  95. 2 2
      MediaBrowser.Providers/BoxSets/BoxSetXmlProvider.cs
  96. 1 1
      MediaBrowser.Providers/Folders/FolderXmlProvider.cs
  97. 1 1
      MediaBrowser.Providers/Games/GameSystemXmlProvider.cs
  98. 1 1
      MediaBrowser.Providers/Games/GameXmlProvider.cs
  99. 1 1
      MediaBrowser.Providers/LiveTv/ChannelXmlProvider.cs
  100. 10 12
      MediaBrowser.Providers/Manager/MetadataService.cs

+ 4 - 1
MediaBrowser.Api/BaseApiService.cs

@@ -164,7 +164,10 @@ namespace MediaBrowser.Api
                 return name;
             }
 
-            return libraryManager.GetAllArtists()
+            return libraryManager.RootFolder.RecursiveChildren
+                .OfType<Audio>()
+                .SelectMany(i => i.AllArtists)
+                .Distinct(StringComparer.OrdinalIgnoreCase)
                 .FirstOrDefault(i =>
                 {
                     i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));

+ 4 - 2
MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs

@@ -194,8 +194,10 @@ namespace MediaBrowser.Api.DefaultTheme
                 .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
                 .ToList();
 
-            var artists = _libraryManager.GetAllArtists(allItems)
-            .Randomize()
+            var artists = allItems.OfType<Audio>()
+                .SelectMany(i => i.AllArtists)
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .Randomize()
             .Select(i =>
             {
                 try

+ 1 - 1
MediaBrowser.Api/GamesService.cs

@@ -155,7 +155,7 @@ namespace MediaBrowser.Api
 
             var games = items.OfType<Game>().ToList();
 
-            summary.ClientInstalledGameCount = games.Count(i => !i.IsPlaceHolder);
+            summary.ClientInstalledGameCount = games.Count(i => i.IsPlaceHolder);
 
             summary.GameCount = games.Count;
 

+ 8 - 8
MediaBrowser.Api/ItemUpdateService.cs

@@ -98,7 +98,7 @@ namespace MediaBrowser.Api
 
             UpdateItem(request, item);
 
-            await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+            await item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
 
             if (dontFetchMetaChanged && item.IsFolder)
             {
@@ -107,7 +107,7 @@ namespace MediaBrowser.Api
                 foreach (var child in folder.RecursiveChildren.ToList())
                 {
                     child.DontFetchMeta = newLockData;
-                    await _libraryManager.UpdateItem(child, ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+                    await child.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
                 }
             }
         }
@@ -125,7 +125,7 @@ namespace MediaBrowser.Api
 
             UpdateItem(request, item);
 
-            await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+            await item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
         }
 
         public void Post(UpdateArtist request)
@@ -141,7 +141,7 @@ namespace MediaBrowser.Api
 
             UpdateItem(request, item);
 
-            await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+            await item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
         }
 
         public void Post(UpdateStudio request)
@@ -157,7 +157,7 @@ namespace MediaBrowser.Api
 
             UpdateItem(request, item);
 
-            await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+            await item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
         }
 
         public void Post(UpdateMusicGenre request)
@@ -173,7 +173,7 @@ namespace MediaBrowser.Api
 
             UpdateItem(request, item);
 
-            await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+            await item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
         }
 
         public void Post(UpdateGameGenre request)
@@ -189,7 +189,7 @@ namespace MediaBrowser.Api
 
             UpdateItem(request, item);
 
-            await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+            await item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
         }
 
         public void Post(UpdateGenre request)
@@ -205,7 +205,7 @@ namespace MediaBrowser.Api
 
             UpdateItem(request, item);
 
-            await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+            await item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
         }
 
         private void UpdateItem(BaseItemDto request, BaseItem item)

+ 5 - 4
MediaBrowser.Api/MediaBrowser.Api.csproj

@@ -66,7 +66,8 @@
     <Compile Include="..\SharedVersion.cs">
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
-    <Compile Include="AlbumsService.cs" />
+    <Compile Include="Movies\CollectionService.cs" />
+    <Compile Include="Music\AlbumsService.cs" />
     <Compile Include="AppThemeService.cs" />
     <Compile Include="BaseApiService.cs" />
     <Compile Include="ConfigurationService.cs" />
@@ -81,7 +82,7 @@
     <Compile Include="Images\ImageRequest.cs" />
     <Compile Include="Images\ImageService.cs" />
     <Compile Include="Images\ImageWriter.cs" />
-    <Compile Include="InstantMixService.cs" />
+    <Compile Include="Music\InstantMixService.cs" />
     <Compile Include="ItemLookupService.cs" />
     <Compile Include="ItemRefreshService.cs" />
     <Compile Include="ItemUpdateService.cs" />
@@ -91,7 +92,7 @@
     <Compile Include="Library\LibraryStructureService.cs" />
     <Compile Include="LiveTv\LiveTvService.cs" />
     <Compile Include="LocalizationService.cs" />
-    <Compile Include="MoviesService.cs" />
+    <Compile Include="Movies\MoviesService.cs" />
     <Compile Include="NewsService.cs" />
     <Compile Include="NotificationsService.cs" />
     <Compile Include="PackageReviewService.cs" />
@@ -118,7 +119,7 @@
     <Compile Include="SessionsService.cs" />
     <Compile Include="SimilarItemsHelper.cs" />
     <Compile Include="SystemService.cs" />
-    <Compile Include="TrailersService.cs" />
+    <Compile Include="Movies\TrailersService.cs" />
     <Compile Include="TvShowsService.cs" />
     <Compile Include="UserLibrary\ArtistsService.cs" />
     <Compile Include="UserLibrary\BaseItemsByNameService.cs" />

+ 80 - 0
MediaBrowser.Api/Movies/CollectionService.cs

@@ -0,0 +1,80 @@
+using MediaBrowser.Controller.Collections;
+using ServiceStack;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Movies
+{
+    [Route("/Collections", "POST")]
+    [Api(Description = "Creates a new collection")]
+    public class CreateCollection : IReturnVoid
+    {
+        [ApiMember(Name = "IsLocked", Description = "Whether or not to lock the new collection.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
+        public bool IsLocked { get; set; }
+
+        [ApiMember(Name = "Name", Description = "The name of the new collection.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string Name { get; set; }
+
+        [ApiMember(Name = "ParentId", Description = "Optional - create the collection within a specific folder", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public Guid? ParentId { get; set; }
+    }
+
+    [Route("/Collections/{Id}/Items", "POST")]
+    [Api(Description = "Adds items to a collection")]
+    public class AddToCollection : IReturnVoid
+    {
+        [ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string Ids { get; set; }
+
+        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public Guid Id { get; set; }
+    }
+
+    [Route("/Collections/{Id}/Items", "DELETE")]
+    [Api(Description = "Removes items from a collection")]
+    public class RemoveFromCollection : IReturnVoid
+    {
+        [ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string Ids { get; set; }
+
+        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
+        public Guid Id { get; set; }
+    }
+
+    public class CollectionService : BaseApiService
+    {
+        private readonly ICollectionManager _collectionManager;
+
+        public CollectionService(ICollectionManager collectionManager)
+        {
+            _collectionManager = collectionManager;
+        }
+
+        public void Post(CreateCollection request)
+        {
+            var task = _collectionManager.CreateCollection(new CollectionCreationOptions
+            {
+                IsLocked = request.IsLocked,
+                Name = request.Name,
+                ParentId = request.ParentId
+            });
+
+            Task.WaitAll(task);
+        }
+
+        public void Post(AddToCollection request)
+        {
+            var task = _collectionManager.AddToCollection(request.Id, request.Ids.Split(',').Select(i => new Guid(i)));
+
+            Task.WaitAll(task);
+        }
+
+        public void Delete(RemoveFromCollection request)
+        {
+            var task = _collectionManager.RemoveFromCollection(request.Id, request.Ids.Split(',').Select(i => new Guid(i)));
+
+            Task.WaitAll(task);
+        }
+    }
+}

+ 324 - 0
MediaBrowser.Api/Movies/MoviesService.cs

@@ -0,0 +1,324 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using ServiceStack;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Api.Movies
+{
+    /// <summary>
+    /// Class GetSimilarMovies
+    /// </summary>
+    [Route("/Movies/{Id}/Similar", "GET")]
+    [Api(Description = "Finds movies and trailers similar to a given movie.")]
+    public class GetSimilarMovies : BaseGetSimilarItemsFromItem
+    {
+        [ApiMember(Name = "IncludeTrailers", Description = "Whether or not to include trailers within the results. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool IncludeTrailers { get; set; }
+
+        public GetSimilarMovies()
+        {
+            IncludeTrailers = true;
+        }
+    }
+
+    [Route("/Movies/Recommendations", "GET")]
+    [Api(Description = "Gets movie recommendations")]
+    public class GetMovieRecommendations : IReturn<RecommendationDto[]>, IHasItemFields
+    {
+        [ApiMember(Name = "CategoryLimit", Description = "The max number of categories to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int CategoryLimit { get; set; }
+
+        [ApiMember(Name = "ItemLimit", Description = "The max number of items to return per category", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int ItemLimit { get; set; }
+
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public Guid? UserId { get; set; }
+
+        public GetMovieRecommendations()
+        {
+            CategoryLimit = 5;
+            ItemLimit = 8;
+        }
+
+        public string Fields { get; set; }
+    }
+
+    /// <summary>
+    /// Class MoviesService
+    /// </summary>
+    public class MoviesService : BaseApiService
+    {
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// The _user data repository
+        /// </summary>
+        private readonly IUserDataManager _userDataRepository;
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly ILibraryManager _libraryManager;
+
+        private readonly IItemRepository _itemRepo;
+        private readonly IDtoService _dtoService;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MoviesService"/> class.
+        /// </summary>
+        /// <param name="userManager">The user manager.</param>
+        /// <param name="userDataRepository">The user data repository.</param>
+        /// <param name="libraryManager">The library manager.</param>
+        public MoviesService(IUserManager userManager, IUserDataManager userDataRepository, ILibraryManager libraryManager, IItemRepository itemRepo, IDtoService dtoService)
+        {
+            _userManager = userManager;
+            _userDataRepository = userDataRepository;
+            _libraryManager = libraryManager;
+            _itemRepo = itemRepo;
+            _dtoService = dtoService;
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetSimilarMovies request)
+        {
+            var result = SimilarItemsHelper.GetSimilarItemsResult(_userManager,
+                _itemRepo,
+                _libraryManager,
+                _userDataRepository,
+                _dtoService,
+                Logger,
+                request, item => item is Movie || (item is Trailer && request.IncludeTrailers),
+                SimilarItemsHelper.GetSimiliarityScore);
+
+            return ToOptimizedSerializedResultUsingCache(result);
+        }
+
+        public object Get(GetMovieRecommendations request)
+        {
+            var user = request.UserId.HasValue ? _userManager.GetUserById(request.UserId.Value) : null;
+
+            var folder = user.RootFolder;
+            var movies = folder.RecursiveChildren.OfType<Movie>().ToList();
+
+            var result = GetRecommendationCategories(user, movies, request.CategoryLimit, request.ItemLimit, request.GetItemFields().ToList());
+
+            return ToOptimizedResult(result);
+        }
+
+        private IEnumerable<RecommendationDto> GetRecommendationCategories(User user, List<Movie> allMovies, int categoryLimit, int itemLimit, List<ItemFields> fields)
+        {
+            var categories = new List<RecommendationDto>();
+
+            var recentlyPlayedMovies = allMovies
+                .Select(i =>
+                {
+                    var userdata = _userDataRepository.GetUserData(user.Id, i.GetUserDataKey());
+                    return new Tuple<Movie, bool, DateTime>(i, userdata.Played, userdata.LastPlayedDate ?? DateTime.MinValue);
+                })
+                .Where(i => i.Item2)
+                .OrderByDescending(i => i.Item3)
+                .Select(i => i.Item1)
+                .ToList();
+
+            var excludeFromLiked = recentlyPlayedMovies.Take(10);
+            var likedMovies = allMovies
+                .Select(i =>
+                {
+                    var score = 0;
+                    var userData = _userDataRepository.GetUserData(user.Id, i.GetUserDataKey());
+
+                    if (userData.IsFavorite)
+                    {
+                        score = 2;
+                    }
+                    else
+                    {
+                        score = userData.Likes.HasValue ? userData.Likes.Value ? 1 : -1 : 0;
+                    }
+
+                    return new Tuple<Movie, int>(i, score);
+                })
+                .OrderByDescending(i => i.Item2)
+                .ThenBy(i => Guid.NewGuid())
+                .Where(i => i.Item2 > 0)
+                .Select(i => i.Item1)
+                .Where(i => !excludeFromLiked.Contains(i));
+
+            var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList();
+            // Get recently played directors
+            var recentDirectors = GetDirectors(mostRecentMovies)
+                .OrderBy(i => Guid.NewGuid())
+                .ToList();
+
+            // Get recently played actors
+            var recentActors = GetActors(mostRecentMovies)
+                .OrderBy(i => Guid.NewGuid())
+                .ToList();
+
+            var similarToRecentlyPlayed = GetSimilarTo(user, allMovies, recentlyPlayedMovies.Take(7).OrderBy(i => Guid.NewGuid()), itemLimit, fields, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator();
+            var similarToLiked = GetSimilarTo(user, allMovies, likedMovies, itemLimit, fields, RecommendationType.SimilarToLikedItem).GetEnumerator();
+
+            var hasDirectorFromRecentlyPlayed = GetWithDirector(user, allMovies, recentDirectors, itemLimit, fields, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator();
+            var hasActorFromRecentlyPlayed = GetWithActor(user, allMovies, recentActors, itemLimit, fields, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator();
+
+            var categoryTypes = new List<IEnumerator<RecommendationDto>>
+            {
+                // Give this extra weight
+                similarToRecentlyPlayed,
+                similarToRecentlyPlayed,
+
+                // Give this extra weight
+                similarToLiked,
+                similarToLiked,
+
+                hasDirectorFromRecentlyPlayed,
+                hasActorFromRecentlyPlayed
+            };
+
+            while (categories.Count < categoryLimit)
+            {
+                var allEmpty = true;
+
+                foreach (var category in categoryTypes)
+                {
+                    if (category.MoveNext())
+                    {
+                        categories.Add(category.Current);
+                        allEmpty = false;
+
+                        if (categories.Count >= categoryLimit)
+                        {
+                            break;
+                        }
+                    }
+                }
+
+                if (allEmpty)
+                {
+                    break;
+                }
+            }
+
+            //// Get the lead actor for all movies
+            //var allActors = GetActors(allMovies)
+            //    .ToList();
+
+            //foreach (var actor in recentActors)
+            //{
+
+            //}
+
+            return categories.OrderBy(i => i.RecommendationType).ThenBy(i => Guid.NewGuid());
+        }
+
+        private IEnumerable<RecommendationDto> GetWithDirector(User user, List<Movie> allMovies, IEnumerable<string> directors, int itemLimit, List<ItemFields> fields, RecommendationType type)
+        {
+            var userId = user.Id;
+
+            foreach (var director in directors)
+            {
+                var items = allMovies
+                    .Where(i => !_userDataRepository.GetUserData(userId, i.GetUserDataKey()).Played && i.People.Any(p => string.Equals(p.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase) && string.Equals(p.Name, director, StringComparison.OrdinalIgnoreCase)))
+                    .Take(itemLimit)
+                    .ToList();
+
+                if (items.Count > 0)
+                {
+                    yield return new RecommendationDto
+                    {
+                        BaselineItemName = director,
+                        CategoryId = director.GetMD5().ToString("N"),
+                        RecommendationType = type,
+                        Items = items.Select(i => _dtoService.GetBaseItemDto(i, fields, user)).ToArray()
+                    };
+                }
+            }
+        }
+
+        private IEnumerable<RecommendationDto> GetWithActor(User user, List<Movie> allMovies, IEnumerable<string> names, int itemLimit, List<ItemFields> fields, RecommendationType type)
+        {
+            var userId = user.Id;
+
+            foreach (var name in names)
+            {
+                var items = allMovies
+                    .Where(i => !_userDataRepository.GetUserData(userId, i.GetUserDataKey()).Played && i.People.Any(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)))
+                    .Take(itemLimit)
+                    .ToList();
+
+                if (items.Count > 0)
+                {
+                    yield return new RecommendationDto
+                    {
+                        BaselineItemName = name,
+                        CategoryId = name.GetMD5().ToString("N"),
+                        RecommendationType = type,
+                        Items = items.Select(i => _dtoService.GetBaseItemDto(i, fields, user)).ToArray()
+                    };
+                }
+            }
+        }
+
+        private IEnumerable<RecommendationDto> GetSimilarTo(User user, List<Movie> allMovies, IEnumerable<Movie> baselineItems, int itemLimit, List<ItemFields> fields, RecommendationType type)
+        {
+            var userId = user.Id;
+
+            foreach (var item in baselineItems)
+            {
+                var similar = SimilarItemsHelper
+                    .GetSimilaritems(item, allMovies, SimilarItemsHelper.GetSimiliarityScore)
+                    .Where(i => !_userDataRepository.GetUserData(userId, i.GetUserDataKey()).Played)
+                    .Take(itemLimit)
+                    .ToList();
+
+                if (similar.Count > 0)
+                {
+                    yield return new RecommendationDto
+                    {
+                        BaselineItemName = item.Name,
+                        CategoryId = item.Id.ToString("N"),
+                        RecommendationType = type,
+                        Items = similar.Select(i => _dtoService.GetBaseItemDto(i, fields, user)).ToArray()
+                    };
+                }
+            }
+        }
+
+        private IEnumerable<string> GetActors(IEnumerable<BaseItem> items)
+        {
+            // Get the two leading actors for all movies
+            return items
+                .SelectMany(i => i.People.Where(p => !string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)).Take(2))
+                .Select(i => i.Name)
+                .Distinct(StringComparer.OrdinalIgnoreCase);
+        }
+
+        private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
+        {
+            return items
+                .Select(i => i.People.FirstOrDefault(p => string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)))
+                .Where(i => i != null)
+                .Select(i => i.Name)
+                .Distinct(StringComparer.OrdinalIgnoreCase);
+        }
+    }
+}

+ 1 - 1
MediaBrowser.Api/TrailersService.cs → MediaBrowser.Api/Movies/TrailersService.cs

@@ -5,7 +5,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using ServiceStack;
 
-namespace MediaBrowser.Api
+namespace MediaBrowser.Api.Movies
 {
     /// <summary>
     /// Class GetSimilarTrailers

+ 0 - 82
MediaBrowser.Api/MoviesService.cs

@@ -1,82 +0,0 @@
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
-using ServiceStack;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class GetSimilarMovies
-    /// </summary>
-    [Route("/Movies/{Id}/Similar", "GET")]
-    [Api(Description = "Finds movies and trailers similar to a given movie.")]
-    public class GetSimilarMovies : BaseGetSimilarItemsFromItem
-    {
-        [ApiMember(Name = "IncludeTrailers", Description = "Whether or not to include trailers within the results. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool IncludeTrailers { get; set; }
-
-        public GetSimilarMovies()
-        {
-            IncludeTrailers = true;
-        }
-    }
-
-    /// <summary>
-    /// Class MoviesService
-    /// </summary>
-    public class MoviesService : BaseApiService
-    {
-        /// <summary>
-        /// The _user manager
-        /// </summary>
-        private readonly IUserManager _userManager;
-
-        /// <summary>
-        /// The _user data repository
-        /// </summary>
-        private readonly IUserDataManager _userDataRepository;
-        /// <summary>
-        /// The _library manager
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-
-        private readonly IItemRepository _itemRepo;
-        private readonly IDtoService _dtoService;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="MoviesService"/> class.
-        /// </summary>
-        /// <param name="userManager">The user manager.</param>
-        /// <param name="userDataRepository">The user data repository.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        public MoviesService(IUserManager userManager, IUserDataManager userDataRepository, ILibraryManager libraryManager, IItemRepository itemRepo, IDtoService dtoService)
-        {
-            _userManager = userManager;
-            _userDataRepository = userDataRepository;
-            _libraryManager = libraryManager;
-            _itemRepo = itemRepo;
-            _dtoService = dtoService;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetSimilarMovies request)
-        {
-            var result = SimilarItemsHelper.GetSimilarItemsResult(_userManager,
-                _itemRepo,
-                _libraryManager,
-                _userDataRepository,
-                _dtoService,
-                Logger,
-                request, item => item is Movie || (item is Trailer && request.IncludeTrailers),
-                SimilarItemsHelper.GetSimiliarityScore);
-
-            return ToOptimizedSerializedResultUsingCache(result);
-        }
-    }
-}

+ 1 - 1
MediaBrowser.Api/AlbumsService.cs → MediaBrowser.Api/Music/AlbumsService.cs

@@ -8,7 +8,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 
-namespace MediaBrowser.Api
+namespace MediaBrowser.Api.Music
 {
     [Route("/Albums/{Id}/Similar", "GET")]
     [Api(Description = "Finds albums similar to a given album.")]

+ 1 - 1
MediaBrowser.Api/InstantMixService.cs → MediaBrowser.Api/Music/InstantMixService.cs

@@ -7,7 +7,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 
-namespace MediaBrowser.Api
+namespace MediaBrowser.Api.Music
 {
     [Route("/Songs/{Id}/InstantMix", "GET")]
     [Api(Description = "Creates an instant playlist based on a given song")]

+ 75 - 53
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -279,8 +279,19 @@ namespace MediaBrowser.Api.Playback
         /// </summary>
         /// <returns>System.Int32.</returns>
         /// <exception cref="System.Exception">Unrecognized MediaEncodingQuality value.</exception>
-        protected int GetNumberOfThreads(bool isWebm)
+        protected int GetNumberOfThreads(StreamState state, bool isWebm)
         {
+            // Use more when this is true. -re will keep cpu usage under control
+            if (state.ReadInputAtNativeFramerate)
+            {
+                if (isWebm)
+                {
+                    return Math.Max(Environment.ProcessorCount - 1, 1);
+                }
+
+                return 0;
+            }
+
             // Webm: http://www.webmproject.org/docs/encoder-parameters/
             // The decoder will usually automatically use an appropriate number of threads according to how many cores are available but it can only use multiple threads 
             // for the coefficient data if the encoder selected --token-parts > 0 at encode time.
@@ -491,16 +502,16 @@ namespace MediaBrowser.Api.Playback
 
                 return string.Format("{4} -vf \"{0}scale=trunc({1}/2)*2:trunc({2}/2)*2{3}\"", yadifParam, widthParam, heightParam, assSubtitleParam, copyTsParam);
             }
-            
-              // If Max dimensions were supplied
-              //this makes my brain hurt. For width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size
-             if (request.MaxWidth.HasValue && request.MaxHeight.HasValue)
-             {
-                 var MaxwidthParam = request.MaxWidth.Value.ToString(UsCulture);
-                 var MaxheightParam = request.MaxHeight.Value.ToString(UsCulture);
- 
-                 return string.Format("{4} -vf \"{0}scale=trunc(min(iw\\,{1})/2)*2:trunc(min((iw/dar)\\,{2})/2)*2{3}\"", yadifParam, MaxwidthParam, MaxheightParam, assSubtitleParam, copyTsParam);
-             }
+
+            // If Max dimensions were supplied
+            //this makes my brain hurt. For width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size
+            if (request.MaxWidth.HasValue && request.MaxHeight.HasValue)
+            {
+                var MaxwidthParam = request.MaxWidth.Value.ToString(UsCulture);
+                var MaxheightParam = request.MaxHeight.Value.ToString(UsCulture);
+
+                return string.Format("{4} -vf \"{0}scale=trunc(min(iw\\,{1})/2)*2:trunc(min((iw/dar)\\,{2})/2)*2{3}\"", yadifParam, MaxwidthParam, MaxheightParam, assSubtitleParam, copyTsParam);
+            }
 
             var isH264Output = outputVideoCodec.Equals("libx264", StringComparison.OrdinalIgnoreCase);
 
@@ -603,7 +614,7 @@ namespace MediaBrowser.Api.Playback
         private string GetExtractedAssPath(StreamState state, bool performConversion)
         {
             var path = EncodingManager.GetSubtitleCachePath(state.MediaPath, state.SubtitleStream.Index, ".ass");
-            
+
             if (performConversion)
             {
                 InputType type;
@@ -987,20 +998,15 @@ namespace MediaBrowser.Api.Playback
 
             if (state.VideoStream != null)
             {
-                var isUpscaling = false;
-
-                if (state.VideoRequest.Height.HasValue && state.VideoStream.Height.HasValue &&
-                    state.VideoRequest.Height.Value > state.VideoStream.Height.Value)
-                {
-                    isUpscaling = true;
-                }
+                var isUpscaling = state.VideoRequest.Height.HasValue && state.VideoStream.Height.HasValue &&
+                                   state.VideoRequest.Height.Value > state.VideoStream.Height.Value;
 
                 if (state.VideoRequest.Width.HasValue && state.VideoStream.Width.HasValue &&
                     state.VideoRequest.Width.Value > state.VideoStream.Width.Value)
                 {
                     isUpscaling = true;
                 }
-                
+
                 // Don't allow bitrate increases unless upscaling
                 if (!isUpscaling)
                 {
@@ -1198,66 +1204,74 @@ namespace MediaBrowser.Api.Playback
                     request.DeviceId = val;
                 }
                 else if (i == 1)
+                {
+                    request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+                }
+                else if (i == 2)
                 {
                     if (videoRequest != null)
                     {
                         videoRequest.VideoCodec = (VideoCodecs)Enum.Parse(typeof(VideoCodecs), val, true);
                     }
                 }
-                else if (i == 2)
+                else if (i == 3)
                 {
                     request.AudioCodec = (AudioCodecs)Enum.Parse(typeof(AudioCodecs), val, true);
                 }
-                else if (i == 3)
+                else if (i == 4)
                 {
                     if (videoRequest != null)
                     {
                         videoRequest.AudioStreamIndex = int.Parse(val, UsCulture);
                     }
                 }
-                else if (i == 4)
+                else if (i == 5)
                 {
                     if (videoRequest != null)
                     {
                         videoRequest.SubtitleStreamIndex = int.Parse(val, UsCulture);
                     }
                 }
-                else if (i == 5)
+                else if (i == 6)
                 {
                     if (videoRequest != null)
                     {
                         videoRequest.VideoBitRate = int.Parse(val, UsCulture);
                     }
                 }
-                else if (i == 6)
+                else if (i == 7)
                 {
                     request.AudioBitRate = int.Parse(val, UsCulture);
                 }
-                else if (i == 7)
+                else if (i == 8)
                 {
                     request.AudioChannels = int.Parse(val, UsCulture);
                 }
-                else if (i == 8)
+                else if (i == 9)
                 {
                     if (videoRequest != null)
                     {
                         request.StartTimeTicks = long.Parse(val, UsCulture);
                     }
                 }
-                else if (i == 9)
+                else if (i == 10)
                 {
                     if (videoRequest != null)
                     {
                         videoRequest.Profile = val;
                     }
                 }
-                else if (i == 10)
+                else if (i == 11)
                 {
                     if (videoRequest != null)
                     {
                         videoRequest.Level = val;
                     }
                 }
+                else if (i == 12)
+                {
+                    request.ForcedMimeType = val;
+                }
             }
         }
 
@@ -1309,37 +1323,39 @@ namespace MediaBrowser.Api.Playback
                 state.IsInputVideo = string.Equals(recording.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
                 state.PlayableStreamFileNames = new List<string>();
 
-                if (!string.IsNullOrEmpty(recording.RecordingInfo.Path) && File.Exists(recording.RecordingInfo.Path))
+                var path = recording.RecordingInfo.Path;
+                var mediaUrl = recording.RecordingInfo.Url;
+
+                if (string.IsNullOrWhiteSpace(path) && string.IsNullOrWhiteSpace(mediaUrl))
                 {
-                    state.MediaPath = recording.RecordingInfo.Path;
+                    var streamInfo = await LiveTvManager.GetRecordingStream(request.Id, cancellationToken).ConfigureAwait(false);
+
+                    state.LiveTvStreamId = streamInfo.Id;
+
+                    path = streamInfo.Path;
+                    mediaUrl = streamInfo.Url;
+                }
+
+                if (!string.IsNullOrEmpty(path) && File.Exists(path))
+                {
+                    state.MediaPath = path;
                     state.IsRemote = false;
+
+                    state.SendInputOverStandardInput = recording.RecordingInfo.Status == RecordingStatus.InProgress;
                 }
-                else if (!string.IsNullOrEmpty(recording.RecordingInfo.Url))
+                else if (!string.IsNullOrEmpty(mediaUrl))
                 {
-                    state.MediaPath = recording.RecordingInfo.Url;
+                    state.MediaPath = mediaUrl;
                     state.IsRemote = true;
                 }
-                else
-                {
-                    var streamInfo = await LiveTvManager.GetRecordingStream(request.Id, cancellationToken).ConfigureAwait(false);
-
-                    state.LiveTvStreamId = streamInfo.Id;
 
-                    if (!string.IsNullOrEmpty(streamInfo.Path) && File.Exists(streamInfo.Path))
-                    {
-                        state.MediaPath = streamInfo.Path;
-                        state.IsRemote = false;
-                    }
-                    else if (!string.IsNullOrEmpty(streamInfo.Url))
-                    {
-                        state.MediaPath = streamInfo.Url;
-                        state.IsRemote = true;
-                    }
+                //state.RunTimeTicks = recording.RunTimeTicks;
+                if (recording.RecordingInfo.Status == RecordingStatus.InProgress && !state.IsRemote)
+                {
+                    await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
                 }
 
-                //state.RunTimeTicks = recording.RunTimeTicks;
                 state.ReadInputAtNativeFramerate = recording.RecordingInfo.Status == RecordingStatus.InProgress;
-                state.SendInputOverStandardInput = recording.RecordingInfo.Status == RecordingStatus.InProgress;
                 state.AudioSync = "1000";
                 state.DeInterlace = true;
             }
@@ -1359,6 +1375,8 @@ namespace MediaBrowser.Api.Playback
                 {
                     state.MediaPath = streamInfo.Path;
                     state.IsRemote = false;
+
+                    await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
                 }
                 else if (!string.IsNullOrEmpty(streamInfo.Url))
                 {
@@ -1366,7 +1384,6 @@ namespace MediaBrowser.Api.Playback
                     state.IsRemote = true;
                 }
 
-                state.SendInputOverStandardInput = true;
                 state.ReadInputAtNativeFramerate = true;
                 state.AudioSync = "1000";
                 state.DeInterlace = true;
@@ -1411,6 +1428,11 @@ namespace MediaBrowser.Api.Playback
                 state.SubtitleStream = GetMediaStream(mediaStreams, videoRequest.SubtitleStreamIndex, MediaStreamType.Subtitle, false);
                 state.AudioStream = GetMediaStream(mediaStreams, videoRequest.AudioStreamIndex, MediaStreamType.Audio);
 
+                if (state.VideoStream != null && state.VideoStream.IsInterlaced)
+                {
+                    state.DeInterlace = true;
+                }
+
                 EnforceResolutionLimit(state, videoRequest);
             }
             else
@@ -1420,8 +1442,8 @@ namespace MediaBrowser.Api.Playback
 
             state.HasMediaStreams = mediaStreams.Count > 0;
 
-            state.SegmentLength = state.ReadInputAtNativeFramerate ? 3 : 10;
-            state.HlsListSize = state.ReadInputAtNativeFramerate ? 20 : 1440;
+            state.SegmentLength = state.ReadInputAtNativeFramerate ? 5 : 10;
+            state.HlsListSize = state.ReadInputAtNativeFramerate ? 100 : 1440;
 
             return state;
         }

+ 1 - 1
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -278,7 +278,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds);
 
-            var threads = GetNumberOfThreads(false);
+            var threads = GetNumberOfThreads(state, false);
 
             var inputModifier = GetInputModifier(state);
 

+ 1 - 1
MediaBrowser.Api/Playback/Progressive/AudioService.cs

@@ -102,7 +102,7 @@ namespace MediaBrowser.Api.Playback.Progressive
 
             const string vn = " -vn";
 
-            var threads = GetNumberOfThreads(false);
+            var threads = GetNumberOfThreads(state, false);
 
             var inputModifier = GetInputModifier(state);
 

+ 7 - 3
MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs

@@ -214,12 +214,16 @@ namespace MediaBrowser.Api.Playback.Progressive
 
             if (request.Static)
             {
-                return ResultFactory.GetStaticFileResult(Request, state.MediaPath, FileShare.Read, responseHeaders, isHeadRequest);
+                var contentType = state.GetMimeType(state.MediaPath);
+
+                return ResultFactory.GetStaticFileResult(Request, state.MediaPath, contentType, FileShare.Read, responseHeaders, isHeadRequest);
             }
 
             if (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive))
             {
-                return ResultFactory.GetStaticFileResult(Request, outputPath, FileShare.Read, responseHeaders, isHeadRequest);
+                var contentType = state.GetMimeType(outputPath);
+
+                return ResultFactory.GetStaticFileResult(Request, outputPath, contentType, FileShare.Read, responseHeaders, isHeadRequest);
             }
 
             return GetStreamResult(state, responseHeaders, isHeadRequest).Result;
@@ -287,7 +291,7 @@ namespace MediaBrowser.Api.Playback.Progressive
 
             responseHeaders["Accept-Ranges"] = "none";
 
-            var contentType = MimeTypes.GetMimeType(outputPath);
+            var contentType = state.GetMimeType(outputPath);
 
             // Headers only
             if (isHeadRequest)

+ 1 - 1
MediaBrowser.Api/Playback/Progressive/VideoService.cs

@@ -102,7 +102,7 @@ namespace MediaBrowser.Api.Playback.Progressive
                 format = " -f mp4 -movflags frag_keyframe+empty_moov";
             }
 
-            var threads = GetNumberOfThreads(string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase));
+            var threads = GetNumberOfThreads(state, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase));
 
             var inputModifier = GetInputModifier(state);
 

+ 2 - 0
MediaBrowser.Api/Playback/StreamRequest.cs

@@ -66,6 +66,8 @@ namespace MediaBrowser.Api.Playback
         public bool ThrowDebugError { get; set; }
 
         public string Params { get; set; }
+
+        public string ForcedMimeType { get; set; }
     }
 
     public class VideoStreamRequest : StreamRequest

+ 17 - 1
MediaBrowser.Api/Playback/StreamState.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Model.Entities;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using System.Collections.Generic;
 using System.IO;
@@ -72,5 +73,20 @@ namespace MediaBrowser.Api.Playback
         public string InputVideoCodec { get; set; }
 
         public string InputAudioCodec { get; set; }
+
+        public string GetMimeType(string outputPath)
+        {
+            if (!string.IsNullOrWhiteSpace(Request.ForcedMimeType))
+            {
+                if (VideoRequest == null)
+                {
+                    return "audio/" + Request.ForcedMimeType;
+                }
+
+                return "video/" + Request.ForcedMimeType;
+            }
+
+            return MimeTypes.GetMimeType(outputPath);
+        }
     }
 }

+ 9 - 3
MediaBrowser.Api/SearchService.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Drawing;
+using System;
+using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -63,6 +64,9 @@ namespace MediaBrowser.Api
         [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; }
+        
         public GetSearchHints()
         {
             IncludeArtists = true;
@@ -130,7 +134,8 @@ namespace MediaBrowser.Api
                 IncludePeople = request.IncludePeople,
                 IncludeStudios = request.IncludeStudios,
                 StartIndex = request.StartIndex,
-                UserId = request.UserId
+                UserId = request.UserId,
+                IncludeItemTypes = (request.IncludeItemTypes ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray()
 
             }).ConfigureAwait(false);
 
@@ -206,7 +211,8 @@ namespace MediaBrowser.Api
 
                 result.SongCount = songs.Count;
 
-                result.Artists = _libraryManager.GetAllArtists(songs)
+                result.Artists = songs.SelectMany(i => i.AllArtists)
+                    .Distinct(StringComparer.OrdinalIgnoreCase)
                     .ToArray();
 
                 result.AlbumArtist = songs.Select(i => i.AlbumArtist).FirstOrDefault(i => !string.IsNullOrEmpty(i));

+ 2 - 2
MediaBrowser.Api/SessionsService.cs

@@ -211,7 +211,7 @@ namespace MediaBrowser.Api
         [ApiMember(Name = "PlayableMediaTypes", Description = "A list of playable media types, comma delimited. Audio, Video, Book, Game, Photo.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
         public string PlayableMediaTypes { get; set; }
     }
-    
+
     /// <summary>
     /// Class SessionsService
     /// </summary>
@@ -368,4 +368,4 @@ namespace MediaBrowser.Api
                 .ToList();
         }
     }
-}
+}

+ 6 - 6
MediaBrowser.Api/SimilarItemsHelper.cs

@@ -73,7 +73,7 @@ namespace MediaBrowser.Api
 
             var item = string.IsNullOrEmpty(request.Id) ?
                 (request.UserId.HasValue ? user.RootFolder :
-                (Folder)libraryManager.RootFolder) : dtoService.GetItemByDtoId(request.Id, request.UserId);
+                libraryManager.RootFolder) : dtoService.GetItemByDtoId(request.Id, request.UserId);
 
             var fields = request.GetItemFields().ToList();
 
@@ -81,7 +81,7 @@ namespace MediaBrowser.Api
                                  ? libraryManager.RootFolder.GetRecursiveChildren(i => i.Id != item.Id)
                                  : user.RootFolder.GetRecursiveChildren(user, i => i.Id != item.Id);
 
-            var items = GetSimilaritems(item, inputItems, includeInSearch, getSimilarityScore)
+            var items = GetSimilaritems(item, inputItems.Where(includeInSearch), getSimilarityScore)
                 .ToList();
 
             IEnumerable<BaseItem> returnItems = items;
@@ -106,12 +106,12 @@ namespace MediaBrowser.Api
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="inputItems">The input items.</param>
-        /// <param name="includeInSearch">The include in search.</param>
         /// <param name="getSimilarityScore">The get similarity score.</param>
         /// <returns>IEnumerable{BaseItem}.</returns>
-        internal static IEnumerable<BaseItem> GetSimilaritems(BaseItem item, IEnumerable<BaseItem> inputItems, Func<BaseItem, bool> includeInSearch, Func<BaseItem, BaseItem, int> getSimilarityScore)
+        internal static IEnumerable<BaseItem> GetSimilaritems(BaseItem item, IEnumerable<BaseItem> inputItems, Func<BaseItem, BaseItem, int> getSimilarityScore)
         {
-            inputItems = inputItems.Where(includeInSearch);
+            var itemId = item.Id;
+            inputItems = inputItems.Where(i => i.Id != itemId);
 
             return inputItems.Select(i => new Tuple<BaseItem, int>(i, getSimilarityScore(item, i)))
                 .Where(i => i.Item2 > 2)
@@ -153,7 +153,7 @@ namespace MediaBrowser.Api
 
             if (!string.IsNullOrEmpty(item1.OfficialRating) && string.Equals(item1.OfficialRating, item2.OfficialRating, StringComparison.OrdinalIgnoreCase))
             {
-                points += 1;
+                points += 10;
             }
 
             // Find common genres

+ 87 - 17
MediaBrowser.Api/TvShowsService.cs

@@ -18,7 +18,7 @@ namespace MediaBrowser.Api
     /// Class GetNextUpEpisodes
     /// </summary>
     [Route("/Shows/NextUp", "GET")]
-    [Api(("Gets a list of currently installed plugins"))]
+    [Api(("Gets a list of next up episodes"))]
     public class GetNextUpEpisodes : IReturn<ItemsResult>, IHasItemFields
     {
         /// <summary>
@@ -53,6 +53,39 @@ namespace MediaBrowser.Api
         public string SeriesId { get; set; }
     }
 
+    [Route("/Shows/Upcoming", "GET")]
+    [Api(("Gets a list of upcoming episodes"))]
+    public class GetUpcomingEpisodes : IReturn<ItemsResult>, IHasItemFields
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public Guid UserId { get; set; }
+
+        /// <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>
+        /// Fields to return within the items, in addition to basic information
+        /// </summary>
+        /// <value>The fields.</value>
+        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, OverviewHtml, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
+        public string Fields { get; set; }
+    }
+
     [Route("/Shows/{Id}/Similar", "GET")]
     [Api(Description = "Finds tv shows similar to a given one.")]
     public class GetSimilarShows : BaseGetSimilarItemsFromItem
@@ -85,7 +118,7 @@ namespace MediaBrowser.Api
 
         [ApiMember(Name = "SeasonId", Description = "Optional. Filter by season id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string SeasonId { get; set; }
-        
+
         [ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
         public bool? IsMissing { get; set; }
 
@@ -186,6 +219,39 @@ namespace MediaBrowser.Api
             return ToOptimizedSerializedResultUsingCache(result);
         }
 
+        public object Get(GetUpcomingEpisodes request)
+        {
+            var user = _userManager.GetUserById(request.UserId);
+
+            var items = GetAllLibraryItems(request.UserId, _userManager, _libraryManager)
+                .OfType<Episode>();
+
+            var itemsList = _libraryManager.Sort(items, user, new[] { "PremiereDate", "AirTime", "SortName" }, SortOrder.Ascending)
+                .Cast<Episode>()
+                .ToList();
+
+            var unairedEpisodes = itemsList.Where(i => i.IsUnaired).ToList();
+
+            var minPremiereDate = DateTime.Now.Date.AddDays(-1).ToUniversalTime();
+            var previousEpisodes = itemsList.Where(i => !i.IsUnaired && (i.PremiereDate ?? DateTime.MinValue) >= minPremiereDate).ToList();
+
+            previousEpisodes.AddRange(unairedEpisodes);
+
+            var pagedItems = ApplyPaging(previousEpisodes, request.StartIndex, request.Limit);
+
+            var fields = request.GetItemFields().ToList();
+
+            var returnItems = pagedItems.Select(i => _dtoService.GetBaseItemDto(i, fields, user)).ToArray();
+
+            var result = new ItemsResult
+            {
+                TotalRecordCount = itemsList.Count,
+                Items = returnItems
+            };
+
+            return ToOptimizedSerializedResultUsingCache(result);
+        }
+
         /// <summary>
         /// Gets the specified request.
         /// </summary>
@@ -198,7 +264,7 @@ namespace MediaBrowser.Api
             var itemsList = GetNextUpEpisodes(request)
                 .ToList();
 
-            var pagedItems = ApplyPaging(request, itemsList);
+            var pagedItems = ApplyPaging(itemsList, request.StartIndex, request.Limit);
 
             var fields = request.GetItemFields().ToList();
 
@@ -234,11 +300,13 @@ namespace MediaBrowser.Api
 
             return FilterSeries(request, series)
                 .AsParallel()
-                .Select(i => GetNextUp(i, currentUser, request).Item1)
-                .Where(i => i != null)
+                .Select(i => GetNextUp(i, currentUser))
+                .Where(i => i.Item1 != null)
                 .OrderByDescending(i =>
                 {
-                    var seriesUserData = _userDataManager.GetUserData(user.Id, i.Series.GetUserDataKey());
+                    var episode = i.Item1;
+
+                    var seriesUserData = _userDataManager.GetUserData(user.Id, episode.Series.GetUserDataKey());
 
                     if (seriesUserData.IsFavorite)
                     {
@@ -252,7 +320,9 @@ namespace MediaBrowser.Api
 
                     return 0;
                 })
-                .ThenByDescending(i => i.PremiereDate ?? DateTime.MinValue);
+                .ThenByDescending(i =>i.Item2)
+                .ThenByDescending(i => i.Item1.PremiereDate ?? DateTime.MinValue)
+                .Select(i => i.Item1);
         }
 
         /// <summary>
@@ -260,9 +330,8 @@ namespace MediaBrowser.Api
         /// </summary>
         /// <param name="series">The series.</param>
         /// <param name="user">The user.</param>
-        /// <param name="request">The request.</param>
         /// <returns>Task{Episode}.</returns>
-        private Tuple<Episode, DateTime> GetNextUp(Series series, User user, GetNextUpEpisodes request)
+        private Tuple<Episode, DateTime> GetNextUp(Series series, User user)
         {
             // Get them in display order, then reverse
             var allEpisodes = series.GetSeasons(user, true, true)
@@ -321,21 +390,22 @@ namespace MediaBrowser.Api
         /// <summary>
         /// Applies the paging.
         /// </summary>
-        /// <param name="request">The request.</param>
         /// <param name="items">The items.</param>
+        /// <param name="startIndex">The start index.</param>
+        /// <param name="limit">The limit.</param>
         /// <returns>IEnumerable{BaseItem}.</returns>
-        private IEnumerable<BaseItem> ApplyPaging(GetNextUpEpisodes request, IEnumerable<BaseItem> items)
+        private IEnumerable<BaseItem> ApplyPaging(IEnumerable<BaseItem> items, int? startIndex, int? limit)
         {
             // Start at
-            if (request.StartIndex.HasValue)
+            if (startIndex.HasValue)
             {
-                items = items.Skip(request.StartIndex.Value);
+                items = items.Skip(startIndex.Value);
             }
 
             // Return limit
-            if (request.Limit.HasValue)
+            if (limit.HasValue)
             {
-                items = items.Take(request.Limit.Value);
+                items = items.Take(limit.Value);
             }
 
             return items;
@@ -409,7 +479,7 @@ namespace MediaBrowser.Api
 
             return items;
         }
-        
+
         public object Get(GetEpisodes request)
         {
             var user = _userManager.GetUserById(request.UserId);
@@ -435,7 +505,7 @@ namespace MediaBrowser.Api
                 {
                     throw new ResourceNotFoundException("No season exists with Id " + request.SeasonId);
                 }
-                
+
                 episodes = season.GetEpisodes(user);
             }
 

+ 6 - 8
MediaBrowser.Api/UserLibrary/ArtistsService.cs

@@ -85,10 +85,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var user = UserManager.GetUserById(request.UserId.Value);
 
-                return DtoService.GetBaseItemDto(item, fields.ToList(), user);
+                return DtoService.GetItemByNameDto(item, fields.ToList(), user);
             }
 
-            return DtoService.GetBaseItemDto(item, fields.ToList());
+            return DtoService.GetItemByNameDto(item, fields.ToList());
         }
 
         /// <summary>
@@ -111,7 +111,10 @@ namespace MediaBrowser.Api.UserLibrary
         /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
         protected override IEnumerable<MusicArtist> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items)
         {
-            return LibraryManager.GetAllArtists(items)
+            return items
+                .OfType<Audio>()
+                .SelectMany(i => i.AllArtists)
+                .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Select(name =>
                 {
                     try
@@ -126,10 +129,5 @@ namespace MediaBrowser.Api.UserLibrary
 
                 }).Where(i => i != null);
         }
-
-        protected override IEnumerable<BaseItem> GetLibraryItems(MusicArtist item, IEnumerable<BaseItem> libraryItems)
-        {
-            return libraryItems.OfType<IHasArtist>().Where(i => i.HasArtist(item.Name)).Cast<BaseItem>();
-        }
     }
 }

+ 25 - 18
MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs

@@ -56,15 +56,21 @@ namespace MediaBrowser.Api.UserLibrary
         {
             User user = null;
             BaseItem item;
+            List<BaseItem> libraryItems;
 
             if (request.UserId.HasValue)
             {
                 user = UserManager.GetUserById(request.UserId.Value);
                 item = string.IsNullOrEmpty(request.ParentId) ? user.RootFolder : DtoService.GetItemByDtoId(request.ParentId, user.Id);
+
+                libraryItems = user.RootFolder.GetRecursiveChildren(user).ToList();
+
             }
             else
             {
                 item = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : DtoService.GetItemByDtoId(request.ParentId);
+
+                libraryItems = LibraryManager.RootFolder.RecursiveChildren.ToList();
             }
 
             IEnumerable<BaseItem> items;
@@ -93,7 +99,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             var filteredItems = FilterItems(request, extractedItems, user);
 
-            filteredItems = FilterByLibraryItems(request, filteredItems, user);
+            filteredItems = FilterByLibraryItems(request, filteredItems, user, libraryItems);
 
             filteredItems = ItemsService.ApplySortOrder(request, filteredItems, user, LibraryManager).Cast<TItemType>();
 
@@ -122,45 +128,39 @@ namespace MediaBrowser.Api.UserLibrary
 
             var fields = request.GetItemFields().ToList();
 
-            var dtos = ibnItems.Select(i => GetDto(i, user, fields));
+            var tuples = ibnItems.Select(i => new Tuple<TItemType, List<BaseItem>>(i, i.GetTaggedItems(libraryItems).ToList()));
+
+            var dtos = tuples.Select(i => GetDto(i.Item1, user, fields, i.Item2));
 
             result.Items = dtos.Where(i => i != null).ToArray();
 
             return result;
         }
 
-        private IEnumerable<TItemType> FilterByLibraryItems(GetItemsByName request, IEnumerable<TItemType> items, User user)
+        private IEnumerable<TItemType> FilterByLibraryItems(GetItemsByName request, IEnumerable<TItemType> items, User user, IEnumerable<BaseItem> libraryItems)
         {
             var filters = request.GetFilters().ToList();
 
             if (filters.Contains(ItemFilter.IsPlayed))
             {
-                var libraryItems = user.RootFolder.GetRecursiveChildren(user).ToList();
-
-                items = items.Where(i => GetLibraryItems(i, libraryItems).All(l => l.IsPlayed(user)));
+                items = items.Where(i => i.GetTaggedItems(libraryItems).All(l => l.IsPlayed(user)));
             }
 
             if (filters.Contains(ItemFilter.IsUnplayed))
             {
-                var libraryItems = user.RootFolder.GetRecursiveChildren(user).ToList();
-
-                items = items.Where(i => GetLibraryItems(i, libraryItems).All(l => l.IsUnplayed(user)));
+                items = items.Where(i => i.GetTaggedItems(libraryItems).All(l => l.IsUnplayed(user)));
             }
 
             if (request.IsPlayed.HasValue)
             {
                 var val = request.IsPlayed.Value;
 
-                var libraryItems = user.RootFolder.GetRecursiveChildren(user).ToList();
-
-                items = items.Where(i => GetLibraryItems(i, libraryItems).All(l => l.IsPlayed(user)) == val);
+                items = items.Where(i => i.GetTaggedItems(libraryItems).All(l => l.IsPlayed(user)) == val);
             }
 
             return items;
         }
 
-        protected abstract IEnumerable<BaseItem> GetLibraryItems(TItemType item, IEnumerable<BaseItem> libraryItems);
-
         /// <summary>
         /// Filters the items.
         /// </summary>
@@ -174,6 +174,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 items = items.Where(i => string.Compare(request.NameStartsWithOrGreater, i.SortName, StringComparison.CurrentCultureIgnoreCase) < 1);
             }
+            if (!string.IsNullOrEmpty(request.NameStartsWith))
+            {
+                items = items.Where(i => string.Compare(request.NameStartsWith, i.SortName.Substring(0, 1), StringComparison.CurrentCultureIgnoreCase) == 0);
+            }
 
             if (!string.IsNullOrEmpty(request.NameLessThan))
             {
@@ -288,11 +292,11 @@ namespace MediaBrowser.Api.UserLibrary
         /// <param name="item">The item.</param>
         /// <param name="user">The user.</param>
         /// <param name="fields">The fields.</param>
+        /// <param name="libraryItems">The library items.</param>
         /// <returns>Task{DtoBaseItem}.</returns>
-        private BaseItemDto GetDto(TItemType item, User user, List<ItemFields> fields)
+        private BaseItemDto GetDto(TItemType item, User user, List<ItemFields> fields, List<BaseItem> libraryItems)
         {
-            var dto = user == null ? DtoService.GetBaseItemDto(item, fields) :
-                 DtoService.GetBaseItemDto(item, fields, user);
+            var dto = DtoService.GetItemByNameDto(item, fields, libraryItems, user);
 
             return dto;
         }
@@ -313,9 +317,12 @@ namespace MediaBrowser.Api.UserLibrary
         [ApiMember(Name = "NameStartsWithOrGreater", Description = "Optional filter by items whose name is sorted equally or greater than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string NameStartsWithOrGreater { get; set; }
 
+        [ApiMember(Name = "NameStartsWith", Description = "Optional filter by items whose name is sorted equally than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string NameStartsWith { get; set; }
+
         [ApiMember(Name = "NameLessThan", Description = "Optional filter by items whose name is sorted less than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string NameLessThan { get; set; }
-        
+
         public GetItemsByName()
         {
             Recursive = true;

+ 2 - 7
MediaBrowser.Api/UserLibrary/GameGenresService.cs

@@ -76,10 +76,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var user = UserManager.GetUserById(request.UserId.Value);
 
-                return DtoService.GetBaseItemDto(item, fields.ToList(), user);
+                return DtoService.GetItemByNameDto(item, fields.ToList(), user);
             }
 
-            return DtoService.GetBaseItemDto(item, fields.ToList());
+            return DtoService.GetItemByNameDto(item, fields.ToList());
         }
 
         /// <summary>
@@ -109,10 +109,5 @@ namespace MediaBrowser.Api.UserLibrary
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Select(name => LibraryManager.GetGameGenre(name));
         }
-
-        protected override IEnumerable<BaseItem> GetLibraryItems(GameGenre item, IEnumerable<BaseItem> libraryItems)
-        {
-            return libraryItems.Where(i => (i is Game) && i.Genres.Contains(item.Name, StringComparer.OrdinalIgnoreCase));
-        }
     }
 }

+ 2 - 7
MediaBrowser.Api/UserLibrary/GenresService.cs

@@ -81,10 +81,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var user = UserManager.GetUserById(request.UserId.Value);
 
-                return DtoService.GetBaseItemDto(item, fields.ToList(), user);
+                return DtoService.GetItemByNameDto(item, fields.ToList(), user);
             }
 
-            return DtoService.GetBaseItemDto(item, fields.ToList());
+            return DtoService.GetItemByNameDto(item, fields.ToList());
         }
 
         /// <summary>
@@ -112,10 +112,5 @@ namespace MediaBrowser.Api.UserLibrary
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Select(name => LibraryManager.GetGenre(name));
         }
-
-        protected override IEnumerable<BaseItem> GetLibraryItems(Genre item, IEnumerable<BaseItem> libraryItems)
-        {
-            return libraryItems.Where(i => !(i is Game) && !(i is IHasMusicGenres) && i.Genres.Contains(item.Name, StringComparer.OrdinalIgnoreCase));
-        }
     }
 }

+ 15 - 0
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -111,6 +111,12 @@ namespace MediaBrowser.Api.UserLibrary
         [ApiMember(Name = "NameStartsWithOrGreater", Description = "Optional filter by items whose name is sorted equally or greater than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string NameStartsWithOrGreater { get; set; }
 
+        [ApiMember(Name = "NameStartsWith", Description = "Optional filter by items whose name is sorted equally than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string NameStartsWith { get; set; }
+        
+        [ApiMember(Name = "NameLessThan", Description = "Optional filter by items whose name is equally or lesser than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string NameLessThan { get; set; }
+
         [ApiMember(Name = "AlbumArtistStartsWithOrGreater", Description = "Optional filter by items whose album artist is sorted equally or greater than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string AlbumArtistStartsWithOrGreater { get; set; }
 
@@ -768,6 +774,15 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 items = items.Where(i => string.Compare(request.NameStartsWithOrGreater, i.SortName, StringComparison.CurrentCultureIgnoreCase) < 1);
             }
+            if (!string.IsNullOrEmpty(request.NameStartsWith))
+            {
+                items = items.Where(i => string.Compare(request.NameStartsWith, i.SortName.Substring(0, 1), StringComparison.CurrentCultureIgnoreCase) == 0);
+            }
+            
+            if (!string.IsNullOrEmpty(request.NameLessThan))
+            {
+                items = items.Where(i => string.Compare(request.NameLessThan, i.SortName, StringComparison.CurrentCultureIgnoreCase) == 1);
+            }
 
             if (!string.IsNullOrEmpty(request.AlbumArtistStartsWithOrGreater))
             {

+ 2 - 7
MediaBrowser.Api/UserLibrary/MusicGenresService.cs

@@ -76,10 +76,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var user = UserManager.GetUserById(request.UserId.Value);
 
-                return  DtoService.GetBaseItemDto(item, fields.ToList(), user);
+                return DtoService.GetItemByNameDto(item, fields.ToList(), user);
             }
 
-            return  DtoService.GetBaseItemDto(item, fields.ToList());
+            return DtoService.GetItemByNameDto(item, fields.ToList());
         }
 
         /// <summary>
@@ -109,10 +109,5 @@ namespace MediaBrowser.Api.UserLibrary
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Select(name => LibraryManager.GetMusicGenre(name));
         }
-
-        protected override IEnumerable<BaseItem> GetLibraryItems(MusicGenre item, IEnumerable<BaseItem> libraryItems)
-        {
-            return libraryItems.Where(i => (i is IHasMusicGenres) && i.Genres.Contains(item.Name, StringComparer.OrdinalIgnoreCase));
-        }
     }
 }

+ 2 - 7
MediaBrowser.Api/UserLibrary/PersonsService.cs

@@ -93,10 +93,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var user = UserManager.GetUserById(request.UserId.Value);
 
-                return DtoService.GetBaseItemDto(item, fields.ToList(), user);
+                return DtoService.GetItemByNameDto(item, fields.ToList(), user);
             }
 
-            return DtoService.GetBaseItemDto(item, fields.ToList());
+            return DtoService.GetItemByNameDto(item, fields.ToList());
         }
 
         /// <summary>
@@ -163,10 +163,5 @@ namespace MediaBrowser.Api.UserLibrary
 
                 people.Where(p => personTypes.Contains(p.Type ?? string.Empty, StringComparer.OrdinalIgnoreCase) || personTypes.Contains(p.Role ?? string.Empty, StringComparer.OrdinalIgnoreCase));
         }
-
-        protected override IEnumerable<BaseItem> GetLibraryItems(Person item, IEnumerable<BaseItem> libraryItems)
-        {
-            return libraryItems.Where(i => i.People.Any(p => string.Equals(p.Name, item.Name, StringComparison.OrdinalIgnoreCase)));
-        }
     }
 }

+ 2 - 7
MediaBrowser.Api/UserLibrary/StudiosService.cs

@@ -81,10 +81,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var user = UserManager.GetUserById(request.UserId.Value);
 
-                return DtoService.GetBaseItemDto(item, fields.ToList(), user);
+                return DtoService.GetItemByNameDto(item, fields.ToList(), user);
             }
 
-            return DtoService.GetBaseItemDto(item, fields.ToList());
+            return DtoService.GetItemByNameDto(item, fields.ToList());
         }
 
         /// <summary>
@@ -114,10 +114,5 @@ namespace MediaBrowser.Api.UserLibrary
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Select(name => LibraryManager.GetStudio(name));
         }
-
-        protected override IEnumerable<BaseItem> GetLibraryItems(Studio item, IEnumerable<BaseItem> libraryItems)
-        {
-            return libraryItems.Where(i => i.Studios.Contains(item.Name, StringComparer.OrdinalIgnoreCase));
-        }
     }
 }

+ 37 - 5
MediaBrowser.Api/UserLibrary/UserLibraryService.cs

@@ -679,19 +679,35 @@ namespace MediaBrowser.Api.UserLibrary
         /// </summary>
         /// <param name="request">The request.</param>
         public object Post(MarkPlayedItem request)
+        {
+            var result = MarkPlayed(request).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        private async Task<UserItemDataDto> MarkPlayed(MarkPlayedItem request)
         {
             var user = _userManager.GetUserById(request.UserId);
 
             DateTime? datePlayed = null;
-            
+
             if (!string.IsNullOrEmpty(request.DatePlayed))
             {
                 datePlayed = DateTime.ParseExact(request.DatePlayed, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
             }
 
-            var task = UpdatePlayedStatus(user, request.Id, true, datePlayed);
+            var session = GetSession();
+
+            var dto = await UpdatePlayedStatus(user, request.Id, true, datePlayed).ConfigureAwait(false);
 
-            return ToOptimizedResult(task.Result);
+            foreach (var additionalUserInfo in session.AdditionalUsers)
+            {
+                var additionalUser = _userManager.GetUserById(new Guid(additionalUserInfo.UserId));
+
+                await UpdatePlayedStatus(additionalUser, request.Id, true, datePlayed).ConfigureAwait(false);
+            }
+
+            return dto;
         }
 
         private SessionInfo GetSession()
@@ -779,12 +795,28 @@ namespace MediaBrowser.Api.UserLibrary
         /// </summary>
         /// <param name="request">The request.</param>
         public object Delete(MarkUnplayedItem request)
+        {
+            var task = MarkUnplayed(request);
+
+            return ToOptimizedResult(task.Result);
+        }
+
+        private async Task<UserItemDataDto> MarkUnplayed(MarkUnplayedItem request)
         {
             var user = _userManager.GetUserById(request.UserId);
 
-            var task = UpdatePlayedStatus(user, request.Id, false, null);
+            var session = GetSession();
+
+            var dto = await UpdatePlayedStatus(user, request.Id, false, null).ConfigureAwait(false);
 
-            return ToOptimizedResult(task.Result);
+            foreach (var additionalUserInfo in session.AdditionalUsers)
+            {
+                var additionalUser = _userManager.GetUserById(new Guid(additionalUserInfo.UserId));
+
+                await UpdatePlayedStatus(additionalUser, request.Id, false, null).ConfigureAwait(false);
+            }
+
+            return dto;
         }
 
         /// <summary>

+ 2 - 17
MediaBrowser.Api/UserLibrary/YearsService.cs

@@ -7,7 +7,6 @@ using MediaBrowser.Model.Querying;
 using ServiceStack;
 using System;
 using System.Collections.Generic;
-using System.Globalization;
 using System.Linq;
 
 namespace MediaBrowser.Api.UserLibrary
@@ -81,10 +80,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var user = UserManager.GetUserById(request.UserId.Value);
 
-                return DtoService.GetBaseItemDto(item, fields.ToList(), user);
+                return DtoService.GetItemByNameDto(item, fields.ToList(), user);
             }
 
-            return DtoService.GetBaseItemDto(item, fields.ToList());
+            return DtoService.GetItemByNameDto(item, fields.ToList());
         }
 
         /// <summary>
@@ -115,19 +114,5 @@ namespace MediaBrowser.Api.UserLibrary
                 .Distinct()
                 .Select(year => LibraryManager.GetYear(year));
         }
-
-        protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
-        
-        protected override IEnumerable<BaseItem> GetLibraryItems(Year item, IEnumerable<BaseItem> libraryItems)
-        {
-            int year;
-
-            if (!int.TryParse(item.Name, NumberStyles.Integer, UsCulture, out year))
-            {
-                return libraryItems;
-            }
-
-            return libraryItems.Where(i => i.ProductionYear.HasValue && i.ProductionYear.Value == year);
-        }
     }
 }

+ 67 - 0
MediaBrowser.Controller/Channels/ChannelItemInfo.cs

@@ -0,0 +1,67 @@
+using MediaBrowser.Controller.Entities;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Channels
+{
+    public class ChannelItemInfo
+    {
+        public string Name { get; set; }
+
+        public string Id { get; set; }
+
+        public ChannelItemType Type { get; set; }
+
+        public string OfficialRating { get; set; }
+
+        public string Overview { get; set; }
+
+        public List<string> Genres { get; set; }
+
+        public List<PersonInfo> People { get; set; }
+        
+        public float? CommunityRating { get; set; }
+
+        public long? RunTimeTicks { get; set; }
+
+        public bool IsInfinite { get; set; }
+        
+        public string ImageUrl { get; set; }
+
+        public ChannelMediaType MediaType { get; set; }
+
+        public ChannelMediaContentType ContentType { get; set; }
+        
+        public ChannelItemInfo()
+        {
+            Genres = new List<string>();
+            People = new List<PersonInfo>();
+        }
+    }
+
+    public enum ChannelItemType
+    {
+        Media = 0,
+
+        Category = 1
+    }
+
+    public enum ChannelMediaType
+    {
+        Audio = 0,
+
+        Video = 1
+    }
+
+    public enum ChannelMediaContentType
+    {
+        Clip = 0,
+
+        Podcast = 1,
+
+        Trailer = 2,
+
+        Movie = 3,
+
+        Episode = 4
+    }
+}

+ 59 - 0
MediaBrowser.Controller/Channels/IChannel.cs

@@ -0,0 +1,59 @@
+using MediaBrowser.Controller.Entities;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Channels
+{
+    public interface IChannel
+    {
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        string Name { get; }
+
+        /// <summary>
+        /// Gets the home page URL.
+        /// </summary>
+        /// <value>The home page URL.</value>
+        string HomePageUrl { get; }
+
+        /// <summary>
+        /// Gets the capabilities.
+        /// </summary>
+        /// <returns>ChannelCapabilities.</returns>
+        ChannelCapabilities GetCapabilities();
+
+        /// <summary>
+        /// Searches the specified search term.
+        /// </summary>
+        /// <param name="searchTerm">The search term.</param>
+        /// <param name="user">The user.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns>
+        Task<IEnumerable<ChannelItemInfo>> Search(string searchTerm, User user, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the channel items.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{ChannelItem}}.</returns>
+        Task<IEnumerable<ChannelItemInfo>> GetChannelItems(User user, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the channel items.
+        /// </summary>
+        /// <param name="categoryId">The category identifier.</param>
+        /// <param name="user">The user.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{ChannelItem}}.</returns>
+        Task<IEnumerable<ChannelItemInfo>> GetChannelItems(string categoryId, User user, CancellationToken cancellationToken);
+    }
+
+    public class ChannelCapabilities
+    {
+        public bool CanSearch { get; set; }
+    }
+}

+ 12 - 0
MediaBrowser.Controller/Channels/IChannelManager.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Channels
+{
+    public interface IChannelManager
+    {
+    }
+}

+ 22 - 0
MediaBrowser.Controller/Collections/CollectionCreationOptions.cs

@@ -0,0 +1,22 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Collections
+{
+    public class CollectionCreationOptions : IHasProviderIds
+    {
+        public string Name { get; set; }
+
+        public Guid? ParentId { get; set; }
+
+        public bool IsLocked { get; set; }
+
+        public Dictionary<string, string> ProviderIds { get; set; }
+
+        public CollectionCreationOptions()
+        {
+            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+        }
+    }
+}

+ 32 - 0
MediaBrowser.Controller/Collections/ICollectionManager.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Collections
+{
+    public interface ICollectionManager
+    {
+        /// <summary>
+        /// Creates the collection.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <returns>Task.</returns>
+        Task CreateCollection(CollectionCreationOptions options);
+
+        /// <summary>
+        /// Adds to collection.
+        /// </summary>
+        /// <param name="collectionId">The collection identifier.</param>
+        /// <param name="itemIds">The item ids.</param>
+        /// <returns>Task.</returns>
+        Task AddToCollection(Guid collectionId, IEnumerable<Guid> itemIds);
+
+        /// <summary>
+        /// Removes from collection.
+        /// </summary>
+        /// <param name="collectionId">The collection identifier.</param>
+        /// <param name="itemIds">The item ids.</param>
+        /// <returns>Task.</returns>
+        Task RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds);
+    }
+}

+ 21 - 0
MediaBrowser.Controller/Dto/IDtoService.cs

@@ -73,5 +73,26 @@ namespace MediaBrowser.Controller.Dto
         /// <param name="owner">The owner.</param>
         /// <returns>Task{BaseItemDto}.</returns>
         BaseItemDto GetBaseItemDto(BaseItem item, List<ItemFields> fields, User user = null, BaseItem owner = null);
+
+        /// <summary>
+        /// Gets the item by name dto.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="fields">The fields.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>BaseItemDto.</returns>
+        BaseItemDto GetItemByNameDto<T>(T item, List<ItemFields> fields, User user = null)
+            where T : BaseItem, IItemByName;
+
+        /// <summary>
+        /// Gets the item by name dto.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="fields">The fields.</param>
+        /// <param name="taggedItems">The tagged items.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>BaseItemDto.</returns>
+        BaseItemDto GetItemByNameDto<T>(T item, List<ItemFields> fields, List<BaseItem> taggedItems, User user = null)
+            where T : BaseItem, IItemByName;
     }
 }

+ 18 - 0
MediaBrowser.Controller/Entities/Audio/Audio.cs

@@ -66,6 +66,24 @@ namespace MediaBrowser.Controller.Entities.Audio
         /// <value>The artist.</value>
         public List<string> Artists { get; set; }
 
+        [IgnoreDataMember]
+        public List<string> AllArtists
+        {
+            get
+            {
+                var list = new List<string>();
+
+                if (!string.IsNullOrEmpty(AlbumArtist))
+                {
+                    list.Add(AlbumArtist);
+                }
+                list.AddRange(Artists);
+
+                return list;
+
+            }
+        }
+
         /// <summary>
         /// Gets or sets the album.
         /// </summary>

+ 5 - 6
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -1,12 +1,10 @@
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using System.Runtime.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
 
@@ -17,9 +15,6 @@ namespace MediaBrowser.Controller.Entities.Audio
     /// </summary>
     public class MusicArtist : Folder, IMetadataContainer, IItemByName, IHasMusicGenres, IHasDualAccess, IHasTags, IHasProductionLocations, IHasLookupInfo<ArtistInfo>
     {
-        [IgnoreDataMember]
-        public List<ItemByNameCounts> UserItemCountList { get; set; }
-
         public bool IsAccessedByName { get; set; }
 
         /// <summary>
@@ -65,7 +60,6 @@ namespace MediaBrowser.Controller.Entities.Audio
 
         public MusicArtist()
         {
-            UserItemCountList = new List<ItemByNameCounts>();
             Tags = new List<string>();
             ProductionLocations = new List<string>();
         }
@@ -230,5 +224,10 @@ namespace MediaBrowser.Controller.Entities.Audio
 
             return info;
         }
+
+        public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
+        {
+            return inputItems.OfType<IHasArtist>().Where(i => i.HasArtist(Name)).Cast<BaseItem>();
+        }
     }
 }

+ 7 - 11
MediaBrowser.Controller/Entities/Audio/MusicGenre.cs

@@ -1,7 +1,6 @@
-using System.Runtime.Serialization;
-using MediaBrowser.Model.Dto;
-using System;
+using System;
 using System.Collections.Generic;
+using System.Linq;
 
 namespace MediaBrowser.Controller.Entities.Audio
 {
@@ -10,11 +9,6 @@ namespace MediaBrowser.Controller.Entities.Audio
     /// </summary>
     public class MusicGenre : BaseItem, IItemByName
     {
-        public MusicGenre()
-        {
-            UserItemCountList = new List<ItemByNameCounts>();
-        }
-
         /// <summary>
         /// Gets the user data key.
         /// </summary>
@@ -24,9 +18,6 @@ namespace MediaBrowser.Controller.Entities.Audio
             return "MusicGenre-" + Name;
         }
 
-        [IgnoreDataMember]
-        public List<ItemByNameCounts> UserItemCountList { get; set; }
-
         /// <summary>
         /// Returns the folder containing the item.
         /// If the item is a folder, it returns the folder itself
@@ -51,5 +42,10 @@ namespace MediaBrowser.Controller.Entities.Audio
                 return false;
             }
         }
+
+        public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
+        {
+            return inputItems.Where(i => (i is IHasMusicGenres) && i.Genres.Contains(Name, StringComparer.OrdinalIgnoreCase));
+        }
     }
 }

+ 11 - 2
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -124,6 +124,15 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        [IgnoreDataMember]
+        public virtual bool IsHidden
+        {
+            get
+            {
+                return false;
+            }
+        }
+
         [IgnoreDataMember]
         public virtual bool IsOwnedItem
         {
@@ -1175,7 +1184,7 @@ namespace MediaBrowser.Controller.Entities
             return GetImageInfo(type, imageIndex) != null;
         }
 
-        public void SetImagePath(ImageType type, int index, FileInfo file)
+        public void SetImagePath(ImageType type, int index, FileSystemInfo file)
         {
             if (type == ImageType.Chapter)
             {
@@ -1330,7 +1339,7 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="images">The images.</param>
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
         /// <exception cref="System.ArgumentException">Cannot call AddImages with chapter images</exception>
-        public bool AddImages(ImageType imageType, IEnumerable<FileInfo> images)
+        public bool AddImages(ImageType imageType, IEnumerable<FileSystemInfo> images)
         {
             if (imageType == ImageType.Chapter)
             {

+ 1 - 14
MediaBrowser.Controller/Entities/BasePluginFolder.cs

@@ -1,5 +1,4 @@
-using MediaBrowser.Model.Entities;
-
+
 namespace MediaBrowser.Controller.Entities
 {
     /// <summary>
@@ -8,18 +7,6 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public abstract class BasePluginFolder : Folder, ICollectionFolder, IByReferenceItem
     {
-        /// <summary>
-        /// Gets or sets the type of the location.
-        /// </summary>
-        /// <value>The type of the location.</value>
-        public override LocationType LocationType
-        {
-            get
-            {
-                return LocationType.Virtual;
-            }
-        }
-
         protected BasePluginFolder()
         {
             DisplayMediaType = "CollectionFolder";

+ 52 - 18
MediaBrowser.Controller/Entities/Folder.cs

@@ -264,7 +264,7 @@ namespace MediaBrowser.Controller.Entities
         [IgnoreDataMember]
         public IEnumerable<BaseItem> Children
         {
-            get { return ActualChildren; }
+            get { return ActualChildren.Where(i => !i.IsHidden); }
         }
 
         /// <summary>
@@ -745,9 +745,9 @@ namespace MediaBrowser.Controller.Entities
 
             var list = new List<BaseItem>();
 
-            AddChildrenToList(user, includeLinkedChildren, list, false, null);
+            var hasLinkedChildren = AddChildrenToList(user, includeLinkedChildren, list, false, null);
 
-            return list;
+            return hasLinkedChildren ? list.DistinctBy(i => i.Id).ToList() : list;
         }
 
         /// <summary>
@@ -905,13 +905,6 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>BaseItem.</returns>
         private BaseItem GetLinkedChild(LinkedChild info)
         {
-            if (string.IsNullOrEmpty(info.Path))
-            {
-                throw new ArgumentException("Encountered linked child with empty path.");
-            }
-
-            BaseItem item = null;
-
             // First get using the cached Id
             if (info.ItemId.HasValue)
             {
@@ -920,20 +913,19 @@ namespace MediaBrowser.Controller.Entities
                     return null;
                 }
 
-                item = LibraryManager.GetItemById(info.ItemId.Value);
-            }
+                var itemById = LibraryManager.GetItemById(info.ItemId.Value);
 
-            // If still null, search by path
-            if (item == null)
-            {
-                item = LibraryManager.RootFolder.FindByPath(info.Path);
+                if (itemById != null)
+                {
+                    return itemById;
+                }
             }
 
+            var item = FindLinkedChild(info);
+
             // If still null, log
             if (item == null)
             {
-                Logger.Warn("Unable to find linked item at {0}", info.Path);
-
                 // Don't keep searching over and over
                 info.ItemId = Guid.Empty;
             }
@@ -946,6 +938,43 @@ namespace MediaBrowser.Controller.Entities
             return item;
         }
 
+        private BaseItem FindLinkedChild(LinkedChild info)
+        {
+            if (!string.IsNullOrEmpty(info.Path))
+            {
+                var itemByPath = LibraryManager.RootFolder.FindByPath(info.Path);
+
+                if (itemByPath == null)
+                {
+                    Logger.Warn("Unable to find linked item at path {0}", info.Path);
+                }
+
+                return itemByPath;
+            }
+
+            if (!string.IsNullOrWhiteSpace(info.ItemName) && !string.IsNullOrWhiteSpace(info.ItemType))
+            {
+                return LibraryManager.RootFolder.RecursiveChildren.FirstOrDefault(i =>
+                {
+                    if (string.Equals(i.Name, info.ItemName, StringComparison.OrdinalIgnoreCase))
+                    {
+                        if (string.Equals(i.GetType().Name, info.ItemType, StringComparison.OrdinalIgnoreCase))
+                        {
+                            if (info.ItemYear.HasValue)
+                            {
+                                return info.ItemYear.Value == (i.ProductionYear ?? -1);
+                            }
+                            return true;
+                        }
+                    }
+
+                    return false;
+                });
+            }
+
+            return null;
+        }
+
         protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
         {
             var changesFound = false;
@@ -1106,5 +1135,10 @@ namespace MediaBrowser.Controller.Entities
             return GetRecursiveChildren(user).Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual)
                 .All(i => i.IsUnplayed(user));
         }
+
+        public IEnumerable<BaseItem> GetHiddenChildren()
+        {
+            return ActualChildren.Where(i => i.IsHidden);
+        }
     }
 }

+ 7 - 10
MediaBrowser.Controller/Entities/GameGenre.cs

@@ -1,16 +1,11 @@
-using MediaBrowser.Model.Dto;
+using System;
 using System.Collections.Generic;
-using System.Runtime.Serialization;
+using System.Linq;
 
 namespace MediaBrowser.Controller.Entities
 {
     public class GameGenre : BaseItem, IItemByName
     {
-        public GameGenre()
-        {
-            UserItemCountList = new List<ItemByNameCounts>();
-        }
-
         /// <summary>
         /// Gets the user data key.
         /// </summary>
@@ -20,9 +15,6 @@ namespace MediaBrowser.Controller.Entities
             return "GameGenre-" + Name;
         }
 
-        [IgnoreDataMember]
-        public List<ItemByNameCounts> UserItemCountList { get; set; }
-
         /// <summary>
         /// Returns the folder containing the item.
         /// If the item is a folder, it returns the folder itself
@@ -47,5 +39,10 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
         }
+
+        public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
+        {
+            return inputItems.Where(i => (i is Game) && i.Genres.Contains(Name, StringComparer.OrdinalIgnoreCase));
+        }
     }
 }

+ 8 - 10
MediaBrowser.Controller/Entities/Genre.cs

@@ -1,6 +1,7 @@
-using MediaBrowser.Model.Dto;
+using MediaBrowser.Controller.Entities.Audio;
+using System;
 using System.Collections.Generic;
-using System.Runtime.Serialization;
+using System.Linq;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -9,11 +10,6 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class Genre : BaseItem, IItemByName
     {
-        public Genre()
-        {
-            UserItemCountList = new List<ItemByNameCounts>();
-        }
-
         /// <summary>
         /// Gets the user data key.
         /// </summary>
@@ -23,9 +19,6 @@ namespace MediaBrowser.Controller.Entities
             return "Genre-" + Name;
         }
 
-        [IgnoreDataMember]
-        public List<ItemByNameCounts> UserItemCountList { get; set; }
-
         /// <summary>
         /// Returns the folder containing the item.
         /// If the item is a folder, it returns the folder itself
@@ -50,5 +43,10 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
         }
+
+        public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
+        {
+            return inputItems.Where(i => !(i is Game) && !(i is IHasMusicGenres) && i.Genres.Contains(Name, StringComparer.OrdinalIgnoreCase));
+        }
     }
 }

+ 3 - 3
MediaBrowser.Controller/Entities/IHasImages.cs

@@ -62,7 +62,7 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="type">The type.</param>
         /// <param name="index">The index.</param>
         /// <param name="file">The file.</param>
-        void SetImagePath(ImageType type, int index, FileInfo file);
+        void SetImagePath(ImageType type, int index, FileSystemInfo file);
 
         /// <summary>
         /// Determines whether the specified type has image.
@@ -129,7 +129,7 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="imageType">Type of the image.</param>
         /// <param name="images">The images.</param>
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
-        bool AddImages(ImageType imageType, IEnumerable<FileInfo> images);
+        bool AddImages(ImageType imageType, IEnumerable<FileSystemInfo> images);
 
         /// <summary>
         /// Determines whether [is save local metadata enabled].
@@ -180,7 +180,7 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="item">The item.</param>
         /// <param name="imageType">Type of the image.</param>
         /// <param name="file">The file.</param>
-        public static void SetImagePath(this IHasImages item, ImageType imageType, FileInfo file)
+        public static void SetImagePath(this IHasImages item, ImageType imageType, FileSystemInfo file)
         {
             item.SetImagePath(imageType, 0, file);
         }

+ 7 - 31
MediaBrowser.Controller/Entities/IItemByName.cs

@@ -1,7 +1,4 @@
-using MediaBrowser.Model.Dto;
-using System;
-using System.Collections.Generic;
-using System.Linq;
+using System.Collections.Generic;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -10,37 +7,16 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public interface IItemByName
     {
-        List<ItemByNameCounts> UserItemCountList { get; set; }
+        /// <summary>
+        /// Gets the tagged items.
+        /// </summary>
+        /// <param name="inputItems">The input items.</param>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems);
     }
 
     public interface IHasDualAccess : IItemByName
     {
         bool IsAccessedByName { get; }
     }
-
-    public static class ItemByNameExtensions
-    {
-        public static ItemByNameCounts GetItemByNameCounts(this IItemByName item, Guid userId)
-        {
-            if (userId == Guid.Empty)
-            {
-                throw new ArgumentNullException("userId");
-            }
-
-            return item.UserItemCountList.FirstOrDefault(i => i.UserId == userId);
-        }
-
-        public static void SetItemByNameCounts(this IItemByName item, Guid userId, ItemByNameCounts counts)
-        {
-            var current = item.UserItemCountList.FirstOrDefault(i => i.UserId == userId);
-
-            if (current != null)
-            {
-                item.UserItemCountList.Remove(current);
-            }
-
-            counts.UserId = userId;
-            item.UserItemCountList.Add(counts);
-        }
-    }
 }

+ 7 - 3
MediaBrowser.Controller/Entities/LinkedChild.cs

@@ -9,6 +9,10 @@ namespace MediaBrowser.Controller.Entities
         public string Path { get; set; }
         public LinkedChildType Type { get; set; }
 
+        public string ItemName { get; set; }
+        public string ItemType { get; set; }
+        public int? ItemYear { get; set; }
+
         /// <summary>
         /// Serves as a cache
         /// </summary>
@@ -18,8 +22,8 @@ namespace MediaBrowser.Controller.Entities
 
     public enum LinkedChildType
     {
-        Manual = 1,
-        Shortcut = 2
+        Manual = 0,
+        Shortcut = 1
     }
 
     public class LinkedChildComparer : IEqualityComparer<LinkedChild>
@@ -35,7 +39,7 @@ namespace MediaBrowser.Controller.Entities
 
         public int GetHashCode(LinkedChild obj)
         {
-            return (obj.Path + obj.Type.ToString()).GetHashCode();
+            return (obj.Path + obj.Type).GetHashCode();
         }
     }
 }

+ 7 - 10
MediaBrowser.Controller/Entities/Person.cs

@@ -1,7 +1,7 @@
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
+using System;
 using System.Collections.Generic;
-using System.Runtime.Serialization;
+using System.Linq;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -10,19 +10,11 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class Person : BaseItem, IItemByName, IHasLookupInfo<PersonLookupInfo>
     {
-        public Person()
-        {
-            UserItemCountList = new List<ItemByNameCounts>();
-        }
-
         /// <summary>
         /// Gets or sets the place of birth.
         /// </summary>
         /// <value>The place of birth.</value>
         public string PlaceOfBirth { get; set; }
-
-        [IgnoreDataMember]
-        public List<ItemByNameCounts> UserItemCountList { get; set; }
         
         /// <summary>
         /// Gets the user data key.
@@ -62,6 +54,11 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
         }
+
+        public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
+        {
+            return inputItems.Where(i => i.People.Any(p => string.Equals(p.Name, Name, StringComparison.OrdinalIgnoreCase)));
+        }
     }
 
     /// <summary>

+ 7 - 11
MediaBrowser.Controller/Entities/Studio.cs

@@ -1,7 +1,6 @@
-using System.Runtime.Serialization;
-using MediaBrowser.Model.Dto;
-using System;
+using System;
 using System.Collections.Generic;
+using System.Linq;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -10,11 +9,6 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class Studio : BaseItem, IItemByName
     {
-        public Studio()
-        {
-            UserItemCountList = new List<ItemByNameCounts>();
-        }
-
         /// <summary>
         /// Gets the user data key.
         /// </summary>
@@ -24,9 +18,6 @@ namespace MediaBrowser.Controller.Entities
             return "Studio-" + Name;
         }
 
-        [IgnoreDataMember]
-        public List<ItemByNameCounts> UserItemCountList { get; set; }
-
         /// <summary>
         /// Returns the folder containing the item.
         /// If the item is a folder, it returns the folder itself
@@ -51,5 +42,10 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
         }
+
+        public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
+        {
+            return inputItems.Where(i => i.Studios.Contains(Name, StringComparer.OrdinalIgnoreCase));
+        }
     }
 }

+ 2 - 1
MediaBrowser.Controller/Entities/UserRootFolder.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.Providers;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 
@@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.Entities
         {
             var hasChanges = base.BeforeMetadataRefresh();
 
-            if (string.Equals("default", Name, System.StringComparison.OrdinalIgnoreCase))
+            if (string.Equals("default", Name, StringComparison.OrdinalIgnoreCase))
             {
                 Name = "Media Folders";
                 hasChanges = true;

+ 17 - 12
MediaBrowser.Controller/Entities/Year.cs

@@ -1,7 +1,6 @@
-using System.Runtime.Serialization;
-using MediaBrowser.Model.Dto;
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -10,14 +9,6 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class Year : BaseItem, IItemByName
     {
-        public Year()
-        {
-            UserItemCountList = new List<ItemByNameCounts>();
-        }
-
-        [IgnoreDataMember]
-        public List<ItemByNameCounts> UserItemCountList { get; set; }
-
         /// <summary>
         /// Gets the user data key.
         /// </summary>
@@ -51,5 +42,19 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
         }
+
+        public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
+        {
+            int year;
+
+            var usCulture = new CultureInfo("en-US"); 
+            
+            if (!int.TryParse(Name, NumberStyles.Integer, usCulture, out year))
+            {
+                return inputItems;
+            }
+
+            return inputItems.Where(i => i.ProductionYear.HasValue && i.ProductionYear.Value == year);
+        }
     }
 }

+ 0 - 13
MediaBrowser.Controller/Library/ILibraryManager.cs

@@ -304,19 +304,6 @@ namespace MediaBrowser.Controller.Library
         /// <returns>System.String.</returns>
         string FindCollectionType(BaseItem item);
 
-        /// <summary>
-        /// Gets all artists.
-        /// </summary>
-        /// <returns>IEnumerable{System.String}.</returns>
-        IEnumerable<string> GetAllArtists();
-
-        /// <summary>
-        /// Gets all artists.
-        /// </summary>
-        /// <param name="items">The items.</param>
-        /// <returns>IEnumerable{System.String}.</returns>
-        IEnumerable<string> GetAllArtists(IEnumerable<BaseItem> items);
-
         /// <summary>
         /// Normalizes the root path list.
         /// </summary>

+ 1 - 2
MediaBrowser.Controller/Library/IUserManager.cs

@@ -51,9 +51,8 @@ namespace MediaBrowser.Controller.Library
         /// Refreshes metadata for each user
         /// </summary>
         /// <param name="cancellationToken">The cancellation token.</param>
-        /// <param name="force">if set to <c>true</c> [force].</param>
         /// <returns>Task.</returns>
-        Task RefreshUsersMetadata(CancellationToken cancellationToken, bool force = false);
+        Task RefreshUsersMetadata(CancellationToken cancellationToken);
 
         /// <summary>
         /// Renames the user.

+ 80 - 80
MediaBrowser.Controller/Library/TVUtils.cs

@@ -27,94 +27,94 @@ namespace MediaBrowser.Controller.Library
         /// <summary>
         /// A season folder must contain one of these somewhere in the name
         /// </summary>
-        private static readonly string[] SeasonFolderNames = new[]
-                                                                 {
-                                                                     "season",
-                                                                     "sæson",
-                                                                     "temporada",
-                                                                     "saison",
-                                                                     "staffel",
-                                                                     "series",
-                                                                     "сезон"
-                                                                 };
+        private static readonly string[] SeasonFolderNames =
+        {
+            "season",
+            "sæson",
+            "temporada",
+            "saison",
+            "staffel",
+            "series",
+            "сезон"
+        };
 
         /// <summary>
         /// Used to detect paths that represent episodes, need to make sure they don't also
         /// match movie titles like "2001 A Space..."
         /// Currently we limit the numbers here to 2 digits to try and avoid this
         /// </summary>
-        private static readonly Regex[] EpisodeExpressions = new[]
-                                                                 {
-                                                                     new Regex(
-                                                                         @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})[^\\\/]*$",
-                                                                         RegexOptions.Compiled),
-                                                                     new Regex(
-                                                                         @".*(\\|\/)[sS](?<seasonnumber>\d{1,4})[x,X]?[eE](?<epnumber>\d{1,3})[^\\\/]*$",
-                                                                         RegexOptions.Compiled),
-                                                                     new Regex(
-                                                                         @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))[^\\\/]*$",
-                                                                         RegexOptions.Compiled),
-                                                                     new Regex(
-                                                                         @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})[^\\\/]*$",
-                                                                         RegexOptions.Compiled)
-                                                                 };
-        private static readonly Regex[] MultipleEpisodeExpressions = new[]
-                                                                 {
-                                                                     new Regex(
-                                                                         @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                                                                         RegexOptions.Compiled),
-                                                                     new Regex(
-                                                                         @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                                                                         RegexOptions.Compiled),
-                                                                     new Regex(
-                                                                         @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                                                                         RegexOptions.Compiled),
-                                                                     new Regex(
-                                                                         @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                                                                         RegexOptions.Compiled),
-                                                                     new Regex(
-                                                                         @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                                                                         RegexOptions.Compiled),
-                                                                     new Regex(
-                                                                         @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                                                                         RegexOptions.Compiled),
-                                                                     new Regex(
-                                                                         @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                                                                         RegexOptions.Compiled),
-                                                                     new Regex(
-                                                                         @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                                                                         RegexOptions.Compiled),
-                                                                     new Regex(
-                                                                         @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                                                                         RegexOptions.Compiled),
-                                                                     new Regex(
-                                                                         @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                                                                         RegexOptions.Compiled)
-                                                                 };
+        private static readonly Regex[] EpisodeExpressions =
+        {
+            new Regex(
+                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})[^\\\/]*$",
+                RegexOptions.Compiled),
+            new Regex(
+                @".*(\\|\/)[sS](?<seasonnumber>\d{1,4})[x,X]?[eE](?<epnumber>\d{1,3})[^\\\/]*$",
+                RegexOptions.Compiled),
+            new Regex(
+                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))[^\\\/]*$",
+                RegexOptions.Compiled),
+            new Regex(
+                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})[^\\\/]*$",
+                RegexOptions.Compiled)
+        };
+        private static readonly Regex[] MultipleEpisodeExpressions =
+        {
+            new Regex(
+                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
+                RegexOptions.Compiled),
+            new Regex(
+                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
+                RegexOptions.Compiled),
+            new Regex(
+                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
+                RegexOptions.Compiled),
+            new Regex(
+                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
+                RegexOptions.Compiled),
+            new Regex(
+                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
+                RegexOptions.Compiled),
+            new Regex(
+                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
+                RegexOptions.Compiled),
+            new Regex(
+                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
+                RegexOptions.Compiled),
+            new Regex(
+                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
+                RegexOptions.Compiled),
+            new Regex(
+                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
+                RegexOptions.Compiled),
+            new Regex(
+                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
+                RegexOptions.Compiled)
+        };
 
         /// <summary>
         /// To avoid the following matching movies they are only valid when contained in a folder which has been matched as a being season
         /// </summary>
-        private static readonly Regex[] EpisodeExpressionsInASeasonFolder = new[]
-                                                                                {
-                                                                                    new Regex(
-                                                                                        @".*(\\|\/)(?<epnumber>\d{1,2})\s?-\s?[^\\\/]*$",
-                                                                                        RegexOptions.Compiled),
-                                                                                    // 01 - blah.avi, 01-blah.avi
-                                                                                    new Regex(
-                                                                                        @".*(\\|\/)(?<epnumber>\d{1,2})[^\d\\]*[^\\\/]*$",
-                                                                                        RegexOptions.Compiled),
-                                                                                    // 01.avi, 01.blah.avi "01 - 22 blah.avi" 
-                                                                                    new Regex(
-                                                                                        @".*(\\|\/)(?<seasonnumber>\d)(?<epnumber>\d{1,2})[^\d\\]+[^\\\/]*$",
-                                                                                        RegexOptions.Compiled),
-                                                                                    // 01.avi, 01.blah.avi
-                                                                                    new Regex(
-                                                                                        @".*(\\|\/)\D*\d+(?<epnumber>\d{2})",
-                                                                                        RegexOptions.Compiled)
-                                                                                    // hell0 - 101 -  hello.avi
-
-                                                                                };
+        private static readonly Regex[] EpisodeExpressionsInASeasonFolder =
+        {
+            new Regex(
+                @".*(\\|\/)(?<epnumber>\d{1,2})\s?-\s?[^\\\/]*$",
+                RegexOptions.Compiled),
+            // 01 - blah.avi, 01-blah.avi
+            new Regex(
+                @".*(\\|\/)(?<epnumber>\d{1,2})[^\d\\]*[^\\\/]*$",
+                RegexOptions.Compiled),
+            // 01.avi, 01.blah.avi "01 - 22 blah.avi" 
+            new Regex(
+                @".*(\\|\/)(?<seasonnumber>\d)(?<epnumber>\d{1,2})[^\d\\]+[^\\\/]*$",
+                RegexOptions.Compiled),
+            // 01.avi, 01.blah.avi
+            new Regex(
+                @".*(\\|\/)\D*\d+(?<epnumber>\d{2})",
+                RegexOptions.Compiled)
+            // hell0 - 101 -  hello.avi
+
+        };
 
         /// <summary>
         /// Gets the season number from path.
@@ -151,8 +151,8 @@ namespace MediaBrowser.Controller.Library
         /// <returns>System.Nullable{System.Int32}.</returns>
         private static int? GetSeasonNumberFromPathSubstring(string path)
         {
-            int numericStart = -1;
-            int length = 0;
+            var numericStart = -1;
+            var length = 0;
 
             // Find out where the numbers start, and then keep going until they end
             for (var i = 0; i < path.Length; i++)

+ 0 - 7
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -138,13 +138,6 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="id">The identifier.</param>
         /// <returns>Channel.</returns>
         LiveTvChannel GetInternalChannel(string id);
-
-        /// <summary>
-        /// Gets the internal program.
-        /// </summary>
-        /// <param name="id">The identifier.</param>
-        /// <returns>LiveTvProgram.</returns>
-        LiveTvProgram GetInternalProgram(string id);
         
         /// <summary>
         /// Gets the recording.

+ 5 - 10
MediaBrowser.Controller/LiveTv/LiveTvChannel.cs

@@ -1,20 +1,13 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.LiveTv;
 using System.Collections.Generic;
-using System.Runtime.Serialization;
 using System.Linq;
 
 namespace MediaBrowser.Controller.LiveTv
 {
     public class LiveTvChannel : BaseItem, IItemByName
     {
-        public LiveTvChannel()
-        {
-            UserItemCountList = new List<ItemByNameCounts>();
-        }
-
         /// <summary>
         /// Gets the user data key.
         /// </summary>
@@ -24,9 +17,6 @@ namespace MediaBrowser.Controller.LiveTv
             return GetClientTypeName() + "-" + Name;
         }
 
-        [IgnoreDataMember]
-        public List<ItemByNameCounts> UserItemCountList { get; set; }
-
         /// <summary>
         /// Returns the folder containing the item.
         /// If the item is a folder, it returns the folder itself
@@ -119,5 +109,10 @@ namespace MediaBrowser.Controller.LiveTv
         {
             return "Channel";
         }
+
+        public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
+        {
+            return new List<BaseItem>();
+        }
     }
 }

+ 5 - 0
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -68,6 +68,11 @@
     <Compile Include="..\SharedVersion.cs">
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
+    <Compile Include="Channels\ChannelItemInfo.cs" />
+    <Compile Include="Channels\IChannel.cs" />
+    <Compile Include="Channels\IChannelManager.cs" />
+    <Compile Include="Collections\CollectionCreationOptions.cs" />
+    <Compile Include="Collections\ICollectionManager.cs" />
     <Compile Include="Drawing\IImageProcessor.cs" />
     <Compile Include="Drawing\ImageFormat.cs" />
     <Compile Include="Drawing\ImageProcessingOptions.cs" />

+ 12 - 0
MediaBrowser.Controller/Net/IHttpResultFactory.cs

@@ -95,6 +95,18 @@ namespace MediaBrowser.Controller.Net
         /// <returns>System.Object.</returns>
         object GetStaticFileResult(IRequest requestContext, string path, FileShare fileShare = FileShare.Read, IDictionary<string, string> responseHeaders = null, bool isHeadRequest = false);
 
+        /// <summary>
+        /// Gets the static file result.
+        /// </summary>
+        /// <param name="requestContext">The request context.</param>
+        /// <param name="path">The path.</param>
+        /// <param name="contentType">Type of the content.</param>
+        /// <param name="fileShare">The file share.</param>
+        /// <param name="responseHeaders">The response headers.</param>
+        /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
+        /// <returns>System.Object.</returns>
+        object GetStaticFileResult(IRequest requestContext, string path, string contentType, FileShare fileShare = FileShare.Read, IDictionary<string, string> responseHeaders = null, bool isHeadRequest = false);
+        
         /// <summary>
         /// Gets the optimized serialized result using cache.
         /// </summary>

+ 7 - 23
MediaBrowser.Controller/Providers/DirectoryService.cs

@@ -1,6 +1,6 @@
-using System.Collections.Concurrent;
-using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Logging;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
@@ -10,10 +10,8 @@ namespace MediaBrowser.Controller.Providers
     public interface IDirectoryService
     {
         List<FileSystemInfo> GetFileSystemEntries(string path);
-        IEnumerable<FileInfo> GetFiles(string path);
-        IEnumerable<DirectoryInfo> GetDirectories(string path);
-        FileInfo GetFile(string path);
-        DirectoryInfo GetDirectory(string path);
+        IEnumerable<FileSystemInfo> GetFiles(string path);
+        FileSystemInfo GetFile(string path);
     }
 
     public class DirectoryService : IDirectoryService
@@ -50,31 +48,17 @@ namespace MediaBrowser.Controller.Providers
             return entries;
         }
 
-        public IEnumerable<FileInfo> GetFiles(string path)
+        public IEnumerable<FileSystemInfo> GetFiles(string path)
         {
-            return GetFileSystemEntries(path).OfType<FileInfo>();
+            return GetFileSystemEntries(path).Where(i => (i.Attributes & FileAttributes.Directory) != FileAttributes.Directory);
         }
 
-        public IEnumerable<DirectoryInfo> GetDirectories(string path)
-        {
-            return GetFileSystemEntries(path).OfType<DirectoryInfo>();
-        }
-
-        public FileInfo GetFile(string path)
+        public FileSystemInfo GetFile(string path)
         {
             var directory = Path.GetDirectoryName(path);
             var filename = Path.GetFileName(path);
 
             return GetFiles(directory).FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase));
         }
-
-
-        public DirectoryInfo GetDirectory(string path)
-        {
-            var directory = Path.GetDirectoryName(path);
-            var name = Path.GetFileName(path);
-
-            return GetDirectories(directory).FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
-        }
     }
 }

+ 1 - 1
MediaBrowser.Controller/Providers/ILocalImageProvider.cs

@@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.Providers
 
     public class LocalImageInfo
     {
-        public FileInfo FileInfo { get; set; }
+        public FileSystemInfo FileInfo { get; set; }
         public ImageType Type { get; set; }
     }
 

+ 7 - 0
MediaBrowser.Controller/Session/ISessionManager.cs

@@ -76,6 +76,13 @@ namespace MediaBrowser.Controller.Session
         /// <exception cref="System.ArgumentNullException"></exception>
         Task OnPlaybackStopped(PlaybackStopInfo info);
 
+        /// <summary>
+        /// Reports the session ended.
+        /// </summary>
+        /// <param name="sessionId">The session identifier.</param>
+        /// <returns>Task.</returns>
+        Task ReportSessionEnded(Guid sessionId);
+
         /// <summary>
         /// Sends the system command.
         /// </summary>

+ 1 - 1
MediaBrowser.Dlna/MediaBrowser.Dlna.csproj

@@ -53,7 +53,7 @@
     </Compile>
     <Compile Include="PlayTo\Argument.cs" />
     <Compile Include="PlayTo\Configuration\DlnaProfile.cs" />
-    <Compile Include="PlayTo\Configuration\PluginConfiguration.cs" />
+    <Compile Include="PlayTo\Configuration\PlayToConfiguration.cs" />
     <Compile Include="PlayTo\Configuration\TranscodeSetting.cs" />
     <Compile Include="PlayTo\CurrentIdEventArgs.cs" />
     <Compile Include="PlayTo\Device.cs">

+ 18 - 3
MediaBrowser.Dlna/PlayTo/Configuration/PluginConfiguration.cs → MediaBrowser.Dlna/PlayTo/Configuration/PlayToConfiguration.cs

@@ -29,7 +29,12 @@
                 FriendlyName = "^TV$",
                 ModelNumber = @"1\.0",
                 ModelName = "Samsung DTV DMR",
-                TranscodeSettings = TranscodeSettings.GetDefaultTranscodingSettings()
+                TranscodeSettings = new[]
+                {
+                    new TranscodeSettings {Container = "mkv", MimeType = "x-mkv"},
+                    new TranscodeSettings {Container = "flac", TargetContainer = "mp3"},
+                    new TranscodeSettings {Container = "m4a", TargetContainer = "mp3"}
+                }
             };
 
             var profile1 = new DlnaProfile
@@ -38,7 +43,12 @@
                 ClientType = "DLNA",
                 FriendlyName = @"(^\[TV\][A-Z]{2}\d{2}(E|F)[A-Z]?\d{3,4}.*)|^\[TV\] Samsung",
                 ModelNumber = @"(1\.0)|(AllShare1\.0)",
-                TranscodeSettings = TranscodeSettings.GetDefaultTranscodingSettings()
+                TranscodeSettings = new[]
+                {
+                    new TranscodeSettings {Container = "mkv", MimeType = "x-mkv"},
+                    new TranscodeSettings {Container = "flac", TargetContainer = "mp3"},
+                    new TranscodeSettings {Container = "m4a", TargetContainer = "mp3"}
+                }
             };
 
             var profile2 = new DlnaProfile
@@ -47,7 +57,12 @@
                 ClientType = "DLNA",
                 FriendlyName = @"(^TV-\d{2}C\d{3}.*)|(^\[TV\][A-Z]{2}\d{2}(D)[A-Z]?\d{3,4}.*)|^\[TV\] Samsung",
                 ModelNumber = @"(1\.0)|(AllShare1\.0)",
-                TranscodeSettings = TranscodeSettings.GetDefaultTranscodingSettings()
+                TranscodeSettings = new[]
+                {
+                    new TranscodeSettings {Container = "mkv", MimeType = "x-mkv"},
+                    new TranscodeSettings {Container = "flac", TargetContainer = "mp3"},
+                    new TranscodeSettings {Container = "m4a", TargetContainer = "mp3"}
+                }
             };
 
             var profile3 = new DlnaProfile

+ 12 - 3
MediaBrowser.Dlna/PlayTo/Configuration/TranscodeSetting.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Text.RegularExpressions;
 
 namespace MediaBrowser.Dlna.PlayTo.Configuration
 {
@@ -20,6 +21,14 @@ namespace MediaBrowser.Dlna.PlayTo.Configuration
         /// </value>
         public string TargetContainer { get; set; }
 
+        /// <summary>
+        /// Gets or sets the Mimetype to enforce
+        /// </summary>
+        /// <value>
+        /// The MimeType.
+        /// </value>
+        public string MimeType { get; set; }
+
         /// <summary>
         /// The default transcoding settings
         /// </summary>
@@ -46,19 +55,19 @@ namespace MediaBrowser.Dlna.PlayTo.Configuration
             {
                 if (!string.IsNullOrEmpty(profile.FriendlyName))
                 {
-                    if (!string.Equals(deviceProperties.Name, profile.FriendlyName, StringComparison.OrdinalIgnoreCase))
+                    if (!Regex.IsMatch(deviceProperties.Name, profile.FriendlyName))
                         continue;
                 }
 
                 if (!string.IsNullOrEmpty(profile.ModelNumber))
                 {
-                    if (!string.Equals(deviceProperties.ModelNumber, profile.ModelNumber, StringComparison.OrdinalIgnoreCase))
+                    if (!Regex.IsMatch(deviceProperties.ModelNumber, profile.ModelNumber))
                         continue;
                 }
 
                 if (!string.IsNullOrEmpty(profile.ModelName))
                 {
-                    if (!string.Equals(deviceProperties.ModelName, profile.ModelName, StringComparison.OrdinalIgnoreCase))
+                    if (!Regex.IsMatch(deviceProperties.ModelName, profile.ModelName))
                         continue;
                 }
 

+ 68 - 28
MediaBrowser.Dlna/PlayTo/Device.cs

@@ -76,8 +76,8 @@ namespace MediaBrowser.Dlna.PlayTo
 
                 _transportState = value;
 
-                if (value == "PLAYING" || value == "STOPPED")
-                    NotifyPlaybackChanged(value == "STOPPED");
+                if (value == TRANSPORTSTATE.PLAYING || value == TRANSPORTSTATE.STOPPED)
+                    NotifyPlaybackChanged(value == TRANSPORTSTATE.STOPPED);
             }
         }
 
@@ -85,7 +85,7 @@ namespace MediaBrowser.Dlna.PlayTo
         {
             get
             {
-                return TransportState == "PLAYING";
+                return TransportState == TRANSPORTSTATE.PLAYING;
             }
         }
 
@@ -93,7 +93,7 @@ namespace MediaBrowser.Dlna.PlayTo
         {
             get
             {
-                return (TransportState == "TRANSITIONING");
+                return (TransportState == TRANSPORTSTATE.TRANSITIONING);
             }
         }
 
@@ -101,7 +101,7 @@ namespace MediaBrowser.Dlna.PlayTo
         {
             get
             {
-                return TransportState == "PAUSED" || TransportState == "PAUSED_PLAYBACK";
+                return TransportState == TRANSPORTSTATE.PAUSED || TransportState == TRANSPORTSTATE.PAUSED_PLAYBACK;
             }
         }
 
@@ -109,7 +109,7 @@ namespace MediaBrowser.Dlna.PlayTo
         {
             get
             {
-                return (TransportState == "STOPPED");
+                return TransportState == TRANSPORTSTATE.STOPPED;
             }
         }
 
@@ -127,23 +127,39 @@ namespace MediaBrowser.Dlna.PlayTo
             _logger = logger;
         }
 
-        private int GetTimerIntervalMs()
+        private int GetPlaybackTimerIntervalMs()
         {
-            return 10000;
+            return 2000;
+        }
+
+        private int GetInactiveTimerIntervalMs()
+        {
+            return 20000;
         }
 
         public void Start()
         {
             UpdateTime = DateTime.UtcNow;
 
-            var interval = GetTimerIntervalMs();
+            var interval = GetPlaybackTimerIntervalMs();
 
             _timer = new Timer(TimerCallback, null, interval, interval);
         }
 
         private void RestartTimer()
         {
-            var interval = GetTimerIntervalMs();
+            var interval = GetPlaybackTimerIntervalMs();
+
+            _timer.Change(interval, interval);
+        }
+
+
+        /// <summary>
+        /// Restarts the timer in inactive mode.
+        /// </summary>
+        private void RestartTimerInactive()
+        {
+            var interval = GetInactiveTimerIntervalMs();
 
             _timer.Change(interval, interval);
         }
@@ -230,11 +246,9 @@ namespace MediaBrowser.Dlna.PlayTo
         {
             StopTimer();
 
-            TransportState = "STOPPED";
+            await SetStop().ConfigureAwait(false);
             CurrentId = "0";
 
-            await Task.Delay(50).ConfigureAwait(false);
-
             var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
             if (command == null)
                 return false;
@@ -261,7 +275,7 @@ namespace MediaBrowser.Dlna.PlayTo
                 await SetPlay().ConfigureAwait(false);
             }
 
-            _count = 5;
+            _lapsCount = GetLapsCount();
             RestartTimer();
 
             return true;
@@ -322,7 +336,7 @@ namespace MediaBrowser.Dlna.PlayTo
             var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, 1))
                 .ConfigureAwait(false);
 
-            _count = 5;
+            _lapsCount = GetLapsCount();
             return true;
         }
 
@@ -338,7 +352,6 @@ namespace MediaBrowser.Dlna.PlayTo
                 .ConfigureAwait(false);
 
             await Task.Delay(50).ConfigureAwait(false);
-            _count = 4;
             return true;
         }
 
@@ -362,8 +375,13 @@ namespace MediaBrowser.Dlna.PlayTo
 
         #region Get data
 
-        // TODO: What is going on here
-        int _count = 5;
+        private int GetLapsCount()
+        {
+            // No need to get all data every lap, just every X time. 
+            return 10;
+        }
+
+        int _lapsCount = 0;
 
         private async void TimerCallback(object sender)
         {
@@ -374,18 +392,24 @@ namespace MediaBrowser.Dlna.PlayTo
 
             try
             {
-                var hasTrack = await GetPositionInfo().ConfigureAwait(false);
+                await GetTransportInfo().ConfigureAwait(false);
 
-                // TODO: Why make these requests if hasTrack==false?
-                if (_count > 5)
+                //If we're not playing anything no need to get additional data
+                if (TransportState != TRANSPORTSTATE.STOPPED)
                 {
-                    await GetTransportInfo().ConfigureAwait(false);
-                    if (!hasTrack)
+                    var hasTrack = await GetPositionInfo().ConfigureAwait(false);
+
+                    // TODO: Why make these requests if hasTrack==false?
+                    // TODO ANSWER Some vendors don't include track in GetPositionInfo, use GetMediaInfo instead.
+                    if (_lapsCount > GetLapsCount())
                     {
-                        await GetMediaInfo().ConfigureAwait(false);
+                        if (!hasTrack)
+                        {
+                            await GetMediaInfo().ConfigureAwait(false);
+                        }
+                        await GetVolume().ConfigureAwait(false);
+                        _lapsCount = 0;
                     }
-                    await GetVolume().ConfigureAwait(false);
-                    _count = 0;
                 }
             }
             catch (Exception ex)
@@ -393,11 +417,16 @@ namespace MediaBrowser.Dlna.PlayTo
                 _logger.ErrorException("Error updating device info", ex);
             }
 
-            _count++;
+            _lapsCount++;
+
             if (_disposed)
                 return;
 
-            RestartTimer();
+            //If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
+            if (TransportState != TRANSPORTSTATE.STOPPED)
+                RestartTimer();
+            else
+                RestartTimerInactive();
         }
 
         private async Task GetVolume()
@@ -747,5 +776,16 @@ namespace MediaBrowser.Dlna.PlayTo
         {
             return String.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);
         }
+
+        private class TRANSPORTSTATE
+        {
+            public const string STOPPED = "STOPPED";
+            public const string PLAYING = "PLAYING";
+            public const string TRANSITIONING = "TRANSITIONING";
+            public const string PAUSED_PLAYBACK = "PAUSED_PLAYBACK";
+            public const string PAUSED = "PAUSED";
+        }
+
     }
 }
+

+ 10 - 1
MediaBrowser.Dlna/PlayTo/DlnaController.cs

@@ -17,7 +17,7 @@ using Timer = System.Timers.Timer;
 
 namespace MediaBrowser.Dlna.PlayTo
 {
-    public class PlayToController : ISessionController
+    public class PlayToController : ISessionController, IDisposable
     {
         private Device _device;
         private BaseItem _currentItem = null;
@@ -131,6 +131,14 @@ namespace MediaBrowser.Dlna.PlayTo
 
             ((Timer)sender).Stop();
 
+
+            if (!IsSessionActive)
+            {
+                //Session is inactive, mark it for Disposal and don't start the elapsed timer.
+                await _sessionManager.ReportSessionEnded(this._session.Id);
+                return;
+            }
+
             await ReportProgress().ConfigureAwait(false);
 
             if (!_disposed && IsSessionActive)
@@ -479,3 +487,4 @@ namespace MediaBrowser.Dlna.PlayTo
         }
     }
 }
+

+ 79 - 12
MediaBrowser.Dlna/PlayTo/PlayToServerEntryPoint.cs

@@ -1,40 +1,107 @@
 using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Logging;
+using System;
 
 namespace MediaBrowser.Dlna.PlayTo
 {
     public class PlayToServerEntryPoint : IServerEntryPoint
     {
-        private bool _disposed;
+        private  PlayToManager _manager;
+        private readonly IServerConfigurationManager _config;
+        private readonly ILogger _logger;
+        private readonly ISessionManager _sessionManager;
+        private readonly IHttpClient _httpClient;
+        private readonly IItemRepository _itemRepo;
+        private readonly ILibraryManager _libraryManager;
+        private readonly INetworkManager _networkManager;
+        private readonly IUserManager _userManager;
 
-        private readonly PlayToManager _manager;
-
-        public PlayToServerEntryPoint(ILogManager logManager, ISessionManager sessionManager, IUserManager userManager, IHttpClient httpClient, INetworkManager networkManager, IItemRepository itemRepository, ILibraryManager libraryManager)
+        public PlayToServerEntryPoint(ILogManager logManager, IServerConfigurationManager config, ISessionManager sessionManager, IHttpClient httpClient, IItemRepository itemRepo, ILibraryManager libraryManager, INetworkManager networkManager, IUserManager userManager)
         {
-            _manager = new PlayToManager(logManager.GetLogger("PlayTo"), sessionManager, httpClient, itemRepository, libraryManager, networkManager, userManager);
+            _config = config;
+            _sessionManager = sessionManager;
+            _httpClient = httpClient;
+            _itemRepo = itemRepo;
+            _libraryManager = libraryManager;
+            _networkManager = networkManager;
+            _userManager = userManager;
+            _logger = logManager.GetLogger("PlayTo");
         }
 
         public void Run()
         {
-            //_manager.Start();
+            _config.ConfigurationUpdated += ConfigurationUpdated;
+            ReloadPlayToManager();
         }
 
-        #region Dispose
+        void ConfigurationUpdated(object sender, EventArgs e)
+        {
+            ReloadPlayToManager();
+        }
 
-        public void Dispose()
+        private void ReloadPlayToManager()
+        {
+            var isStarted = _manager != null;
+
+            if (_config.Configuration.DlnaOptions.EnablePlayTo && !isStarted)
+            {
+                StartPlayToManager();
+            }
+            else if (!_config.Configuration.DlnaOptions.EnablePlayTo && isStarted)
+            {
+                DisposePlayToManager();
+            }
+        }
+
+        private readonly object _syncLock = new object();
+        private void StartPlayToManager()
+        {
+            lock (_syncLock)
+            {
+                try
+                {
+                    _manager = new PlayToManager(_logger, _sessionManager, _httpClient, _itemRepo, _libraryManager, _networkManager, _userManager);
+                    _manager.Start();
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error starting PlayTo manager", ex);
+                }
+            }
+        }
+
+        private void DisposePlayToManager()
         {
-            if (!_disposed)
+            lock (_syncLock)
             {
-                _disposed = true;
-                _manager.Stop();
-                _manager.Dispose();
+                if (_manager != null)
+                {
+                    try
+                    {
+                        _manager.Stop();
+                        _manager.Dispose();
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.ErrorException("Error disposing PlayTo manager", ex);
+                    }
+                    _manager = null;
+                }
             }
         }
 
+        #region Dispose
+
+        public void Dispose()
+        {
+            DisposePlayToManager();
+        }
+
         #endregion
     }
 }

+ 14 - 1
MediaBrowser.Dlna/PlayTo/PlaylistItem.cs

@@ -16,6 +16,8 @@ namespace MediaBrowser.Dlna.PlayTo
 
         public string FileFormat { get; set; }
 
+        public string MimeType { get; set; }
+
         public int PlayState { get; set; }
 
         public string StreamUrl { get; set; }
@@ -51,10 +53,21 @@ namespace MediaBrowser.Dlna.PlayTo
                 {
                     if (string.IsNullOrWhiteSpace(transcodeSetting.Container))
                         continue;
-                    if (path.EndsWith(transcodeSetting.Container))
+                    if (path.EndsWith(transcodeSetting.Container) && !string.IsNullOrWhiteSpace(transcodeSetting.TargetContainer))
                     {
                         playlistItem.Transcode = true;
                         playlistItem.FileFormat = transcodeSetting.TargetContainer;
+                        
+                        if (string.IsNullOrWhiteSpace(transcodeSetting.MimeType))
+                            playlistItem.MimeType = transcodeSetting.MimeType;
+                        
+                        return playlistItem;
+                    }
+                    if (path.EndsWith(transcodeSetting.Container) && !string.IsNullOrWhiteSpace(transcodeSetting.MimeType))
+                    {
+                        playlistItem.Transcode = false;
+                        playlistItem.FileFormat = transcodeSetting.Container;
+                        playlistItem.MimeType = transcodeSetting.MimeType;
                         return playlistItem;
                     }
                 }

+ 9 - 5
MediaBrowser.Dlna/PlayTo/StreamHelper.cs

@@ -96,9 +96,12 @@ namespace MediaBrowser.Dlna.PlayTo
         /// <returns>The url to send to the device</returns>
         internal static string GetVideoUrl(DeviceProperties deviceProperties, PlaylistItem item, List<MediaStream> streams, string serverAddress)
         {
+            string dlnaCommand = string.Empty;
             if (!item.Transcode)
-                return string.Format("{0}/Videos/{1}/stream.{2}?Static=True", serverAddress, item.ItemId, item.FileFormat);
-
+            {
+                dlnaCommand = BuildDlnaUrl(deviceProperties.UUID, !item.Transcode, null, null, null, null, null, null, null, null, null, null, item.MimeType);
+                return string.Format("{0}/Videos/{1}/stream.{2}?{3}", serverAddress, item.ItemId, item.FileFormat, dlnaCommand);                
+            }
             var videostream = streams.Where(m => m.Type == MediaStreamType.Video).OrderBy(m => m.IsDefault).FirstOrDefault();
             var audiostream = streams.Where(m => m.Type == MediaStreamType.Audio).OrderBy(m => m.IsDefault).FirstOrDefault();
 
@@ -117,7 +120,7 @@ namespace MediaBrowser.Dlna.PlayTo
                 audioChannels = 2;
             }
 
-            string dlnaCommand = BuildDlnaUrl(deviceProperties.UUID, videoCodec, audioCodec, null, null, videoBitrate, audioChannels, audioBitrate, item.StartPositionTicks, "baseline", "3");
+            dlnaCommand = BuildDlnaUrl(deviceProperties.UUID, !item.Transcode, videoCodec, audioCodec, null, null, videoBitrate, audioChannels, audioBitrate, item.StartPositionTicks, "baseline", "3", item.MimeType);
             return string.Format("{0}/Videos/{1}/stream.{2}?{3}", serverAddress, item.ItemId, item.FileFormat, dlnaCommand);
         }
 
@@ -162,12 +165,12 @@ namespace MediaBrowser.Dlna.PlayTo
         /// <summary>
         /// Builds the dlna URL.
         /// </summary>
-        private static string BuildDlnaUrl(string deviceID, VideoCodecs? videoCodec, AudioCodecs? audioCodec, int? subtitleIndex, int? audiostreamIndex, int? videoBitrate, int? audiochannels, int? audioBitrate, long? startPositionTicks, string profile, string videoLevel)
+        private static string BuildDlnaUrl(string deviceID, bool isStatic, VideoCodecs? videoCodec, AudioCodecs? audioCodec, int? subtitleIndex, int? audiostreamIndex, int? videoBitrate, int? audiochannels, int? audioBitrate, long? startPositionTicks, string profile, string videoLevel, string mimeType)
         {
             var usCulture = new CultureInfo("en-US");
 
             var dlnaparam = string.Format("Params={0};", deviceID);
-
+            dlnaparam += isStatic ? "true;" : "false;";
             dlnaparam += videoCodec.HasValue ? videoCodec.Value + ";" : ";";
             dlnaparam += audioCodec.HasValue ? audioCodec.Value + ";" : ";";
             dlnaparam += audiostreamIndex.HasValue ? audiostreamIndex.Value.ToString(usCulture) + ";" : ";";
@@ -178,6 +181,7 @@ namespace MediaBrowser.Dlna.PlayTo
             dlnaparam += startPositionTicks.HasValue ? startPositionTicks.Value.ToString(usCulture) + ";" : ";";
             dlnaparam += profile + ";";
             dlnaparam += videoLevel + ";";
+            dlnaparam += mimeType + ";";
 
             return dlnaparam;
         }

+ 6 - 0
MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj

@@ -80,6 +80,9 @@
     <Compile Include="..\MediaBrowser.Model\Configuration\BaseApplicationConfiguration.cs">
       <Link>Configuration\BaseApplicationConfiguration.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Configuration\DlnaOptions.cs">
+      <Link>Configuration\DlnaOptions.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Configuration\ManualLoginCategory.cs">
       <Link>Configuration\ManualLoginCategory.cs</Link>
     </Compile>
@@ -131,6 +134,9 @@
     <Compile Include="..\MediaBrowser.Model\Dto\ItemIndex.cs">
       <Link>Dto\ItemIndex.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Dto\RecommendationDto.cs">
+      <Link>Dto\RecommendationDto.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Dto\StreamOptions.cs">
       <Link>Dto\StreamOptions.cs</Link>
     </Compile>

+ 6 - 0
MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj

@@ -67,6 +67,9 @@
     <Compile Include="..\MediaBrowser.Model\Configuration\BaseApplicationConfiguration.cs">
       <Link>Configuration\BaseApplicationConfiguration.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Configuration\DlnaOptions.cs">
+      <Link>Configuration\DlnaOptions.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Configuration\ManualLoginCategory.cs">
       <Link>Configuration\ManualLoginCategory.cs</Link>
     </Compile>
@@ -118,6 +121,9 @@
     <Compile Include="..\MediaBrowser.Model\Dto\ItemIndex.cs">
       <Link>Dto\ItemIndex.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Dto\RecommendationDto.cs">
+      <Link>Dto\RecommendationDto.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Dto\StreamOptions.cs">
       <Link>Dto\StreamOptions.cs</Link>
     </Compile>

+ 80 - 10
MediaBrowser.Model/ApiClient/IApiClient.cs

@@ -244,7 +244,7 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="query">The query.</param>
         /// <returns>Task{ItemsResult}.</returns>
         Task<ItemsResult> GetSeasonsAsync(SeasonQuery query);
-        
+
         /// <summary>
         /// Queries for items
         /// </summary>
@@ -346,7 +346,14 @@ namespace MediaBrowser.Model.ApiClient
         /// </summary>
         /// <param name="query">The query.</param>
         /// <returns>Task{ItemsResult}.</returns>
-        Task<ItemsResult> GetNextUpAsync(NextUpQuery query);
+        Task<ItemsResult> GetNextUpEpisodesAsync(NextUpQuery query);
+
+        /// <summary>
+        /// Gets the upcoming episodes asynchronous.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <returns>Task{ItemsResult}.</returns>
+        Task<ItemsResult> GetUpcomingEpisodesAsync(NextUpQuery query);
 
         /// <summary>
         /// Gets a genre
@@ -772,7 +779,7 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="options">The options.</param>
         /// <returns>System.String.</returns>
         string GetImageUrl(ProgramInfoDto item, ImageOptions options);
-        
+
         /// <summary>
         /// Gets an image url that can be used to download an image from the api
         /// </summary>
@@ -902,7 +909,7 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="options">The options.</param>
         /// <returns>System.String.</returns>
         string GetThumbImageUrl(BaseItemDto item, ImageOptions options);
-        
+
         /// <summary>
         /// Gets the url needed to stream an audio file
         /// </summary>
@@ -958,7 +965,7 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{ChannelInfoDto}.</returns>
         Task<ChannelInfoDto> GetLiveTvChannelAsync(string id, string userId, CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Gets the live tv recordings asynchronous.
         /// </summary>
@@ -975,7 +982,7 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{RecordingInfoDto}.</returns>
         Task<RecordingInfoDto> GetLiveTvRecordingAsync(string id, string userId, CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Gets the live tv recording groups asynchronous.
         /// </summary>
@@ -992,7 +999,7 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{RecordingGroupDto}.</returns>
         Task<RecordingGroupDto> GetLiveTvRecordingGroupAsync(string id, string userId, CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Gets the live tv timers asynchronous.
         /// </summary>
@@ -1009,6 +1016,15 @@ namespace MediaBrowser.Model.ApiClient
         /// <returns>Task{QueryResult{ProgramInfoDto}}.</returns>
         Task<QueryResult<ProgramInfoDto>> GetLiveTvProgramsAsync(ProgramQuery query, CancellationToken cancellationToken);
 
+        /// <summary>
+        /// Gets the live tv program asynchronous.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <param name="userId">The user identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{ProgramInfoDto}.</returns>
+        Task<ProgramInfoDto> GetLiveTvProgramAsync(string id, string userId, CancellationToken cancellationToken);
+
         /// <summary>
         /// Gets the recommended live tv programs asynchronous.
         /// </summary>
@@ -1016,7 +1032,39 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{QueryResult{ProgramInfoDto}}.</returns>
         Task<QueryResult<ProgramInfoDto>> GetRecommendedLiveTvProgramsAsync(RecommendedProgramQuery query, CancellationToken cancellationToken);
-        
+
+        /// <summary>
+        /// Creates the live tv timer asynchronous.
+        /// </summary>
+        /// <param name="timer">The timer.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task CreateLiveTvTimerAsync(TimerInfoDto timer, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Updates the live tv timer asynchronous.
+        /// </summary>
+        /// <param name="timer">The timer.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task UpdateLiveTvTimerAsync(TimerInfoDto timer, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Creates the live tv series timer asynchronous.
+        /// </summary>
+        /// <param name="timer">The timer.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task CreateLiveTvSeriesTimerAsync(SeriesTimerInfoDto timer, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Updates the live tv series timer asynchronous.
+        /// </summary>
+        /// <param name="timer">The timer.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task UpdateLiveTvSeriesTimerAsync(SeriesTimerInfoDto timer, CancellationToken cancellationToken);
+
         /// <summary>
         /// Gets the live tv timer asynchronous.
         /// </summary>
@@ -1024,7 +1072,7 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{TimerInfoDto}.</returns>
         Task<TimerInfoDto> GetLiveTvTimerAsync(string id, CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Gets the live tv series timers asynchronous.
         /// </summary>
@@ -1056,7 +1104,7 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         Task CancelLiveTvSeriesTimerAsync(string id, CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Deletes the live tv recording asynchronous.
         /// </summary>
@@ -1064,5 +1112,27 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         Task DeleteLiveTvRecordingAsync(string id, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the default timer information.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{SeriesTimerInfoDto}.</returns>
+        Task<SeriesTimerInfoDto> GetDefaultLiveTvTimerInfo(CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the live tv guide information.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{GuideInfo}.</returns>
+        Task<GuideInfo> GetLiveTvGuideInfo(CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the default timer information.
+        /// </summary>
+        /// <param name="programId">The program identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{SeriesTimerInfoDto}.</returns>
+        Task<SeriesTimerInfoDto> GetDefaultLiveTvTimerInfo(string programId, CancellationToken cancellationToken);
     }
 }

+ 4 - 0
MediaBrowser.Model/Configuration/AutoOrganize.cs

@@ -19,6 +19,8 @@ namespace MediaBrowser.Model.Configuration
 
         public bool DeleteEmptyFolders { get; set; }
 
+        public bool CopyOriginalFile { get; set; }
+
         public TvFileOrganizationOptions()
         {
             MinFileSizeMb = 50;
@@ -31,6 +33,8 @@ namespace MediaBrowser.Model.Configuration
             MultiEpisodeNamePattern = "%sn - %sx%0e-x%0ed - %en.%ext";
             SeasonFolderPattern = "Season %s";
             SeasonZeroFolderName = "Season 0";
+
+            CopyOriginalFile = false;
         }
     }
 }

+ 8 - 0
MediaBrowser.Model/Configuration/DlnaOptions.cs

@@ -0,0 +1,8 @@
+
+namespace MediaBrowser.Model.Configuration
+{
+    public class DlnaOptions
+    {
+        public bool EnablePlayTo { get; set; }
+    }
+}

+ 4 - 0
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -212,6 +212,8 @@ namespace MediaBrowser.Model.Configuration
         public string ServerName { get; set; }
         public string WanDdns { get; set; }
 
+        public DlnaOptions DlnaOptions { get; set; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
         /// </summary>
@@ -271,6 +273,8 @@ namespace MediaBrowser.Model.Configuration
             };
 
             MetadataOptions = options.ToArray();
+
+            DlnaOptions = new DlnaOptions();
         }
     }
 

+ 29 - 0
MediaBrowser.Model/Dto/RecommendationDto.cs

@@ -0,0 +1,29 @@
+
+namespace MediaBrowser.Model.Dto
+{
+    public class RecommendationDto
+    {
+        public BaseItemDto[] Items { get; set; }
+
+        public RecommendationType RecommendationType { get; set; }
+
+        public string BaselineItemName { get; set; }
+
+        public string CategoryId { get; set; }
+    }
+
+    public enum RecommendationType
+    {
+        SimilarToRecentlyPlayed = 0,
+
+        SimilarToLikedItem = 1,
+
+        HasDirectorFromRecentlyPlayed = 2,
+
+        HasActorFromRecentlyPlayed = 3,
+
+        HasLikedDirector = 4,
+
+        HasLikedActor = 5
+    }
+}

+ 24 - 0
MediaBrowser.Model/Entities/BaseItemInfo.cs

@@ -46,6 +46,30 @@ namespace MediaBrowser.Model.Entities
         /// <value>The primary image tag.</value>
         public Guid? PrimaryImageTag { get; set; }
 
+        /// <summary>
+        /// Gets or sets the thumb image tag.
+        /// </summary>
+        /// <value>The thumb image tag.</value>
+        public Guid? ThumbImageTag { get; set; }
+
+        /// <summary>
+        /// Gets or sets the thumb item identifier.
+        /// </summary>
+        /// <value>The thumb item identifier.</value>
+        public string ThumbItemId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the thumb image tag.
+        /// </summary>
+        /// <value>The thumb image tag.</value>
+        public Guid? BackdropImageTag { get; set; }
+
+        /// <summary>
+        /// Gets or sets the thumb item identifier.
+        /// </summary>
+        /// <value>The thumb item identifier.</value>
+        public string BackdropItemId { get; set; }
+        
         /// <summary>
         /// Gets a value indicating whether this instance has primary image.
         /// </summary>

+ 2 - 0
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -60,6 +60,7 @@
     <Compile Include="ApiClient\ServerEventArgs.cs" />
     <Compile Include="Configuration\AutoOrganize.cs" />
     <Compile Include="Configuration\BaseApplicationConfiguration.cs" />
+    <Compile Include="Configuration\DlnaOptions.cs" />
     <Compile Include="Configuration\ManualLoginCategory.cs" />
     <Compile Include="Configuration\MetadataPlugin.cs" />
     <Compile Include="Configuration\MetadataOptions.cs" />
@@ -73,6 +74,7 @@
     <Compile Include="Dto\ItemByNameCounts.cs" />
     <Compile Include="Dto\ItemCounts.cs" />
     <Compile Include="Dto\ItemIndex.cs" />
+    <Compile Include="Dto\RecommendationDto.cs" />
     <Compile Include="Entities\PackageReviewInfo.cs" />
     <Compile Include="FileOrganization\FileOrganizationResult.cs" />
     <Compile Include="FileOrganization\FileOrganizationQuery.cs" />

+ 12 - 0
MediaBrowser.Model/Querying/ItemQuery.cs

@@ -218,6 +218,18 @@ namespace MediaBrowser.Model.Querying
         /// <value>The name starts with or greater.</value>
         public string NameStartsWithOrGreater { get; set; }
 
+        /// <summary>
+        /// Gets or sets the name starts with.
+        /// </summary>
+        /// <value>The name starts with or greater.</value>
+        public string NameStartsWith { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the name starts with.
+        /// </summary>
+        /// <value>The name lessthan.</value>
+        public string NameLessThan { get; set; }
+
         /// <summary>
         /// Gets or sets the album artist starts with or greater.
         /// </summary>

+ 3 - 7
MediaBrowser.Model/Querying/ItemSortBy.cs

@@ -75,16 +75,12 @@ namespace MediaBrowser.Model.Querying
         public const string IsFolder = "IsFolder";
         public const string IsUnplayed = "IsUnplayed";
         public const string IsPlayed = "IsPlayed";
-        public const string TrailerCount = "TrailerCount";
-        public const string MovieCount = "MovieCount";
-        public const string SeriesCount = "SeriesCount";
-        public const string EpisodeCount = "EpisodeCount";
-        public const string SongCount = "SongCount";
-        public const string AlbumCount = "AlbumCount";
-        public const string MusicVideoCount = "MusicVideoCount";
         public const string SeriesSortName = "SeriesSortName";
         public const string VideoBitRate = "VideoBitRate";
         public const string AirTime = "AirTime";
         public const string Metascore = "Metascore";
+        public const string Studio = "Studio";
+        public const string Players = "Players";
+        public const string GameSystem = "GameSystem";
     }
 }

+ 4 - 0
MediaBrowser.Model/Querying/ItemsByNameQuery.cs

@@ -86,6 +86,10 @@ namespace MediaBrowser.Model.Querying
         public string NameStartsWithOrGreater { get; set; }
 
         /// <summary>
+        /// Gets or sets the name starts with
+        /// </summary>
+        /// <value>The name starts with or greater.</value>
+        public string NameStartsWith { get; set; }
         /// Gets or sets the name less than.
         /// </summary>
         /// <value>The name less than.</value>

+ 28 - 0
MediaBrowser.Model/Querying/NextUpQuery.cs

@@ -33,4 +33,32 @@ namespace MediaBrowser.Model.Querying
         /// <value>The fields.</value>
         public ItemFields[] Fields { get; set; }
     }
+
+    public class UpcomingEpisodesQuery
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public string UserId { get; set; }
+
+        /// <summary>
+        /// Skips over a given number of items within the results. Use for paging.
+        /// </summary>
+        /// <value>The start index.</value>
+        public int? StartIndex { get; set; }
+
+        /// <summary>
+        /// The maximum number of items to return
+        /// </summary>
+        /// <value>The limit.</value>
+        public int? Limit { get; set; }
+
+        /// <summary>
+        /// Fields to return within the items, in addition to basic information
+        /// </summary>
+        /// <value>The fields.</value>
+        public ItemFields[] Fields { get; set; }
+    }
+
 }

+ 4 - 0
MediaBrowser.Model/Search/SearchQuery.cs

@@ -33,6 +33,8 @@ namespace MediaBrowser.Model.Search
         public bool IncludeStudios { get; set; }
         public bool IncludeArtists { get; set; }
 
+        public string[] IncludeItemTypes { get; set; }
+
         public SearchQuery()
         {
             IncludeArtists = true;
@@ -40,6 +42,8 @@ namespace MediaBrowser.Model.Search
             IncludeMedia = true;
             IncludePeople = true;
             IncludeStudios = true;
+
+            IncludeItemTypes = new string[] { };
         }
     }
 }

+ 2 - 2
MediaBrowser.Model/Session/BrowseRequest.cs

@@ -23,7 +23,7 @@ namespace MediaBrowser.Model.Session
         /// </summary>
         /// <value>The name of the item.</value>
         public string ItemName { get; set; }
-        
+
         /// <summary>
         /// Gets or sets the context (Movies, Music, Tv, etc)
         /// Applicable to genres, studios and persons only because the context of items and artists can be inferred.
@@ -40,4 +40,4 @@ namespace MediaBrowser.Model.Session
         public const string TvShows = "TvShows";
         public const string Games = "Games";
     }
-}
+}

+ 2 - 2
MediaBrowser.Model/Session/MessageCommand.cs

@@ -4,9 +4,9 @@ namespace MediaBrowser.Model.Session
     public class MessageCommand
     {
         public string Header { get; set; }
-        
+
         public string Text { get; set; }
 
         public long? TimeoutMs { get; set; }
     }
-}
+}

+ 1 - 1
MediaBrowser.Model/Session/PlayRequest.cs

@@ -43,4 +43,4 @@ namespace MediaBrowser.Model.Session
         /// </summary>
         PlayLast
     }
-}
+}

+ 1 - 1
MediaBrowser.Model/Session/PlaystateCommand.cs

@@ -38,4 +38,4 @@ namespace MediaBrowser.Model.Session
 
         public long? SeekPositionTicks { get; set; }
     }
-}
+}

+ 1 - 1
MediaBrowser.Providers/AdultVideos/AdultVideoXmlProvider.cs

@@ -23,7 +23,7 @@ namespace MediaBrowser.Providers.AdultVideos
             new MovieXmlParser(_logger).Fetch(result.Item, path, cancellationToken);
         }
 
-        protected override FileInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+        protected override FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
         {
             return MovieXmlProvider.GetXmlFileInfo(info, FileSystem);
         }

+ 1 - 1
MediaBrowser.Providers/All/LocalImageProvider.cs

@@ -201,7 +201,7 @@ namespace MediaBrowser.Providers.All
             PopulateBackdrops(images, files, imagePrefix, "background", "background-", ImageType.Backdrop);
             PopulateBackdrops(images, files, imagePrefix, "art", "art-", ImageType.Backdrop);
 
-            var extraFanartFolder = files.OfType<DirectoryInfo>()
+            var extraFanartFolder = files
                 .FirstOrDefault(i => string.Equals(i.Name, "extrafanart", StringComparison.OrdinalIgnoreCase));
 
             if (extraFanartFolder != null)

+ 1 - 1
MediaBrowser.Providers/BaseXmlProvider.cs

@@ -59,7 +59,7 @@ namespace MediaBrowser.Providers
             FileSystem = fileSystem;
         }
 
-        protected abstract FileInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService);
+        protected abstract FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService);
 
         public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date)
         {

+ 10 - 0
MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
@@ -37,6 +38,15 @@ namespace MediaBrowser.Providers.BoxSets
         protected override void MergeData(BoxSet source, BoxSet target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
         {
             ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+
+            if (mergeMetadataSettings)
+            {
+                var list = source.LinkedChildren.ToList();
+
+                list.AddRange(target.LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut));
+
+                target.LinkedChildren = list;
+            }
         }
 
         protected override ItemUpdateType BeforeSave(BoxSet item)

+ 129 - 0
MediaBrowser.Providers/BoxSets/BoxSetXmlParser.cs

@@ -0,0 +1,129 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Logging;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Xml;
+
+namespace MediaBrowser.Providers.BoxSets
+{
+    public class BoxSetXmlParser : BaseItemXmlParser<BoxSet>
+    {
+        private readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+        public BoxSetXmlParser(ILogger logger)
+            : base(logger)
+        {
+        }
+
+        protected override void FetchDataFromXmlNode(XmlReader reader, BoxSet item)
+        {
+            switch (reader.Name)
+            {
+                case "CollectionItems":
+
+                    using (var subReader = reader.ReadSubtree())
+                    {
+                        FetchFromCollectionItemsNode(subReader, item);
+                    }
+                    break;
+
+                default:
+                    base.FetchDataFromXmlNode(reader, item);
+                    break;
+            }
+        }
+
+        private void FetchFromCollectionItemsNode(XmlReader reader, BoxSet item)
+        {
+            reader.MoveToContent();
+
+            var list = new List<LinkedChild>();
+
+            while (reader.Read())
+            {
+                if (reader.NodeType == XmlNodeType.Element)
+                {
+                    switch (reader.Name)
+                    {
+                        case "CollectionItem":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    var child = GetLinkedChild(subReader);
+
+                                    if (child != null)
+                                    {
+                                        list.Add(child);
+                                    }
+                                }
+
+                                break;
+                            }
+
+                        default:
+                            reader.Skip();
+                            break;
+                    }
+                }
+            }
+
+            item.LinkedChildren = list;
+        }
+
+        private LinkedChild GetLinkedChild(XmlReader reader)
+        {
+            reader.MoveToContent();
+
+            var linkedItem = new LinkedChild
+            {
+                Type = LinkedChildType.Manual
+            };
+
+            while (reader.Read())
+            {
+                if (reader.NodeType == XmlNodeType.Element)
+                {
+                    switch (reader.Name)
+                    {
+                        case "Name":
+                            {
+                                linkedItem.ItemName = reader.ReadElementContentAsString();
+                                break;
+                            }
+
+                        case "Type":
+                            {
+                                linkedItem.ItemType = reader.ReadElementContentAsString();
+                                break;
+                            }
+
+                        case "Year":
+                            {
+                                var val = reader.ReadElementContentAsString();
+
+                                if (!string.IsNullOrWhiteSpace(val))
+                                {
+                                    int rval;
+
+                                    if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval))
+                                    {
+                                        linkedItem.ItemYear = rval;
+                                    }
+                                }
+
+                                break;
+                            }
+
+                        default:
+                            reader.Skip();
+                            break;
+                    }
+                }
+            }
+
+            return string.IsNullOrWhiteSpace(linkedItem.ItemName) || string.IsNullOrWhiteSpace(linkedItem.ItemType) ? null : linkedItem;
+        }
+    }
+}

+ 2 - 2
MediaBrowser.Providers/BoxSets/BoxSetXmlProvider.cs

@@ -22,10 +22,10 @@ namespace MediaBrowser.Providers.BoxSets
 
         protected override void Fetch(LocalMetadataResult<BoxSet> result, string path, CancellationToken cancellationToken)
         {
-            new BaseItemXmlParser<BoxSet>(_logger).Fetch(result.Item, path, cancellationToken);
+            new BoxSetXmlParser(_logger).Fetch(result.Item, path, cancellationToken);
         }
 
-        protected override FileInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+        protected override FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
         {
             return directoryService.GetFile(Path.Combine(info.Path, "collection.xml"));
         }

+ 1 - 1
MediaBrowser.Providers/Folders/FolderXmlProvider.cs

@@ -25,7 +25,7 @@ namespace MediaBrowser.Providers.Folders
             new BaseItemXmlParser<Folder>(_logger).Fetch(result.Item, path, cancellationToken);
         }
 
-        protected override FileInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+        protected override FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
         {
             return new FileInfo(Path.Combine(info.Path, "folder.xml"));
         }

+ 1 - 1
MediaBrowser.Providers/Games/GameSystemXmlProvider.cs

@@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Games
             new GameSystemXmlParser(_logger).Fetch(result.Item, path, cancellationToken);
         }
 
-        protected override FileInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+        protected override FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
         {
             return directoryService.GetFile(Path.Combine(info.Path, "gamesystem.xml"));
         }

+ 1 - 1
MediaBrowser.Providers/Games/GameXmlProvider.cs

@@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Games
             new GameXmlParser(_logger).Fetch(result.Item, path, cancellationToken);
         }
 
-        protected override FileInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+        protected override FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
         {
             var fileInfo = FileSystem.GetFileSystemInfo(info.Path);
 

+ 1 - 1
MediaBrowser.Providers/LiveTv/ChannelXmlProvider.cs

@@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.LiveTv
             new BaseItemXmlParser<LiveTvChannel>(_logger).Fetch(result.Item, path, cancellationToken);
         }
 
-        protected override FileInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+        protected override FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
         {
             return directoryService.GetFile(Path.Combine(info.Path, "channel.xml"));
         }

+ 10 - 12
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -305,22 +305,20 @@ namespace MediaBrowser.Providers.Manager
                             refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.ImageUpdate;
                         }
 
-                        if (!string.IsNullOrEmpty(localItem.Item.Name))
+                        if (string.IsNullOrWhiteSpace(localItem.Item.Name))
                         {
-                            MergeData(localItem.Item, temp, new List<MetadataFields>(), !options.ReplaceAllMetadata, true);
-                            refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataImport;
-
-                            // Only one local provider allowed per item
-                            hasLocalMetadata = true;
-                            break;
+                            localItem.Item.Name = item.Name ?? Path.GetFileNameWithoutExtension(item.Path);
                         }
 
-                        Logger.Error("Invalid local metadata found for: " + item.Path);
-                    }
-                    else
-                    {
-                        Logger.Debug("{0} returned no metadata for {1}", providerName, item.Path ?? item.Name);
+                        MergeData(localItem.Item, temp, new List<MetadataFields>(), !options.ReplaceAllMetadata, true);
+                        refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataImport;
+
+                        // Only one local provider allowed per item
+                        hasLocalMetadata = true;
+                        break;
                     }
+
+                    Logger.Debug("{0} returned no metadata for {1}", providerName, item.Path ?? item.Name);
                 }
                 catch (OperationCanceledException)
                 {

Някои файлове не бяха показани, защото твърде много файлове са промени