Răsfoiți Sursa

Add GPL modules

Andrew Rabert 6 ani în urmă
părinte
comite
a86b71899e
100 a modificat fișierele cu 10825 adăugiri și 116 ștergeri
  1. 1 1
      Emby.Notifications/Notifications.cs
  2. 3 9
      Emby.Server.Implementations/ApplicationHost.cs
  3. 2 2
      Emby.Server.Implementations/Channels/ChannelManager.cs
  4. 3 3
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  5. 1 3
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  6. 1 3
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  7. 0 46
      Emby.Server.Implementations/Library/ConnectManager.cs
  8. 1 3
      Emby.Server.Implementations/Library/UserManager.cs
  9. 1 39
      MediaBrowser.Api/PackageService.cs
  10. 2 3
      MediaBrowser.Api/Playback/StreamState.cs
  11. 1 1
      MediaBrowser.Api/SearchService.cs
  12. 1 3
      MediaBrowser.Api/StartupWizardService.cs
  13. 18 0
      MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs
  14. 84 0
      MediaBrowser.Common/Configuration/IApplicationPaths.cs
  15. 22 0
      MediaBrowser.Common/Configuration/IConfigurationFactory.cs
  16. 82 0
      MediaBrowser.Common/Configuration/IConfigurationManager.cs
  17. 108 0
      MediaBrowser.Common/Events/EventHelper.cs
  18. 58 0
      MediaBrowser.Common/Extensions/BaseExtensions.cs
  19. 63 0
      MediaBrowser.Common/Extensions/ResourceNotFoundException.cs
  20. 147 0
      MediaBrowser.Common/IApplicationHost.cs
  21. 16 0
      MediaBrowser.Common/MediaBrowser.Common.csproj
  22. 157 0
      MediaBrowser.Common/Net/HttpRequestOptions.cs
  23. 75 0
      MediaBrowser.Common/Net/HttpResponseInfo.cs
  24. 59 0
      MediaBrowser.Common/Net/IHttpClient.cs
  25. 66 0
      MediaBrowser.Common/Net/INetworkManager.cs
  26. 276 0
      MediaBrowser.Common/Plugins/BasePlugin.cs
  27. 83 0
      MediaBrowser.Common/Plugins/IPlugin.cs
  28. 54 0
      MediaBrowser.Common/Progress/ActionableProgress.cs
  29. 27 0
      MediaBrowser.Common/Properties/AssemblyInfo.cs
  30. 15 0
      MediaBrowser.Common/Security/IRequiresRegistration.cs
  31. 32 0
      MediaBrowser.Common/Security/ISecurityManager.cs
  32. 8 0
      MediaBrowser.Common/Security/PaymentRequiredException.cs
  33. 278 0
      MediaBrowser.Common/Updates/GithubUpdater.cs
  34. 121 0
      MediaBrowser.Common/Updates/IInstallationManager.cs
  35. 11 0
      MediaBrowser.Common/Updates/InstallationEventArgs.cs
  36. 9 0
      MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs
  37. 14 0
      MediaBrowser.Controller/Authentication/AuthenticationResult.cs
  38. 35 0
      MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
  39. 94 0
      MediaBrowser.Controller/Channels/Channel.cs
  40. 82 0
      MediaBrowser.Controller/Channels/ChannelItemInfo.cs
  41. 16 0
      MediaBrowser.Controller/Channels/ChannelItemResult.cs
  42. 9 0
      MediaBrowser.Controller/Channels/ChannelItemType.cs
  43. 15 0
      MediaBrowser.Controller/Channels/ChannelParentalRating.cs
  44. 14 0
      MediaBrowser.Controller/Channels/ChannelSearchInfo.cs
  45. 76 0
      MediaBrowser.Controller/Channels/IChannel.cs
  46. 89 0
      MediaBrowser.Controller/Channels/IChannelManager.cs
  47. 13 0
      MediaBrowser.Controller/Channels/IHasCacheKey.cs
  48. 15 0
      MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs
  49. 50 0
      MediaBrowser.Controller/Channels/ISearchableChannel.cs
  50. 61 0
      MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
  51. 21 0
      MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs
  52. 23 0
      MediaBrowser.Controller/Chapters/IChapterManager.cs
  53. 27 0
      MediaBrowser.Controller/Collections/CollectionCreationOptions.cs
  54. 37 0
      MediaBrowser.Controller/Collections/CollectionEvents.cs
  55. 57 0
      MediaBrowser.Controller/Collections/ICollectionManager.cs
  56. 25 0
      MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs
  57. 45 0
      MediaBrowser.Controller/Connect/IConnectManager.cs
  58. 10 0
      MediaBrowser.Controller/Connect/UserLinkResult.cs
  59. 10 0
      MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs
  60. 73 0
      MediaBrowser.Controller/Devices/IDeviceManager.cs
  61. 76 0
      MediaBrowser.Controller/Dlna/IDlnaManager.cs
  62. 49 0
      MediaBrowser.Controller/Drawing/IImageEncoder.cs
  63. 118 0
      MediaBrowser.Controller/Drawing/IImageProcessor.cs
  64. 27 0
      MediaBrowser.Controller/Drawing/ImageCollageOptions.cs
  65. 72 0
      MediaBrowser.Controller/Drawing/ImageHelper.cs
  66. 114 0
      MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
  67. 25 0
      MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
  68. 28 0
      MediaBrowser.Controller/Drawing/ImageStream.cs
  69. 72 0
      MediaBrowser.Controller/Dto/DtoOptions.cs
  70. 70 0
      MediaBrowser.Controller/Dto/IDtoService.cs
  71. 219 0
      MediaBrowser.Controller/Entities/AggregateFolder.cs
  72. 216 0
      MediaBrowser.Controller/Entities/Audio/Audio.cs
  73. 15 0
      MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
  74. 9 0
      MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs
  75. 272 0
      MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
  76. 275 0
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  77. 145 0
      MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
  78. 69 0
      MediaBrowser.Controller/Entities/AudioBook.cs
  79. 2960 0
      MediaBrowser.Controller/Entities/BaseItem.cs
  80. 65 0
      MediaBrowser.Controller/Entities/BaseItemExtensions.cs
  81. 54 0
      MediaBrowser.Controller/Entities/BasePluginFolder.cs
  82. 72 0
      MediaBrowser.Controller/Entities/Book.cs
  83. 405 0
      MediaBrowser.Controller/Entities/CollectionFolder.cs
  84. 71 0
      MediaBrowser.Controller/Entities/DayOfWeekHelper.cs
  85. 46 0
      MediaBrowser.Controller/Entities/Extensions.cs
  86. 1803 0
      MediaBrowser.Controller/Entities/Folder.cs
  87. 129 0
      MediaBrowser.Controller/Entities/Game.cs
  88. 128 0
      MediaBrowser.Controller/Entities/GameGenre.cs
  89. 101 0
      MediaBrowser.Controller/Entities/GameSystem.cs
  90. 140 0
      MediaBrowser.Controller/Entities/Genre.cs
  91. 27 0
      MediaBrowser.Controller/Entities/ICollectionFolder.cs
  92. 14 0
      MediaBrowser.Controller/Entities/IHasAspectRatio.cs
  93. 15 0
      MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
  94. 19 0
      MediaBrowser.Controller/Entities/IHasMediaSources.cs
  95. 17 0
      MediaBrowser.Controller/Entities/IHasProgramAttributes.cs
  96. 10 0
      MediaBrowser.Controller/Entities/IHasScreenshots.cs
  97. 20 0
      MediaBrowser.Controller/Entities/IHasSeries.cs
  98. 13 0
      MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
  99. 9 0
      MediaBrowser.Controller/Entities/IHasStartDate.cs
  100. 39 0
      MediaBrowser.Controller/Entities/IHasTrailers.cs

+ 1 - 1
Emby.Notifications/Notifications.cs

@@ -231,7 +231,7 @@ namespace Emby.Notifications
                 }
             }
 
-            var hasSeries = item as IHasSeriesName;
+            var hasSeries = item as IHasSeries;
 
             if (hasSeries != null)
             {

+ 3 - 9
Emby.Server.Implementations/ApplicationHost.cs

@@ -318,7 +318,6 @@ namespace Emby.Server.Implementations
         private IMediaEncoder MediaEncoder { get; set; }
         private ISubtitleEncoder SubtitleEncoder { get; set; }
 
-        private IConnectManager ConnectManager { get; set; }
         private ISessionManager SessionManager { get; set; }
 
         private ILiveTvManager LiveTvManager { get; set; }
@@ -839,8 +838,6 @@ namespace Emby.Server.Implementations
             }
         }
 
-        protected abstract IConnectManager CreateConnectManager();
-
         protected virtual IHttpClient CreateHttpClient()
         {
             return new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, GetDefaultUserAgent);
@@ -947,7 +944,7 @@ namespace Emby.Server.Implementations
             AuthenticationRepository = GetAuthenticationRepository();
             RegisterSingleInstance(AuthenticationRepository);
 
-            UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, () => ConnectManager, this, JsonSerializer, FileSystemManager, CryptographyProvider);
+            UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager, CryptographyProvider);
             RegisterSingleInstance(UserManager);
 
             LibraryManager = new LibraryManager(this, Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager);
@@ -986,9 +983,6 @@ namespace Emby.Server.Implementations
             var encryptionManager = new EncryptionManager();
             RegisterSingleInstance<IEncryptionManager>(encryptionManager);
 
-            ConnectManager = CreateConnectManager();
-            RegisterSingleInstance(ConnectManager);
-
             DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager, LogManager.GetLogger("DeviceManager"), NetworkManager);
             RegisterSingleInstance(DeviceManager);
 
@@ -1045,11 +1039,11 @@ namespace Emby.Server.Implementations
             RegisterSingleInstance(activityLogRepo);
             RegisterSingleInstance<IActivityManager>(new ActivityManager(LogManager.GetLogger("ActivityManager"), activityLogRepo, UserManager));
 
-            var authContext = new AuthorizationContext(AuthenticationRepository, ConnectManager, UserManager);
+            var authContext = new AuthorizationContext(AuthenticationRepository, UserManager);
             RegisterSingleInstance<IAuthorizationContext>(authContext);
             RegisterSingleInstance<ISessionContext>(new SessionContext(UserManager, authContext, SessionManager));
 
-            AuthService = new AuthService(UserManager, authContext, ServerConfigurationManager, ConnectManager, SessionManager, NetworkManager);
+            AuthService = new AuthService(UserManager, authContext, ServerConfigurationManager, SessionManager, NetworkManager);
             RegisterSingleInstance<IAuthService>(AuthService);
 
             SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, ProcessFactory, TextEncoding);

+ 2 - 2
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -1086,7 +1086,7 @@ namespace Emby.Server.Implementations.Channels
             }
             item.ParentId = parentFolderId;
 
-            var hasSeries = item as IHasSeriesName;
+            var hasSeries = item as IHasSeries;
             if (hasSeries != null)
             {
                 if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase))
@@ -1215,4 +1215,4 @@ namespace Emby.Server.Implementations.Channels
             return result;
         }
     }
-}
+}

+ 3 - 3
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -942,7 +942,7 @@ namespace Emby.Server.Implementations.Data
             saveItemStatement.TryBind("@Album", item.Album);
             saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
 
-            var hasSeriesName = item as IHasSeriesName;
+            var hasSeriesName = item as IHasSeries;
             if (hasSeriesName != null)
             {
                 saveItemStatement.TryBind("@SeriesName", hasSeriesName.SeriesName);
@@ -1757,7 +1757,7 @@ namespace Emby.Server.Implementations.Data
             }
             index++;
 
-            var hasSeriesName = item as IHasSeriesName;
+            var hasSeriesName = item as IHasSeries;
             if (hasSeriesName != null)
             {
                 if (!reader.IsDBNull(index))
@@ -6441,4 +6441,4 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
         }
 
     }
-}
+}

+ 1 - 3
Emby.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -17,19 +17,17 @@ namespace Emby.Server.Implementations.HttpServer.Security
     {
         private readonly IServerConfigurationManager _config;
 
-        public AuthService(IUserManager userManager, IAuthorizationContext authorizationContext, IServerConfigurationManager config, IConnectManager connectManager, ISessionManager sessionManager, INetworkManager networkManager)
+        public AuthService(IUserManager userManager, IAuthorizationContext authorizationContext, IServerConfigurationManager config, ISessionManager sessionManager, INetworkManager networkManager)
         {
             AuthorizationContext = authorizationContext;
             _config = config;
             SessionManager = sessionManager;
-            ConnectManager = connectManager;
             UserManager = userManager;
             NetworkManager = networkManager;
         }
 
         public IUserManager UserManager { get; private set; }
         public IAuthorizationContext AuthorizationContext { get; private set; }
-        public IConnectManager ConnectManager { get; private set; }
         public ISessionManager SessionManager { get; private set; }
         public INetworkManager NetworkManager { get; private set; }
 

+ 1 - 3
Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

@@ -13,13 +13,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
     public class AuthorizationContext : IAuthorizationContext
     {
         private readonly IAuthenticationRepository _authRepo;
-        private readonly IConnectManager _connectManager;
         private readonly IUserManager _userManager;
 
-        public AuthorizationContext(IAuthenticationRepository authRepo, IConnectManager connectManager, IUserManager userManager)
+        public AuthorizationContext(IAuthenticationRepository authRepo, IUserManager userManager)
         {
             _authRepo = authRepo;
-            _connectManager = connectManager;
             _userManager = userManager;
         }
 

+ 0 - 46
Emby.Server.Implementations/Library/ConnectManager.cs

@@ -1,46 +0,0 @@
-using MediaBrowser.Common.Events;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Connect;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Connect;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Users;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Cryptography;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Controller.Plugins;
-
-namespace Emby.Server.Implementations.Library
-{
-    public class ConnectManager : IConnectManager
-    {
-        public ConnectManager()
-        {
-        }
-
-    }
-}

+ 1 - 3
Emby.Server.Implementations/Library/UserManager.cs

@@ -74,7 +74,6 @@ namespace Emby.Server.Implementations.Library
 
         private readonly Func<IImageProcessor> _imageProcessorFactory;
         private readonly Func<IDtoService> _dtoServiceFactory;
-        private readonly Func<IConnectManager> _connectFactory;
         private readonly IServerApplicationHost _appHost;
         private readonly IFileSystem _fileSystem;
         private readonly ICryptoProvider _cryptographyProvider;
@@ -82,7 +81,7 @@ namespace Emby.Server.Implementations.Library
         private IAuthenticationProvider[] _authenticationProviders;
         private DefaultAuthenticationProvider _defaultAuthenticationProvider;
 
-        public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, Func<IImageProcessor> imageProcessorFactory, Func<IDtoService> dtoServiceFactory, Func<IConnectManager> connectFactory, IServerApplicationHost appHost, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ICryptoProvider cryptographyProvider)
+        public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, Func<IImageProcessor> imageProcessorFactory, Func<IDtoService> dtoServiceFactory, IServerApplicationHost appHost, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ICryptoProvider cryptographyProvider)
         {
             _logger = logger;
             UserRepository = userRepository;
@@ -90,7 +89,6 @@ namespace Emby.Server.Implementations.Library
             _networkManager = networkManager;
             _imageProcessorFactory = imageProcessorFactory;
             _dtoServiceFactory = dtoServiceFactory;
-            _connectFactory = connectFactory;
             _appHost = appHost;
             _jsonSerializer = jsonSerializer;
             _fileSystem = fileSystem;

+ 1 - 39
MediaBrowser.Api/PackageService.cs

@@ -61,21 +61,6 @@ namespace MediaBrowser.Api
         public bool? IsAppStoreEnabled { get; set; }
     }
 
-    /// <summary>
-    /// Class GetPackageVersionUpdates
-    /// </summary>
-    [Route("/Packages/Updates", "GET", Summary = "Gets available package updates for currently installed packages")]
-    [Authenticated(Roles = "Admin")]
-    public class GetPackageVersionUpdates : IReturn<PackageVersionInfo[]>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "PackageType", Description = "Package type filter (System/UserInstalled)", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string PackageType { get; set; }
-    }
-
     /// <summary>
     /// Class InstallPackage
     /// </summary>
@@ -146,30 +131,7 @@ namespace MediaBrowser.Api
         /// </summary>
         /// <param name="request">The request.</param>
         /// <returns>System.Object.</returns>
-        public async Task<object> Get(GetPackageVersionUpdates request)
-        {
-            PackageVersionInfo[] result = null;
-
-            if (string.Equals(request.PackageType, "UserInstalled", StringComparison.OrdinalIgnoreCase) || string.Equals(request.PackageType, "All", StringComparison.OrdinalIgnoreCase))
-            {
-                result = (await _installationManager.GetAvailablePluginUpdates(_appHost.ApplicationVersion, false, CancellationToken.None).ConfigureAwait(false)).ToArray();
-            }
-
-            else if (string.Equals(request.PackageType, "System", StringComparison.OrdinalIgnoreCase) ||
-                     string.Equals(request.PackageType, "All", StringComparison.OrdinalIgnoreCase))
-            {
-                var updateCheckResult = await _appHost
-                    .CheckForApplicationUpdate(CancellationToken.None, new SimpleProgress<double>()).ConfigureAwait(false);
-
-                if (updateCheckResult.IsUpdateAvailable)
-                {
-                    result = new PackageVersionInfo[] { updateCheckResult.Package };
-                }
-            }
-
-            return ToOptimizedResult(result ?? new PackageVersionInfo[] { });
-        }
-
+        ///
         /// <summary>
         /// Gets the specified request.
         /// </summary>

+ 2 - 3
MediaBrowser.Api/Playback/StreamState.cs

@@ -148,7 +148,6 @@ namespace MediaBrowser.Api.Playback
             DisposeTranscodingThrottler();
             DisposeLiveStream();
             DisposeLogStream();
-            DisposeIsoMount();
 
             TranscodingJob = null;
         }
@@ -251,9 +250,9 @@ namespace MediaBrowser.Api.Playback
         public DeviceProfile DeviceProfile { get; set; }
 
         public TranscodingJob TranscodingJob;
-        public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
+        public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate)
         {
-            ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
+            ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, 0, percentComplete, 0, bitRate);
         }
     }
 }

+ 1 - 1
MediaBrowser.Api/SearchService.cs

@@ -230,7 +230,7 @@ namespace MediaBrowser.Api
                 result.StartDate = program.StartDate;
             }
 
-            var hasSeries = item as IHasSeriesName;
+            var hasSeries = item as IHasSeries;
             if (hasSeries != null)
             {
                 result.Series = hasSeries.SeriesName;

+ 1 - 3
MediaBrowser.Api/StartupWizardService.cs

@@ -51,16 +51,14 @@ namespace MediaBrowser.Api
         private readonly IServerConfigurationManager _config;
         private readonly IServerApplicationHost _appHost;
         private readonly IUserManager _userManager;
-        private readonly IConnectManager _connectManager;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IHttpClient _httpClient;
 
-        public StartupWizardService(IServerConfigurationManager config, IHttpClient httpClient, IServerApplicationHost appHost, IUserManager userManager, IConnectManager connectManager, IMediaEncoder mediaEncoder)
+        public StartupWizardService(IServerConfigurationManager config, IHttpClient httpClient, IServerApplicationHost appHost, IUserManager userManager, IMediaEncoder mediaEncoder)
         {
             _config = config;
             _appHost = appHost;
             _userManager = userManager;
-            _connectManager = connectManager;
             _mediaEncoder = mediaEncoder;
             _httpClient = httpClient;
         }

+ 18 - 0
MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs

@@ -0,0 +1,18 @@
+using System;
+
+namespace MediaBrowser.Common.Configuration
+{
+    public class ConfigurationUpdateEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Gets or sets the key.
+        /// </summary>
+        /// <value>The key.</value>
+        public string Key { get; set; }
+        /// <summary>
+        /// Gets or sets the new configuration.
+        /// </summary>
+        /// <value>The new configuration.</value>
+        public object NewConfiguration { get; set; }
+    }
+}

+ 84 - 0
MediaBrowser.Common/Configuration/IApplicationPaths.cs

@@ -0,0 +1,84 @@
+
+namespace MediaBrowser.Common.Configuration
+{
+    /// <summary>
+    /// Interface IApplicationPaths
+    /// </summary>
+    public interface IApplicationPaths
+    {
+        /// <summary>
+        /// Gets the path to the program data folder
+        /// </summary>
+        /// <value>The program data path.</value>
+        string ProgramDataPath { get; }
+
+        /// <summary>
+        /// Gets the path to the program system folder
+        /// </summary>
+        /// <value>The program data path.</value>
+        string ProgramSystemPath { get; }
+
+        /// <summary>
+        /// Gets the folder path to the data directory
+        /// </summary>
+        /// <value>The data directory.</value>
+        string DataPath { get; }
+
+        /// <summary>
+        /// Gets the image cache path.
+        /// </summary>
+        /// <value>The image cache path.</value>
+        string ImageCachePath { get; }
+
+        /// <summary>
+        /// Gets the path to the plugin directory
+        /// </summary>
+        /// <value>The plugins path.</value>
+        string PluginsPath { get; }
+
+        /// <summary>
+        /// Gets the path to the plugin configurations directory
+        /// </summary>
+        /// <value>The plugin configurations path.</value>
+        string PluginConfigurationsPath { get; }
+
+        /// <summary>
+        /// Gets the path to where temporary update files will be stored
+        /// </summary>
+        /// <value>The plugin configurations path.</value>
+        string TempUpdatePath { get; }
+
+        /// <summary>
+        /// Gets the path to the log directory
+        /// </summary>
+        /// <value>The log directory path.</value>
+        string LogDirectoryPath { get; }
+
+        /// <summary>
+        /// Gets the path to the application configuration root directory
+        /// </summary>
+        /// <value>The configuration directory path.</value>
+        string ConfigurationDirectoryPath { get; }
+
+        /// <summary>
+        /// Gets the path to the system configuration file
+        /// </summary>
+        /// <value>The system configuration file path.</value>
+        string SystemConfigurationFilePath { get; }
+
+        /// <summary>
+        /// Gets the folder path to the cache directory
+        /// </summary>
+        /// <value>The cache directory.</value>
+        string CachePath { get; }
+
+        /// <summary>
+        /// Gets the folder path to the temp directory within the cache folder
+        /// </summary>
+        /// <value>The temp directory.</value>
+        string TempDirectory { get; }
+
+        string VirtualDataPath { get; }
+    }
+
+}

+ 22 - 0
MediaBrowser.Common/Configuration/IConfigurationFactory.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Common.Configuration
+{
+    public interface IConfigurationFactory
+    {
+        IEnumerable<ConfigurationStore> GetConfigurations();
+    }
+
+    public class ConfigurationStore
+    {
+        public string Key { get; set; }
+
+        public Type ConfigurationType { get; set; }
+    }
+
+    public interface IValidatingConfiguration
+    {
+        void Validate(object oldConfig, object newConfig);
+    }
+}

+ 82 - 0
MediaBrowser.Common/Configuration/IConfigurationManager.cs

@@ -0,0 +1,82 @@
+using MediaBrowser.Model.Configuration;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Common.Configuration
+{
+    public interface IConfigurationManager
+    {
+        /// <summary>
+        /// Occurs when [configuration updating].
+        /// </summary>
+        event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
+
+        /// <summary>
+        /// Occurs when [configuration updated].
+        /// </summary>
+        event EventHandler<EventArgs> ConfigurationUpdated;
+
+        /// <summary>
+        /// Occurs when [named configuration updated].
+        /// </summary>
+        event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated;
+
+        /// <summary>
+        /// Gets or sets the application paths.
+        /// </summary>
+        /// <value>The application paths.</value>
+        IApplicationPaths CommonApplicationPaths { get; }
+
+        /// <summary>
+        /// Gets the configuration.
+        /// </summary>
+        /// <value>The configuration.</value>
+        BaseApplicationConfiguration CommonConfiguration { get; }
+
+        /// <summary>
+        /// Saves the configuration.
+        /// </summary>
+        void SaveConfiguration();
+
+        /// <summary>
+        /// Replaces the configuration.
+        /// </summary>
+        /// <param name="newConfiguration">The new configuration.</param>
+        void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration);
+
+        /// <summary>
+        /// Gets the configuration.
+        /// </summary>
+        /// <param name="key">The key.</param>
+        /// <returns>System.Object.</returns>
+        object GetConfiguration(string key);
+
+        /// <summary>
+        /// Gets the type of the configuration.
+        /// </summary>
+        /// <param name="key">The key.</param>
+        /// <returns>Type.</returns>
+        Type GetConfigurationType(string key);
+
+        /// <summary>
+        /// Saves the configuration.
+        /// </summary>
+        /// <param name="key">The key.</param>
+        /// <param name="configuration">The configuration.</param>
+        void SaveConfiguration(string key, object configuration);
+
+        /// <summary>
+        /// Adds the parts.
+        /// </summary>
+        /// <param name="factories">The factories.</param>
+        void AddParts(IEnumerable<IConfigurationFactory> factories);
+    }
+
+    public static class ConfigurationManagerExtensions
+    {
+        public static T GetConfiguration<T>(this IConfigurationManager manager, string key)
+        {
+            return (T)manager.GetConfiguration(key);
+        }
+    }
+}

+ 108 - 0
MediaBrowser.Common/Events/EventHelper.cs

@@ -0,0 +1,108 @@
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Events
+{
+    /// <summary>
+    /// Class EventHelper
+    /// </summary>
+    public static class EventHelper
+    {
+        /// <summary>
+        /// Fires the event.
+        /// </summary>
+        /// <param name="handler">The handler.</param>
+        /// <param name="sender">The sender.</param>
+        /// <param name="args">The <see cref="EventArgs" /> instance containing the event data.</param>
+        /// <param name="logger">The logger.</param>
+        public static void QueueEventIfNotNull(EventHandler handler, object sender, EventArgs args, ILogger logger)
+        {
+            if (handler != null)
+            {
+                Task.Run(() =>
+                {
+                    try
+                    {
+                        handler(sender, args);
+                    }
+                    catch (Exception ex)
+                    {
+                        logger.ErrorException("Error in event handler", ex);
+                    }
+                });
+            }
+        }
+
+        /// <summary>
+        /// Queues the event.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="handler">The handler.</param>
+        /// <param name="sender">The sender.</param>
+        /// <param name="args">The args.</param>
+        /// <param name="logger">The logger.</param>
+        public static void QueueEventIfNotNull<T>(EventHandler<T> handler, object sender, T args, ILogger logger)
+        {
+            if (handler != null)
+            {
+                Task.Run(() =>
+                {
+                    try
+                    {
+                        handler(sender, args);
+                    }
+                    catch (Exception ex)
+                    {
+                        logger.ErrorException("Error in event handler", ex);
+                    }
+                });
+            }
+        }
+
+        /// <summary>
+        /// Fires the event.
+        /// </summary>
+        /// <param name="handler">The handler.</param>
+        /// <param name="sender">The sender.</param>
+        /// <param name="args">The <see cref="EventArgs" /> instance containing the event data.</param>
+        /// <param name="logger">The logger.</param>
+        public static void FireEventIfNotNull(EventHandler handler, object sender, EventArgs args, ILogger logger)
+        {
+            if (handler != null)
+            {
+                try
+                {
+                    handler(sender, args);
+                }
+                catch (Exception ex)
+                {
+                    logger.ErrorException("Error in event handler", ex);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Fires the event.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="handler">The handler.</param>
+        /// <param name="sender">The sender.</param>
+        /// <param name="args">The args.</param>
+        /// <param name="logger">The logger.</param>
+        public static void FireEventIfNotNull<T>(EventHandler<T> handler, object sender, T args, ILogger logger)
+        {
+            if (handler != null)
+            {
+                try
+                {
+                    handler(sender, args);
+                }
+                catch (Exception ex)
+                {
+                    logger.ErrorException("Error in event handler", ex);
+                }
+            }
+        }
+    }
+}

+ 58 - 0
MediaBrowser.Common/Extensions/BaseExtensions.cs

@@ -0,0 +1,58 @@
+using System;
+using System.Globalization;
+using System.Text.RegularExpressions;
+using MediaBrowser.Model.Cryptography;
+
+namespace MediaBrowser.Common.Extensions
+{
+    /// <summary>
+    /// Class BaseExtensions
+    /// </summary>
+    public static class BaseExtensions
+    {
+        public static ICryptoProvider CryptographyProvider { get; set; }
+
+        /// <summary>
+        /// Strips the HTML.
+        /// </summary>
+        /// <param name="htmlString">The HTML string.</param>
+        /// <returns>System.String.</returns>
+        public static string StripHtml(this string htmlString)
+        {
+            // http://stackoverflow.com/questions/1349023/how-can-i-strip-html-from-text-in-net
+            const string pattern = @"<(.|\n)*?>";
+
+            return Regex.Replace(htmlString, pattern, string.Empty).Trim();
+        }
+
+        /// <summary>
+        /// Gets the M d5.
+        /// </summary>
+        /// <param name="str">The STR.</param>
+        /// <returns>Guid.</returns>
+        public static Guid GetMD5(this string str)
+        {
+            return CryptographyProvider.GetMD5(str);
+        }
+
+        /// <summary>
+        /// Gets the MB id.
+        /// </summary>
+        /// <param name="str">The STR.</param>
+        /// <param name="type">The type.</param>
+        /// <returns>Guid.</returns>
+        /// <exception cref="System.ArgumentNullException">type</exception>
+        [Obsolete("Use LibraryManager.GetNewItemId")]
+        public static Guid GetMBId(this string str, Type type)
+        {
+            if (type == null)
+            {
+                throw new ArgumentNullException("type");
+            }
+
+            var key = type.FullName + str.ToLower();
+
+            return key.GetMD5();
+        }
+    }
+}

+ 63 - 0
MediaBrowser.Common/Extensions/ResourceNotFoundException.cs

@@ -0,0 +1,63 @@
+using System;
+
+namespace MediaBrowser.Common.Extensions
+{
+    /// <summary>
+    /// Class ResourceNotFoundException
+    /// </summary>
+    public class ResourceNotFoundException : Exception
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ResourceNotFoundException" /> class.
+        /// </summary>
+        public ResourceNotFoundException()
+        {
+
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ResourceNotFoundException" /> class.
+        /// </summary>
+        /// <param name="message">The message.</param>
+        public ResourceNotFoundException(string message)
+            : base(message)
+        {
+
+        }
+    }
+
+    public class RemoteServiceUnavailableException : Exception
+    {
+        public RemoteServiceUnavailableException()
+        {
+
+        }
+
+        public RemoteServiceUnavailableException(string message)
+            : base(message)
+        {
+
+        }
+    }
+
+    public class RateLimitExceededException : Exception
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class.
+        /// </summary>
+        public RateLimitExceededException()
+        {
+
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class.
+        /// </summary>
+        /// <param name="message">The message.</param>
+        public RateLimitExceededException(string message)
+            : base(message)
+        {
+
+        }
+    }
+}

+ 147 - 0
MediaBrowser.Common/IApplicationHost.cs

@@ -0,0 +1,147 @@
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Updates;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common
+{
+    /// <summary>
+    /// An interface to be implemented by the applications hosting a kernel
+    /// </summary>
+    public interface IApplicationHost
+    {
+        /// <summary>
+        /// Gets the display name of the operating system.
+        /// </summary>
+        /// <value>The display name of the operating system.</value>
+        string OperatingSystemDisplayName { get; }
+
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        string Name { get; }
+
+        /// <summary>
+        /// Gets the device identifier.
+        /// </summary>
+        /// <value>The device identifier.</value>
+        string SystemId { get; }
+
+        /// <summary>
+        /// Occurs when [application updated].
+        /// </summary>
+        event EventHandler<GenericEventArgs<PackageVersionInfo>> ApplicationUpdated;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance has pending kernel reload.
+        /// </summary>
+        /// <value><c>true</c> if this instance has pending kernel reload; otherwise, <c>false</c>.</value>
+        bool HasPendingRestart { get; }
+
+        bool IsShuttingDown { get; }
+
+        /// <summary>
+        /// Gets a value indicating whether this instance can self restart.
+        /// </summary>
+        /// <value><c>true</c> if this instance can self restart; otherwise, <c>false</c>.</value>
+        bool CanSelfRestart { get; }
+
+        /// <summary>
+        /// Occurs when [has pending restart changed].
+        /// </summary>
+        event EventHandler HasPendingRestartChanged;
+
+        /// <summary>
+        /// Notifies the pending restart.
+        /// </summary>
+        void NotifyPendingRestart();
+
+        /// <summary>
+        /// Restarts this instance.
+        /// </summary>
+        void Restart();
+
+        /// <summary>
+        /// Gets the application version.
+        /// </summary>
+        /// <value>The application version.</value>
+        Version ApplicationVersion { get; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance can self update.
+        /// </summary>
+        /// <value><c>true</c> if this instance can self update; otherwise, <c>false</c>.</value>
+        bool CanSelfUpdate { get; }
+
+        /// <summary>
+        /// Gets the exports.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="manageLiftime">if set to <c>true</c> [manage liftime].</param>
+        /// <returns>IEnumerable{``0}.</returns>
+        IEnumerable<T> GetExports<T>(bool manageLiftime = true);
+
+        /// <summary>
+        /// Checks for update.
+        /// </summary>
+        /// <returns>Task{CheckForUpdateResult}.</returns>
+        Task<CheckForUpdateResult> CheckForApplicationUpdate(CancellationToken cancellationToken, IProgress<double> progress);
+
+        /// <summary>
+        /// Updates the application.
+        /// </summary>
+        /// <returns>Task.</returns>
+        Task UpdateApplication(PackageVersionInfo package, CancellationToken cancellationToken, IProgress<double> progress);
+
+        /// <summary>
+        /// Resolves this instance.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <returns>``0.</returns>
+        T Resolve<T>();
+
+        /// <summary>
+        /// Resolves this instance.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <returns>``0.</returns>
+        T TryResolve<T>();
+
+        /// <summary>
+        /// Shuts down.
+        /// </summary>
+        Task Shutdown();
+
+        /// <summary>
+        /// Gets the plugins.
+        /// </summary>
+        /// <value>The plugins.</value>
+        IPlugin[] Plugins { get; }
+
+        /// <summary>
+        /// Removes the plugin.
+        /// </summary>
+        /// <param name="plugin">The plugin.</param>
+        void RemovePlugin(IPlugin plugin);
+
+        /// <summary>
+        /// Inits this instance.
+        /// </summary>
+        void Init();
+
+        /// <summary>
+        /// Creates the instance.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <returns>System.Object.</returns>
+        object CreateInstance(Type type);
+
+        PackageVersionClass SystemUpdateLevel { get; }
+
+        string GetValue(string name);
+    }
+}

+ 16 - 0
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <ItemGroup>
+    <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Include="..\SharedVersion.cs"/>
+  </ItemGroup>
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+  </PropertyGroup>
+
+</Project>

+ 157 - 0
MediaBrowser.Common/Net/HttpRequestOptions.cs

@@ -0,0 +1,157 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Text;
+
+namespace MediaBrowser.Common.Net
+{
+    /// <summary>
+    /// Class HttpRequestOptions
+    /// </summary>
+    public class HttpRequestOptions
+    {
+        /// <summary>
+        /// Gets or sets the URL.
+        /// </summary>
+        /// <value>The URL.</value>
+        public string Url { get; set; }
+
+        public CompressionMethod? DecompressionMethod { get; set; }
+
+        /// <summary>
+        /// Gets or sets the accept header.
+        /// </summary>
+        /// <value>The accept header.</value>
+        public string AcceptHeader
+        {
+            get { return GetHeaderValue("Accept"); }
+            set
+            {
+                RequestHeaders["Accept"] = value;
+            }
+        }
+        /// <summary>
+        /// Gets or sets the cancellation token.
+        /// </summary>
+        /// <value>The cancellation token.</value>
+        public CancellationToken CancellationToken { get; set; }
+
+        /// <summary>
+        /// Gets or sets the resource pool.
+        /// </summary>
+        /// <value>The resource pool.</value>
+        public SemaphoreSlim ResourcePool { get; set; }
+
+        /// <summary>
+        /// Gets or sets the user agent.
+        /// </summary>
+        /// <value>The user agent.</value>
+        public string UserAgent
+        {
+            get { return GetHeaderValue("User-Agent"); }
+            set
+            {
+                RequestHeaders["User-Agent"] = value;
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the referrer.
+        /// </summary>
+        /// <value>The referrer.</value>
+        public string Referer { get; set; }
+
+        /// <summary>
+        /// Gets or sets the host.
+        /// </summary>
+        /// <value>The host.</value>
+        public string Host { get; set; }
+
+        /// <summary>
+        /// Gets or sets the progress.
+        /// </summary>
+        /// <value>The progress.</value>
+        public IProgress<double> Progress { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether [enable HTTP compression].
+        /// </summary>
+        /// <value><c>true</c> if [enable HTTP compression]; otherwise, <c>false</c>.</value>
+        public bool EnableHttpCompression { get; set; }
+
+        public Dictionary<string, string> RequestHeaders { get; private set; }
+
+        public string RequestContentType { get; set; }
+
+        public string RequestContent { get; set; }
+        public byte[] RequestContentBytes { get; set; }
+
+        public bool BufferContent { get; set; }
+
+        public bool LogRequest { get; set; }
+        public bool LogRequestAsDebug { get; set; }
+        public bool LogErrors { get; set; }
+        public bool LogResponse { get; set; }
+        public bool LogResponseHeaders { get; set; }
+
+        public bool LogErrorResponseBody { get; set; }
+        public bool EnableKeepAlive { get; set; }
+
+        public CacheMode CacheMode { get; set; }
+        public TimeSpan CacheLength { get; set; }
+
+        public int TimeoutMs { get; set; }
+        public bool EnableDefaultUserAgent { get; set; }
+
+        public bool AppendCharsetToMimeType { get; set; }
+        public string DownloadFilePath { get; set; }
+
+        private string GetHeaderValue(string name)
+        {
+            string value;
+
+            RequestHeaders.TryGetValue(name, out value);
+
+            return value;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="HttpRequestOptions"/> class.
+        /// </summary>
+        public HttpRequestOptions()
+        {
+            EnableHttpCompression = true;
+
+            RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+            LogRequest = true;
+            LogErrors = true;
+            CacheMode = CacheMode.None;
+
+            TimeoutMs = 20000;
+        }
+
+        public void SetPostData(IDictionary<string,string> values)
+        {
+            var strings = values.Keys.Select(key => string.Format("{0}={1}", key, values[key]));
+            var postContent = string.Join("&", strings.ToArray());
+
+            RequestContent = postContent;
+            RequestContentType = "application/x-www-form-urlencoded";
+        }
+    }
+
+    public enum CacheMode
+    {
+        None = 0,
+        Unconditional = 1
+    }
+
+    public enum CompressionMethod
+    {
+        Deflate,
+        Gzip
+    }
+}

+ 75 - 0
MediaBrowser.Common/Net/HttpResponseInfo.cs

@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+
+namespace MediaBrowser.Common.Net
+{
+    /// <summary>
+    /// Class HttpResponseInfo
+    /// </summary>
+    public class HttpResponseInfo : IDisposable
+    {
+        /// <summary>
+        /// Gets or sets the type of the content.
+        /// </summary>
+        /// <value>The type of the content.</value>
+        public string ContentType { get; set; }
+
+        /// <summary>
+        /// Gets or sets the response URL.
+        /// </summary>
+        /// <value>The response URL.</value>
+        public string ResponseUrl { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the content.
+        /// </summary>
+        /// <value>The content.</value>
+        public Stream Content { get; set; }
+
+        /// <summary>
+        /// Gets or sets the status code.
+        /// </summary>
+        /// <value>The status code.</value>
+        public HttpStatusCode StatusCode { get; set; }
+
+        /// <summary>
+        /// Gets or sets the temp file path.
+        /// </summary>
+        /// <value>The temp file path.</value>
+        public string TempFilePath { get; set; }
+
+        /// <summary>
+        /// Gets or sets the length of the content.
+        /// </summary>
+        /// <value>The length of the content.</value>
+        public long? ContentLength { get; set; }
+
+        /// <summary>
+        /// Gets or sets the headers.
+        /// </summary>
+        /// <value>The headers.</value>
+        public Dictionary<string,string> Headers { get; set; }
+
+        private readonly IDisposable _disposable;
+
+        public HttpResponseInfo(IDisposable disposable)
+        {
+            _disposable = disposable;
+            Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+        }
+        public HttpResponseInfo()
+        {
+            Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+        }
+
+        public void Dispose()
+        {
+            if (_disposable != null)
+            {
+                _disposable.Dispose();
+            }
+        }
+    }
+}

+ 59 - 0
MediaBrowser.Common/Net/IHttpClient.cs

@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net
+{
+    /// <summary>
+    /// Interface IHttpClient
+    /// </summary>
+    public interface IHttpClient
+    {
+        /// <summary>
+        /// Gets the response.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <returns>Task{HttpResponseInfo}.</returns>
+        Task<HttpResponseInfo> GetResponse(HttpRequestOptions options);
+
+        /// <summary>
+        /// Gets the specified options.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <returns>Task{Stream}.</returns>
+        Task<Stream> Get(HttpRequestOptions options);
+
+        /// <summary>
+        /// Sends the asynchronous.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <param name="httpMethod">The HTTP method.</param>
+        /// <returns>Task{HttpResponseInfo}.</returns>
+        Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod);
+
+        /// <summary>
+        /// Posts the specified options.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <returns>Task{HttpResponseInfo}.</returns>
+        Task<HttpResponseInfo> Post(HttpRequestOptions options);
+
+        /// <summary>
+        /// Downloads the contents of a given url into a temporary location
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <returns>Task{System.String}.</returns>
+        /// <exception cref="System.ArgumentNullException">progress</exception>
+        /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
+        Task<string> GetTempFile(HttpRequestOptions options);
+
+        /// <summary>
+        /// Gets the temporary file response.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <returns>Task{HttpResponseInfo}.</returns>
+        Task<HttpResponseInfo> GetTempFileResponse(HttpRequestOptions options);
+    }
+}

+ 66 - 0
MediaBrowser.Common/Net/INetworkManager.cs

@@ -0,0 +1,66 @@
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using System.Collections.Generic;
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net
+{
+    public interface INetworkManager
+    {
+        event EventHandler NetworkChanged;
+
+        /// <summary>
+        /// Gets a random port number that is currently available
+        /// </summary>
+        /// <returns>System.Int32.</returns>
+        int GetRandomUnusedTcpPort();
+
+        int GetRandomUnusedUdpPort();
+
+        Func<string[]> LocalSubnetsFn { get; set; }
+
+        /// <summary>
+        /// Returns MAC Address from first Network Card in Computer
+        /// </summary>
+        /// <returns>[string] MAC Address</returns>
+        List<string> GetMacAddresses();
+
+        /// <summary>
+        /// Determines whether [is in private address space] [the specified endpoint].
+        /// </summary>
+        /// <param name="endpoint">The endpoint.</param>
+        /// <returns><c>true</c> if [is in private address space] [the specified endpoint]; otherwise, <c>false</c>.</returns>
+        bool IsInPrivateAddressSpace(string endpoint);
+
+        /// <summary>
+        /// Gets the network shares.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <returns>IEnumerable{NetworkShare}.</returns>
+        IEnumerable<NetworkShare> GetNetworkShares(string path);
+
+        /// <summary>
+        /// Gets available devices within the domain
+        /// </summary>
+        /// <returns>PC's in the Domain</returns>
+        IEnumerable<FileSystemEntryInfo> GetNetworkDevices();
+
+        /// <summary>
+        /// Determines whether [is in local network] [the specified endpoint].
+        /// </summary>
+        /// <param name="endpoint">The endpoint.</param>
+        /// <returns><c>true</c> if [is in local network] [the specified endpoint]; otherwise, <c>false</c>.</returns>
+        bool IsInLocalNetwork(string endpoint);
+
+        IpAddressInfo[] GetLocalIpAddresses();
+
+        IpAddressInfo ParseIpAddress(string ipAddress);
+
+        bool TryParseIpAddress(string ipAddress, out IpAddressInfo ipAddressInfo);
+
+        Task<IpAddressInfo[]> GetHostAddressesAsync(string host);
+
+        bool IsAddressInSubnets(string addressString, string[] subnets);
+    }
+}

+ 276 - 0
MediaBrowser.Common/Plugins/BasePlugin.cs

@@ -0,0 +1,276 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.IO;
+
+namespace MediaBrowser.Common.Plugins
+{
+    public abstract class BasePlugin : IPlugin, IPluginAssembly
+    {
+        /// <summary>
+        /// Gets the name of the plugin
+        /// </summary>
+        /// <value>The name.</value>
+        public abstract string Name { get; }
+
+        /// <summary>
+        /// Gets the description.
+        /// </summary>
+        /// <value>The description.</value>
+        public virtual string Description
+        {
+            get { return string.Empty; }
+        }
+
+        /// <summary>
+        /// Gets the unique id.
+        /// </summary>
+        /// <value>The unique id.</value>
+        public virtual Guid Id { get; private set; }
+
+        /// <summary>
+        /// Gets the plugin version
+        /// </summary>
+        /// <value>The version.</value>
+        public Version Version { get; private set; }
+
+        /// <summary>
+        /// Gets the path to the assembly file
+        /// </summary>
+        /// <value>The assembly file path.</value>
+        public string AssemblyFilePath { get; private set; }
+
+        /// <summary>
+        /// Gets the plugin info.
+        /// </summary>
+        /// <returns>PluginInfo.</returns>
+        public virtual PluginInfo GetPluginInfo()
+        {
+            var info = new PluginInfo
+            {
+                Name = Name,
+                Version = Version.ToString(),
+                Description = Description,
+                Id = Id.ToString()
+            };
+
+            return info;
+        }
+
+        /// <summary>
+        /// Called when just before the plugin is uninstalled from the server.
+        /// </summary>
+        public virtual void OnUninstalling()
+        {
+
+        }
+
+        public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion)
+        {
+            AssemblyFilePath = assemblyFilePath;
+            DataFolderPath = dataFolderPath;
+            Version = assemblyVersion;
+        }
+
+        public void SetId(Guid assemblyId)
+        {
+            Id = assemblyId;
+        }
+
+        /// <summary>
+        /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed
+        /// </summary>
+        /// <value>The data folder path.</value>
+        public string DataFolderPath { get; private set; }
+    }
+
+    /// <summary>
+    /// Provides a common base class for all plugins
+    /// </summary>
+    /// <typeparam name="TConfigurationType">The type of the T configuration type.</typeparam>
+    public abstract class BasePlugin<TConfigurationType> : BasePlugin, IHasPluginConfiguration
+        where TConfigurationType : BasePluginConfiguration
+    {
+        /// <summary>
+        /// Gets the application paths.
+        /// </summary>
+        /// <value>The application paths.</value>
+        protected IApplicationPaths ApplicationPaths { get; private set; }
+
+        /// <summary>
+        /// Gets the XML serializer.
+        /// </summary>
+        /// <value>The XML serializer.</value>
+        protected IXmlSerializer XmlSerializer { get; private set; }
+
+        /// <summary>
+        /// Gets the type of configuration this plugin uses
+        /// </summary>
+        /// <value>The type of the configuration.</value>
+        public Type ConfigurationType
+        {
+            get { return typeof(TConfigurationType); }
+        }
+
+        private Action<string> _directoryCreateFn;
+        public void SetStartupInfo(Action<string> directoryCreateFn)
+        {
+            // hack alert, until the .net core transition is complete
+            _directoryCreateFn = directoryCreateFn;
+        }
+
+        /// <summary>
+        /// Gets the name the assembly file
+        /// </summary>
+        /// <value>The name of the assembly file.</value>
+        protected string AssemblyFileName
+        {
+            get
+            {
+                return Path.GetFileName(AssemblyFilePath);
+            }
+        }
+
+        /// <summary>
+        /// The _configuration sync lock
+        /// </summary>
+        private readonly object _configurationSyncLock = new object();
+        /// <summary>
+        /// The _configuration
+        /// </summary>
+        private TConfigurationType _configuration;
+        /// <summary>
+        /// Gets the plugin's configuration
+        /// </summary>
+        /// <value>The configuration.</value>
+        public TConfigurationType Configuration
+        {
+            get
+            {
+                // Lazy load
+                if (_configuration == null)
+                {
+                    lock (_configurationSyncLock)
+                    {
+                        if (_configuration == null)
+                        {
+                            _configuration = LoadConfiguration();
+                        }
+                    }
+                }
+                return _configuration;
+            }
+            protected set
+            {
+                _configuration = value;
+            }
+        }
+
+        private TConfigurationType LoadConfiguration()
+        {
+            var path = ConfigurationFilePath;
+
+            try
+            {
+                return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path);
+            }
+            catch
+            {
+                return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
+            }
+        }
+
+        /// <summary>
+        /// Gets the name of the configuration file. Subclasses should override
+        /// </summary>
+        /// <value>The name of the configuration file.</value>
+        public virtual string ConfigurationFileName
+        {
+            get { return Path.ChangeExtension(AssemblyFileName, ".xml"); }
+        }
+
+        /// <summary>
+        /// Gets the full path to the configuration file
+        /// </summary>
+        /// <value>The configuration file path.</value>
+        public string ConfigurationFilePath
+        {
+            get
+            {
+                return Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName);
+            }
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class.
+        /// </summary>
+        /// <param name="applicationPaths">The application paths.</param>
+        /// <param name="xmlSerializer">The XML serializer.</param>
+        protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+        {
+            ApplicationPaths = applicationPaths;
+            XmlSerializer = xmlSerializer;
+        }
+
+        /// <summary>
+        /// The _save lock
+        /// </summary>
+        private readonly object _configurationSaveLock = new object();
+
+        /// <summary>
+        /// Saves the current configuration to the file system
+        /// </summary>
+        public virtual void SaveConfiguration()
+        {
+            lock (_configurationSaveLock)
+            {
+                _directoryCreateFn(Path.GetDirectoryName(ConfigurationFilePath));
+
+                XmlSerializer.SerializeToFile(Configuration, ConfigurationFilePath);
+            }
+        }
+
+        /// <summary>
+        /// Completely overwrites the current configuration with a new copy
+        /// Returns true or false indicating success or failure
+        /// </summary>
+        /// <param name="configuration">The configuration.</param>
+        /// <exception cref="System.ArgumentNullException">configuration</exception>
+        public virtual void UpdateConfiguration(BasePluginConfiguration configuration)
+        {
+            if (configuration == null)
+            {
+                throw new ArgumentNullException("configuration");
+            }
+
+            Configuration = (TConfigurationType)configuration;
+
+            SaveConfiguration();
+        }
+
+        /// <summary>
+        /// Gets the plugin's configuration
+        /// </summary>
+        /// <value>The configuration.</value>
+        BasePluginConfiguration IHasPluginConfiguration.Configuration
+        {
+            get { return Configuration; }
+        }
+
+        public override PluginInfo GetPluginInfo()
+        {
+            var info = base.GetPluginInfo();
+
+            info.ConfigurationFileName = ConfigurationFileName;
+
+            return info;
+        }
+    }
+
+    public interface IPluginAssembly
+    {
+        void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion);
+        void SetId(Guid assemblyId);
+    }
+}

+ 83 - 0
MediaBrowser.Common/Plugins/IPlugin.cs

@@ -0,0 +1,83 @@
+using MediaBrowser.Model.Plugins;
+using System;
+
+namespace MediaBrowser.Common.Plugins
+{
+    /// <summary>
+    /// Interface IPlugin
+    /// </summary>
+    public interface IPlugin
+    {
+        /// <summary>
+        /// Gets the name of the plugin
+        /// </summary>
+        /// <value>The name.</value>
+        string Name { get; }
+
+        /// <summary>
+        /// Gets the description.
+        /// </summary>
+        /// <value>The description.</value>
+        string Description { get; }
+
+        /// <summary>
+        /// Gets the unique id.
+        /// </summary>
+        /// <value>The unique id.</value>
+        Guid Id { get; }
+
+        /// <summary>
+        /// Gets the plugin version
+        /// </summary>
+        /// <value>The version.</value>
+        Version Version { get; }
+
+        /// <summary>
+        /// Gets the path to the assembly file
+        /// </summary>
+        /// <value>The assembly file path.</value>
+        string AssemblyFilePath { get; }
+
+        /// <summary>
+        /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed
+        /// </summary>
+        /// <value>The data folder path.</value>
+        string DataFolderPath { get; }
+
+        /// <summary>
+        /// Gets the plugin info.
+        /// </summary>
+        /// <returns>PluginInfo.</returns>
+        PluginInfo GetPluginInfo();
+
+        /// <summary>
+        /// Called when just before the plugin is uninstalled from the server.
+        /// </summary>
+        void OnUninstalling();
+    }
+
+    public interface IHasPluginConfiguration
+    {
+        /// <summary>
+        /// Gets the type of configuration this plugin uses
+        /// </summary>
+        /// <value>The type of the configuration.</value>
+        Type ConfigurationType { get; }
+
+        /// <summary>
+        /// Completely overwrites the current configuration with a new copy
+        /// Returns true or false indicating success or failure
+        /// </summary>
+        /// <param name="configuration">The configuration.</param>
+        /// <exception cref="System.ArgumentNullException">configuration</exception>
+        void UpdateConfiguration(BasePluginConfiguration configuration);
+
+        /// <summary>
+        /// Gets the plugin's configuration
+        /// </summary>
+        /// <value>The configuration.</value>
+        BasePluginConfiguration Configuration { get; }
+
+        void SetStartupInfo(Action<string> directoryCreateFn);
+    }
+}

+ 54 - 0
MediaBrowser.Common/Progress/ActionableProgress.cs

@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Common.Progress
+{
+    /// <summary>
+    /// Class ActionableProgress
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
+    public class ActionableProgress<T> : IProgress<T>
+    {
+        /// <summary>
+        /// The _actions
+        /// </summary>
+        private Action<T> _action;
+        public event EventHandler<T> ProgressChanged;
+
+        /// <summary>
+        /// Registers the action.
+        /// </summary>
+        /// <param name="action">The action.</param>
+        public void RegisterAction(Action<T> action)
+        {
+            _action = action;
+        }
+
+        public void Report(T value)
+        {
+            if (ProgressChanged != null)
+            {
+                ProgressChanged(this, value);
+            }
+
+            var action = _action;
+            if (action != null)
+            {
+                action(value);
+            }
+        }
+    }
+
+    public class SimpleProgress<T> : IProgress<T>
+    {
+        public event EventHandler<T> ProgressChanged;
+
+        public void Report(T value)
+        {
+            if (ProgressChanged != null)
+            {
+                ProgressChanged(this, value);
+            }
+        }
+    }
+}

+ 27 - 0
MediaBrowser.Common/Properties/AssemblyInfo.cs

@@ -0,0 +1,27 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("MediaBrowser.Common")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.Common")]
+[assembly: AssemblyCopyright("Copyright ©  2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//

+ 15 - 0
MediaBrowser.Common/Security/IRequiresRegistration.cs

@@ -0,0 +1,15 @@
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Security
+{
+    public interface IRequiresRegistration
+    {
+        /// <summary>
+        /// Load all registration information required for this entity.
+        /// Your class should re-load all MBRegistrationRecords when this is called even if they were
+        /// previously loaded.
+        /// </summary>
+        /// <returns></returns>
+        Task LoadRegistrationInfoAsync();
+    }
+}

+ 32 - 0
MediaBrowser.Common/Security/ISecurityManager.cs

@@ -0,0 +1,32 @@
+using MediaBrowser.Model.Entities;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Security
+{
+    public interface ISecurityManager
+    {
+        /// <summary>
+        /// Gets a value indicating whether this instance is MB supporter.
+        /// </summary>
+        /// <value><c>true</c> if this instance is MB supporter; otherwise, <c>false</c>.</value>
+        Task<bool> IsSupporter();
+
+        /// <summary>
+        /// Gets or sets the supporter key.
+        /// </summary>
+        /// <value>The supporter key.</value>
+        string SupporterKey { get; }
+
+        /// <summary>
+        /// Gets the registration status. Overload to support existing plug-ins.
+        /// </summary>
+        Task<MBRegistrationRecord> GetRegistrationStatus(string feature);
+
+        /// <summary>
+        /// Register and app store sale with our back-end
+        /// </summary>
+        /// <param name="parameters">Json parameters to pass to admin server</param>
+        Task RegisterAppStoreSale(string parameters);
+        Task UpdateSupporterKey(string newValue);
+    }
+}

+ 8 - 0
MediaBrowser.Common/Security/PaymentRequiredException.cs

@@ -0,0 +1,8 @@
+using System;
+
+namespace MediaBrowser.Common.Security
+{
+    public class PaymentRequiredException : Exception
+    {
+    }
+}

+ 278 - 0
MediaBrowser.Common/Updates/GithubUpdater.cs

@@ -0,0 +1,278 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Updates;
+
+namespace MediaBrowser.Common.Updates
+{
+    public class GithubUpdater
+    {
+        private readonly IHttpClient _httpClient;
+        private readonly IJsonSerializer _jsonSerializer;
+
+        public GithubUpdater(IHttpClient httpClient, IJsonSerializer jsonSerializer)
+        {
+            _httpClient = httpClient;
+            _jsonSerializer = jsonSerializer;
+        }
+
+        public async Task<CheckForUpdateResult> CheckForUpdateResult(string organzation, string repository, Version minVersion, PackageVersionClass updateLevel, string assetFilename, string packageName, string targetFilename, TimeSpan cacheLength, CancellationToken cancellationToken)
+        {
+            var url = string.Format("https://api.github.com/repos/{0}/{1}/releases", organzation, repository);
+
+            var options = new HttpRequestOptions
+            {
+                Url = url,
+                EnableKeepAlive = false,
+                CancellationToken = cancellationToken,
+                UserAgent = "Emby/3.0",
+                BufferContent = false
+            };
+
+            if (cacheLength.Ticks > 0)
+            {
+                options.CacheMode = CacheMode.Unconditional;
+                options.CacheLength = cacheLength;
+            }
+
+            using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false))
+            {
+                using (var stream = response.Content)
+                {
+                    var obj = _jsonSerializer.DeserializeFromStream<RootObject[]>(stream);
+
+                    return CheckForUpdateResult(obj, minVersion, updateLevel, assetFilename, packageName, targetFilename);
+                }
+            }
+        }
+
+        private CheckForUpdateResult CheckForUpdateResult(RootObject[] obj, Version minVersion, PackageVersionClass updateLevel, string assetFilename, string packageName, string targetFilename)
+        {
+            if (updateLevel == PackageVersionClass.Release)
+            {
+                // Technically all we need to do is check that it's not pre-release
+                // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly.
+                obj = obj.Where(i => !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) && !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase)).ToArray();
+            }
+            else if (updateLevel == PackageVersionClass.Beta)
+            {
+                obj = obj.Where(i => i.prerelease && i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase)).ToArray();
+            }
+            else if (updateLevel == PackageVersionClass.Dev)
+            {
+                obj = obj.Where(i => !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) || i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase)).ToArray();
+            }
+
+            var availableUpdate = obj
+                .Select(i => CheckForUpdateResult(i, minVersion, assetFilename, packageName, targetFilename))
+                .Where(i => i != null)
+                .OrderByDescending(i => Version.Parse(i.AvailableVersion))
+                .FirstOrDefault();
+
+            return availableUpdate ?? new CheckForUpdateResult
+            {
+                IsUpdateAvailable = false
+            };
+        }
+
+        private bool MatchesUpdateLevel(RootObject i, PackageVersionClass updateLevel)
+        {
+            if (updateLevel == PackageVersionClass.Beta)
+            {
+                return i.prerelease && i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase);
+            }
+            if (updateLevel == PackageVersionClass.Dev)
+            {
+                return !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) ||
+                       i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase);
+            }
+
+            // Technically all we need to do is check that it's not pre-release
+            // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly.
+            return !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) &&
+                   !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase);
+        }
+
+        public async Task<List<RootObject>> GetLatestReleases(string organzation, string repository, string assetFilename, CancellationToken cancellationToken)
+        {
+            var list = new List<RootObject>();
+
+            var url = string.Format("https://api.github.com/repos/{0}/{1}/releases", organzation, repository);
+
+            var options = new HttpRequestOptions
+            {
+                Url = url,
+                EnableKeepAlive = false,
+                CancellationToken = cancellationToken,
+                UserAgent = "Emby/3.0",
+                BufferContent = false
+            };
+
+            using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false))
+            {
+                using (var stream = response.Content)
+                {
+                    var obj = _jsonSerializer.DeserializeFromStream<RootObject[]>(stream);
+
+                    obj = obj.Where(i => (i.assets ?? new List<Asset>()).Any(a => IsAsset(a, assetFilename, i.tag_name))).ToArray();
+
+                    list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Release)).OrderByDescending(GetVersion).Take(1));
+                    list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Beta)).OrderByDescending(GetVersion).Take(1));
+                    list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Dev)).OrderByDescending(GetVersion).Take(1));
+
+                    return list;
+                }
+            }
+        }
+
+        public Version GetVersion(RootObject obj)
+        {
+            Version version;
+            if (!Version.TryParse(obj.tag_name, out version))
+            {
+                return new Version(1, 0);
+            }
+
+            return version;
+        }
+
+        private CheckForUpdateResult CheckForUpdateResult(RootObject obj, Version minVersion, string assetFilename, string packageName, string targetFilename)
+        {
+            Version version;
+            var versionString = obj.tag_name;
+            if (!Version.TryParse(versionString, out version))
+            {
+                return null;
+            }
+
+            if (version < minVersion)
+            {
+                return null;
+            }
+
+            var asset = (obj.assets ?? new List<Asset>()).FirstOrDefault(i => IsAsset(i, assetFilename, versionString));
+
+            if (asset == null)
+            {
+                return null;
+            }
+
+            return new CheckForUpdateResult
+            {
+                AvailableVersion = version.ToString(),
+                IsUpdateAvailable = version > minVersion,
+                Package = new PackageVersionInfo
+                {
+                    classification = obj.prerelease ?
+                        (obj.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase) ? PackageVersionClass.Dev : PackageVersionClass.Beta) :
+                        PackageVersionClass.Release,
+                    name = packageName,
+                    sourceUrl = asset.browser_download_url,
+                    targetFilename = targetFilename,
+                    versionStr = version.ToString(),
+                    requiredVersionStr = "1.0.0",
+                    description = obj.body,
+                    infoUrl = obj.html_url
+                }
+            };
+        }
+
+        private bool IsAsset(Asset asset, string assetFilename, string version)
+        {
+            var downloadFilename = Path.GetFileName(asset.browser_download_url) ?? string.Empty;
+
+            assetFilename = assetFilename.Replace("{version}", version);
+
+            if (downloadFilename.IndexOf(assetFilename, StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                return true;
+            }
+
+            return string.Equals(assetFilename, downloadFilename, StringComparison.OrdinalIgnoreCase);
+        }
+
+        public class Uploader
+        {
+            public string login { get; set; }
+            public int id { get; set; }
+            public string avatar_url { get; set; }
+            public string gravatar_id { get; set; }
+            public string url { get; set; }
+            public string html_url { get; set; }
+            public string followers_url { get; set; }
+            public string following_url { get; set; }
+            public string gists_url { get; set; }
+            public string starred_url { get; set; }
+            public string subscriptions_url { get; set; }
+            public string organizations_url { get; set; }
+            public string repos_url { get; set; }
+            public string events_url { get; set; }
+            public string received_events_url { get; set; }
+            public string type { get; set; }
+            public bool site_admin { get; set; }
+        }
+
+        public class Asset
+        {
+            public string url { get; set; }
+            public int id { get; set; }
+            public string name { get; set; }
+            public object label { get; set; }
+            public Uploader uploader { get; set; }
+            public string content_type { get; set; }
+            public string state { get; set; }
+            public int size { get; set; }
+            public int download_count { get; set; }
+            public string created_at { get; set; }
+            public string updated_at { get; set; }
+            public string browser_download_url { get; set; }
+        }
+
+        public class Author
+        {
+            public string login { get; set; }
+            public int id { get; set; }
+            public string avatar_url { get; set; }
+            public string gravatar_id { get; set; }
+            public string url { get; set; }
+            public string html_url { get; set; }
+            public string followers_url { get; set; }
+            public string following_url { get; set; }
+            public string gists_url { get; set; }
+            public string starred_url { get; set; }
+            public string subscriptions_url { get; set; }
+            public string organizations_url { get; set; }
+            public string repos_url { get; set; }
+            public string events_url { get; set; }
+            public string received_events_url { get; set; }
+            public string type { get; set; }
+            public bool site_admin { get; set; }
+        }
+
+        public class RootObject
+        {
+            public string url { get; set; }
+            public string assets_url { get; set; }
+            public string upload_url { get; set; }
+            public string html_url { get; set; }
+            public int id { get; set; }
+            public string tag_name { get; set; }
+            public string target_commitish { get; set; }
+            public string name { get; set; }
+            public bool draft { get; set; }
+            public Author author { get; set; }
+            public bool prerelease { get; set; }
+            public string created_at { get; set; }
+            public string published_at { get; set; }
+            public List<Asset> assets { get; set; }
+            public string tarball_url { get; set; }
+            public string zipball_url { get; set; }
+            public string body { get; set; }
+        }
+    }
+}

+ 121 - 0
MediaBrowser.Common/Updates/IInstallationManager.cs

@@ -0,0 +1,121 @@
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Updates;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Updates
+{
+    public interface IInstallationManager : IDisposable
+    {
+        event EventHandler<InstallationEventArgs> PackageInstalling;
+        event EventHandler<InstallationEventArgs> PackageInstallationCompleted;
+        event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed;
+        event EventHandler<InstallationEventArgs> PackageInstallationCancelled;
+
+        /// <summary>
+        /// The current installations
+        /// </summary>
+        List<Tuple<InstallationInfo, CancellationTokenSource>> CurrentInstallations { get; set; }
+
+        /// <summary>
+        /// The completed installations
+        /// </summary>
+        IEnumerable<InstallationInfo> CompletedInstallations { get; }
+
+        /// <summary>
+        /// Occurs when [plugin uninstalled].
+        /// </summary>
+        event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled;
+
+        /// <summary>
+        /// Occurs when [plugin updated].
+        /// </summary>
+        event EventHandler<GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>>> PluginUpdated;
+
+        /// <summary>
+        /// Occurs when [plugin updated].
+        /// </summary>
+        event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled;
+
+        /// <summary>
+        /// Gets all available packages.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="withRegistration">if set to <c>true</c> [with registration].</param>
+        /// <param name="packageType">Type of the package.</param>
+        /// <param name="applicationVersion">The application version.</param>
+        /// <returns>Task{List{PackageInfo}}.</returns>
+        Task<List<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken,
+            bool withRegistration = true,
+                                                                                  string packageType = null,
+                                                                                  Version applicationVersion = null);
+
+        /// <summary>
+        /// Gets all available packages from a static resource.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{List{PackageInfo}}.</returns>
+        Task<List<PackageInfo>> GetAvailablePackagesWithoutRegistrationInfo(CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the package.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <param name="guid">The assembly guid</param>
+        /// <param name="classification">The classification.</param>
+        /// <param name="version">The version.</param>
+        /// <returns>Task{PackageVersionInfo}.</returns>
+        Task<PackageVersionInfo> GetPackage(string name, string guid, PackageVersionClass classification, Version version);
+
+        /// <summary>
+        /// Gets the latest compatible version.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <param name="guid">The assembly guid</param>
+        /// <param name="currentServerVersion">The current server version.</param>
+        /// <param name="classification">The classification.</param>
+        /// <returns>Task{PackageVersionInfo}.</returns>
+        Task<PackageVersionInfo> GetLatestCompatibleVersion(string name, string guid, Version currentServerVersion, PackageVersionClass classification = PackageVersionClass.Release);
+
+        /// <summary>
+        /// Gets the latest compatible version.
+        /// </summary>
+        /// <param name="availablePackages">The available packages.</param>
+        /// <param name="name">The name.</param>
+        /// <param name="guid">The assembly guid</param>
+        /// <param name="currentServerVersion">The current server version.</param>
+        /// <param name="classification">The classification.</param>
+        /// <returns>PackageVersionInfo.</returns>
+        PackageVersionInfo GetLatestCompatibleVersion(IEnumerable<PackageInfo> availablePackages, string name, string guid, Version currentServerVersion, PackageVersionClass classification = PackageVersionClass.Release);
+
+        /// <summary>
+        /// Gets the available plugin updates.
+        /// </summary>
+        /// <param name="applicationVersion">The current server version.</param>
+        /// <param name="withAutoUpdateEnabled">if set to <c>true</c> [with auto update enabled].</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{PackageVersionInfo}}.</returns>
+        Task<IEnumerable<PackageVersionInfo>> GetAvailablePluginUpdates(Version applicationVersion, bool withAutoUpdateEnabled, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Installs the package.
+        /// </summary>
+        /// <param name="package">The package.</param>
+        /// <param name="isPlugin">if set to <c>true</c> [is plugin].</param>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="System.ArgumentNullException">package</exception>
+        Task InstallPackage(PackageVersionInfo package, bool isPlugin, IProgress<double> progress, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Uninstalls a plugin
+        /// </summary>
+        /// <param name="plugin">The plugin.</param>
+        /// <exception cref="System.ArgumentException"></exception>
+        void UninstallPlugin(IPlugin plugin);
+    }
+}

+ 11 - 0
MediaBrowser.Common/Updates/InstallationEventArgs.cs

@@ -0,0 +1,11 @@
+using MediaBrowser.Model.Updates;
+
+namespace MediaBrowser.Common.Updates
+{
+    public class InstallationEventArgs
+    {
+        public InstallationInfo InstallationInfo { get; set; }
+
+        public PackageVersionInfo PackageVersionInfo { get; set; }
+    }
+}

+ 9 - 0
MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs

@@ -0,0 +1,9 @@
+using System;
+
+namespace MediaBrowser.Common.Updates
+{
+    public class InstallationFailedEventArgs : InstallationEventArgs
+    {
+        public Exception Exception { get; set; }
+    }
+}

+ 14 - 0
MediaBrowser.Controller/Authentication/AuthenticationResult.cs

@@ -0,0 +1,14 @@
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
+
+
+namespace MediaBrowser.Controller.Authentication
+{
+    public class AuthenticationResult
+    {
+        public UserDto User { get; set; }
+        public SessionInfo SessionInfo { get; set; }
+        public string AccessToken { get; set; }
+        public string ServerId { get; set; }
+    }
+}

+ 35 - 0
MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs

@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Users;
+
+namespace MediaBrowser.Controller.Authentication
+{
+    public interface IAuthenticationProvider
+    {
+        string Name { get; }
+        bool IsEnabled { get; }
+        Task<ProviderAuthenticationResult> Authenticate(string username, string password);
+        Task<bool> HasPassword(User user);
+        Task ChangePassword(User user, string newPassword);
+    }
+
+    public interface IRequiresResolvedUser
+    {
+        Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser);
+    }
+
+    public interface IHasNewUserPolicy
+    {
+        UserPolicy GetNewUserPolicy();
+    }
+
+    public class ProviderAuthenticationResult
+    {
+        public string Username { get; set; }
+        public string DisplayName { get; set; }
+    }
+}

+ 94 - 0
MediaBrowser.Controller/Channels/Channel.cs

@@ -0,0 +1,94 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Linq;
+using MediaBrowser.Model.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Progress;
+
+namespace MediaBrowser.Controller.Channels
+{
+    public class Channel : Folder
+    {
+        public override bool IsVisible(User user)
+        {
+            if (user.Policy.BlockedChannels != null)
+            {
+                if (user.Policy.BlockedChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
+                {
+                    return false;
+                }
+            }
+            else
+            {
+                if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
+                {
+                    return false;
+                }
+            }
+
+            return base.IsVisible(user);
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsInheritedParentImages
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override SourceType SourceType
+        {
+            get { return SourceType.Channel; }
+        }
+
+        protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
+        {
+            try
+            {
+                query.Parent = this;
+                query.ChannelIds = new Guid[] { Id };
+
+                // Don't blow up here because it could cause parent screens with other content to fail
+                return ChannelManager.GetChannelItemsInternal(query, new SimpleProgress<double>(), CancellationToken.None).Result;
+            }
+            catch
+            {
+                // Already logged at lower levels
+                return new QueryResult<BaseItem>();
+            }
+        }
+
+        protected override string GetInternalMetadataPath(string basePath)
+        {
+            return GetInternalMetadataPath(basePath, Id);
+        }
+
+        public static string GetInternalMetadataPath(string basePath, Guid id)
+        {
+            return System.IO.Path.Combine(basePath, "channels", id.ToString("N"), "metadata");
+        }
+
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
+        protected override bool IsAllowTagFilterEnforced()
+        {
+            return false;
+        }
+
+        internal static bool IsChannelVisible(BaseItem channelItem, User user)
+        {
+            var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString(""));
+
+            return channel.IsVisible(user);
+        }
+    }
+}

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

@@ -0,0 +1,82 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Controller.Channels
+{
+    public class ChannelItemInfo : IHasProviderIds
+    {
+        public string Name { get; set; }
+
+        public string SeriesName { get; set; }
+
+        public string Id { get; set; }
+
+        public DateTime DateModified { get; set; }
+
+        public ChannelItemType Type { get; set; }
+
+        public string OfficialRating { get; set; }
+
+        public string Overview { get; set; }
+
+        public List<string> Genres { get; set; }
+        public List<string> Studios { get; set; }
+        public List<string> Tags { get; set; }
+
+        public List<PersonInfo> People { get; set; }
+
+        public float? CommunityRating { get; set; }
+
+        public long? RunTimeTicks { get; set; }
+
+        public string ImageUrl { get; set; }
+        public string OriginalTitle { get; set; }
+
+        public ChannelMediaType MediaType { get; set; }
+        public ChannelFolderType FolderType { get; set; }
+
+        public ChannelMediaContentType ContentType { get; set; }
+        public ExtraType ExtraType { get; set; }
+        public List<TrailerType> TrailerTypes { get; set; }
+
+        public Dictionary<string, string> ProviderIds { get; set; }
+
+        public DateTime? PremiereDate { get; set; }
+        public int? ProductionYear { get; set; }
+
+        public DateTime? DateCreated { get; set; }
+
+        public DateTime? StartDate { get; set; }
+        public DateTime? EndDate { get; set; }
+
+        public int? IndexNumber { get; set; }
+        public int? ParentIndexNumber { get; set; }
+
+        public List<MediaSourceInfo> MediaSources { get; set; }
+
+        public string HomePageUrl { get; set; }
+
+        public List<string> Artists { get; set; }
+
+        public List<string> AlbumArtists { get; set; }
+        public bool IsLiveStream { get; set; }
+        public string Etag { get; set; }
+
+        public ChannelItemInfo()
+        {
+            MediaSources = new List<MediaSourceInfo>();
+            TrailerTypes = new List<TrailerType>();
+            Genres = new List<string>();
+            Studios = new List<string>();
+            People = new List<PersonInfo>();
+            Tags = new List<string>();
+            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            Artists = new List<string>();
+            AlbumArtists = new List<string>();
+        }
+    }
+}

+ 16 - 0
MediaBrowser.Controller/Channels/ChannelItemResult.cs

@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Channels
+{
+    public class ChannelItemResult
+    {
+        public List<ChannelItemInfo> Items { get; set; }
+
+        public int? TotalRecordCount { get; set; }
+
+        public ChannelItemResult()
+        {
+            Items = new List<ChannelItemInfo>();
+        }
+    }
+}

+ 9 - 0
MediaBrowser.Controller/Channels/ChannelItemType.cs

@@ -0,0 +1,9 @@
+namespace MediaBrowser.Controller.Channels
+{
+    public enum ChannelItemType
+    {
+        Media = 0,
+
+        Folder = 1
+    }
+}

+ 15 - 0
MediaBrowser.Controller/Channels/ChannelParentalRating.cs

@@ -0,0 +1,15 @@
+namespace MediaBrowser.Controller.Channels
+{
+    public enum ChannelParentalRating
+    {
+        GeneralAudience = 0,
+
+        UsPG = 1,
+
+        UsPG13 = 2,
+
+        UsR = 3,
+
+        Adult = 4
+    }
+}

+ 14 - 0
MediaBrowser.Controller/Channels/ChannelSearchInfo.cs

@@ -0,0 +1,14 @@
+namespace MediaBrowser.Controller.Channels
+{
+    public class ChannelSearchInfo
+    {
+        public string SearchTerm { get; set; }
+
+        public string UserId { get; set; }
+    }
+
+    public class ChannelLatestMediaSearch
+    {
+        public string UserId { get; set; }
+    }
+}

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

@@ -0,0 +1,76 @@
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Channels
+{
+    public interface IChannel
+    {
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        string Name { get; }
+
+        /// <summary>
+        /// Gets the description.
+        /// </summary>
+        /// <value>The description.</value>
+        string Description { get; }
+
+        /// <summary>
+        /// Gets the data version.
+        /// </summary>
+        /// <value>The data version.</value>
+        string DataVersion { get; }
+
+        /// <summary>
+        /// Gets the home page URL.
+        /// </summary>
+        /// <value>The home page URL.</value>
+        string HomePageUrl { get; }
+
+        /// <summary>
+        /// Gets the parental rating.
+        /// </summary>
+        /// <value>The parental rating.</value>
+        ChannelParentalRating ParentalRating { get; }
+
+        /// <summary>
+        /// Gets the channel information.
+        /// </summary>
+        /// <returns>ChannelFeatures.</returns>
+        InternalChannelFeatures GetChannelFeatures();
+
+        /// <summary>
+        /// Determines whether [is enabled for] [the specified user].
+        /// </summary>
+        /// <param name="userId">The user identifier.</param>
+        /// <returns><c>true</c> if [is enabled for] [the specified user]; otherwise, <c>false</c>.</returns>
+        bool IsEnabledFor(string userId);
+
+        /// <summary>
+        /// Gets the channel items.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{ChannelItem}}.</returns>
+        Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the channel image.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{DynamicImageInfo}.</returns>
+        Task<DynamicImageResponse> GetChannelImage(ImageType type, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the supported channel images.
+        /// </summary>
+        /// <returns>IEnumerable{ImageType}.</returns>
+        IEnumerable<ImageType> GetSupportedChannelImages();
+    }
+}

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

@@ -0,0 +1,89 @@
+using System;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Channels
+{
+    public interface IChannelManager
+    {
+        /// <summary>
+        /// Adds the parts.
+        /// </summary>
+        /// <param name="channels">The channels.</param>
+        void AddParts(IEnumerable<IChannel> channels);
+
+        /// <summary>
+        /// Gets the channel features.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <returns>ChannelFeatures.</returns>
+        ChannelFeatures GetChannelFeatures(string id);
+
+        /// <summary>
+        /// Gets all channel features.
+        /// </summary>
+        /// <returns>IEnumerable{ChannelFeatures}.</returns>
+        ChannelFeatures[] GetAllChannelFeatures();
+
+        bool EnableMediaSourceDisplay(BaseItem item);
+        bool CanDelete(BaseItem item);
+
+        Task DeleteItem(BaseItem item);
+
+        /// <summary>
+        /// Gets the channel.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <returns>Channel.</returns>
+        Channel GetChannel(string id);
+
+        /// <summary>
+        /// Gets the channels internal.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        QueryResult<Channel> GetChannelsInternal(ChannelQuery query);
+
+        /// <summary>
+        /// Gets the channels.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        QueryResult<BaseItemDto> GetChannels(ChannelQuery query);
+
+        /// <summary>
+        /// Gets the latest media.
+        /// </summary>
+        Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the latest media.
+        /// </summary>
+        Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the channel items.
+        /// </summary>
+        Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the channel items internal.
+        /// </summary>
+        Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> progress, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the channel item media sources.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{MediaSourceInfo}}.</returns>
+        IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken);
+
+        bool EnableMediaProbe(BaseItem item);
+    }
+}

+ 13 - 0
MediaBrowser.Controller/Channels/IHasCacheKey.cs

@@ -0,0 +1,13 @@
+
+namespace MediaBrowser.Controller.Channels
+{
+    public interface IHasCacheKey
+    {
+        /// <summary>
+        /// Gets the cache key.
+        /// </summary>
+        /// <param name="userId">The user identifier.</param>
+        /// <returns>System.String.</returns>
+        string GetCacheKey(string userId);
+    }
+}

+ 15 - 0
MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs

@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Controller.Channels
+{
+    public interface IRequiresMediaInfoCallback
+    {
+        /// <summary>
+        /// Gets the channel item media information.
+        /// </summary>
+        Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaInfo(string id, CancellationToken cancellationToken);
+    }
+}

+ 50 - 0
MediaBrowser.Controller/Channels/ISearchableChannel.cs

@@ -0,0 +1,50 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Channels
+{
+    public interface ISearchableChannel
+    {
+        /// <summary>
+        /// Searches the specified search term.
+        /// </summary>
+        /// <param name="searchInfo">The search information.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns>
+        Task<IEnumerable<ChannelItemInfo>> Search(ChannelSearchInfo searchInfo, CancellationToken cancellationToken);
+    }
+
+    public interface ISupportsLatestMedia
+    {
+        /// <summary>
+        /// Gets the latest media.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns>
+        Task<IEnumerable<ChannelItemInfo>> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken);
+    }
+
+    public interface ISupportsDelete
+    {
+        bool CanDelete(BaseItem item);
+        Task DeleteItem(string id, CancellationToken cancellationToken);
+    }
+
+    public interface IDisableMediaSourceDisplay
+    {
+
+    }
+
+    public interface ISupportsMediaProbe
+    {
+
+    }
+
+    public interface IHasFolderAttributes
+    {
+        string[] Attributes { get; }
+    }
+}

+ 61 - 0
MediaBrowser.Controller/Channels/InternalChannelFeatures.cs

@@ -0,0 +1,61 @@
+using System;
+using MediaBrowser.Model.Channels;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Channels
+{
+    public class InternalChannelFeatures
+    {
+        /// <summary>
+        /// Gets or sets the media types.
+        /// </summary>
+        /// <value>The media types.</value>
+        public List<ChannelMediaType> MediaTypes { get; set; }
+
+        /// <summary>
+        /// Gets or sets the content types.
+        /// </summary>
+        /// <value>The content types.</value>
+        public List<ChannelMediaContentType> ContentTypes { get; set; }
+
+        /// <summary>
+        /// Represents the maximum number of records the channel allows retrieving at a time
+        /// </summary>
+        public int? MaxPageSize { get; set; }
+
+        /// <summary>
+        /// Gets or sets the default sort orders.
+        /// </summary>
+        /// <value>The default sort orders.</value>
+        public List<ChannelItemSortField> DefaultSortFields { get; set; }
+
+        /// <summary>
+        /// Indicates if a sort ascending/descending toggle is supported or not.
+        /// </summary>
+        public bool SupportsSortOrderToggle { get; set; }
+        /// <summary>
+        /// Gets or sets the automatic refresh levels.
+        /// </summary>
+        /// <value>The automatic refresh levels.</value>
+        public int? AutoRefreshLevels { get; set; }
+
+        /// <summary>
+        /// Gets or sets the daily download limit.
+        /// </summary>
+        /// <value>The daily download limit.</value>
+        public int? DailyDownloadLimit { get; set; }
+        /// <summary>
+        /// Gets or sets a value indicating whether [supports downloading].
+        /// </summary>
+        /// <value><c>true</c> if [supports downloading]; otherwise, <c>false</c>.</value>
+        public bool SupportsContentDownloading { get; set; }
+
+        public InternalChannelFeatures()
+        {
+            MediaTypes = new List<ChannelMediaType>();
+            ContentTypes = new List<ChannelMediaContentType>();
+
+            DefaultSortFields = new List<ChannelItemSortField>();
+        }
+    }
+}

+ 21 - 0
MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs

@@ -0,0 +1,21 @@
+using MediaBrowser.Model.Channels;
+using System;
+
+
+namespace MediaBrowser.Controller.Channels
+{
+    public class InternalChannelItemQuery
+    {
+        public string FolderId { get; set; }
+
+        public Guid UserId { get; set; }
+
+        public int? StartIndex { get; set; }
+
+        public int? Limit { get; set; }
+
+        public ChannelItemSortField? SortBy { get; set; }
+
+        public bool SortDescending { get; set; }
+    }
+}

+ 23 - 0
MediaBrowser.Controller/Chapters/IChapterManager.cs

@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Chapters
+{
+    /// <summary>
+    /// Interface IChapterManager
+    /// </summary>
+    public interface IChapterManager
+    {
+        /// <summary>
+        /// Gets the chapters.
+        /// </summary>
+        /// <param name="itemId">The item identifier.</param>
+        /// <returns>List{ChapterInfo}.</returns>
+
+        /// <summary>
+        /// Saves the chapters.
+        /// </summary>
+        void SaveChapters(string itemId, List<ChapterInfo> chapters);
+    }
+}

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

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

+ 37 - 0
MediaBrowser.Controller/Collections/CollectionEvents.cs

@@ -0,0 +1,37 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Collections
+{
+    public class CollectionCreatedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Gets or sets the collection.
+        /// </summary>
+        /// <value>The collection.</value>
+        public BoxSet Collection { get; set; }
+
+        /// <summary>
+        /// Gets or sets the options.
+        /// </summary>
+        /// <value>The options.</value>
+        public CollectionCreationOptions Options { get; set; }
+    }
+
+    public class CollectionModifiedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Gets or sets the collection.
+        /// </summary>
+        /// <value>The collection.</value>
+        public BoxSet Collection { get; set; }
+
+        /// <summary>
+        /// Gets or sets the items changed.
+        /// </summary>
+        /// <value>The items changed.</value>
+        public List<BaseItem> ItemsChanged { get; set; }
+    }
+}

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

@@ -0,0 +1,57 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Collections
+{
+    public interface ICollectionManager
+    {
+        /// <summary>
+        /// Occurs when [collection created].
+        /// </summary>
+        event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
+
+        /// <summary>
+        /// Occurs when [items added to collection].
+        /// </summary>
+        event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
+
+        /// <summary>
+        /// Occurs when [items removed from collection].
+        /// </summary>
+        event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
+
+        /// <summary>
+        /// Creates the collection.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        BoxSet CreateCollection(CollectionCreationOptions options);
+
+        /// <summary>
+        /// Adds to collection.
+        /// </summary>
+        /// <param name="collectionId">The collection identifier.</param>
+        /// <param name="itemIds">The item ids.</param>
+        void AddToCollection(Guid collectionId, IEnumerable<string> itemIds);
+
+        /// <summary>
+        /// Removes from collection.
+        /// </summary>
+        /// <param name="collectionId">The collection identifier.</param>
+        /// <param name="itemIds">The item ids.</param>
+        void RemoveFromCollection(Guid collectionId, IEnumerable<string> itemIds);
+
+        void AddToCollection(Guid collectionId, IEnumerable<Guid> itemIds);
+        void RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds);
+
+        /// <summary>
+        /// Collapses the items within box sets.
+        /// </summary>
+        /// <param name="items">The items.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user);
+    }
+}

+ 25 - 0
MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs

@@ -0,0 +1,25 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.Configuration
+{
+    /// <summary>
+    /// Interface IServerConfigurationManager
+    /// </summary>
+    public interface IServerConfigurationManager : IConfigurationManager
+    {
+        /// <summary>
+        /// Gets the application paths.
+        /// </summary>
+        /// <value>The application paths.</value>
+        IServerApplicationPaths ApplicationPaths { get; }
+
+        /// <summary>
+        /// Gets the configuration.
+        /// </summary>
+        /// <value>The configuration.</value>
+        ServerConfiguration Configuration { get; }
+
+        bool SetOptimalValues();
+    }
+}

+ 45 - 0
MediaBrowser.Controller/Connect/IConnectManager.cs

@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Connect;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Connect
+{
+    public interface IConnectManager
+    {
+        /// <summary>
+        /// Gets the wan API address.
+        /// </summary>
+        /// <value>The wan API address.</value>
+        string WanApiAddress { get; }
+
+        /// <summary>
+        /// Links the user.
+        /// </summary>
+        /// <param name="userId">The user identifier.</param>
+        /// <param name="connectUsername">The connect username.</param>
+        /// <returns>Task.</returns>
+        Task<UserLinkResult> LinkUser(string userId, string connectUsername);
+
+        /// <summary>
+        /// Removes the link.
+        /// </summary>
+        /// <param name="userId">The user identifier.</param>
+        /// <returns>Task.</returns>
+        Task RemoveConnect(string userId);
+
+        User GetUserFromExchangeToken(string token);
+
+        /// <summary>
+        /// Authenticates the specified username.
+        /// </summary>
+        Task<ConnectAuthenticationResult> Authenticate(string username, string password, string passwordMd5);
+
+        /// <summary>
+        /// Determines whether [is authorization token valid] [the specified token].
+        /// </summary>
+        /// <param name="token">The token.</param>
+        /// <returns><c>true</c> if [is authorization token valid] [the specified token]; otherwise, <c>false</c>.</returns>
+        bool IsAuthorizationTokenValid(string token);
+    }
+}

+ 10 - 0
MediaBrowser.Controller/Connect/UserLinkResult.cs

@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Controller.Connect
+{
+    public class UserLinkResult
+    {
+        public bool IsPending { get; set; }
+        public bool IsNewUserInvitation { get; set; }
+        public string GuestDisplayName { get; set; }
+    }
+}

+ 10 - 0
MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs

@@ -0,0 +1,10 @@
+using MediaBrowser.Model.Devices;
+
+namespace MediaBrowser.Controller.Devices
+{
+    public class CameraImageUploadInfo
+    {
+        public LocalFileInfo FileInfo { get; set; }
+        public DeviceInfo Device { get; set; }
+    }
+}

+ 73 - 0
MediaBrowser.Controller/Devices/IDeviceManager.cs

@@ -0,0 +1,73 @@
+using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Session;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Devices
+{
+    public interface IDeviceManager
+    {
+        /// <summary>
+        /// Occurs when [camera image uploaded].
+        /// </summary>
+        event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
+
+        /// <summary>
+        /// Saves the capabilities.
+        /// </summary>
+        /// <param name="reportedId">The reported identifier.</param>
+        /// <param name="capabilities">The capabilities.</param>
+        /// <returns>Task.</returns>
+        void SaveCapabilities(string reportedId, ClientCapabilities capabilities);
+
+        /// <summary>
+        /// Gets the capabilities.
+        /// </summary>
+        /// <param name="reportedId">The reported identifier.</param>
+        /// <returns>ClientCapabilities.</returns>
+        ClientCapabilities GetCapabilities(string reportedId);
+
+        /// <summary>
+        /// Gets the device information.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <returns>DeviceInfo.</returns>
+        DeviceInfo GetDevice(string id);
+
+        /// <summary>
+        /// Gets the devices.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
+        QueryResult<DeviceInfo> GetDevices(DeviceQuery query);
+
+        /// <summary>
+        /// Gets the upload history.
+        /// </summary>
+        /// <param name="deviceId">The device identifier.</param>
+        /// <returns>ContentUploadHistory.</returns>
+        ContentUploadHistory GetCameraUploadHistory(string deviceId);
+
+        /// <summary>
+        /// Accepts the upload.
+        /// </summary>
+        /// <param name="deviceId">The device identifier.</param>
+        /// <param name="stream">The stream.</param>
+        /// <param name="file">The file.</param>
+        /// <returns>Task.</returns>
+        Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file);
+
+        /// <summary>
+        /// Determines whether this instance [can access device] the specified user identifier.
+        /// </summary>
+        bool CanAccessDevice(User user, string deviceId);
+
+        void UpdateDeviceOptions(string deviceId, DeviceOptions options);
+        DeviceOptions GetDeviceOptions(string deviceId);
+        event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
+    }
+}

+ 76 - 0
MediaBrowser.Controller/Dlna/IDlnaManager.cs

@@ -0,0 +1,76 @@
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Dlna;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Dlna
+{
+    public interface IDlnaManager
+    {
+        /// <summary>
+        /// Gets the profile infos.
+        /// </summary>
+        /// <returns>IEnumerable{DeviceProfileInfo}.</returns>
+        IEnumerable<DeviceProfileInfo> GetProfileInfos();
+
+        /// <summary>
+        /// Gets the profile.
+        /// </summary>
+        /// <param name="headers">The headers.</param>
+        /// <returns>DeviceProfile.</returns>
+        DeviceProfile GetProfile(IDictionary<string,string> headers);
+
+        /// <summary>
+        /// Gets the default profile.
+        /// </summary>
+        /// <returns>DeviceProfile.</returns>
+        DeviceProfile GetDefaultProfile();
+
+        /// <summary>
+        /// Creates the profile.
+        /// </summary>
+        /// <param name="profile">The profile.</param>
+        void CreateProfile(DeviceProfile profile);
+        
+        /// <summary>
+        /// Updates the profile.
+        /// </summary>
+        /// <param name="profile">The profile.</param>
+        void UpdateProfile(DeviceProfile profile);
+        
+        /// <summary>
+        /// Deletes the profile.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        void DeleteProfile(string id);
+        
+        /// <summary>
+        /// Gets the profile.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <returns>DeviceProfile.</returns>
+        DeviceProfile GetProfile(string id);
+        
+        /// <summary>
+        /// Gets the profile.
+        /// </summary>
+        /// <param name="deviceInfo">The device information.</param>
+        /// <returns>DeviceProfile.</returns>
+        DeviceProfile GetProfile(DeviceIdentification deviceInfo);
+
+        /// <summary>
+        /// Gets the server description XML.
+        /// </summary>
+        /// <param name="headers">The headers.</param>
+        /// <param name="serverUuId">The server uu identifier.</param>
+        /// <param name="serverAddress">The server address.</param>
+        /// <returns>System.String.</returns>
+        string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId, string serverAddress);
+
+        /// <summary>
+        /// Gets the icon.
+        /// </summary>
+        /// <param name="filename">The filename.</param>
+        /// <returns>DlnaIconResponse.</returns>
+        ImageStream GetIcon(string filename);
+    }
+}

+ 49 - 0
MediaBrowser.Controller/Drawing/IImageEncoder.cs

@@ -0,0 +1,49 @@
+using System;
+using MediaBrowser.Model.Drawing;
+
+namespace MediaBrowser.Controller.Drawing
+{
+    public interface IImageEncoder
+    {
+        /// <summary>
+        /// Gets the supported input formats.
+        /// </summary>
+        /// <value>The supported input formats.</value>
+        string[] SupportedInputFormats { get; }
+        /// <summary>
+        /// Gets the supported output formats.
+        /// </summary>
+        /// <value>The supported output formats.</value>
+        ImageFormat[] SupportedOutputFormats { get; }
+
+        /// <summary>
+        /// Encodes the image.
+        /// </summary>
+        string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat);
+
+        /// <summary>
+        /// Creates the image collage.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        void CreateImageCollage(ImageCollageOptions options);
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        string Name { get; }
+
+        /// <summary>
+        /// Gets a value indicating whether [supports image collage creation].
+        /// </summary>
+        /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value>
+        bool SupportsImageCollageCreation { get; }
+
+        /// <summary>
+        /// Gets a value indicating whether [supports image encoding].
+        /// </summary>
+        /// <value><c>true</c> if [supports image encoding]; otherwise, <c>false</c>.</value>
+        bool SupportsImageEncoding { get; }
+
+        ImageSize GetImageSize(string path);
+    }
+}

+ 118 - 0
MediaBrowser.Controller/Drawing/IImageProcessor.cs

@@ -0,0 +1,118 @@
+using System;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Drawing
+{
+    /// <summary>
+    /// Interface IImageProcessor
+    /// </summary>
+    public interface IImageProcessor
+    {
+        /// <summary>
+        /// Gets the supported input formats.
+        /// </summary>
+        /// <value>The supported input formats.</value>
+        string[] SupportedInputFormats { get; }
+
+        /// <summary>
+        /// Gets the image enhancers.
+        /// </summary>
+        /// <value>The image enhancers.</value>
+        IImageEnhancer[] ImageEnhancers { get; }
+
+        ImageSize GetImageSize(string path);
+
+        /// <summary>
+        /// Gets the size of the image.
+        /// </summary>
+        /// <param name="info">The information.</param>
+        /// <returns>ImageSize.</returns>
+        ImageSize GetImageSize(BaseItem item, ItemImageInfo info);
+
+        ImageSize GetImageSize(BaseItem item, ItemImageInfo info, bool allowSlowMethods, bool updateItem);
+
+        /// <summary>
+        /// Adds the parts.
+        /// </summary>
+        /// <param name="enhancers">The enhancers.</param>
+        void AddParts(IEnumerable<IImageEnhancer> enhancers);
+
+        /// <summary>
+        /// Gets the supported enhancers.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="imageType">Type of the image.</param>
+        /// <returns>IEnumerable{IImageEnhancer}.</returns>
+        IImageEnhancer[] GetSupportedEnhancers(BaseItem item, ImageType imageType);
+
+        /// <summary>
+        /// Gets the image cache tag.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="image">The image.</param>
+        /// <returns>Guid.</returns>
+        string GetImageCacheTag(BaseItem item, ItemImageInfo image);
+        string GetImageCacheTag(BaseItem item, ChapterInfo info);
+
+        /// <summary>
+        /// Gets the image cache tag.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="image">The image.</param>
+        /// <param name="imageEnhancers">The image enhancers.</param>
+        /// <returns>Guid.</returns>
+        string GetImageCacheTag(BaseItem item, ItemImageInfo image, IImageEnhancer[] imageEnhancers);
+
+        /// <summary>
+        /// Processes the image.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <param name="toStream">To stream.</param>
+        /// <returns>Task.</returns>
+        Task ProcessImage(ImageProcessingOptions options, Stream toStream);
+
+        /// <summary>
+        /// Processes the image.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <returns>Task.</returns>
+        Task<Tuple<string, string, DateTime>> ProcessImage(ImageProcessingOptions options);
+
+        /// <summary>
+        /// Gets the enhanced image.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="imageType">Type of the image.</param>
+        /// <param name="imageIndex">Index of the image.</param>
+        /// <returns>Task{System.String}.</returns>
+        Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex);
+
+        /// <summary>
+        /// Gets the supported image output formats.
+        /// </summary>
+        /// <returns>ImageOutputFormat[].</returns>
+        ImageFormat[] GetSupportedImageOutputFormats();
+
+        /// <summary>
+        /// Creates the image collage.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        void CreateImageCollage(ImageCollageOptions options);
+
+        /// <summary>
+        /// Gets a value indicating whether [supports image collage creation].
+        /// </summary>
+        /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value>
+        bool SupportsImageCollageCreation { get; }
+
+        IImageEncoder ImageEncoder { get; set; }
+
+        bool SupportsTransparency(string path);
+    }
+}

+ 27 - 0
MediaBrowser.Controller/Drawing/ImageCollageOptions.cs

@@ -0,0 +1,27 @@
+
+namespace MediaBrowser.Controller.Drawing
+{
+    public class ImageCollageOptions
+    {
+        /// <summary>
+        /// Gets or sets the input paths.
+        /// </summary>
+        /// <value>The input paths.</value>
+        public string[] InputPaths { get; set; }
+        /// <summary>
+        /// Gets or sets the output path.
+        /// </summary>
+        /// <value>The output path.</value>
+        public string OutputPath { get; set; }
+        /// <summary>
+        /// Gets or sets the width.
+        /// </summary>
+        /// <value>The width.</value>
+        public int Width { get; set; }
+        /// <summary>
+        /// Gets or sets the height.
+        /// </summary>
+        /// <value>The height.</value>
+        public int Height { get; set; }
+    }
+}

+ 72 - 0
MediaBrowser.Controller/Drawing/ImageHelper.cs

@@ -0,0 +1,72 @@
+using System;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Drawing
+{
+    public static class ImageHelper
+    {
+        public static ImageSize GetNewImageSize(ImageProcessingOptions options, ImageSize? originalImageSize)
+        {
+            if (originalImageSize.HasValue)
+            {
+                // Determine the output size based on incoming parameters
+                var newSize = DrawingUtils.Resize(originalImageSize.Value, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0);
+
+                return newSize;
+            }
+            return GetSizeEstimate(options);
+        }
+
+        public static IImageProcessor ImageProcessor { get; set; }
+
+        private static ImageSize GetSizeEstimate(ImageProcessingOptions options)
+        {
+            if (options.Width.HasValue && options.Height.HasValue)
+            {
+                return new ImageSize(options.Width.Value, options.Height.Value);
+            }
+
+            var aspect = GetEstimatedAspectRatio(options.Image.Type, options.Item);
+
+            var width = options.Width ?? options.MaxWidth;
+
+            if (width.HasValue)
+            {
+                var heightValue = width.Value / aspect;
+                return new ImageSize(width.Value, heightValue);
+            }
+
+            var height = options.Height ?? options.MaxHeight ?? 200;
+            var widthValue = aspect * height;
+            return new ImageSize(widthValue, height);
+        }
+
+        private static double GetEstimatedAspectRatio(ImageType type, BaseItem item)
+        {
+            switch (type)
+            {
+                case ImageType.Art:
+                case ImageType.Backdrop:
+                case ImageType.Chapter:
+                case ImageType.Screenshot:
+                case ImageType.Thumb:
+                    return 1.78;
+                case ImageType.Banner:
+                    return 5.4;
+                case ImageType.Box:
+                case ImageType.BoxRear:
+                case ImageType.Disc:
+                case ImageType.Menu:
+                    return 1;
+                case ImageType.Logo:
+                    return 2.58;
+                case ImageType.Primary:
+                    return item.GetDefaultPrimaryImageAspectRatio();
+                default:
+                    return 1;
+            }
+        }
+    }
+}

+ 114 - 0
MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs

@@ -0,0 +1,114 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Drawing;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Drawing
+{
+    public class ImageProcessingOptions
+    {
+        public ImageProcessingOptions()
+        {
+            RequiresAutoOrientation = true;
+        }
+
+        public Guid ItemId { get; set; }
+        public BaseItem Item { get; set; }
+
+        public ItemImageInfo Image { get; set; }
+
+        public int ImageIndex { get; set; }
+
+        public bool CropWhiteSpace { get; set; }
+
+        public int? Width { get; set; }
+
+        public int? Height { get; set; }
+
+        public int? MaxWidth { get; set; }
+
+        public int? MaxHeight { get; set; }
+
+        public int Quality { get; set; }
+
+        public IImageEnhancer[] Enhancers { get; set; }
+
+        public ImageFormat[] SupportedOutputFormats { get; set; }
+
+        public bool AddPlayedIndicator { get; set; }
+
+        public int? UnplayedCount { get; set; }
+        public int? Blur { get; set; }
+
+        public double PercentPlayed { get; set; }
+
+        public string BackgroundColor { get; set; }
+        public string ForegroundLayer { get; set; }
+        public bool RequiresAutoOrientation { get; set; }
+
+        private bool HasDefaultOptions(string originalImagePath)
+        {
+            return HasDefaultOptionsWithoutSize(originalImagePath) &&
+                !Width.HasValue &&
+                !Height.HasValue &&
+                !MaxWidth.HasValue &&
+                !MaxHeight.HasValue;
+        }
+
+        public bool HasDefaultOptions(string originalImagePath, ImageSize? size)
+        {
+            if (!size.HasValue)
+            {
+                return HasDefaultOptions(originalImagePath);
+            }
+
+            if (!HasDefaultOptionsWithoutSize(originalImagePath))
+            {
+                return false;
+            }
+
+            var sizeValue = size.Value;
+
+            if (Width.HasValue && !sizeValue.Width.Equals(Width.Value))
+            {
+                return false;
+            }
+            if (Height.HasValue && !sizeValue.Height.Equals(Height.Value))
+            {
+                return false;
+            }
+            if (MaxWidth.HasValue && sizeValue.Width > MaxWidth.Value)
+            {
+                return false;
+            }
+            if (MaxHeight.HasValue && sizeValue.Height > MaxHeight.Value)
+            {
+                return false;
+            }
+
+            return true;
+        }
+
+        private bool HasDefaultOptionsWithoutSize(string originalImagePath)
+        {
+            return (Quality >= 90) &&
+                IsFormatSupported(originalImagePath) &&
+                !AddPlayedIndicator &&
+                PercentPlayed.Equals(0) &&
+                !UnplayedCount.HasValue &&
+                !Blur.HasValue &&
+                !CropWhiteSpace &&
+                string.IsNullOrEmpty(BackgroundColor) &&
+                string.IsNullOrEmpty(ForegroundLayer);
+        }
+
+        private bool IsFormatSupported(string originalImagePath)
+        {
+            var ext = Path.GetExtension(originalImagePath);
+            return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase));
+        }
+    }
+}

+ 25 - 0
MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs

@@ -0,0 +1,25 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Drawing
+{
+    public static class ImageProcessorExtensions
+    {
+        public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType)
+        {
+            return processor.GetImageCacheTag(item, imageType, 0);
+        }
+        
+        public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex)
+        {
+            var imageInfo = item.GetImageInfo(imageType, imageIndex);
+
+            if (imageInfo == null)
+            {
+                return null;
+            }
+
+            return processor.GetImageCacheTag(item, imageInfo);
+        }
+    }
+}

+ 28 - 0
MediaBrowser.Controller/Drawing/ImageStream.cs

@@ -0,0 +1,28 @@
+using MediaBrowser.Model.Drawing;
+using System;
+using System.IO;
+
+namespace MediaBrowser.Controller.Drawing
+{
+    public class ImageStream : IDisposable
+    {
+        /// <summary>
+        /// Gets or sets the stream.
+        /// </summary>
+        /// <value>The stream.</value>
+        public Stream Stream { get; set; }
+        /// <summary>
+        /// Gets or sets the format.
+        /// </summary>
+        /// <value>The format.</value>
+        public ImageFormat Format { get; set; }
+
+        public void Dispose()
+        {
+            if (Stream != null)
+            {
+                Stream.Dispose();
+            }
+        }
+    }
+}

+ 72 - 0
MediaBrowser.Controller/Dto/DtoOptions.cs

@@ -0,0 +1,72 @@
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Dto
+{
+    public class DtoOptions
+    {
+        private static readonly ItemFields[] DefaultExcludedFields = new []
+        {
+            ItemFields.SeasonUserData,
+            ItemFields.RefreshState
+        };
+
+        public ItemFields[] Fields { get; set; }
+        public ImageType[] ImageTypes { get; set; }
+        public int ImageTypeLimit { get; set; }
+        public bool EnableImages { get; set; }
+        public bool AddProgramRecordingInfo { get; set; }
+        public bool EnableUserData { get; set; }
+        public bool AddCurrentProgram { get; set; }
+
+        public DtoOptions()
+            : this(true)
+        {
+        }
+
+        private static readonly ImageType[] AllImageTypes = Enum.GetNames(typeof(ImageType))
+            .Select(i => (ImageType)Enum.Parse(typeof(ImageType), i, true))
+            .ToArray();
+
+        private static readonly ItemFields[] AllItemFields = Enum.GetNames(typeof(ItemFields))
+            .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
+            .Except(DefaultExcludedFields)
+            .ToArray();
+
+        public bool ContainsField(ItemFields field)
+        {
+            return AllItemFields.Contains(field);
+        }
+
+        public DtoOptions(bool allFields)
+        {
+            ImageTypeLimit = int.MaxValue;
+            EnableImages = true;
+            EnableUserData = true;
+            AddCurrentProgram = true;
+
+            if (allFields)
+            {
+                Fields = AllItemFields;
+            }
+            else
+            {
+                Fields = new ItemFields[] { };
+            }
+
+            ImageTypes = AllImageTypes;
+        }
+
+        public int GetImageLimit(ImageType type)
+        {
+            if (EnableImages && ImageTypes.Contains(type))
+            {
+                return ImageTypeLimit;
+            }
+
+            return 0;
+        }
+    }
+}

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

@@ -0,0 +1,70 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Sync;
+
+namespace MediaBrowser.Controller.Dto
+{
+    /// <summary>
+    /// Interface IDtoService
+    /// </summary>
+    public interface IDtoService
+    {
+        /// <summary>
+        /// Gets the dto id.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>System.String.</returns>
+        string GetDtoId(BaseItem item);
+
+        /// <summary>
+        /// Attaches the primary image aspect ratio.
+        /// </summary>
+        /// <param name="dto">The dto.</param>
+        /// <param name="item">The item.</param>
+        void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item);
+
+        /// <summary>
+        /// Gets the primary image aspect ratio.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>System.Nullable&lt;System.Double&gt;.</returns>
+        double? GetPrimaryImageAspectRatio(BaseItem item);
+
+        /// <summary>
+        /// Gets the base item dto.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="fields">The fields.</param>
+        /// <param name="user">The user.</param>
+        /// <param name="owner">The owner.</param>
+        BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null);
+
+        /// <summary>
+        /// Gets the base item dto.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="options">The options.</param>
+        /// <param name="user">The user.</param>
+        /// <param name="owner">The owner.</param>
+        /// <returns>BaseItemDto.</returns>
+        BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null);
+
+        /// <summary>
+        /// Gets the base item dtos.
+        /// </summary>
+        /// <param name="items">The items.</param>
+        /// <param name="options">The options.</param>
+        /// <param name="user">The user.</param>
+        /// <param name="owner">The owner.</param>
+        BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null);
+
+        BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
+
+        /// <summary>
+        /// Gets the item by name dto.
+        /// </summary>
+        BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null);
+    }
+}

+ 219 - 0
MediaBrowser.Controller/Entities/AggregateFolder.cs

@@ -0,0 +1,219 @@
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// Specialized folder that can have items added to it's children by external entities.
+    /// Used for our RootFolder so plug-ins can add items.
+    /// </summary>
+    public class AggregateFolder : Folder
+    {
+        public AggregateFolder()
+        {
+            PhysicalLocationsList = new string[] { };
+        }
+
+        [IgnoreDataMember]
+        public override bool IsPhysicalRoot
+        {
+            get { return true; }
+        }
+
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPlayedStatus
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// The _virtual children
+        /// </summary>
+        private readonly ConcurrentBag<BaseItem> _virtualChildren = new ConcurrentBag<BaseItem>();
+
+        /// <summary>
+        /// Gets the virtual children.
+        /// </summary>
+        /// <value>The virtual children.</value>
+        public ConcurrentBag<BaseItem> VirtualChildren
+        {
+            get { return _virtualChildren; }
+        }
+
+        [IgnoreDataMember]
+        public override string[] PhysicalLocations
+        {
+            get
+            {
+                return PhysicalLocationsList;
+            }
+        }
+
+        public string[] PhysicalLocationsList { get; set; }
+
+        protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
+        {
+            return CreateResolveArgs(directoryService, true).FileSystemChildren;
+        }
+
+        private Guid[] _childrenIds = null;
+        private readonly object _childIdsLock = new object();
+        protected override List<BaseItem> LoadChildren()
+        {
+            lock (_childIdsLock)
+            {
+                if (_childrenIds == null || _childrenIds.Length == 0)
+                {
+                    var list = base.LoadChildren();
+                    _childrenIds = list.Select(i => i.Id).ToArray();
+                    return list;
+                }
+
+                return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList();
+            }
+        }
+
+        private void ClearCache()
+        {
+            lock (_childIdsLock)
+            {
+                _childrenIds = null;
+            }
+        }
+
+        private bool _requiresRefresh;
+        public override bool RequiresRefresh()
+        {
+            var changed = base.RequiresRefresh() || _requiresRefresh;
+
+            if (!changed)
+            {
+                var locations = PhysicalLocations;
+
+                var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations;
+
+                if (!locations.SequenceEqual(newLocations))
+                {
+                    changed = true;
+                }
+            }
+
+            return changed;
+        }
+
+        public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+        {
+            ClearCache();
+
+            var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh;
+            _requiresRefresh = false;
+            return changed;
+        }
+
+        private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService, bool setPhysicalLocations)
+        {
+            ClearCache();
+
+            var path = ContainingFolderPath;
+
+            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
+            {
+                FileInfo = FileSystem.GetDirectoryInfo(path),
+                Path = path
+            };
+
+            // Gather child folder and files
+            if (args.IsDirectory)
+            {
+                // When resolving the root, we need it's grandchildren (children of user views)
+                var flattenFolderDepth = 2;
+
+                var files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, FileSystem, CollectionFolder.ApplicationHost, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: true);
+
+                // Need to remove subpaths that may have been resolved from shortcuts
+                // Example: if \\server\movies exists, then strip out \\server\movies\action
+                files = LibraryManager.NormalizeRootPathList(files).ToArray();
+
+                args.FileSystemChildren = files;
+            }
+
+            _requiresRefresh = _requiresRefresh || !args.PhysicalLocations.SequenceEqual(PhysicalLocations);
+            if (setPhysicalLocations)
+            {
+                PhysicalLocationsList = args.PhysicalLocations;
+            }
+
+            return args;
+        }
+
+        protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
+        {
+            return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren);
+        }
+
+        protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+        {
+            ClearCache();
+
+            await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService)
+                .ConfigureAwait(false);
+
+            ClearCache();
+        }
+
+        /// <summary>
+        /// Adds the virtual child.
+        /// </summary>
+        /// <param name="child">The child.</param>
+        /// <exception cref="System.ArgumentNullException"></exception>
+        public void AddVirtualChild(BaseItem child)
+        {
+            if (child == null)
+            {
+                throw new ArgumentNullException();
+            }
+
+            _virtualChildren.Add(child);
+        }
+
+        /// <summary>
+        /// Finds the virtual child.
+        /// </summary>
+        /// <param name="id">The id.</param>
+        /// <returns>BaseItem.</returns>
+        /// <exception cref="System.ArgumentNullException">id</exception>
+        public BaseItem FindVirtualChild(Guid id)
+        {
+            if (id.Equals(Guid.Empty))
+            {
+                throw new ArgumentNullException("id");
+            }
+
+            foreach (var child in _virtualChildren)
+            {
+                if (child.Id == id)
+                {
+                    return child;
+                }
+            }
+            return null;
+        }
+    }
+}

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

@@ -0,0 +1,216 @@
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+    /// <summary>
+    /// Class Audio
+    /// </summary>
+    public class Audio : BaseItem,
+        IHasAlbumArtist,
+        IHasArtist,
+        IHasMusicGenres,
+        IHasLookupInfo<SongInfo>,
+        IHasMediaSources
+    {
+        /// <summary>
+        /// Gets or sets the artist.
+        /// </summary>
+        /// <value>The artist.</value>
+        [IgnoreDataMember]
+        public string[] Artists { get; set; }
+
+        [IgnoreDataMember]
+        public string[] AlbumArtists { get; set; }
+
+        public Audio()
+        {
+            Artists = new string[] {};
+            AlbumArtists = new string[] {};
+        }
+
+        public override double GetDefaultPrimaryImageAspectRatio()
+        {
+            return 1;
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPlayedStatus
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPeople
+        {
+            get { return false; }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsAddingToPlaylist
+        {
+            get { return true; }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsInheritedParentImages
+        {
+            get { return true; }
+        }
+
+        [IgnoreDataMember]
+        protected override bool SupportsOwnedItems
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override Folder LatestItemsIndexContainer
+        {
+            get
+            {
+                return AlbumEntity;
+            }
+        }
+
+        public override bool CanDownload()
+        {
+            return IsFileProtocol;
+        }
+
+        [IgnoreDataMember]
+        public string[] AllArtists
+        {
+            get
+            {
+                var list = new string[AlbumArtists.Length + Artists.Length];
+
+                var index = 0;
+                foreach (var artist in AlbumArtists)
+                {
+                    list[index] = artist;
+                    index++;
+                }
+                foreach (var artist in Artists)
+                {
+                    list[index] = artist;
+                    index++;
+                }
+
+                return list;
+
+            }
+        }
+
+        [IgnoreDataMember]
+        public MusicAlbum AlbumEntity
+        {
+            get { return FindParent<MusicAlbum>(); }
+        }
+
+        /// <summary>
+        /// Gets the type of the media.
+        /// </summary>
+        /// <value>The type of the media.</value>
+        [IgnoreDataMember]
+        public override string MediaType
+        {
+            get
+            {
+                return Model.Entities.MediaType.Audio;
+            }
+        }
+
+        /// <summary>
+        /// Creates the name of the sort.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        protected override string CreateSortName()
+        {
+            return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ") : "")
+                    + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name;
+        }
+
+        public override List<string> GetUserDataKeys()
+        {
+            var list = base.GetUserDataKeys();
+
+            var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000") : string.Empty;
+
+
+            if (ParentIndexNumber.HasValue)
+            {
+                songKey = ParentIndexNumber.Value.ToString("0000") + "-" + songKey;
+            }
+            songKey += Name;
+
+            if (!string.IsNullOrEmpty(Album))
+            {
+                songKey = Album + "-" + songKey;
+            }
+
+            var albumArtist = AlbumArtists.Length == 0 ? null : AlbumArtists[0];
+            if (!string.IsNullOrEmpty(albumArtist))
+            {
+                songKey = albumArtist + "-" + songKey;
+            }
+
+            list.Insert(0, songKey);
+
+            return list;
+        }
+
+        public override UnratedItem GetBlockUnratedType()
+        {
+            if (SourceType == SourceType.Library)
+            {
+                return UnratedItem.Music;
+            }
+            return base.GetBlockUnratedType();
+        }
+
+        public List<MediaStream> GetMediaStreams(MediaStreamType type)
+        {
+            return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
+            {
+                ItemId = Id,
+                Type = type
+            });
+        }
+
+        public SongInfo GetLookupInfo()
+        {
+            var info = GetItemLookupInfo<SongInfo>();
+
+            info.AlbumArtists = AlbumArtists;
+            info.Album = Album;
+            info.Artists = Artists;
+
+            return info;
+        }
+
+        protected override List<Tuple<BaseItem, MediaSourceType>> GetAllItemsForMediaSources()
+        {
+            var list = new List<Tuple<BaseItem, MediaSourceType>>();
+            list.Add(new Tuple<BaseItem, MediaSourceType>(this, MediaSourceType.Default));
+            return list;
+        }
+    }
+}

+ 15 - 0
MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs

@@ -0,0 +1,15 @@
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+    public interface IHasAlbumArtist
+    {
+        string[] AlbumArtists { get; set; }
+    }
+
+    public interface IHasArtist
+    {
+        string[] AllArtists { get; }
+
+        string[] Artists { get; set; }
+    }
+}

+ 9 - 0
MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs

@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+    public interface IHasMusicGenres
+    {
+        string[] Genres { get; }
+    }
+}

+ 272 - 0
MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs

@@ -0,0 +1,272 @@
+using System;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Users;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Library;
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+    /// <summary>
+    /// Class MusicAlbum
+    /// </summary>
+    public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer
+    {
+        public string[] AlbumArtists { get; set; }
+        public string[] Artists { get; set; }
+
+        public MusicAlbum()
+        {
+            Artists = new string[] {};
+            AlbumArtists = new string[] {};
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsAddingToPlaylist
+        {
+            get { return true; }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsInheritedParentImages
+        {
+            get { return true; }
+        }
+
+        [IgnoreDataMember]
+        public MusicArtist MusicArtist
+        {
+            get { return GetMusicArtist(new DtoOptions(true)); }
+        }
+
+        public MusicArtist GetMusicArtist(DtoOptions options)
+        {
+            var parents = GetParents();
+            foreach (var parent in parents)
+            {
+                var artist = parent as MusicArtist;
+                if (artist != null)
+                {
+                    return artist;
+                }
+            }
+
+            var name = AlbumArtist;
+            if (!string.IsNullOrEmpty(name))
+            {
+                return LibraryManager.GetArtist(name, options);
+            }
+            return null;
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPlayedStatus
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsCumulativeRunTimeTicks
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        [IgnoreDataMember]
+        public string[] AllArtists
+        {
+            get
+            {
+                var list = new string[AlbumArtists.Length + Artists.Length];
+
+                var index = 0;
+                foreach (var artist in AlbumArtists)
+                {
+                    list[index] = artist;
+                    index++;
+                }
+                foreach (var artist in Artists)
+                {
+                    list[index] = artist;
+                    index++;
+                }
+
+                return list;
+            }
+        }
+
+        [IgnoreDataMember]
+        public string AlbumArtist
+        {
+            get { return AlbumArtists.Length == 0 ? null : AlbumArtists[0]; }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPeople
+        {
+            get { return false; }
+        }
+
+        /// <summary>
+        /// Gets the tracks.
+        /// </summary>
+        /// <value>The tracks.</value>
+        [IgnoreDataMember]
+        public IEnumerable<BaseItem> Tracks
+        {
+            get
+            {
+                return GetRecursiveChildren(i => i is Audio);
+            }
+        }
+
+        protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
+        {
+            return Tracks;
+        }
+
+        public override double GetDefaultPrimaryImageAspectRatio()
+        {
+            return 1;
+        }
+
+        public override List<string> GetUserDataKeys()
+        {
+            var list = base.GetUserDataKeys();
+
+            var albumArtist = AlbumArtist;
+            if (!string.IsNullOrEmpty(albumArtist))
+            {
+                list.Insert(0, albumArtist + "-" + Name);
+            }
+
+            var id = this.GetProviderId(MetadataProviders.MusicBrainzAlbum);
+
+            if (!string.IsNullOrEmpty(id))
+            {
+                list.Insert(0, "MusicAlbum-Musicbrainz-" + id);
+            }
+
+            id = this.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup);
+
+            if (!string.IsNullOrEmpty(id))
+            {
+                list.Insert(0, "MusicAlbum-MusicBrainzReleaseGroup-" + id);
+            }
+
+            return list;
+        }
+
+        protected override bool GetBlockUnratedValue(UserPolicy config)
+        {
+            return config.BlockUnratedItems.Contains(UnratedItem.Music);
+        }
+
+        public override UnratedItem GetBlockUnratedType()
+        {
+            return UnratedItem.Music;
+        }
+
+        public AlbumInfo GetLookupInfo()
+        {
+            var id = GetItemLookupInfo<AlbumInfo>();
+
+            id.AlbumArtists = AlbumArtists;
+
+            var artist = GetMusicArtist(new DtoOptions(false));
+
+            if (artist != null)
+            {
+                id.ArtistProviderIds = artist.ProviderIds;
+            }
+
+            id.SongInfos = GetRecursiveChildren(i => i is Audio)
+                .Cast<Audio>()
+                .Select(i => i.GetLookupInfo())
+                .ToList();
+
+            var album = id.SongInfos
+                .Select(i => i.Album)
+                .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+
+            if (!string.IsNullOrEmpty(album))
+            {
+                id.Name = album;
+            }
+
+            return id;
+        }
+
+        public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var items = GetRecursiveChildren();
+
+            var totalItems = items.Count;
+            var numComplete = 0;
+
+            var childUpdateType = ItemUpdateType.None;
+
+            // Refresh songs
+            foreach (var item in items)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+
+                var updateType = await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+                childUpdateType = childUpdateType | updateType;
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= totalItems;
+                progress.Report(percent * 95);
+            }
+
+            var parentRefreshOptions = refreshOptions;
+            if (childUpdateType > ItemUpdateType.None)
+            {
+                parentRefreshOptions = new MetadataRefreshOptions(refreshOptions);
+                parentRefreshOptions.MetadataRefreshMode = MetadataRefreshMode.FullRefresh;
+            }
+
+            // Refresh current item
+            await RefreshMetadata(parentRefreshOptions, cancellationToken).ConfigureAwait(false);
+
+            if (!refreshOptions.IsAutomated)
+            {
+                await RefreshArtists(refreshOptions, cancellationToken).ConfigureAwait(false);
+            }
+        }
+
+        private async Task RefreshArtists(MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
+        {
+            var all = AllArtists;
+            foreach (var i in all)
+            {
+                // This should not be necessary but we're seeing some cases of it
+                if (string.IsNullOrEmpty(i))
+                {
+                    continue;
+                }
+
+                var artist = LibraryManager.GetArtist(i);
+
+                if (!artist.IsAccessedByName)
+                {
+                    continue;
+                }
+
+                await artist.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+            }
+        }
+    }
+}

+ 275 - 0
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -0,0 +1,275 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Users;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+    /// <summary>
+    /// Class MusicArtist
+    /// </summary>
+    public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo>
+    {
+        [IgnoreDataMember]
+        public bool IsAccessedByName
+        {
+            get { return ParentId.Equals(Guid.Empty); }
+        }
+
+        [IgnoreDataMember]
+        public override bool IsFolder
+        {
+            get
+            {
+                return !IsAccessedByName;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsInheritedParentImages
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsCumulativeRunTimeTicks
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool IsDisplayedAsFolder
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsAddingToPlaylist
+        {
+            get { return true; }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPlayedStatus
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public override double GetDefaultPrimaryImageAspectRatio()
+        {
+            return 1;
+        }
+
+        public override bool CanDelete()
+        {
+            return !IsAccessedByName;
+        }
+
+        public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+        {
+            if (query.IncludeItemTypes.Length == 0)
+            {
+                query.IncludeItemTypes = new[] { typeof(Audio).Name, typeof(MusicVideo).Name, typeof(MusicAlbum).Name };
+                query.ArtistIds = new[] { Id };
+            }
+
+            return LibraryManager.GetItemList(query);
+        }
+
+        [IgnoreDataMember]
+        public override IEnumerable<BaseItem> Children
+        {
+            get
+            {
+                if (IsAccessedByName)
+                {
+                    return new List<BaseItem>();
+                }
+
+                return base.Children;
+            }
+        }
+
+        public override int GetChildCount(User user)
+        {
+            if (IsAccessedByName)
+            {
+                return 0;
+            }
+            return base.GetChildCount(user);
+        }
+
+        public override bool IsSaveLocalMetadataEnabled()
+        {
+            if (IsAccessedByName)
+            {
+                return true;
+            }
+
+            return base.IsSaveLocalMetadataEnabled();
+        }
+
+        private readonly Task _cachedTask = Task.FromResult(true);
+        protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+        {
+            if (IsAccessedByName)
+            {
+                // Should never get in here anyway
+                return _cachedTask;
+            }
+
+            return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService);
+        }
+
+        public override List<string> GetUserDataKeys()
+        {
+            var list = base.GetUserDataKeys();
+
+            list.InsertRange(0, GetUserDataKeys(this));
+            return list;
+        }
+
+        /// <summary>
+        /// Returns the folder containing the item.
+        /// If the item is a folder, it returns the folder itself
+        /// </summary>
+        /// <value>The containing folder path.</value>
+        [IgnoreDataMember]
+        public override string ContainingFolderPath
+        {
+            get
+            {
+                return Path;
+            }
+        }
+
+        /// <summary>
+        /// Gets the user data key.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>System.String.</returns>
+        private static List<string> GetUserDataKeys(MusicArtist item)
+        {
+            var list = new List<string>();
+            var id = item.GetProviderId(MetadataProviders.MusicBrainzArtist);
+
+            if (!string.IsNullOrEmpty(id))
+            {
+                list.Add("Artist-Musicbrainz-" + id);
+            }
+
+            list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics());
+            return list;
+        }
+        public override string CreatePresentationUniqueKey()
+        {
+            return "Artist-" + (Name ?? string.Empty).RemoveDiacritics();
+        }
+        protected override bool GetBlockUnratedValue(UserPolicy config)
+        {
+            return config.BlockUnratedItems.Contains(UnratedItem.Music);
+        }
+
+        public override UnratedItem GetBlockUnratedType()
+        {
+            return UnratedItem.Music;
+        }
+
+        public ArtistInfo GetLookupInfo()
+        {
+            var info = GetItemLookupInfo<ArtistInfo>();
+
+            info.SongInfos = GetRecursiveChildren(i => i is Audio)
+                .Cast<Audio>()
+                .Select(i => i.GetLookupInfo())
+                .ToList();
+
+            return info;
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPeople
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public static string GetPath(string name)
+        {
+            return GetPath(name, true);
+        }
+
+        public static string GetPath(string name, bool normalizeName)
+        {
+            // Trim the period at the end because windows will have a hard time with that
+            var validName = normalizeName ?
+                FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+                name;
+
+            return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.ArtistsPath, validName);
+        }
+
+        private string GetRebasedPath()
+        {
+            return GetPath(System.IO.Path.GetFileName(Path), false);
+        }
+
+        public override bool RequiresRefresh()
+        {
+            if (IsAccessedByName)
+            {
+                var newPath = GetRebasedPath();
+                if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+                {
+                    Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+                    return true;
+                }
+            }
+            return base.RequiresRefresh();
+        }
+
+        /// <summary>
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
+        /// </summary>
+        public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+        {
+            var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+            if (IsAccessedByName)
+            {
+                var newPath = GetRebasedPath();
+                if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+                {
+                    Path = newPath;
+                    hasChanges = true;
+                }
+            }
+
+            return hasChanges;
+        }
+    }
+}

+ 145 - 0
MediaBrowser.Controller/Entities/Audio/MusicGenre.cs

@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+    /// <summary>
+    /// Class MusicGenre
+    /// </summary>
+    public class MusicGenre : BaseItem, IItemByName
+    {
+        public override List<string> GetUserDataKeys()
+        {
+            var list = base.GetUserDataKeys();
+
+            list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
+            return list;
+        }
+        public override string CreatePresentationUniqueKey()
+        {
+            return GetUserDataKeys()[0];
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsAddingToPlaylist
+        {
+            get { return true; }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsAncestors
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool IsDisplayedAsFolder
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        /// <summary>
+        /// Returns the folder containing the item.
+        /// If the item is a folder, it returns the folder itself
+        /// </summary>
+        /// <value>The containing folder path.</value>
+        [IgnoreDataMember]
+        public override string ContainingFolderPath
+        {
+            get
+            {
+                return Path;
+            }
+        }
+
+        public override double GetDefaultPrimaryImageAspectRatio()
+        {
+            return 1;
+        }
+
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
+        public override bool IsSaveLocalMetadataEnabled()
+        {
+            return true;
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPeople
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+        {
+            query.GenreIds = new[] { Id };
+            query.IncludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name };
+
+            return LibraryManager.GetItemList(query);
+        }
+
+        public static string GetPath(string name)
+        {
+            return GetPath(name, true);
+        }
+
+        public static string GetPath(string name, bool normalizeName)
+        {
+            // Trim the period at the end because windows will have a hard time with that
+            var validName = normalizeName ?
+                FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+                name;
+
+            return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.MusicGenrePath, validName);
+        }
+
+        private string GetRebasedPath()
+        {
+            return GetPath(System.IO.Path.GetFileName(Path), false);
+        }
+
+        public override bool RequiresRefresh()
+        {
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+                return true;
+            }
+            return base.RequiresRefresh();
+        }
+
+        /// <summary>
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
+        /// </summary>
+        public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+        {
+            var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Path = newPath;
+                hasChanges = true;
+            }
+
+            return hasChanges;
+        }
+    }
+}

+ 69 - 0
MediaBrowser.Controller/Entities/AudioBook.cs

@@ -0,0 +1,69 @@
+using System;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public class AudioBook : Audio.Audio, IHasSeries, IHasLookupInfo<SongInfo>
+    {
+        [IgnoreDataMember]
+        public override bool SupportsPositionTicksResume
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPlayedStatus
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        [IgnoreDataMember]
+        public string SeriesPresentationUniqueKey { get; set; }
+        [IgnoreDataMember]
+        public string SeriesName { get; set; }
+        [IgnoreDataMember]
+        public Guid SeriesId { get; set; }
+
+        public string FindSeriesSortName()
+        {
+            return SeriesName;
+        }
+        public string FindSeriesName()
+        {
+            return SeriesName;
+        }
+        public string FindSeriesPresentationUniqueKey()
+        {
+            return SeriesPresentationUniqueKey;
+        }
+
+        public override double GetDefaultPrimaryImageAspectRatio()
+        {
+            return 0;
+        }
+
+        public Guid FindSeriesId()
+        {
+            return SeriesId;
+        }
+
+        public override bool CanDownload()
+        {
+            return IsFileProtocol;
+        }
+
+        public override UnratedItem GetBlockUnratedType()
+        {
+            return UnratedItem.Book;
+        }
+    }
+}

+ 2960 - 0
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -0,0 +1,2960 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Collections;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Library;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Users;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.MediaInfo;
+
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// Class BaseItem
+    /// </summary>
+    public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>
+    {
+        protected static MetadataFields[] EmptyMetadataFieldsArray = new MetadataFields[] { };
+        protected static MediaUrl[] EmptyMediaUrlArray = new MediaUrl[] { };
+        protected static ItemImageInfo[] EmptyItemImageInfoArray = new ItemImageInfo[] { };
+        public static readonly LinkedChild[] EmptyLinkedChildArray = new LinkedChild[] { };
+
+        protected BaseItem()
+        {
+            ThemeSongIds = new Guid[] {};
+            ThemeVideoIds = new Guid[] {};
+            Tags = new string[] {};
+            Genres = new string[] {};
+            Studios = new string[] {};
+            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            LockedFields = EmptyMetadataFieldsArray;
+            ImageInfos = EmptyItemImageInfoArray;
+            ProductionLocations = new string[] {};
+            RemoteTrailers = new MediaUrl[] { };
+            ExtraIds = new Guid[] {};
+        }
+
+        public static readonly char[] SlugReplaceChars = { '?', '/', '&' };
+        public static char SlugChar = '-';
+
+        /// <summary>
+        /// The supported image extensions
+        /// </summary>
+        public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".tbn", ".gif" };
+        public static readonly List<string> SupportedImageExtensionsList = SupportedImageExtensions.ToList();
+
+        /// <summary>
+        /// The trailer folder name
+        /// </summary>
+        public static string TrailerFolderName = "trailers";
+        public static string ThemeSongsFolderName = "theme-music";
+        public static string ThemeSongFilename = "theme";
+        public static string ThemeVideosFolderName = "backdrops";
+
+        [IgnoreDataMember]
+        public Guid[] ThemeSongIds { get; set; }
+        [IgnoreDataMember]
+        public Guid[] ThemeVideoIds { get; set; }
+
+        [IgnoreDataMember]
+        public string PreferredMetadataCountryCode { get; set; }
+        [IgnoreDataMember]
+        public string PreferredMetadataLanguage { get; set; }
+
+        public long? Size { get; set; }
+        public string Container { get; set; }
+
+        [IgnoreDataMember]
+        public string Tagline { get; set; }
+
+        [IgnoreDataMember]
+        public virtual ItemImageInfo[] ImageInfos { get; set; }
+
+        [IgnoreDataMember]
+        public bool IsVirtualItem { get; set; }
+
+        /// <summary>
+        /// Gets or sets the album.
+        /// </summary>
+        /// <value>The album.</value>
+        [IgnoreDataMember]
+        public string Album { get; set; }
+
+        /// <summary>
+        /// Gets or sets the channel identifier.
+        /// </summary>
+        /// <value>The channel identifier.</value>
+        [IgnoreDataMember]
+        public Guid ChannelId { get; set; }
+
+        [IgnoreDataMember]
+        public virtual bool SupportsAddingToPlaylist
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        public virtual bool AlwaysScanInternalMetadataPath
+        {
+            get { return false; }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether this instance is in mixed folder.
+        /// </summary>
+        /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value>
+        [IgnoreDataMember]
+        public bool IsInMixedFolder { get; set; }
+
+        [IgnoreDataMember]
+        public virtual bool SupportsPlayedStatus
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        public virtual bool SupportsPositionTicksResume
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        public virtual bool SupportsRemoteImageDownloading
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        private string _name;
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        [IgnoreDataMember]
+        public virtual string Name
+        {
+            get
+            {
+                return _name;
+            }
+            set
+            {
+                _name = value;
+
+                // lazy load this again
+                _sortName = null;
+            }
+        }
+
+        [IgnoreDataMember]
+        public bool IsUnaired
+        {
+            get { return PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date; }
+        }
+
+        [IgnoreDataMember]
+        public int? TotalBitrate { get; set; }
+        [IgnoreDataMember]
+        public ExtraType? ExtraType { get; set; }
+
+        [IgnoreDataMember]
+        public bool IsThemeMedia
+        {
+            get
+            {
+                return ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || ExtraType.Value == Model.Entities.ExtraType.ThemeVideo);
+            }
+        }
+
+        [IgnoreDataMember]
+        public string OriginalTitle { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [IgnoreDataMember]
+        public Guid Id { get; set; }
+
+        [IgnoreDataMember]
+        public Guid OwnerId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the audio.
+        /// </summary>
+        /// <value>The audio.</value>
+        [IgnoreDataMember]
+        public ProgramAudio? Audio { get; set; }
+
+        /// <summary>
+        /// Return the id that should be used to key display prefs for this item.
+        /// Default is based on the type for everything except actual generic folders.
+        /// </summary>
+        /// <value>The display prefs id.</value>
+        [IgnoreDataMember]
+        public virtual Guid DisplayPreferencesId
+        {
+            get
+            {
+                var thisType = GetType();
+                return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5();
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        /// <value>The path.</value>
+        [IgnoreDataMember]
+        public virtual string Path { get; set; }
+
+        [IgnoreDataMember]
+        public virtual SourceType SourceType
+        {
+            get
+            {
+                if (!ChannelId.Equals(Guid.Empty))
+                {
+                    return SourceType.Channel;
+                }
+
+                return SourceType.Library;
+            }
+        }
+
+        /// <summary>
+        /// Returns the folder containing the item.
+        /// If the item is a folder, it returns the folder itself
+        /// </summary>
+        [IgnoreDataMember]
+        public virtual string ContainingFolderPath
+        {
+            get
+            {
+                if (IsFolder)
+                {
+                    return Path;
+                }
+
+                return FileSystem.GetDirectoryName(Path);
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the name of the service.
+        /// </summary>
+        /// <value>The name of the service.</value>
+        [IgnoreDataMember]
+        public string ServiceName { get; set; }
+
+        /// <summary>
+        /// If this content came from an external service, the id of the content on that service
+        /// </summary>
+        [IgnoreDataMember]
+        public string ExternalId { get; set; }
+
+        [IgnoreDataMember]
+        public string ExternalSeriesId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the etag.
+        /// </summary>
+        /// <value>The etag.</value>
+        [IgnoreDataMember]
+        public string ExternalEtag { get; set; }
+
+        [IgnoreDataMember]
+        public virtual bool IsHidden
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public BaseItem GetOwner()
+        {
+            var ownerId = OwnerId;
+            return ownerId.Equals(Guid.Empty) ? null : LibraryManager.GetItemById(ownerId);
+        }
+
+        /// <summary>
+        /// Gets or sets the type of the location.
+        /// </summary>
+        /// <value>The type of the location.</value>
+        [IgnoreDataMember]
+        public virtual LocationType LocationType
+        {
+            get
+            {
+                //if (IsOffline)
+                //{
+                //    return LocationType.Offline;
+                //}
+
+                var path = Path;
+                if (string.IsNullOrEmpty(path))
+                {
+                    if (SourceType == SourceType.Channel)
+                    {
+                        return LocationType.Remote;
+                    }
+
+                    return LocationType.Virtual;
+                }
+
+                return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote;
+            }
+        }
+
+        [IgnoreDataMember]
+        public MediaProtocol? PathProtocol
+        {
+            get
+            {
+                var path = Path;
+
+                if (string.IsNullOrEmpty(path))
+                {
+                    return null;
+                }
+
+                return MediaSourceManager.GetPathProtocol(path);
+            }
+        }
+
+        public bool IsPathProtocol(MediaProtocol protocol)
+        {
+            var current = PathProtocol;
+
+            return current.HasValue && current.Value == protocol;
+        }
+
+        [IgnoreDataMember]
+        public bool IsFileProtocol
+        {
+            get
+            {
+                return IsPathProtocol(MediaProtocol.File);
+            }
+        }
+
+        [IgnoreDataMember]
+        public bool HasPathProtocol
+        {
+            get
+            {
+                return PathProtocol.HasValue;
+            }
+        }
+
+        [IgnoreDataMember]
+        public virtual bool SupportsLocalMetadata
+        {
+            get
+            {
+                if (SourceType == SourceType.Channel)
+                {
+                    return false;
+                }
+
+                return IsFileProtocol;
+            }
+        }
+
+        [IgnoreDataMember]
+        public virtual string FileNameWithoutExtension
+        {
+            get
+            {
+                if (IsFileProtocol)
+                {
+                    return System.IO.Path.GetFileNameWithoutExtension(Path);
+                }
+
+                return null;
+            }
+        }
+
+        [IgnoreDataMember]
+        public virtual bool EnableAlphaNumericSorting
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1)
+        {
+            var list = new List<Tuple<StringBuilder, bool>>();
+
+            int thisMarker = 0, thisNumericChunk = 0;
+
+            while (thisMarker < s1.Length)
+            {
+                if (thisMarker >= s1.Length)
+                {
+                    break;
+                }
+                char thisCh = s1[thisMarker];
+
+                StringBuilder thisChunk = new StringBuilder();
+
+                while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || SortHelper.InChunk(thisCh, thisChunk[0])))
+                {
+                    thisChunk.Append(thisCh);
+                    thisMarker++;
+
+                    if (thisMarker < s1.Length)
+                    {
+                        thisCh = s1[thisMarker];
+                    }
+                }
+
+                var isNumeric = thisChunk.Length > 0 && char.IsDigit(thisChunk[0]);
+                list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric));
+            }
+
+            return list;
+        }
+
+        /// <summary>
+        /// This is just a helper for convenience
+        /// </summary>
+        /// <value>The primary image path.</value>
+        [IgnoreDataMember]
+        public string PrimaryImagePath
+        {
+            get { return this.GetImagePath(ImageType.Primary); }
+        }
+
+        public bool IsMetadataFetcherEnabled(LibraryOptions libraryOptions, string name)
+        {
+            if (SourceType == SourceType.Channel)
+            {
+                // hack alert
+                return !EnableMediaSourceDisplay;
+            }
+
+            var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
+            if (typeOptions != null)
+            {
+                return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+            }
+
+            if (!libraryOptions.EnableInternetProviders)
+            {
+                return false;
+            }
+
+            var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
+
+            return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+        }
+
+        public bool IsImageFetcherEnabled(LibraryOptions libraryOptions, string name)
+        {
+            if (this is Channel)
+            {
+                // hack alert
+                return true;
+            }
+            if (SourceType == SourceType.Channel)
+            {
+                // hack alert
+                return !EnableMediaSourceDisplay;
+            }
+
+            var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
+            if (typeOptions != null)
+            {
+                return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+            }
+
+            if (!libraryOptions.EnableInternetProviders)
+            {
+                return false;
+            }
+
+            var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
+
+            return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+        }
+
+        public virtual bool CanDelete()
+        {
+            if (SourceType == SourceType.Channel)
+            {
+                return ChannelManager.CanDelete(this);
+            }
+
+            return IsFileProtocol;
+        }
+
+        public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
+        {
+            if (user.Policy.EnableContentDeletion)
+            {
+                return true;
+            }
+
+            var allowed = user.Policy.EnableContentDeletionFromFolders;
+
+            if (SourceType == SourceType.Channel)
+            {
+                return allowed.Contains(ChannelId.ToString(""), StringComparer.OrdinalIgnoreCase);
+            }
+            else
+            {
+                var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
+
+                foreach (var folder in collectionFolders)
+                {
+                    if (allowed.Contains(folder.Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        public bool CanDelete(User user, List<Folder> allCollectionFolders)
+        {
+            return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
+        }
+
+        public bool CanDelete(User user)
+        {
+            var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
+
+            return CanDelete(user, allCollectionFolders);
+        }
+
+        public virtual bool CanDownload()
+        {
+            return false;
+        }
+
+        public virtual bool IsAuthorizedToDownload(User user)
+        {
+            return user.Policy.EnableContentDownloading;
+        }
+
+        public bool CanDownload(User user)
+        {
+            return CanDownload() && IsAuthorizedToDownload(user);
+        }
+
+        /// <summary>
+        /// Gets or sets the date created.
+        /// </summary>
+        /// <value>The date created.</value>
+        [IgnoreDataMember]
+        public DateTime DateCreated { get; set; }
+
+        /// <summary>
+        /// Gets or sets the date modified.
+        /// </summary>
+        /// <value>The date modified.</value>
+        [IgnoreDataMember]
+        public DateTime DateModified { get; set; }
+
+        [IgnoreDataMember]
+        public DateTime DateLastSaved { get; set; }
+
+        [IgnoreDataMember]
+        public DateTime DateLastRefreshed { get; set; }
+
+        /// <summary>
+        /// The logger
+        /// </summary>
+        public static ILogger Logger { get; set; }
+        public static ILibraryManager LibraryManager { get; set; }
+        public static IServerConfigurationManager ConfigurationManager { get; set; }
+        public static IProviderManager ProviderManager { get; set; }
+        public static ILocalizationManager LocalizationManager { get; set; }
+        public static IItemRepository ItemRepository { get; set; }
+        public static IFileSystem FileSystem { get; set; }
+        public static IUserDataManager UserDataManager { get; set; }
+        public static IChannelManager ChannelManager { get; set; }
+        public static IMediaSourceManager MediaSourceManager { get; set; }
+
+        /// <summary>
+        /// Returns a <see cref="System.String" /> that represents this instance.
+        /// </summary>
+        /// <returns>A <see cref="System.String" /> that represents this instance.</returns>
+        public override string ToString()
+        {
+            return Name;
+        }
+
+        [IgnoreDataMember]
+        public bool IsLocked { get; set; }
+
+        /// <summary>
+        /// Gets or sets the locked fields.
+        /// </summary>
+        /// <value>The locked fields.</value>
+        [IgnoreDataMember]
+        public MetadataFields[] LockedFields { get; set; }
+
+        /// <summary>
+        /// Gets the type of the media.
+        /// </summary>
+        /// <value>The type of the media.</value>
+        [IgnoreDataMember]
+        public virtual string MediaType
+        {
+            get
+            {
+                return null;
+            }
+        }
+
+        [IgnoreDataMember]
+        public virtual string[] PhysicalLocations
+        {
+            get
+            {
+                if (!IsFileProtocol)
+                {
+                    return new string[] { };
+                }
+
+                return new[] { Path };
+            }
+        }
+
+        private string _forcedSortName;
+        /// <summary>
+        /// Gets or sets the name of the forced sort.
+        /// </summary>
+        /// <value>The name of the forced sort.</value>
+        [IgnoreDataMember]
+        public string ForcedSortName
+        {
+            get { return _forcedSortName; }
+            set { _forcedSortName = value; _sortName = null; }
+        }
+
+        private string _sortName;
+        /// <summary>
+        /// Gets the name of the sort.
+        /// </summary>
+        /// <value>The name of the sort.</value>
+        [IgnoreDataMember]
+        public string SortName
+        {
+            get
+            {
+                if (_sortName == null)
+                {
+                    if (!string.IsNullOrEmpty(ForcedSortName))
+                    {
+                        // Need the ToLower because that's what CreateSortName does
+                        _sortName = ModifySortChunks(ForcedSortName).ToLower();
+                    }
+                    else
+                    {
+                        _sortName = CreateSortName();
+                    }
+                }
+                return _sortName;
+            }
+            set
+            {
+                _sortName = value;
+            }
+        }
+
+        public string GetInternalMetadataPath()
+        {
+            var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
+
+            return GetInternalMetadataPath(basePath);
+        }
+
+        protected virtual string GetInternalMetadataPath(string basePath)
+        {
+            if (SourceType == SourceType.Channel)
+            {
+                return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N"), Id.ToString("N"));
+            }
+
+            var idString = Id.ToString("N");
+
+            basePath = System.IO.Path.Combine(basePath, "library");
+
+            return System.IO.Path.Combine(basePath, idString.Substring(0, 2), idString);
+        }
+
+        /// <summary>
+        /// Creates the name of the sort.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        protected virtual string CreateSortName()
+        {
+            if (Name == null) return null; //some items may not have name filled in properly
+
+            if (!EnableAlphaNumericSorting)
+            {
+                return Name.TrimStart();
+            }
+
+            var sortable = Name.Trim().ToLower();
+
+            foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
+            {
+                sortable = sortable.Replace(removeChar, string.Empty);
+            }
+
+            foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
+            {
+                sortable = sortable.Replace(replaceChar, " ");
+            }
+
+            foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
+            {
+                // Remove from beginning if a space follows
+                if (sortable.StartsWith(search + " "))
+                {
+                    sortable = sortable.Remove(0, search.Length + 1);
+                }
+                // Remove from middle if surrounded by spaces
+                sortable = sortable.Replace(" " + search + " ", " ");
+
+                // Remove from end if followed by a space
+                if (sortable.EndsWith(" " + search))
+                {
+                    sortable = sortable.Remove(sortable.Length - (search.Length + 1));
+                }
+            }
+
+            return ModifySortChunks(sortable);
+        }
+
+        private string ModifySortChunks(string name)
+        {
+            var chunks = GetSortChunks(name);
+
+            var builder = new StringBuilder();
+
+            foreach (var chunk in chunks)
+            {
+                var chunkBuilder = chunk.Item1;
+
+                // This chunk is numeric
+                if (chunk.Item2)
+                {
+                    while (chunkBuilder.Length < 10)
+                    {
+                        chunkBuilder.Insert(0, '0');
+                    }
+                }
+
+                builder.Append(chunkBuilder);
+            }
+            //Logger.Debug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
+            return builder.ToString().RemoveDiacritics();
+        }
+
+        [IgnoreDataMember]
+        public bool EnableMediaSourceDisplay
+        {
+            get
+            {
+                if (SourceType == SourceType.Channel)
+                {
+                    return ChannelManager.EnableMediaSourceDisplay(this);
+                }
+
+                return true;
+            }
+        }
+
+        [IgnoreDataMember]
+        public Guid ParentId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the parent.
+        /// </summary>
+        /// <value>The parent.</value>
+        [IgnoreDataMember]
+        public Folder Parent
+        {
+            get { return GetParent() as Folder; }
+            set
+            {
+
+            }
+        }
+
+        public void SetParent(Folder parent)
+        {
+            ParentId = parent == null ? Guid.Empty : parent.Id;
+        }
+
+        public BaseItem GetParent()
+        {
+            var parentId = ParentId;
+            if (!parentId.Equals(Guid.Empty))
+            {
+                return LibraryManager.GetItemById(parentId);
+            }
+
+            return null;
+        }
+
+        public IEnumerable<BaseItem> GetParents()
+        {
+            var parent = GetParent();
+
+            while (parent != null)
+            {
+                yield return parent;
+
+                parent = parent.GetParent();
+            }
+        }
+
+        /// <summary>
+        /// Finds a parent of a given type
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <returns>``0.</returns>
+        public T FindParent<T>()
+            where T : Folder
+        {
+            foreach (var parent in GetParents())
+            {
+                var item = parent as T;
+                if (item != null)
+                {
+                    return item;
+                }
+            }
+            return null;
+        }
+
+        [IgnoreDataMember]
+        public virtual Guid DisplayParentId
+        {
+            get
+            {
+                var parentId = ParentId;
+                return parentId;
+            }
+        }
+
+        [IgnoreDataMember]
+        public BaseItem DisplayParent
+        {
+            get
+            {
+                var id = DisplayParentId;
+                if (id.Equals(Guid.Empty))
+                {
+                    return null;
+                }
+                return LibraryManager.GetItemById(id);
+            }
+        }
+
+        /// <summary>
+        /// When the item first debuted. For movies this could be premiere date, episodes would be first aired
+        /// </summary>
+        /// <value>The premiere date.</value>
+        [IgnoreDataMember]
+        public DateTime? PremiereDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the end date.
+        /// </summary>
+        /// <value>The end date.</value>
+        [IgnoreDataMember]
+        public DateTime? EndDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the official rating.
+        /// </summary>
+        /// <value>The official rating.</value>
+        [IgnoreDataMember]
+        public string OfficialRating { get; set; }
+
+        [IgnoreDataMember]
+        public int InheritedParentalRatingValue { get; set; }
+
+        /// <summary>
+        /// Gets or sets the critic rating.
+        /// </summary>
+        /// <value>The critic rating.</value>
+        [IgnoreDataMember]
+        public float? CriticRating { get; set; }
+
+        /// <summary>
+        /// Gets or sets the custom rating.
+        /// </summary>
+        /// <value>The custom rating.</value>
+        [IgnoreDataMember]
+        public string CustomRating { get; set; }
+
+        /// <summary>
+        /// Gets or sets the overview.
+        /// </summary>
+        /// <value>The overview.</value>
+        [IgnoreDataMember]
+        public string Overview { get; set; }
+
+        /// <summary>
+        /// Gets or sets the studios.
+        /// </summary>
+        /// <value>The studios.</value>
+        [IgnoreDataMember]
+        public string[] Studios { get; set; }
+
+        /// <summary>
+        /// Gets or sets the genres.
+        /// </summary>
+        /// <value>The genres.</value>
+        [IgnoreDataMember]
+        public string[] Genres { get; set; }
+
+        /// <summary>
+        /// Gets or sets the tags.
+        /// </summary>
+        /// <value>The tags.</value>
+        [IgnoreDataMember]
+        public string[] Tags { get; set; }
+
+        [IgnoreDataMember]
+        public string[] ProductionLocations { get; set; }
+
+        /// <summary>
+        /// Gets or sets the home page URL.
+        /// </summary>
+        /// <value>The home page URL.</value>
+        [IgnoreDataMember]
+        public string HomePageUrl { get; set; }
+
+        /// <summary>
+        /// Gets or sets the community rating.
+        /// </summary>
+        /// <value>The community rating.</value>
+        [IgnoreDataMember]
+        public float? CommunityRating { get; set; }
+
+        /// <summary>
+        /// Gets or sets the run time ticks.
+        /// </summary>
+        /// <value>The run time ticks.</value>
+        [IgnoreDataMember]
+        public long? RunTimeTicks { get; set; }
+
+        /// <summary>
+        /// Gets or sets the production year.
+        /// </summary>
+        /// <value>The production year.</value>
+        [IgnoreDataMember]
+        public int? ProductionYear { get; set; }
+
+        /// <summary>
+        /// If the item is part of a series, this is it's number in the series.
+        /// This could be episode number, album track number, etc.
+        /// </summary>
+        /// <value>The index number.</value>
+        [IgnoreDataMember]
+        public int? IndexNumber { get; set; }
+
+        /// <summary>
+        /// For an episode this could be the season number, or for a song this could be the disc number.
+        /// </summary>
+        /// <value>The parent index number.</value>
+        [IgnoreDataMember]
+        public int? ParentIndexNumber { get; set; }
+
+        [IgnoreDataMember]
+        public virtual bool HasLocalAlternateVersions
+        {
+            get { return false; }
+        }
+
+        [IgnoreDataMember]
+        public string OfficialRatingForComparison
+        {
+            get
+            {
+                var officialRating = OfficialRating;
+                if (!string.IsNullOrEmpty(officialRating))
+                {
+                    return officialRating;
+                }
+
+                var parent = DisplayParent;
+                if (parent != null)
+                {
+                    return parent.OfficialRatingForComparison;
+                }
+
+                return null;
+            }
+        }
+
+        [IgnoreDataMember]
+        public string CustomRatingForComparison
+        {
+            get
+            {
+                var customRating = CustomRating;
+                if (!string.IsNullOrEmpty(customRating))
+                {
+                    return customRating;
+                }
+
+                var parent = DisplayParent;
+                if (parent != null)
+                {
+                    return parent.CustomRatingForComparison;
+                }
+
+                return null;
+            }
+        }
+
+        /// <summary>
+        /// Gets the play access.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <returns>PlayAccess.</returns>
+        public PlayAccess GetPlayAccess(User user)
+        {
+            if (!user.Policy.EnableMediaPlayback)
+            {
+                return PlayAccess.None;
+            }
+
+            //if (!user.IsParentalScheduleAllowed())
+            //{
+            //    return PlayAccess.None;
+            //}
+
+            return PlayAccess.Full;
+        }
+
+        public virtual List<MediaStream> GetMediaStreams()
+        {
+            return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
+            {
+                ItemId = Id
+            });
+        }
+
+        protected virtual bool IsActiveRecording()
+        {
+            return false;
+        }
+
+        public virtual List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
+        {
+            if (SourceType == SourceType.Channel)
+            {
+                var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None)
+                           .ToList();
+
+                if (sources.Count > 0)
+                {
+                    return sources;
+                }
+            }
+
+            var list = GetAllItemsForMediaSources();
+            var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item1, i.Item2)).ToList();
+
+            if (IsActiveRecording())
+            {
+                foreach (var mediaSource in result)
+                {
+                    mediaSource.Type = MediaSourceType.Placeholder;
+                }
+            }
+
+            return result.OrderBy(i =>
+            {
+                if (i.VideoType == VideoType.VideoFile)
+                {
+                    return 0;
+                }
+
+                return 1;
+
+            }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
+            .ThenByDescending(i =>
+            {
+                var stream = i.VideoStream;
+
+                return stream == null || stream.Width == null ? 0 : stream.Width.Value;
+            })
+            .ToList();
+        }
+
+        protected virtual List<Tuple<BaseItem, MediaSourceType>> GetAllItemsForMediaSources()
+        {
+            return new List<Tuple<BaseItem, MediaSourceType>>();
+        }
+
+        private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type)
+        {
+            if (item == null)
+            {
+                throw new ArgumentNullException("media");
+            }
+
+            var protocol = item.PathProtocol;
+
+            var info = new MediaSourceInfo
+            {
+                Id = item.Id.ToString("N"),
+                Protocol = protocol ?? MediaProtocol.File,
+                MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
+                Name = GetMediaSourceName(item),
+                Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path,
+                RunTimeTicks = item.RunTimeTicks,
+                Container = item.Container,
+                Size = item.Size,
+                Type = type
+            };
+
+            if (string.IsNullOrEmpty(info.Path))
+            {
+                info.Type = MediaSourceType.Placeholder;
+            }
+
+            if (info.Protocol == MediaProtocol.File)
+            {
+                info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N");
+            }
+
+            var video = item as Video;
+            if (video != null)
+            {
+                info.IsoType = video.IsoType;
+                info.VideoType = video.VideoType;
+                info.Video3DFormat = video.Video3DFormat;
+                info.Timestamp = video.Timestamp;
+
+                if (video.IsShortcut)
+                {
+                    info.IsRemote = true;
+                    info.Path = video.ShortcutPath;
+                    info.Protocol = MediaSourceManager.GetPathProtocol(info.Path);
+                }
+
+                if (string.IsNullOrEmpty(info.Container))
+                {
+                    if (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso)
+                    {
+                        if (protocol.HasValue && protocol.Value == MediaProtocol.File)
+                        {
+                            info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
+                        }
+                    }
+                }
+            }
+
+            if (string.IsNullOrEmpty(info.Container))
+            {
+                if (protocol.HasValue && protocol.Value == MediaProtocol.File)
+                {
+                    info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
+                }
+            }
+
+            if (info.SupportsDirectStream && !string.IsNullOrEmpty(info.Path))
+            {
+                info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol);
+            }
+
+            if (video != null && video.VideoType != VideoType.VideoFile)
+            {
+                info.SupportsDirectStream = false;
+            }
+
+            info.Bitrate = item.TotalBitrate;
+            info.InferTotalBitrate();
+
+            return info;
+        }
+
+        private string GetMediaSourceName(BaseItem item)
+        {
+            var terms = new List<string>();
+
+            var path = item.Path;
+            if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
+            {
+                if (HasLocalAlternateVersions)
+                {
+                    var displayName = System.IO.Path.GetFileNameWithoutExtension(path)
+                        .Replace(System.IO.Path.GetFileName(ContainingFolderPath), string.Empty, StringComparison.OrdinalIgnoreCase)
+                        .TrimStart(new char[] { ' ', '-' });
+
+                    if (!string.IsNullOrEmpty(displayName))
+                    {
+                        terms.Add(displayName);
+                    }
+                }
+
+                if (terms.Count == 0)
+                {
+                    var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
+                    terms.Add(displayName);
+                }
+            }
+
+            if (terms.Count == 0)
+            {
+                terms.Add(item.Name);
+            }
+
+            var video = item as Video;
+            if (video != null)
+            {
+                if (video.Video3DFormat.HasValue)
+                {
+                    terms.Add("3D");
+                }
+
+                if (video.VideoType == VideoType.BluRay)
+                {
+                    terms.Add("Bluray");
+                }
+                else if (video.VideoType == VideoType.Dvd)
+                {
+                    terms.Add("DVD");
+                }
+                else if (video.VideoType == VideoType.Iso)
+                {
+                    if (video.IsoType.HasValue)
+                    {
+                        if (video.IsoType.Value == Model.Entities.IsoType.BluRay)
+                        {
+                            terms.Add("Bluray");
+                        }
+                        else if (video.IsoType.Value == Model.Entities.IsoType.Dvd)
+                        {
+                            terms.Add("DVD");
+                        }
+                    }
+                    else
+                    {
+                        terms.Add("ISO");
+                    }
+                }
+            }
+
+            return string.Join("/", terms.ToArray(terms.Count));
+        }
+
+        /// <summary>
+        /// Loads the theme songs.
+        /// </summary>
+        /// <returns>List{Audio.Audio}.</returns>
+        private static Audio.Audio[] LoadThemeSongs(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
+        {
+            var files = fileSystemChildren.Where(i => i.IsDirectory)
+                .Where(i => string.Equals(i.Name, ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase))
+                .SelectMany(i => FileSystem.GetFiles(i.FullName))
+                .ToList();
+
+            // Support plex/xbmc convention
+            files.AddRange(fileSystemChildren
+                .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase))
+                );
+
+            return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
+                .OfType<Audio.Audio>()
+                .Select(audio =>
+                {
+                    // Try to retrieve it from the db. If we don't find it, use the resolved version
+                    var dbItem = LibraryManager.GetItemById(audio.Id) as Audio.Audio;
+
+                    if (dbItem != null)
+                    {
+                        audio = dbItem;
+                    }
+                    else
+                    {
+                        // item is new
+                        audio.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeSong;
+                    }
+
+                    return audio;
+
+                    // Sort them so that the list can be easily compared for changes
+                }).OrderBy(i => i.Path).ToArray();
+        }
+
+        /// <summary>
+        /// Loads the video backdrops.
+        /// </summary>
+        /// <returns>List{Video}.</returns>
+        private static Video[] LoadThemeVideos(IEnumerable<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
+        {
+            var files = fileSystemChildren.Where(i => i.IsDirectory)
+                .Where(i => string.Equals(i.Name, ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
+                .SelectMany(i => FileSystem.GetFiles(i.FullName));
+
+            return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
+                .OfType<Video>()
+                .Select(item =>
+                {
+                    // Try to retrieve it from the db. If we don't find it, use the resolved version
+                    var dbItem = LibraryManager.GetItemById(item.Id) as Video;
+
+                    if (dbItem != null)
+                    {
+                        item = dbItem;
+                    }
+                    else
+                    {
+                        // item is new
+                        item.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeVideo;
+                    }
+
+                    return item;
+
+                    // Sort them so that the list can be easily compared for changes
+                }).OrderBy(i => i.Path).ToArray();
+        }
+
+        public Task RefreshMetadata(CancellationToken cancellationToken)
+        {
+            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken);
+        }
+
+        protected virtual void TriggerOnRefreshStart()
+        {
+
+        }
+
+        protected virtual void TriggerOnRefreshComplete()
+        {
+
+        }
+
+        /// <summary>
+        /// Overrides the base implementation to refresh metadata for local trailers
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>true if a provider reports we changed</returns>
+        public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken)
+        {
+            TriggerOnRefreshStart();
+
+            var requiresSave = false;
+
+            if (SupportsOwnedItems)
+            {
+                try
+                {
+                    var files = IsFileProtocol ?
+                        GetFileSystemChildren(options.DirectoryService).ToList() :
+                        new List<FileSystemMetadata>();
+
+                    var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
+
+                    if (ownedItemsChanged)
+                    {
+                        requiresSave = true;
+                    }
+                }
+                catch (Exception ex)
+                {
+                    Logger.ErrorException("Error refreshing owned items for {0}", ex, Path ?? Name);
+                }
+            }
+
+            try
+            {
+                var refreshOptions = requiresSave
+                    ? new MetadataRefreshOptions(options)
+                    {
+                        ForceSave = true
+                    }
+                    : options;
+
+                return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
+            }
+            finally
+            {
+                TriggerOnRefreshComplete();
+            }
+        }
+
+        [IgnoreDataMember]
+        protected virtual bool SupportsOwnedItems
+        {
+            get { return !ParentId.Equals(Guid.Empty) && IsFileProtocol; }
+        }
+
+        [IgnoreDataMember]
+        public virtual bool SupportsPeople
+        {
+            get { return false; }
+        }
+
+        [IgnoreDataMember]
+        public virtual bool SupportsThemeMedia
+        {
+            get { return false; }
+        }
+
+        /// <summary>
+        /// Refreshes owned items such as trailers, theme videos, special features, etc.
+        /// Returns true or false indicating if changes were found.
+        /// </summary>
+        /// <param name="options"></param>
+        /// <param name="fileSystemChildren"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
+        {
+            var themeSongsChanged = false;
+
+            var themeVideosChanged = false;
+
+            var localTrailersChanged = false;
+
+            if (IsFileProtocol && SupportsOwnedItems)
+            {
+                if (SupportsThemeMedia)
+                {
+                    if (!IsInMixedFolder)
+                    {
+                        themeSongsChanged = await RefreshThemeSongs(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+
+                        themeVideosChanged = await RefreshThemeVideos(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+                    }
+                }
+
+                var hasTrailers = this as IHasTrailers;
+                if (hasTrailers != null)
+                {
+                    localTrailersChanged = await RefreshLocalTrailers(hasTrailers, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+                }
+            }
+
+            return themeSongsChanged || themeVideosChanged || localTrailersChanged;
+        }
+
+        protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
+        {
+            var path = ContainingFolderPath;
+
+            return directoryService.GetFileSystemEntries(path);
+        }
+
+        private async Task<bool> RefreshLocalTrailers(IHasTrailers item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
+        {
+            var newItems = LibraryManager.FindTrailers(this, fileSystemChildren, options.DirectoryService).ToList();
+
+            var newItemIds = newItems.Select(i => i.Id).ToArray();
+
+            var itemsChanged = !item.LocalTrailerIds.SequenceEqual(newItemIds);
+            var ownerId = item.Id;
+
+            var tasks = newItems.Select(i =>
+            {
+                var subOptions = new MetadataRefreshOptions(options);
+
+                if (!i.ExtraType.HasValue ||
+                    i.ExtraType.Value != Model.Entities.ExtraType.Trailer ||
+                    i.OwnerId != ownerId ||
+                    !i.ParentId.Equals(Guid.Empty))
+                {
+                    i.ExtraType = Model.Entities.ExtraType.Trailer;
+                    i.OwnerId = ownerId;
+                    i.ParentId = Guid.Empty;
+                    subOptions.ForceSave = true;
+                }
+
+                return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
+            });
+
+            await Task.WhenAll(tasks).ConfigureAwait(false);
+
+            item.LocalTrailerIds = newItemIds;
+
+            return itemsChanged;
+        }
+
+        private async Task<bool> RefreshThemeVideos(BaseItem item, MetadataRefreshOptions options, IEnumerable<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
+        {
+            var newThemeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService);
+
+            var newThemeVideoIds = newThemeVideos.Select(i => i.Id).ToArray(newThemeVideos.Length);
+
+            var themeVideosChanged = !item.ThemeVideoIds.SequenceEqual(newThemeVideoIds);
+
+            var ownerId = item.Id;
+
+            var tasks = newThemeVideos.Select(i =>
+            {
+                var subOptions = new MetadataRefreshOptions(options);
+
+                if (!i.ExtraType.HasValue ||
+                    i.ExtraType.Value != Model.Entities.ExtraType.ThemeVideo ||
+                    i.OwnerId != ownerId ||
+                    !i.ParentId.Equals(Guid.Empty))
+                {
+                    i.ExtraType = Model.Entities.ExtraType.ThemeVideo;
+                    i.OwnerId = ownerId;
+                    i.ParentId = Guid.Empty;
+                    subOptions.ForceSave = true;
+                }
+
+                return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
+            });
+
+            await Task.WhenAll(tasks).ConfigureAwait(false);
+
+            item.ThemeVideoIds = newThemeVideoIds;
+
+            return themeVideosChanged;
+        }
+
+        /// <summary>
+        /// Refreshes the theme songs.
+        /// </summary>
+        private async Task<bool> RefreshThemeSongs(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
+        {
+            var newThemeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService);
+            var newThemeSongIds = newThemeSongs.Select(i => i.Id).ToArray(newThemeSongs.Length);
+
+            var themeSongsChanged = !item.ThemeSongIds.SequenceEqual(newThemeSongIds);
+
+            var ownerId = item.Id;
+
+            var tasks = newThemeSongs.Select(i =>
+            {
+                var subOptions = new MetadataRefreshOptions(options);
+
+                if (!i.ExtraType.HasValue ||
+                    i.ExtraType.Value != Model.Entities.ExtraType.ThemeSong ||
+                    i.OwnerId != ownerId ||
+                    !i.ParentId.Equals(Guid.Empty))
+                {
+                    i.ExtraType = Model.Entities.ExtraType.ThemeSong;
+                    i.OwnerId = ownerId;
+                    i.ParentId = Guid.Empty;
+                    subOptions.ForceSave = true;
+                }
+
+                return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
+            });
+
+            await Task.WhenAll(tasks).ConfigureAwait(false);
+
+            item.ThemeSongIds = newThemeSongIds;
+
+            return themeSongsChanged;
+        }
+
+        /// <summary>
+        /// Gets or sets the provider ids.
+        /// </summary>
+        /// <value>The provider ids.</value>
+        [IgnoreDataMember]
+        public Dictionary<string, string> ProviderIds { get; set; }
+
+        [IgnoreDataMember]
+        public virtual Folder LatestItemsIndexContainer
+        {
+            get { return null; }
+        }
+
+        public virtual double GetDefaultPrimaryImageAspectRatio()
+        {
+            return 0;
+        }
+
+        public virtual string CreatePresentationUniqueKey()
+        {
+            return Id.ToString("N");
+        }
+
+        [IgnoreDataMember]
+        public string PresentationUniqueKey { get; set; }
+
+        public string GetPresentationUniqueKey()
+        {
+            return PresentationUniqueKey ?? CreatePresentationUniqueKey();
+        }
+
+        public virtual bool RequiresRefresh()
+        {
+            return false;
+        }
+
+        public virtual List<string> GetUserDataKeys()
+        {
+            var list = new List<string>();
+
+            if (SourceType == SourceType.Channel)
+            {
+                if (!string.IsNullOrEmpty(ExternalId))
+                {
+                    list.Add(ExternalId);
+                }
+            }
+
+            list.Add(Id.ToString());
+            return list;
+        }
+
+        internal virtual ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
+        {
+            var updateType = ItemUpdateType.None;
+
+            if (IsInMixedFolder != newItem.IsInMixedFolder)
+            {
+                IsInMixedFolder = newItem.IsInMixedFolder;
+                updateType |= ItemUpdateType.MetadataImport;
+            }
+
+            return updateType;
+        }
+
+        public void AfterMetadataRefresh()
+        {
+            _sortName = null;
+        }
+
+        /// <summary>
+        /// Gets the preferred metadata language.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        public string GetPreferredMetadataLanguage()
+        {
+            string lang = PreferredMetadataLanguage;
+
+            if (string.IsNullOrEmpty(lang))
+            {
+                lang = GetParents()
+                    .Select(i => i.PreferredMetadataLanguage)
+                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+            }
+
+            if (string.IsNullOrEmpty(lang))
+            {
+                lang = LibraryManager.GetCollectionFolders(this)
+                    .Select(i => i.PreferredMetadataLanguage)
+                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+            }
+
+            if (string.IsNullOrEmpty(lang))
+            {
+                lang = LibraryManager.GetLibraryOptions(this).PreferredMetadataLanguage;
+            }
+
+            if (string.IsNullOrEmpty(lang))
+            {
+                lang = ConfigurationManager.Configuration.PreferredMetadataLanguage;
+            }
+
+            return lang;
+        }
+
+        /// <summary>
+        /// Gets the preferred metadata language.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        public string GetPreferredMetadataCountryCode()
+        {
+            string lang = PreferredMetadataCountryCode;
+
+            if (string.IsNullOrEmpty(lang))
+            {
+                lang = GetParents()
+                    .Select(i => i.PreferredMetadataCountryCode)
+                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+            }
+
+            if (string.IsNullOrEmpty(lang))
+            {
+                lang = LibraryManager.GetCollectionFolders(this)
+                    .Select(i => i.PreferredMetadataCountryCode)
+                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+            }
+
+            if (string.IsNullOrEmpty(lang))
+            {
+                lang = LibraryManager.GetLibraryOptions(this).MetadataCountryCode;
+            }
+
+            if (string.IsNullOrEmpty(lang))
+            {
+                lang = ConfigurationManager.Configuration.MetadataCountryCode;
+            }
+
+            return lang;
+        }
+
+        public virtual bool IsSaveLocalMetadataEnabled()
+        {
+            if (SourceType == SourceType.Channel)
+            {
+                return false;
+            }
+
+            var libraryOptions = LibraryManager.GetLibraryOptions(this);
+
+            return libraryOptions.SaveLocalMetadata;
+        }
+
+        /// <summary>
+        /// Determines if a given user has access to this item
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
+        /// <exception cref="System.ArgumentNullException">user</exception>
+        public bool IsParentalAllowed(User user)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException("user");
+            }
+
+            if (!IsVisibleViaTags(user))
+            {
+                return false;
+            }
+
+            var maxAllowedRating = user.Policy.MaxParentalRating;
+
+            if (maxAllowedRating == null)
+            {
+                return true;
+            }
+
+            var rating = CustomRatingForComparison;
+
+            if (string.IsNullOrEmpty(rating))
+            {
+                rating = OfficialRatingForComparison;
+            }
+
+            if (string.IsNullOrEmpty(rating))
+            {
+                return !GetBlockUnratedValue(user.Policy);
+            }
+
+            var value = LocalizationManager.GetRatingLevel(rating);
+
+            // Could not determine the integer value
+            if (!value.HasValue)
+            {
+                var isAllowed = !GetBlockUnratedValue(user.Policy);
+
+                if (!isAllowed)
+                {
+                    Logger.Debug("{0} has an unrecognized parental rating of {1}.", Name, rating);
+                }
+
+                return isAllowed;
+            }
+
+            return value.Value <= maxAllowedRating.Value;
+        }
+
+        public int? GetParentalRatingValue()
+        {
+            var rating = CustomRating;
+
+            if (string.IsNullOrEmpty(rating))
+            {
+                rating = OfficialRating;
+            }
+
+            if (string.IsNullOrEmpty(rating))
+            {
+                return null;
+            }
+
+            return LocalizationManager.GetRatingLevel(rating);
+        }
+
+        public int? GetInheritedParentalRatingValue()
+        {
+            var rating = CustomRatingForComparison;
+
+            if (string.IsNullOrEmpty(rating))
+            {
+                rating = OfficialRatingForComparison;
+            }
+
+            if (string.IsNullOrEmpty(rating))
+            {
+                return null;
+            }
+
+            return LocalizationManager.GetRatingLevel(rating);
+        }
+
+        public List<string> GetInheritedTags()
+        {
+            var list = new List<string>();
+            list.AddRange(Tags);
+
+            foreach (var parent in GetParents())
+            {
+                list.AddRange(parent.Tags);
+            }
+
+            return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
+        }
+
+        private bool IsVisibleViaTags(User user)
+        {
+            var policy = user.Policy;
+            if (policy.BlockedTags.Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase)))
+            {
+                return false;
+            }
+
+            return true;
+        }
+
+        protected virtual bool IsAllowTagFilterEnforced()
+        {
+            return true;
+        }
+
+        public virtual UnratedItem GetBlockUnratedType()
+        {
+            if (SourceType == SourceType.Channel)
+            {
+                return UnratedItem.ChannelContent;
+            }
+
+            return UnratedItem.Other;
+        }
+
+        /// <summary>
+        /// Gets the block unrated value.
+        /// </summary>
+        /// <param name="config">The configuration.</param>
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+        protected virtual bool GetBlockUnratedValue(UserPolicy config)
+        {
+            // Don't block plain folders that are unrated. Let the media underneath get blocked
+            // Special folders like series and albums will override this method.
+            if (IsFolder)
+            {
+                return false;
+            }
+            if (this is IItemByName)
+            {
+                return false;
+            }
+
+            return config.BlockUnratedItems.Contains(GetBlockUnratedType());
+        }
+
+        /// <summary>
+        /// Determines if this folder should be visible to a given user.
+        /// Default is just parental allowed. Can be overridden for more functionality.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
+        /// <exception cref="System.ArgumentNullException">user</exception>
+        public virtual bool IsVisible(User user)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException("user");
+            }
+
+            return IsParentalAllowed(user);
+        }
+
+        public virtual bool IsVisibleStandalone(User user)
+        {
+            if (SourceType == SourceType.Channel)
+            {
+                return IsVisibleStandaloneInternal(user, false) && Channel.IsChannelVisible(this, user);
+            }
+
+            return IsVisibleStandaloneInternal(user, true);
+        }
+
+        [IgnoreDataMember]
+        public virtual bool SupportsInheritedParentImages
+        {
+            get { return false; }
+        }
+
+        protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
+        {
+            if (!IsVisible(user))
+            {
+                return false;
+            }
+
+            if (GetParents().Any(i => !i.IsVisible(user)))
+            {
+                return false;
+            }
+
+            if (checkFolders)
+            {
+                var topParent = GetParents().LastOrDefault() ?? this;
+
+                if (string.IsNullOrEmpty(topParent.Path))
+                {
+                    return true;
+                }
+
+                var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList();
+
+                if (itemCollectionFolders.Count > 0)
+                {
+                    var userCollectionFolders = LibraryManager.GetUserRootFolder().GetChildren(user, true).Select(i => i.Id).ToList();
+                    if (!itemCollectionFolders.Any(userCollectionFolders.Contains))
+                    {
+                        return false;
+                    }
+                }
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether this instance is folder.
+        /// </summary>
+        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
+        [IgnoreDataMember]
+        public virtual bool IsFolder
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        public virtual bool IsDisplayedAsFolder
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public virtual string GetClientTypeName()
+        {
+            if (IsFolder && SourceType == SourceType.Channel && !(this is Channel))
+            {
+                return "ChannelFolderItem";
+            }
+
+            return GetType().Name;
+        }
+
+        /// <summary>
+        /// Gets the linked child.
+        /// </summary>
+        /// <param name="info">The info.</param>
+        /// <returns>BaseItem.</returns>
+        protected BaseItem GetLinkedChild(LinkedChild info)
+        {
+            // First get using the cached Id
+            if (info.ItemId.HasValue)
+            {
+                if (info.ItemId.Value.Equals(Guid.Empty))
+                {
+                    return null;
+                }
+
+                var itemById = LibraryManager.GetItemById(info.ItemId.Value);
+
+                if (itemById != null)
+                {
+                    return itemById;
+                }
+            }
+
+            var item = FindLinkedChild(info);
+
+            // If still null, log
+            if (item == null)
+            {
+                // Don't keep searching over and over
+                info.ItemId = Guid.Empty;
+            }
+            else
+            {
+                // Cache the id for next time
+                info.ItemId = item.Id;
+            }
+
+            return item;
+        }
+
+        private BaseItem FindLinkedChild(LinkedChild info)
+        {
+            var path = info.Path;
+
+            if (!string.IsNullOrEmpty(path))
+            {
+                path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path);
+
+                var itemByPath = LibraryManager.FindByPath(path, null);
+
+                if (itemByPath == null)
+                {
+                    //Logger.Warn("Unable to find linked item at path {0}", info.Path);
+                }
+
+                return itemByPath;
+            }
+
+            if (!string.IsNullOrEmpty(info.LibraryItemId))
+            {
+                var item = LibraryManager.GetItemById(info.LibraryItemId);
+
+                if (item == null)
+                {
+                    //Logger.Warn("Unable to find linked item at path {0}", info.Path);
+                }
+
+                return item;
+            }
+
+            return null;
+        }
+
+        [IgnoreDataMember]
+        public virtual bool EnableRememberingTrackSelections
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        /// <summary>
+        /// Adds a studio to the item
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <exception cref="System.ArgumentNullException"></exception>
+        public void AddStudio(string name)
+        {
+            if (string.IsNullOrEmpty(name))
+            {
+                throw new ArgumentNullException("name");
+            }
+
+            var current = Studios;
+
+            if (!current.Contains(name, StringComparer.OrdinalIgnoreCase))
+            {
+                if (current.Length == 0)
+                {
+                    Studios = new[] { name };
+                }
+                else
+                {
+                    var list = current.ToArray(current.Length + 1);
+                    list[list.Length - 1] = name;
+                    Studios = list;
+                }
+            }
+        }
+
+        public void SetStudios(IEnumerable<string> names)
+        {
+            Studios = names.Distinct().ToArray();
+        }
+
+        /// <summary>
+        /// Adds a genre to the item
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <exception cref="System.ArgumentNullException"></exception>
+        public void AddGenre(string name)
+        {
+            if (string.IsNullOrEmpty(name))
+            {
+                throw new ArgumentNullException("name");
+            }
+
+            var genres = Genres;
+            if (!genres.Contains(name, StringComparer.OrdinalIgnoreCase))
+            {
+                var list = genres.ToList();
+                list.Add(name);
+                Genres = list.ToArray();
+            }
+        }
+
+        /// <summary>
+        /// Marks the played.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <param name="datePlayed">The date played.</param>
+        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="System.ArgumentNullException"></exception>
+        public virtual void MarkPlayed(User user,
+            DateTime? datePlayed,
+            bool resetPosition)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException();
+            }
+
+            var data = UserDataManager.GetUserData(user, this);
+
+            if (datePlayed.HasValue)
+            {
+                // Increment
+                data.PlayCount++;
+            }
+
+            // Ensure it's at least one
+            data.PlayCount = Math.Max(data.PlayCount, 1);
+
+            if (resetPosition)
+            {
+                data.PlaybackPositionTicks = 0;
+            }
+
+            data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
+            data.Played = true;
+
+            UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
+        }
+
+        /// <summary>
+        /// Marks the unplayed.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="System.ArgumentNullException"></exception>
+        public virtual void MarkUnplayed(User user)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException();
+            }
+
+            var data = UserDataManager.GetUserData(user, this);
+
+            //I think it is okay to do this here.
+            // if this is only called when a user is manually forcing something to un-played
+            // then it probably is what we want to do...
+            data.PlayCount = 0;
+            data.PlaybackPositionTicks = 0;
+            data.LastPlayedDate = null;
+            data.Played = false;
+
+            UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
+        }
+
+        /// <summary>
+        /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
+        /// </summary>
+        /// <returns>Task.</returns>
+        public virtual void ChangedExternally()
+        {
+            ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(FileSystem)
+            {
+
+            }, RefreshPriority.High);
+        }
+
+        /// <summary>
+        /// Gets an image
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <param name="imageIndex">Index of the image.</param>
+        /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
+        /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
+        public bool HasImage(ImageType type, int imageIndex)
+        {
+            return GetImageInfo(type, imageIndex) != null;
+        }
+
+        public void SetImage(ItemImageInfo image, int index)
+        {
+            if (image.Type == ImageType.Chapter)
+            {
+                throw new ArgumentException("Cannot set chapter images using SetImagePath");
+            }
+
+            var existingImage = GetImageInfo(image.Type, index);
+
+            if (existingImage != null)
+            {
+                existingImage.Path = image.Path;
+                existingImage.DateModified = image.DateModified;
+                existingImage.Width = image.Width;
+                existingImage.Height = image.Height;
+            }
+
+            else
+            {
+                var currentCount = ImageInfos.Length;
+                var newList = ImageInfos.ToArray(currentCount + 1);
+                newList[currentCount] = image;
+                ImageInfos = newList;
+            }
+        }
+
+        public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
+        {
+            if (type == ImageType.Chapter)
+            {
+                throw new ArgumentException("Cannot set chapter images using SetImagePath");
+            }
+
+            var image = GetImageInfo(type, index);
+
+            if (image == null)
+            {
+                var currentCount = ImageInfos.Length;
+                var newList = ImageInfos.ToArray(currentCount + 1);
+                newList[currentCount] = GetImageInfo(file, type);
+                ImageInfos = newList;
+            }
+            else
+            {
+                var imageInfo = GetImageInfo(file, type);
+
+                image.Path = file.FullName;
+                image.DateModified = imageInfo.DateModified;
+
+                // reset these values
+                image.Width = 0;
+                image.Height = 0;
+            }
+        }
+
+        /// <summary>
+        /// Deletes the image.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <param name="index">The index.</param>
+        /// <returns>Task.</returns>
+        public void DeleteImage(ImageType type, int index)
+        {
+            var info = GetImageInfo(type, index);
+
+            if (info == null)
+            {
+                // Nothing to do
+                return;
+            }
+
+            // Remove it from the item
+            RemoveImage(info);
+
+            if (info.IsLocalFile)
+            {
+                FileSystem.DeleteFile(info.Path);
+            }
+
+            UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+        }
+
+        public void RemoveImage(ItemImageInfo image)
+        {
+            RemoveImages(new List<ItemImageInfo> { image });
+        }
+
+        public void RemoveImages(List<ItemImageInfo> deletedImages)
+        {
+            ImageInfos = ImageInfos.Except(deletedImages).ToArray();
+        }
+
+        public virtual void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken)
+        {
+            LibraryManager.UpdateItem(this, GetParent(), updateReason, cancellationToken);
+        }
+
+        /// <summary>
+        /// Validates that images within the item are still on the file system
+        /// </summary>
+        public bool ValidateImages(IDirectoryService directoryService)
+        {
+            var allFiles = ImageInfos
+                .Where(i => i.IsLocalFile)
+                .Select(i => FileSystem.GetDirectoryName(i.Path))
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .SelectMany(i => directoryService.GetFilePaths(i))
+                .ToList();
+
+            var deletedImages = ImageInfos
+                .Where(image => image.IsLocalFile && !allFiles.Contains(image.Path, StringComparer.OrdinalIgnoreCase))
+                .ToList();
+
+            if (deletedImages.Count > 0)
+            {
+                ImageInfos = ImageInfos.Except(deletedImages).ToArray();
+            }
+
+            return deletedImages.Count > 0;
+        }
+
+        /// <summary>
+        /// Gets the image path.
+        /// </summary>
+        /// <param name="imageType">Type of the image.</param>
+        /// <param name="imageIndex">Index of the image.</param>
+        /// <returns>System.String.</returns>
+        /// <exception cref="System.InvalidOperationException">
+        /// </exception>
+        /// <exception cref="System.ArgumentNullException">item</exception>
+        public string GetImagePath(ImageType imageType, int imageIndex)
+        {
+            var info = GetImageInfo(imageType, imageIndex);
+
+            return info == null ? null : info.Path;
+        }
+
+        /// <summary>
+        /// Gets the image information.
+        /// </summary>
+        /// <param name="imageType">Type of the image.</param>
+        /// <param name="imageIndex">Index of the image.</param>
+        /// <returns>ItemImageInfo.</returns>
+        public ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex)
+        {
+            if (imageType == ImageType.Chapter)
+            {
+                var chapter = ItemRepository.GetChapter(this, imageIndex);
+
+                if (chapter == null)
+                {
+                    return null;
+                }
+
+                var path = chapter.ImagePath;
+
+                if (string.IsNullOrEmpty(path))
+                {
+                    return null;
+                }
+
+                return new ItemImageInfo
+                {
+                    Path = path,
+                    DateModified = chapter.ImageDateModified,
+                    Type = imageType
+                };
+            }
+
+            return GetImages(imageType)
+                .ElementAtOrDefault(imageIndex);
+        }
+
+        public IEnumerable<ItemImageInfo> GetImages(ImageType imageType)
+        {
+            if (imageType == ImageType.Chapter)
+            {
+                throw new ArgumentException("No image info for chapter images");
+            }
+
+            return ImageInfos.Where(i => i.Type == imageType);
+        }
+
+        /// <summary>
+        /// Adds the images.
+        /// </summary>
+        /// <param name="imageType">Type of the image.</param>
+        /// <param name="images">The images.</param>
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+        /// <exception cref="System.ArgumentException">Cannot call AddImages with chapter images</exception>
+        public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
+        {
+            if (imageType == ImageType.Chapter)
+            {
+                throw new ArgumentException("Cannot call AddImages with chapter images");
+            }
+
+            var existingImages = GetImages(imageType)
+                .ToList();
+
+            var newImageList = new List<FileSystemMetadata>();
+            var imageAdded = false;
+            var imageUpdated = false;
+
+            foreach (var newImage in images)
+            {
+                if (newImage == null)
+                {
+                    throw new ArgumentException("null image found in list");
+                }
+
+                var existing = existingImages
+                    .FirstOrDefault(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
+
+                if (existing == null)
+                {
+                    newImageList.Add(newImage);
+                    imageAdded = true;
+                }
+                else
+                {
+                    if (existing.IsLocalFile)
+                    {
+                        var newDateModified = FileSystem.GetLastWriteTimeUtc(newImage);
+
+                        // If date changed then we need to reset saved image dimensions
+                        if (existing.DateModified != newDateModified && (existing.Width > 0 || existing.Height > 0))
+                        {
+                            existing.Width = 0;
+                            existing.Height = 0;
+                            imageUpdated = true;
+                        }
+
+                        existing.DateModified = newDateModified;
+                    }
+                }
+            }
+
+            if (imageAdded || images.Count != existingImages.Count)
+            {
+                var newImagePaths = images.Select(i => i.FullName).ToList();
+
+                var deleted = existingImages
+                    .Where(i => i.IsLocalFile && !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !FileSystem.FileExists(i.Path))
+                    .ToList();
+
+                if (deleted.Count > 0)
+                {
+                    ImageInfos = ImageInfos.Except(deleted).ToArray();
+                }
+            }
+
+            if (newImageList.Count > 0)
+            {
+                var currentCount = ImageInfos.Length;
+                var newList = ImageInfos.ToArray(currentCount + newImageList.Count);
+
+                foreach (var image in newImageList)
+                {
+                    newList[currentCount] = GetImageInfo(image, imageType);
+                    currentCount++;
+                }
+
+                ImageInfos = newList;
+            }
+
+            return imageUpdated || newImageList.Count > 0;
+        }
+
+        private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type)
+        {
+            return new ItemImageInfo
+            {
+                Path = file.FullName,
+                Type = type,
+                DateModified = FileSystem.GetLastWriteTimeUtc(file)
+            };
+        }
+
+        /// <summary>
+        /// Gets the file system path to delete when the item is to be deleted
+        /// </summary>
+        /// <returns></returns>
+        public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
+        {
+            return new[] {
+                new FileSystemMetadata
+                {
+                    FullName = Path,
+                    IsDirectory = IsFolder
+                }
+            }.Concat(GetLocalMetadataFilesToDelete());
+        }
+
+        protected List<FileSystemMetadata> GetLocalMetadataFilesToDelete()
+        {
+            if (IsFolder || !IsInMixedFolder)
+            {
+                return new List<FileSystemMetadata>();
+            }
+
+            var filename = System.IO.Path.GetFileNameWithoutExtension(Path);
+            var extensions = new List<string> { ".nfo", ".xml", ".srt", ".vtt", ".sub", ".idx", ".txt", ".edl", ".bif", ".smi", ".ttml" };
+            extensions.AddRange(SupportedImageExtensions);
+
+            return FileSystem.GetFiles(FileSystem.GetDirectoryName(Path), extensions.ToArray(extensions.Count), false, false)
+                .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison.OrdinalIgnoreCase))
+                .ToList();
+        }
+
+        public bool AllowsMultipleImages(ImageType type)
+        {
+            return type == ImageType.Backdrop || type == ImageType.Screenshot || type == ImageType.Chapter;
+        }
+
+        public void SwapImages(ImageType type, int index1, int index2)
+        {
+            if (!AllowsMultipleImages(type))
+            {
+                throw new ArgumentException("The change index operation is only applicable to backdrops and screenshots");
+            }
+
+            var info1 = GetImageInfo(type, index1);
+            var info2 = GetImageInfo(type, index2);
+
+            if (info1 == null || info2 == null)
+            {
+                // Nothing to do
+                return;
+            }
+
+            if (!info1.IsLocalFile || !info2.IsLocalFile)
+            {
+                // TODO: Not supported  yet
+                return;
+            }
+
+            var path1 = info1.Path;
+            var path2 = info2.Path;
+
+            FileSystem.SwapFiles(path1, path2);
+
+            // Refresh these values
+            info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path);
+            info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path);
+
+            info1.Width = 0;
+            info1.Height = 0;
+            info2.Width = 0;
+            info2.Height = 0;
+
+            UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+        }
+
+        public virtual bool IsPlayed(User user)
+        {
+            var userdata = UserDataManager.GetUserData(user, this);
+
+            return userdata != null && userdata.Played;
+        }
+
+        public bool IsFavoriteOrLiked(User user)
+        {
+            var userdata = UserDataManager.GetUserData(user, this);
+
+            return userdata != null && (userdata.IsFavorite || (userdata.Likes ?? false));
+        }
+
+        public virtual bool IsUnplayed(User user)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException("user");
+            }
+
+            var userdata = UserDataManager.GetUserData(user, this);
+
+            return userdata == null || !userdata.Played;
+        }
+
+        ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo()
+        {
+            return GetItemLookupInfo<ItemLookupInfo>();
+        }
+
+        protected T GetItemLookupInfo<T>()
+            where T : ItemLookupInfo, new()
+        {
+            return new T
+            {
+                MetadataCountryCode = GetPreferredMetadataCountryCode(),
+                MetadataLanguage = GetPreferredMetadataLanguage(),
+                Name = GetNameForMetadataLookup(),
+                ProviderIds = ProviderIds,
+                IndexNumber = IndexNumber,
+                ParentIndexNumber = ParentIndexNumber,
+                Year = ProductionYear,
+                PremiereDate = PremiereDate
+            };
+        }
+
+        protected virtual string GetNameForMetadataLookup()
+        {
+            return Name;
+        }
+
+        /// <summary>
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
+        /// </summary>
+        public virtual bool BeforeMetadataRefresh(bool replaceAllMetdata)
+        {
+            _sortName = null;
+
+            var hasChanges = false;
+
+            if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path))
+            {
+                Name = FileSystem.GetFileNameWithoutExtension(Path);
+                hasChanges = true;
+            }
+
+            return hasChanges;
+        }
+
+        protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol)
+        {
+            if (protocol.HasValue && protocol.Value == MediaProtocol.File)
+            {
+                return LibraryManager.GetPathAfterNetworkSubstitution(path, item);
+            }
+
+            return path;
+        }
+
+        public virtual void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields)
+        {
+            if (RunTimeTicks.HasValue)
+            {
+                double pct = RunTimeTicks.Value;
+
+                if (pct > 0)
+                {
+                    pct = userData.PlaybackPositionTicks / pct;
+
+                    if (pct > 0)
+                    {
+                        dto.PlayedPercentage = 100 * pct;
+                    }
+                }
+            }
+        }
+
+        protected Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken)
+        {
+            var newOptions = new MetadataRefreshOptions(options);
+            newOptions.SearchResult = null;
+
+            var item = this;
+
+            if (copyTitleMetadata)
+            {
+                // Take some data from the main item, for querying purposes
+                if (!item.Genres.SequenceEqual(ownedItem.Genres, StringComparer.Ordinal))
+                {
+                    newOptions.ForceSave = true;
+                    ownedItem.Genres = item.Genres;
+                }
+                if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal))
+                {
+                    newOptions.ForceSave = true;
+                    ownedItem.Studios = item.Studios;
+                }
+                if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal))
+                {
+                    newOptions.ForceSave = true;
+                    ownedItem.ProductionLocations = item.ProductionLocations;
+                }
+                if (item.CommunityRating != ownedItem.CommunityRating)
+                {
+                    ownedItem.CommunityRating = item.CommunityRating;
+                    newOptions.ForceSave = true;
+                }
+                if (item.CriticRating != ownedItem.CriticRating)
+                {
+                    ownedItem.CriticRating = item.CriticRating;
+                    newOptions.ForceSave = true;
+                }
+                if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal))
+                {
+                    ownedItem.Overview = item.Overview;
+                    newOptions.ForceSave = true;
+                }
+                if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal))
+                {
+                    ownedItem.OfficialRating = item.OfficialRating;
+                    newOptions.ForceSave = true;
+                }
+                if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal))
+                {
+                    ownedItem.CustomRating = item.CustomRating;
+                    newOptions.ForceSave = true;
+                }
+            }
+
+            return ownedItem.RefreshMetadata(newOptions, cancellationToken);
+        }
+
+        protected Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
+        {
+            var newOptions = new MetadataRefreshOptions(options);
+            newOptions.SearchResult = null;
+
+            var id = LibraryManager.GetNewItemId(path, typeof(Video));
+
+            // Try to retrieve it from the db. If we don't find it, use the resolved version
+            var video = LibraryManager.GetItemById(id) as Video;
+
+            if (video == null)
+            {
+                video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
+
+                newOptions.ForceSave = true;
+            }
+
+            //var parentId = Id;
+            //if (!video.IsOwnedItem || video.ParentId != parentId)
+            //{
+            //    video.IsOwnedItem = true;
+            //    video.ParentId = parentId;
+            //    newOptions.ForceSave = true;
+            //}
+
+            if (video == null)
+            {
+                return Task.FromResult(true);
+            }
+
+            return RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken);
+        }
+
+        public string GetEtag(User user)
+        {
+            var list = GetEtagValues(user);
+
+            return string.Join("|", list.ToArray(list.Count)).GetMD5().ToString("N");
+        }
+
+        protected virtual List<string> GetEtagValues(User user)
+        {
+            return new List<string>
+            {
+                DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
+            };
+        }
+
+        public virtual IEnumerable<Guid> GetAncestorIds()
+        {
+            return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id));
+        }
+
+        public BaseItem GetTopParent()
+        {
+            if (IsTopParent)
+            {
+                return this;
+            }
+
+            foreach (var parent in GetParents())
+            {
+                if (parent.IsTopParent)
+                {
+                    return parent;
+                }
+            }
+            return null;
+        }
+
+        [IgnoreDataMember]
+        public virtual bool IsTopParent
+        {
+            get
+            {
+                if (this is BasePluginFolder || this is Channel)
+                {
+                    return true;
+                }
+
+                var view = this as IHasCollectionType;
+                if (view != null)
+                {
+                    if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase))
+                    {
+                        return true;
+                    }
+                }
+
+                if (GetParent() is AggregateFolder)
+                {
+                    return true;
+                }
+
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        public virtual bool SupportsAncestors
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        [IgnoreDataMember]
+        public virtual bool StopRefreshIfLocalMetadataFound
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
+        {
+            return new[] { Id };
+        }
+
+        public virtual List<ExternalUrl> GetRelatedUrls()
+        {
+            return new List<ExternalUrl>();
+        }
+
+        public virtual double? GetRefreshProgress()
+        {
+            return null;
+        }
+
+        public virtual ItemUpdateType OnMetadataChanged()
+        {
+            var updateType = ItemUpdateType.None;
+
+            var item = this;
+
+            var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? 0;
+            if (inheritedParentalRatingValue != item.InheritedParentalRatingValue)
+            {
+                item.InheritedParentalRatingValue = inheritedParentalRatingValue;
+                updateType |= ItemUpdateType.MetadataImport;
+            }
+
+            return updateType;
+        }
+
+        /// <summary>
+        /// Updates the official rating based on content and returns true or false indicating if it changed.
+        /// </summary>
+        /// <returns></returns>
+        public bool UpdateRatingToItems(IList<BaseItem> children)
+        {
+            var currentOfficialRating = OfficialRating;
+
+            // Gather all possible ratings
+            var ratings = children
+                .Select(i => i.OfficialRating)
+                .Where(i => !string.IsNullOrEmpty(i))
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .Select(i => new Tuple<string, int?>(i, LocalizationManager.GetRatingLevel(i)))
+                .OrderBy(i => i.Item2 ?? 1000)
+                .Select(i => i.Item1);
+
+            OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
+
+            return !string.Equals(currentOfficialRating ?? string.Empty, OfficialRating ?? string.Empty,
+                StringComparison.OrdinalIgnoreCase);
+        }
+
+        public IEnumerable<BaseItem> GetThemeSongs()
+        {
+            return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeSong)).OrderBy(i => i.SortName);
+        }
+
+        public IEnumerable<BaseItem> GetThemeVideos()
+        {
+            return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName);
+        }
+
+        public MediaUrl[] RemoteTrailers { get; set; }
+
+        public IEnumerable<BaseItem> GetExtras()
+        {
+            return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName);
+        }
+
+        public IEnumerable<BaseItem> GetExtras(ExtraType[] unused)
+        {
+            return GetExtras();
+        }
+
+        public IEnumerable<BaseItem> GetDisplayExtras()
+        {
+            return GetExtras();
+        }
+
+        public virtual bool IsHD { 
+            get{ 
+                return Height >= 720;
+            } 
+        }
+        public bool IsShortcut{ get; set;}
+        public string ShortcutPath{ get; set;}
+        public int Width { get; set; }
+        public int Height { get; set; }
+        public Guid[] ExtraIds { get; set; }
+        public virtual long GetRunTimeTicksForPlayState() {
+            return RunTimeTicks ?? 0;
+        }
+        // what does this do?
+        public static ExtraType[] DisplayExtraTypes = new[] {Model.Entities.ExtraType.ThemeSong, Model.Entities.ExtraType.ThemeVideo };
+        public virtual bool SupportsExternalTransfer {
+            get {
+                return false;
+            }
+        }
+    }
+}

+ 65 - 0
MediaBrowser.Controller/Entities/BaseItemExtensions.cs

@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public static class BaseItemExtensions
+    {
+        /// <summary>
+        /// Gets the image path.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="imageType">Type of the image.</param>
+        /// <returns>System.String.</returns>
+        public static string GetImagePath(this BaseItem item, ImageType imageType)
+        {
+            return item.GetImagePath(imageType, 0);
+        }
+
+        public static bool HasImage(this BaseItem item, ImageType imageType)
+        {
+            return item.HasImage(imageType, 0);
+        }
+
+        /// <summary>
+        /// Sets the image path.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="imageType">Type of the image.</param>
+        /// <param name="file">The file.</param>
+        public static void SetImagePath(this BaseItem item, ImageType imageType, FileSystemMetadata file)
+        {
+            item.SetImagePath(imageType, 0, file);
+        }
+
+        /// <summary>
+        /// Sets the image path.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="imageType">Type of the image.</param>
+        /// <param name="file">The file.</param>
+        public static void SetImagePath(this BaseItem item, ImageType imageType, string file)
+        {
+            if (file.StartsWith("http", System.StringComparison.OrdinalIgnoreCase))
+            {
+                item.SetImage(new ItemImageInfo
+                {
+                    Path = file,
+                    Type = imageType
+                }, 0);
+            }
+            else
+            {
+                item.SetImagePath(imageType, BaseItem.FileSystem.GetFileInfo(file));
+            }
+        }
+    }
+}

+ 54 - 0
MediaBrowser.Controller/Entities/BasePluginFolder.cs

@@ -0,0 +1,54 @@
+
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// Plugins derive from and export this class to create a folder that will appear in the root along
+    /// with all the other actual physical folders in the system.
+    /// </summary>
+    public abstract class BasePluginFolder : Folder, ICollectionFolder
+    {
+        [IgnoreDataMember]
+        public virtual string CollectionType
+        {
+            get { return null; }
+        }
+
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
+        public override bool IsSaveLocalMetadataEnabled()
+        {
+            return true;
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsInheritedParentImages
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPeople
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        //public override double? GetDefaultPrimaryImageAspectRatio()
+        //{
+        //    double value = 16;
+        //    value /= 9;
+
+        //    return value;
+        //}
+    }
+}

+ 72 - 0
MediaBrowser.Controller/Entities/Book.cs

@@ -0,0 +1,72 @@
+using System;
+using System.Linq;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries
+    {
+        [IgnoreDataMember]
+        public override string MediaType
+        {
+            get
+            {
+                return Model.Entities.MediaType.Book;
+            }
+        }
+
+        [IgnoreDataMember]
+        public string SeriesPresentationUniqueKey { get; set; }
+        [IgnoreDataMember]
+        public string SeriesName { get; set; }
+        [IgnoreDataMember]
+        public Guid SeriesId { get; set; }
+
+        public string FindSeriesSortName()
+        {
+            return SeriesName;
+        }
+        public string FindSeriesName()
+        {
+            return SeriesName;
+        }
+        public string FindSeriesPresentationUniqueKey()
+        {
+            return SeriesPresentationUniqueKey;
+        }
+
+        public Guid FindSeriesId()
+        {
+            return SeriesId;
+        }
+
+        public override bool CanDownload()
+        {
+            return IsFileProtocol;
+        }
+
+        public override UnratedItem GetBlockUnratedType()
+        {
+            return UnratedItem.Book;
+        }
+
+        public BookInfo GetLookupInfo()
+        {
+            var info = GetItemLookupInfo<BookInfo>();
+
+            if (string.IsNullOrEmpty(SeriesName))
+            {
+                info.SeriesName = GetParents().Select(i => i.Name).FirstOrDefault();
+            }
+            else
+            {
+                info.SeriesName = SeriesName;
+            }
+
+            return info;
+        }
+    }
+}

+ 405 - 0
MediaBrowser.Controller/Entities/CollectionFolder.cs

@@ -0,0 +1,405 @@
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// Specialized Folder class that points to a subset of the physical folders in the system.
+    /// It is created from the user-specific folders within the system root
+    /// </summary>
+    public class CollectionFolder : Folder, ICollectionFolder
+    {
+        public static IXmlSerializer XmlSerializer { get; set; }
+        public static IJsonSerializer JsonSerializer { get; set; }
+        public static IServerApplicationHost ApplicationHost { get; set; }
+
+        public CollectionFolder()
+        {
+            PhysicalLocationsList = new string[] { };
+            PhysicalFolderIds = new Guid[] { };
+        }
+
+        //public override double? GetDefaultPrimaryImageAspectRatio()
+        //{
+        //    double value = 16;
+        //    value /= 9;
+
+        //    return value;
+        //}
+
+        [IgnoreDataMember]
+        public override bool SupportsPlayedStatus
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsInheritedParentImages
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
+        public string CollectionType { get; set; }
+
+        private static readonly Dictionary<string, LibraryOptions> LibraryOptions = new Dictionary<string, LibraryOptions>();
+        public LibraryOptions GetLibraryOptions()
+        {
+            return GetLibraryOptions(Path);
+        }
+
+        private static LibraryOptions LoadLibraryOptions(string path)
+        {
+            try
+            {
+                var result = XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) as LibraryOptions;
+
+                if (result == null)
+                {
+                    return new LibraryOptions();
+                }
+
+                foreach (var mediaPath in result.PathInfos)
+                {
+                    if (!string.IsNullOrEmpty(mediaPath.Path))
+                    {
+                        mediaPath.Path = ApplicationHost.ExpandVirtualPath(mediaPath.Path);
+                    }
+                }
+
+                return result;
+            }
+            catch (FileNotFoundException)
+            {
+                return new LibraryOptions();
+            }
+            catch (IOException)
+            {
+                return new LibraryOptions();
+            }
+            catch (Exception ex)
+            {
+                Logger.ErrorException("Error loading library options", ex);
+
+                return new LibraryOptions();
+            }
+        }
+
+        private static string GetLibraryOptionsPath(string path)
+        {
+            return System.IO.Path.Combine(path, "options.xml");
+        }
+
+        public void UpdateLibraryOptions(LibraryOptions options)
+        {
+            SaveLibraryOptions(Path, options);
+        }
+
+        public static LibraryOptions GetLibraryOptions(string path)
+        {
+            lock (LibraryOptions)
+            {
+                LibraryOptions options;
+                if (!LibraryOptions.TryGetValue(path, out options))
+                {
+                    options = LoadLibraryOptions(path);
+                    LibraryOptions[path] = options;
+                }
+
+                return options;
+            }
+        }
+
+        public static void SaveLibraryOptions(string path, LibraryOptions options)
+        {
+            lock (LibraryOptions)
+            {
+                LibraryOptions[path] = options;
+
+                var clone = JsonSerializer.DeserializeFromString<LibraryOptions>(JsonSerializer.SerializeToString(options));
+                foreach (var mediaPath in clone.PathInfos)
+                {
+                    if (!string.IsNullOrEmpty(mediaPath.Path))
+                    {
+                        mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
+                    }
+                }
+
+                XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
+            }
+        }
+
+        public static void OnCollectionFolderChange()
+        {
+            lock (LibraryOptions)
+            {
+                LibraryOptions.Clear();
+            }
+        }
+
+        /// <summary>
+        /// Allow different display preferences for each collection folder
+        /// </summary>
+        /// <value>The display prefs id.</value>
+        [IgnoreDataMember]
+        public override Guid DisplayPreferencesId
+        {
+            get
+            {
+                return Id;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override string[] PhysicalLocations
+        {
+            get
+            {
+                return PhysicalLocationsList;
+            }
+        }
+
+        public override bool IsSaveLocalMetadataEnabled()
+        {
+            return true;
+        }
+
+        public string[] PhysicalLocationsList { get; set; }
+        public Guid[] PhysicalFolderIds { get; set; }
+
+        protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
+        {
+            return CreateResolveArgs(directoryService, true).FileSystemChildren;
+        }
+
+        private bool _requiresRefresh;
+        public override bool RequiresRefresh()
+        {
+            var changed = base.RequiresRefresh() || _requiresRefresh;
+
+            if (!changed)
+            {
+                var locations = PhysicalLocations;
+
+                var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations;
+
+                if (!locations.SequenceEqual(newLocations))
+                {
+                    changed = true;
+                }
+            }
+
+            if (!changed)
+            {
+                var folderIds = PhysicalFolderIds;
+
+                var newFolderIds = GetPhysicalFolders(false).Select(i => i.Id).ToList();
+
+                if (!folderIds.SequenceEqual(newFolderIds))
+                {
+                    changed = true;
+                }
+            }
+
+            return changed;
+        }
+
+        public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+        {
+            var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh;
+            _requiresRefresh = false;
+            return changed;
+        }
+
+        public override double? GetRefreshProgress()
+        {
+            var folders = GetPhysicalFolders(true).ToList();
+            double totalProgresses = 0;
+            var foldersWithProgress = 0;
+
+            foreach (var folder in folders)
+            {
+                var progress = ProviderManager.GetRefreshProgress(folder.Id);
+                if (progress.HasValue)
+                {
+                    totalProgresses += progress.Value;
+                    foldersWithProgress++;
+                }
+            }
+
+            if (foldersWithProgress == 0)
+            {
+                return null;
+            }
+
+            return (totalProgresses / foldersWithProgress);
+        }
+
+        protected override bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
+        {
+            return RefreshLinkedChildrenInternal(true);
+        }
+
+        private bool RefreshLinkedChildrenInternal(bool setFolders)
+        {
+            var physicalFolders = GetPhysicalFolders(false)
+                .ToList();
+
+            var linkedChildren = physicalFolders
+                .SelectMany(c => c.LinkedChildren)
+                .ToList();
+
+            var changed = !linkedChildren.SequenceEqual(LinkedChildren, new LinkedChildComparer(FileSystem));
+
+            LinkedChildren = linkedChildren.ToArray(linkedChildren.Count);
+
+            var folderIds = PhysicalFolderIds;
+            var newFolderIds = physicalFolders.Select(i => i.Id).ToArray();
+
+            if (!folderIds.SequenceEqual(newFolderIds))
+            {
+                changed = true;
+                if (setFolders)
+                {
+                    PhysicalFolderIds = newFolderIds;
+                }
+            }
+
+            return changed;
+        }
+
+        private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService, bool setPhysicalLocations)
+        {
+            var path = ContainingFolderPath;
+
+            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
+            {
+                FileInfo = FileSystem.GetDirectoryInfo(path),
+                Path = path,
+                Parent = GetParent() as Folder,
+                CollectionType = CollectionType
+            };
+
+            // Gather child folder and files
+            if (args.IsDirectory)
+            {
+                var flattenFolderDepth = 0;
+
+                var files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, FileSystem, ApplicationHost, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: true);
+
+                args.FileSystemChildren = files;
+            }
+
+            _requiresRefresh = _requiresRefresh || !args.PhysicalLocations.SequenceEqual(PhysicalLocations);
+
+            if (setPhysicalLocations)
+            {
+                PhysicalLocationsList = args.PhysicalLocations;
+            }
+
+            return args;
+        }
+
+        /// <summary>
+        /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
+        /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+        /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
+        /// <param name="refreshOptions">The refresh options.</param>
+        /// <param name="directoryService">The directory service.</param>
+        /// <returns>Task.</returns>
+        protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+        {
+            return Task.FromResult(true);
+        }
+
+        /// <summary>
+        /// Our children are actually just references to the ones in the physical root...
+        /// </summary>
+        /// <value>The actual children.</value>
+        [IgnoreDataMember]
+        public override IEnumerable<BaseItem> Children
+        {
+            get { return GetActualChildren(); }
+        }
+
+        public IEnumerable<BaseItem> GetActualChildren()
+        {
+            return GetPhysicalFolders(true).SelectMany(c => c.Children);
+        }
+
+        public IEnumerable<Folder> GetPhysicalFolders()
+        {
+            return GetPhysicalFolders(true);
+        }
+
+        private IEnumerable<Folder> GetPhysicalFolders(bool enableCache)
+        {
+            if (enableCache)
+            {
+                return PhysicalFolderIds.Select(i => LibraryManager.GetItemById(i)).OfType<Folder>();
+            }
+
+            var rootChildren = LibraryManager.RootFolder.Children
+                .OfType<Folder>()
+                .ToList();
+
+            return PhysicalLocations.Where(i => !FileSystem.AreEqual(i, Path)).SelectMany(i => GetPhysicalParents(i, rootChildren)).DistinctBy(i => i.Id);
+        }
+
+        private IEnumerable<Folder> GetPhysicalParents(string path, List<Folder> rootChildren)
+        {
+            var result = rootChildren
+                .Where(i => FileSystem.AreEqual(i.Path, path))
+                .ToList();
+
+            if (result.Count == 0)
+            {
+                var folder = LibraryManager.FindByPath(path, true) as Folder;
+
+                if (folder != null)
+                {
+                    result.Add(folder);
+                }
+            }
+
+            return result;
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPeople
+        {
+            get
+            {
+                return false;
+            }
+        }
+    }
+}

+ 71 - 0
MediaBrowser.Controller/Entities/DayOfWeekHelper.cs

@@ -0,0 +1,71 @@
+using MediaBrowser.Model.Configuration;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public static class DayOfWeekHelper
+    {
+        public static List<DayOfWeek> GetDaysOfWeek(DynamicDayOfWeek day)
+        {
+            return GetDaysOfWeek(new List<DynamicDayOfWeek> { day });
+        }
+
+        public static List<DayOfWeek> GetDaysOfWeek(List<DynamicDayOfWeek> days)
+        {
+            var list = new List<DayOfWeek>();
+
+            if (days.Contains(DynamicDayOfWeek.Sunday) ||
+                days.Contains(DynamicDayOfWeek.Weekend) ||
+                days.Contains(DynamicDayOfWeek.Everyday))
+            {
+                list.Add(DayOfWeek.Sunday);
+            }
+
+            if (days.Contains(DynamicDayOfWeek.Saturday) ||
+                days.Contains(DynamicDayOfWeek.Weekend) ||
+                days.Contains(DynamicDayOfWeek.Everyday))
+            {
+                list.Add(DayOfWeek.Saturday);
+            }
+
+            if (days.Contains(DynamicDayOfWeek.Monday) ||
+                days.Contains(DynamicDayOfWeek.Weekday) ||
+                days.Contains(DynamicDayOfWeek.Everyday))
+            {
+                list.Add(DayOfWeek.Monday);
+            }
+
+            if (days.Contains(DynamicDayOfWeek.Tuesday) ||
+                days.Contains(DynamicDayOfWeek.Weekday) ||
+                days.Contains(DynamicDayOfWeek.Everyday))
+            {
+                list.Add(DayOfWeek.Tuesday
+                    );
+            }
+
+            if (days.Contains(DynamicDayOfWeek.Wednesday) ||
+                days.Contains(DynamicDayOfWeek.Weekday) ||
+                days.Contains(DynamicDayOfWeek.Everyday))
+            {
+                list.Add(DayOfWeek.Wednesday);
+            }
+
+            if (days.Contains(DynamicDayOfWeek.Thursday) ||
+                days.Contains(DynamicDayOfWeek.Weekday) ||
+                days.Contains(DynamicDayOfWeek.Everyday))
+            {
+                list.Add(DayOfWeek.Thursday);
+            }
+
+            if (days.Contains(DynamicDayOfWeek.Friday) ||
+                days.Contains(DynamicDayOfWeek.Weekday) ||
+                days.Contains(DynamicDayOfWeek.Everyday))
+            {
+                list.Add(DayOfWeek.Friday);
+            }
+
+            return list;
+        }
+    }
+}

+ 46 - 0
MediaBrowser.Controller/Entities/Extensions.cs

@@ -0,0 +1,46 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Linq;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// Class Extensions
+    /// </summary>
+    public static class Extensions
+    {
+        /// <summary>
+        /// Adds the trailer URL.
+        /// </summary>
+        public static void AddTrailerUrl(this BaseItem item, string url)
+        {
+            if (string.IsNullOrEmpty(url))
+            {
+                throw new ArgumentNullException("url");
+            }
+
+            var current = item.RemoteTrailers.FirstOrDefault(i => string.Equals(i.Url, url, StringComparison.OrdinalIgnoreCase));
+
+            if (current == null)
+            {
+                var mediaUrl = new MediaUrl
+                {
+                    Url = url
+                };
+
+                if (item.RemoteTrailers.Length == 0)
+                {
+                    item.RemoteTrailers = new[] { mediaUrl };
+                }
+                else
+                {
+                    var list = item.RemoteTrailers.ToArray(item.RemoteTrailers.Length + 1);
+                    list[list.Length - 1] = mediaUrl;
+
+                    item.RemoteTrailers = list;
+                }
+            }
+        }
+    }
+}

+ 1803 - 0
MediaBrowser.Controller/Entities/Folder.cs

@@ -0,0 +1,1803 @@
+using MediaBrowser.Common.Progress;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Controller.Collections;
+using MediaBrowser.Controller.Configuration;
+
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// Class Folder
+    /// </summary>
+    public class Folder : BaseItem
+    {
+        public static IUserManager UserManager { get; set; }
+        public static IUserViewManager UserViewManager { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance is root.
+        /// </summary>
+        /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
+        public bool IsRoot { get; set; }
+
+        public LinkedChild[] LinkedChildren { get; set; }
+
+        [IgnoreDataMember]
+        public DateTime? DateLastMediaAdded { get; set; }
+
+        public Folder()
+        {
+            LinkedChildren = EmptyLinkedChildArray;
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsThemeMedia
+        {
+            get { return true; }
+        }
+
+        [IgnoreDataMember]
+        public virtual bool IsPreSorted
+        {
+            get { return false; }
+        }
+
+        [IgnoreDataMember]
+        public virtual bool IsPhysicalRoot
+        {
+            get { return false; }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsInheritedParentImages
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPlayedStatus
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether this instance is folder.
+        /// </summary>
+        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
+        [IgnoreDataMember]
+        public override bool IsFolder
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool IsDisplayedAsFolder
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        [IgnoreDataMember]
+        public virtual bool SupportsCumulativeRunTimeTicks
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        public virtual bool SupportsDateLastMediaAdded
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public override bool CanDelete()
+        {
+            if (IsRoot)
+            {
+                return false;
+            }
+
+            return base.CanDelete();
+        }
+
+        public override bool RequiresRefresh()
+        {
+            var baseResult = base.RequiresRefresh();
+
+            if (SupportsCumulativeRunTimeTicks && !RunTimeTicks.HasValue)
+            {
+                baseResult = true;
+            }
+
+            return baseResult;
+        }
+
+        [IgnoreDataMember]
+        public override string FileNameWithoutExtension
+        {
+            get
+            {
+                if (IsFileProtocol)
+                {
+                    return System.IO.Path.GetFileName(Path);
+                }
+
+                return null;
+            }
+        }
+
+        protected override bool IsAllowTagFilterEnforced()
+        {
+            if (this is ICollectionFolder)
+            {
+                return false;
+            }
+            if (this is UserView)
+            {
+                return false;
+            }
+            return true;
+        }
+
+        [IgnoreDataMember]
+        protected virtual bool SupportsShortcutChildren
+        {
+            get { return false; }
+        }
+
+        /// <summary>
+        /// Adds the child.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="System.InvalidOperationException">Unable to add  + item.Name</exception>
+        public void AddChild(BaseItem item, CancellationToken cancellationToken)
+        {
+            item.SetParent(this);
+
+            if (item.Id.Equals(Guid.Empty))
+            {
+                item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType());
+            }
+
+            if (item.DateCreated == DateTime.MinValue)
+            {
+                item.DateCreated = DateTime.UtcNow;
+            }
+            if (item.DateModified == DateTime.MinValue)
+            {
+                item.DateModified = DateTime.UtcNow;
+            }
+
+            LibraryManager.CreateItem(item, this);
+        }
+
+        /// <summary>
+        /// Gets the actual children.
+        /// </summary>
+        /// <value>The actual children.</value>
+        [IgnoreDataMember]
+        public virtual IEnumerable<BaseItem> Children
+        {
+            get
+            {
+                return LoadChildren();
+            }
+        }
+
+        /// <summary>
+        /// thread-safe access to all recursive children of this folder - without regard to user
+        /// </summary>
+        /// <value>The recursive children.</value>
+        [IgnoreDataMember]
+        public IEnumerable<BaseItem> RecursiveChildren
+        {
+            get { return GetRecursiveChildren(); }
+        }
+
+        public override bool IsVisible(User user)
+        {
+            if (this is ICollectionFolder && !(this is BasePluginFolder))
+            {
+                if (user.Policy.BlockedMediaFolders != null)
+                {
+                    if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) ||
+
+                        // Backwards compatibility
+                        user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
+                    {
+                        return false;
+                    }
+                }
+                else
+                {
+                    if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
+                    {
+                        return false;
+                    }
+                }
+            }
+
+            return base.IsVisible(user);
+        }
+
+        /// <summary>
+        /// Loads our children.  Validation will occur externally.
+        /// We want this sychronous.
+        /// </summary>
+        protected virtual List<BaseItem> LoadChildren()
+        {
+            //Logger.Debug("Loading children from {0} {1} {2}", GetType().Name, Id, Path);
+            //just load our children from the repo - the library will be validated and maintained in other processes
+            return GetCachedChildren();
+        }
+
+        public override double? GetRefreshProgress()
+        {
+            return ProviderManager.GetRefreshProgress(Id);
+        }
+
+        public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)));
+        }
+
+        /// <summary>
+        /// Validates that the children of the folder still exist
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="metadataRefreshOptions">The metadata refresh options.</param>
+        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+        /// <returns>Task.</returns>
+        public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true)
+        {
+            return ValidateChildrenInternal(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService);
+        }
+
+        private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
+        {
+            var dictionary = new Dictionary<Guid, BaseItem>();
+
+            var childrenList = Children.ToList();
+
+            foreach (var child in childrenList)
+            {
+                var id = child.Id;
+                if (dictionary.ContainsKey(id))
+                {
+                    Logger.Error("Found folder containing items with duplicate id. Path: {0}, Child Name: {1}",
+                        Path ?? Name,
+                        child.Path ?? child.Name);
+                }
+                else
+                {
+                    dictionary[id] = child;
+                }
+            }
+
+            return dictionary;
+        }
+
+        protected override void TriggerOnRefreshStart()
+        {
+        }
+
+        protected override void TriggerOnRefreshComplete()
+        {
+        }
+
+        /// <summary>
+        /// Validates the children internal.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+        /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
+        /// <param name="refreshOptions">The refresh options.</param>
+        /// <param name="directoryService">The directory service.</param>
+        /// <returns>Task.</returns>
+        protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+        {
+            if (recursive)
+            {
+                ProviderManager.OnRefreshStart(this);
+            }
+
+            try
+            {
+                await ValidateChildrenInternal2(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService).ConfigureAwait(false);
+            }
+            finally
+            {
+                if (recursive)
+                {
+                    ProviderManager.OnRefreshComplete(this);
+                }
+            }
+        }
+
+        private async Task ValidateChildrenInternal2(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var validChildren = new List<BaseItem>();
+            var validChildrenNeedGeneration = false;
+
+            if (IsFileProtocol)
+            {
+                IEnumerable<BaseItem> nonCachedChildren;
+
+                try
+                {
+                    nonCachedChildren = GetNonCachedChildren(directoryService);
+                }
+                catch (Exception ex)
+                {
+                    return;
+                }
+
+                progress.Report(5);
+
+                if (recursive)
+                {
+                    ProviderManager.OnRefreshProgress(this, 5);
+                }
+
+                //build a dictionary of the current children we have now by Id so we can compare quickly and easily
+                var currentChildren = GetActualChildrenDictionary();
+
+                //create a list for our validated children
+                var newItems = new List<BaseItem>();
+
+                cancellationToken.ThrowIfCancellationRequested();
+
+                foreach (var child in nonCachedChildren)
+                {
+                    BaseItem currentChild;
+
+                    if (currentChildren.TryGetValue(child.Id, out currentChild))
+                    {
+                        validChildren.Add(currentChild);
+
+                        if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None)
+                        {
+                            currentChild.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken);
+                        }
+
+                        continue;
+                    }
+
+                    // Brand new item - needs to be added
+                    child.SetParent(this);
+                    newItems.Add(child);
+                    validChildren.Add(child);
+                }
+
+                // If any items were added or removed....
+                if (newItems.Count > 0 || currentChildren.Count != validChildren.Count)
+                {
+                    // That's all the new and changed ones - now see if there are any that are missing
+                    var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
+
+                    foreach (var item in itemsRemoved)
+                    {
+                        if (!item.IsFileProtocol)
+                        {
+                        }
+
+                        else
+                        {
+                            Logger.Debug("Removed item: " + item.Path);
+
+                            item.SetParent(null);
+                            LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
+                        }
+                    }
+
+                    LibraryManager.CreateItems(newItems, this, cancellationToken);
+                }
+            }
+            else
+            {
+                validChildrenNeedGeneration = true;
+            }
+
+            progress.Report(10);
+
+            if (recursive)
+            {
+                ProviderManager.OnRefreshProgress(this, 10);
+            }
+
+            cancellationToken.ThrowIfCancellationRequested();
+
+            if (recursive)
+            {
+                var innerProgress = new ActionableProgress<double>();
+
+                var folder = this;
+                innerProgress.RegisterAction(p =>
+                {
+                    double newPct = .80 * p + 10;
+                    progress.Report(newPct);
+                    ProviderManager.OnRefreshProgress(folder, newPct);
+                });
+
+                if (validChildrenNeedGeneration)
+                {
+                    validChildren = Children.ToList();
+                    validChildrenNeedGeneration = false;
+                }
+
+                await ValidateSubFolders(validChildren.OfType<Folder>().ToList(), directoryService, innerProgress, cancellationToken).ConfigureAwait(false);
+            }
+
+            if (refreshChildMetadata)
+            {
+                progress.Report(90);
+
+                if (recursive)
+                {
+                    ProviderManager.OnRefreshProgress(this, 90);
+                }
+
+                var container = this as IMetadataContainer;
+
+                var innerProgress = new ActionableProgress<double>();
+
+                var folder = this;
+                innerProgress.RegisterAction(p =>
+                {
+                    double newPct = .10 * p + 90;
+                    progress.Report(newPct);
+                    if (recursive)
+                    {
+                        ProviderManager.OnRefreshProgress(folder, newPct);
+                    }
+                });
+
+                if (container != null)
+                {
+                    await RefreshAllMetadataForContainer(container, refreshOptions, innerProgress, cancellationToken).ConfigureAwait(false);
+                }
+                else
+                {
+                    if (validChildrenNeedGeneration)
+                    {
+                        validChildren = Children.ToList();
+                    }
+
+                    await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken);
+                }
+            }
+        }
+
+        private async Task RefreshMetadataRecursive(List<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var numComplete = 0;
+            var count = children.Count;
+            double currentPercent = 0;
+
+            foreach (var child in children)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+
+                var innerProgress = new ActionableProgress<double>();
+
+                // Avoid implicitly captured closure
+                var currentInnerPercent = currentPercent;
+
+                innerProgress.RegisterAction(p =>
+                {
+                    double innerPercent = currentInnerPercent;
+                    innerPercent += p / (count);
+                    progress.Report(innerPercent);
+                });
+
+                await RefreshChildMetadata(child, refreshOptions, recursive && child.IsFolder, innerProgress, cancellationToken)
+                    .ConfigureAwait(false);
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= count;
+                percent *= 100;
+                currentPercent = percent;
+
+                progress.Report(percent);
+            }
+        }
+
+        private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var series = container as Series;
+            if (series != null)
+            {
+                await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+
+            }
+            await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
+        }
+
+        private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var container = child as IMetadataContainer;
+
+            if (container != null)
+            {
+                await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAwait(false);
+            }
+            else
+            {
+                if (refreshOptions.RefreshItem(child))
+                {
+                    await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+                }
+
+                if (recursive)
+                {
+                    var folder = child as Folder;
+
+                    if (folder != null)
+                    {
+                        await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Refreshes the children.
+        /// </summary>
+        /// <param name="children">The children.</param>
+        /// <param name="directoryService">The directory service.</param>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var numComplete = 0;
+            var count = children.Count;
+            double currentPercent = 0;
+
+            foreach (var child in children)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+
+                var innerProgress = new ActionableProgress<double>();
+
+                // Avoid implicitly captured closure
+                var currentInnerPercent = currentPercent;
+
+                innerProgress.RegisterAction(p =>
+                {
+                    double innerPercent = currentInnerPercent;
+                    innerPercent += p / (count);
+                    progress.Report(innerPercent);
+                });
+
+                await child.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService)
+                        .ConfigureAwait(false);
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= count;
+                percent *= 100;
+                currentPercent = percent;
+
+                progress.Report(percent);
+            }
+        }
+
+        /// <summary>
+        /// Get the children of this folder from the actual file system
+        /// </summary>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
+        {
+            var collectionType = LibraryManager.GetContentType(this);
+            var libraryOptions = LibraryManager.GetLibraryOptions(this);
+
+            return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, libraryOptions, collectionType);
+        }
+
+        /// <summary>
+        /// Get our children from the repo - stubbed for now
+        /// </summary>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        protected List<BaseItem> GetCachedChildren()
+        {
+            return ItemRepository.GetItemList(new InternalItemsQuery
+            {
+                Parent = this,
+                GroupByPresentationUniqueKey = false,
+                DtoOptions = new DtoOptions(true)
+            });
+        }
+
+        public virtual int GetChildCount(User user)
+        {
+            if (LinkedChildren.Length > 0)
+            {
+                if (!(this is ICollectionFolder))
+                {
+                    return GetChildren(user, true).Count;
+                }
+            }
+
+            var result = GetItems(new InternalItemsQuery(user)
+            {
+                Recursive = false,
+                Limit = 0,
+                Parent = this,
+                DtoOptions = new DtoOptions(false)
+                {
+                    EnableImages = false
+                }
+
+            });
+
+            return result.TotalRecordCount;
+        }
+
+        public virtual int GetRecursiveChildCount(User user)
+        {
+            return GetItems(new InternalItemsQuery(user)
+            {
+                Recursive = true,
+                IsFolder = false,
+                IsVirtualItem = false,
+                EnableTotalRecordCount = true,
+                Limit = 0,
+                DtoOptions = new DtoOptions(false)
+                {
+                    EnableImages = false
+                }
+
+            }).TotalRecordCount;
+        }
+
+        public QueryResult<BaseItem> QueryRecursive(InternalItemsQuery query)
+        {
+            var user = query.User;
+
+            if (!query.ForceDirect && RequiresPostFiltering(query))
+            {
+                IEnumerable<BaseItem> items;
+                Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
+
+                if (query.User == null)
+                {
+                    items = GetRecursiveChildren(filter);
+                }
+                else
+                {
+                    items = GetRecursiveChildren(user, query);
+                }
+
+                return PostFilterAndSort(items, query, true);
+            }
+
+            if (!(this is UserRootFolder) && !(this is AggregateFolder))
+            {
+                if (!query.ParentId.Equals(Guid.Empty))
+                {
+                    query.Parent = this;
+                }
+            }
+
+            if (RequiresPostFiltering2(query))
+            {
+                return QueryWithPostFiltering2(query);
+            }
+
+            return LibraryManager.GetItemsResult(query);
+        }
+
+        private QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query)
+        {
+            var startIndex = query.StartIndex;
+            var limit = query.Limit;
+
+            query.StartIndex = null;
+            query.Limit = null;
+
+            var itemsList = LibraryManager.GetItemList(query);
+            var user = query.User;
+
+            if (user != null)
+            {
+                // needed for boxsets
+                itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User)).ToList();
+            }
+
+            BaseItem[] returnItems;
+            int totalCount = 0;
+
+            if (query.EnableTotalRecordCount)
+            {
+                var itemsArray = itemsList.ToArray();
+                totalCount = itemsArray.Length;
+                returnItems = itemsArray;
+            }
+            else
+            {
+                returnItems = itemsList.ToArray();
+            }
+
+            if (limit.HasValue)
+            {
+                returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value).ToArray();
+            }
+            else if (startIndex.HasValue)
+            {
+                returnItems = returnItems.Skip(startIndex.Value).ToArray();
+            }
+
+            return new QueryResult<BaseItem>
+            {
+                TotalRecordCount = totalCount,
+                Items = returnItems.ToArray()
+            };
+        }
+
+        private bool RequiresPostFiltering2(InternalItemsQuery query)
+        {
+            if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(BoxSet).Name, StringComparison.OrdinalIgnoreCase))
+            {
+                Logger.Debug("Query requires post-filtering due to BoxSet query");
+                return true;
+            }
+
+            return false;
+        }
+
+        private bool RequiresPostFiltering(InternalItemsQuery query)
+        {
+            if (LinkedChildren.Length > 0)
+            {
+                if (!(this is ICollectionFolder))
+                {
+                    Logger.Debug("Query requires post-filtering due to LinkedChildren. Type: " + GetType().Name);
+                    return true;
+                }
+            }
+
+            // Filter by Video3DFormat
+            if (query.Is3D.HasValue)
+            {
+                Logger.Debug("Query requires post-filtering due to Is3D");
+                return true;
+            }
+
+            if (query.HasOfficialRating.HasValue)
+            {
+                Logger.Debug("Query requires post-filtering due to HasOfficialRating");
+                return true;
+            }
+
+            if (query.IsPlaceHolder.HasValue)
+            {
+                Logger.Debug("Query requires post-filtering due to IsPlaceHolder");
+                return true;
+            }
+
+            if (query.HasSpecialFeature.HasValue)
+            {
+                Logger.Debug("Query requires post-filtering due to HasSpecialFeature");
+                return true;
+            }
+
+            if (query.HasSubtitles.HasValue)
+            {
+                Logger.Debug("Query requires post-filtering due to HasSubtitles");
+                return true;
+            }
+
+            if (query.HasTrailer.HasValue)
+            {
+                Logger.Debug("Query requires post-filtering due to HasTrailer");
+                return true;
+            }
+
+            // Filter by VideoType
+            if (query.VideoTypes.Length > 0)
+            {
+                Logger.Debug("Query requires post-filtering due to VideoTypes");
+                return true;
+            }
+
+            if (CollapseBoxSetItems(query, this, query.User, ConfigurationManager))
+            {
+                Logger.Debug("Query requires post-filtering due to CollapseBoxSetItems");
+                return true;
+            }
+
+            if (!string.IsNullOrEmpty(query.AdjacentTo))
+            {
+                Logger.Debug("Query requires post-filtering due to AdjacentTo");
+                return true;
+            }
+
+            if (query.SeriesStatuses.Length > 0)
+            {
+                Logger.Debug("Query requires post-filtering due to SeriesStatuses");
+                return true;
+            }
+
+            if (query.AiredDuringSeason.HasValue)
+            {
+                Logger.Debug("Query requires post-filtering due to AiredDuringSeason");
+                return true;
+            }
+
+            if (query.IsPlayed.HasValue)
+            {
+                if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(typeof(Series).Name))
+                {
+                    Logger.Debug("Query requires post-filtering due to IsPlayed");
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
+        {
+            if (query.ItemIds.Length > 0)
+            {
+                var result = LibraryManager.GetItemsResult(query);
+
+                if (query.OrderBy.Length == 0)
+                {
+                    var ids = query.ItemIds.ToList();
+
+                    // Try to preserve order
+                    result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id)).ToArray();
+                }
+                return result;
+            }
+
+            return GetItemsInternal(query);
+        }
+
+        public BaseItem[] GetItemList(InternalItemsQuery query)
+        {
+            query.EnableTotalRecordCount = false;
+
+            if (query.ItemIds.Length > 0)
+            {
+                var result = LibraryManager.GetItemList(query);
+
+                if (query.OrderBy.Length == 0)
+                {
+                    var ids = query.ItemIds.ToList();
+
+                    // Try to preserve order
+                    return result.OrderBy(i => ids.IndexOf(i.Id)).ToArray();
+                }
+                return result.ToArray(result.Count);
+            }
+
+            return GetItemsInternal(query).Items;
+        }
+
+        protected virtual QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
+        {
+            if (SourceType == SourceType.Channel)
+            {
+                try
+                {
+                    query.Parent = this;
+                    query.ChannelIds = new Guid[] { ChannelId };
+
+                    // Don't blow up here because it could cause parent screens with other content to fail
+                    return ChannelManager.GetChannelItemsInternal(query, new SimpleProgress<double>(), CancellationToken.None).Result;
+                }
+                catch
+                {
+                    // Already logged at lower levels
+                    return new QueryResult<BaseItem>();
+                }
+            }
+
+            if (query.Recursive)
+            {
+                return QueryRecursive(query);
+            }
+
+            var user = query.User;
+
+            Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
+
+            IEnumerable<BaseItem> items;
+
+            if (query.User == null)
+            {
+                items = Children.Where(filter);
+            }
+            else
+            {
+                items = GetChildren(user, true).Where(filter);
+            }
+
+            return PostFilterAndSort(items, query, true);
+        }
+
+        public static ICollectionManager CollectionManager { get; set; }
+
+        protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting)
+        {
+            var user = query.User;
+
+            // Check recursive - don't substitute in plain folder views
+            if (user != null)
+            {
+                items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager);
+            }
+
+            if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater))
+            {
+                items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.CurrentCultureIgnoreCase) < 1);
+            }
+            if (!string.IsNullOrEmpty(query.NameStartsWith))
+            {
+                items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase));
+            }
+
+            if (!string.IsNullOrEmpty(query.NameLessThan))
+            {
+                items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.CurrentCultureIgnoreCase) == 1);
+            }
+
+            // This must be the last filter
+            if (!string.IsNullOrEmpty(query.AdjacentTo))
+            {
+                items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo);
+            }
+
+            return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting);
+        }
+
+        private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(IEnumerable<BaseItem> items,
+            InternalItemsQuery query,
+            BaseItem queryParent,
+            User user,
+            IServerConfigurationManager configurationManager, ICollectionManager collectionManager)
+        {
+            if (items == null)
+            {
+                throw new ArgumentNullException("items");
+            }
+
+            if (CollapseBoxSetItems(query, queryParent, user, configurationManager))
+            {
+                items = collectionManager.CollapseItemsWithinBoxSets(items, user);
+            }
+
+            return items;
+        }
+
+        private static bool CollapseBoxSetItems(InternalItemsQuery query,
+            BaseItem queryParent,
+            User user,
+            IServerConfigurationManager configurationManager)
+        {
+            // Could end up stuck in a loop like this
+            if (queryParent is BoxSet)
+            {
+                return false;
+            }
+            if (queryParent is Series)
+            {
+                return false;
+            }
+            if (queryParent is Season)
+            {
+                return false;
+            }
+            if (queryParent is MusicAlbum)
+            {
+                return false;
+            }
+            if (queryParent is MusicArtist)
+            {
+                return false;
+            }
+
+            var param = query.CollapseBoxSetItems;
+
+            if (!param.HasValue)
+            {
+                if (user != null && !configurationManager.Configuration.EnableGroupingIntoCollections)
+                {
+                    return false;
+                }
+
+                if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains("Movie", StringComparer.OrdinalIgnoreCase))
+                {
+                    param = true;
+                }
+            }
+
+            return param.HasValue && param.Value && AllowBoxSetCollapsing(query);
+        }
+
+        private static bool AllowBoxSetCollapsing(InternalItemsQuery request)
+        {
+            if (request.IsFavorite.HasValue)
+            {
+                return false;
+            }
+            if (request.IsFavoriteOrLiked.HasValue)
+            {
+                return false;
+            }
+            if (request.IsLiked.HasValue)
+            {
+                return false;
+            }
+            if (request.IsPlayed.HasValue)
+            {
+                return false;
+            }
+            if (request.IsResumable.HasValue)
+            {
+                return false;
+            }
+            if (request.IsFolder.HasValue)
+            {
+                return false;
+            }
+
+            if (request.Genres.Length > 0)
+            {
+                return false;
+            }
+
+            if (request.GenreIds.Length > 0)
+            {
+                return false;
+            }
+
+            if (request.HasImdbId.HasValue)
+            {
+                return false;
+            }
+
+            if (request.HasOfficialRating.HasValue)
+            {
+                return false;
+            }
+
+            if (request.HasOverview.HasValue)
+            {
+                return false;
+            }
+
+            if (request.HasParentalRating.HasValue)
+            {
+                return false;
+            }
+
+            if (request.HasSpecialFeature.HasValue)
+            {
+                return false;
+            }
+
+            if (request.HasSubtitles.HasValue)
+            {
+                return false;
+            }
+
+            if (request.HasThemeSong.HasValue)
+            {
+                return false;
+            }
+
+            if (request.HasThemeVideo.HasValue)
+            {
+                return false;
+            }
+
+            if (request.HasTmdbId.HasValue)
+            {
+                return false;
+            }
+
+            if (request.HasTrailer.HasValue)
+            {
+                return false;
+            }
+
+            if (request.ImageTypes.Length > 0)
+            {
+                return false;
+            }
+
+            if (request.Is3D.HasValue)
+            {
+                return false;
+            }
+
+            if (request.IsHD.HasValue)
+            {
+                return false;
+            }
+
+            if (request.IsLocked.HasValue)
+            {
+                return false;
+            }
+
+            if (request.IsPlaceHolder.HasValue)
+            {
+                return false;
+            }
+
+            if (request.IsPlayed.HasValue)
+            {
+                return false;
+            }
+
+            if (!string.IsNullOrWhiteSpace(request.Person))
+            {
+                return false;
+            }
+
+            if (request.PersonIds.Length > 0)
+            {
+                return false;
+            }
+
+            if (request.ItemIds.Length > 0)
+            {
+                return false;
+            }
+
+            if (request.StudioIds.Length > 0)
+            {
+                return false;
+            }
+
+            if (request.GenreIds.Length > 0)
+            {
+                return false;
+            }
+
+            if (request.VideoTypes.Length > 0)
+            {
+                return false;
+            }
+
+            if (request.Years.Length > 0)
+            {
+                return false;
+            }
+
+            if (request.Tags.Length > 0)
+            {
+                return false;
+            }
+
+            if (request.OfficialRatings.Length > 0)
+            {
+                return false;
+            }
+
+            if (request.MinPlayers.HasValue)
+            {
+                return false;
+            }
+
+            if (request.MaxPlayers.HasValue)
+            {
+                return false;
+            }
+
+            if (request.MinCommunityRating.HasValue)
+            {
+                return false;
+            }
+
+            if (request.MinCriticRating.HasValue)
+            {
+                return false;
+            }
+
+            if (request.MinIndexNumber.HasValue)
+            {
+                return false;
+            }
+
+            return true;
+        }
+
+        public List<BaseItem> GetChildren(User user, bool includeLinkedChildren)
+        {
+            return GetChildren(user, includeLinkedChildren, null);
+        }
+
+        public virtual List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException();
+            }
+
+            //the true root should return our users root folder children
+            if (IsPhysicalRoot) return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren);
+
+            var result = new Dictionary<Guid, BaseItem>();
+
+            AddChildren(user, includeLinkedChildren, result, false, query);
+
+            return result.Values.ToList();
+        }
+
+        protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
+        {
+            return Children;
+        }
+
+        /// <summary>
+        /// Adds the children to list.
+        /// </summary>
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+        private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query)
+        {
+            foreach (var child in GetEligibleChildrenForRecursiveChildren(user))
+            {
+                bool? isVisibleToUser = null;
+
+                if (query == null || UserViewBuilder.FilterItem(child, query))
+                {
+                    isVisibleToUser = child.IsVisible(user);
+
+                    if (isVisibleToUser.Value)
+                    {
+                        result[child.Id] = child;
+                    }
+                }
+
+                if (isVisibleToUser ?? child.IsVisible(user))
+                {
+                    if (recursive && child.IsFolder)
+                    {
+                        var folder = (Folder)child;
+
+                        folder.AddChildren(user, includeLinkedChildren, result, true, query);
+                    }
+                }
+            }
+
+            if (includeLinkedChildren)
+            {
+                foreach (var child in GetLinkedChildren(user))
+                {
+                    if (query == null || UserViewBuilder.FilterItem(child, query))
+                    {
+                        if (child.IsVisible(user))
+                        {
+                            result[child.Id] = child;
+                        }
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets allowed recursive children of an item
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        /// <exception cref="System.ArgumentNullException"></exception>
+        public IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true)
+        {
+            return GetRecursiveChildren(user, null);
+        }
+
+        public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException("user");
+            }
+
+            var result = new Dictionary<Guid, BaseItem>();
+
+            AddChildren(user, true, result, true, query);
+
+            return result.Values;
+        }
+
+        /// <summary>
+        /// Gets the recursive children.
+        /// </summary>
+        /// <returns>IList{BaseItem}.</returns>
+        public IList<BaseItem> GetRecursiveChildren()
+        {
+            return GetRecursiveChildren(true);
+        }
+
+        public IList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren)
+        {
+            return GetRecursiveChildren(i => true, includeLinkedChildren);
+        }
+
+        public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
+        {
+            return GetRecursiveChildren(filter, true);
+        }
+
+        public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren)
+        {
+            var result = new Dictionary<Guid, BaseItem>();
+
+            AddChildrenToList(result, includeLinkedChildren, true, filter);
+
+            return result.Values.ToList();
+        }
+
+        /// <summary>
+        /// Adds the children to list.
+        /// </summary>
+        private void AddChildrenToList(Dictionary<Guid, BaseItem> result, bool includeLinkedChildren, bool recursive, Func<BaseItem, bool> filter)
+        {
+            foreach (var child in Children)
+            {
+                if (filter == null || filter(child))
+                {
+                    result[child.Id] = child;
+                }
+
+                if (recursive && child.IsFolder)
+                {
+                    var folder = (Folder)child;
+
+                    // We can only support includeLinkedChildren for the first folder, or we might end up stuck in a loop of linked items
+                    folder.AddChildrenToList(result, false, true, filter);
+                }
+            }
+
+            if (includeLinkedChildren)
+            {
+                foreach (var child in GetLinkedChildren())
+                {
+                    if (filter == null || filter(child))
+                    {
+                        result[child.Id] = child;
+                    }
+                }
+            }
+        }
+
+
+        /// <summary>
+        /// Gets the linked children.
+        /// </summary>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        public List<BaseItem> GetLinkedChildren()
+        {
+            var linkedChildren = LinkedChildren;
+            var list = new List<BaseItem>(linkedChildren.Length);
+
+            foreach (var i in linkedChildren)
+            {
+                var child = GetLinkedChild(i);
+
+                if (child != null)
+                {
+                    list.Add(child);
+                }
+            }
+            return list;
+        }
+
+        protected virtual bool FilterLinkedChildrenPerUser
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public bool ContainsLinkedChildByItemId(Guid itemId)
+        {
+            var linkedChildren = LinkedChildren;
+            foreach (var i in linkedChildren)
+            {
+                if (i.ItemId.HasValue && i.ItemId.Value == itemId)
+                {
+                    return true;
+                }
+
+                var child = GetLinkedChild(i);
+
+                if (child != null && child.Id == itemId)
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        public List<BaseItem> GetLinkedChildren(User user)
+        {
+            if (!FilterLinkedChildrenPerUser || user == null)
+            {
+                return GetLinkedChildren();
+            }
+
+            var linkedChildren = LinkedChildren;
+            var list = new List<BaseItem>(linkedChildren.Length);
+
+            if (linkedChildren.Length == 0)
+            {
+                return list;
+            }
+
+            var allUserRootChildren = LibraryManager.GetUserRootFolder()
+                .GetChildren(user, true)
+                .OfType<Folder>()
+                .ToList();
+
+            var collectionFolderIds = allUserRootChildren
+                .Select(i => i.Id)
+                .ToList();
+
+            foreach (var i in linkedChildren)
+            {
+                var child = GetLinkedChild(i);
+
+                if (child == null)
+                {
+                    continue;
+                }
+
+                var childOwner = child.GetOwner() ?? child;
+
+                if (childOwner != null && !(child is IItemByName))
+                {
+                    var childProtocol = childOwner.PathProtocol;
+                    if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File)
+                    {
+                        if (!childOwner.IsVisibleStandalone(user))
+                        {
+                            continue;
+                        }
+                    }
+                    else
+                    {
+                        var itemCollectionFolderIds =
+                            LibraryManager.GetCollectionFolders(childOwner, allUserRootChildren).Select(f => f.Id);
+
+                        if (!itemCollectionFolderIds.Any(collectionFolderIds.Contains))
+                        {
+                            continue;
+                        }
+                    }
+                }
+
+                list.Add(child);
+            }
+
+            return list;
+        }
+
+        /// <summary>
+        /// Gets the linked children.
+        /// </summary>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        public IEnumerable<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos()
+        {
+            return LinkedChildren
+                .Select(i => new Tuple<LinkedChild, BaseItem>(i, GetLinkedChild(i)))
+                .Where(i => i.Item2 != null);
+        }
+
+        [IgnoreDataMember]
+        protected override bool SupportsOwnedItems
+        {
+            get
+            {
+                return base.SupportsOwnedItems || SupportsShortcutChildren;
+            }
+        }
+
+        protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
+        {
+            var changesFound = false;
+
+            if (IsFileProtocol)
+            {
+                if (RefreshLinkedChildren(fileSystemChildren))
+                {
+                    changesFound = true;
+                }
+            }
+
+            var baseHasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+
+            return baseHasChanges || changesFound;
+        }
+
+        /// <summary>
+        /// Refreshes the linked children.
+        /// </summary>
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+        protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
+        {
+            if (SupportsShortcutChildren)
+            {
+                var newShortcutLinks = fileSystemChildren
+                    .Where(i => !i.IsDirectory && FileSystem.IsShortcut(i.FullName))
+                    .Select(i =>
+                    {
+                        try
+                        {
+                            Logger.Debug("Found shortcut at {0}", i.FullName);
+
+                            var resolvedPath = CollectionFolder.ApplicationHost.ExpandVirtualPath(FileSystem.ResolveShortcut(i.FullName));
+
+                            if (!string.IsNullOrEmpty(resolvedPath))
+                            {
+                                return new LinkedChild
+                                {
+                                    Path = resolvedPath,
+                                    Type = LinkedChildType.Shortcut
+                                };
+                            }
+
+                            Logger.Error("Error resolving shortcut {0}", i.FullName);
+
+                            return null;
+                        }
+                        catch (IOException ex)
+                        {
+                            Logger.ErrorException("Error resolving shortcut {0}", ex, i.FullName);
+                            return null;
+                        }
+                    })
+                    .Where(i => i != null)
+                    .ToList();
+
+                var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList();
+
+                if (!newShortcutLinks.SequenceEqual(currentShortcutLinks, new LinkedChildComparer(FileSystem)))
+                {
+                    Logger.Info("Shortcut links have changed for {0}", Path);
+
+                    newShortcutLinks.AddRange(LinkedChildren.Where(i => i.Type == LinkedChildType.Manual));
+                    LinkedChildren = newShortcutLinks.ToArray(newShortcutLinks.Count);
+                    return true;
+                }
+            }
+
+            foreach (var child in LinkedChildren)
+            {
+                // Reset the cached value
+                child.ItemId = null;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Marks the played.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <param name="datePlayed">The date played.</param>
+        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
+        /// <returns>Task.</returns>
+        public override void MarkPlayed(User user,
+            DateTime? datePlayed,
+            bool resetPosition)
+        {
+            var query = new InternalItemsQuery
+            {
+                User = user,
+                Recursive = true,
+                IsFolder = false,
+                EnableTotalRecordCount = false
+            };
+
+            if (!user.Configuration.DisplayMissingEpisodes)
+            {
+                query.IsVirtualItem = false;
+            }
+
+            var itemsResult = GetItemList(query);
+
+            // Sweep through recursively and update status
+            foreach (var item in itemsResult)
+            {
+                if (item.IsVirtualItem)
+                {
+                    // The querying doesn't support virtual unaired
+                    var episode = item as Episode;
+                    if (episode != null && episode.IsUnaired)
+                    {
+                        continue;
+                    }
+                }
+
+                item.MarkPlayed(user, datePlayed, resetPosition);
+            }
+        }
+
+        /// <summary>
+        /// Marks the unplayed.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <returns>Task.</returns>
+        public override void MarkUnplayed(User user)
+        {
+            var itemsResult = GetItemList(new InternalItemsQuery
+            {
+                User = user,
+                Recursive = true,
+                IsFolder = false,
+                EnableTotalRecordCount = false
+
+            });
+
+            // Sweep through recursively and update status
+            foreach (var item in itemsResult)
+            {
+                item.MarkUnplayed(user);
+            }
+        }
+
+        public override bool IsPlayed(User user)
+        {
+            var itemsResult = GetItemList(new InternalItemsQuery(user)
+            {
+                Recursive = true,
+                IsFolder = false,
+                IsVirtualItem = false,
+                EnableTotalRecordCount = false
+
+            });
+
+            return itemsResult
+                .All(i => i.IsPlayed(user));
+        }
+
+        public override bool IsUnplayed(User user)
+        {
+            return !IsPlayed(user);
+        }
+
+        [IgnoreDataMember]
+        public virtual bool SupportsUserDataFromChildren
+        {
+            get
+            {
+                // These are just far too slow. 
+                if (this is ICollectionFolder)
+                {
+                    return false;
+                }
+                if (this is UserView)
+                {
+                    return false;
+                }
+                if (this is UserRootFolder)
+                {
+                    return false;
+                }
+                if (this is Channel)
+                {
+                    return false;
+                }
+                if (SourceType != SourceType.Library)
+                {
+                    return false;
+                }
+                var iItemByName = this as IItemByName;
+                if (iItemByName != null)
+                {
+                    var hasDualAccess = this as IHasDualAccess;
+                    if (hasDualAccess == null || hasDualAccess.IsAccessedByName)
+                    {
+                        return false;
+                    }
+                }
+
+                return true;
+            }
+        }
+
+        public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields)
+        {
+            if (!SupportsUserDataFromChildren)
+            {
+                return;
+            }
+
+            if (itemDto != null)
+            {
+                if (fields.ContainsField(ItemFields.RecursiveItemCount))
+                {
+                    itemDto.RecursiveItemCount = GetRecursiveChildCount(user);
+                }
+            }
+
+            if (SupportsPlayedStatus)
+            {
+                var unplayedQueryResult = GetItems(new InternalItemsQuery(user)
+                {
+                    Recursive = true,
+                    IsFolder = false,
+                    IsVirtualItem = false,
+                    EnableTotalRecordCount = true,
+                    Limit = 0,
+                    IsPlayed = false,
+                    DtoOptions = new DtoOptions(false)
+                    {
+                        EnableImages = false
+                    }
+
+                });
+
+                double unplayedCount = unplayedQueryResult.TotalRecordCount;
+
+                dto.UnplayedItemCount = unplayedQueryResult.TotalRecordCount;
+
+                if (itemDto != null && itemDto.RecursiveItemCount.HasValue)
+                {
+                    if (itemDto.RecursiveItemCount.Value > 0)
+                    {
+                        var unplayedPercentage = (unplayedCount / itemDto.RecursiveItemCount.Value) * 100;
+                        dto.PlayedPercentage = 100 - unplayedPercentage;
+                        dto.Played = dto.PlayedPercentage.Value >= 100;
+                    }
+                }
+                else
+                {
+                    dto.Played = (dto.UnplayedItemCount ?? 0) == 0;
+                }
+            }
+        }
+    }
+}

+ 129 - 0
MediaBrowser.Controller/Entities/Game.cs

@@ -0,0 +1,129 @@
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public class Game : BaseItem, IHasTrailers, IHasScreenshots, ISupportsPlaceHolders, IHasLookupInfo<GameInfo>
+    {
+        public Game()
+        {
+            MultiPartGameFiles = new string[] {};
+            RemoteTrailers = EmptyMediaUrlArray;
+            LocalTrailerIds = new Guid[] {};
+            RemoteTrailerIds = new Guid[] {};
+        }
+
+        public Guid[] LocalTrailerIds { get; set; }
+        public Guid[] RemoteTrailerIds { get; set; }
+
+        public override bool CanDownload()
+        {
+            return IsFileProtocol;
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsThemeMedia
+        {
+            get { return true; }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPeople
+        {
+            get { return false; }
+        }
+
+        /// <summary>
+        /// Gets or sets the remote trailers.
+        /// </summary>
+        /// <value>The remote trailers.</value>
+        public MediaUrl[] RemoteTrailers { get; set; }
+
+        /// <summary>
+        /// Gets the type of the media.
+        /// </summary>
+        /// <value>The type of the media.</value>
+        [IgnoreDataMember]
+        public override string MediaType
+        {
+            get { return Model.Entities.MediaType.Game; }
+        }
+
+        /// <summary>
+        /// Gets or sets the players supported.
+        /// </summary>
+        /// <value>The players supported.</value>
+        public int? PlayersSupported { get; set; }
+
+        /// <summary>
+        /// Gets a value indicating whether this instance is place holder.
+        /// </summary>
+        /// <value><c>true</c> if this instance is place holder; otherwise, <c>false</c>.</value>
+        public bool IsPlaceHolder { get; set; }
+
+        /// <summary>
+        /// Gets or sets the game system.
+        /// </summary>
+        /// <value>The game system.</value>
+        public string GameSystem { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance is multi part.
+        /// </summary>
+        /// <value><c>true</c> if this instance is multi part; otherwise, <c>false</c>.</value>
+        public bool IsMultiPart { get; set; }
+
+        /// <summary>
+        /// Holds the paths to the game files in the event this is a multipart game
+        /// </summary>
+        public string[] MultiPartGameFiles { get; set; }
+
+        public override List<string> GetUserDataKeys()
+        {
+            var list = base.GetUserDataKeys();
+            var id = this.GetProviderId(MetadataProviders.Gamesdb);
+
+            if (!string.IsNullOrEmpty(id))
+            {
+                list.Insert(0, "Game-Gamesdb-" + id);
+            }
+            return list;
+        }
+
+        public override IEnumerable<FileSystemMetadata> GetDeletePaths()
+        {
+            if (!IsInMixedFolder)
+            {
+                return new[] {
+                    new FileSystemMetadata
+                    {
+                        FullName = FileSystem.GetDirectoryName(Path),
+                        IsDirectory = true
+                    }
+                };
+            }
+
+            return base.GetDeletePaths();
+        }
+
+        public override UnratedItem GetBlockUnratedType()
+        {
+            return UnratedItem.Game;
+        }
+
+        public GameInfo GetLookupInfo()
+        {
+            var id = GetItemLookupInfo<GameInfo>();
+
+            id.GameSystem = GameSystem;
+
+            return id;
+        }
+    }
+}

+ 128 - 0
MediaBrowser.Controller/Entities/GameGenre.cs

@@ -0,0 +1,128 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public class GameGenre : BaseItem, IItemByName
+    {
+        public override List<string> GetUserDataKeys()
+        {
+            var list = base.GetUserDataKeys();
+
+            list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
+            return list;
+        }
+
+        public override string CreatePresentationUniqueKey()
+        {
+            return GetUserDataKeys()[0];
+        }
+
+        public override double GetDefaultPrimaryImageAspectRatio()
+        {
+            return 1;
+        }
+
+        /// <summary>
+        /// Returns the folder containing the item.
+        /// If the item is a folder, it returns the folder itself
+        /// </summary>
+        /// <value>The containing folder path.</value>
+        [IgnoreDataMember]
+        public override string ContainingFolderPath
+        {
+            get
+            {
+                return Path;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsAncestors
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public override bool IsSaveLocalMetadataEnabled()
+        {
+            return true;
+        }
+
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
+        public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+        {
+            query.GenreIds = new[] { Id };
+            query.IncludeItemTypes = new[] { typeof(Game).Name };
+
+            return LibraryManager.GetItemList(query);
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPeople
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public static string GetPath(string name)
+        {
+            return GetPath(name, true);
+        }
+
+        public static string GetPath(string name, bool normalizeName)
+        {
+            // Trim the period at the end because windows will have a hard time with that
+            var validName = normalizeName ?
+                FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+                name;
+
+            return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.GameGenrePath, validName);
+        }
+
+        private string GetRebasedPath()
+        {
+            return GetPath(System.IO.Path.GetFileName(Path), false);
+        }
+
+        public override bool RequiresRefresh()
+        {
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+                return true;
+            }
+            return base.RequiresRefresh();
+        }
+
+        /// <summary>
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
+        /// </summary>
+        public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+        {
+            var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Path = newPath;
+                hasChanges = true;
+            }
+
+            return hasChanges;
+        }
+    }
+}

+ 101 - 0
MediaBrowser.Controller/Entities/GameSystem.cs

@@ -0,0 +1,101 @@
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Users;
+
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// Class GameSystem
+    /// </summary>
+    public class GameSystem : Folder, IHasLookupInfo<GameSystemInfo>
+    {
+        /// <summary>
+        /// Return the id that should be used to key display prefs for this item.
+        /// Default is based on the type for everything except actual generic folders.
+        /// </summary>
+        /// <value>The display prefs id.</value>
+        [IgnoreDataMember]
+        public override Guid DisplayPreferencesId
+        {
+            get
+            {
+                return Id;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPlayedStatus
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsInheritedParentImages
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public override double GetDefaultPrimaryImageAspectRatio()
+        {
+            double value = 16;
+            value /= 9;
+
+            return value;
+        }
+
+        /// <summary>
+        /// Gets or sets the game system.
+        /// </summary>
+        /// <value>The game system.</value>
+        public string GameSystemName { get; set; }
+
+        public override List<string> GetUserDataKeys()
+        {
+            var list = base.GetUserDataKeys();
+
+            if (!string.IsNullOrEmpty(GameSystemName))
+            {
+                list.Insert(0, "GameSystem-" + GameSystemName);
+            }
+            return list;
+        }
+
+        protected override bool GetBlockUnratedValue(UserPolicy config)
+        {
+            // Don't block. Determine by game
+            return false;
+        }
+
+        public override UnratedItem GetBlockUnratedType()
+        {
+            return UnratedItem.Game;
+        }
+
+        public GameSystemInfo GetLookupInfo()
+        {
+            var id = GetItemLookupInfo<GameSystemInfo>();
+
+            id.Path = Path;
+
+            return id;
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPeople
+        {
+            get
+            {
+                return false;
+            }
+        }
+    }
+}

+ 140 - 0
MediaBrowser.Controller/Entities/Genre.cs

@@ -0,0 +1,140 @@
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Controller.Entities.Audio;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// Class Genre
+    /// </summary>
+    public class Genre : BaseItem, IItemByName
+    {
+        public override List<string> GetUserDataKeys()
+        {
+            var list = base.GetUserDataKeys();
+
+            list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
+            return list;
+        }
+        public override string CreatePresentationUniqueKey()
+        {
+            return GetUserDataKeys()[0];
+        }
+
+        public override double GetDefaultPrimaryImageAspectRatio()
+        {
+            return 1;
+        }
+
+        /// <summary>
+        /// Returns the folder containing the item.
+        /// If the item is a folder, it returns the folder itself
+        /// </summary>
+        /// <value>The containing folder path.</value>
+        [IgnoreDataMember]
+        public override string ContainingFolderPath
+        {
+            get
+            {
+                return Path;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool IsDisplayedAsFolder
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsAncestors
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public override bool IsSaveLocalMetadataEnabled()
+        {
+            return true;
+        }
+
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
+        public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+        {
+            query.GenreIds = new[] { Id };
+            query.ExcludeItemTypes = new[] { typeof(Game).Name, typeof(MusicVideo).Name, typeof(Audio.Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name };
+
+            return LibraryManager.GetItemList(query);
+        }
+
+        [IgnoreDataMember]
+        public override bool SupportsPeople
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public static string GetPath(string name)
+        {
+            return GetPath(name, true);
+        }
+
+        public static string GetPath(string name, bool normalizeName)
+        {
+            // Trim the period at the end because windows will have a hard time with that
+            var validName = normalizeName ?
+                FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+                name;
+
+            return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.GenrePath, validName);
+        }
+
+        private string GetRebasedPath()
+        {
+            return GetPath(System.IO.Path.GetFileName(Path), false);
+        }
+
+        public override bool RequiresRefresh()
+        {
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+                return true;
+            }
+            return base.RequiresRefresh();
+        }
+
+        /// <summary>
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
+        /// </summary>
+        public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+        {
+            var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Path = newPath;
+                hasChanges = true;
+            }
+
+            return hasChanges;
+        }
+    }
+}

+ 27 - 0
MediaBrowser.Controller/Entities/ICollectionFolder.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// This is just a marker interface to denote top level folders
+    /// </summary>
+    public interface ICollectionFolder : IHasCollectionType
+    {
+        string Path { get; }
+        string Name { get; }
+        Guid Id { get; }
+        string[] PhysicalLocations { get; }
+    }
+
+    public interface ISupportsUserSpecificView
+    {
+        bool EnableUserSpecificView { get; }
+    }
+
+    public interface IHasCollectionType
+    {
+        string CollectionType { get; }
+    }
+}

+ 14 - 0
MediaBrowser.Controller/Entities/IHasAspectRatio.cs

@@ -0,0 +1,14 @@
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// Interface IHasAspectRatio
+    /// </summary>
+    public interface IHasAspectRatio
+    {
+        /// <summary>
+        /// Gets or sets the aspect ratio.
+        /// </summary>
+        /// <value>The aspect ratio.</value>
+        string AspectRatio { get; set; }
+    }
+}

+ 15 - 0
MediaBrowser.Controller/Entities/IHasDisplayOrder.cs

@@ -0,0 +1,15 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// Interface IHasDisplayOrder
+    /// </summary>
+    public interface IHasDisplayOrder
+    {
+        /// <summary>
+        /// Gets or sets the display order.
+        /// </summary>
+        /// <value>The display order.</value>
+        string DisplayOrder { get; set; }
+    }
+}

+ 19 - 0
MediaBrowser.Controller/Entities/IHasMediaSources.cs

@@ -0,0 +1,19 @@
+using MediaBrowser.Model.Dto;
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public interface IHasMediaSources
+    {
+        /// <summary>
+        /// Gets the media sources.
+        /// </summary>
+        List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution);
+        List<MediaStream> GetMediaStreams();
+        Guid Id { get; set; }
+        long? RunTimeTicks { get; set; }
+        string Path { get; }
+    }
+}

+ 17 - 0
MediaBrowser.Controller/Entities/IHasProgramAttributes.cs

@@ -0,0 +1,17 @@
+using MediaBrowser.Model.LiveTv;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public interface IHasProgramAttributes
+    {
+        bool IsMovie { get; set; }
+        bool IsSports { get; }
+        bool IsNews { get; }
+        bool IsKids { get; }
+        bool IsRepeat { get; set; }
+        bool IsSeries { get; set; }
+        ProgramAudio? Audio { get; set; }
+        string EpisodeTitle { get; set; }
+        string ServiceName { get; set; }
+    }
+}

+ 10 - 0
MediaBrowser.Controller/Entities/IHasScreenshots.cs

@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// Interface IHasScreenshots
+    /// </summary>
+    public interface IHasScreenshots
+    {
+    }
+}

+ 20 - 0
MediaBrowser.Controller/Entities/IHasSeries.cs

@@ -0,0 +1,20 @@
+
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public interface IHasSeries
+    {
+        /// <summary>
+        /// Gets the name of the series.
+        /// </summary>
+        /// <value>The name of the series.</value>
+        string SeriesName { get; set; }
+        string FindSeriesName();
+        string FindSeriesSortName();
+        Guid SeriesId { get; set; }
+        Guid FindSeriesId();
+        string SeriesPresentationUniqueKey { get; set; }
+        string FindSeriesPresentationUniqueKey();
+    }
+}

+ 13 - 0
MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs

@@ -0,0 +1,13 @@
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public interface IHasSpecialFeatures
+    {
+        /// <summary>
+        /// Gets or sets the special feature ids.
+        /// </summary>
+        /// <value>The special feature ids.</value>
+        Guid[] SpecialFeatureIds { get; set; }
+    }
+}

+ 9 - 0
MediaBrowser.Controller/Entities/IHasStartDate.cs

@@ -0,0 +1,9 @@
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public interface IHasStartDate
+    {
+        DateTime StartDate { get; set; }
+    }
+}

+ 39 - 0
MediaBrowser.Controller/Entities/IHasTrailers.cs

@@ -0,0 +1,39 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public interface IHasTrailers : IHasProviderIds
+    {
+        /// <summary>
+        /// Gets or sets the remote trailers.
+        /// </summary>
+        /// <value>The remote trailers.</value>
+        MediaUrl[] RemoteTrailers { get; set; }
+
+        /// <summary>
+        /// Gets or sets the local trailer ids.
+        /// </summary>
+        /// <value>The local trailer ids.</value>
+        Guid[] LocalTrailerIds { get; set; }
+        Guid[] RemoteTrailerIds { get; set; }
+        Guid Id { get; set; }
+    }
+
+    public static class HasTrailerExtensions
+    {
+        /// <summary>
+        /// Gets the trailer ids.
+        /// </summary>
+        /// <returns>List&lt;Guid&gt;.</returns>
+        public static List<Guid> GetTrailerIds(this IHasTrailers item)
+        {
+            var list = item.LocalTrailerIds.ToList();
+            list.AddRange(item.RemoteTrailerIds);
+            return list;
+        }
+
+    }
+}

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff