Browse Source

Merge remote-tracking branch 'upstream/dev' into dev

Alex Stevens 9 years ago
parent
commit
4c52fc094f
62 changed files with 684 additions and 668 deletions
  1. 1 1
      MediaBrowser.Api/FilterService.cs
  2. 1 5
      MediaBrowser.Api/ItemUpdateService.cs
  3. 11 0
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  4. 1 7
      MediaBrowser.Api/SimilarItemsHelper.cs
  5. 2 1
      MediaBrowser.Api/StartupWizardService.cs
  6. 19 7
      MediaBrowser.Api/Subtitles/SubtitleService.cs
  7. 1 6
      MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
  8. 0 1
      MediaBrowser.Controller/Entities/Audio/Audio.cs
  9. 3 8
      MediaBrowser.Controller/Entities/BaseItem.cs
  10. 1 1
      MediaBrowser.Controller/Entities/Book.cs
  11. 5 76
      MediaBrowser.Controller/Entities/Folder.cs
  12. 1 1
      MediaBrowser.Controller/Entities/Game.cs
  13. 1 1
      MediaBrowser.Controller/Entities/Photo.cs
  14. 1 1
      MediaBrowser.Controller/Entities/Studio.cs
  15. 7 1
      MediaBrowser.Controller/Entities/TV/Episode.cs
  16. 7 42
      MediaBrowser.Controller/Entities/TV/Season.cs
  17. 44 75
      MediaBrowser.Controller/Entities/TV/Series.cs
  18. 1 14
      MediaBrowser.Controller/Entities/TagExtensions.cs
  19. 1 1
      MediaBrowser.Controller/Entities/UserView.cs
  20. 19 19
      MediaBrowser.Controller/Entities/UserViewBuilder.cs
  21. 0 1
      MediaBrowser.Controller/Entities/Video.cs
  22. 1 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  23. 2 0
      MediaBrowser.Controller/Persistence/IUserDataRepository.cs
  24. 2 6
      MediaBrowser.Controller/Providers/BaseItemXmlParser.cs
  25. 7 11
      MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs
  26. 2 0
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  27. 2 1
      MediaBrowser.Model/Configuration/UserConfiguration.cs
  28. 12 12
      MediaBrowser.Model/Entities/ImageType.cs
  29. 6 0
      MediaBrowser.Model/LiveTv/LiveTvOptions.cs
  30. 2 8
      MediaBrowser.Providers/Manager/ProviderUtils.cs
  31. 1 1
      MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs
  32. 27 21
      MediaBrowser.Providers/Omdb/OmdbImageProvider.cs
  33. 10 4
      MediaBrowser.Providers/Omdb/OmdbItemProvider.cs
  34. 89 29
      MediaBrowser.Providers/Omdb/OmdbProvider.cs
  35. 10 4
      MediaBrowser.Providers/TV/Omdb/OmdbEpisodeProvider.cs
  36. 16 0
      MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeProvider.cs
  37. 13 1
      MediaBrowser.Providers/TV/TheMovieDb/MovieDbProviderBase.cs
  38. 1 1
      MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeasonProvider.cs
  39. 30 3
      MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeriesProvider.cs
  40. 1 1
      MediaBrowser.Providers/TV/TvExternalIds.cs
  41. 0 1
      MediaBrowser.Server.Implementations/Connect/Responses.cs
  42. 1 10
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  43. 4 3
      MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs
  44. 24 74
      MediaBrowser.Server.Implementations/Library/UserDataManager.cs
  45. 6 0
      MediaBrowser.Server.Implementations/Library/UserViewManager.cs
  46. 0 1
      MediaBrowser.Server.Implementations/Persistence/IDbConnector.cs
  47. 2 116
      MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs
  48. 115 27
      MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs
  49. 48 0
      MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs
  50. 15 27
      MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs
  51. 0 5
      MediaBrowser.Server.Mono/Native/DbConnector.cs
  52. 3 2
      MediaBrowser.Server.Startup.Common/ApplicationHost.cs
  53. 2 0
      MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj
  54. 44 0
      MediaBrowser.Server.Startup.Common/Migrations/CollectionGroupingMigration.cs
  55. 40 0
      MediaBrowser.Server.Startup.Common/Migrations/FolderViewSettingMigration.cs
  56. 0 5
      MediaBrowser.ServerApplication/Native/DbConnector.cs
  57. 6 3
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
  58. 1 5
      MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
  59. 7 11
      MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
  60. 2 2
      Nuget/MediaBrowser.Common.Internal.nuspec
  61. 1 1
      Nuget/MediaBrowser.Common.nuspec
  62. 2 2
      Nuget/MediaBrowser.Server.Core.nuspec

+ 1 - 1
MediaBrowser.Api/FilterService.cs

@@ -80,7 +80,7 @@ namespace MediaBrowser.Api
                 .OrderBy(i => i)
                 .OrderBy(i => i)
                 .ToArray();
                 .ToArray();
 
 
-            result.Tags = items.OfType<IHasTags>()
+            result.Tags = items
                 .SelectMany(i => i.Tags)
                 .SelectMany(i => i.Tags)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .OrderBy(i => i)
                 .OrderBy(i => i)

+ 1 - 5
MediaBrowser.Api/ItemUpdateService.cs

@@ -280,11 +280,7 @@ namespace MediaBrowser.Api
                 episode.AbsoluteEpisodeNumber = request.AbsoluteEpisodeNumber;
                 episode.AbsoluteEpisodeNumber = request.AbsoluteEpisodeNumber;
             }
             }
 
 
-            var hasTags = item as IHasTags;
-            if (hasTags != null)
-            {
-                hasTags.Tags = request.Tags;
-            }
+            item.Tags = request.Tags;
 
 
             var hasTaglines = item as IHasTaglines;
             var hasTaglines = item as IHasTaglines;
             if (hasTaglines != null)
             if (hasTaglines != null)

+ 11 - 0
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -439,6 +439,12 @@ namespace MediaBrowser.Api.LiveTv
         public string Id { get; set; }
         public string Id { get; set; }
     }
     }
 
 
+    [Route("/LiveTv/ListingProviders/Default", "GET")]
+    [Authenticated(AllowBeforeStartupWizard = true)]
+    public class GetDefaultListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo>
+    {
+    }
+
     [Route("/LiveTv/ListingProviders", "POST", Summary = "Adds a listing provider")]
     [Route("/LiveTv/ListingProviders", "POST", Summary = "Adds a listing provider")]
     [Authenticated(AllowBeforeStartupWizard = true)]
     [Authenticated(AllowBeforeStartupWizard = true)]
     public class AddListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo>
     public class AddListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo>
@@ -525,6 +531,11 @@ namespace MediaBrowser.Api.LiveTv
             _dtoService = dtoService;
             _dtoService = dtoService;
         }
         }
 
 
+        public object Get(GetDefaultListingProvider request)
+        {
+            return ToOptimizedResult(new ListingsProviderInfo());
+        }
+
         public async Task<object> Get(GetSatChannnelScanResult request)
         public async Task<object> Get(GetSatChannnelScanResult request)
         {
         {
             var result = await _liveTvManager.GetSatChannelScanResult(request, CancellationToken.None).ConfigureAwait(false);
             var result = await _liveTvManager.GetSatChannelScanResult(request, CancellationToken.None).ConfigureAwait(false);

+ 1 - 7
MediaBrowser.Api/SimilarItemsHelper.cs

@@ -116,13 +116,7 @@ namespace MediaBrowser.Api
 
 
         private static IEnumerable<string> GetTags(BaseItem item)
         private static IEnumerable<string> GetTags(BaseItem item)
         {
         {
-            var hasTags = item as IHasTags;
-            if (hasTags != null)
-            {
-                return hasTags.Tags;
-            }
-
-            return new List<string>();
+            return item.Tags;
         }
         }
 
 
         private static IEnumerable<string> GetKeywords(BaseItem item)
         private static IEnumerable<string> GetKeywords(BaseItem item)

+ 2 - 1
MediaBrowser.Api/StartupWizardService.cs

@@ -113,7 +113,8 @@ namespace MediaBrowser.Api
             config.EnableCustomPathSubFolders = true;
             config.EnableCustomPathSubFolders = true;
             config.EnableStandaloneMusicKeys = true;
             config.EnableStandaloneMusicKeys = true;
             config.EnableCaseSensitiveItemIds = true;
             config.EnableCaseSensitiveItemIds = true;
-            config.SchemaVersion = 79;
+            config.EnableFolderView = true;
+            config.SchemaVersion = 89;
         }
         }
 
 
         public void Post(UpdateStartupConfiguration request)
         public void Post(UpdateStartupConfiguration request)

+ 19 - 7
MediaBrowser.Api/Subtitles/SubtitleService.cs

@@ -101,6 +101,7 @@ namespace MediaBrowser.Api.Subtitles
 
 
         [ApiMember(Name = "CopyTimestamps", Description = "CopyTimestamps", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
         [ApiMember(Name = "CopyTimestamps", Description = "CopyTimestamps", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
         public bool CopyTimestamps { get; set; }
         public bool CopyTimestamps { get; set; }
+        public bool AddVttTimeMap { get; set; }
     }
     }
 
 
     [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/subtitles.m3u8", "GET", Summary = "Gets an HLS subtitle playlist.")]
     [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/subtitles.m3u8", "GET", Summary = "Gets an HLS subtitle playlist.")]
@@ -178,7 +179,7 @@ namespace MediaBrowser.Api.Subtitles
 
 
                 var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
                 var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
 
 
-                var url = string.Format("stream.vtt?CopyTimestamps=true,StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
+                var url = string.Format("stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
                     positionTicks.ToString(CultureInfo.InvariantCulture),
                     positionTicks.ToString(CultureInfo.InvariantCulture),
                     endPositionTicks.ToString(CultureInfo.InvariantCulture),
                     endPositionTicks.ToString(CultureInfo.InvariantCulture),
                     accessToken);
                     accessToken);
@@ -193,7 +194,7 @@ namespace MediaBrowser.Api.Subtitles
             return ResultFactory.GetResult(builder.ToString(), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
             return ResultFactory.GetResult(builder.ToString(), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
         }
         }
 
 
-        public object Get(GetSubtitle request)
+        public async Task<object> Get(GetSubtitle request)
         {
         {
             if (string.Equals(request.Format, "js", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(request.Format, "js", StringComparison.OrdinalIgnoreCase))
             {
             {
@@ -212,21 +213,32 @@ namespace MediaBrowser.Api.Subtitles
                 return ToStaticFileResult(subtitleStream.Path);
                 return ToStaticFileResult(subtitleStream.Path);
             }
             }
 
 
-            var stream = GetSubtitles(request).Result;
+            using (var stream = await GetSubtitles(request).ConfigureAwait(false))
+            {
+                using (var reader = new StreamReader(stream))
+                {
+                    var text = reader.ReadToEnd();
+
+                    if (string.Equals(request.Format, "vtt", StringComparison.OrdinalIgnoreCase) && request.AddVttTimeMap)
+                    {
+                        text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000");
+                    }
 
 
-            return ResultFactory.GetResult(stream, MimeTypes.GetMimeType("file." + request.Format));
+                    return ResultFactory.GetResult(text, MimeTypes.GetMimeType("file." + request.Format));
+                }
+            }
         }
         }
 
 
-        private async Task<Stream> GetSubtitles(GetSubtitle request)
+        private  Task<Stream> GetSubtitles(GetSubtitle request)
         {
         {
-            return await _subtitleEncoder.GetSubtitles(request.Id,
+            return  _subtitleEncoder.GetSubtitles(request.Id,
                 request.MediaSourceId,
                 request.MediaSourceId,
                 request.Index,
                 request.Index,
                 request.Format,
                 request.Format,
                 request.StartPositionTicks,
                 request.StartPositionTicks,
                 request.EndPositionTicks,
                 request.EndPositionTicks,
                 request.CopyTimestamps,
                 request.CopyTimestamps,
-                CancellationToken.None).ConfigureAwait(false);
+                CancellationToken.None);
         }
         }
 
 
         public object Get(SearchRemoteSubtitles request)
         public object Get(SearchRemoteSubtitles request)

+ 1 - 6
MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs

@@ -333,12 +333,7 @@ namespace MediaBrowser.Api.UserLibrary
                 var tags = request.GetTags();
                 var tags = request.GetTags();
                 if (tags.Length > 0)
                 if (tags.Length > 0)
                 {
                 {
-                    var hasTags = i as IHasTags;
-                    if (hasTags == null)
-                    {
-                        return false;
-                    }
-                    if (!tags.Any(v => hasTags.Tags.Contains(v, StringComparer.OrdinalIgnoreCase)))
+                    if (!tags.Any(v => i.Tags.Contains(v, StringComparer.OrdinalIgnoreCase)))
                     {
                     {
                         return false;
                         return false;
                     }
                     }

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

@@ -20,7 +20,6 @@ namespace MediaBrowser.Controller.Entities.Audio
         IHasArtist,
         IHasArtist,
         IHasMusicGenres,
         IHasMusicGenres,
         IHasLookupInfo<SongInfo>,
         IHasLookupInfo<SongInfo>,
-        IHasTags,
         IHasMediaSources,
         IHasMediaSources,
         IThemeMedia,
         IThemeMedia,
         IArchivable
         IArchivable

+ 3 - 8
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -1397,15 +1397,10 @@ namespace MediaBrowser.Controller.Entities
 
 
         private bool IsVisibleViaTags(User user)
         private bool IsVisibleViaTags(User user)
         {
         {
-            var hasTags = this as IHasTags;
-
-            if (hasTags != null)
+            var policy = user.Policy;
+            if (policy.BlockedTags.Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase)))
             {
             {
-                var policy = user.Policy;
-                if (policy.BlockedTags.Any(i => hasTags.Tags.Contains(i, StringComparer.OrdinalIgnoreCase)))
-                {
-                    return false;
-                }
+                return false;
             }
             }
 
 
             return true;
             return true;

+ 1 - 1
MediaBrowser.Controller/Entities/Book.cs

@@ -6,7 +6,7 @@ using MediaBrowser.Model.Entities;
 
 
 namespace MediaBrowser.Controller.Entities
 namespace MediaBrowser.Controller.Entities
 {
 {
-    public class Book : BaseItem, IHasTags, IHasLookupInfo<BookInfo>, IHasSeries
+    public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries
     {
     {
         [IgnoreDataMember]
         [IgnoreDataMember]
         public override string MediaType
         public override string MediaType

+ 5 - 76
MediaBrowser.Controller/Entities/Folder.cs

@@ -20,7 +20,7 @@ namespace MediaBrowser.Controller.Entities
     /// <summary>
     /// <summary>
     /// Class Folder
     /// Class Folder
     /// </summary>
     /// </summary>
-    public class Folder : BaseItem, IHasThemeMedia, IHasTags
+    public class Folder : BaseItem, IHasThemeMedia
     {
     {
         public static IUserManager UserManager { get; set; }
         public static IUserManager UserManager { get; set; }
         public static IUserViewManager UserViewManager { get; set; }
         public static IUserViewManager UserViewManager { get; set; }
@@ -164,49 +164,15 @@ namespace MediaBrowser.Controller.Entities
                 item.DateModified = DateTime.UtcNow;
                 item.DateModified = DateTime.UtcNow;
             }
             }
 
 
-            AddChildInternal(item.Id);
-
             await LibraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false);
             await LibraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false);
         }
         }
 
 
-        protected void AddChildrenInternal(List<Guid> children)
-        {
-            lock (_childrenSyncLock)
-            {
-                var newChildren = ChildIds.ToList();
-                newChildren.AddRange(children);
-                _children = newChildren.ToList();
-            }
-        }
-        protected void AddChildInternal(Guid child)
-        {
-            lock (_childrenSyncLock)
-            {
-                var childIds = ChildIds.ToList();
-                if (!childIds.Contains(child))
-                {
-                    childIds.Add(child);
-                    _children = childIds.ToList();
-                }
-            }
-        }
-
-        protected void RemoveChildrenInternal(List<Guid> children)
-        {
-            lock (_childrenSyncLock)
-            {
-                _children = ChildIds.Except(children).ToList();
-            }
-        }
-
         /// <summary>
         /// <summary>
         /// Removes the child.
         /// Removes the child.
         /// </summary>
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
         public void RemoveChild(BaseItem item)
         public void RemoveChild(BaseItem item)
         {
         {
-            RemoveChildrenInternal(new[] { item.Id }.ToList());
-
             item.SetParent(null);
             item.SetParent(null);
         }
         }
 
 
@@ -241,33 +207,6 @@ namespace MediaBrowser.Controller.Entities
 
 
         #endregion
         #endregion
 
 
-        /// <summary>
-        /// The children
-        /// </summary>
-        private IReadOnlyList<Guid> _children;
-        /// <summary>
-        /// The _children sync lock
-        /// </summary>
-        private readonly object _childrenSyncLock = new object();
-        /// <summary>
-        /// Gets or sets the actual children.
-        /// </summary>
-        /// <value>The actual children.</value>
-        protected virtual IEnumerable<Guid> ChildIds
-        {
-            get
-            {
-                lock (_childrenSyncLock)
-                {
-                    if (_children == null)
-                    {
-                        _children = LoadChildren().ToList();
-                    }
-                    return _children.ToList();
-                }
-            }
-        }
-
         /// <summary>
         /// <summary>
         /// Gets the actual children.
         /// Gets the actual children.
         /// </summary>
         /// </summary>
@@ -277,7 +216,7 @@ namespace MediaBrowser.Controller.Entities
         {
         {
             get
             get
             {
             {
-                return ChildIds.Select(LibraryManager.GetItemById).Where(i => i != null);
+                return LoadChildren().Select(LibraryManager.GetItemById).Where(i => i != null);
             }
             }
         }
         }
 
 
@@ -479,8 +418,6 @@ namespace MediaBrowser.Controller.Entities
 
 
                     if (actualRemovals.Count > 0)
                     if (actualRemovals.Count > 0)
                     {
                     {
-                        RemoveChildrenInternal(actualRemovals.Select(i => i.Id).ToList());
-
                         foreach (var item in actualRemovals)
                         foreach (var item in actualRemovals)
                         {
                         {
                             Logger.Debug("Removed item: " + item.Path);
                             Logger.Debug("Removed item: " + item.Path);
@@ -493,8 +430,6 @@ namespace MediaBrowser.Controller.Entities
                     }
                     }
 
 
                     await LibraryManager.CreateItems(newItems, cancellationToken).ConfigureAwait(false);
                     await LibraryManager.CreateItems(newItems, cancellationToken).ConfigureAwait(false);
-
-                    AddChildrenInternal(newItems.Select(i => i.Id).ToList());
                 }
                 }
             }
             }
 
 
@@ -766,7 +701,7 @@ namespace MediaBrowser.Controller.Entities
             {
             {
                 if (!(this is ICollectionFolder))
                 if (!(this is ICollectionFolder))
                 {
                 {
-                    Logger.Debug("Query requires post-filtering due to LinkedChildren");
+                    Logger.Debug("Query requires post-filtering due to LinkedChildren. Type: " + GetType().Name);
                     return true;
                     return true;
                 }
                 }
             }
             }
@@ -889,12 +824,6 @@ namespace MediaBrowser.Controller.Entities
                 return true;
                 return true;
             }
             }
 
 
-            if (query.ImageTypes.Length > 0)
-            {
-                Logger.Debug("Query requires post-filtering due to ImageTypes");
-                return true;
-            }
-
             // Apply studio filter
             // Apply studio filter
             if (query.StudioIds.Length > 0)
             if (query.StudioIds.Length > 0)
             {
             {
@@ -946,7 +875,7 @@ namespace MediaBrowser.Controller.Entities
                 return true;
                 return true;
             }
             }
 
 
-            if (UserViewBuilder.CollapseBoxSetItems(query, this, query.User))
+            if (UserViewBuilder.CollapseBoxSetItems(query, this, query.User, ConfigurationManager))
             {
             {
                 Logger.Debug("Query requires post-filtering due to CollapseBoxSetItems");
                 Logger.Debug("Query requires post-filtering due to CollapseBoxSetItems");
                 return true;
                 return true;
@@ -1054,7 +983,7 @@ namespace MediaBrowser.Controller.Entities
 
 
         protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
         protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
         {
         {
-            return UserViewBuilder.PostFilterAndSort(items, this, null, query, LibraryManager);
+            return UserViewBuilder.PostFilterAndSort(items, this, null, query, LibraryManager, ConfigurationManager);
         }
         }
 
 
         public virtual IEnumerable<BaseItem> GetChildren(User user, bool includeLinkedChildren)
         public virtual IEnumerable<BaseItem> GetChildren(User user, bool includeLinkedChildren)

+ 1 - 1
MediaBrowser.Controller/Entities/Game.cs

@@ -7,7 +7,7 @@ using System.Linq;
 
 
 namespace MediaBrowser.Controller.Entities
 namespace MediaBrowser.Controller.Entities
 {
 {
-    public class Game : BaseItem, IHasTrailers, IHasThemeMedia, IHasTags, IHasScreenshots, ISupportsPlaceHolders, IHasLookupInfo<GameInfo>
+    public class Game : BaseItem, IHasTrailers, IHasThemeMedia, IHasScreenshots, ISupportsPlaceHolders, IHasLookupInfo<GameInfo>
     {
     {
         public List<Guid> ThemeSongIds { get; set; }
         public List<Guid> ThemeSongIds { get; set; }
         public List<Guid> ThemeVideoIds { get; set; }
         public List<Guid> ThemeVideoIds { get; set; }

+ 1 - 1
MediaBrowser.Controller/Entities/Photo.cs

@@ -5,7 +5,7 @@ using System.Runtime.Serialization;
 
 
 namespace MediaBrowser.Controller.Entities
 namespace MediaBrowser.Controller.Entities
 {
 {
-    public class Photo : BaseItem, IHasTags, IHasTaglines
+    public class Photo : BaseItem, IHasTaglines
     {
     {
         public List<string> Taglines { get; set; }
         public List<string> Taglines { get; set; }
 
 

+ 1 - 1
MediaBrowser.Controller/Entities/Studio.cs

@@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Entities
     /// <summary>
     /// <summary>
     /// Class Studio
     /// Class Studio
     /// </summary>
     /// </summary>
-    public class Studio : BaseItem, IItemByName, IHasTags
+    public class Studio : BaseItem, IItemByName
     {
     {
         public override List<string> GetUserDataKeys()
         public override List<string> GetUserDataKeys()
         {
         {

+ 7 - 1
MediaBrowser.Controller/Entities/TV/Episode.cs

@@ -108,7 +108,13 @@ namespace MediaBrowser.Controller.Entities.TV
             var series = Series;
             var series = Series;
             if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue)
             if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue)
             {
             {
-                list.InsertRange(0, series.GetUserDataKeys().Select(i => i + ParentIndexNumber.Value.ToString("000") + IndexNumber.Value.ToString("000")));
+                var seriesUserDataKeys = series.GetUserDataKeys();
+                var take = seriesUserDataKeys.Count;
+                if (seriesUserDataKeys.Count > 1)
+                {
+                    take--;
+                }
+                list.InsertRange(0, seriesUserDataKeys.Take(take).Select(i => i + ParentIndexNumber.Value.ToString("000") + IndexNumber.Value.ToString("000")));
             }
             }
 
 
             return list;
             return list;

+ 7 - 42
MediaBrowser.Controller/Entities/TV/Season.cs

@@ -196,52 +196,17 @@ namespace MediaBrowser.Controller.Entities.TV
         {
         {
             var config = user.Configuration;
             var config = user.Configuration;
 
 
-            return GetEpisodes(user, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes);
+            return GetEpisodes(Series, user, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes);
         }
         }
 
 
-        public IEnumerable<Episode> GetEpisodes(User user, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes)
+        public IEnumerable<Episode> GetEpisodes(Series series, User user, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes)
         {
         {
-            var series = Series;
-
-            if (IndexNumber.HasValue && series != null)
-            {
-                return series.GetEpisodes(user, this, includeMissingEpisodes, includeVirtualUnairedEpisodes);
-            }
-
-            var episodes = GetRecursiveChildren(user)
-                .OfType<Episode>();
-
-            if (series != null && series.ContainsEpisodesWithoutSeasonFolders)
-            {
-                var seasonNumber = IndexNumber;
-                var list = episodes.ToList();
-
-                if (seasonNumber.HasValue)
-                {
-                    list.AddRange(series.GetRecursiveChildren(user).OfType<Episode>()
-                        .Where(i => i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == seasonNumber.Value));
-                }
-                else
-                {
-                    list.AddRange(series.GetRecursiveChildren(user).OfType<Episode>()
-                        .Where(i => !i.ParentIndexNumber.HasValue));
-                }
-
-                episodes = list.DistinctBy(i => i.Id);
-            }
-
-            if (!includeMissingEpisodes)
-            {
-                episodes = episodes.Where(i => !i.IsMissingEpisode);
-            }
-            if (!includeVirtualUnairedEpisodes)
-            {
-                episodes = episodes.Where(i => !i.IsVirtualUnaired);
-            }
+            return GetEpisodes(series, user, includeMissingEpisodes, includeVirtualUnairedEpisodes, null);
+        }
 
 
-            return LibraryManager
-                .Sort(episodes, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending)
-                .Cast<Episode>();
+        public IEnumerable<Episode> GetEpisodes(Series series, User user, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes, IEnumerable<Episode> allSeriesEpisodes)
+        {
+            return series.GetEpisodes(user, this, includeMissingEpisodes, includeVirtualUnairedEpisodes, allSeriesEpisodes);
         }
         }
 
 
         public IEnumerable<Episode> GetEpisodes()
         public IEnumerable<Episode> GetEpisodes()

+ 44 - 75
MediaBrowser.Controller/Entities/TV/Series.cs

@@ -107,9 +107,11 @@ namespace MediaBrowser.Controller.Entities.TV
         {
         {
             get
             get
             {
             {
-                if (EnablePooling())
+                var userdatakeys = GetUserDataKeys();
+
+                if (userdatakeys.Count > 1)
                 {
                 {
-                    return GetUserDataKeys().First();
+                    return userdatakeys[0];
                 }
                 }
                 return base.PresentationUniqueKey;
                 return base.PresentationUniqueKey;
             }
             }
@@ -183,24 +185,20 @@ namespace MediaBrowser.Controller.Entities.TV
 
 
         protected override Task<QueryResult<BaseItem>> GetItemsInternal(InternalItemsQuery query)
         protected override Task<QueryResult<BaseItem>> GetItemsInternal(InternalItemsQuery query)
         {
         {
+            if (query.User == null)
+            {
+                return base.GetItemsInternal(query);
+            }
+
             var user = query.User;
             var user = query.User;
 
 
             Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
             Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
 
 
             IEnumerable<BaseItem> items;
             IEnumerable<BaseItem> items;
 
 
-            if (query.User == null)
-            {
-                items = query.Recursive
-                   ? GetRecursiveChildren(filter)
-                   : Children.Where(filter);
-            }
-            else
-            {
-                items = query.Recursive
-                   ? GetSeasons(user).Cast<BaseItem>().Concat(GetEpisodes(user)).Where(filter)
-                   : GetSeasons(user).Where(filter);
-            }
+            items = query.Recursive
+               ? GetSeasons(user).Cast<BaseItem>().Concat(GetEpisodes(user)).Where(filter)
+               : GetSeasons(user).Where(filter);
 
 
             var result = PostFilterAndSort(items, query);
             var result = PostFilterAndSort(items, query);
 
 
@@ -211,33 +209,13 @@ namespace MediaBrowser.Controller.Entities.TV
         {
         {
             IEnumerable<Season> seasons;
             IEnumerable<Season> seasons;
 
 
-            if (EnablePooling())
+            seasons = LibraryManager.GetItemList(new InternalItemsQuery(user)
             {
             {
-                var seriesIds = LibraryManager.GetItemIds(new InternalItemsQuery(user)
-                {
-                    PresentationUniqueKey = PresentationUniqueKey,
-                    IncludeItemTypes = new[] { typeof(Series).Name }
-                });
+                AncestorWithPresentationUniqueKey = PresentationUniqueKey,
+                IncludeItemTypes = new[] { typeof(Season).Name },
+                SortBy = new[] { ItemSortBy.SortName }
 
 
-                if (seriesIds.Count > 1)
-                {
-                    seasons = LibraryManager.GetItemList(new InternalItemsQuery(user)
-                    {
-                        AncestorIds = seriesIds.Select(i => i.ToString("N")).ToArray(),
-                        IncludeItemTypes = new[] { typeof(Season).Name },
-                        SortBy = new[] { ItemSortBy.SortName }
-
-                    }).Cast<Season>();
-                }
-                else
-                {
-                    seasons = LibraryManager.Sort(base.GetChildren(user, true), user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).OfType<Season>();
-                }
-            }
-            else
-            {
-                seasons = LibraryManager.Sort(base.GetChildren(user, true), user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).OfType<Season>();
-            }
+            }).Cast<Season>();
 
 
             if (!includeMissingSeasons)
             if (!includeMissingSeasons)
             {
             {
@@ -260,8 +238,17 @@ namespace MediaBrowser.Controller.Entities.TV
 
 
         public IEnumerable<Episode> GetEpisodes(User user, bool includeMissing, bool includeVirtualUnaired)
         public IEnumerable<Episode> GetEpisodes(User user, bool includeMissing, bool includeVirtualUnaired)
         {
         {
-            var allEpisodes = GetSeasons(user, true, true)
-                .SelectMany(i => i.GetEpisodes(user, includeMissing, includeVirtualUnaired))
+            var allItems = LibraryManager.GetItemList(new InternalItemsQuery(user)
+            {
+                AncestorWithPresentationUniqueKey = PresentationUniqueKey,
+                IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name },
+                SortBy = new[] { ItemSortBy.SortName }
+            }).ToList();
+
+            var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
+
+            var allEpisodes = allItems.OfType<Season>()
+                .SelectMany(i => i.GetEpisodes(this, user, includeMissing, includeVirtualUnaired, allSeriesEpisodes))
                 .Reverse()
                 .Reverse()
                 .ToList();
                 .ToList();
 
 
@@ -345,50 +332,32 @@ namespace MediaBrowser.Controller.Entities.TV
             return GetEpisodes(user, season, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes);
             return GetEpisodes(user, season, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes);
         }
         }
 
 
-        private bool EnablePooling()
+        private IEnumerable<Episode> GetAllEpisodes(User user)
         {
         {
-            return false;
+            return LibraryManager.GetItemList(new InternalItemsQuery(user)
+            {
+                AncestorWithPresentationUniqueKey = PresentationUniqueKey,
+                IncludeItemTypes = new[] { typeof(Episode).Name },
+                SortBy = new[] { ItemSortBy.SortName }
+
+            }).Cast<Episode>();
         }
         }
 
 
         public IEnumerable<Episode> GetEpisodes(User user, Season parentSeason, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes)
         public IEnumerable<Episode> GetEpisodes(User user, Season parentSeason, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes)
         {
         {
-            IEnumerable<Episode> episodes;
+            IEnumerable<Episode> episodes = GetAllEpisodes(user);
 
 
-            if (EnablePooling())
-            {
-                var seriesIds = LibraryManager.GetItemIds(new InternalItemsQuery(user)
-                {
-                    PresentationUniqueKey = PresentationUniqueKey,
-                    IncludeItemTypes = new[] { typeof(Series).Name }
-                });
-
-                if (seriesIds.Count > 1)
-                {
-                    episodes = LibraryManager.GetItemList(new InternalItemsQuery(user)
-                    {
-                        AncestorIds = seriesIds.Select(i => i.ToString("N")).ToArray(),
-                        IncludeItemTypes = new[] { typeof(Episode).Name },
-                        SortBy = new[] { ItemSortBy.SortName }
+            return GetEpisodes(user, parentSeason, includeMissingEpisodes, includeVirtualUnairedEpisodes, episodes);
+        }
 
 
-                    }).Cast<Episode>();
-                }
-                else
-                {
-                    episodes = GetRecursiveChildren(user, new InternalItemsQuery(user)
-                    {
-                        IncludeItemTypes = new[] { typeof(Episode).Name }
-                    }).Cast<Episode>();
-                }
-            }
-            else
+        public IEnumerable<Episode> GetEpisodes(User user, Season parentSeason, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes, IEnumerable<Episode> allSeriesEpisodes)
+        {
+            if (allSeriesEpisodes == null)
             {
             {
-                episodes = GetRecursiveChildren(user, new InternalItemsQuery(user)
-                {
-                    IncludeItemTypes = new[] { typeof(Episode).Name }
-                }).Cast<Episode>();
+                return GetEpisodes(user, parentSeason, includeMissingEpisodes, includeVirtualUnairedEpisodes);
             }
             }
 
 
-            episodes = FilterEpisodesBySeason(episodes, parentSeason, DisplaySpecialsWithSeasons);
+            var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, DisplaySpecialsWithSeasons);
 
 
             if (!includeMissingEpisodes)
             if (!includeMissingEpisodes)
             {
             {

+ 1 - 14
MediaBrowser.Controller/Entities/IHasTags.cs → MediaBrowser.Controller/Entities/TagExtensions.cs

@@ -1,24 +1,11 @@
 using System;
 using System;
-using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 
 
 namespace MediaBrowser.Controller.Entities
 namespace MediaBrowser.Controller.Entities
 {
 {
-    /// <summary>
-    /// Interface IHasTags
-    /// </summary>
-    public interface IHasTags
-    {
-        /// <summary>
-        /// Gets or sets the tags.
-        /// </summary>
-        /// <value>The tags.</value>
-        List<string> Tags { get; set; }
-    }
-
     public static class TagExtensions
     public static class TagExtensions
     {
     {
-        public static void AddTag(this IHasTags item, string name)
+        public static void AddTag(this BaseItem item, string name)
         {
         {
             if (string.IsNullOrWhiteSpace(name))
             if (string.IsNullOrWhiteSpace(name))
             {
             {

+ 1 - 1
MediaBrowser.Controller/Entities/UserView.cs

@@ -58,7 +58,7 @@ namespace MediaBrowser.Controller.Entities
                 parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent;
                 parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent;
             }
             }
 
 
-            return new UserViewBuilder(UserViewManager, LiveTvManager, ChannelManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, CollectionManager, PlaylistManager)
+            return new UserViewBuilder(UserViewManager, LiveTvManager, ChannelManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, ConfigurationManager, PlaylistManager)
                 .GetUserItems(parent, this, ViewType, query);
                 .GetUserItems(parent, this, ViewType, query);
         }
         }
 
 

+ 19 - 19
MediaBrowser.Controller/Entities/UserViewBuilder.cs

@@ -18,6 +18,8 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Configuration;
 
 
 namespace MediaBrowser.Controller.Entities
 namespace MediaBrowser.Controller.Entities
 {
 {
@@ -30,10 +32,10 @@ namespace MediaBrowser.Controller.Entities
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IUserDataManager _userDataManager;
         private readonly IUserDataManager _userDataManager;
         private readonly ITVSeriesManager _tvSeriesManager;
         private readonly ITVSeriesManager _tvSeriesManager;
-        private readonly ICollectionManager _collectionManager;
+        private readonly IServerConfigurationManager _config;
         private readonly IPlaylistManager _playlistManager;
         private readonly IPlaylistManager _playlistManager;
 
 
-        public UserViewBuilder(IUserViewManager userViewManager, ILiveTvManager liveTvManager, IChannelManager channelManager, ILibraryManager libraryManager, ILogger logger, IUserDataManager userDataManager, ITVSeriesManager tvSeriesManager, ICollectionManager collectionManager, IPlaylistManager playlistManager)
+        public UserViewBuilder(IUserViewManager userViewManager, ILiveTvManager liveTvManager, IChannelManager channelManager, ILibraryManager libraryManager, ILogger logger, IUserDataManager userDataManager, ITVSeriesManager tvSeriesManager, IServerConfigurationManager config, IPlaylistManager playlistManager)
         {
         {
             _userViewManager = userViewManager;
             _userViewManager = userViewManager;
             _liveTvManager = liveTvManager;
             _liveTvManager = liveTvManager;
@@ -42,7 +44,7 @@ namespace MediaBrowser.Controller.Entities
             _logger = logger;
             _logger = logger;
             _userDataManager = userDataManager;
             _userDataManager = userDataManager;
             _tvSeriesManager = tvSeriesManager;
             _tvSeriesManager = tvSeriesManager;
-            _collectionManager = collectionManager;
+            _config = config;
             _playlistManager = playlistManager;
             _playlistManager = playlistManager;
         }
         }
 
 
@@ -159,7 +161,7 @@ namespace MediaBrowser.Controller.Entities
                     return await GetTvGenres(queryParent, user, query).ConfigureAwait(false);
                     return await GetTvGenres(queryParent, user, query).ConfigureAwait(false);
 
 
                 case SpecialFolder.TvGenre:
                 case SpecialFolder.TvGenre:
-                    return await GetTvGenreItems(queryParent, displayParent, user, query).ConfigureAwait(false);
+                    return GetTvGenreItems(queryParent, displayParent, user, query);
 
 
                 case SpecialFolder.TvResume:
                 case SpecialFolder.TvResume:
                     return GetTvResume(queryParent, user, query);
                     return GetTvResume(queryParent, user, query);
@@ -740,7 +742,7 @@ namespace MediaBrowser.Controller.Entities
             return GetResult(genres, parent, query);
             return GetResult(genres, parent, query);
         }
         }
 
 
-        private async Task<QueryResult<BaseItem>> GetTvGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query)
+        private QueryResult<BaseItem> GetTvGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query)
         {
         {
             query.Recursive = true;
             query.Recursive = true;
             query.ParentId = queryParent.Id;
             query.ParentId = queryParent.Id;
@@ -769,7 +771,7 @@ namespace MediaBrowser.Controller.Entities
         {
         {
             items = items.Where(i => Filter(i, query.User, query, _userDataManager, _libraryManager));
             items = items.Where(i => Filter(i, query.User, query, _userDataManager, _libraryManager));
 
 
-            return PostFilterAndSort(items, queryParent, null, query, _libraryManager);
+            return PostFilterAndSort(items, queryParent, null, query, _libraryManager, _config);
         }
         }
 
 
         public static bool FilterItem(BaseItem item, InternalItemsQuery query)
         public static bool FilterItem(BaseItem item, InternalItemsQuery query)
@@ -782,14 +784,15 @@ namespace MediaBrowser.Controller.Entities
             int? totalRecordLimit,
             int? totalRecordLimit,
             InternalItemsQuery query)
             InternalItemsQuery query)
         {
         {
-            return PostFilterAndSort(items, queryParent, totalRecordLimit, query, _libraryManager);
+            return PostFilterAndSort(items, queryParent, totalRecordLimit, query, _libraryManager, _config);
         }
         }
 
 
         public static QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items,
         public static QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items,
             BaseItem queryParent,
             BaseItem queryParent,
             int? totalRecordLimit,
             int? totalRecordLimit,
             InternalItemsQuery query,
             InternalItemsQuery query,
-            ILibraryManager libraryManager)
+            ILibraryManager libraryManager,
+            IServerConfigurationManager configurationManager)
         {
         {
             var user = query.User;
             var user = query.User;
 
 
@@ -798,7 +801,7 @@ namespace MediaBrowser.Controller.Entities
                 query.IsVirtualUnaired,
                 query.IsVirtualUnaired,
                 query.IsUnaired);
                 query.IsUnaired);
 
 
-            items = CollapseBoxSetItemsIfNeeded(items, query, queryParent, user);
+            items = CollapseBoxSetItemsIfNeeded(items, query, queryParent, user, configurationManager);
 
 
             // This must be the last filter
             // This must be the last filter
             if (!string.IsNullOrEmpty(query.AdjacentTo))
             if (!string.IsNullOrEmpty(query.AdjacentTo))
@@ -812,14 +815,15 @@ namespace MediaBrowser.Controller.Entities
         public static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(IEnumerable<BaseItem> items,
         public static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(IEnumerable<BaseItem> items,
             InternalItemsQuery query,
             InternalItemsQuery query,
             BaseItem queryParent,
             BaseItem queryParent,
-            User user)
+            User user,
+            IServerConfigurationManager configurationManager)
         {
         {
             if (items == null)
             if (items == null)
             {
             {
                 throw new ArgumentNullException("items");
                 throw new ArgumentNullException("items");
             }
             }
 
 
-            if (CollapseBoxSetItems(query, queryParent, user))
+            if (CollapseBoxSetItems(query, queryParent, user, configurationManager))
             {
             {
                 items = BaseItem.CollectionManager.CollapseItemsWithinBoxSets(items, user);
                 items = BaseItem.CollectionManager.CollapseItemsWithinBoxSets(items, user);
             }
             }
@@ -852,7 +856,8 @@ namespace MediaBrowser.Controller.Entities
 
 
         public static bool CollapseBoxSetItems(InternalItemsQuery query,
         public static bool CollapseBoxSetItems(InternalItemsQuery query,
             BaseItem queryParent,
             BaseItem queryParent,
-            User user)
+            User user,
+            IServerConfigurationManager configurationManager)
         {
         {
             // Could end up stuck in a loop like this
             // Could end up stuck in a loop like this
             if (queryParent is BoxSet)
             if (queryParent is BoxSet)
@@ -864,7 +869,7 @@ namespace MediaBrowser.Controller.Entities
 
 
             if (!param.HasValue)
             if (!param.HasValue)
             {
             {
-                if (user != null && !user.Configuration.GroupMoviesIntoBoxSets)
+                if (user != null && !configurationManager.Configuration.EnableGroupingIntoCollections)
                 {
                 {
                     return false;
                     return false;
                 }
                 }
@@ -1646,12 +1651,7 @@ namespace MediaBrowser.Controller.Entities
             var tags = query.Tags;
             var tags = query.Tags;
             if (tags.Length > 0)
             if (tags.Length > 0)
             {
             {
-                var hasTags = item as IHasTags;
-                if (hasTags == null)
-                {
-                    return false;
-                }
-                if (!tags.Any(v => hasTags.Tags.Contains(v, StringComparer.OrdinalIgnoreCase)))
+                if (!tags.Any(v => item.Tags.Contains(v, StringComparer.OrdinalIgnoreCase)))
                 {
                 {
                     return false;
                     return false;
                 }
                 }

+ 0 - 1
MediaBrowser.Controller/Entities/Video.cs

@@ -21,7 +21,6 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     /// </summary>
     public class Video : BaseItem,
     public class Video : BaseItem,
         IHasAspectRatio,
         IHasAspectRatio,
-        IHasTags,
         ISupportsPlaceHolders,
         ISupportsPlaceHolders,
         IHasMediaSources,
         IHasMediaSources,
         IHasShortOverview,
         IHasShortOverview,

+ 1 - 1
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -154,7 +154,6 @@
     <Compile Include="Entities\IHasSpecialFeatures.cs" />
     <Compile Include="Entities\IHasSpecialFeatures.cs" />
     <Compile Include="Entities\IHasStartDate.cs" />
     <Compile Include="Entities\IHasStartDate.cs" />
     <Compile Include="Entities\IHasTaglines.cs" />
     <Compile Include="Entities\IHasTaglines.cs" />
-    <Compile Include="Entities\IHasTags.cs" />
     <Compile Include="Entities\IHasThemeMedia.cs" />
     <Compile Include="Entities\IHasThemeMedia.cs" />
     <Compile Include="Entities\IHasTrailers.cs" />
     <Compile Include="Entities\IHasTrailers.cs" />
     <Compile Include="Entities\IHasUserData.cs" />
     <Compile Include="Entities\IHasUserData.cs" />
@@ -177,6 +176,7 @@
     <Compile Include="Entities\PhotoAlbum.cs" />
     <Compile Include="Entities\PhotoAlbum.cs" />
     <Compile Include="Entities\Share.cs" />
     <Compile Include="Entities\Share.cs" />
     <Compile Include="Entities\SourceType.cs" />
     <Compile Include="Entities\SourceType.cs" />
+    <Compile Include="Entities\TagExtensions.cs" />
     <Compile Include="Entities\UserView.cs" />
     <Compile Include="Entities\UserView.cs" />
     <Compile Include="Entities\UserViewBuilder.cs" />
     <Compile Include="Entities\UserViewBuilder.cs" />
     <Compile Include="FileOrganization\IFileOrganizationService.cs" />
     <Compile Include="FileOrganization\IFileOrganizationService.cs" />

+ 2 - 0
MediaBrowser.Controller/Persistence/IUserDataRepository.cs

@@ -29,6 +29,8 @@ namespace MediaBrowser.Controller.Persistence
         /// <returns>Task{UserItemData}.</returns>
         /// <returns>Task{UserItemData}.</returns>
         UserItemData GetUserData(Guid userId, string key);
         UserItemData GetUserData(Guid userId, string key);
 
 
+        UserItemData GetUserData(Guid userId, List<string> keys);
+
         /// <summary>
         /// <summary>
         /// Return all user data associated with the given user
         /// Return all user data associated with the given user
         /// </summary>
         /// </summary>

+ 2 - 6
MediaBrowser.Controller/Providers/BaseItemXmlParser.cs

@@ -803,11 +803,7 @@ namespace MediaBrowser.Controller.Providers
                     {
                     {
                         using (var subtree = reader.ReadSubtree())
                         using (var subtree = reader.ReadSubtree())
                         {
                         {
-                            var hasTags = item as IHasTags;
-                            if (hasTags != null)
-                            {
-                                FetchFromTagsNode(subtree, hasTags);
-                            }
+                            FetchFromTagsNode(subtree, item);
                         }
                         }
                         break;
                         break;
                     }
                     }
@@ -1066,7 +1062,7 @@ namespace MediaBrowser.Controller.Providers
             }
             }
         }
         }
 
 
-        private void FetchFromTagsNode(XmlReader reader, IHasTags item)
+        private void FetchFromTagsNode(XmlReader reader, BaseItem item)
         {
         {
             reader.MoveToContent();
             reader.MoveToContent();
 
 

+ 7 - 11
MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs

@@ -593,20 +593,16 @@ namespace MediaBrowser.LocalMetadata.Savers
                 builder.Append("</Studios>");
                 builder.Append("</Studios>");
             }
             }
 
 
-            var hasTags = item as IHasTags;
-            if (hasTags != null)
+            if (item.Tags.Count > 0)
             {
             {
-                if (hasTags.Tags.Count > 0)
-                {
-                    builder.Append("<Tags>");
-
-                    foreach (var tag in hasTags.Tags)
-                    {
-                        builder.Append("<Tag>" + SecurityElement.Escape(tag) + "</Tag>");
-                    }
+                builder.Append("<Tags>");
 
 
-                    builder.Append("</Tags>");
+                foreach (var tag in item.Tags)
+                {
+                    builder.Append("<Tag>" + SecurityElement.Escape(tag) + "</Tag>");
                 }
                 }
+
+                builder.Append("</Tags>");
             }
             }
 
 
             if (item.Keywords.Count > 0)
             if (item.Keywords.Count > 0)

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

@@ -198,6 +198,8 @@ namespace MediaBrowser.Model.Configuration
         public bool EnableAnonymousUsageReporting { get; set; }
         public bool EnableAnonymousUsageReporting { get; set; }
         public bool EnableStandaloneMusicKeys { get; set; }
         public bool EnableStandaloneMusicKeys { get; set; }
         public bool EnableLocalizedGuids { get; set; }
         public bool EnableLocalizedGuids { get; set; }
+        public bool EnableFolderView { get; set; }
+        public bool EnableGroupingIntoCollections { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.

+ 2 - 1
MediaBrowser.Model/Configuration/UserConfiguration.cs

@@ -48,7 +48,8 @@ namespace MediaBrowser.Model.Configuration
         public bool RememberAudioSelections { get; set; }
         public bool RememberAudioSelections { get; set; }
         public bool RememberSubtitleSelections { get; set; }
         public bool RememberSubtitleSelections { get; set; }
         public bool EnableNextEpisodeAutoPlay { get; set; }
         public bool EnableNextEpisodeAutoPlay { get; set; }
-    
+        public bool DisplayFoldersView { get; set; }
+
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="UserConfiguration" /> class.
         /// Initializes a new instance of the <see cref="UserConfiguration" /> class.
         /// </summary>
         /// </summary>

+ 12 - 12
MediaBrowser.Model/Entities/ImageType.cs

@@ -9,50 +9,50 @@ namespace MediaBrowser.Model.Entities
         /// <summary>
         /// <summary>
         /// The primary
         /// The primary
         /// </summary>
         /// </summary>
-        Primary,
+        Primary = 0,
         /// <summary>
         /// <summary>
         /// The art
         /// The art
         /// </summary>
         /// </summary>
-        Art,
+        Art = 1,
         /// <summary>
         /// <summary>
         /// The backdrop
         /// The backdrop
         /// </summary>
         /// </summary>
-        Backdrop,
+        Backdrop = 2,
         /// <summary>
         /// <summary>
         /// The banner
         /// The banner
         /// </summary>
         /// </summary>
-        Banner,
+        Banner = 3,
         /// <summary>
         /// <summary>
         /// The logo
         /// The logo
         /// </summary>
         /// </summary>
-        Logo,
+        Logo = 4,
         /// <summary>
         /// <summary>
         /// The thumb
         /// The thumb
         /// </summary>
         /// </summary>
-        Thumb,
+        Thumb = 5,
         /// <summary>
         /// <summary>
         /// The disc
         /// The disc
         /// </summary>
         /// </summary>
-        Disc,
+        Disc = 6,
         /// <summary>
         /// <summary>
         /// The box
         /// The box
         /// </summary>
         /// </summary>
-        Box,
+        Box = 7,
         /// <summary>
         /// <summary>
         /// The screenshot
         /// The screenshot
         /// </summary>
         /// </summary>
-        Screenshot,
+        Screenshot = 8,
         /// <summary>
         /// <summary>
         /// The menu
         /// The menu
         /// </summary>
         /// </summary>
-        Menu,
+        Menu = 9,
         /// <summary>
         /// <summary>
         /// The chapter image
         /// The chapter image
         /// </summary>
         /// </summary>
-        Chapter,
+        Chapter = 10,
         /// <summary>
         /// <summary>
         /// The box rear
         /// The box rear
         /// </summary>
         /// </summary>
-        BoxRear
+        BoxRear = 11
     }
     }
 }
 }

+ 6 - 0
MediaBrowser.Model/LiveTv/LiveTvOptions.cs

@@ -73,9 +73,15 @@ namespace MediaBrowser.Model.LiveTv
 
 
         public string[] EnabledTuners { get; set; }
         public string[] EnabledTuners { get; set; }
         public bool EnableAllTuners { get; set; }
         public bool EnableAllTuners { get; set; }
+        public string[] NewsGenres { get; set; }
+        public string[] SportsGenres { get; set; }
+        public string[] KidsGenres { get; set; }
 
 
         public ListingsProviderInfo()
         public ListingsProviderInfo()
         {
         {
+            NewsGenres = new string[] { "news" };
+            SportsGenres = new string[] { "sports", "basketball", "baseball", "football" };
+            KidsGenres = new string[] { "kids", "family", "children" };
             EnabledTuners = new string[] { };
             EnabledTuners = new string[] { };
             EnableAllTuners = true;
             EnableAllTuners = true;
         }
         }

+ 2 - 8
MediaBrowser.Providers/Manager/ProviderUtils.cs

@@ -151,15 +151,9 @@ namespace MediaBrowser.Providers.Manager
 
 
             if (!lockedFields.Contains(MetadataFields.Tags))
             if (!lockedFields.Contains(MetadataFields.Tags))
             {
             {
-                var sourceHasTags = source as IHasTags;
-                var targetHasTags = target as IHasTags;
-
-                if (sourceHasTags != null && targetHasTags != null)
+                if (replaceData || target.Tags.Count == 0)
                 {
                 {
-                    if (replaceData || targetHasTags.Tags.Count == 0)
-                    {
-                        targetHasTags.Tags = sourceHasTags.Tags;
-                    }
+                    target.Tags = source.Tags;
                 }
                 }
             }
             }
 
 

+ 1 - 1
MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs

@@ -25,7 +25,7 @@ namespace MediaBrowser.Providers.Music
         private readonly IApplicationHost _appHost;
         private readonly IApplicationHost _appHost;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
 
 
-        public static string MusicBrainzBaseUrl = "http://musicbrainz.fercasas.com:5000";
+        public static string MusicBrainzBaseUrl = "https://www.musicbrainz.org";
 
 
         public MusicBrainzAlbumProvider(IHttpClient httpClient, IApplicationHost appHost, ILogger logger)
         public MusicBrainzAlbumProvider(IHttpClient httpClient, IApplicationHost appHost, ILogger logger)
         {
         {

+ 27 - 21
MediaBrowser.Providers/Omdb/OmdbImageProvider.cs

@@ -1,4 +1,6 @@
-using MediaBrowser.Common.Net;
+using CommonIO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
@@ -6,7 +8,10 @@ using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.IO;
+using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
@@ -15,10 +20,16 @@ namespace MediaBrowser.Providers.Omdb
     public class OmdbImageProvider : IRemoteImageProvider, IHasOrder
     public class OmdbImageProvider : IRemoteImageProvider, IHasOrder
     {
     {
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
+        private readonly IJsonSerializer _jsonSerializer;
+        private readonly IFileSystem _fileSystem;
+        private readonly IServerConfigurationManager _configurationManager;
 
 
-        public OmdbImageProvider(IHttpClient httpClient)
+        public OmdbImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
         {
         {
+            _jsonSerializer = jsonSerializer;
             _httpClient = httpClient;
             _httpClient = httpClient;
+            _fileSystem = fileSystem;
+            _configurationManager = configurationManager;
         }
         }
 
 
         public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
         public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
@@ -29,22 +40,29 @@ namespace MediaBrowser.Providers.Omdb
             };
             };
         }
         }
 
 
-        public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
         {
             var imdbId = item.GetProviderId(MetadataProviders.Imdb);
             var imdbId = item.GetProviderId(MetadataProviders.Imdb);
 
 
             var list = new List<RemoteImageInfo>();
             var list = new List<RemoteImageInfo>();
 
 
+            var provider = new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _configurationManager);
+
             if (!string.IsNullOrWhiteSpace(imdbId))
             if (!string.IsNullOrWhiteSpace(imdbId))
             {
             {
-                list.Add(new RemoteImageInfo
+                OmdbProvider.RootObject rootObject = await provider.GetRootObject(imdbId, cancellationToken).ConfigureAwait(false);
+
+                if (!string.IsNullOrEmpty(rootObject.Poster))
                 {
                 {
-                    ProviderName = Name,
-                    Url = string.Format("https://img.omdbapi.com/?i={0}&apikey=82e83907", imdbId)
-                });
+                    list.Add(new RemoteImageInfo
+                    {
+                        ProviderName = Name,
+                        Url = string.Format("https://img.omdbapi.com/?i={0}&apikey=82e83907", imdbId)
+                    });
+                }
             }
             }
 
 
-            return Task.FromResult<IEnumerable<RemoteImageInfo>>(list);
+            return list;
         }
         }
 
 
         public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
         public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
@@ -65,18 +83,6 @@ namespace MediaBrowser.Providers.Omdb
 
 
         public bool Supports(IHasImages item)
         public bool Supports(IHasImages item)
         {
         {
-            // We'll hammer Omdb if we enable this
-            if (item is Person)
-            {
-                return false;
-            }
-
-            // Save the http requests since we know it's not currently supported
-            if (item is Season || item is Episode)
-            {
-                return false;
-            }
-
             // Supports images for tv movies
             // Supports images for tv movies
             var tvProgram = item as LiveTvProgram;
             var tvProgram = item as LiveTvProgram;
             if (tvProgram != null && tvProgram.IsMovie)
             if (tvProgram != null && tvProgram.IsMovie)
@@ -84,7 +90,7 @@ namespace MediaBrowser.Providers.Omdb
                 return true;
                 return true;
             }
             }
 
 
-            return item is Movie || item is Trailer;
+            return item is Movie || item is Trailer || item is Episode;
         }
         }
 
 
         public int Order
         public int Order

+ 10 - 4
MediaBrowser.Providers/Omdb/OmdbItemProvider.cs

@@ -1,4 +1,6 @@
-using MediaBrowser.Common.Net;
+using CommonIO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
@@ -26,13 +28,17 @@ namespace MediaBrowser.Providers.Omdb
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
+        private readonly IFileSystem _fileSystem;
+        private readonly IServerConfigurationManager _configurationManager;
 
 
-        public OmdbItemProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager)
+        public OmdbItemProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
         {
         {
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
             _httpClient = httpClient;
             _httpClient = httpClient;
             _logger = logger;
             _logger = logger;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
+            _fileSystem = fileSystem;
+            _configurationManager = configurationManager;
         }
         }
 
 
         public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
         public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
@@ -220,7 +226,7 @@ namespace MediaBrowser.Providers.Omdb
                 result.Item.SetProviderId(MetadataProviders.Imdb, imdbId);
                 result.Item.SetProviderId(MetadataProviders.Imdb, imdbId);
                 result.HasMetadata = true;
                 result.HasMetadata = true;
 
 
-                await new OmdbProvider(_jsonSerializer, _httpClient).Fetch(result.Item, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+                await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _configurationManager).Fetch(result.Item, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
             }
             }
 
 
             return result;
             return result;
@@ -259,7 +265,7 @@ namespace MediaBrowser.Providers.Omdb
                 result.Item.SetProviderId(MetadataProviders.Imdb, imdbId);
                 result.Item.SetProviderId(MetadataProviders.Imdb, imdbId);
                 result.HasMetadata = true;
                 result.HasMetadata = true;
 
 
-                await new OmdbProvider(_jsonSerializer, _httpClient).Fetch(result.Item, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+                await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _configurationManager).Fetch(result.Item, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
             }
             }
 
 
             return result;
             return result;

+ 89 - 29
MediaBrowser.Providers/Omdb/OmdbProvider.cs

@@ -1,11 +1,16 @@
-using MediaBrowser.Common.Net;
+using CommonIO;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
+using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Net;
 using System.Net;
+using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
@@ -15,17 +20,17 @@ namespace MediaBrowser.Providers.Omdb
     {
     {
         internal static readonly SemaphoreSlim ResourcePool = new SemaphoreSlim(1, 1);
         internal static readonly SemaphoreSlim ResourcePool = new SemaphoreSlim(1, 1);
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
+        private readonly IFileSystem _fileSystem;
+        private readonly IServerConfigurationManager _configurationManager;
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
 
-        public static OmdbProvider Current;
-
-        public OmdbProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient)
+        public OmdbProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
         {
         {
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
             _httpClient = httpClient;
             _httpClient = httpClient;
-
-            Current = this;
+            _fileSystem = fileSystem;
+            _configurationManager = configurationManager;
         }
         }
 
 
         public async Task Fetch(BaseItem item, string imdbId, string language, string country, CancellationToken cancellationToken)
         public async Task Fetch(BaseItem item, string imdbId, string language, string country, CancellationToken cancellationToken)
@@ -35,19 +40,7 @@ namespace MediaBrowser.Providers.Omdb
                 throw new ArgumentNullException("imdbId");
                 throw new ArgumentNullException("imdbId");
             }
             }
 
 
-            var imdbParam = imdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? imdbId : "tt" + imdbId;
-
-            var url = string.Format("https://www.omdbapi.com/?i={0}&tomatoes=true", imdbParam);
-
-            using (var stream = await _httpClient.Get(new HttpRequestOptions
-            {
-                Url = url,
-                ResourcePool = ResourcePool,
-                CancellationToken = cancellationToken
-
-            }).ConfigureAwait(false))
-            {
-                var result = _jsonSerializer.DeserializeFromStream<RootObject>(stream);
+            var result = await GetRootObject(imdbId, cancellationToken);
 
 
                 // Only take the name and rating if the user's language is set to english, since Omdb has no localization
                 // Only take the name and rating if the user's language is set to english, since Omdb has no localization
                 if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
                 if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
@@ -84,7 +77,6 @@ namespace MediaBrowser.Providers.Omdb
                     }
                     }
 
 
                     if (!string.IsNullOrEmpty(result.tomatoConsensus)
                     if (!string.IsNullOrEmpty(result.tomatoConsensus)
-                        && !string.Equals(result.tomatoConsensus, "n/a", StringComparison.OrdinalIgnoreCase)
                         && !string.Equals(result.tomatoConsensus, "No consensus yet.", StringComparison.OrdinalIgnoreCase))
                         && !string.Equals(result.tomatoConsensus, "No consensus yet.", StringComparison.OrdinalIgnoreCase))
                     {
                     {
                         hasCriticRating.CriticRatingSummary = WebUtility.HtmlDecode(result.tomatoConsensus);
                         hasCriticRating.CriticRatingSummary = WebUtility.HtmlDecode(result.tomatoConsensus);
@@ -109,20 +101,90 @@ namespace MediaBrowser.Providers.Omdb
                     item.CommunityRating = imdbRating;
                     item.CommunityRating = imdbRating;
                 }
                 }
 
 
-                if (!string.IsNullOrEmpty(result.Website)
-                        && !string.Equals(result.Website, "n/a", StringComparison.OrdinalIgnoreCase))
+                if (!string.IsNullOrEmpty(result.Website))
                 {
                 {
                     item.HomePageUrl = result.Website;
                     item.HomePageUrl = result.Website;
                 }
                 }
 
 
-                if (!string.IsNullOrWhiteSpace(result.imdbID)
-                        && !string.Equals(result.imdbID, "n/a", StringComparison.OrdinalIgnoreCase))
+                if (!string.IsNullOrWhiteSpace(result.imdbID))
                 {
                 {
                     item.SetProviderId(MetadataProviders.Imdb, result.imdbID);
                     item.SetProviderId(MetadataProviders.Imdb, result.imdbID);
                 }
                 }
 
 
                 ParseAdditionalMetadata(item, result);
                 ParseAdditionalMetadata(item, result);
+        }
+
+        internal async Task<RootObject> GetRootObject(string imdbId, CancellationToken cancellationToken)
+        {
+            var path = await EnsureItemInfo(imdbId, cancellationToken);
+
+            string resultString;
+
+            using (Stream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 131072))
+            {
+                using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
+                {
+                    resultString = reader.ReadToEnd();
+                    resultString = resultString.Replace("\"N/A\"", "\"\"");
+                }
             }
             }
+
+            var result = _jsonSerializer.DeserializeFromString<RootObject>(resultString);
+            return result;
+        }
+
+        private async Task<string> EnsureItemInfo(string imdbId, CancellationToken cancellationToken)
+        {
+            if (string.IsNullOrWhiteSpace(imdbId))
+            {
+                throw new ArgumentNullException("imdbId");
+            }
+
+            var imdbParam = imdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? imdbId : "tt" + imdbId;
+
+            var path = GetDataFilePath(imdbParam);
+
+            var fileInfo = _fileSystem.GetFileSystemInfo(path);
+
+            if (fileInfo.Exists)
+            {
+                // If it's recent or automatic updates are enabled, don't re-download
+                if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 3)
+                {
+                    return path;
+                }
+            }
+
+            var url = string.Format("https://www.omdbapi.com/?i={0}&tomatoes=true", imdbParam);
+
+            using (var stream = await _httpClient.Get(new HttpRequestOptions
+            {
+                Url = url,
+                ResourcePool = ResourcePool,
+                CancellationToken = cancellationToken
+
+            }).ConfigureAwait(false))
+            {
+                var rootObject = _jsonSerializer.DeserializeFromStream<RootObject>(stream);
+                _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
+                _jsonSerializer.SerializeToFile(rootObject, path);
+            }
+
+            return path;
+        }
+
+        internal string GetDataFilePath(string imdbId)
+        {
+            if (string.IsNullOrEmpty(imdbId))
+            {
+                throw new ArgumentNullException("imdbId");
+            }
+
+            var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb");
+
+            var filename = string.Format("{0}.json", imdbId);
+
+            return Path.Combine(dataPath, filename);
         }
         }
 
 
         private void ParseAdditionalMetadata(BaseItem item, RootObject result)
         private void ParseAdditionalMetadata(BaseItem item, RootObject result)
@@ -130,8 +192,7 @@ namespace MediaBrowser.Providers.Omdb
             // Grab series genres because imdb data is better than tvdb. Leave movies alone
             // Grab series genres because imdb data is better than tvdb. Leave movies alone
             // But only do it if english is the preferred language because this data will not be localized
             // But only do it if english is the preferred language because this data will not be localized
             if (ShouldFetchGenres(item) &&
             if (ShouldFetchGenres(item) &&
-                !string.IsNullOrWhiteSpace(result.Genre) &&
-                !string.Equals(result.Genre, "n/a", StringComparison.OrdinalIgnoreCase))
+                !string.IsNullOrWhiteSpace(result.Genre))
             {
             {
                 item.Genres.Clear();
                 item.Genres.Clear();
 
 
@@ -156,8 +217,7 @@ namespace MediaBrowser.Providers.Omdb
             }
             }
 
 
             var hasAwards = item as IHasAwards;
             var hasAwards = item as IHasAwards;
-            if (hasAwards != null && !string.IsNullOrEmpty(result.Awards) &&
-                !string.Equals(result.Awards, "n/a", StringComparison.OrdinalIgnoreCase))
+            if (hasAwards != null && !string.IsNullOrEmpty(result.Awards))
             {
             {
                 hasAwards.AwardSummary = WebUtility.HtmlDecode(result.Awards);
                 hasAwards.AwardSummary = WebUtility.HtmlDecode(result.Awards);
             }
             }
@@ -178,7 +238,7 @@ namespace MediaBrowser.Providers.Omdb
             return string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase);
             return string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase);
         }
         }
 
 
-        private class RootObject
+        internal class RootObject
         {
         {
             public string Title { get; set; }
             public string Title { get; set; }
             public string Year { get; set; }
             public string Year { get; set; }

+ 10 - 4
MediaBrowser.Providers/TV/Omdb/OmdbEpisodeProvider.cs

@@ -1,4 +1,6 @@
-using MediaBrowser.Common.Net;
+using CommonIO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
@@ -21,12 +23,16 @@ namespace MediaBrowser.Providers.TV
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
         private readonly OmdbItemProvider _itemProvider;
         private readonly OmdbItemProvider _itemProvider;
+        private readonly IFileSystem _fileSystem;
+        private readonly IServerConfigurationManager _configurationManager;
 
 
-        public OmdbEpisodeProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager)
+        public OmdbEpisodeProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
         {
         {
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
             _httpClient = httpClient;
             _httpClient = httpClient;
-            _itemProvider = new OmdbItemProvider(jsonSerializer, httpClient, logger, libraryManager);
+            _fileSystem = fileSystem;
+            _configurationManager = configurationManager;
+            _itemProvider = new OmdbItemProvider(jsonSerializer, httpClient, logger, libraryManager, fileSystem, configurationManager);
         }
         }
 
 
         public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
         public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
@@ -58,7 +64,7 @@ namespace MediaBrowser.Providers.TV
                 result.Item.SetProviderId(MetadataProviders.Imdb, imdbId);
                 result.Item.SetProviderId(MetadataProviders.Imdb, imdbId);
                 result.HasMetadata = true;
                 result.HasMetadata = true;
 
 
-                await new OmdbProvider(_jsonSerializer, _httpClient).Fetch(result.Item, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+                await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _configurationManager).Fetch(result.Item, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
             }
             }
 
 
             return result;
             return result;

+ 16 - 0
MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeProvider.cs

@@ -114,6 +114,22 @@ namespace MediaBrowser.Providers.TV
                 item.CommunityRating = (float)response.vote_average;
                 item.CommunityRating = (float)response.vote_average;
                 item.VoteCount = response.vote_count;
                 item.VoteCount = response.vote_count;
 
 
+                if (response.videos != null && response.videos.results != null)
+                {
+                    foreach (var video in response.videos.results)
+                    {
+                        if (video.type.Equals("trailer", System.StringComparison.OrdinalIgnoreCase) 
+                            || video.type.Equals("clip", System.StringComparison.OrdinalIgnoreCase))
+                        {
+                            if (video.site.Equals("youtube", System.StringComparison.OrdinalIgnoreCase))
+                            {
+                                var videoUrl = string.Format("http://www.youtube.com/watch?v={0}", video.key);
+                                item.AddTrailerUrl(videoUrl, true);
+                            }
+                        }
+                    }
+                }
+
                 result.ResetPeople();
                 result.ResetPeople();
 
 
                 var credits = response.credits;
                 var credits = response.credits;

+ 13 - 1
MediaBrowser.Providers/TV/TheMovieDb/MovieDbProviderBase.cs

@@ -210,7 +210,19 @@ namespace MediaBrowser.Providers.TV
 
 
         public class Videos
         public class Videos
         {
         {
-            public List<object> results { get; set; }
+            public List<Video> results { get; set; }
+        }
+
+        public class Video
+        {
+            public string id { get; set; }
+            public string iso_639_1 { get; set; }
+            public string iso_3166_1 { get; set; }
+            public string key { get; set; }
+            public string name { get; set; }
+            public string site { get; set; }
+            public string size { get; set; }
+            public string type { get; set; }
         }
         }
 
 
         public class RootObject
         public class RootObject

+ 1 - 1
MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeasonProvider.cs

@@ -58,7 +58,7 @@ namespace MediaBrowser.Providers.TV
 
 
                     result.HasMetadata = true;
                     result.HasMetadata = true;
                     result.Item = new Season();
                     result.Item = new Season();
-                    result.Item.Name = info.Name;
+                    result.Item.Name = seasonInfo.name;
                     result.Item.IndexNumber = seasonNumber;
                     result.Item.IndexNumber = seasonNumber;
 
 
                     result.Item.Overview = seasonInfo.overview;
                     result.Item.Overview = seasonInfo.overview;

+ 30 - 3
MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeriesProvider.cs

@@ -168,7 +168,7 @@ namespace MediaBrowser.Providers.TV
             {
             {
                 cancellationToken.ThrowIfCancellationRequested();
                 cancellationToken.ThrowIfCancellationRequested();
 
 
-                result.Item = await FetchMovieData(tmdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+                result.Item = await FetchSeriesData(tmdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
 
 
                 result.HasMetadata = result.Item != null;
                 result.HasMetadata = result.Item != null;
             }
             }
@@ -176,7 +176,7 @@ namespace MediaBrowser.Providers.TV
             return result;
             return result;
         }
         }
 
 
-        private async Task<Series> FetchMovieData(string tmdbId, string language, string preferredCountryCode, CancellationToken cancellationToken)
+        private async Task<Series> FetchSeriesData(string tmdbId, string language, string preferredCountryCode, CancellationToken cancellationToken)
         {
         {
             string dataFilePath = null;
             string dataFilePath = null;
             RootObject seriesInfo = null;
             RootObject seriesInfo = null;
@@ -285,6 +285,21 @@ namespace MediaBrowser.Providers.TV
                 series.OfficialRating = minimumRelease.rating;
                 series.OfficialRating = minimumRelease.rating;
             }
             }
 
 
+            if (seriesInfo.videos != null && seriesInfo.videos.results != null)
+            {
+                foreach (var video in seriesInfo.videos.results)
+                {
+                    if (video.type.Equals("trailer", System.StringComparison.OrdinalIgnoreCase)
+                        || video.type.Equals("clip", System.StringComparison.OrdinalIgnoreCase))
+                    {
+                        if (video.site.Equals("youtube", System.StringComparison.OrdinalIgnoreCase))
+                        {
+                            var videoUrl = string.Format("http://www.youtube.com/watch?v={0}", video.key);
+                            series.AddTrailerUrl(videoUrl, true);
+                        }
+                    }
+                }
+            }
         }
         }
 
 
         internal static string GetSeriesDataPath(IApplicationPaths appPaths, string tmdbId)
         internal static string GetSeriesDataPath(IApplicationPaths appPaths, string tmdbId)
@@ -555,7 +570,19 @@ namespace MediaBrowser.Providers.TV
 
 
         public class Videos
         public class Videos
         {
         {
-            public List<object> results { get; set; }
+            public List<Video> results { get; set; }
+        }
+
+        public class Video
+        {
+            public string id { get; set; }
+            public string iso_639_1 { get; set; }
+            public string iso_3166_1 { get; set; }
+            public string key { get; set; }
+            public string name { get; set; }
+            public string site { get; set; }
+            public string size { get; set; }
+            public string type { get; set; }
         }
         }
 
 
         public class ContentRating
         public class ContentRating

+ 1 - 1
MediaBrowser.Providers/TV/TvExternalIds.cs

@@ -88,7 +88,7 @@ namespace MediaBrowser.Providers.TV
 
 
         public string UrlFormatString
         public string UrlFormatString
         {
         {
-            get { return null; }
+            get { return "https://thetvdb.com/index.php?tab=episode&id={0}"; }
         }
         }
 
 
         public bool Supports(IHasProviderIds item)
         public bool Supports(IHasProviderIds item)

+ 0 - 1
MediaBrowser.Server.Implementations/Connect/Responses.cs

@@ -60,7 +60,6 @@ namespace MediaBrowser.Server.Implementations.Connect
         {
         {
             return new ConnectUserPreferences
             return new ConnectUserPreferences
             {
             {
-                GroupMoviesIntoBoxSets = config.GroupMoviesIntoBoxSets,
                 PlayDefaultAudioTrack = config.PlayDefaultAudioTrack,
                 PlayDefaultAudioTrack = config.PlayDefaultAudioTrack,
                 SubtitleMode = config.SubtitleMode,
                 SubtitleMode = config.SubtitleMode,
                 PreferredAudioLanguages = string.IsNullOrWhiteSpace(config.AudioLanguagePreference) ? new string[] { } : new[] { config.AudioLanguagePreference },
                 PreferredAudioLanguages = string.IsNullOrWhiteSpace(config.AudioLanguagePreference) ? new string[] { } : new[] { config.AudioLanguagePreference },

+ 1 - 10
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -969,16 +969,7 @@ namespace MediaBrowser.Server.Implementations.Dto
 
 
             if (fields.Contains(ItemFields.Tags))
             if (fields.Contains(ItemFields.Tags))
             {
             {
-                var hasTags = item as IHasTags;
-                if (hasTags != null)
-                {
-                    dto.Tags = hasTags.Tags;
-                }
-
-                if (dto.Tags == null)
-                {
-                    dto.Tags = new List<string>();
-                }
+                dto.Tags = item.Tags;
             }
             }
 
 
             if (fields.Contains(ItemFields.Keywords))
             if (fields.Contains(ItemFields.Keywords))

+ 4 - 3
MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs

@@ -562,9 +562,10 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
                     series = _libraryManager.GetItemList(new Controller.Entities.InternalItemsQuery
                     series = _libraryManager.GetItemList(new Controller.Entities.InternalItemsQuery
                     {
                     {
                         IncludeItemTypes = new[] { typeof(Series).Name },
                         IncludeItemTypes = new[] { typeof(Series).Name },
-                        Recursive = true
-                    }).Cast<Series>()
-                    .FirstOrDefault(i => string.Equals(i.Name, info.ItemName, StringComparison.OrdinalIgnoreCase));
+                        Recursive = true,
+                        Name = info.ItemName
+
+                    }).Cast<Series>().FirstOrDefault();
                 }
                 }
             }
             }
 
 

+ 24 - 74
MediaBrowser.Server.Implementations/Library/UserDataManager.cs

@@ -23,7 +23,8 @@ namespace MediaBrowser.Server.Implementations.Library
     {
     {
         public event EventHandler<UserDataSaveEventArgs> UserDataSaved;
         public event EventHandler<UserDataSaveEventArgs> UserDataSaved;
 
 
-        private readonly Dictionary<string, UserItemData> _userData = new Dictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
+        private readonly ConcurrentDictionary<string, UserItemData> _userData =
+            new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
 
 
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
@@ -64,13 +65,6 @@ namespace MediaBrowser.Server.Implementations.Library
                 try
                 try
                 {
                 {
                     await Repository.SaveUserData(userId, key, userData, cancellationToken).ConfigureAwait(false);
                     await Repository.SaveUserData(userId, key, userData, cancellationToken).ConfigureAwait(false);
-
-                    var newValue = userData;
-
-                    lock (_userData)
-                    {
-                        _userData[GetCacheKey(userId, key)] = newValue;
-                    }
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {
@@ -80,6 +74,9 @@ namespace MediaBrowser.Server.Implementations.Library
                 }
                 }
             }
             }
 
 
+            var cacheKey = GetCacheKey(userId, item.Id);
+            _userData.AddOrUpdate(cacheKey, userData, (k, v) => userData);
+
             EventHelper.FireEventIfNotNull(UserDataSaved, this, new UserDataSaveEventArgs
             EventHelper.FireEventIfNotNull(UserDataSaved, this, new UserDataSaveEventArgs
             {
             {
                 Keys = keys,
                 Keys = keys,
@@ -122,7 +119,7 @@ namespace MediaBrowser.Server.Implementations.Library
 
 
                 throw;
                 throw;
             }
             }
-            
+
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -140,7 +137,7 @@ namespace MediaBrowser.Server.Implementations.Library
             return Repository.GetAllUserData(userId);
             return Repository.GetAllUserData(userId);
         }
         }
 
 
-        public UserItemData GetUserData(Guid userId, List<string> keys)
+        public UserItemData GetUserData(Guid userId, Guid itemId, List<string> keys)
         {
         {
             if (userId == Guid.Empty)
             if (userId == Guid.Empty)
             {
             {
@@ -150,26 +147,23 @@ namespace MediaBrowser.Server.Implementations.Library
             {
             {
                 throw new ArgumentNullException("keys");
                 throw new ArgumentNullException("keys");
             }
             }
-
-            lock (_userData)
+            if (keys.Count == 0)
             {
             {
-                foreach (var key in keys)
-                {
-                    var cacheKey = GetCacheKey(userId, key);
-                    UserItemData value;
-                    if (_userData.TryGetValue(cacheKey, out value))
-                    {
-                        return value;
-                    }
+                throw new ArgumentException("UserData keys cannot be empty.");
+            }
 
 
-                    value = Repository.GetUserData(userId, key);
+            var cacheKey = GetCacheKey(userId, itemId);
 
 
-                    if (value != null)
-                    {
-                        _userData[cacheKey] = value;
-                        return value;
-                    }
-                }
+            return _userData.GetOrAdd(cacheKey, k => GetUserDataInternal(userId, keys));
+        }
+
+        private UserItemData GetUserDataInternal(Guid userId, List<string> keys)
+        {
+            var userData = Repository.GetUserData(userId, keys);
+
+            if (userData != null)
+            {
+                return userData;
             }
             }
 
 
             if (keys.Count > 0)
             if (keys.Count > 0)
@@ -184,57 +178,13 @@ namespace MediaBrowser.Server.Implementations.Library
             return null;
             return null;
         }
         }
 
 
-        /// <summary>
-        /// Gets the user data.
-        /// </summary>
-        /// <param name="userId">The user id.</param>
-        /// <param name="key">The key.</param>
-        /// <returns>Task{UserItemData}.</returns>
-        public UserItemData GetUserData(Guid userId, string key)
-        {
-            if (userId == Guid.Empty)
-            {
-                throw new ArgumentNullException("userId");
-            }
-            if (string.IsNullOrEmpty(key))
-            {
-                throw new ArgumentNullException("key");
-            }
-
-            lock (_userData)
-            {
-                var cacheKey = GetCacheKey(userId, key);
-                UserItemData value;
-                if (_userData.TryGetValue(cacheKey, out value))
-                {
-                    return value;
-                }
-
-                value = Repository.GetUserData(userId, key);
-
-                if (value == null)
-                {
-                    value = new UserItemData
-                    {
-                        UserId = userId,
-                        Key = key
-                    };
-                }
-
-                _userData[cacheKey] = value;
-                return value;
-            }
-        }
-
         /// <summary>
         /// <summary>
         /// Gets the internal key.
         /// Gets the internal key.
         /// </summary>
         /// </summary>
-        /// <param name="userId">The user id.</param>
-        /// <param name="key">The key.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        private string GetCacheKey(Guid userId, string key)
+        private string GetCacheKey(Guid userId, Guid itemId)
         {
         {
-            return userId + key;
+            return userId.ToString("N") + itemId.ToString("N");
         }
         }
 
 
         public UserItemData GetUserData(IHasUserData user, IHasUserData item)
         public UserItemData GetUserData(IHasUserData user, IHasUserData item)
@@ -249,7 +199,7 @@ namespace MediaBrowser.Server.Implementations.Library
 
 
         public UserItemData GetUserData(Guid userId, IHasUserData item)
         public UserItemData GetUserData(Guid userId, IHasUserData item)
         {
         {
-            return GetUserData(userId, item.GetUserDataKeys());
+            return GetUserData(userId, item.Id, item.GetUserDataKeys());
         }
         }
 
 
         public UserItemDataDto GetUserDataDto(IHasUserData item, User user)
         public UserItemDataDto GetUserDataDto(IHasUserData item, User user)

+ 6 - 0
MediaBrowser.Server.Implementations/Library/UserViewManager.cs

@@ -105,6 +105,12 @@ namespace MediaBrowser.Server.Implementations.Library
                 }
                 }
             }
             }
 
 
+            if (_config.Configuration.EnableFolderView)
+            {
+                var name = _localizationManager.GetLocalizedString("ViewType" + CollectionType.Folders);
+                list.Add(await _libraryManager.GetNamedView(name, CollectionType.Folders, string.Empty, cancellationToken).ConfigureAwait(false));
+            }
+
             if (query.IncludeExternalContent)
             if (query.IncludeExternalContent)
             {
             {
                 var channelResult = await _channelManager.GetChannelsInternal(new ChannelQuery
                 var channelResult = await _channelManager.GetChannelsInternal(new ChannelQuery

+ 0 - 1
MediaBrowser.Server.Implementations/Persistence/IDbConnector.cs

@@ -6,6 +6,5 @@ namespace MediaBrowser.Server.Implementations.Persistence
     public interface IDbConnector
     public interface IDbConnector
     {
     {
         Task<IDbConnection> Connect(string dbPath);
         Task<IDbConnection> Connect(string dbPath);
-        void BindSimilarityScoreFunction(IDbConnection connection);
     }
     }
 }
 }

+ 2 - 116
MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs

@@ -5,6 +5,8 @@ using System.Data.SQLite;
 using System.Linq;
 using System.Linq;
 using System.Text;
 using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
 
 
 namespace MediaBrowser.Server.Implementations.Persistence
 namespace MediaBrowser.Server.Implementations.Persistence
@@ -46,13 +48,6 @@ namespace MediaBrowser.Server.Implementations.Persistence
             return connection;
             return connection;
         }
         }
 
 
-        public static void BindGetSimilarityScore(IDbConnection connection, ILogger logger)
-        {
-            var sqlConnection = (SQLiteConnection) connection;
-            SimiliarToFunction.Logger = logger;
-            sqlConnection.BindFunction(new SimiliarToFunction());
-        }
-
         public static void BindFunction(this SQLiteConnection connection, SQLiteFunction function)
         public static void BindFunction(this SQLiteConnection connection, SQLiteFunction function)
         {
         {
             var attributes = function.GetType().GetCustomAttributes(typeof(SQLiteFunctionAttribute), true).Cast<SQLiteFunctionAttribute>().ToArray();
             var attributes = function.GetType().GetCustomAttributes(typeof(SQLiteFunctionAttribute), true).Cast<SQLiteFunctionAttribute>().ToArray();
@@ -63,113 +58,4 @@ namespace MediaBrowser.Server.Implementations.Persistence
             connection.BindFunction(attributes[0], function);
             connection.BindFunction(attributes[0], function);
         }
         }
     }
     }
-
-    [SQLiteFunction(Name = "GetSimilarityScore", Arguments = 12, FuncType = FunctionType.Scalar)]
-    public class SimiliarToFunction : SQLiteFunction
-    {
-        internal static ILogger Logger;
-
-        public override object Invoke(object[] args)
-        {
-            var score = 0;
-
-            var inputOfficialRating = args[0] as string;
-            var rowOfficialRating = args[1] as string;
-            if (!string.IsNullOrWhiteSpace(inputOfficialRating) && string.Equals(inputOfficialRating, rowOfficialRating))
-            {
-                score += 10;
-            }
-
-            long? inputYear = args[2] == null ? (long?)null : (long)args[2];
-            long? rowYear = args[3] == null ? (long?)null : (long)args[3];
-
-            if (inputYear.HasValue && rowYear.HasValue)
-            {
-                var diff = Math.Abs(inputYear.Value - rowYear.Value);
-
-                // Add if they came out within the same decade
-                if (diff < 10)
-                {
-                    score += 2;
-                }
-
-                // And more if within five years
-                if (diff < 5)
-                {
-                    score += 2;
-                }
-            }
-
-            // genres
-            score += GetListScore(args, 4, 5);
-
-            // tags
-            score += GetListScore(args, 6, 7);
-
-            // keywords
-            score += GetListScore(args, 8, 9);
-
-            // studios
-            score += GetListScore(args, 10, 11, 3);
-
-
-            // TODO: People
-    //        var item2PeopleNames = allPeople.Where(i => i.ItemId == item2.Id)
-    //.Select(i => i.Name)
-    //.Where(i => !string.IsNullOrWhiteSpace(i))
-    //.DistinctNames()
-    //.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
-
-    //        points += item1People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i =>
-    //        {
-    //            if (string.Equals(i.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase))
-    //            {
-    //                return 5;
-    //            }
-    //            if (string.Equals(i.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
-    //            {
-    //                return 3;
-    //            }
-    //            if (string.Equals(i.Type, PersonType.Composer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Composer, StringComparison.OrdinalIgnoreCase))
-    //            {
-    //                return 3;
-    //            }
-    //            if (string.Equals(i.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
-    //            {
-    //                return 3;
-    //            }
-    //            if (string.Equals(i.Type, PersonType.Writer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase))
-    //            {
-    //                return 2;
-    //            }
-
-    //            return 1;
-    //        });
-
-    //        return points;
-
-            //Logger.Debug("Returning score {0}", score);
-            return score;
-        }
-
-        private int GetListScore(object[] args, int index1, int index2, int value = 10)
-        {
-            var score = 0;
-
-            var inputGenres = args[index1] as string;
-            var rowGenres = args[index2] as string;
-            var inputGenreList = string.IsNullOrWhiteSpace(inputGenres) ? new string[] { } : inputGenres.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
-            var rowGenresList = string.IsNullOrWhiteSpace(rowGenres) ? new string[] { } : rowGenres.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
-
-            foreach (var genre in inputGenreList)
-            {
-                if (rowGenresList.Contains(genre, StringComparer.OrdinalIgnoreCase))
-                {
-                    score += value;
-                }
-            }
-
-            return score;
-        }
-    }
 }
 }

+ 115 - 27
MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs

@@ -88,10 +88,13 @@ namespace MediaBrowser.Server.Implementations.Persistence
         private IDbCommand _deleteProviderIdsCommand;
         private IDbCommand _deleteProviderIdsCommand;
         private IDbCommand _saveProviderIdsCommand;
         private IDbCommand _saveProviderIdsCommand;
 
 
+        private IDbCommand _deleteImagesCommand;
+        private IDbCommand _saveImagesCommand;
+
         private IDbCommand _updateInheritedRatingCommand;
         private IDbCommand _updateInheritedRatingCommand;
         private IDbCommand _updateInheritedTagsCommand;
         private IDbCommand _updateInheritedTagsCommand;
 
 
-        public const int LatestSchemaVersion = 83;
+        public const int LatestSchemaVersion = 89;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
         /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
@@ -132,9 +135,9 @@ namespace MediaBrowser.Server.Implementations.Persistence
             string[] queries = {
             string[] queries = {
 
 
                                 "create table if not exists TypedBaseItems (guid GUID primary key, type TEXT, data BLOB, ParentId GUID, Path TEXT)",
                                 "create table if not exists TypedBaseItems (guid GUID primary key, type TEXT, data BLOB, ParentId GUID, Path TEXT)",
-                                "create index if not exists idx_TypedBaseItems on TypedBaseItems(guid)",
                                 "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)",
                                 "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)",
                                 "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)",
                                 "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)",
+                                "create index if not exists idx_TypedBaseItems2 on TypedBaseItems(Type,Guid)",
 
 
                                 "create table if not exists AncestorIds (ItemId GUID, AncestorId GUID, AncestorIdText TEXT, PRIMARY KEY (ItemId, AncestorId))",
                                 "create table if not exists AncestorIds (ItemId GUID, AncestorId GUID, AncestorIdText TEXT, PRIMARY KEY (ItemId, AncestorId))",
                                 "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)",
                                 "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)",
@@ -145,10 +148,14 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
 
                                 "create table if not exists ItemValues (ItemId GUID, Type INT, Value TEXT)",
                                 "create table if not exists ItemValues (ItemId GUID, Type INT, Value TEXT)",
                                 "create index if not exists idx_ItemValues on ItemValues(ItemId)",
                                 "create index if not exists idx_ItemValues on ItemValues(ItemId)",
+                                "create index if not exists idx_ItemValues2 on ItemValues(ItemId,Type)",
 
 
-                                "create table if not exists ProviderIds (ItemId GUID, Name TEXT, Value TEXT)",
+                                "create table if not exists ProviderIds (ItemId GUID, Name TEXT, Value TEXT, PRIMARY KEY (ItemId, Name))",
                                 "create index if not exists Idx_ProviderIds on ProviderIds(ItemId)",
                                 "create index if not exists Idx_ProviderIds on ProviderIds(ItemId)",
 
 
+                                "create table if not exists Images (ItemId GUID NOT NULL, Path TEXT NOT NULL, ImageType INT NOT NULL, DateModified DATETIME, IsPlaceHolder BIT NOT NULL, SortOrder INT)",
+                                "create index if not exists idx_Images on Images(ItemId)",
+
                                 "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)",
                                 "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)",
                                 "create index if not exists idxPeopleItemId on People(ItemId)",
                                 "create index if not exists idxPeopleItemId on People(ItemId)",
                                 "create index if not exists idxPeopleName on People(Name)",
                                 "create index if not exists idxPeopleName on People(Name)",
@@ -265,8 +272,6 @@ namespace MediaBrowser.Server.Implementations.Persistence
             new MediaStreamColumns(_connection, Logger).AddColumns();
             new MediaStreamColumns(_connection, Logger).AddColumns();
 
 
             DataExtensions.Attach(_connection, Path.Combine(_config.ApplicationPaths.DataPath, "userdata_v2.db"), "UserDataDb");
             DataExtensions.Attach(_connection, Path.Combine(_config.ApplicationPaths.DataPath, "userdata_v2.db"), "UserDataDb");
-
-            dbConnector.BindSimilarityScoreFunction(_connection);
         }
         }
 
 
         private readonly string[] _retriveItemColumns =
         private readonly string[] _retriveItemColumns =
@@ -565,6 +570,19 @@ namespace MediaBrowser.Server.Implementations.Persistence
             _saveProviderIdsCommand.Parameters.Add(_saveProviderIdsCommand, "@Name");
             _saveProviderIdsCommand.Parameters.Add(_saveProviderIdsCommand, "@Name");
             _saveProviderIdsCommand.Parameters.Add(_saveProviderIdsCommand, "@Value");
             _saveProviderIdsCommand.Parameters.Add(_saveProviderIdsCommand, "@Value");
 
 
+            // images
+            _deleteImagesCommand = _connection.CreateCommand();
+            _deleteImagesCommand.CommandText = "delete from Images where ItemId=@Id";
+            _deleteImagesCommand.Parameters.Add(_deleteImagesCommand, "@Id");
+
+            _saveImagesCommand = _connection.CreateCommand();
+            _saveImagesCommand.CommandText = "insert into Images (ItemId, ImageType, Path, DateModified, IsPlaceHolder, SortOrder) values (@ItemId, @ImageType, @Path, @DateModified, @IsPlaceHolder, @SortOrder)";
+            _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@ItemId");
+            _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@ImageType");
+            _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@Path");
+            _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@DateModified");
+            _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@IsPlaceHolder");
+            _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@SortOrder");
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -879,6 +897,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
                     }
                     }
 
 
                     UpdateUserDataKeys(item.Id, item.GetUserDataKeys().Distinct(StringComparer.OrdinalIgnoreCase).ToList(), transaction);
                     UpdateUserDataKeys(item.Id, item.GetUserDataKeys().Distinct(StringComparer.OrdinalIgnoreCase).ToList(), transaction);
+                    UpdateImages(item.Id, item.ImageInfos, transaction);
                     UpdateProviderIds(item.Id, item.ProviderIds, transaction);
                     UpdateProviderIds(item.Id, item.ProviderIds, transaction);
                     UpdateItemValues(item.Id, GetItemValues(item), transaction);
                     UpdateItemValues(item.Id, GetItemValues(item), transaction);
                 }
                 }
@@ -956,7 +975,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
 
             if (type == null)
             if (type == null)
             {
             {
-                Logger.Debug("Unknown type {0}", typeString);
+                //Logger.Debug("Unknown type {0}", typeString);
 
 
                 return null;
                 return null;
             }
             }
@@ -1621,34 +1640,34 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 var item = query.SimilarTo;
                 var item = query.SimilarTo;
 
 
                 var builder = new StringBuilder();
                 var builder = new StringBuilder();
-                builder.Append("GetSimilarityScore(");
+                builder.Append("(");
+
+                builder.Append("((OfficialRating=@ItemOfficialRating) * 10)");
+                //builder.Append("+ ((ProductionYear=@ItemProductionYear) * 10)");
+
+                builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 10 Then 2 Else 0 End )");
+                builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 2 Else 0 End )");
+
+                //// genres
+                builder.Append("+ ((Select count(value) from ItemValues where ItemId=Guid and Type=2 and value in (select value from itemvalues where ItemId=@SimilarItemId and type=2)) * 10)");
 
 
-                builder.Append("@ItemOfficialRating,");
-                builder.Append("OfficialRating,");
+                //// tags
+                builder.Append("+ ((Select count(value) from ItemValues where ItemId=Guid and Type=4 and value in (select value from itemvalues where ItemId=@SimilarItemId and type=4)) * 10)");
 
 
-                builder.Append("@ItemProductionYear,");
-                builder.Append("ProductionYear,");
+                builder.Append("+ ((Select count(value) from ItemValues where ItemId=Guid and Type=5 and value in (select value from itemvalues where ItemId=@SimilarItemId and type=5)) * 10)");
 
 
-                builder.Append("@ItemGenres,");
-                builder.Append("Genres,");
+                builder.Append("+ ((Select count(value) from ItemValues where ItemId=Guid and Type=3 and value in (select value from itemvalues where ItemId=@SimilarItemId and type=3)) * 3)");
 
 
-                builder.Append("@ItemTags,");
-                builder.Append("Tags,");
+                //builder.Append("+ ((Select count(Name) from People where ItemId=Guid and Name in (select Name from People where ItemId=@SimilarItemId)) * 3)");
 
 
-                builder.Append("@ItemKeywords,");
-                builder.Append("(select group_concat((Select Value from ItemValues where ItemId=Guid and Type=5), '|')),");
+                ////builder.Append("(select group_concat((Select Name from People where ItemId=Guid and Name in (Select Name from People where ItemId=@SimilarItemId)), '|'))");
 
 
-                builder.Append("@ItemStudios,");
-                builder.Append("Studios");
                 builder.Append(") as SimilarityScore");
                 builder.Append(") as SimilarityScore");
 
 
                 list.Add(builder.ToString());
                 list.Add(builder.ToString());
                 cmd.Parameters.Add(cmd, "@ItemOfficialRating", DbType.String).Value = item.OfficialRating;
                 cmd.Parameters.Add(cmd, "@ItemOfficialRating", DbType.String).Value = item.OfficialRating;
-                cmd.Parameters.Add(cmd, "@ItemProductionYear", DbType.Int32).Value = item.ProductionYear ?? -1;
-                cmd.Parameters.Add(cmd, "@ItemGenres", DbType.String).Value = string.Join("|", item.Genres.ToArray());
-                cmd.Parameters.Add(cmd, "@ItemTags", DbType.String).Value = string.Join("|", item.Tags.ToArray());
-                cmd.Parameters.Add(cmd, "@ItemKeywords", DbType.String).Value = string.Join("|", item.Keywords.ToArray());
-                cmd.Parameters.Add(cmd, "@ItemStudios", DbType.String).Value = string.Join("|", item.Studios.ToArray());
+                cmd.Parameters.Add(cmd, "@ItemProductionYear", DbType.Int32).Value = item.ProductionYear ?? 0;
+                cmd.Parameters.Add(cmd, "@SimilarItemId", DbType.Guid).Value = item.Id;
 
 
                 var excludeIds = query.ExcludeItemIds.ToList();
                 var excludeIds = query.ExcludeItemIds.ToList();
                 excludeIds.Add(item.Id.ToString("N"));
                 excludeIds.Add(item.Id.ToString("N"));
@@ -1862,7 +1881,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 {
                 {
                     if (query.User != null)
                     if (query.User != null)
                     {
                     {
-                        query.SortBy = new[] { "SimilarityScore", "IsUnplayed", "Random" };
+                        query.SortBy = new[] { "SimilarityScore", "IsPlayed", "Random" };
                     }
                     }
                     else
                     else
                     {
                     {
@@ -2475,6 +2494,19 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 cmd.Parameters.Add(cmd, "@NameLessThan", DbType.String).Value = query.NameLessThan.ToLower();
                 cmd.Parameters.Add(cmd, "@NameLessThan", DbType.String).Value = query.NameLessThan.ToLower();
             }
             }
 
 
+            if (query.ImageTypes.Length > 0 && _config.Configuration.SchemaVersion >= 87)
+            {
+                var requiredImageIndex = 0;
+
+                foreach (var requiredImage in query.ImageTypes)
+                {
+                    var paramName = "@RequiredImageType" + requiredImageIndex;
+                    whereClauses.Add("(select path from images where ItemId=Guid and ImageType=" + paramName + " limit 1) not null");
+                    cmd.Parameters.Add(cmd, paramName, DbType.Int32).Value = (int)requiredImage;
+                    requiredImageIndex++;
+                }
+            }
+
             if (query.IsLiked.HasValue)
             if (query.IsLiked.HasValue)
             {
             {
                 if (query.IsLiked.Value)
                 if (query.IsLiked.Value)
@@ -2738,8 +2770,13 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 var index = 0;
                 var index = 0;
                 foreach (var pair in query.ExcludeProviderIds)
                 foreach (var pair in query.ExcludeProviderIds)
                 {
                 {
+                    if (string.Equals(pair.Key, MetadataProviders.TmdbCollection.ToString(), StringComparison.OrdinalIgnoreCase))
+                    {
+                        continue;
+                    }
+
                     var paramName = "@ExcludeProviderId" + index;
                     var paramName = "@ExcludeProviderId" + index;
-                    excludeIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = 'Imdb'), '') <> " + paramName + ")");
+                    excludeIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")");
                     cmd.Parameters.Add(cmd, paramName, DbType.String).Value = pair.Value;
                     cmd.Parameters.Add(cmd, paramName, DbType.String).Value = pair.Value;
                     index++;
                     index++;
                 }
                 }
@@ -3180,6 +3217,11 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 _deleteProviderIdsCommand.Transaction = transaction;
                 _deleteProviderIdsCommand.Transaction = transaction;
                 _deleteProviderIdsCommand.ExecuteNonQuery();
                 _deleteProviderIdsCommand.ExecuteNonQuery();
 
 
+                // Delete images
+                _deleteImagesCommand.GetParameter(0).Value = id;
+                _deleteImagesCommand.Transaction = transaction;
+                _deleteImagesCommand.ExecuteNonQuery();
+
                 // Delete the item
                 // Delete the item
                 _deleteItemCommand.GetParameter(0).Value = id;
                 _deleteItemCommand.GetParameter(0).Value = id;
                 _deleteItemCommand.Transaction = transaction;
                 _deleteItemCommand.Transaction = transaction;
@@ -3396,6 +3438,52 @@ namespace MediaBrowser.Server.Implementations.Persistence
             return list;
             return list;
         }
         }
 
 
+        private void UpdateImages(Guid itemId, List<ItemImageInfo> images, IDbTransaction transaction)
+        {
+            if (itemId == Guid.Empty)
+            {
+                throw new ArgumentNullException("itemId");
+            }
+
+            if (images == null)
+            {
+                throw new ArgumentNullException("images");
+            }
+
+            CheckDisposed();
+
+            // First delete 
+            _deleteImagesCommand.GetParameter(0).Value = itemId;
+            _deleteImagesCommand.Transaction = transaction;
+
+            _deleteImagesCommand.ExecuteNonQuery();
+
+            var index = 0;
+            foreach (var image in images)
+            {
+                _saveImagesCommand.GetParameter(0).Value = itemId;
+                _saveImagesCommand.GetParameter(1).Value = image.Type;
+                _saveImagesCommand.GetParameter(2).Value = image.Path;
+
+                if (image.DateModified == default(DateTime))
+                {
+                    _saveImagesCommand.GetParameter(3).Value = null;
+                }
+                else
+                {
+                    _saveImagesCommand.GetParameter(3).Value = image.DateModified;
+                }
+
+                _saveImagesCommand.GetParameter(4).Value = image.IsPlaceholder;
+                _saveImagesCommand.GetParameter(5).Value = index;
+
+                _saveImagesCommand.Transaction = transaction;
+
+                _saveImagesCommand.ExecuteNonQuery();
+                index++;
+            }
+        }
+
         private void UpdateProviderIds(Guid itemId, Dictionary<string, string> values, IDbTransaction transaction)
         private void UpdateProviderIds(Guid itemId, Dictionary<string, string> values, IDbTransaction transaction)
         {
         {
             if (itemId == Guid.Empty)
             if (itemId == Guid.Empty)
@@ -3405,7 +3493,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
 
             if (values == null)
             if (values == null)
             {
             {
-                throw new ArgumentNullException("keys");
+                throw new ArgumentNullException("values");
             }
             }
 
 
             CheckDisposed();
             CheckDisposed();

+ 48 - 0
MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs

@@ -5,7 +5,9 @@ using MediaBrowser.Model.Logging;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Data;
 using System.Data;
+using System.Globalization;
 using System.IO;
 using System.IO;
+using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
@@ -300,6 +302,52 @@ namespace MediaBrowser.Server.Implementations.Persistence
             }
             }
         }
         }
 
 
+        public UserItemData GetUserData(Guid userId, List<string> keys)
+        {
+            if (userId == Guid.Empty)
+            {
+                throw new ArgumentNullException("userId");
+            }
+            if (keys == null)
+            {
+                throw new ArgumentNullException("keys");
+            }
+
+            using (var cmd = _connection.CreateCommand())
+            {
+                var index = 0;
+                var excludeIds = new List<string>();
+                var builder = new StringBuilder();
+                foreach (var key in keys)
+                {
+                    var paramName = "@Key" + index;
+                    excludeIds.Add("Key =" + paramName);
+                    cmd.Parameters.Add(cmd, paramName, DbType.String).Value = key;
+                    builder.Append(" WHEN Key=" + paramName + " THEN " + index);
+                    index++;
+                }
+
+                var keyText = string.Join(" OR ", excludeIds.ToArray());
+
+                cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where userId=@userId AND (" + keyText + ") ";
+
+                cmd.CommandText += " ORDER BY (Case " + builder + " Else " + keys.Count.ToString(CultureInfo.InvariantCulture) + " End )";
+                cmd.CommandText += " LIMIT 1";
+
+                cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId;
+
+                using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow))
+                {
+                    if (reader.Read())
+                    {
+                        return ReadRow(reader);
+                    }
+                }
+
+                return null;
+            }
+        }
+
         /// <summary>
         /// <summary>
         /// Return all user-data associated with the given user
         /// Return all user-data associated with the given user
         /// </summary>
         /// </summary>

+ 15 - 27
MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs

@@ -111,24 +111,6 @@ namespace MediaBrowser.Server.Implementations.TV
                 .Select(i => GetNextUp(i, currentUser))
                 .Select(i => GetNextUp(i, currentUser))
                 // Include if an episode was found, and either the series is not unwatched or the specific series was requested
                 // Include if an episode was found, and either the series is not unwatched or the specific series was requested
                 .Where(i => i.Item1 != null && (!i.Item3 || !string.IsNullOrWhiteSpace(request.SeriesId)))
                 .Where(i => i.Item1 != null && (!i.Item3 || !string.IsNullOrWhiteSpace(request.SeriesId)))
-                //.OrderByDescending(i =>
-                //{
-                //    var episode = i.Item1;
-
-                //    var seriesUserData = _userDataManager.GetUserData(user, episode.Series);
-
-                //    if (seriesUserData.IsFavorite)
-                //    {
-                //        return 2;
-                //    }
-
-                //    if (seriesUserData.Likes.HasValue)
-                //    {
-                //        return seriesUserData.Likes.Value ? 1 : -1;
-                //    }
-
-                //    return 0;
-                //})
                 .OrderByDescending(i => i.Item2)
                 .OrderByDescending(i => i.Item2)
                 .ThenByDescending(i => i.Item1.PremiereDate ?? DateTime.MinValue)
                 .ThenByDescending(i => i.Item1.PremiereDate ?? DateTime.MinValue)
                 .Select(i => i.Item1);
                 .Select(i => i.Item1);
@@ -143,9 +125,8 @@ namespace MediaBrowser.Server.Implementations.TV
         private Tuple<Episode, DateTime, bool> GetNextUp(Series series, User user)
         private Tuple<Episode, DateTime, bool> GetNextUp(Series series, User user)
         {
         {
             // Get them in display order, then reverse
             // Get them in display order, then reverse
-            var allEpisodes = series.GetSeasons(user, true, true)
-                .Where(i => !i.IndexNumber.HasValue || i.IndexNumber.Value != 0)
-                .SelectMany(i => i.GetEpisodes(user))
+            var allEpisodes = series.GetEpisodes(user, false, false)
+                .Where(i => !i.ParentIndexNumber.HasValue || i.ParentIndexNumber.Value != 0)
                 .Reverse()
                 .Reverse()
                 .ToList();
                 .ToList();
 
 
@@ -153,7 +134,7 @@ namespace MediaBrowser.Server.Implementations.TV
             var lastWatchedDate = DateTime.MinValue;
             var lastWatchedDate = DateTime.MinValue;
             Episode nextUp = null;
             Episode nextUp = null;
 
 
-            var includeMissing = user.Configuration.DisplayMissingEpisodes;
+            var unplayedEpisodes = new List<Episode>();
 
 
             // Go back starting with the most recent episodes
             // Go back starting with the most recent episodes
             foreach (var episode in allEpisodes)
             foreach (var episode in allEpisodes)
@@ -172,10 +153,9 @@ namespace MediaBrowser.Server.Implementations.TV
                 }
                 }
                 else
                 else
                 {
                 {
-                    if (!episode.IsVirtualUnaired && (includeMissing || !episode.IsMissingEpisode))
-                    {
-                        nextUp = episode;
-                    }
+                    unplayedEpisodes.Add(episode);
+
+                    nextUp = episode;
                 }
                 }
             }
             }
 
 
@@ -184,7 +164,15 @@ namespace MediaBrowser.Server.Implementations.TV
                 return new Tuple<Episode, DateTime, bool>(nextUp, lastWatchedDate, false);
                 return new Tuple<Episode, DateTime, bool>(nextUp, lastWatchedDate, false);
             }
             }
 
 
-            var firstEpisode = allEpisodes.LastOrDefault(i => !i.IsVirtualUnaired && (includeMissing || !i.IsMissingEpisode) && !i.IsPlayed(user));
+            Episode firstEpisode = null;
+            // Find the first unplayed episode. Start from the back of the list since they're in reverse order
+            for (var i = unplayedEpisodes.Count - 1; i >= 0; i--)
+            {
+                var unplayedEpisode = unplayedEpisodes[i];
+
+                firstEpisode = unplayedEpisode;
+                break;
+            }
 
 
             // Return the first episode
             // Return the first episode
             return new Tuple<Episode, DateTime, bool>(firstEpisode, DateTime.MinValue, true);
             return new Tuple<Episode, DateTime, bool>(firstEpisode, DateTime.MinValue, true);

+ 0 - 5
MediaBrowser.Server.Mono/Native/DbConnector.cs

@@ -16,11 +16,6 @@ namespace MediaBrowser.Server.Mono.Native
             _logger = logger;
             _logger = logger;
         }
         }
 
 
-        public void BindSimilarityScoreFunction(IDbConnection connection)
-        {
-            SqliteExtensions.BindGetSimilarityScore(connection, _logger);
-        }
-
         public Task<IDbConnection> Connect(string dbPath)
         public Task<IDbConnection> Connect(string dbPath)
         {
         {
             return SqliteExtensions.ConnectToDb(dbPath, _logger);
             return SqliteExtensions.ConnectToDb(dbPath, _logger);

+ 3 - 2
MediaBrowser.Server.Startup.Common/ApplicationHost.cs

@@ -380,7 +380,8 @@ namespace MediaBrowser.Server.Startup.Common
             {
             {
                 new OmdbEpisodeProviderMigration(ServerConfigurationManager),
                 new OmdbEpisodeProviderMigration(ServerConfigurationManager),
                 new MovieDbEpisodeProviderMigration(ServerConfigurationManager),
                 new MovieDbEpisodeProviderMigration(ServerConfigurationManager),
-                new DbMigration(ServerConfigurationManager, TaskManager)
+                new DbMigration(ServerConfigurationManager, TaskManager),
+                new FolderViewSettingMigration(ServerConfigurationManager, UserManager)
             };
             };
 
 
             foreach (var task in migrations)
             foreach (var task in migrations)
@@ -568,7 +569,7 @@ namespace MediaBrowser.Server.Startup.Common
 
 
             SubtitleEncoder = new SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager);
             SubtitleEncoder = new SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager);
             RegisterSingleInstance(SubtitleEncoder);
             RegisterSingleInstance(SubtitleEncoder);
-            
+
             await displayPreferencesRepo.Initialize(NativeApp.GetDbConnector()).ConfigureAwait(false);
             await displayPreferencesRepo.Initialize(NativeApp.GetDbConnector()).ConfigureAwait(false);
             await ConfigureUserDataRepositories().ConfigureAwait(false);
             await ConfigureUserDataRepositories().ConfigureAwait(false);
             await itemRepo.Initialize(NativeApp.GetDbConnector()).ConfigureAwait(false);
             await itemRepo.Initialize(NativeApp.GetDbConnector()).ConfigureAwait(false);

+ 2 - 0
MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj

@@ -71,6 +71,8 @@
     <Compile Include="FFMpeg\FFmpegValidator.cs" />
     <Compile Include="FFMpeg\FFmpegValidator.cs" />
     <Compile Include="INativeApp.cs" />
     <Compile Include="INativeApp.cs" />
     <Compile Include="MbLinkShortcutHandler.cs" />
     <Compile Include="MbLinkShortcutHandler.cs" />
+    <Compile Include="Migrations\CollectionGroupingMigration.cs" />
+    <Compile Include="Migrations\FolderViewSettingMigration.cs" />
     <Compile Include="Migrations\IVersionMigration.cs" />
     <Compile Include="Migrations\IVersionMigration.cs" />
     <Compile Include="Migrations\DbMigration.cs" />
     <Compile Include="Migrations\DbMigration.cs" />
     <Compile Include="Migrations\MovieDbEpisodeProviderMigration.cs" />
     <Compile Include="Migrations\MovieDbEpisodeProviderMigration.cs" />

+ 44 - 0
MediaBrowser.Server.Startup.Common/Migrations/CollectionGroupingMigration.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+
+namespace MediaBrowser.Server.Startup.Common.Migrations
+{
+    public class CollectionGroupingMigration : IVersionMigration
+    {
+        private readonly IServerConfigurationManager _config;
+        private readonly IUserManager _userManager;
+
+        public CollectionGroupingMigration(IServerConfigurationManager config, IUserManager userManager)
+        {
+            _config = config;
+            _userManager = userManager;
+        }
+
+        public void Run()
+        {
+            var migrationKey = this.GetType().Name;
+            var migrationKeyList = _config.Configuration.Migrations.ToList();
+
+            if (!migrationKeyList.Contains(migrationKey))
+            {
+                if (_config.Configuration.IsStartupWizardCompleted)
+                {
+                    if (_userManager.Users.Any(i => i.Configuration.GroupMoviesIntoBoxSets))
+                    {
+                        _config.Configuration.EnableGroupingIntoCollections = true;
+                    }
+                }
+
+                migrationKeyList.Add(migrationKey);
+                _config.Configuration.Migrations = migrationKeyList.ToArray();
+                _config.SaveConfiguration();
+            }
+
+        }
+    }
+}

+ 40 - 0
MediaBrowser.Server.Startup.Common/Migrations/FolderViewSettingMigration.cs

@@ -0,0 +1,40 @@
+using System.Linq;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+
+namespace MediaBrowser.Server.Startup.Common.Migrations
+{
+    public class FolderViewSettingMigration : IVersionMigration
+    {
+        private readonly IServerConfigurationManager _config;
+        private readonly IUserManager _userManager;
+
+        public FolderViewSettingMigration(IServerConfigurationManager config, IUserManager userManager)
+        {
+            _config = config;
+            _userManager = userManager;
+        }
+
+        public void Run()
+        {
+            var migrationKey = this.GetType().Name;
+            var migrationKeyList = _config.Configuration.Migrations.ToList();
+
+            if (!migrationKeyList.Contains(migrationKey))
+            {
+                if (_config.Configuration.IsStartupWizardCompleted)
+                {
+                    if (_userManager.Users.Any(i => i.Configuration.DisplayFoldersView))
+                    {
+                        _config.Configuration.EnableFolderView = true;
+                    }
+                }
+
+                migrationKeyList.Add(migrationKey);
+                _config.Configuration.Migrations = migrationKeyList.ToArray();
+                _config.SaveConfiguration();
+            }
+
+        }
+    }
+}

+ 0 - 5
MediaBrowser.ServerApplication/Native/DbConnector.cs

@@ -16,11 +16,6 @@ namespace MediaBrowser.ServerApplication.Native
             _logger = logger;
             _logger = logger;
         }
         }
 
 
-        public void BindSimilarityScoreFunction(IDbConnection connection)
-        {
-            SqliteExtensions.BindGetSimilarityScore(connection, _logger);
-        }
-
         public async Task<IDbConnection> Connect(string dbPath)
         public async Task<IDbConnection> Connect(string dbPath)
         {
         {
             try
             try

+ 6 - 3
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -137,9 +137,6 @@
     <Content Include="dashboard-ui\components\remotecontrolautoplay.js">
     <Content Include="dashboard-ui\components\remotecontrolautoplay.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
-    <Content Include="dashboard-ui\components\scrollthreshold.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
     <Content Include="dashboard-ui\components\tvproviders\xmltv.js">
     <Content Include="dashboard-ui\components\tvproviders\xmltv.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
@@ -275,6 +272,9 @@
     <Content Include="dashboard-ui\legacy\selectmenu.js">
     <Content Include="dashboard-ui\legacy\selectmenu.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
+    <Content Include="dashboard-ui\librarydisplay.html">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\livetvguideprovider.html">
     <Content Include="dashboard-ui\livetvguideprovider.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
@@ -311,6 +311,9 @@
     <Content Include="dashboard-ui\scripts\homeupcoming.js">
     <Content Include="dashboard-ui\scripts\homeupcoming.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
+    <Content Include="dashboard-ui\scripts\librarydisplay.js">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\scripts\livetvguideprovider.js">
     <Content Include="dashboard-ui\scripts\livetvguideprovider.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>

+ 1 - 5
MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs

@@ -919,11 +919,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
                         var val = reader.ReadElementContentAsString();
                         var val = reader.ReadElementContentAsString();
                         if (!string.IsNullOrWhiteSpace(val))
                         if (!string.IsNullOrWhiteSpace(val))
                         {
                         {
-                            var hasTags = item as IHasTags;
-                            if (hasTags != null)
-                            {
-                                hasTags.AddTag(val);
-                            }
+                            item.AddTag(val);
                         }
                         }
                         break;
                         break;
                     }
                     }

+ 7 - 11
MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs

@@ -736,19 +736,15 @@ namespace MediaBrowser.XbmcMetadata.Savers
                 writer.WriteElementString("studio", studio);
                 writer.WriteElementString("studio", studio);
             }
             }
 
 
-            var hasTags = item as IHasTags;
-            if (hasTags != null)
+            foreach (var tag in item.Tags)
             {
             {
-                foreach (var tag in hasTags.Tags)
+                if (item is MusicAlbum || item is MusicArtist)
                 {
                 {
-                    if (item is MusicAlbum || item is MusicArtist)
-                    {
-                        writer.WriteElementString("style", tag);
-                    }
-                    else
-                    {
-                        writer.WriteElementString("tag", tag);
-                    }
+                    writer.WriteElementString("style", tag);
+                }
+                else
+                {
+                    writer.WriteElementString("tag", tag);
                 }
                 }
             }
             }
 
 

+ 2 - 2
Nuget/MediaBrowser.Common.Internal.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
     <metadata>
         <id>MediaBrowser.Common.Internal</id>
         <id>MediaBrowser.Common.Internal</id>
-        <version>3.0.649</version>
+        <version>3.0.650</version>
         <title>MediaBrowser.Common.Internal</title>
         <title>MediaBrowser.Common.Internal</title>
         <authors>Luke</authors>
         <authors>Luke</authors>
         <owners>ebr,Luke,scottisafool</owners>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains common components shared by Emby Theater and Emby Server. Not intended for plugin developer consumption.</description>
         <description>Contains common components shared by Emby Theater and Emby Server. Not intended for plugin developer consumption.</description>
         <copyright>Copyright © Emby 2013</copyright>
         <copyright>Copyright © Emby 2013</copyright>
         <dependencies>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.649" />
+            <dependency id="MediaBrowser.Common" version="3.0.650" />
             <dependency id="NLog" version="4.3.4" />
             <dependency id="NLog" version="4.3.4" />
             <dependency id="SimpleInjector" version="3.1.5" />
             <dependency id="SimpleInjector" version="3.1.5" />
         </dependencies>
         </dependencies>

+ 1 - 1
Nuget/MediaBrowser.Common.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
     <metadata>
         <id>MediaBrowser.Common</id>
         <id>MediaBrowser.Common</id>
-        <version>3.0.649</version>
+        <version>3.0.650</version>
         <title>MediaBrowser.Common</title>
         <title>MediaBrowser.Common</title>
         <authors>Emby Team</authors>
         <authors>Emby Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
         <owners>ebr,Luke,scottisafool</owners>

+ 2 - 2
Nuget/MediaBrowser.Server.Core.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
     <metadata>
     <metadata>
         <id>MediaBrowser.Server.Core</id>
         <id>MediaBrowser.Server.Core</id>
-        <version>3.0.649</version>
+        <version>3.0.650</version>
         <title>Media Browser.Server.Core</title>
         <title>Media Browser.Server.Core</title>
         <authors>Emby Team</authors>
         <authors>Emby Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains core components required to build plugins for Emby Server.</description>
         <description>Contains core components required to build plugins for Emby Server.</description>
         <copyright>Copyright © Emby 2013</copyright>
         <copyright>Copyright © Emby 2013</copyright>
         <dependencies>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.649" />
+            <dependency id="MediaBrowser.Common" version="3.0.650" />
 			<dependency id="Interfaces.IO" version="1.0.0.5" />
 			<dependency id="Interfaces.IO" version="1.0.0.5" />
         </dependencies>
         </dependencies>
     </metadata>
     </metadata>