瀏覽代碼

Merge pull request #1222 from MediaBrowser/dev

3.0.5768.7
Luke 9 年之前
父節點
當前提交
35778ebc02
共有 100 個文件被更改,包括 1262 次插入1386 次删除
  1. 2 1
      Emby.Drawing/Common/ImageHeader.cs
  2. 18 2
      Emby.Drawing/Emby.Drawing.csproj
  3. 1 0
      Emby.Drawing/GDI/DynamicImageHelpers.cs
  4. 28 5
      Emby.Drawing/GDI/GDIImageEncoder.cs
  5. 二進制
      Emby.Drawing/GDI/empty.png
  6. 12 6
      Emby.Drawing/IImageEncoder.cs
  7. 35 11
      Emby.Drawing/ImageMagick/ImageMagickEncoder.cs
  8. 16 12
      Emby.Drawing/ImageMagick/PlayedIndicatorDrawer.cs
  9. 11 7
      Emby.Drawing/ImageMagick/StripCollageBuilder.cs
  10. 6 2
      Emby.Drawing/ImageMagick/UnplayedCountIndicator.cs
  11. 69 46
      Emby.Drawing/ImageProcessor.cs
  12. 64 0
      Emby.Drawing/NullImageEncoder.cs
  13. 2 0
      Emby.Drawing/packages.config
  14. 4 3
      MediaBrowser.Api/ApiEntryPoint.cs
  15. 0 100
      MediaBrowser.Api/AppThemeService.cs
  16. 1 0
      MediaBrowser.Api/ConfigurationService.cs
  17. 0 1
      MediaBrowser.Api/Dlna/DlnaServerService.cs
  18. 7 6
      MediaBrowser.Api/EnvironmentService.cs
  19. 11 11
      MediaBrowser.Api/Images/ImageByNameService.cs
  20. 55 22
      MediaBrowser.Api/Images/ImageService.cs
  21. 4 3
      MediaBrowser.Api/Images/RemoteImageService.cs
  22. 5 4
      MediaBrowser.Api/ItemLookupService.cs
  23. 6 2
      MediaBrowser.Api/ItemRefreshService.cs
  24. 2 12
      MediaBrowser.Api/ItemUpdateService.cs
  25. 5 6
      MediaBrowser.Api/Library/LibraryHelpers.cs
  26. 7 3
      MediaBrowser.Api/Library/LibraryService.cs
  27. 36 9
      MediaBrowser.Api/Library/LibraryStructureService.cs
  28. 2 2
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  29. 10 9
      MediaBrowser.Api/MediaBrowser.Api.csproj
  30. 64 49
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  31. 9 9
      MediaBrowser.Api/Playback/Dash/MpegDashService.cs
  32. 7 5
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  33. 9 9
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  34. 1 0
      MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
  35. 11 5
      MediaBrowser.Api/Playback/MediaInfoService.cs
  36. 1 0
      MediaBrowser.Api/Playback/Progressive/AudioService.cs
  37. 3 2
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  38. 1 0
      MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
  39. 2 0
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  40. 12 1
      MediaBrowser.Api/Playback/StreamState.cs
  41. 25 1
      MediaBrowser.Api/PlaylistService.cs
  42. 18 0
      MediaBrowser.Api/PluginService.cs
  43. 0 1
      MediaBrowser.Api/Reports/Common/HeaderMetadata.cs
  44. 0 1
      MediaBrowser.Api/Reports/Common/ItemViewType.cs
  45. 0 19
      MediaBrowser.Api/Reports/Data/ReportBuilder.cs
  46. 0 4
      MediaBrowser.Api/Reports/Model/ReportRow.cs
  47. 0 1
      MediaBrowser.Api/Reports/ReportsService.cs
  48. 1 1
      MediaBrowser.Api/Reports/Stat/ReportStatBuilder.cs
  49. 16 2
      MediaBrowser.Api/Social/SharingService.cs
  50. 2 1
      MediaBrowser.Api/StartupWizardService.cs
  51. 6 2
      MediaBrowser.Api/Subtitles/SubtitleService.cs
  52. 9 1
      MediaBrowser.Api/Sync/SyncService.cs
  53. 5 6
      MediaBrowser.Api/System/SystemService.cs
  54. 0 3
      MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
  55. 0 1
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  56. 34 0
      MediaBrowser.Api/UserLibrary/UserViewsService.cs
  57. 1 0
      MediaBrowser.Api/VideosService.cs
  58. 3 1
      MediaBrowser.Api/packages.config
  59. 14 5
      MediaBrowser.Common.Implementations/Archiving/ZipClient.cs
  60. 19 8
      MediaBrowser.Common.Implementations/BaseApplicationHost.cs
  61. 28 8
      MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs
  62. 4 3
      MediaBrowser.Common.Implementations/Configuration/ConfigurationHelper.cs
  63. 13 5
      MediaBrowser.Common.Implementations/Devices/DeviceId.cs
  64. 76 69
      MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs
  65. 0 433
      MediaBrowser.Common.Implementations/IO/CommonFileSystem.cs
  66. 2 2
      MediaBrowser.Common.Implementations/Logging/NlogManager.cs
  67. 11 7
      MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj
  68. 15 7
      MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  69. 48 7
      MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs
  70. 6 5
      MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
  71. 2 1
      MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
  72. 4 4
      MediaBrowser.Common.Implementations/Security/MBLicenseFile.cs
  73. 67 1
      MediaBrowser.Common.Implementations/Security/PluginSecurityManager.cs
  74. 1 0
      MediaBrowser.Common.Implementations/Security/RegRecord.cs
  75. 8 3
      MediaBrowser.Common.Implementations/Serialization/JsonSerializer.cs
  76. 10 1
      MediaBrowser.Common.Implementations/Serialization/XmlSerializer.cs
  77. 5 4
      MediaBrowser.Common.Implementations/Updates/InstallationManager.cs
  78. 4 2
      MediaBrowser.Common.Implementations/packages.config
  79. 0 165
      MediaBrowser.Common/IO/IFileSystem.cs
  80. 0 1
      MediaBrowser.Common/MediaBrowser.Common.csproj
  81. 2 0
      MediaBrowser.Common/Net/HttpRequestOptions.cs
  82. 5 0
      MediaBrowser.Common/ScheduledTasks/IScheduledTaskWorker.cs
  83. 3 3
      MediaBrowser.Common/ScheduledTasks/IntervalTrigger.cs
  84. 7 0
      MediaBrowser.Common/Security/ISecurityManager.cs
  85. 0 2
      MediaBrowser.Controller/Channels/Channel.cs
  86. 3 10
      MediaBrowser.Controller/Channels/ChannelAudioItem.cs
  87. 3 7
      MediaBrowser.Controller/Channels/ChannelFolderItem.cs
  88. 8 11
      MediaBrowser.Controller/Channels/ChannelVideoItem.cs
  89. 0 14
      MediaBrowser.Controller/Channels/IChannelFactory.cs
  90. 0 6
      MediaBrowser.Controller/Channels/IChannelItem.cs
  91. 1 1
      MediaBrowser.Controller/Channels/IChannelManager.cs
  92. 1 1
      MediaBrowser.Controller/Channels/IChannelMediaItem.cs
  93. 11 5
      MediaBrowser.Controller/Drawing/IImageProcessor.cs
  94. 5 3
      MediaBrowser.Controller/Entities/AggregateFolder.cs
  95. 10 11
      MediaBrowser.Controller/Entities/Audio/Audio.cs
  96. 140 102
      MediaBrowser.Controller/Entities/BaseItem.cs
  97. 1 9
      MediaBrowser.Controller/Entities/Book.cs
  98. 5 3
      MediaBrowser.Controller/Entities/CollectionFolder.cs
  99. 60 38
      MediaBrowser.Controller/Entities/Folder.cs
  100. 1 9
      MediaBrowser.Controller/Entities/Game.cs

+ 2 - 1
Emby.Drawing/Common/ImageHeader.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using CommonIO;
 
 namespace Emby.Drawing.Common
 {
@@ -220,4 +221,4 @@ namespace Emby.Drawing.Common
             throw new ArgumentException(ErrorMessage);
         }
     }
-}
+}

+ 18 - 2
Emby.Drawing/Emby.Drawing.csproj

@@ -12,7 +12,6 @@
     <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
     <FileAlignment>512</FileAlignment>
     <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
-    <RestorePackages>true</RestorePackages>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
     <DebugSymbols>true</DebugSymbols>
@@ -32,10 +31,20 @@
     <WarningLevel>4</WarningLevel>
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="CommonIO, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\CommonIO.1.0.0.5\lib\net45\CommonIO.dll</HintPath>
+    </Reference>
     <Reference Include="ImageMagickSharp, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
       <HintPath>..\packages\ImageMagickSharp.1.0.0.16\lib\net45\ImageMagickSharp.dll</HintPath>
     </Reference>
+    <Reference Include="Patterns.Logging">
+      <HintPath>..\packages\Patterns.Logging.1.0.0.2\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.Logging.dll</HintPath>
+    </Reference>
+    <Reference Include="policy.2.0.taglib-sharp">
+      <HintPath>..\packages\taglib.2.1.0.0\lib\policy.2.0.taglib-sharp.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
     <Reference Include="System.Drawing" />
@@ -44,11 +53,15 @@
     <Reference Include="Microsoft.CSharp" />
     <Reference Include="System.Data" />
     <Reference Include="System.Xml" />
+    <Reference Include="taglib-sharp">
+      <HintPath>..\packages\taglib.2.1.0.0\lib\taglib-sharp.dll</HintPath>
+    </Reference>
   </ItemGroup>
   <ItemGroup>
     <Compile Include="..\SharedVersion.cs">
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
+    <Compile Include="Common\ImageHeader.cs" />
     <Compile Include="GDI\DynamicImageHelpers.cs" />
     <Compile Include="GDI\GDIImageEncoder.cs" />
     <Compile Include="GDI\ImageExtensions.cs" />
@@ -56,13 +69,13 @@
     <Compile Include="GDI\PlayedIndicatorDrawer.cs" />
     <Compile Include="GDI\UnplayedCountIndicator.cs" />
     <Compile Include="IImageEncoder.cs" />
-    <Compile Include="Common\ImageHeader.cs" />
     <Compile Include="ImageHelpers.cs" />
     <Compile Include="ImageMagick\ImageMagickEncoder.cs" />
     <Compile Include="ImageMagick\StripCollageBuilder.cs" />
     <Compile Include="ImageProcessor.cs" />
     <Compile Include="ImageMagick\PercentPlayedDrawer.cs" />
     <Compile Include="ImageMagick\PlayedIndicatorDrawer.cs" />
+    <Compile Include="NullImageEncoder.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="ImageMagick\UnplayedCountIndicator.cs" />
   </ItemGroup>
@@ -87,6 +100,9 @@
   <ItemGroup>
     <None Include="packages.config" />
   </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="GDI\empty.png" />
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.

+ 1 - 0
Emby.Drawing/GDI/DynamicImageHelpers.cs

@@ -4,6 +4,7 @@ using System.Drawing;
 using System.Drawing.Drawing2D;
 using System.Drawing.Imaging;
 using System.IO;
+using CommonIO;
 
 namespace Emby.Drawing.GDI
 {

+ 28 - 5
Emby.Drawing/GDI/GDIImageEncoder.cs

@@ -1,5 +1,4 @@
-using MediaBrowser.Common.IO;
-using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Logging;
 using System;
@@ -8,6 +7,7 @@ using System.Drawing.Drawing2D;
 using System.Drawing.Imaging;
 using System.IO;
 using System.Linq;
+using CommonIO;
 using ImageFormat = MediaBrowser.Model.Drawing.ImageFormat;
 
 namespace Emby.Drawing.GDI
@@ -22,7 +22,20 @@ namespace Emby.Drawing.GDI
             _fileSystem = fileSystem;
             _logger = logger;
 
-            _logger.Info("GDI image processor initialized");
+            LogInfo();
+        }
+
+        private void LogInfo()
+        {
+            _logger.Info("GDIImageEncoder starting");
+            using (var stream = GetType().Assembly.GetManifestResourceStream(GetType().Namespace + ".empty.png"))
+            {
+                using (var img = Image.FromStream(stream))
+                {
+                    
+                }
+            }
+            _logger.Info("GDIImageEncoder started");
         }
 
         public string[] SupportedInputFormats
@@ -66,7 +79,7 @@ namespace Emby.Drawing.GDI
             {
                 using (var croppedImage = image.CropWhitespace())
                 {
-                    Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+					_fileSystem.CreateDirectory(Path.GetDirectoryName(outputPath));
 
                     using (var outputStream = _fileSystem.GetFileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.Read, false))
                     {
@@ -120,7 +133,7 @@ namespace Emby.Drawing.GDI
 
                         var outputFormat = GetOutputFormat(originalImage, selectedOutputFormat);
 
-                        Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
+						_fileSystem.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
 
                         // Save to the cache location
                         using (var cacheFileStream = _fileSystem.GetFileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, false))
@@ -252,5 +265,15 @@ namespace Emby.Drawing.GDI
         {
             get { return "GDI"; }
         }
+
+        public bool SupportsImageCollageCreation
+        {
+            get { return true; }
+        }
+
+        public bool SupportsImageEncoding
+        {
+            get { return true; }
+        }
     }
 }

二進制
Emby.Drawing/GDI/empty.png


+ 12 - 6
Emby.Drawing/IImageEncoder.cs

@@ -17,12 +17,6 @@ namespace Emby.Drawing
         /// <value>The supported output formats.</value>
         ImageFormat[] SupportedOutputFormats { get; }
         /// <summary>
-        /// Gets the size of the image.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns>ImageSize.</returns>
-        ImageSize GetImageSize(string path);
-        /// <summary>
         /// Crops the white space.
         /// </summary>
         /// <param name="inputPath">The input path.</param>
@@ -49,5 +43,17 @@ namespace Emby.Drawing
         /// </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; }
     }
 }

+ 35 - 11
Emby.Drawing/ImageMagick/ImageMagickEncoder.cs

@@ -8,6 +8,7 @@ using MediaBrowser.Model.Logging;
 using System;
 using System.IO;
 using System.Linq;
+using CommonIO;
 
 namespace Emby.Drawing.ImageMagick
 {
@@ -16,14 +17,16 @@ namespace Emby.Drawing.ImageMagick
         private readonly ILogger _logger;
         private readonly IApplicationPaths _appPaths;
         private readonly IHttpClient _httpClient;
+        private readonly IFileSystem _fileSystem;
 
-        public ImageMagickEncoder(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient)
+        public ImageMagickEncoder(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient, IFileSystem fileSystem)
         {
             _logger = logger;
             _appPaths = appPaths;
             _httpClient = httpClient;
+            _fileSystem = fileSystem;
 
-            LogImageMagickVersion();
+            LogVersion();
         }
 
         public string[] SupportedInputFormats
@@ -64,7 +67,7 @@ namespace Emby.Drawing.ImageMagick
             }
         }
 
-        private void LogImageMagickVersion()
+        private void LogVersion()
         {
             _logger.Info("ImageMagick version: " + Wand.VersionString);
             TestWebp();
@@ -77,16 +80,16 @@ namespace Emby.Drawing.ImageMagick
             try
             {
                 var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".webp");
-                Directory.CreateDirectory(Path.GetDirectoryName(tmpPath));
+                _fileSystem.CreateDirectory(Path.GetDirectoryName(tmpPath));
 
                 using (var wand = new MagickWand(1, 1, new PixelWand("none", 1)))
                 {
                     wand.SaveImage(tmpPath);
                 }
             }
-            catch (Exception ex)
+            catch 
             {
-                _logger.ErrorException("Error loading webp: ", ex);
+                //_logger.ErrorException("Error loading webp: ", ex);
                 _webpAvailable = false;
             }
         }
@@ -100,6 +103,7 @@ namespace Emby.Drawing.ImageMagick
                 wand.CurrentImage.TrimImage(10);
                 wand.SaveImage(outputPath);
             }
+            SaveDelay();
         }
 
         public ImageSize GetImageSize(string path)
@@ -159,6 +163,7 @@ namespace Emby.Drawing.ImageMagick
                     }
                 }
             }
+            SaveDelay();
         }
 
         /// <summary>
@@ -181,14 +186,14 @@ namespace Emby.Drawing.ImageMagick
                 {
                     var currentImageSize = new ImageSize(imageWidth, imageHeight);
 
-                    var task = new PlayedIndicatorDrawer(_appPaths, _httpClient).DrawPlayedIndicator(wand, currentImageSize);
+                    var task = new PlayedIndicatorDrawer(_appPaths, _httpClient, _fileSystem).DrawPlayedIndicator(wand, currentImageSize);
                     Task.WaitAll(task);
                 }
                 else if (options.UnplayedCount.HasValue)
                 {
                     var currentImageSize = new ImageSize(imageWidth, imageHeight);
 
-                    new UnplayedCountIndicator(_appPaths).DrawUnplayedCountIndicator(wand, currentImageSize, options.UnplayedCount.Value);
+                    new UnplayedCountIndicator(_appPaths, _fileSystem).DrawUnplayedCountIndicator(wand, currentImageSize, options.UnplayedCount.Value);
                 }
 
                 if (options.PercentPlayed > 0)
@@ -209,16 +214,25 @@ namespace Emby.Drawing.ImageMagick
 
             if (ratio >= 1.4)
             {
-                new StripCollageBuilder(_appPaths).BuildThumbCollage(options.InputPaths.ToList(), options.OutputPath, options.Width, options.Height, options.Text);
+                new StripCollageBuilder(_appPaths, _fileSystem).BuildThumbCollage(options.InputPaths.ToList(), options.OutputPath, options.Width, options.Height, options.Text);
             }
             else if (ratio >= .9)
             {
-                new StripCollageBuilder(_appPaths).BuildSquareCollage(options.InputPaths.ToList(), options.OutputPath, options.Width, options.Height, options.Text);
+                new StripCollageBuilder(_appPaths, _fileSystem).BuildSquareCollage(options.InputPaths.ToList(), options.OutputPath, options.Width, options.Height, options.Text);
             }
             else
             {
-                new StripCollageBuilder(_appPaths).BuildPosterCollage(options.InputPaths.ToList(), options.OutputPath, options.Width, options.Height, options.Text);
+                new StripCollageBuilder(_appPaths, _fileSystem).BuildPosterCollage(options.InputPaths.ToList(), options.OutputPath, options.Width, options.Height, options.Text);
             }
+
+            SaveDelay();
+        }
+
+        private void SaveDelay()
+        {
+            // For some reason the images are not always getting released right away
+            var task = Task.Delay(300);
+            Task.WaitAll(task);
         }
 
         public string Name
@@ -240,5 +254,15 @@ namespace Emby.Drawing.ImageMagick
                 throw new ObjectDisposedException(GetType().Name);
             }
         }
+
+        public bool SupportsImageCollageCreation
+        {
+            get { return true; }
+        }
+
+        public bool SupportsImageEncoding
+        {
+            get { return true; }
+        }
     }
 }

+ 16 - 12
Emby.Drawing/ImageMagick/PlayedIndicatorDrawer.cs

@@ -5,6 +5,8 @@ using MediaBrowser.Model.Drawing;
 using System;
 using System.IO;
 using System.Threading.Tasks;
+using CommonIO;
+using MediaBrowser.Common.IO;
 
 namespace Emby.Drawing.ImageMagick
 {
@@ -14,12 +16,14 @@ namespace Emby.Drawing.ImageMagick
         private const int OffsetFromTopRightCorner = 38;
 
         private readonly IApplicationPaths _appPaths;
-        private readonly IHttpClient _iHttpClient;
+		private readonly IHttpClient _iHttpClient;
+		private readonly IFileSystem _fileSystem;
 
-        public PlayedIndicatorDrawer(IApplicationPaths appPaths, IHttpClient iHttpClient)
+		public PlayedIndicatorDrawer(IApplicationPaths appPaths, IHttpClient iHttpClient, IFileSystem fileSystem)
         {
             _appPaths = appPaths;
             _iHttpClient = iHttpClient;
+			_fileSystem = fileSystem;
         }
 
         public async Task DrawPlayedIndicator(MagickWand wand, ImageSize imageSize)
@@ -38,7 +42,7 @@ namespace Emby.Drawing.ImageMagick
                     pixel.Opacity = 0;
                     pixel.Color = "white";
                     draw.FillColor = pixel;
-                    draw.Font = await DownloadFont("webdings.ttf", "https://github.com/MediaBrowser/Emby.Resources/raw/master/fonts/webdings.ttf", _appPaths, _iHttpClient).ConfigureAwait(false);
+                    draw.Font = await DownloadFont("webdings.ttf", "https://github.com/MediaBrowser/Emby.Resources/raw/master/fonts/webdings.ttf", _appPaths, _iHttpClient, _fileSystem).ConfigureAwait(false);
                     draw.FontSize = FontSize;
                     draw.FontStyle = FontStyleType.NormalStyle;
                     draw.TextAlignment = TextAlignType.CenterAlign;
@@ -52,18 +56,18 @@ namespace Emby.Drawing.ImageMagick
             }
         }
 
-        internal static string ExtractFont(string name, IApplicationPaths paths)
+		internal static string ExtractFont(string name, IApplicationPaths paths, IFileSystem fileSystem)
         {
             var filePath = Path.Combine(paths.ProgramDataPath, "fonts", name);
 
-            if (File.Exists(filePath))
+			if (fileSystem.FileExists(filePath))
             {
                 return filePath;
             }
 
             var namespacePath = typeof(PlayedIndicatorDrawer).Namespace + ".fonts." + name;
             var tempPath = Path.Combine(paths.TempDirectory, Guid.NewGuid().ToString("N") + ".ttf");
-            Directory.CreateDirectory(Path.GetDirectoryName(tempPath));
+			fileSystem.CreateDirectory(Path.GetDirectoryName(tempPath));
 
             using (var stream = typeof(PlayedIndicatorDrawer).Assembly.GetManifestResourceStream(namespacePath))
             {
@@ -73,11 +77,11 @@ namespace Emby.Drawing.ImageMagick
                 }
             }
 
-            Directory.CreateDirectory(Path.GetDirectoryName(filePath));
+			fileSystem.CreateDirectory(Path.GetDirectoryName(filePath));
 
             try
             {
-                File.Copy(tempPath, filePath, false);
+				fileSystem.CopyFile(tempPath, filePath, false);
             }
             catch (IOException)
             {
@@ -87,11 +91,11 @@ namespace Emby.Drawing.ImageMagick
             return tempPath;
         }
 
-        internal static async Task<string> DownloadFont(string name, string url, IApplicationPaths paths, IHttpClient httpClient)
+		internal static async Task<string> DownloadFont(string name, string url, IApplicationPaths paths, IHttpClient httpClient, IFileSystem fileSystem)
         {
             var filePath = Path.Combine(paths.ProgramDataPath, "fonts", name);
 
-            if (File.Exists(filePath))
+			if (fileSystem.FileExists(filePath))
             {
                 return filePath;
             }
@@ -103,11 +107,11 @@ namespace Emby.Drawing.ImageMagick
 
             }).ConfigureAwait(false);
 
-            Directory.CreateDirectory(Path.GetDirectoryName(filePath));
+			fileSystem.CreateDirectory(Path.GetDirectoryName(filePath));
 
             try
             {
-                File.Copy(tempPath, filePath, false);
+				fileSystem.CopyFile(tempPath, filePath, false);
             }
             catch (IOException)
             {

+ 11 - 7
Emby.Drawing/ImageMagick/StripCollageBuilder.cs

@@ -2,16 +2,20 @@
 using MediaBrowser.Common.Configuration;
 using System;
 using System.Collections.Generic;
+using CommonIO;
+using MediaBrowser.Common.IO;
 
 namespace Emby.Drawing.ImageMagick
 {
     public class StripCollageBuilder
     {
         private readonly IApplicationPaths _appPaths;
+		private readonly IFileSystem _fileSystem;
 
-        public StripCollageBuilder(IApplicationPaths appPaths)
+		public StripCollageBuilder(IApplicationPaths appPaths, IFileSystem fileSystem)
         {
             _appPaths = appPaths;
+			_fileSystem = fileSystem;
         }
 
         public void BuildPosterCollage(List<string> paths, string outputPath, int width, int height, string text)
@@ -145,17 +149,17 @@ namespace Emby.Drawing.ImageMagick
 
         private MagickWand BuildPosterCollageWand(List<string> paths, int width, int height)
         {
-            var inputPaths = ImageHelpers.ProjectPaths(paths, 4);
+            var inputPaths = ImageHelpers.ProjectPaths(paths, 3);
             using (var wandImages = new MagickWand(inputPaths.ToArray()))
             {
                 var wand = new MagickWand(width, height);
                 wand.OpenImage("gradient:#111111-#111111");
                 using (var draw = new DrawingWand())
                 {
-                    var iSlice = Convert.ToInt32(width * 0.225);
+                    var iSlice = Convert.ToInt32(width * 0.3);
                     int iTrans = Convert.ToInt32(height * .25);
                     int iHeight = Convert.ToInt32(height * .65);
-                    var horizontalImagePadding = Convert.ToInt32(width * 0.0275);
+                    var horizontalImagePadding = Convert.ToInt32(width * 0.0366);
 
                     foreach (var element in wandImages.ImageList)
                     {
@@ -350,14 +354,14 @@ namespace Emby.Drawing.ImageMagick
 
         private MagickWand BuildSquareCollageWand(List<string> paths, int width, int height)
         {
-            var inputPaths = ImageHelpers.ProjectPaths(paths, 4);
+            var inputPaths = ImageHelpers.ProjectPaths(paths, 3);
             using (var wandImages = new MagickWand(inputPaths.ToArray()))
             {
                 var wand = new MagickWand(width, height);
                 wand.OpenImage("gradient:#111111-#111111");
                 using (var draw = new DrawingWand())
                 {
-                    var iSlice = Convert.ToInt32(width * .225);
+                    var iSlice = Convert.ToInt32(width * .3);
                     int iTrans = Convert.ToInt32(height * .25);
                     int iHeight = Convert.ToInt32(height * .63);
                     var horizontalImagePadding = Convert.ToInt32(width * 0.02);
@@ -490,7 +494,7 @@ namespace Emby.Drawing.ImageMagick
 
         private string MontserratLightFont
         {
-            get { return PlayedIndicatorDrawer.ExtractFont("MontserratLight.otf", _appPaths); }
+			get { return PlayedIndicatorDrawer.ExtractFont("MontserratLight.otf", _appPaths, _fileSystem); }
         }
     }
 }

+ 6 - 2
Emby.Drawing/ImageMagick/UnplayedCountIndicator.cs

@@ -1,7 +1,9 @@
 using ImageMagickSharp;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Model.Drawing;
 using System.Globalization;
+using CommonIO;
 
 namespace Emby.Drawing.ImageMagick
 {
@@ -10,10 +12,12 @@ namespace Emby.Drawing.ImageMagick
         private const int OffsetFromTopRightCorner = 38;
 
         private readonly IApplicationPaths _appPaths;
+        private readonly IFileSystem _fileSystem;
 
-        public UnplayedCountIndicator(IApplicationPaths appPaths)
+        public UnplayedCountIndicator(IApplicationPaths appPaths, IFileSystem fileSystem)
         {
             _appPaths = appPaths;
+            _fileSystem = fileSystem;
         }
 
         public void DrawUnplayedCountIndicator(MagickWand wand, ImageSize imageSize, int count)
@@ -33,7 +37,7 @@ namespace Emby.Drawing.ImageMagick
                     pixel.Opacity = 0;
                     pixel.Color = "white";
                     draw.FillColor = pixel;
-                    draw.Font = PlayedIndicatorDrawer.ExtractFont("robotoregular.ttf", _appPaths);
+                    draw.Font = PlayedIndicatorDrawer.ExtractFont("robotoregular.ttf", _appPaths, _fileSystem);
                     draw.FontStyle = FontStyleType.NormalStyle;
                     draw.TextAlignment = TextAlignType.CenterAlign;
                     draw.FontWeight = FontWeightType.RegularStyle;

+ 69 - 46
Emby.Drawing/ImageProcessor.cs

@@ -1,5 +1,4 @@
-using Emby.Drawing.Common;
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Drawing;
@@ -17,6 +16,9 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
+using Emby.Drawing.Common;
+using MediaBrowser.Controller.Library;
 
 namespace Emby.Drawing
 {
@@ -52,18 +54,20 @@ namespace Emby.Drawing
         private readonly IServerApplicationPaths _appPaths;
         private readonly IImageEncoder _imageEncoder;
         private readonly SemaphoreSlim _imageProcessingSemaphore;
+        private readonly Func<ILibraryManager> _libraryManager;
 
         public ImageProcessor(ILogger logger,
             IServerApplicationPaths appPaths,
             IFileSystem fileSystem,
             IJsonSerializer jsonSerializer,
             IImageEncoder imageEncoder,
-            int maxConcurrentImageProcesses)
+            int maxConcurrentImageProcesses, Func<ILibraryManager> libraryManager)
         {
             _logger = logger;
             _fileSystem = fileSystem;
             _jsonSerializer = jsonSerializer;
             _imageEncoder = imageEncoder;
+            _libraryManager = libraryManager;
             _appPaths = appPaths;
 
             ImageEnhancers = new List<IImageEnhancer>();
@@ -106,6 +110,15 @@ namespace Emby.Drawing
             }
         }
 
+
+        public bool SupportsImageCollageCreation
+        {
+            get
+            {
+                return _imageEncoder.SupportsImageCollageCreation;
+            }
+        }
+
         private string ResizedImageCachePath
         {
             get
@@ -157,7 +170,19 @@ namespace Emby.Drawing
                 throw new ArgumentNullException("options");
             }
 
-            var originalImagePath = options.Image.Path;
+            var originalImage = options.Image;
+
+            if (!originalImage.IsLocalFile)
+            {
+                originalImage = await _libraryManager().ConvertImageToLocal(options.Item, originalImage, options.ImageIndex).ConfigureAwait(false);
+            }
+
+            var originalImagePath = originalImage.Path;
+
+            if (!_imageEncoder.SupportsImageEncoding)
+            {
+                return originalImagePath;
+            }
 
             if (options.HasDefaultOptions(originalImagePath) && options.Enhancers.Count == 0 && !options.CropWhiteSpace)
             {
@@ -165,9 +190,9 @@ namespace Emby.Drawing
                 return originalImagePath;
             }
 
-            var dateModified = options.Image.DateModified;
+            var dateModified = originalImage.DateModified;
 
-            if (options.CropWhiteSpace)
+            if (options.CropWhiteSpace && _imageEncoder.SupportsImageEncoding)
             {
                 var tuple = await GetWhitespaceCroppedImage(originalImagePath, dateModified).ConfigureAwait(false);
 
@@ -180,7 +205,7 @@ namespace Emby.Drawing
                 var tuple = await GetEnhancedImage(new ItemImageInfo
                 {
                     DateModified = dateModified,
-                    Type = options.Image.Type,
+                    Type = originalImage.Type,
                     Path = originalImagePath
 
                 }, options.Item, options.ImageIndex, options.Enhancers).ConfigureAwait(false);
@@ -215,21 +240,18 @@ namespace Emby.Drawing
             {
                 CheckDisposed();
 
-                if (!File.Exists(cacheFilePath))
+                if (!_fileSystem.FileExists(cacheFilePath))
                 {
                     var newWidth = Convert.ToInt32(newSize.Width);
                     var newHeight = Convert.ToInt32(newSize.Height);
 
-                    Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
+                    _fileSystem.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
 
                     await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false);
 
                     imageProcessingLockTaken = true;
 
                     _imageEncoder.EncodeImage(originalImagePath, cacheFilePath, newWidth, newHeight, quality, options);
-
-                    // ImageMagick doesn't seem to always release it right away
-                    await Task.Delay(100).ConfigureAwait(false);
                 }
             }
             finally
@@ -270,7 +292,7 @@ namespace Emby.Drawing
             await semaphore.WaitAsync().ConfigureAwait(false);
 
             // Check again in case of contention
-            if (File.Exists(croppedImagePath))
+            if (_fileSystem.FileExists(croppedImagePath))
             {
                 semaphore.Release();
                 return GetResult(croppedImagePath);
@@ -280,13 +302,18 @@ namespace Emby.Drawing
 
             try
             {
-                Directory.CreateDirectory(Path.GetDirectoryName(croppedImagePath));
+                _fileSystem.CreateDirectory(Path.GetDirectoryName(croppedImagePath));
 
                 await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false);
                 imageProcessingLockTaken = true;
 
                 _imageEncoder.CropWhiteSpace(originalImagePath, croppedImagePath);
             }
+            catch (NotImplementedException)
+            {
+                // No need to spam the log with an error message
+                return new Tuple<string, DateTime>(originalImagePath, dateModified);
+            }
             catch (Exception ex)
             {
                 // We have to have a catch-all here because some of the .net image methods throw a plain old Exception
@@ -359,19 +386,14 @@ namespace Emby.Drawing
             return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLower());
         }
 
-        /// <summary>
-        /// Gets the size of the image.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns>ImageSize.</returns>
-        public ImageSize GetImageSize(string path)
+        public ImageSize GetImageSize(ItemImageInfo info)
         {
-            return GetImageSize(path, File.GetLastWriteTimeUtc(path), false);
+            return GetImageSize(info.Path, info.DateModified, false);
         }
 
-        public ImageSize GetImageSize(ItemImageInfo info)
+        public ImageSize GetImageSize(string path)
         {
-            return GetImageSize(info.Path, info.DateModified, false);
+            return GetImageSize(path, _fileSystem.GetLastWriteTimeUtc(path), false);
         }
 
         /// <summary>
@@ -399,7 +421,11 @@ namespace Emby.Drawing
             {
                 size = GetImageSizeInternal(path, allowSlowMethod);
 
-                _cachedImagedSizes.AddOrUpdate(cacheHash, size, (keyName, oldValue) => size);
+                if (size.Width > 0 && size.Height > 0)
+                {
+                    StartSaveImageSizeTimer();
+                    _cachedImagedSizes.AddOrUpdate(cacheHash, size, (keyName, oldValue) => size);
+                }
             }
 
             return size;
@@ -413,28 +439,26 @@ namespace Emby.Drawing
         /// <returns>ImageSize.</returns>
         private ImageSize GetImageSizeInternal(string path, bool allowSlowMethod)
         {
-            ImageSize size;
-
             try
             {
-                size = ImageHeader.GetDimensions(path, _logger, _fileSystem);
-            }
-            catch
-            {
-                if (!allowSlowMethod)
+                using (var file = TagLib.File.Create(path))
                 {
-                    throw;
-                }
-                //_logger.Info("Failed to read image header for {0}. Doing it the slow way.", path);
+                    var image = file as TagLib.Image.File;
 
-                CheckDisposed();
+                    var properties = image.Properties;
 
-                size = _imageEncoder.GetImageSize(path);
+                    return new ImageSize
+                    {
+                        Height = properties.PhotoHeight,
+                        Width = properties.PhotoWidth
+                    };
+                }
+            }
+            catch
+            {
             }
 
-            StartSaveImageSizeTimer();
-
-            return size;
+            return ImageHeader.GetDimensions(path, _logger, _fileSystem);
         }
 
         private readonly Timer _saveImageSizeTimer;
@@ -452,7 +476,7 @@ namespace Emby.Drawing
                 try
                 {
                     var path = ImageSizeFile;
-                    Directory.CreateDirectory(Path.GetDirectoryName(path));
+                    _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
                     _jsonSerializer.SerializeToFile(_cachedImagedSizes, path);
                 }
                 catch (Exception ex)
@@ -624,7 +648,7 @@ namespace Emby.Drawing
             await semaphore.WaitAsync().ConfigureAwait(false);
 
             // Check again in case of contention
-            if (File.Exists(enhancedImagePath))
+            if (_fileSystem.FileExists(enhancedImagePath))
             {
                 semaphore.Release();
                 return enhancedImagePath;
@@ -634,7 +658,7 @@ namespace Emby.Drawing
 
             try
             {
-                Directory.CreateDirectory(Path.GetDirectoryName(enhancedImagePath));
+                _fileSystem.CreateDirectory(Path.GetDirectoryName(enhancedImagePath));
 
                 await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false);
 
@@ -773,11 +797,11 @@ namespace Emby.Drawing
 
             try
             {
-                _logger.Debug("Creating image collage and saving to {0}", options.OutputPath);
+                _logger.Info("Creating image collage and saving to {0}", options.OutputPath);
 
                 _imageEncoder.CreateImageCollage(options);
 
-                _logger.Debug("Completed creation of image collage and saved to {0}", options.OutputPath);
+                _logger.Info("Completed creation of image collage and saved to {0}", options.OutputPath);
             }
             finally
             {
@@ -799,7 +823,6 @@ namespace Emby.Drawing
 
                     return false;
                 }
-
             });
         }
 
@@ -819,4 +842,4 @@ namespace Emby.Drawing
             }
         }
     }
-}
+}

+ 64 - 0
Emby.Drawing/NullImageEncoder.cs

@@ -0,0 +1,64 @@
+using System;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Drawing;
+
+namespace Emby.Drawing
+{
+    public class NullImageEncoder : IImageEncoder
+    {
+        public string[] SupportedInputFormats
+        {
+            get
+            {
+                return new[]
+                {
+                    "png",
+                    "jpeg",
+                    "jpg"
+                };
+            }
+        }
+
+        public ImageFormat[] SupportedOutputFormats
+        {
+            get
+            {
+                return new[] { ImageFormat.Jpg, ImageFormat.Png };
+            }
+        }
+
+        public void CropWhiteSpace(string inputPath, string outputPath)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void EncodeImage(string inputPath, string outputPath, int width, int height, int quality, ImageProcessingOptions options)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void CreateImageCollage(ImageCollageOptions options)
+        {
+            throw new NotImplementedException();
+        }
+
+        public string Name
+        {
+            get { return "Null Image Encoder"; }
+        }
+
+        public bool SupportsImageCollageCreation
+        {
+            get { return false; }
+        }
+
+        public bool SupportsImageEncoding
+        {
+            get { return false; }
+        }
+
+        public void Dispose()
+        {
+        }
+    }
+}

+ 2 - 0
Emby.Drawing/packages.config

@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
+  <package id="CommonIO" version="1.0.0.5" targetFramework="net45" />
   <package id="ImageMagickSharp" version="1.0.0.16" targetFramework="net45" />
+  <package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" />
 </packages>

+ 4 - 3
MediaBrowser.Api/ApiEntryPoint.cs

@@ -15,6 +15,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Api
 {
@@ -95,7 +96,7 @@ namespace MediaBrowser.Api
         {
             var path = _config.ApplicationPaths.TranscodingTempPath;
 
-            foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
+            foreach (var file in _fileSystem.GetFilePaths(path, true)
                 .ToList())
             {
                 _fileSystem.DeleteFile(file);
@@ -567,7 +568,7 @@ namespace MediaBrowser.Api
             var directory = Path.GetDirectoryName(outputFilePath);
             var name = Path.GetFileNameWithoutExtension(outputFilePath);
 
-            var filesToDelete = Directory.EnumerateFiles(directory)
+            var filesToDelete = _fileSystem.GetFilePaths(directory)
                 .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1)
                 .ToList();
 
@@ -577,7 +578,7 @@ namespace MediaBrowser.Api
             {
                 try
                 {
-                    Logger.Info("Deleting HLS file {0}", file);
+                    Logger.Debug("Deleting HLS file {0}", file);
                     _fileSystem.DeleteFile(file);
                 }
                 catch (DirectoryNotFoundException)

+ 0 - 100
MediaBrowser.Api/AppThemeService.cs

@@ -1,100 +0,0 @@
-using MediaBrowser.Common.IO;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Themes;
-using MediaBrowser.Model.Themes;
-using ServiceStack;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Themes", "GET", Summary = "Gets a list of available themes for an app")]
-    public class GetAppThemes : IReturn<List<AppThemeInfo>>
-    {
-        [ApiMember(Name = "App", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string App { get; set; }
-    }
-
-    [Route("/Themes/Info", "GET", Summary = "Gets an app theme")]
-    public class GetAppTheme : IReturn<AppTheme>
-    {
-        [ApiMember(Name = "App", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string App { get; set; }
-
-        [ApiMember(Name = "Name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Name { get; set; }
-    }
-
-    [Route("/Themes/Images", "GET", Summary = "Gets an app theme")]
-    public class GetAppThemeImage
-    {
-        [ApiMember(Name = "App", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string App { get; set; }
-
-        [ApiMember(Name = "Theme", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Theme { get; set; }
-
-        [ApiMember(Name = "Name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Name { get; set; }
-
-        [ApiMember(Name = "CacheTag", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string CacheTag { get; set; }
-    }
-
-    [Route("/Themes", "POST", Summary = "Saves a theme")]
-    public class SaveTheme : AppTheme, IReturnVoid
-    {
-    }
-
-    [Authenticated]
-    public class AppThemeService : BaseApiService
-    {
-        private readonly IAppThemeManager _themeManager;
-        private readonly IFileSystem _fileSystem;
-
-        public AppThemeService(IAppThemeManager themeManager, IFileSystem fileSystem)
-        {
-            _themeManager = themeManager;
-            _fileSystem = fileSystem;
-        }
-
-        public object Get(GetAppThemes request)
-        {
-            var result = _themeManager.GetThemes(request.App).ToList();
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetAppTheme request)
-        {
-            var result = _themeManager.GetTheme(request.App, request.Name);
-
-            return ToOptimizedResult(result);
-        }
-
-        public void Post(SaveTheme request)
-        {
-            _themeManager.SaveTheme(request);
-        }
-
-        public object Get(GetAppThemeImage request)
-        {
-            var info = _themeManager.GetImageImageInfo(request.App, request.Theme, request.Name);
-
-            var cacheGuid = new Guid(info.CacheTag);
-
-            TimeSpan? cacheDuration = null;
-
-            if (!string.IsNullOrEmpty(request.CacheTag) && cacheGuid == new Guid(request.CacheTag))
-            {
-                cacheDuration = TimeSpan.FromDays(365);
-            }
-
-            var contentType = MimeTypes.GetMimeType(info.Path);
-
-            return ResultFactory.GetCachedResult(Request, cacheGuid, null, cacheDuration, () => _fileSystem.GetFileStream(info.Path, FileMode.Open, FileAccess.Read, FileShare.Read), contentType);
-        }
-    }
-}

+ 1 - 0
MediaBrowser.Api/ConfigurationService.cs

@@ -11,6 +11,7 @@ using ServiceStack.Web;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using CommonIO;
 
 namespace MediaBrowser.Api
 {

+ 0 - 1
MediaBrowser.Api/Dlna/DlnaServerService.cs

@@ -108,7 +108,6 @@ namespace MediaBrowser.Api.Dlna
         private readonly IConnectionManager _connectionManager;
         private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
 
-        // TODO: Add utf-8
         private const string XMLContentType = "text/xml; charset=UTF-8";
 
         public DlnaServerService(IDlnaManager dlnaManager, IContentDirectory contentDirectory, IConnectionManager connectionManager, IMediaReceiverRegistrar mediaReceiverRegistrar)

+ 7 - 6
MediaBrowser.Api/EnvironmentService.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
@@ -8,6 +9,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using CommonIO;
 
 namespace MediaBrowser.Api
 {
@@ -96,13 +98,14 @@ namespace MediaBrowser.Api
         /// The _network manager
         /// </summary>
         private readonly INetworkManager _networkManager;
+        private IFileSystem _fileSystem;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="EnvironmentService" /> class.
         /// </summary>
         /// <param name="networkManager">The network manager.</param>
         /// <exception cref="System.ArgumentNullException">networkManager</exception>
-        public EnvironmentService(INetworkManager networkManager)
+        public EnvironmentService(INetworkManager networkManager, IFileSystem fileSystem)
         {
             if (networkManager == null)
             {
@@ -110,6 +113,7 @@ namespace MediaBrowser.Api
             }
 
             _networkManager = networkManager;
+            _fileSystem = fileSystem;
         }
 
         /// <summary>
@@ -117,8 +121,6 @@ namespace MediaBrowser.Api
         /// </summary>
         /// <param name="request">The request.</param>
         /// <returns>System.Object.</returns>
-        /// <exception cref="System.ArgumentNullException">Path</exception>
-        /// <exception cref="System.ArgumentException"></exception>
         public object Get(GetDirectoryContents request)
         {
             var path = request.Path;
@@ -222,8 +224,7 @@ namespace MediaBrowser.Api
         private IEnumerable<FileSystemEntryInfo> GetFileSystemEntries(GetDirectoryContents request)
         {
             // using EnumerateFileSystemInfos doesn't handle reparse points (symlinks)
-            var entries = new DirectoryInfo(request.Path).EnumerateDirectories("*", SearchOption.TopDirectoryOnly)
-                .Concat<FileSystemInfo>(new DirectoryInfo(request.Path).EnumerateFiles("*", SearchOption.TopDirectoryOnly)).Where(i =>
+            var entries = _fileSystem.GetFileSystemEntries(request.Path).Where(i =>
             {
                 if (!request.IncludeHidden && i.Attributes.HasFlag(FileAttributes.Hidden))
                 {

+ 11 - 11
MediaBrowser.Api/Images/ImageByNameService.cs

@@ -9,6 +9,7 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using CommonIO;
 
 namespace MediaBrowser.Api.Images
 {
@@ -130,8 +131,7 @@ namespace MediaBrowser.Api.Images
         {
             try
             {
-                return new DirectoryInfo(path)
-                    .GetFiles("*", SearchOption.AllDirectories)
+				return _fileSystem.GetFiles(path)
                     .Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.Ordinal))
                     .Select(i => new ImageByNameInfo
                     {
@@ -184,7 +184,7 @@ namespace MediaBrowser.Api.Images
 
             var paths = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(_appPaths.GeneralPath, request.Name, filename + i)).ToList();
 
-            var path = paths.FirstOrDefault(File.Exists) ?? paths.FirstOrDefault();
+			var path = paths.FirstOrDefault(_fileSystem.FileExists) ?? paths.FirstOrDefault();
 
             return ToStaticFileResult(path);
         }
@@ -198,11 +198,11 @@ namespace MediaBrowser.Api.Images
         {
             var themeFolder = Path.Combine(_appPaths.RatingsPath, request.Theme);
 
-            if (Directory.Exists(themeFolder))
+			if (_fileSystem.DirectoryExists(themeFolder))
             {
                 var path = BaseItem.SupportedImageExtensions
                     .Select(i => Path.Combine(themeFolder, request.Name + i))
-                    .FirstOrDefault(File.Exists);
+					.FirstOrDefault(_fileSystem.FileExists);
 
                 if (!string.IsNullOrEmpty(path))
                 {
@@ -212,14 +212,14 @@ namespace MediaBrowser.Api.Images
 
             var allFolder = Path.Combine(_appPaths.RatingsPath, "all");
 
-            if (Directory.Exists(allFolder))
+			if (_fileSystem.DirectoryExists(allFolder))
             {
                 // Avoid implicitly captured closure
                 var currentRequest = request;
 
                 var path = BaseItem.SupportedImageExtensions
                     .Select(i => Path.Combine(allFolder, currentRequest.Name + i))
-                    .FirstOrDefault(File.Exists);
+					.FirstOrDefault(_fileSystem.FileExists);
 
                 if (!string.IsNullOrEmpty(path))
                 {
@@ -239,10 +239,10 @@ namespace MediaBrowser.Api.Images
         {
             var themeFolder = Path.Combine(_appPaths.MediaInfoImagesPath, request.Theme);
 
-            if (Directory.Exists(themeFolder))
+			if (_fileSystem.DirectoryExists(themeFolder))
             {
                 var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, request.Name + i))
-                    .FirstOrDefault(File.Exists);
+					.FirstOrDefault(_fileSystem.FileExists);
 
                 if (!string.IsNullOrEmpty(path))
                 {
@@ -252,13 +252,13 @@ namespace MediaBrowser.Api.Images
 
             var allFolder = Path.Combine(_appPaths.MediaInfoImagesPath, "all");
 
-            if (Directory.Exists(allFolder))
+			if (_fileSystem.DirectoryExists(allFolder))
             {
                 // Avoid implicitly captured closure
                 var currentRequest = request;
 
                 var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, currentRequest.Name + i))
-                    .FirstOrDefault(File.Exists);
+					.FirstOrDefault(_fileSystem.FileExists);
 
                 if (!string.IsNullOrEmpty(path))
                 {

+ 55 - 22
MediaBrowser.Api/Images/ImageService.cs

@@ -17,6 +17,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
 
 namespace MediaBrowser.Api.Images
@@ -311,17 +312,23 @@ namespace MediaBrowser.Api.Images
         {
             try
             {
-                var fileInfo = new FileInfo(info.Path);
-
                 int? width = null;
                 int? height = null;
+                long length = 0;
 
                 try
                 {
-                    var size = _imageProcessor.GetImageSize(info);
+                    if (info.IsLocalFile)
+                    {
+                        var fileInfo = new FileInfo(info.Path);
+                        length = fileInfo.Length;
+
+                        var size = _imageProcessor.GetImageSize(info);
 
-                    width = Convert.ToInt32(size.Width);
-                    height = Convert.ToInt32(size.Height);
+                        width = Convert.ToInt32(size.Width);
+                        height = Convert.ToInt32(size.Height);
+
+                    }
                 }
                 catch
                 {
@@ -333,7 +340,7 @@ namespace MediaBrowser.Api.Images
                     ImageIndex = imageIndex,
                     ImageType = info.Type,
                     ImageTag = _imageProcessor.GetImageCacheTag(item, info),
-                    Size = fileInfo.Length,
+                    Size = length,
                     Width = width,
                     Height = height
                 };
@@ -557,7 +564,14 @@ namespace MediaBrowser.Api.Images
 
             }).ToList() : new List<IImageEnhancer>();
 
-            var format = GetOutputFormat(request, imageInfo, supportedImageEnhancers);
+            var cropwhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art;
+
+            if (request.CropWhitespace.HasValue)
+            {
+                cropwhitespace = request.CropWhitespace.Value;
+            }
+
+            var format = GetOutputFormat(request, imageInfo, cropwhitespace, supportedImageEnhancers);
             var contentType = GetMimeType(format, imageInfo.Path);
 
             var cacheGuid = new Guid(_imageProcessor.GetImageCacheTag(item, imageInfo, supportedImageEnhancers));
@@ -578,6 +592,7 @@ namespace MediaBrowser.Api.Images
             return GetImageResult(item,
                 request,
                 imageInfo,
+                cropwhitespace,
                 format,
                 supportedImageEnhancers,
                 contentType,
@@ -590,6 +605,7 @@ namespace MediaBrowser.Api.Images
         private async Task<object> GetImageResult(IHasImages item,
             ImageRequest request,
             ItemImageInfo image,
+            bool cropwhitespace,
             ImageFormat format,
             List<IImageEnhancer> enhancers,
             string contentType,
@@ -597,13 +613,6 @@ namespace MediaBrowser.Api.Images
             IDictionary<string, string> headers,
             bool isHeadRequest)
         {
-            var cropwhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art;
-
-            if (request.CropWhitespace.HasValue)
-            {
-                cropwhitespace = request.CropWhitespace.Value;
-            }
-
             var options = new ImageProcessingOptions
             {
                 CropWhiteSpace = cropwhitespace,
@@ -637,7 +646,7 @@ namespace MediaBrowser.Api.Images
             });
         }
 
-        private ImageFormat GetOutputFormat(ImageRequest request, ItemImageInfo image, List<IImageEnhancer> enhancers)
+        private ImageFormat GetOutputFormat(ImageRequest request, ItemImageInfo image, bool cropwhitespace, List<IImageEnhancer> enhancers)
         {
             if (!string.IsNullOrWhiteSpace(request.Format))
             {
@@ -648,10 +657,37 @@ namespace MediaBrowser.Api.Images
                 }
             }
 
+            var extension = Path.GetExtension(image.Path);
+            ImageFormat? inputFormat = null;
+
+            if (string.Equals(extension, ".jpg", StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
+            {
+                inputFormat = ImageFormat.Jpg;
+            }
+            else if (string.Equals(extension, ".png", StringComparison.OrdinalIgnoreCase))
+            {
+                inputFormat = ImageFormat.Png;
+            }
+
+            var clientSupportedFormats = GetClientSupportedFormats();
+            if (inputFormat.HasValue && clientSupportedFormats.Contains(inputFormat.Value) && enhancers.Count == 0)
+            {
+                if ((request.Quality ?? 100) == 100 && !request.Height.HasValue && !request.Width.HasValue &&
+                    !request.AddPlayedIndicator && !request.PercentPlayed.HasValue && !request.UnplayedCount.HasValue && string.IsNullOrWhiteSpace(request.BackgroundColor))
+                {
+                    // TODO: Allow this when specfying max width/height if the value is in range
+                    if (!cropwhitespace && !request.MaxHeight.HasValue && !request.MaxWidth.HasValue)
+                    {
+                        return inputFormat.Value;
+                    }
+                }
+            }
+
             var serverFormats = _imageProcessor.GetSupportedImageOutputFormats();
 
-            if (serverFormats.Contains(ImageFormat.Webp) &&
-                GetClientSupportedFormats().Contains(ImageFormat.Webp))
+            // Client doesn't care about format, so start with webp if supported
+            if (serverFormats.Contains(ImageFormat.Webp) && clientSupportedFormats.Contains(ImageFormat.Webp))
             {
                 return ImageFormat.Webp;
             }
@@ -661,10 +697,7 @@ namespace MediaBrowser.Api.Images
                 return ImageFormat.Png;
             }
 
-            var extension = Path.GetExtension(image.Path);
-
-            if (string.Equals(extension, ".jpg", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
+            if (inputFormat.HasValue && inputFormat.Value == ImageFormat.Jpg)
             {
                 return ImageFormat.Jpg;
             }
@@ -675,7 +708,7 @@ namespace MediaBrowser.Api.Images
 
         private ImageFormat[] GetClientSupportedFormats()
         {
-            var supportsWebP = (Request.AcceptTypes ?? new string[] {}).Contains("image/webp", StringComparer.OrdinalIgnoreCase);
+            var supportsWebP = (Request.AcceptTypes ?? new string[] { }).Contains("image/webp", StringComparer.OrdinalIgnoreCase);
 
             var userAgent = Request.UserAgent ?? string.Empty;
 

+ 4 - 3
MediaBrowser.Api/Images/RemoteImageService.cs

@@ -16,6 +16,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Api.Images
 {
@@ -237,7 +238,7 @@ namespace MediaBrowser.Api.Images
                     contentPath = await reader.ReadToEndAsync().ConfigureAwait(false);
                 }
 
-                if (File.Exists(contentPath))
+				if (_fileSystem.FileExists(contentPath))
                 {
                     return ToStaticFileResult(contentPath);
                 }
@@ -281,7 +282,7 @@ namespace MediaBrowser.Api.Images
 
             var fullCachePath = GetFullCachePath(urlHash + "." + ext);
 
-            Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
+			_fileSystem.CreateDirectory(Path.GetDirectoryName(fullCachePath));
             using (var stream = result.Content)
             {
                 using (var filestream = _fileSystem.GetFileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, true))
@@ -290,7 +291,7 @@ namespace MediaBrowser.Api.Images
                 }
             }
 
-            Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
+			_fileSystem.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
             using (var writer = new StreamWriter(pointerCachePath))
             {
                 await writer.WriteAsync(fullCachePath).ConfigureAwait(false);

+ 5 - 4
MediaBrowser.Api/ItemLookupService.cs

@@ -16,6 +16,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Api
 {
@@ -200,7 +201,7 @@ namespace MediaBrowser.Api
             //}
             item.ProviderIds = request.ProviderIds;
 
-            var task = _providerManager.RefreshFullItem(item, new MetadataRefreshOptions
+			var task = _providerManager.RefreshFullItem(item, new MetadataRefreshOptions(_fileSystem)
             {
                 MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
                 ImageRefreshMode = ImageRefreshMode.FullRefresh,
@@ -230,7 +231,7 @@ namespace MediaBrowser.Api
                     contentPath = await reader.ReadToEndAsync().ConfigureAwait(false);
                 }
 
-                if (File.Exists(contentPath))
+				if (_fileSystem.FileExists(contentPath))
                 {
                     return ToStaticFileResult(contentPath);
                 }
@@ -271,7 +272,7 @@ namespace MediaBrowser.Api
 
             var fullCachePath = GetFullCachePath(urlHash + "." + ext);
 
-            Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
+			_fileSystem.CreateDirectory(Path.GetDirectoryName(fullCachePath));
             using (var stream = result.Content)
             {
                 using (var filestream = _fileSystem.GetFileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, true))
@@ -280,7 +281,7 @@ namespace MediaBrowser.Api
                 }
             }
 
-            Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
+			_fileSystem.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
             using (var writer = new StreamWriter(pointerCachePath))
             {
                 await writer.WriteAsync(fullCachePath).ConfigureAwait(false);

+ 6 - 2
MediaBrowser.Api/ItemRefreshService.cs

@@ -4,6 +4,8 @@ using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
 using ServiceStack;
 using System.Threading;
+using CommonIO;
+using MediaBrowser.Common.IO;
 
 namespace MediaBrowser.Api
 {
@@ -37,11 +39,13 @@ namespace MediaBrowser.Api
     {
         private readonly ILibraryManager _libraryManager;
         private readonly IProviderManager _providerManager;
+        private readonly IFileSystem _fileSystem;
 
-        public ItemRefreshService(ILibraryManager libraryManager, IProviderManager providerManager)
+        public ItemRefreshService(ILibraryManager libraryManager, IProviderManager providerManager, IFileSystem fileSystem)
         {
             _libraryManager = libraryManager;
             _providerManager = providerManager;
+            _fileSystem = fileSystem;
         }
 
         /// <summary>
@@ -66,7 +70,7 @@ namespace MediaBrowser.Api
 
         private MetadataRefreshOptions GetRefreshOptions(BaseRefreshRequest request)
         {
-            return new MetadataRefreshOptions(new DirectoryService())
+            return new MetadataRefreshOptions(new DirectoryService(_fileSystem))
             {
                 MetadataRefreshMode = request.MetadataRefreshMode,
                 ImageRefreshMode = request.ImageRefreshMode,

+ 2 - 12
MediaBrowser.Api/ItemUpdateService.cs

@@ -211,11 +211,6 @@ namespace MediaBrowser.Api
 
             UpdateItem(request, item);
 
-            if (isLockedChanged && item.IsLocked)
-            {
-                item.IsUnidentified = false;
-            }
-
             await item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
 
             if (request.People != null)
@@ -321,13 +316,8 @@ namespace MediaBrowser.Api
 
             SetProductionLocations(item, request);
 
-            var hasLang = item as IHasPreferredMetadataLanguage;
-
-            if (hasLang != null)
-            {
-                hasLang.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode;
-                hasLang.PreferredMetadataLanguage = request.PreferredMetadataLanguage;
-            }
+            item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode;
+            item.PreferredMetadataLanguage = request.PreferredMetadataLanguage;
 
             var hasDisplayOrder = item as IHasDisplayOrder;
             if (hasDisplayOrder != null)

+ 5 - 6
MediaBrowser.Api/Library/LibraryHelpers.cs

@@ -3,6 +3,7 @@ using MediaBrowser.Controller;
 using System;
 using System.IO;
 using System.Linq;
+using CommonIO;
 
 namespace MediaBrowser.Api.Library
 {
@@ -33,11 +34,11 @@ namespace MediaBrowser.Api.Library
             var rootFolderPath = appPaths.DefaultUserViewsPath;
             var path = Path.Combine(rootFolderPath, virtualFolderName);
 
-            if (!Directory.Exists(path))
+			if (!fileSystem.DirectoryExists(path))
             {
                 throw new DirectoryNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName));
             }
-
+            
             var shortcut = Directory.EnumerateFiles(path, ShortcutFileSearch, SearchOption.AllDirectories).FirstOrDefault(f => fileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
 
             if (!string.IsNullOrEmpty(shortcut))
@@ -53,11 +54,9 @@ namespace MediaBrowser.Api.Library
         /// <param name="virtualFolderName">Name of the virtual folder.</param>
         /// <param name="path">The path.</param>
         /// <param name="appPaths">The app paths.</param>
-        /// <exception cref="System.IO.DirectoryNotFoundException">The path does not exist.</exception>
-        /// <exception cref="System.ArgumentException">The path is not valid.</exception>
         public static void AddMediaPath(IFileSystem fileSystem, string virtualFolderName, string path, IServerApplicationPaths appPaths)
         {
-            if (!Directory.Exists(path))
+			if (!fileSystem.DirectoryExists(path))
             {
                 throw new DirectoryNotFoundException("The path does not exist.");
             }
@@ -69,7 +68,7 @@ namespace MediaBrowser.Api.Library
 
             var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 
-            while (File.Exists(lnk))
+			while (fileSystem.FileExists(lnk))
             {
                 shortcutFilename += "1";
                 lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);

+ 7 - 3
MediaBrowser.Api/Library/LibraryService.cs

@@ -27,6 +27,8 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
+using MediaBrowser.Common.IO;
 
 namespace MediaBrowser.Api.Library
 {
@@ -282,12 +284,13 @@ namespace MediaBrowser.Api.Library
         private readonly IChannelManager _channelManager;
         private readonly ITVSeriesManager _tvManager;
         private readonly ILibraryMonitor _libraryMonitor;
+        private readonly IFileSystem _fileSystem;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="LibraryService" /> class.
         /// </summary>
         public LibraryService(IItemRepository itemRepo, ILibraryManager libraryManager, IUserManager userManager,
-                              IDtoService dtoService, IUserDataManager userDataManager, IAuthorizationContext authContext, IActivityManager activityManager, ILocalizationManager localization, ILiveTvManager liveTv, IChannelManager channelManager, ITVSeriesManager tvManager, ILibraryMonitor libraryMonitor)
+                              IDtoService dtoService, IUserDataManager userDataManager, IAuthorizationContext authContext, IActivityManager activityManager, ILocalizationManager localization, ILiveTvManager liveTv, IChannelManager channelManager, ITVSeriesManager tvManager, ILibraryMonitor libraryMonitor, IFileSystem fileSystem)
         {
             _itemRepo = itemRepo;
             _libraryManager = libraryManager;
@@ -301,6 +304,7 @@ namespace MediaBrowser.Api.Library
             _channelManager = channelManager;
             _tvManager = tvManager;
             _libraryMonitor = libraryMonitor;
+            _fileSystem = fileSystem;
         }
 
         public object Get(GetSimilarItems request)
@@ -557,7 +561,7 @@ namespace MediaBrowser.Api.Library
             {
                 throw new ArgumentException("This command cannot be used for remote or virtual items.");
             }
-            if (Directory.Exists(item.Path))
+			if (_fileSystem.DirectoryExists(item.Path))
             {
                 throw new ArgumentException("This command cannot be used for directories.");
             }
@@ -719,7 +723,7 @@ namespace MediaBrowser.Api.Library
 
             if (!item.CanDelete(user))
             {
-                throw new UnauthorizedAccessException();
+                throw new SecurityException("Unauthorized access");
             }
 
             if (item is ILiveTvRecording)

+ 36 - 9
MediaBrowser.Api/Library/LibraryStructureService.cs

@@ -10,6 +10,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Api.Library
 {
@@ -46,6 +47,12 @@ namespace MediaBrowser.Api.Library
         /// </summary>
         /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
         public bool RefreshLibrary { get; set; }
+
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        /// <value>The path.</value>
+        public string[] Paths { get; set; }
     }
 
     [Route("/Library/VirtualFolders", "DELETE")]
@@ -195,22 +202,42 @@ namespace MediaBrowser.Api.Library
 
             var virtualFolderPath = Path.Combine(rootFolderPath, name);
 
-            if (Directory.Exists(virtualFolderPath))
+            if (_fileSystem.DirectoryExists(virtualFolderPath))
             {
-                throw new ArgumentException("There is already a media collection with the name " + name + ".");
+                throw new ArgumentException("There is already a media library with the name " + name + ".");
             }
 
+            if (request.Paths != null)
+            {
+                var invalidpath = request.Paths.FirstOrDefault(i => !_fileSystem.DirectoryExists(i));
+                if (invalidpath != null)
+                {
+                    throw new ArgumentException("The specified path does not exist: " + invalidpath + ".");
+                }
+            }
+            
             _libraryMonitor.Stop();
 
             try
             {
-                Directory.CreateDirectory(virtualFolderPath);
+				_fileSystem.CreateDirectory(virtualFolderPath);
 
                 if (!string.IsNullOrEmpty(request.CollectionType))
                 {
                     var path = Path.Combine(virtualFolderPath, request.CollectionType + ".collection");
 
-                    File.Create(path);
+                    using (File.Create(path))
+                    {
+
+                    }
+                }
+
+                if (request.Paths != null)
+                {
+                    foreach (var path in request.Paths)
+                    {
+                        LibraryHelpers.AddMediaPath(_fileSystem, request.Name, path, _appPaths);
+                    }
                 }
             }
             finally
@@ -256,12 +283,12 @@ namespace MediaBrowser.Api.Library
             var currentPath = Path.Combine(rootFolderPath, request.Name);
             var newPath = Path.Combine(rootFolderPath, request.NewName);
 
-            if (!Directory.Exists(currentPath))
+			if (!_fileSystem.DirectoryExists(currentPath))
             {
                 throw new DirectoryNotFoundException("The media collection does not exist");
             }
 
-            if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
+			if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && _fileSystem.DirectoryExists(newPath))
             {
                 throw new ArgumentException("There is already a media collection with the name " + newPath + ".");
             }
@@ -276,11 +303,11 @@ namespace MediaBrowser.Api.Library
                     //Create an unique name
                     var temporaryName = Guid.NewGuid().ToString();
                     var temporaryPath = Path.Combine(rootFolderPath, temporaryName);
-                    Directory.Move(currentPath, temporaryPath);
+					_fileSystem.MoveDirectory(currentPath, temporaryPath);
                     currentPath = temporaryPath;
                 }
 
-                Directory.Move(currentPath, newPath);
+				_fileSystem.MoveDirectory(currentPath, newPath);
             }
             finally
             {
@@ -319,7 +346,7 @@ namespace MediaBrowser.Api.Library
 
             var path = Path.Combine(rootFolderPath, request.Name);
 
-            if (!Directory.Exists(path))
+			if (!_fileSystem.DirectoryExists(path))
             {
                 throw new DirectoryNotFoundException("The media folder does not exist");
             }

+ 2 - 2
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -521,12 +521,12 @@ namespace MediaBrowser.Api.LiveTv
 
             if (user == null)
             {
-                throw new UnauthorizedAccessException("Anonymous live tv management is not allowed.");
+                throw new SecurityException("Anonymous live tv management is not allowed.");
             }
 
             if (!user.Policy.EnableLiveTvManagement)
             {
-                throw new UnauthorizedAccessException("The current user does not have permission to manage live tv.");
+                throw new SecurityException("The current user does not have permission to manage live tv.");
             }
         }
 

+ 10 - 9
MediaBrowser.Api/MediaBrowser.Api.csproj

@@ -11,10 +11,9 @@
     <AssemblyName>MediaBrowser.Api</AssemblyName>
     <FileAlignment>512</FileAlignment>
     <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
-    <ProductVersion>10.0.0</ProductVersion>
-    <SchemaVersion>2.0</SchemaVersion>
     <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
-    <RestorePackages>true</RestorePackages>
+    <ReleaseVersion>
+    </ReleaseVersion>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
     <DebugSymbols>true</DebugSymbols>
@@ -25,7 +24,6 @@
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
     <PlatformTarget>AnyCPU</PlatformTarget>
-    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
     <DebugType>none</DebugType>
@@ -34,7 +32,6 @@
     <DefineConstants>TRACE</DefineConstants>
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
-    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release Mono|AnyCPU' ">
     <DebugType>none</DebugType>
@@ -43,15 +40,17 @@
     <DefineConstants>TRACE</DefineConstants>
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
-    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
   </PropertyGroup>
   <PropertyGroup>
     <RunPostBuildEvent>Always</RunPostBuildEvent>
   </PropertyGroup>
   <ItemGroup>
-    <Reference Include="MoreLinq, Version=1.1.17511.0, Culture=neutral, PublicKeyToken=384d532d7e88985d, processorArchitecture=MSIL">
+    <Reference Include="CommonIO, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
-      <HintPath>..\packages\morelinq.1.1.0\lib\net35\MoreLinq.dll</HintPath>
+      <HintPath>..\packages\CommonIO.1.0.0.5\lib\net45\CommonIO.dll</HintPath>
+    </Reference>
+    <Reference Include="Patterns.Logging">
+      <HintPath>..\packages\Patterns.Logging.1.0.0.2\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.Logging.dll</HintPath>
     </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
@@ -64,12 +63,14 @@
     <Reference Include="ServiceStack.Text">
       <HintPath>..\ThirdParty\ServiceStack.Text\ServiceStack.Text.dll</HintPath>
     </Reference>
+    <Reference Include="MoreLinq">
+      <HintPath>..\packages\morelinq.1.1.1\lib\net35\MoreLinq.dll</HintPath>
+    </Reference>
   </ItemGroup>
   <ItemGroup>
     <Compile Include="..\SharedVersion.cs">
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
-    <Compile Include="AppThemeService.cs" />
     <Compile Include="BrandingService.cs" />
     <Compile Include="ChannelService.cs" />
     <Compile Include="ConnectService.cs" />

+ 64 - 49
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -23,6 +23,7 @@ using System.Linq;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Api.Playback
 {
@@ -290,13 +291,6 @@ namespace MediaBrowser.Api.Playback
         {
             get
             {
-                var lib = ApiEntryPoint.Instance.GetEncodingOptions().H264Encoder;
-
-                if (!string.IsNullOrWhiteSpace(lib))
-                {
-                    return lib;
-                }
-
                 return "libx264";
             }
         }
@@ -809,6 +803,46 @@ namespace MediaBrowser.Api.Playback
             return "copy";
         }
 
+        /// <summary>
+        /// Gets the name of the output video codec
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <returns>System.String.</returns>
+        protected string GetVideoDecoder(StreamState state)
+        {
+            if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareVideoDecoder, "qsv", StringComparison.OrdinalIgnoreCase))
+            {
+                if (state.VideoStream != null && !string.IsNullOrWhiteSpace(state.VideoStream.Codec))
+                {
+                    switch (state.MediaSource.VideoStream.Codec.ToLower())
+                    {
+                        case "avc":
+                        case "h264":
+                            if (MediaEncoder.SupportsDecoder("h264_qsv"))
+                            {
+                                return "-c:v h264_qsv ";
+                            }
+                            break;
+                        case "mpeg2video":
+                            if (MediaEncoder.SupportsDecoder("mpeg2_qsv"))
+                            {
+                                return "-c:v mpeg2_qsv ";
+                            }
+                            break;
+                        case "vc1":
+                            if (MediaEncoder.SupportsDecoder("vc1_qsv"))
+                            {
+                                return "-c:v vc1_qsv ";
+                            }
+                            break;
+                    }
+                }
+            }
+
+            // leave blank so ffmpeg will decide
+            return null;
+        }
+
         /// <summary>
         /// Gets the input argument.
         /// </summary>
@@ -816,7 +850,7 @@ namespace MediaBrowser.Api.Playback
         /// <returns>System.String.</returns>
         protected string GetInputArgument(StreamState state)
         {
-            var arg = "-i " + GetInputPathArgument(state);
+            var arg = string.Format("-i {0}", GetInputPathArgument(state));
 
             if (state.SubtitleStream != null)
             {
@@ -826,7 +860,7 @@ namespace MediaBrowser.Api.Playback
                 }
             }
 
-            return arg;
+            return arg.Trim();
         }
 
         private string GetInputPathArgument(StreamState state)
@@ -840,7 +874,7 @@ namespace MediaBrowser.Api.Playback
             {
                 if (!(state.VideoType == VideoType.Iso && state.IsoMount == null))
                 {
-                    inputPath = MediaEncoderHelpers.GetInputArgument(mediaPath, state.InputProtocol, state.IsoMount, state.PlayableStreamFileNames);
+                    inputPath = MediaEncoderHelpers.GetInputArgument(FileSystem, mediaPath, state.InputProtocol, state.IsoMount, state.PlayableStreamFileNames);
                 }
             }
 
@@ -889,7 +923,7 @@ namespace MediaBrowser.Api.Playback
             CancellationTokenSource cancellationTokenSource,
             string workingDirectory = null)
         {
-            Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+            FileSystem.CreateDirectory(Path.GetDirectoryName(outputPath));
 
             await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
 
@@ -942,7 +976,7 @@ namespace MediaBrowser.Api.Playback
             Logger.Info(commandLineLogMessage);
 
             var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, "transcode-" + Guid.NewGuid() + ".txt");
-            Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
+            FileSystem.CreateDirectory(Path.GetDirectoryName(logFilePath));
 
             // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
             state.LogFileStream = FileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true);
@@ -972,7 +1006,7 @@ namespace MediaBrowser.Api.Playback
             StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream);
 
             // Wait for the file to exist before proceeeding
-            while (!File.Exists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited)
+			while (!FileSystem.FileExists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited)
             {
                 await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
             }
@@ -1027,6 +1061,7 @@ namespace MediaBrowser.Api.Playback
                         var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
 
                         await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+                        await target.FlushAsync().ConfigureAwait(false);
                     }
                 }
             }
@@ -1151,15 +1186,8 @@ namespace MediaBrowser.Api.Playback
 
             if (bitrate.HasValue)
             {
-                var hasFixedResolution = state.VideoRequest.HasFixedResolution;
-
                 if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
                 {
-                    if (hasFixedResolution)
-                    {
-                        return string.Format(" -minrate:v ({0}*.90) -maxrate:v ({0}*1.10) -bufsize:v {0} -b:v {0}", bitrate.Value.ToString(UsCulture));
-                    }
-
                     // With vpx when crf is used, b:v becomes a max rate
                     // https://trac.ffmpeg.org/wiki/vpxEncodingGuide. But higher bitrate source files -b:v causes judder so limite the bitrate but dont allow it to "saturate" the bitrate. So dont contrain it down just up.
                     return string.Format(" -maxrate:v {0} -bufsize:v ({0}*2) -b:v {0}", bitrate.Value.ToString(UsCulture));
@@ -1170,36 +1198,15 @@ namespace MediaBrowser.Api.Playback
                     return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
                 }
 
-                // h264_qsv
-                if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "libnvenc", StringComparison.OrdinalIgnoreCase))
+                // h264
+                if (isHls)
                 {
-                    if (hasFixedResolution)
-                    {
-                        if (isHls)
-                        {
-                            return string.Format(" -b:v {0} -maxrate ({0}*.80) -bufsize {0}", bitrate.Value.ToString(UsCulture));
-                        }
-
-                        return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
-                    }
-
-                    return string.Format(" -b:v {0} -maxrate ({0}*1.2) -bufsize ({0}*2)", bitrate.Value.ToString(UsCulture));
+                    return string.Format(" -b:v {0} -maxrate {0} -bufsize {1}",
+                        bitrate.Value.ToString(UsCulture),
+                        (bitrate.Value * 2).ToString(UsCulture));
                 }
 
-                // H264
-                if (hasFixedResolution)
-                {
-                    if (isHls)
-                    {
-                        return string.Format(" -b:v {0} -maxrate ({0}*.80) -bufsize {0}", bitrate.Value.ToString(UsCulture));
-                    }
-
-                    return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
-                }
-
-                return string.Format(" -maxrate {0} -bufsize {1}",
-                    bitrate.Value.ToString(UsCulture),
-                    (bitrate.Value * 2).ToString(UsCulture));
+                return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
             }
 
             return string.Empty;
@@ -1986,7 +1993,8 @@ namespace MediaBrowser.Api.Playback
                 state.IsTargetCabac,
                 state.TargetRefFrames,
                 state.TargetVideoStreamCount,
-                state.TargetAudioStreamCount);
+                state.TargetAudioStreamCount,
+                state.TargetVideoCodecTag);
 
             if (mediaProfile != null)
             {
@@ -2083,7 +2091,8 @@ namespace MediaBrowser.Api.Playback
                     state.IsTargetCabac,
                     state.TargetRefFrames,
                     state.TargetVideoStreamCount,
-                    state.TargetAudioStreamCount
+                    state.TargetAudioStreamCount,
+                    state.TargetVideoCodecTag
 
                     ).FirstOrDefault() ?? string.Empty;
             }
@@ -2158,6 +2167,12 @@ namespace MediaBrowser.Api.Playback
                 inputModifier += " -re";
             }
 
+            var videoDecoder = GetVideoDecoder(state);
+            if (!string.IsNullOrWhiteSpace(videoDecoder))
+            {
+                inputModifier += " " + videoDecoder;
+            }
+
             return inputModifier;
         }
 

+ 9 - 9
MediaBrowser.Api/Playback/Dash/MpegDashService.cs

@@ -17,6 +17,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
 
 namespace MediaBrowser.Api.Playback.Dash
@@ -174,7 +175,7 @@ namespace MediaBrowser.Api.Playback.Dash
 
                                 var workingDirectory = Path.Combine(Path.GetDirectoryName(playlistPath), (startNumber == -1 ? 0 : startNumber).ToString(CultureInfo.InvariantCulture));
                                 state.WaitForPath = Path.Combine(workingDirectory, Path.GetFileName(playlistPath));
-                                Directory.CreateDirectory(workingDirectory);
+                                FileSystem.CreateDirectory(workingDirectory);
                                 job = await StartFfMpeg(state, playlistPath, cancellationTokenSource, workingDirectory).ConfigureAwait(false);
                                 await WaitForMinimumDashSegmentCount(Path.Combine(workingDirectory, Path.GetFileName(playlistPath)), 1, cancellationTokenSource.Token).ConfigureAwait(false);
                             }
@@ -322,14 +323,13 @@ namespace MediaBrowser.Api.Playback.Dash
             }
         }
 
-        private static List<FileInfo> GetLastTranscodingFiles(string playlist, string segmentExtension, IFileSystem fileSystem, int count)
+        private static List<FileSystemMetadata> GetLastTranscodingFiles(string playlist, string segmentExtension, IFileSystem fileSystem, int count)
         {
             var folder = Path.GetDirectoryName(playlist);
 
             try
             {
-                return new DirectoryInfo(folder)
-                    .EnumerateFiles("*", SearchOption.AllDirectories)
+				return fileSystem.GetFiles(folder)
                     .Where(i => string.Equals(i.Extension, segmentExtension, StringComparison.OrdinalIgnoreCase))
                     .OrderByDescending(fileSystem.GetLastWriteTimeUtc)
                     .Take(count)
@@ -337,7 +337,7 @@ namespace MediaBrowser.Api.Playback.Dash
             }
             catch (DirectoryNotFoundException)
             {
-                return new List<FileInfo>();
+                return new List<FileSystemMetadata>();
             }
         }
 
@@ -348,20 +348,20 @@ namespace MediaBrowser.Api.Playback.Dash
             if (requestedIndex == -1)
             {
                 var path = Path.Combine(folder, "0", "stream" + representationId + "-" + "init" + segmentExtension);
-                return File.Exists(path) ? path : null;
+				return FileSystem.FileExists(path) ? path : null;
             }
 
             try
             {
-                foreach (var subfolder in new DirectoryInfo(folder).EnumerateDirectories().ToList())
+                foreach (var subfolder in FileSystem.GetDirectoryPaths(folder).ToList())
                 {
-                    var subfolderName = Path.GetFileNameWithoutExtension(subfolder.FullName);
+                    var subfolderName = Path.GetFileNameWithoutExtension(subfolder);
                     int startNumber;
                     if (int.TryParse(subfolderName, NumberStyles.Any, UsCulture, out startNumber))
                     {
                         var segmentIndex = requestedIndex - startNumber + 1;
                         var path = Path.Combine(folder, subfolderName, "stream" + representationId + "-" + segmentIndex.ToString("00000", CultureInfo.InvariantCulture) + segmentExtension);
-                        if (File.Exists(path))
+						if (FileSystem.FileExists(path))
                         {
                             return path;
                         }

+ 7 - 5
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -15,6 +15,7 @@ using System.IO;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Api.Playback.Hls
 {
@@ -23,7 +24,8 @@ namespace MediaBrowser.Api.Playback.Hls
     /// </summary>
     public abstract class BaseHlsService : BaseStreamingService
     {
-        protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer)
+        protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer)
+            : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer)
         {
         }
 
@@ -90,12 +92,12 @@ namespace MediaBrowser.Api.Playback.Hls
             TranscodingJob job = null;
             var playlist = state.OutputFilePath;
 
-            if (!File.Exists(playlist))
+            if (!FileSystem.FileExists(playlist))
             {
                 await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
                 try
                 {
-                    if (!File.Exists(playlist))
+                    if (!FileSystem.FileExists(playlist))
                     {
                         // If the playlist doesn't already exist, startup ffmpeg
                         try
@@ -150,7 +152,7 @@ namespace MediaBrowser.Api.Playback.Hls
             {
                 ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
             }
-            
+
             return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
         }
 
@@ -317,4 +319,4 @@ namespace MediaBrowser.Api.Playback.Hls
             return false;
         }
     }
-}
+}

+ 9 - 9
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -20,6 +20,7 @@ using System.Linq;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
 
 namespace MediaBrowser.Api.Playback.Hls
@@ -165,7 +166,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             TranscodingJob job = null;
 
-            if (File.Exists(segmentPath))
+            if (FileSystem.FileExists(segmentPath))
             {
                 job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
                 return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false);
@@ -174,7 +175,7 @@ namespace MediaBrowser.Api.Playback.Hls
             await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
             try
             {
-                if (File.Exists(segmentPath))
+                if (FileSystem.FileExists(segmentPath))
                 {
                     job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
                     return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false);
@@ -354,7 +355,7 @@ namespace MediaBrowser.Api.Playback.Hls
             }
         }
 
-        private void DeleteFile(FileInfo file, int retryCount)
+        private void DeleteFile(FileSystemMetadata file, int retryCount)
         {
             if (retryCount >= 5)
             {
@@ -378,7 +379,7 @@ namespace MediaBrowser.Api.Playback.Hls
             }
         }
 
-        private static FileInfo GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
+        private static FileSystemMetadata GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
         {
             var folder = Path.GetDirectoryName(playlist);
 
@@ -386,8 +387,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             try
             {
-                return new DirectoryInfo(folder)
-                    .EnumerateFiles("*", SearchOption.TopDirectoryOnly)
+                return fileSystem.GetFiles(folder)
                     .Where(i => string.Equals(i.Extension, segmentExtension, StringComparison.OrdinalIgnoreCase) && Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase))
                     .OrderByDescending(fileSystem.GetLastWriteTimeUtc)
                     .FirstOrDefault();
@@ -432,7 +432,7 @@ namespace MediaBrowser.Api.Playback.Hls
             CancellationToken cancellationToken)
         {
             // If all transcoding has completed, just return immediately
-            if (transcodingJob != null && transcodingJob.HasExited && File.Exists(segmentPath))
+            if (transcodingJob != null && transcodingJob.HasExited && FileSystem.FileExists(segmentPath))
             {
                 return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob);
             }
@@ -452,7 +452,7 @@ namespace MediaBrowser.Api.Playback.Hls
                             // If it appears in the playlist, it's done
                             if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
                             {
-                                if (File.Exists(segmentPath))
+                                if (FileSystem.FileExists(segmentPath))
                                 {
                                     return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob);
                                 }
@@ -989,4 +989,4 @@ namespace MediaBrowser.Api.Playback.Hls
             return base.CanStreamCopyVideo(request, videoStream);
         }
     }
-}
+}

+ 1 - 0
MediaBrowser.Api/Playback/Hls/VideoHlsService.cs

@@ -8,6 +8,7 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using ServiceStack;
 using System;
+using CommonIO;
 
 namespace MediaBrowser.Api.Playback.Hls
 {

+ 11 - 5
MediaBrowser.Api/Playback/MediaInfoService.cs

@@ -57,7 +57,7 @@ namespace MediaBrowser.Api.Playback
             Size = 102400;
         }
     }
-    
+
     [Authenticated]
     public class MediaInfoService : BaseApiService
     {
@@ -289,7 +289,7 @@ namespace MediaBrowser.Api.Playback
             if (mediaSource.SupportsDirectStream)
             {
                 options.MaxBitrate = GetMaxBitrate(maxBitrate);
-                
+
                 // The MediaSource supports direct stream, now test to see if the client supports it
                 var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
                     streamBuilder.BuildAudioItem(options) :
@@ -309,7 +309,7 @@ namespace MediaBrowser.Api.Playback
             if (mediaSource.SupportsTranscoding)
             {
                 options.MaxBitrate = GetMaxBitrate(maxBitrate);
-                
+
                 // The MediaSource supports direct stream, now test to see if the client supports it
                 var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
                     streamBuilder.BuildAudioItem(options) :
@@ -336,9 +336,15 @@ namespace MediaBrowser.Api.Playback
             var maxBitrate = clientMaxBitrate;
             var remoteClientMaxBitrate = _config.Configuration.RemoteClientBitrateLimit;
 
-            if (remoteClientMaxBitrate > 0 && !_networkManager.IsInLocalNetwork(Request.RemoteIp))
+            if (remoteClientMaxBitrate > 0)
             {
-                maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
+                var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.RemoteIp);
+
+                Logger.Info("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.RemoteIp, isInLocalNetwork);
+                if (!isInLocalNetwork)
+                {
+                    maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
+                }
             }
 
             return maxBitrate;

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

@@ -11,6 +11,7 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using ServiceStack;
 using System.Collections.Generic;
+using CommonIO;
 
 namespace MediaBrowser.Api.Playback.Progressive
 {

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

@@ -17,6 +17,7 @@ using System.Globalization;
 using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Api.Playback.Progressive
 {
@@ -139,7 +140,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             }
 
             var outputPath = state.OutputFilePath;
-            var outputPathExists = File.Exists(outputPath);
+			var outputPathExists = FileSystem.FileExists(outputPath);
 
             var isTranscodeCached = outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive);
 
@@ -325,7 +326,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             {
                 TranscodingJob job;
 
-                if (!File.Exists(outputPath))
+				if (!FileSystem.FileExists(outputPath))
                 {
                     job = await StartFfMpeg(state, outputPath, cancellationTokenSource).ConfigureAwait(false);
                 }

+ 1 - 0
MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Api.Playback.Progressive
 {

+ 2 - 0
MediaBrowser.Api/Playback/Progressive/VideoService.cs

@@ -11,12 +11,14 @@ using MediaBrowser.Model.Serialization;
 using ServiceStack;
 using System;
 using System.IO;
+using CommonIO;
 
 namespace MediaBrowser.Api.Playback.Progressive
 {
     /// <summary>
     /// Class GetVideoStream
     /// </summary>
+    [Route("/Videos/{Id}/stream.mpegts", "GET")]
     [Route("/Videos/{Id}/stream.ts", "GET")]
     [Route("/Videos/{Id}/stream.webm", "GET")]
     [Route("/Videos/{Id}/stream.asf", "GET")]

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

@@ -74,7 +74,7 @@ namespace MediaBrowser.Api.Playback
         {
             get
             {
-                return ReadInputAtNativeFramerate ? 1000 : 0;
+                return 0;
             }
         }
 
@@ -457,6 +457,17 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
+        public string TargetVideoCodecTag
+        {
+            get
+            {
+                var stream = VideoStream;
+                return !Request.Static
+                    ? null
+                    : stream == null ? null : stream.CodecTag;
+            }
+        }
+
         public bool? IsTargetAnamorphic
         {
             get

+ 25 - 1
MediaBrowser.Api/PlaylistService.cs

@@ -40,10 +40,27 @@ namespace MediaBrowser.Api
         /// Gets or sets the user id.
         /// </summary>
         /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
         public string UserId { get; set; }
     }
 
+    [Route("/Playlists/{Id}/Items/{ItemId}/Move/{NewIndex}", "POST", Summary = "Moves a playlist item")]
+    public class MoveItem : IReturnVoid
+    {
+        [ApiMember(Name = "ItemId", Description = "ItemId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string ItemId { get; set; }
+
+        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "NewIndex", Description = "NewIndex", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public int NewIndex { get; set; }
+    }
+
     [Route("/Playlists/{Id}/Items", "DELETE", Summary = "Removes items from a playlist")]
     public class RemoveFromPlaylist : IReturnVoid
     {
@@ -105,6 +122,13 @@ namespace MediaBrowser.Api
             _libraryManager = libraryManager;
         }
 
+        public void Post(MoveItem request)
+        {
+            var task = _playlistManager.MoveItem(request.Id, request.ItemId, request.NewIndex);
+
+            Task.WaitAll(task);
+        }
+
         public async Task<object> Post(CreatePlaylist request)
         {
             var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest

+ 18 - 0
MediaBrowser.Api/PluginService.cs

@@ -118,6 +118,14 @@ namespace MediaBrowser.Api
         public string Name { get; set; }
     }
 
+    [Route("/Appstore/Register", "POST", Summary = "Registers an appstore sale")]
+    [Authenticated]
+    public class RegisterAppstoreSale
+    {
+        [ApiMember(Name = "Parameters", Description = "Java representation of parameters to pass through to admin server", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string Parameters { get; set; }
+    }
+
     /// <summary>
     /// Class PluginsService
     /// </summary>
@@ -265,6 +273,16 @@ namespace MediaBrowser.Api
             return ToOptimizedSerializedResultUsingCache(result);
         }
 
+        /// <summary>
+        /// Post app store sale
+        /// </summary>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        public async Task Post(RegisterAppstoreSale request)
+        {
+            await _securityManager.RegisterAppStoreSale(request.Parameters);
+        }
+
         /// <summary>
         /// Posts the specified request.
         /// </summary>

+ 0 - 1
MediaBrowser.Api/Reports/Common/HeaderMetadata.cs

@@ -43,7 +43,6 @@ namespace MediaBrowser.Api.Reports
 		MusicArtist,
 		AudioAlbum,
         Locked,
-        Unidentified,
         ImagePrimary,
         ImageBackdrop,
         ImageLogo,

+ 0 - 1
MediaBrowser.Api/Reports/Common/ItemViewType.cs

@@ -17,7 +17,6 @@ namespace MediaBrowser.Api.Reports
         TrailersImage,
         SpecialsImage,
         LockDataImage,
-        UnidentifiedImage,
         TagsPrimaryImage,
         TagsBackdropImage,
         TagsLogoImage,

+ 0 - 19
MediaBrowser.Api/Reports/Data/ReportBuilder.cs

@@ -105,7 +105,6 @@ namespace MediaBrowser.Api.Reports
 					{   
                         HeaderMetadata.Status,                     
                         HeaderMetadata.Locked,
-						HeaderMetadata.Unidentified,
                         HeaderMetadata.ImagePrimary,
                         HeaderMetadata.ImageBackdrop,
                         HeaderMetadata.ImageLogo,
@@ -122,7 +121,6 @@ namespace MediaBrowser.Api.Reports
 					{     
                         HeaderMetadata.Status,
                         HeaderMetadata.Locked,
-						HeaderMetadata.Unidentified,
                         HeaderMetadata.ImagePrimary,
                         HeaderMetadata.ImageBackdrop,
                         HeaderMetadata.ImageLogo,
@@ -143,7 +141,6 @@ namespace MediaBrowser.Api.Reports
 					{
                         HeaderMetadata.Status,
                         HeaderMetadata.Locked,
-						HeaderMetadata.Unidentified,
                         HeaderMetadata.ImagePrimary,
                         HeaderMetadata.ImageBackdrop,
                         HeaderMetadata.ImageLogo,
@@ -161,7 +158,6 @@ namespace MediaBrowser.Api.Reports
 					{
                         HeaderMetadata.Status,
                         HeaderMetadata.Locked,
-						HeaderMetadata.Unidentified,
                         HeaderMetadata.ImagePrimary,
                         HeaderMetadata.ImageBackdrop,
                         HeaderMetadata.ImageLogo,
@@ -177,7 +173,6 @@ namespace MediaBrowser.Api.Reports
 					{
                         HeaderMetadata.Status,
                         HeaderMetadata.Locked,
-						HeaderMetadata.Unidentified,
                         HeaderMetadata.ImagePrimary,
                         HeaderMetadata.ImageBackdrop,
                         HeaderMetadata.ImageLogo,
@@ -198,7 +193,6 @@ namespace MediaBrowser.Api.Reports
 					{
                         HeaderMetadata.Status,
                         HeaderMetadata.Locked,
-						HeaderMetadata.Unidentified,
                         HeaderMetadata.ImagePrimary,
                         HeaderMetadata.ImageBackdrop,
                         HeaderMetadata.ImageLogo,
@@ -223,7 +217,6 @@ namespace MediaBrowser.Api.Reports
 					{
                         HeaderMetadata.Status,
                         HeaderMetadata.Locked,
-						HeaderMetadata.Unidentified,
                         HeaderMetadata.ImagePrimary,
                         HeaderMetadata.ImageBackdrop,
                         HeaderMetadata.ImageLogo,
@@ -241,7 +234,6 @@ namespace MediaBrowser.Api.Reports
 					{
                         HeaderMetadata.Status,
                         HeaderMetadata.Locked,
-						HeaderMetadata.Unidentified,
                         HeaderMetadata.ImagePrimary,
                         HeaderMetadata.ImageBackdrop,
                         HeaderMetadata.ImageLogo,
@@ -260,7 +252,6 @@ namespace MediaBrowser.Api.Reports
 					{
                         HeaderMetadata.Status,
                         HeaderMetadata.Locked,
-						HeaderMetadata.Unidentified,
                         HeaderMetadata.ImagePrimary,
                         HeaderMetadata.ImageBackdrop,
                         HeaderMetadata.ImageLogo,
@@ -284,7 +275,6 @@ namespace MediaBrowser.Api.Reports
 					{
                         HeaderMetadata.Status,
                         HeaderMetadata.Locked,
-						HeaderMetadata.Unidentified,
                         HeaderMetadata.ImagePrimary,
                         HeaderMetadata.ImageBackdrop,
                         HeaderMetadata.ImageLogo,
@@ -315,11 +305,9 @@ namespace MediaBrowser.Api.Reports
 					{
                         HeaderMetadata.Status,
                         HeaderMetadata.Locked,
-						HeaderMetadata.Unidentified,
                         HeaderMetadata.ImagePrimary,
                         HeaderMetadata.ImageBackdrop,
                         HeaderMetadata.ImageLogo,
-						HeaderMetadata.Unidentified,
                         HeaderMetadata.ImagePrimary,
                         HeaderMetadata.ImageBackdrop,
                         HeaderMetadata.ImageLogo,
@@ -376,12 +364,6 @@ namespace MediaBrowser.Api.Reports
                     option.Header.CanGroup = false;
                     option.Header.DisplayType = ReportDisplayType.Export;
                     break;
-                case HeaderMetadata.Unidentified:
-                    option.Column = (i, r) => this.GetBoolString(r.IsUnidentified);
-                    option.Header.ItemViewType = ItemViewType.UnidentifiedImage;
-                    option.Header.CanGroup = false;
-                    option.Header.DisplayType = ReportDisplayType.Export;
-                    break;
                 case HeaderMetadata.ImagePrimary:
                     option.Column = (i, r) => this.GetBoolString(r.HasImageTagsPrimary);
                     option.Header.ItemViewType = ItemViewType.TagsPrimaryImage;
@@ -633,7 +615,6 @@ namespace MediaBrowser.Api.Reports
             {
                 Id = item.Id.ToString("N"),
                 HasLockData = item.IsLocked,
-                IsUnidentified = item.IsUnidentified,
                 HasLocalTrailer = hasTrailers != null ? hasTrailers.GetTrailerIds().Count() > 0 : false,
                 HasImageTagsPrimary = (item.ImageInfos != null && item.ImageInfos.Count(n => n.Type == ImageType.Primary) > 0),
                 HasImageTagsBackdrop = (item.ImageInfos != null && item.ImageInfos.Count(n => n.Type == ImageType.Backdrop) > 0),

+ 0 - 4
MediaBrowser.Api/Reports/Model/ReportRow.cs

@@ -56,10 +56,6 @@ namespace MediaBrowser.Api.Reports
 		/// <value> true if this object has specials, false if not. </value>
 		public bool HasSpecials { get; set; }
 
-		/// <summary> Gets or sets a value indicating whether this object is unidentified. </summary>
-		/// <value> true if this object is unidentified, false if not. </value>
-		public bool IsUnidentified { get; set; }
-
 		/// <summary> Gets or sets the columns. </summary>
 		/// <value> The columns. </value>
 		public List<ReportItem> Columns { get; set; }

+ 0 - 1
MediaBrowser.Api/Reports/ReportsService.cs

@@ -226,7 +226,6 @@ namespace MediaBrowser.Api.Reports
                 NameStartsWithOrGreater = request.NameStartsWithOrGreater,
                 HasImdbId = request.HasImdbId,
                 IsYearMismatched = request.IsYearMismatched,
-                IsUnidentified = request.IsUnidentified,
                 IsPlaceHolder = request.IsPlaceHolder,
                 IsLocked = request.IsLocked,
                 IsInBoxSet = request.IsInBoxSet,

+ 1 - 1
MediaBrowser.Api/Reports/Stat/ReportStatBuilder.cs

@@ -213,7 +213,7 @@ namespace MediaBrowser.Api.Reports
             };
             foreach (var item in t)
             {
-                var ps = items.Where(x => x.People != null && x.SupportsPeople).SelectMany(x => x.People)
+                var ps = items.SelectMany(x => _libraryManager.GetPeople(x))
                                 .Where(n => n.Type == item.ToString())
                                 .GroupBy(x => x.Name)
                                 .OrderByDescending(x => x.Count())

+ 16 - 2
MediaBrowser.Api/Social/SharingService.cs

@@ -124,7 +124,7 @@ namespace MediaBrowser.Api.Social
             Task.WaitAll(task);
         }
 
-        public object Get(GetShareImage request)
+        public async Task<object> Get(GetShareImage request)
         {
             var share = _sharingManager.GetShareInfo(request.Id);
 
@@ -143,7 +143,21 @@ namespace MediaBrowser.Api.Social
 
             if (image != null)
             {
-                return ToStaticFileResult(image.Path);
+                if (image.IsLocalFile)
+                {
+                    return ToStaticFileResult(image.Path);
+                }
+
+                try
+                {
+                    // Don't fail the request over this
+                    var updatedImage = await _libraryManager.ConvertImageToLocal(item, image, 0).ConfigureAwait(false);
+                    return ToStaticFileResult(updatedImage.Path);
+                }
+                catch
+                {
+                    
+                }
             }
 
             // Grab a dlna icon if nothing else is available

+ 2 - 1
MediaBrowser.Api/StartupWizardService.cs

@@ -69,10 +69,11 @@ namespace MediaBrowser.Api
             _config.Configuration.MergeMetadataAndImagesByName = true;
             _config.Configuration.EnableStandaloneMetadata = true;
             _config.Configuration.EnableLibraryMetadataSubFolder = true;
-            _config.Configuration.EnableUserSpecificUserViews = true;
             _config.Configuration.EnableCustomPathSubFolders = true;
             _config.Configuration.DisableXmlSavers = true;
             _config.Configuration.DisableStartupScan = true;
+            _config.Configuration.EnableUserViews = true;
+            _config.Configuration.EnableDateLastRefresh = true;
             _config.SaveConfiguration();
         }
 

+ 6 - 2
MediaBrowser.Api/Subtitles/SubtitleService.cs

@@ -15,7 +15,9 @@ using System.Linq;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
+using MediaBrowser.Common.IO;
 
 namespace MediaBrowser.Api.Subtitles
 {
@@ -127,14 +129,16 @@ namespace MediaBrowser.Api.Subtitles
         private readonly ISubtitleEncoder _subtitleEncoder;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IProviderManager _providerManager;
+        private readonly IFileSystem _fileSystem;
 
-        public SubtitleService(ILibraryManager libraryManager, ISubtitleManager subtitleManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager, IProviderManager providerManager)
+        public SubtitleService(ILibraryManager libraryManager, ISubtitleManager subtitleManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager, IProviderManager providerManager, IFileSystem fileSystem)
         {
             _libraryManager = libraryManager;
             _subtitleManager = subtitleManager;
             _subtitleEncoder = subtitleEncoder;
             _mediaSourceManager = mediaSourceManager;
             _providerManager = providerManager;
+            _fileSystem = fileSystem;
         }
 
         public async Task<object> Get(GetSubtitlePlaylist request)
@@ -259,7 +263,7 @@ namespace MediaBrowser.Api.Subtitles
                     await _subtitleManager.DownloadSubtitles(video, request.SubtitleId, CancellationToken.None)
                         .ConfigureAwait(false);
 
-                    _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions());
+                    _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(_fileSystem));
                 }
                 catch (Exception ex)
                 {

+ 9 - 1
MediaBrowser.Api/Sync/SyncService.cs

@@ -244,7 +244,15 @@ namespace MediaBrowser.Api.Sync
             var task = _syncManager.ReportSyncJobItemTransferBeginning(request.Id);
             Task.WaitAll(task);
 
-            return ToStaticFileResult(jobItem.OutputPath);
+            return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
+            {
+                Path = jobItem.OutputPath,
+                OnError = () =>
+                {
+                    var failedTask = _syncManager.ReportSyncJobItemTransferFailed(request.Id);
+                    Task.WaitAll(failedTask);
+                }
+            });
         }
 
         public object Get(GetSyncDialogOptions request)

+ 5 - 6
MediaBrowser.Api/System/SystemService.cs

@@ -12,6 +12,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Api.System
 {
@@ -118,18 +119,17 @@ namespace MediaBrowser.Api.System
 
         public object Get(GetServerLogs request)
         {
-            List<FileInfo> files;
+            List<FileSystemMetadata> files;
 
             try
             {
-                files = new DirectoryInfo(_appPaths.LogDirectoryPath)
-                    .EnumerateFiles("*", SearchOption.AllDirectories)
+				files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
                     .Where(i => string.Equals(i.Extension, ".txt", StringComparison.OrdinalIgnoreCase))
                     .ToList();
             }
             catch (DirectoryNotFoundException)
             {
-                files = new List<FileInfo>();
+                files = new List<FileSystemMetadata>();
             }
 
             var result = files.Select(i => new LogFile
@@ -149,8 +149,7 @@ namespace MediaBrowser.Api.System
 
         public object Get(GetLogFile request)
         {
-            var file = new DirectoryInfo(_appPaths.LogDirectoryPath)
-                .EnumerateFiles("*", SearchOption.AllDirectories)
+			var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
                 .First(i => string.Equals(i.Name, request.Name, StringComparison.OrdinalIgnoreCase));
 
             return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.ReadWrite);

+ 0 - 3
MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs

@@ -299,9 +299,6 @@ namespace MediaBrowser.Api.UserLibrary
         [ApiMember(Name = "IsLocked", Description = "Optional filter by items that are locked.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public bool? IsLocked { get; set; }
 
-        [ApiMember(Name = "IsUnidentified", Description = "Optional filter by items that are unidentified by internet metadata providers.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? IsUnidentified { get; set; }
-
         [ApiMember(Name = "IsPlaceHolder", Description = "Optional filter by items that are placeholders", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public bool? IsPlaceHolder { get; set; }
 

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

@@ -192,7 +192,6 @@ namespace MediaBrowser.Api.UserLibrary
                 NameStartsWithOrGreater = request.NameStartsWithOrGreater,
                 HasImdbId = request.HasImdbId,
                 IsYearMismatched = request.IsYearMismatched,
-                IsUnidentified = request.IsUnidentified,
                 IsPlaceHolder = request.IsPlaceHolder,
                 IsLocked = request.IsLocked,
                 IsInBoxSet = request.IsInBoxSet,

+ 34 - 0
MediaBrowser.Api/UserLibrary/UserViewsService.cs

@@ -39,6 +39,17 @@ namespace MediaBrowser.Api.UserLibrary
         public string UserId { get; set; }
     }
 
+    [Route("/Users/{UserId}/GroupingOptions", "GET")]
+    public class GetGroupingOptions : IReturn<List<SpecialViewOption>>
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string UserId { get; set; }
+    }
+
     public class UserViewsService : BaseApiService
     {
         private readonly IUserManager _userManager;
@@ -105,6 +116,29 @@ namespace MediaBrowser.Api.UserLibrary
             return ToOptimizedResult(list);
         }
 
+        public async Task<object> Get(GetGroupingOptions request)
+        {
+            var user = _userManager.GetUserById(request.UserId);
+
+            var views = user.RootFolder
+                .GetChildren(user, true)
+                .OfType<Folder>()
+                .Where(i => !UserView.IsExcludedFromGrouping(i))
+                .ToList();
+
+            var list = views
+                .Select(i => new SpecialViewOption
+                {
+                    Name = i.Name,
+                    Id = i.Id.ToString("N")
+
+                })
+            .OrderBy(i => i.Name)
+            .ToList();
+
+            return ToOptimizedResult(list);
+        }
+
         private bool IsEligibleForSpecialView(ICollectionFolder view)
         {
             var types = new[] { CollectionType.Movies, CollectionType.TvShows, CollectionType.Games, CollectionType.Music, CollectionType.Photos };

+ 1 - 0
MediaBrowser.Api/VideosService.cs

@@ -11,6 +11,7 @@ using System;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Api
 {

+ 3 - 1
MediaBrowser.Api/packages.config

@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="morelinq" version="1.1.0" targetFramework="net45" />
+  <package id="CommonIO" version="1.0.0.5" targetFramework="net45" />
+  <package id="morelinq" version="1.1.1" targetFramework="net45" />
+  <package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" />
 </packages>

+ 14 - 5
MediaBrowser.Common.Implementations/Archiving/ZipClient.cs

@@ -7,6 +7,8 @@ using SharpCompress.Reader;
 using SharpCompress.Reader.Zip;
 using System;
 using System.IO;
+using CommonIO;
+using MediaBrowser.Common.IO;
 
 namespace MediaBrowser.Common.Implementations.Archiving
 {
@@ -15,7 +17,14 @@ namespace MediaBrowser.Common.Implementations.Archiving
     /// </summary>
     public class ZipClient : IZipClient
     {
-        /// <summary>
+		private IFileSystem _fileSystem;
+
+		public ZipClient(IFileSystem fileSystem) 
+		{
+			_fileSystem = fileSystem;
+		}
+
+		/// <summary>
         /// Extracts all.
         /// </summary>
         /// <param name="sourceFile">The source file.</param>
@@ -23,7 +32,7 @@ namespace MediaBrowser.Common.Implementations.Archiving
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles)
         {
-            using (var fileStream = File.OpenRead(sourceFile))
+			using (var fileStream = _fileSystem.OpenRead(sourceFile))
             {
                 ExtractAll(fileStream, targetPath, overwriteExistingFiles);
             }
@@ -73,7 +82,7 @@ namespace MediaBrowser.Common.Implementations.Archiving
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles)
         {
-            using (var fileStream = File.OpenRead(sourceFile))
+			using (var fileStream = _fileSystem.OpenRead(sourceFile))
             {
                 ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles);
             }
@@ -112,7 +121,7 @@ namespace MediaBrowser.Common.Implementations.Archiving
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles)
         {
-            using (var fileStream = File.OpenRead(sourceFile))
+			using (var fileStream = _fileSystem.OpenRead(sourceFile))
             {
                 ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles);
             }
@@ -150,7 +159,7 @@ namespace MediaBrowser.Common.Implementations.Archiving
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         public void ExtractAllFromRar(string sourceFile, string targetPath, bool overwriteExistingFiles)
         {
-            using (var fileStream = File.OpenRead(sourceFile))
+			using (var fileStream = _fileSystem.OpenRead(sourceFile))
             {
                 ExtractAllFromRar(fileStream, targetPath, overwriteExistingFiles);
             }

+ 19 - 8
MediaBrowser.Common.Implementations/BaseApplicationHost.cs

@@ -30,6 +30,7 @@ using System.Reflection;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Common.Implementations
 {
@@ -93,7 +94,7 @@ namespace MediaBrowser.Common.Implementations
         /// <summary>
         /// The _XML serializer
         /// </summary>
-        protected readonly IXmlSerializer XmlSerializer = new XmlSerializer();
+        protected readonly IXmlSerializer XmlSerializer;
 
         /// <summary>
         /// Gets assemblies that failed to load
@@ -180,7 +181,7 @@ namespace MediaBrowser.Common.Implementations
             {
                 if (_deviceId == null)
                 {
-                    _deviceId = new DeviceId(ApplicationPaths, LogManager.GetLogger("SystemId"));
+                    _deviceId = new DeviceId(ApplicationPaths, LogManager.GetLogger("SystemId"), FileSystemManager);
                 }
 
                 return _deviceId.Value;
@@ -199,6 +200,7 @@ namespace MediaBrowser.Common.Implementations
             ILogManager logManager, 
             IFileSystem fileSystem)
         {
+			XmlSerializer = new MediaBrowser.Common.Implementations.Serialization.XmlSerializer (fileSystem);
             FailedAssemblies = new List<string>();
 
             ApplicationPaths = applicationPaths;
@@ -320,7 +322,7 @@ namespace MediaBrowser.Common.Implementations
 
         protected virtual IJsonSerializer CreateJsonSerializer()
         {
-            return new JsonSerializer();
+            return new JsonSerializer(FileSystemManager);
         }
 
         private void SetHttpLimit()
@@ -414,6 +416,8 @@ namespace MediaBrowser.Common.Implementations
         /// </summary>
         protected virtual void FindParts()
         {
+            RegisterModules();
+            
             ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
             Plugins = GetExports<IPlugin>();
         }
@@ -449,7 +453,7 @@ namespace MediaBrowser.Common.Implementations
 
 			RegisterSingleInstance<IApplicationPaths>(ApplicationPaths);
 
-			TaskManager = new TaskManager(ApplicationPaths, JsonSerializer, Logger);
+			TaskManager = new TaskManager(ApplicationPaths, JsonSerializer, Logger, FileSystemManager);
 
 			RegisterSingleInstance(JsonSerializer);
 			RegisterSingleInstance(XmlSerializer);
@@ -473,13 +477,12 @@ namespace MediaBrowser.Common.Implementations
 			InstallationManager = new InstallationManager(Logger, this, ApplicationPaths, HttpClient, JsonSerializer, SecurityManager, ConfigurationManager, FileSystemManager);
 			RegisterSingleInstance(InstallationManager);
 
-			ZipClient = new ZipClient();
+			ZipClient = new ZipClient(FileSystemManager);
 			RegisterSingleInstance(ZipClient);
 
 			IsoManager = new IsoManager();
 			RegisterSingleInstance(IsoManager);
 
-			RegisterModules();
 			return Task.FromResult (true);
         }
 
@@ -522,6 +525,14 @@ namespace MediaBrowser.Common.Implementations
             }
             catch (ReflectionTypeLoadException ex)
             {
+                if (ex.LoaderExceptions != null)
+                {
+                    foreach (var loaderException in ex.LoaderExceptions)
+                    {
+                        Logger.Error("LoaderException: " + loaderException.Message);
+                    }
+                }
+                
                 // If it fails we can still get a list of the Types it was able to resolve
                 return ex.Types.Where(t => t != null);
             }
@@ -581,7 +592,7 @@ namespace MediaBrowser.Common.Implementations
         protected void RegisterSingleInstance<T>(T obj, bool manageLifetime = true)
             where T : class
         {
-            Container.RegisterSingle(obj);
+            Container.RegisterSingleton(obj);
 
             if (manageLifetime)
             {
@@ -607,7 +618,7 @@ namespace MediaBrowser.Common.Implementations
         protected void RegisterSingleInstance<T>(Func<T> func)
             where T : class
         {
-            Container.RegisterSingle(func);
+            Container.RegisterSingleton(func);
         }
 
         void IDependencyContainer.Register(Type typeInterface, Type typeImplementation)

+ 28 - 8
MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs

@@ -9,6 +9,8 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Threading;
+using CommonIO;
+using MediaBrowser.Common.Extensions;
 
 namespace MediaBrowser.Common.Implementations.Configuration
 {
@@ -32,7 +34,7 @@ namespace MediaBrowser.Common.Implementations.Configuration
         /// Occurs when [configuration updating].
         /// </summary>
         public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
-        
+
         /// <summary>
         /// Occurs when [named configuration updated].
         /// </summary>
@@ -54,6 +56,7 @@ namespace MediaBrowser.Common.Implementations.Configuration
         /// </summary>
         /// <value>The application paths.</value>
         public IApplicationPaths CommonApplicationPaths { get; private set; }
+        public readonly IFileSystem FileSystem;
 
         /// <summary>
         /// The _configuration loaded
@@ -87,8 +90,8 @@ namespace MediaBrowser.Common.Implementations.Configuration
             }
         }
 
-        private ConfigurationStore[] _configurationStores = {};
-        private IConfigurationFactory[] _configurationFactories = {};
+        private ConfigurationStore[] _configurationStores = { };
+        private IConfigurationFactory[] _configurationFactories = { };
 
         /// <summary>
         /// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
@@ -96,10 +99,11 @@ namespace MediaBrowser.Common.Implementations.Configuration
         /// <param name="applicationPaths">The application paths.</param>
         /// <param name="logManager">The log manager.</param>
         /// <param name="xmlSerializer">The XML serializer.</param>
-        protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILogManager logManager, IXmlSerializer xmlSerializer)
+        protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILogManager logManager, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
         {
             CommonApplicationPaths = applicationPaths;
             XmlSerializer = xmlSerializer;
+            FileSystem = fileSystem;
             Logger = logManager.GetLogger(GetType().Name);
 
             UpdateCachePath();
@@ -199,9 +203,19 @@ namespace MediaBrowser.Common.Implementations.Configuration
                 {
                     throw new DirectoryNotFoundException(string.Format("{0} does not exist.", newPath));
                 }
+
+                EnsureWriteAccess(newPath);
             }
         }
 
+        protected void EnsureWriteAccess(string path)
+        {
+            var file = Path.Combine(path, Guid.NewGuid().ToString());
+
+            FileSystem.WriteAllText(file, string.Empty);
+            FileSystem.DeleteFile(file);
+        }
+
         private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
 
         private string GetConfigurationFile(string key)
@@ -215,9 +229,15 @@ namespace MediaBrowser.Common.Implementations.Configuration
             {
                 var file = GetConfigurationFile(key);
 
-                var configurationType = _configurationStores
-                    .First(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase))
-                    .ConfigurationType;
+                var configurationInfo = _configurationStores
+                    .FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
+
+                if (configurationInfo == null)
+                {
+                    throw new ResourceNotFoundException("Configuration with key " + key + " not found.");
+                }
+
+                var configurationType = configurationInfo.ConfigurationType;
 
                 lock (_configurationSyncLock)
                 {
@@ -272,7 +292,7 @@ namespace MediaBrowser.Common.Implementations.Configuration
                 NewConfiguration = configuration
 
             }, Logger);
-            
+
             _configurations.AddOrUpdate(key, configuration, (k, v) => configuration);
 
             var path = GetConfigurationFile(key);

+ 4 - 3
MediaBrowser.Common.Implementations/Configuration/ConfigurationHelper.cs

@@ -2,6 +2,7 @@
 using System;
 using System.IO;
 using System.Linq;
+using MediaBrowser.Common.IO;
 
 namespace MediaBrowser.Common.Implementations.Configuration
 {
@@ -27,7 +28,7 @@ namespace MediaBrowser.Common.Implementations.Configuration
             // Use try/catch to avoid the extra file system lookup using File.Exists
             try
             {
-                buffer = File.ReadAllBytes(path);
+				buffer = File.ReadAllBytes(path);
 
                 configuration = xmlSerializer.DeserializeFromBytes(type, buffer);
             }
@@ -46,10 +47,10 @@ namespace MediaBrowser.Common.Implementations.Configuration
                 // If the file didn't exist before, or if something has changed, re-save
                 if (buffer == null || !buffer.SequenceEqual(newBytes))
                 {
-                    Directory.CreateDirectory(Path.GetDirectoryName(path));
+					Directory.CreateDirectory(Path.GetDirectoryName(path));
 
                     // Save it after load in case we got new items
-                    File.WriteAllBytes(path, newBytes);
+					File.WriteAllBytes(path, newBytes);
                 }
 
                 return configuration;

+ 13 - 5
MediaBrowser.Common.Implementations/Devices/DeviceId.cs

@@ -3,13 +3,16 @@ using MediaBrowser.Model.Logging;
 using System;
 using System.IO;
 using System.Text;
+using CommonIO;
+using MediaBrowser.Common.IO;
 
 namespace MediaBrowser.Common.Implementations.Devices
 {
     public class DeviceId
     {
         private readonly IApplicationPaths _appPaths;
-        private readonly ILogger _logger;
+		private readonly ILogger _logger;
+		private readonly IFileSystem _fileSystem;
 
         private readonly object _syncLock = new object();
 
@@ -24,7 +27,7 @@ namespace MediaBrowser.Common.Implementations.Devices
             {
                 lock (_syncLock)
                 {
-                    var value = File.ReadAllText(CachePath, Encoding.UTF8);
+					var value = File.ReadAllText(CachePath, Encoding.UTF8);
 
                     Guid guid;
                     if (Guid.TryParse(value, out guid))
@@ -55,11 +58,11 @@ namespace MediaBrowser.Common.Implementations.Devices
             {
                 var path = CachePath;
 
-                Directory.CreateDirectory(Path.GetDirectoryName(path));
+				_fileSystem.CreateDirectory(Path.GetDirectoryName(path));
 
                 lock (_syncLock)
                 {
-                    File.WriteAllText(path, id, Encoding.UTF8);
+                    _fileSystem.WriteAllText(path, id, Encoding.UTF8);
                 }
             }
             catch (Exception ex)
@@ -88,10 +91,15 @@ namespace MediaBrowser.Common.Implementations.Devices
 
         private string _id;
 
-        public DeviceId(IApplicationPaths appPaths, ILogger logger)
+        public DeviceId(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem)
         {
+			if (fileSystem == null) {
+				throw new ArgumentNullException ("fileSystem");
+			}
+
             _appPaths = appPaths;
             _logger = logger;
+			_fileSystem = fileSystem;
         }
 
         public string Value

+ 76 - 69
MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs

@@ -17,6 +17,7 @@ using System.Net.Cache;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Common.Implementations.HttpClientManager
 {
@@ -282,8 +283,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
             var url = options.Url;
             var urlHash = url.ToLower().GetMD5().ToString("N");
-            var semaphore = GetLock(url);
-
+            
             var responseCachePath = Path.Combine(_appPaths.CachePath, "httpclient", urlHash);
 
             response = await GetCachedResponse(responseCachePath, options.CacheLength, url).ConfigureAwait(false);
@@ -292,6 +292,8 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
                 return response;
             }
 
+            var semaphore = GetLock(url);
+
             await semaphore.WaitAsync(options.CancellationToken).ConfigureAwait(false);
 
             try
@@ -355,7 +357,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
         private async Task CacheResponse(HttpResponseInfo response, string responseCachePath)
         {
-            Directory.CreateDirectory(Path.GetDirectoryName(responseCachePath));
+            _fileSystem.CreateDirectory(Path.GetDirectoryName(responseCachePath));
 
             using (var responseStream = response.Content)
             {
@@ -464,20 +466,11 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
             }
             catch (OperationCanceledException ex)
             {
-                var exception = GetCancellationException(options.Url, options.CancellationToken, ex);
-
-                var httpException = exception as HttpException;
-
-                if (httpException != null && httpException.IsTimedOut)
-                {
-                    client.LastTimeout = DateTime.UtcNow;
-                }
-
-                throw exception;
+                throw GetCancellationException(options, client, options.CancellationToken, ex);
             }
             catch (Exception ex)
             {
-                throw GetException(ex, options);
+                throw GetException(ex, options, client);
             }
             finally
             {
@@ -488,27 +481,6 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
             }
         }
 
-        /// <summary>
-        /// Gets the exception.
-        /// </summary>
-        /// <param name="ex">The ex.</param>
-        /// <param name="options">The options.</param>
-        /// <returns>HttpException.</returns>
-        private HttpException GetException(WebException ex, HttpRequestOptions options)
-        {
-            _logger.ErrorException("Error getting response from " + options.Url, ex);
-
-            var exception = new HttpException(ex.Message, ex);
-
-            var response = ex.Response as HttpWebResponse;
-            if (response != null)
-            {
-                exception.StatusCode = response.StatusCode;
-            }
-
-            return exception;
-        }
-
         private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, Stream content, long? contentLength, IDisposable disposable)
         {
             return new HttpResponseInfo(disposable)
@@ -599,7 +571,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
         {
             ValidateParams(options);
 
-            Directory.CreateDirectory(_appPaths.TempDirectory);
+            _fileSystem.CreateDirectory(_appPaths.TempDirectory);
 
             var tempFile = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp");
 
@@ -624,6 +596,8 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
                 _logger.Info("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
             }
 
+            var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression);
+
             try
             {
                 options.CancellationToken.ThrowIfCancellationRequested();
@@ -632,7 +606,6 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
                 {
                     var httpResponse = (HttpWebResponse)response;
 
-                    var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression);
                     EnsureSuccessStatusCode(client, httpResponse, options);
 
                     options.CancellationToken.ThrowIfCancellationRequested();
@@ -669,7 +642,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
             catch (Exception ex)
             {
                 DeleteTempFile(tempFile);
-                throw GetException(ex, options);
+                throw GetException(ex, options, client);
             }
             finally
             {
@@ -694,14 +667,37 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
         protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
 
-        private Exception GetException(Exception ex, HttpRequestOptions options)
+        private Exception GetException(Exception ex, HttpRequestOptions options, HttpClientInfo client)
         {
+            if (ex is HttpException)
+            {
+                return ex;
+            }
+
             var webException = ex as WebException
                 ?? ex.InnerException as WebException;
 
             if (webException != null)
             {
-                return GetException(webException, options);
+                if (options.LogErrors)
+                {
+                    _logger.ErrorException("Error getting response from " + options.Url, ex);
+                }
+
+                var exception = new HttpException(ex.Message, ex);
+
+                var response = webException.Response as HttpWebResponse;
+                if (response != null)
+                {
+                    exception.StatusCode = response.StatusCode;
+
+                    if ((int)response.StatusCode == 429)
+                    {
+                        client.LastTimeout = DateTime.UtcNow;
+                    }
+                }
+
+                return exception;
             }
 
             var operationCanceledException = ex as OperationCanceledException
@@ -709,10 +705,13 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
             if (operationCanceledException != null)
             {
-                return GetCancellationException(options.Url, options.CancellationToken, operationCanceledException);
+                return GetCancellationException(options, client, options.CancellationToken, operationCanceledException);
             }
 
-            _logger.ErrorException("Error getting response from " + options.Url, ex);
+            if (options.LogErrors)
+            {
+                _logger.ErrorException("Error getting response from " + options.Url, ex);
+            }
 
             return ex;
         }
@@ -784,18 +783,24 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
         /// <summary>
         /// Throws the cancellation exception.
         /// </summary>
-        /// <param name="url">The URL.</param>
+        /// <param name="options">The options.</param>
+        /// <param name="client">The client.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="exception">The exception.</param>
         /// <returns>Exception.</returns>
-        private Exception GetCancellationException(string url, CancellationToken cancellationToken, OperationCanceledException exception)
+        private Exception GetCancellationException(HttpRequestOptions options, HttpClientInfo client, CancellationToken cancellationToken, OperationCanceledException exception)
         {
             // If the HttpClient's timeout is reached, it will cancel the Task internally
             if (!cancellationToken.IsCancellationRequested)
             {
-                var msg = string.Format("Connection to {0} timed out", url);
+                var msg = string.Format("Connection to {0} timed out", options.Url);
+
+                if (options.LogErrors)
+                {
+                    _logger.Error(msg);
+                }
 
-                _logger.Error(msg);
+                client.LastTimeout = DateTime.UtcNow;
 
                 // Throw an HttpException so that the caller doesn't think it was cancelled by user code
                 return new HttpException(msg, exception)
@@ -815,12 +820,6 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
             if (!isSuccessful)
             {
-                if ((int) statusCode == 429)
-                {
-                    client.LastTimeout = DateTime.UtcNow;
-                }
-
-                if (statusCode == HttpStatusCode.RequestEntityTooLarge)
                 if (options.LogErrorResponseBody)
                 {
                     try
@@ -869,25 +868,11 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
             Task<WebResponse> asyncTask = Task.Factory.FromAsync<WebResponse>(request.BeginGetResponse, request.EndGetResponse, null);
 
             ThreadPool.RegisterWaitForSingleObject((asyncTask as IAsyncResult).AsyncWaitHandle, TimeoutCallback, request, timeout, true);
-            asyncTask.ContinueWith(task =>
-            {
-                taskCompletion.TrySetResult(task.Result);
-
-            }, TaskContinuationOptions.NotOnFaulted);
+            var callback = new TaskCallback { taskCompletion = taskCompletion };
+            asyncTask.ContinueWith(callback.OnSuccess, TaskContinuationOptions.NotOnFaulted);
 
             // Handle errors
-            asyncTask.ContinueWith(task =>
-            {
-                if (task.Exception != null)
-                {
-                    taskCompletion.TrySetException(task.Exception);
-                }
-                else
-                {
-                    taskCompletion.TrySetException(new List<Exception>());
-                }
-
-            }, TaskContinuationOptions.OnlyOnFaulted);
+            asyncTask.ContinueWith(callback.OnError, TaskContinuationOptions.OnlyOnFaulted);
 
             return taskCompletion.Task;
         }
@@ -903,5 +888,27 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
                 }
             }
         }
+
+        private class TaskCallback
+        {
+            public TaskCompletionSource<WebResponse> taskCompletion;
+
+            public void OnSuccess(Task<WebResponse> task)
+            {
+                taskCompletion.TrySetResult(task.Result);
+            }
+
+            public void OnError(Task<WebResponse> task)
+            {
+                if (task.Exception != null)
+                {
+                    taskCompletion.TrySetException(task.Exception);
+                }
+                else
+                {
+                    taskCompletion.TrySetException(new List<Exception>());
+                }
+            }
+        }
     }
 }

+ 0 - 433
MediaBrowser.Common.Implementations/IO/CommonFileSystem.cs

@@ -1,433 +0,0 @@
-using MediaBrowser.Model.Extensions;
-using MediaBrowser.Common.IO;
-using MediaBrowser.Model.Logging;
-using System;
-using System.IO;
-using System.Text;
-
-namespace MediaBrowser.Common.Implementations.IO
-{
-    /// <summary>
-    /// Class CommonFileSystem
-    /// </summary>
-    public class CommonFileSystem : IFileSystem
-    {
-        protected ILogger Logger;
-
-        private readonly bool _supportsAsyncFileStreams;
-        private char[] _invalidFileNameChars;
-
-        public CommonFileSystem(ILogger logger, bool supportsAsyncFileStreams, bool usePresetInvalidFileNameChars)
-        {
-            Logger = logger;
-            _supportsAsyncFileStreams = supportsAsyncFileStreams;
-
-            SetInvalidFileNameChars(usePresetInvalidFileNameChars);
-        }
-
-        private void SetInvalidFileNameChars(bool usePresetInvalidFileNameChars)
-        {
-            // GetInvalidFileNameChars is less restrictive in Linux/Mac than Windows, this mimic Windows behavior for mono under Linux/Mac.
-
-            if (usePresetInvalidFileNameChars)
-            {
-                _invalidFileNameChars = new char[41] { '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
-            '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
-            '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
-            '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' };
-            }
-            else
-            {
-                _invalidFileNameChars = Path.GetInvalidFileNameChars();
-            }
-        }
-
-        /// <summary>
-        /// Determines whether the specified filename is shortcut.
-        /// </summary>
-        /// <param name="filename">The filename.</param>
-        /// <returns><c>true</c> if the specified filename is shortcut; otherwise, <c>false</c>.</returns>
-        /// <exception cref="System.ArgumentNullException">filename</exception>
-        public virtual bool IsShortcut(string filename)
-        {
-            if (string.IsNullOrEmpty(filename))
-            {
-                throw new ArgumentNullException("filename");
-            }
-
-            var extension = Path.GetExtension(filename);
-
-            return string.Equals(extension, ".mblink", StringComparison.OrdinalIgnoreCase);
-        }
-
-        /// <summary>
-        /// Resolves the shortcut.
-        /// </summary>
-        /// <param name="filename">The filename.</param>
-        /// <returns>System.String.</returns>
-        /// <exception cref="System.ArgumentNullException">filename</exception>
-        public virtual string ResolveShortcut(string filename)
-        {
-            if (string.IsNullOrEmpty(filename))
-            {
-                throw new ArgumentNullException("filename");
-            }
-
-            if (string.Equals(Path.GetExtension(filename), ".mblink", StringComparison.OrdinalIgnoreCase))
-            {
-                var path = File.ReadAllText(filename);
-
-                return NormalizePath(path);
-            }
-
-            return null;
-        }
-
-        /// <summary>
-        /// Creates the shortcut.
-        /// </summary>
-        /// <param name="shortcutPath">The shortcut path.</param>
-        /// <param name="target">The target.</param>
-        /// <exception cref="System.ArgumentNullException">
-        /// shortcutPath
-        /// or
-        /// target
-        /// </exception>
-        public void CreateShortcut(string shortcutPath, string target)
-        {
-            if (string.IsNullOrEmpty(shortcutPath))
-            {
-                throw new ArgumentNullException("shortcutPath");
-            }
-
-            if (string.IsNullOrEmpty(target))
-            {
-                throw new ArgumentNullException("target");
-            }
-
-            File.WriteAllText(shortcutPath, target);
-        }
-
-        /// <summary>
-        /// Gets the file system info.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns>FileSystemInfo.</returns>
-        public FileSystemInfo GetFileSystemInfo(string path)
-        {
-            if (string.IsNullOrEmpty(path))
-            {
-                throw new ArgumentNullException("path");
-            }
-
-            // Take a guess to try and avoid two file system hits, but we'll double-check by calling Exists
-            if (Path.HasExtension(path))
-            {
-                var fileInfo = new FileInfo(path);
-
-                if (fileInfo.Exists)
-                {
-                    return fileInfo;
-                }
-
-                return new DirectoryInfo(path);
-            }
-            else
-            {
-                var fileInfo = new DirectoryInfo(path);
-
-                if (fileInfo.Exists)
-                {
-                    return fileInfo;
-                }
-
-                return new FileInfo(path);
-            }
-        }
-
-        /// <summary>
-        /// The space char
-        /// </summary>
-        private const char SpaceChar = ' ';
-
-        /// <summary>
-        /// Takes a filename and removes invalid characters
-        /// </summary>
-        /// <param name="filename">The filename.</param>
-        /// <returns>System.String.</returns>
-        /// <exception cref="System.ArgumentNullException">filename</exception>
-        public string GetValidFilename(string filename)
-        {
-            if (string.IsNullOrEmpty(filename))
-            {
-                throw new ArgumentNullException("filename");
-            }
-
-            var builder = new StringBuilder(filename);
-
-            foreach (var c in _invalidFileNameChars)
-            {
-                builder = builder.Replace(c, SpaceChar);
-            }
-
-            return builder.ToString();
-        }
-
-        /// <summary>
-        /// Gets the creation time UTC.
-        /// </summary>
-        /// <param name="info">The info.</param>
-        /// <returns>DateTime.</returns>
-        public DateTime GetCreationTimeUtc(FileSystemInfo info)
-        {
-            // This could throw an error on some file systems that have dates out of range
-            try
-            {
-                return info.CreationTimeUtc;
-            }
-            catch (Exception ex)
-            {
-                Logger.ErrorException("Error determining CreationTimeUtc for {0}", ex, info.FullName);
-                return DateTime.MinValue;
-            }
-        }
-
-        /// <summary>
-        /// Gets the creation time UTC.
-        /// </summary>
-        /// <param name="info">The info.</param>
-        /// <returns>DateTime.</returns>
-        public DateTime GetLastWriteTimeUtc(FileSystemInfo info)
-        {
-            // This could throw an error on some file systems that have dates out of range
-            try
-            {
-                return info.LastWriteTimeUtc;
-            }
-            catch (Exception ex)
-            {
-                Logger.ErrorException("Error determining LastAccessTimeUtc for {0}", ex, info.FullName);
-                return DateTime.MinValue;
-            }
-        }
-
-        /// <summary>
-        /// Gets the last write time UTC.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns>DateTime.</returns>
-        public DateTime GetLastWriteTimeUtc(string path)
-        {
-            return GetLastWriteTimeUtc(GetFileSystemInfo(path));
-        }
-
-        /// <summary>
-        /// Gets the file stream.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="mode">The mode.</param>
-        /// <param name="access">The access.</param>
-        /// <param name="share">The share.</param>
-        /// <param name="isAsync">if set to <c>true</c> [is asynchronous].</param>
-        /// <returns>FileStream.</returns>
-        public FileStream GetFileStream(string path, FileMode mode, FileAccess access, FileShare share, bool isAsync = false)
-        {
-            if (_supportsAsyncFileStreams && isAsync)
-            {
-                return new FileStream(path, mode, access, share, StreamDefaults.DefaultFileStreamBufferSize, true);
-            }
-
-            return new FileStream(path, mode, access, share, StreamDefaults.DefaultFileStreamBufferSize);
-        }
-
-        /// <summary>
-        /// Swaps the files.
-        /// </summary>
-        /// <param name="file1">The file1.</param>
-        /// <param name="file2">The file2.</param>
-        public void SwapFiles(string file1, string file2)
-        {
-            if (string.IsNullOrEmpty(file1))
-            {
-                throw new ArgumentNullException("file1");
-            }
-
-            if (string.IsNullOrEmpty(file2))
-            {
-                throw new ArgumentNullException("file2");
-            }
-
-            var temp1 = Path.GetTempFileName();
-            var temp2 = Path.GetTempFileName();
-
-            // Copying over will fail against hidden files
-            RemoveHiddenAttribute(file1);
-            RemoveHiddenAttribute(file2);
-
-            File.Copy(file1, temp1, true);
-            File.Copy(file2, temp2, true);
-
-            File.Copy(temp1, file2, true);
-            File.Copy(temp2, file1, true);
-
-            DeleteFile(temp1);
-            DeleteFile(temp2);
-        }
-
-        /// <summary>
-        /// Removes the hidden attribute.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        private void RemoveHiddenAttribute(string path)
-        {
-            if (string.IsNullOrEmpty(path))
-            {
-                throw new ArgumentNullException("path");
-            }
-
-            var currentFile = new FileInfo(path);
-
-            // This will fail if the file is hidden
-            if (currentFile.Exists)
-            {
-                if ((currentFile.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
-                {
-                    currentFile.Attributes &= ~FileAttributes.Hidden;
-                }
-            }
-        }
-
-        public bool ContainsSubPath(string parentPath, string path)
-        {
-            if (string.IsNullOrEmpty(parentPath))
-            {
-                throw new ArgumentNullException("parentPath");
-            }
-
-            if (string.IsNullOrEmpty(path))
-            {
-                throw new ArgumentNullException("path");
-            }
-
-            return path.IndexOf(parentPath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) != -1;
-        }
-
-        public bool IsRootPath(string path)
-        {
-            if (string.IsNullOrEmpty(path))
-            {
-                throw new ArgumentNullException("path");
-            }
-
-            var parent = Path.GetDirectoryName(path);
-
-            if (!string.IsNullOrEmpty(parent))
-            {
-                return false;
-            }
-
-            return true;
-        }
-
-        public string NormalizePath(string path)
-        {
-            if (string.IsNullOrEmpty(path))
-            {
-                throw new ArgumentNullException("path");
-            }
-
-            if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
-            {
-                return path;
-            }
-
-            return path.TrimEnd(Path.DirectorySeparatorChar);
-        }
-
-        public string SubstitutePath(string path, string from, string to)
-        {
-            if (string.IsNullOrWhiteSpace(path))
-            {
-                throw new ArgumentNullException("path");
-            }
-            if (string.IsNullOrWhiteSpace(from))
-            {
-                throw new ArgumentNullException("from");
-            }
-            if (string.IsNullOrWhiteSpace(to))
-            {
-                throw new ArgumentNullException("to");
-            }
-
-            var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase);
-
-            if (!string.Equals(newPath, path))
-            {
-                if (to.IndexOf('/') != -1)
-                {
-                    newPath = newPath.Replace('\\', '/');
-                }
-                else
-                {
-                    newPath = newPath.Replace('/', '\\');
-                }
-            }
-
-            return newPath;
-        }
-
-        public string GetFileNameWithoutExtension(FileSystemInfo info)
-        {
-            if (info is DirectoryInfo)
-            {
-                return info.Name;
-            }
-
-            return Path.GetFileNameWithoutExtension(info.FullName);
-        }
-
-        public string GetFileNameWithoutExtension(string path)
-        {
-            return Path.GetFileNameWithoutExtension(path);
-        }
-
-        public bool IsPathFile(string path)
-        {
-            if (string.IsNullOrWhiteSpace(path))
-            {
-                throw new ArgumentNullException("path");
-            }
-
-            // Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\
-
-            if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 &&
-                !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
-            {
-                return false;
-            }
-            return true;
-
-            //return Path.IsPathRooted(path);
-        }
-
-        public void DeleteFile(string path, bool sendToRecycleBin)
-        {
-            File.Delete(path);
-        }
-
-        public void DeleteDirectory(string path, bool recursive, bool sendToRecycleBin)
-        {
-            Directory.Delete(path, recursive);
-        }
-
-        public void DeleteFile(string path)
-        {
-            DeleteFile(path, false);
-        }
-
-        public void DeleteDirectory(string path, bool recursive)
-        {
-            DeleteDirectory(path, recursive, false);
-        }
-    }
-}

+ 2 - 2
MediaBrowser.Common.Implementations/Logging/NlogManager.cs

@@ -170,7 +170,7 @@ namespace MediaBrowser.Common.Implementations.Logging
         /// </summary>
         /// <param name="name">The name.</param>
         /// <returns>ILogger.</returns>
-        public ILogger GetLogger(string name)
+        public Model.Logging.ILogger GetLogger(string name)
         {
             return new NLogger(name, this);
         }
@@ -208,7 +208,7 @@ namespace MediaBrowser.Common.Implementations.Logging
         {
             LogFilePath = Path.Combine(LogDirectory, LogFilePrefix + "-" + decimal.Round(DateTime.Now.Ticks / 10000000) + ".txt");
 
-            Directory.CreateDirectory(Path.GetDirectoryName(LogFilePath));
+			Directory.CreateDirectory(Path.GetDirectoryName(LogFilePath));
 
             AddFileTarget(LogFilePath, level);
 

+ 11 - 7
MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj

@@ -14,7 +14,6 @@
     <ProductVersion>10.0.0</ProductVersion>
     <SchemaVersion>2.0</SchemaVersion>
     <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
-    <RestorePackages>true</RestorePackages>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
     <DebugSymbols>true</DebugSymbols>
@@ -48,18 +47,24 @@
     <RunPostBuildEvent>Always</RunPostBuildEvent>
   </PropertyGroup>
   <ItemGroup>
-    <Reference Include="NLog, Version=3.2.1.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
+    <Reference Include="CommonIO, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
-      <HintPath>..\packages\NLog.3.2.1\lib\net45\NLog.dll</HintPath>
+      <HintPath>..\packages\CommonIO.1.0.0.5\lib\net45\CommonIO.dll</HintPath>
+    </Reference>
+    <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\NLog.4.1.1\lib\net45\NLog.dll</HintPath>
+    </Reference>
+    <Reference Include="Patterns.Logging">
+      <HintPath>..\packages\Patterns.Logging.1.0.0.2\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.Logging.dll</HintPath>
     </Reference>
     <Reference Include="SharpCompress, Version=0.10.2.0, Culture=neutral, PublicKeyToken=beaf6f427e128133, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
       <HintPath>..\ThirdParty\SharpCompress\SharpCompress.dll</HintPath>
     </Reference>
-    <Reference Include="SimpleInjector, Version=2.7.0.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
+    <Reference Include="SimpleInjector, Version=2.8.0.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
-      <HintPath>..\packages\SimpleInjector.2.8.0\lib\net45\SimpleInjector.dll</HintPath>
-      <Private>True</Private>
+      <HintPath>..\packages\SimpleInjector.3.0.5\lib\net45\SimpleInjector.dll</HintPath>
     </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
@@ -82,7 +87,6 @@
     <Compile Include="Devices\DeviceId.cs" />
     <Compile Include="HttpClientManager\HttpClientInfo.cs" />
     <Compile Include="HttpClientManager\HttpClientManager.cs" />
-    <Compile Include="IO\CommonFileSystem.cs" />
     <Compile Include="IO\IsoManager.cs" />
     <Compile Include="Logging\LogHelper.cs" />
     <Compile Include="Logging\NLogger.cs" />

+ 15 - 7
MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs

@@ -12,6 +12,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Common.Implementations.ScheduledTasks
 {
@@ -51,6 +52,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         /// </summary>
         /// <value>The task manager.</value>
         private ITaskManager TaskManager { get; set; }
+        private readonly IFileSystem _fileSystem;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
@@ -71,7 +73,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         /// or
         /// logger
         /// </exception>
-        public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger)
+        public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem)
         {
             if (scheduledTask == null)
             {
@@ -99,6 +101,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
             TaskManager = taskManager;
             JsonSerializer = jsonSerializer;
             Logger = logger;
+            _fileSystem = fileSystem;
 
             ReloadTriggerEvents(true);
         }
@@ -154,7 +157,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
                 _lastExecutionResult = value;
 
                 var path = GetHistoryFilePath();
-                Directory.CreateDirectory(Path.GetDirectoryName(path));
+				_fileSystem.CreateDirectory(Path.GetDirectoryName(path));
 
                 lock (_lastExecutionResultSyncLock)
                 {
@@ -300,6 +303,11 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
             }
         }
 
+        public void ReloadTriggerEvents()
+        {
+            ReloadTriggerEvents(false);
+        }
+
         /// <summary>
         /// Reloads the trigger events.
         /// </summary>
@@ -552,7 +560,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         {
             var path = GetConfigurationFilePath();
 
-            Directory.CreateDirectory(Path.GetDirectoryName(path));
+			_fileSystem.CreateDirectory(Path.GetDirectoryName(path));
 
             JsonSerializer.SerializeToFile(triggers.Select(ScheduledTaskHelpers.GetTriggerInfo), path);
         }
@@ -622,7 +630,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
                 {
                     try
                     {
-                        Logger.Debug(Name + ": Cancelling");
+                        Logger.Info(Name + ": Cancelling");
                         token.Cancel();
                     }
                     catch (Exception ex)
@@ -635,16 +643,16 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
                 {
                     try
                     {
-                        Logger.Debug(Name + ": Waiting on Task");
+                        Logger.Info(Name + ": Waiting on Task");
                         var exited = Task.WaitAll(new[] { task }, 2000);
 
                         if (exited)
                         {
-                            Logger.Debug(Name + ": Task exited");
+                            Logger.Info(Name + ": Task exited");
                         }
                         else
                         {
-                            Logger.Debug(Name + ": Timed out waiting for task to stop");
+                            Logger.Info(Name + ": Timed out waiting for task to stop");
                         }
                     }
                     catch (Exception ex)

+ 48 - 7
MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs

@@ -10,6 +10,9 @@ using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
+using CommonIO;
+using MediaBrowser.Common.IO;
+using Microsoft.Win32;
 
 namespace MediaBrowser.Common.Implementations.ScheduledTasks
 {
@@ -50,6 +53,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         /// </summary>
         /// <value>The logger.</value>
         private ILogger Logger { get; set; }
+        private readonly IFileSystem _fileSystem;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="TaskManager" /> class.
@@ -58,13 +62,36 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         /// <param name="jsonSerializer">The json serializer.</param>
         /// <param name="logger">The logger.</param>
         /// <exception cref="System.ArgumentException">kernel</exception>
-        public TaskManager(IApplicationPaths applicationPaths, IJsonSerializer jsonSerializer, ILogger logger)
+        public TaskManager(IApplicationPaths applicationPaths, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem)
         {
             ApplicationPaths = applicationPaths;
             JsonSerializer = jsonSerializer;
             Logger = logger;
+            _fileSystem = fileSystem;
 
             ScheduledTasks = new IScheduledTaskWorker[] { };
+
+            BindToSystemEvent();
+        }
+
+        private void BindToSystemEvent()
+        {
+            try
+            {
+                SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
+            }
+            catch
+            {
+
+            }
+        }
+
+        void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
+        {
+            foreach (var task in ScheduledTasks)
+            {
+                task.ReloadTriggerEvents();
+            }
         }
 
         /// <summary>
@@ -106,9 +133,16 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         public void QueueScheduledTask<T>(TaskExecutionOptions options)
             where T : IScheduledTask
         {
-            var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
+            var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T));
 
-            QueueScheduledTask(scheduledTask, options);
+            if (scheduledTask == null)
+            {
+                Logger.Error("Unable to find scheduled task of type {0} in QueueScheduledTask.", typeof(T).Name);
+            }
+            else
+            {
+                QueueScheduledTask(scheduledTask, options);
+            }
         }
 
         public void QueueScheduledTask<T>()
@@ -116,7 +150,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         {
             QueueScheduledTask<T>(new TaskExecutionOptions());
         }
-        
+
         /// <summary>
         /// Queues the scheduled task.
         /// </summary>
@@ -124,9 +158,16 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         /// <param name="options">The task options.</param>
         public void QueueScheduledTask(IScheduledTask task, TaskExecutionOptions options)
         {
-            var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == task.GetType());
+            var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == task.GetType());
 
-            QueueScheduledTask(scheduledTask, options);
+            if (scheduledTask == null)
+            {
+                Logger.Error("Unable to find scheduled task of type {0} in QueueScheduledTask.", task.GetType().Name);
+            }
+            else
+            {
+                QueueScheduledTask(scheduledTask, options);
+            }
         }
 
         /// <summary>
@@ -161,7 +202,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
             var myTasks = ScheduledTasks.ToList();
 
             var list = tasks.ToList();
-            myTasks.AddRange(list.Select(t => new ScheduledTaskWorker(t, ApplicationPaths, this, JsonSerializer, Logger)));
+            myTasks.AddRange(list.Select(t => new ScheduledTaskWorker(t, ApplicationPaths, this, JsonSerializer, Logger, _fileSystem)));
 
             ScheduledTasks = myTasks.ToArray();
         }

+ 6 - 5
MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs

@@ -8,6 +8,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
 {
@@ -95,7 +96,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
         /// <param name="progress">The progress.</param>
         private void DeleteCacheFilesFromDirectory(CancellationToken cancellationToken, string directory, DateTime minDateModified, IProgress<double> progress)
         {
-            var filesToDelete = new DirectoryInfo(directory).EnumerateFiles("*", SearchOption.AllDirectories)
+			var filesToDelete = _fileSystem.GetFiles(directory, true)
                 .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
                 .ToList();
 
@@ -120,14 +121,14 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
             progress.Report(100);
         }
 
-        private static void DeleteEmptyFolders(string parent)
+        private void DeleteEmptyFolders(string parent)
         {
-            foreach (var directory in Directory.GetDirectories(parent))
+            foreach (var directory in _fileSystem.GetDirectoryPaths(parent))
             {
                 DeleteEmptyFolders(directory);
-                if (!Directory.EnumerateFileSystemEntries(directory).Any())
+                if (!_fileSystem.GetFileSystemEntryPaths(directory).Any())
                 {
-                    Directory.Delete(directory, false);
+					_fileSystem.DeleteDirectory(directory, false);
                 }
             }
         }

+ 2 - 1
MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs

@@ -7,6 +7,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
 {
@@ -58,7 +59,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
             // Delete log files more than n days old
             var minDateModified = DateTime.UtcNow.AddDays(-(ConfigurationManager.CommonConfiguration.LogFileRetentionDays));
 
-            var filesToDelete = new DirectoryInfo(ConfigurationManager.CommonApplicationPaths.LogDirectoryPath).EnumerateFileSystemInfos("*", SearchOption.AllDirectories)
+			var filesToDelete = _fileSystem.GetFiles(ConfigurationManager.CommonApplicationPaths.LogDirectoryPath, true)
                           .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
                           .ToList();
 

+ 4 - 4
MediaBrowser.Common.Implementations/Security/MBLicenseFile.cs

@@ -99,7 +99,7 @@ namespace MediaBrowser.Common.Implementations.Security
             {
                 try
                 {
-                    contents = File.ReadAllLines(licenseFile);
+					contents = File.ReadAllLines(licenseFile);
                 }
                 catch (DirectoryNotFoundException)
                 {
@@ -107,7 +107,7 @@ namespace MediaBrowser.Common.Implementations.Security
                 }
                 catch (FileNotFoundException)
                 {
-                    (File.Create(licenseFile)).Close();
+					(File.Create(licenseFile)).Close();
                 }
             }
             if (contents != null && contents.Length > 0)
@@ -150,8 +150,8 @@ namespace MediaBrowser.Common.Implementations.Security
             }
 
             var licenseFile = Filename;
-            Directory.CreateDirectory(Path.GetDirectoryName(licenseFile));
-            lock (_fileLock) File.WriteAllLines(licenseFile, lines);
+			Directory.CreateDirectory(Path.GetDirectoryName(licenseFile));
+			lock (_fileLock) File.WriteAllLines(licenseFile, lines);
         }
     }
 }

+ 67 - 1
MediaBrowser.Common.Implementations/Security/PluginSecurityManager.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Configuration;
+using System.IO;
+using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Security;
 using MediaBrowser.Model.Entities;
@@ -18,6 +19,7 @@ namespace MediaBrowser.Common.Implementations.Security
     public class PluginSecurityManager : ISecurityManager
     {
         private const string MBValidateUrl = MbAdmin.HttpsUrl + "service/registration/validate";
+        private const string AppstoreRegUrl = /*MbAdmin.HttpsUrl*/ "http://mb3admin.com/admin/" + "service/appstore/register";
 
         /// <summary>
         /// The _is MB supporter
@@ -185,6 +187,70 @@ namespace MediaBrowser.Common.Implementations.Security
             }
         }
 
+        /// <summary>
+        /// Register an app store sale with our back-end.  It will validate the transaction with the store
+        /// and then register the proper feature and then fill in the supporter key on success.
+        /// </summary>
+        /// <param name="parameters">Json parameters to send to admin server</param>
+        public async Task RegisterAppStoreSale(string parameters)
+        {
+            var options = new HttpRequestOptions()
+            {
+                Url = AppstoreRegUrl,
+                CancellationToken = CancellationToken.None
+            };
+            options.RequestHeaders.Add("X-Emby-Token", _appHost.SystemId);
+            options.RequestContent = parameters;
+            options.RequestContentType = "application/json";
+
+            try
+            {
+                using (var response = await _httpClient.Post(options).ConfigureAwait(false))
+                {
+                    var reg = _jsonSerializer.DeserializeFromStream<RegRecord>(response.Content);
+
+                    if (reg == null)
+                    {
+                        var msg = "Result from appstore registration was null.";
+                        _logger.Error(msg);
+                        throw new ApplicationException(msg);
+                    }
+                    if (!String.IsNullOrEmpty(reg.key))
+                    {
+                        SupporterKey = reg.key;
+                    }
+                }
+
+            }
+            catch (ApplicationException)
+            {
+                SaveAppStoreInfo(parameters);
+                throw;
+            }
+            catch (Exception e)
+            {
+                _logger.ErrorException("Error registering appstore purchase {0}", e, parameters ?? "NO PARMS SENT");
+                SaveAppStoreInfo(parameters);
+                //TODO - could create a re-try routine on start-up if this file is there.  For now we can handle manually.
+                throw new ApplicationException("Error registering store sale");
+            }
+
+        }
+
+        private void SaveAppStoreInfo(string info)
+        {
+            // Save all transaction information to a file
+
+            try
+            {
+                File.WriteAllText(Path.Combine(_appPaths.ProgramDataPath, "apptrans-error.txt"), info);
+            }
+            catch (IOException)
+            {
+
+            }
+        }
+
         private async Task<MBRegistrationRecord> GetRegistrationStatusInternal(string feature,
             string mb2Equivalent = null,
             string version = null)

+ 1 - 0
MediaBrowser.Common.Implementations/Security/RegRecord.cs

@@ -7,5 +7,6 @@ namespace MediaBrowser.Common.Implementations.Security
         public string featId { get; set; }
         public bool registered { get; set; }
         public DateTime expDate { get; set; }
+        public string key { get; set; }
     }
 }

+ 8 - 3
MediaBrowser.Common.Implementations/Serialization/JsonSerializer.cs

@@ -1,6 +1,8 @@
-using MediaBrowser.Model.Serialization;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.Serialization;
 using System;
 using System.IO;
+using CommonIO;
 
 namespace MediaBrowser.Common.Implementations.Serialization
 {
@@ -9,8 +11,11 @@ namespace MediaBrowser.Common.Implementations.Serialization
     /// </summary>
     public class JsonSerializer : IJsonSerializer
     {
-        public JsonSerializer()
+        private readonly IFileSystem _fileSystem;
+        
+        public JsonSerializer(IFileSystem fileSystem)
         {
+            _fileSystem = fileSystem;
             Configure();
         }
 
@@ -53,7 +58,7 @@ namespace MediaBrowser.Common.Implementations.Serialization
                 throw new ArgumentNullException("file");
             }
 
-            using (Stream stream = File.Open(file, FileMode.Create))
+			using (Stream stream = _fileSystem.GetFileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read))
             {
                 SerializeToStream(obj, stream);
             }

+ 10 - 1
MediaBrowser.Common.Implementations/Serialization/XmlSerializer.cs

@@ -3,6 +3,8 @@ using System;
 using System.Collections.Concurrent;
 using System.IO;
 using System.Xml;
+using CommonIO;
+using MediaBrowser.Common.IO;
 
 namespace MediaBrowser.Common.Implementations.Serialization
 {
@@ -11,6 +13,13 @@ namespace MediaBrowser.Common.Implementations.Serialization
     /// </summary>
     public class XmlSerializer : IXmlSerializer
     {
+		private IFileSystem _fileSystem;
+
+		public XmlSerializer(IFileSystem fileSystem) 
+		{
+			_fileSystem = fileSystem;
+		}
+
         // Need to cache these
         // http://dotnetcodebox.blogspot.com/2013/01/xmlserializer-class-may-result-in.html
         private readonly ConcurrentDictionary<string, System.Xml.Serialization.XmlSerializer> _serializers =
@@ -83,7 +92,7 @@ namespace MediaBrowser.Common.Implementations.Serialization
         /// <returns>System.Object.</returns>
         public object DeserializeFromFile(Type type, string file)
         {
-            using (var stream = File.OpenRead(file))
+            using (var stream = _fileSystem.OpenRead(file))
             {
                 return DeserializeFromStream(type, stream);
             }

+ 5 - 4
MediaBrowser.Common.Implementations/Updates/InstallationManager.cs

@@ -19,6 +19,7 @@ using System.Linq;
 using System.Security.Cryptography;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Common.Implementations.Updates
 {
@@ -553,7 +554,7 @@ namespace MediaBrowser.Common.Implementations.Updates
             if (packageChecksum != Guid.Empty) // support for legacy uploads for now
             {
                 using (var crypto = new MD5CryptoServiceProvider())
-                using (var stream = new BufferedStream(File.OpenRead(tempFile), 100000))
+				using (var stream = new BufferedStream(_fileSystem.OpenRead(tempFile), 100000))
                 {
                     var check = Guid.Parse(BitConverter.ToString(crypto.ComputeHash(stream)).Replace("-", String.Empty));
                     if (check != packageChecksum)
@@ -568,12 +569,12 @@ namespace MediaBrowser.Common.Implementations.Updates
             // Success - move it to the real target 
             try
             {
-                Directory.CreateDirectory(Path.GetDirectoryName(target));
-                File.Copy(tempFile, target, true);
+				_fileSystem.CreateDirectory(Path.GetDirectoryName(target));
+				_fileSystem.CopyFile(tempFile, target, true);
                 //If it is an archive - write out a version file so we know what it is
                 if (isArchive)
                 {
-                    File.WriteAllText(target + ".ver", package.versionStr);
+					File.WriteAllText(target + ".ver", package.versionStr);
                 }
             }
             catch (IOException e)

+ 4 - 2
MediaBrowser.Common.Implementations/packages.config

@@ -1,5 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="NLog" version="3.2.1" targetFramework="net45" />
-  <package id="SimpleInjector" version="2.8.0" targetFramework="net45" />
+  <package id="CommonIO" version="1.0.0.5" targetFramework="net45" />
+  <package id="NLog" version="4.1.0" targetFramework="net45" />
+  <package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" />
+  <package id="SimpleInjector" version="3.0.5" targetFramework="net45" />
 </packages>

+ 0 - 165
MediaBrowser.Common/IO/IFileSystem.cs

@@ -1,165 +0,0 @@
-using System;
-using System.IO;
-
-namespace MediaBrowser.Common.IO
-{
-    /// <summary>
-    /// Interface IFileSystem
-    /// </summary>
-    public interface IFileSystem
-    {
-        /// <summary>
-        /// Determines whether the specified filename is shortcut.
-        /// </summary>
-        /// <param name="filename">The filename.</param>
-        /// <returns><c>true</c> if the specified filename is shortcut; otherwise, <c>false</c>.</returns>
-        bool IsShortcut(string filename);
-
-        /// <summary>
-        /// Resolves the shortcut.
-        /// </summary>
-        /// <param name="filename">The filename.</param>
-        /// <returns>System.String.</returns>
-        string ResolveShortcut(string filename);
-
-        /// <summary>
-        /// Creates the shortcut.
-        /// </summary>
-        /// <param name="shortcutPath">The shortcut path.</param>
-        /// <param name="target">The target.</param>
-        void CreateShortcut(string shortcutPath, string target);
-
-        /// <summary>
-        /// Gets the file system info.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns>FileSystemInfo.</returns>
-        FileSystemInfo GetFileSystemInfo(string path);
-
-        /// <summary>
-        /// Gets the valid filename.
-        /// </summary>
-        /// <param name="filename">The filename.</param>
-        /// <returns>System.String.</returns>
-        string GetValidFilename(string filename);
-
-        /// <summary>
-        /// Gets the creation time UTC.
-        /// </summary>
-        /// <param name="info">The info.</param>
-        /// <returns>DateTime.</returns>
-        DateTime GetCreationTimeUtc(FileSystemInfo info);
-
-        /// <summary>
-        /// Gets the last write time UTC.
-        /// </summary>
-        /// <param name="info">The information.</param>
-        /// <returns>DateTime.</returns>
-        DateTime GetLastWriteTimeUtc(FileSystemInfo info);
-
-        /// <summary>
-        /// Gets the last write time UTC.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns>DateTime.</returns>
-        DateTime GetLastWriteTimeUtc(string path);
-
-        /// <summary>
-        /// Gets the file stream.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="mode">The mode.</param>
-        /// <param name="access">The access.</param>
-        /// <param name="share">The share.</param>
-        /// <param name="isAsync">if set to <c>true</c> [is asynchronous].</param>
-        /// <returns>FileStream.</returns>
-        FileStream GetFileStream(string path, FileMode mode, FileAccess access, FileShare share, bool isAsync = false);
-
-        /// <summary>
-        /// Swaps the files.
-        /// </summary>
-        /// <param name="file1">The file1.</param>
-        /// <param name="file2">The file2.</param>
-        void SwapFiles(string file1, string file2);
-
-        /// <summary>
-        /// Determines whether [contains sub path] [the specified parent path].
-        /// </summary>
-        /// <param name="parentPath">The parent path.</param>
-        /// <param name="path">The path.</param>
-        /// <returns><c>true</c> if [contains sub path] [the specified parent path]; otherwise, <c>false</c>.</returns>
-        bool ContainsSubPath(string parentPath, string path);
-
-        /// <summary>
-        /// Determines whether [is root path] [the specified path].
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns><c>true</c> if [is root path] [the specified path]; otherwise, <c>false</c>.</returns>
-        bool IsRootPath(string path);
-
-        /// <summary>
-        /// Normalizes the path.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns>System.String.</returns>
-        string NormalizePath(string path);
-
-        /// <summary>
-        /// Substitutes the path.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="from">From.</param>
-        /// <param name="to">To.</param>
-        /// <returns>System.String.</returns>
-        string SubstitutePath(string path, string from, string to);
-
-        /// <summary>
-        /// Gets the file name without extension.
-        /// </summary>
-        /// <param name="info">The information.</param>
-        /// <returns>System.String.</returns>
-        string GetFileNameWithoutExtension(FileSystemInfo info);
-
-        /// <summary>
-        /// Gets the file name without extension.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns>System.String.</returns>
-        string GetFileNameWithoutExtension(string path);
-
-        /// <summary>
-        /// Determines whether [is path file] [the specified path].
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns><c>true</c> if [is path file] [the specified path]; otherwise, <c>false</c>.</returns>
-        bool IsPathFile(string path);
-
-        /// <summary>
-        /// Deletes the file.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="sendToRecycleBin">if set to <c>true</c> [send to recycle bin].</param>
-        void DeleteFile(string path, bool sendToRecycleBin);
-
-        /// <summary>
-        /// Deletes the directory.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
-        /// <param name="sendToRecycleBin">if set to <c>true</c> [send to recycle bin].</param>
-        void DeleteDirectory(string path, bool recursive, bool sendToRecycleBin);
-
-        /// <summary>
-        /// Deletes the file.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        void DeleteFile(string path);
-
-        /// <summary>
-        /// Deletes the directory.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
-        void DeleteDirectory(string path, bool recursive);
-    }
-}

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

@@ -60,7 +60,6 @@
     <Compile Include="Extensions\BaseExtensions.cs" />
     <Compile Include="Extensions\ResourceNotFoundException.cs" />
     <Compile Include="IDependencyContainer.cs" />
-    <Compile Include="IO\IFileSystem.cs" />
     <Compile Include="IO\ProgressStream.cs" />
     <Compile Include="IO\StreamDefaults.cs" />
     <Compile Include="Configuration\IApplicationPaths.cs" />

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

@@ -87,6 +87,7 @@ namespace MediaBrowser.Common.Net
         public bool BufferContent { get; set; }
 
         public bool LogRequest { get; set; }
+        public bool LogErrors { get; set; }
 
         public bool LogErrorResponseBody { get; set; }
         public bool EnableKeepAlive { get; set; }
@@ -116,6 +117,7 @@ namespace MediaBrowser.Common.Net
             RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
             LogRequest = true;
+            LogErrors = true;
             CacheMode = CacheMode.None;
 
             TimeoutMs = 20000;

+ 5 - 0
MediaBrowser.Common/ScheduledTasks/IScheduledTaskWorker.cs

@@ -69,5 +69,10 @@ namespace MediaBrowser.Common.ScheduledTasks
         /// </summary>
         /// <value>The unique id.</value>
         string Id { get; }
+
+        /// <summary>
+        /// Reloads the trigger events.
+        /// </summary>
+        void ReloadTriggerEvents();
     }
 }

+ 3 - 3
MediaBrowser.Common/ScheduledTasks/IntervalTrigger.cs

@@ -51,8 +51,8 @@ namespace MediaBrowser.Common.ScheduledTasks
             DisposeTimer();
 
             var triggerDate = lastResult != null ?
-                lastResult.EndTimeUtc.Add(Interval) :
-                DateTime.UtcNow.Add(FirstRunDelay);
+                            lastResult.EndTimeUtc.Add(Interval) :
+                            DateTime.UtcNow.Add(FirstRunDelay);
 
             if (DateTime.UtcNow > triggerDate)
             {
@@ -62,7 +62,7 @@ namespace MediaBrowser.Common.ScheduledTasks
                 }
                 else
                 {
-                    triggerDate = DateTime.UtcNow.Add(Interval);
+                    triggerDate = DateTime.UtcNow.AddMinutes(1);
                 }
             }
 

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

@@ -1,3 +1,4 @@
+using System;
 using MediaBrowser.Model.Entities;
 using System.Threading.Tasks;
 
@@ -45,5 +46,11 @@ namespace MediaBrowser.Common.Security
         /// </summary>
         /// <returns>Task&lt;SupporterInfo&gt;.</returns>
         Task<SupporterInfo> GetSupporterInfo();
+
+        /// <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);
     }
 }

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

@@ -10,8 +10,6 @@ namespace MediaBrowser.Controller.Channels
 {
     public class Channel : Folder
     {
-        public string OriginalChannelName { get; set; }
-
         public override bool IsVisible(User user)
         {
             if (user.Policy.BlockedChannels != null)

+ 3 - 10
MediaBrowser.Controller/Channels/ChannelAudioItem.cs

@@ -7,24 +7,15 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Users;
 using System.Collections.Generic;
 using System.Linq;
+using System.Runtime.Serialization;
 using System.Threading;
 
 namespace MediaBrowser.Controller.Channels
 {
     public class ChannelAudioItem : Audio, IChannelMediaItem
     {
-        public string ExternalId { get; set; }
-
-        public string DataVersion { get; set; }
-
-        public ChannelItemType ChannelItemType { get; set; }
-
-        public bool IsInfiniteStream { get; set; }
-
         public ChannelMediaContentType ContentType { get; set; }
 
-        public string OriginalImageUrl { get; set; }
-
         public List<ChannelMediaInfo> ChannelMediaSources { get; set; }
 
         protected override bool GetBlockUnratedValue(UserPolicy config)
@@ -37,6 +28,7 @@ namespace MediaBrowser.Controller.Channels
             return ExternalId;
         }
 
+        [IgnoreDataMember]
         public override bool SupportsLocalMetadata
         {
             get
@@ -55,6 +47,7 @@ namespace MediaBrowser.Controller.Channels
             ChannelMediaSources = new List<ChannelMediaInfo>();
         }
 
+        [IgnoreDataMember]
         public override LocationType LocationType
         {
             get

+ 3 - 7
MediaBrowser.Controller/Channels/ChannelFolderItem.cs

@@ -3,28 +3,24 @@ using MediaBrowser.Model.Channels;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Users;
 using System;
+using System.Runtime.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Model.Entities;
 
 namespace MediaBrowser.Controller.Channels
 {
     public class ChannelFolderItem : Folder, IChannelItem
     {
-        public string ExternalId { get; set; }
-
-        public string DataVersion { get; set; }
-
-        public ChannelItemType ChannelItemType { get; set; }
         public ChannelFolderType ChannelFolderType { get; set; }
 
-        public string OriginalImageUrl { get; set; }
-
         protected override bool GetBlockUnratedValue(UserPolicy config)
         {
             // Don't block. 
             return false;
         }
 
+        [IgnoreDataMember]
         public override bool SupportsLocalMetadata
         {
             get

+ 8 - 11
MediaBrowser.Controller/Channels/ChannelVideoItem.cs

@@ -8,24 +8,15 @@ using MediaBrowser.Model.Users;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Runtime.Serialization;
 using System.Threading;
 
 namespace MediaBrowser.Controller.Channels
 {
     public class ChannelVideoItem : Video, IChannelMediaItem, IHasLookupInfo<ChannelItemLookupInfo>
     {
-        public string ExternalId { get; set; }
-
-        public string DataVersion { get; set; }
-
-        public ChannelItemType ChannelItemType { get; set; }
-
-        public bool IsInfiniteStream { get; set; }
-
         public ChannelMediaContentType ContentType { get; set; }
 
-        public string OriginalImageUrl { get; set; }
-
         public List<ChannelMediaInfo> ChannelMediaSources { get; set; }
 
         protected override string CreateUserDataKey()
@@ -56,6 +47,7 @@ namespace MediaBrowser.Controller.Channels
             return config.BlockUnratedItems.Contains(UnratedItem.ChannelContent);
         }
 
+        [IgnoreDataMember]
         public override bool SupportsLocalMetadata
         {
             get
@@ -74,6 +66,7 @@ namespace MediaBrowser.Controller.Channels
             ChannelMediaSources = new List<ChannelMediaInfo>();
         }
 
+        [IgnoreDataMember]
         public override LocationType LocationType
         {
             get
@@ -115,7 +108,11 @@ namespace MediaBrowser.Controller.Channels
             var info = GetItemLookupInfo<ChannelItemLookupInfo>();
 
             info.ContentType = ContentType;
-            info.ExtraType = ExtraType;
+
+            if (ExtraType.HasValue)
+            {
+                info.ExtraType = ExtraType.Value;
+            }
 
             return info;
         }

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

@@ -1,14 +0,0 @@
-using System.Collections.Generic;
-
-namespace MediaBrowser.Controller.Channels
-{
-    public interface IChannelFactory
-    {
-        IEnumerable<IChannel> GetChannels();
-    }
-
-    public interface IFactoryChannel
-    {
-        
-    }
-}

+ 0 - 6
MediaBrowser.Controller/Channels/IChannelItem.cs

@@ -7,11 +7,5 @@ namespace MediaBrowser.Controller.Channels
         string ChannelId { get; set; }
 
         string ExternalId { get; set; }
-
-        ChannelItemType ChannelItemType { get; set; }
-
-        string OriginalImageUrl { get; set; }
-
-        string DataVersion { get; set; }
     }
 }

+ 1 - 1
MediaBrowser.Controller/Channels/IChannelManager.cs

@@ -16,7 +16,7 @@ namespace MediaBrowser.Controller.Channels
         /// </summary>
         /// <param name="channels">The channels.</param>
         /// <param name="factories">The factories.</param>
-        void AddParts(IEnumerable<IChannel> channels, IEnumerable<IChannelFactory> factories);
+        void AddParts(IEnumerable<IChannel> channels);
 
         /// <summary>
         /// Gets the channel download path.

+ 1 - 1
MediaBrowser.Controller/Channels/IChannelMediaItem.cs

@@ -11,7 +11,7 @@ namespace MediaBrowser.Controller.Channels
 
         ChannelMediaContentType ContentType { get; set; }
 
-        ExtraType ExtraType { get; set; }
+        ExtraType? ExtraType { get; set; }
 
         List<ChannelMediaInfo> ChannelMediaSources { get; set; }
     }

+ 11 - 5
MediaBrowser.Controller/Drawing/IImageProcessor.cs

@@ -28,17 +28,17 @@ namespace MediaBrowser.Controller.Drawing
         /// <summary>
         /// Gets the size of the image.
         /// </summary>
-        /// <param name="path">The path.</param>
+        /// <param name="info">The information.</param>
         /// <returns>ImageSize.</returns>
-        ImageSize GetImageSize(string path);
+        ImageSize GetImageSize(ItemImageInfo info);
 
         /// <summary>
         /// Gets the size of the image.
         /// </summary>
-        /// <param name="info">The information.</param>
+        /// <param name="path">The path.</param>
         /// <returns>ImageSize.</returns>
-        ImageSize GetImageSize(ItemImageInfo info);
-
+        ImageSize GetImageSize(string path);
+        
         /// <summary>
         /// Adds the parts.
         /// </summary>
@@ -105,5 +105,11 @@ namespace MediaBrowser.Controller.Drawing
         /// </summary>
         /// <param name="options">The options.</param>
         Task 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; }
     }
 }

+ 5 - 3
MediaBrowser.Controller/Entities/AggregateFolder.cs

@@ -6,6 +6,8 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Runtime.Serialization;
+using CommonIO;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Providers;
 
 namespace MediaBrowser.Controller.Entities
@@ -62,7 +64,7 @@ namespace MediaBrowser.Controller.Entities
 
         public List<string> PhysicalLocationsList { get; set; }
 
-        protected override IEnumerable<FileSystemInfo> GetFileSystemChildren(IDirectoryService directoryService)
+        protected override IEnumerable<FileSystemMetadata> GetFileSystemChildren(IDirectoryService directoryService)
         {
             return CreateResolveArgs(directoryService).FileSystemChildren;
         }
@@ -73,7 +75,7 @@ namespace MediaBrowser.Controller.Entities
 
             var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths , directoryService)
             {
-                FileInfo = new DirectoryInfo(path),
+                FileInfo = FileSystem.GetDirectoryInfo(path),
                 Path = path,
                 Parent = Parent
             };
@@ -94,7 +96,7 @@ namespace MediaBrowser.Controller.Entities
                 {
                     var paths = LibraryManager.NormalizeRootPathList(fileSystemDictionary.Keys);
 
-                    fileSystemDictionary = paths.Select(i => (FileSystemInfo)new DirectoryInfo(i)).ToDictionary(i => i.FullName);
+                    fileSystemDictionary = paths.Select(FileSystem.GetDirectoryInfo).ToDictionary(i => i.FullName);
                 }
 
                 args.FileSystemDictionary = fileSystemDictionary;

+ 10 - 11
MediaBrowser.Controller/Entities/Audio/Audio.cs

@@ -24,14 +24,20 @@ namespace MediaBrowser.Controller.Entities.Audio
         IThemeMedia,
         IArchivable
     {
-        public string FormatName { get; set; }
         public long? Size { get; set; }
         public string Container { get; set; }
         public int? TotalBitrate { get; set; }
         public List<string> Tags { get; set; }
-        public ExtraType ExtraType { get; set; }
+        public ExtraType? ExtraType { get; set; }
 
-        public bool IsThemeMedia { get; set; }
+        [IgnoreDataMember]
+        public bool IsThemeMedia
+        {
+            get
+            {
+                return ExtraType.HasValue && ExtraType.Value == Model.Entities.ExtraType.ThemeSong;
+            }
+        }
 
         public Audio()
         {
@@ -46,12 +52,6 @@ namespace MediaBrowser.Controller.Entities.Audio
             get { return LocationType == LocationType.FileSystem && RunTimeTicks.HasValue; }
         }
 
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance has embedded image.
-        /// </summary>
-        /// <value><c>true</c> if this instance has embedded image; otherwise, <c>false</c>.</value>
-        public bool HasEmbeddedImage { get; set; }
-
         [IgnoreDataMember]
         protected override bool SupportsOwnedItems
         {
@@ -212,8 +212,7 @@ namespace MediaBrowser.Controller.Entities.Audio
                 Path = enablePathSubstituion ? GetMappedPath(i.Path, locationType) : i.Path,
                 RunTimeTicks = i.RunTimeTicks,
                 Container = i.Container,
-                Size = i.Size,
-                Formats = (i.FormatName ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList()
+                Size = i.Size
             };
 
             if (string.IsNullOrEmpty(info.Container))

+ 140 - 102
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -23,6 +23,7 @@ using System.Linq;
 using System.Runtime.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -38,7 +39,6 @@ namespace MediaBrowser.Controller.Entities
             ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
             LockedFields = new List<MetadataFields>();
             ImageInfos = new List<ItemImageInfo>();
-            Identities = new List<IItemIdentity>();
         }
 
         /// <summary>
@@ -56,12 +56,16 @@ namespace MediaBrowser.Controller.Entities
         public static string ThemeSongFilename = "theme";
         public static string ThemeVideosFolderName = "backdrops";
 
+        public string PreferredMetadataCountryCode { get; set; }
+        public string PreferredMetadataLanguage { get; set; }
+
         public List<ItemImageInfo> ImageInfos { get; set; }
 
         /// <summary>
         /// Gets or sets the channel identifier.
         /// </summary>
         /// <value>The channel identifier.</value>
+        [IgnoreDataMember]
         public string ChannelId { get; set; }
 
         [IgnoreDataMember]
@@ -120,6 +124,12 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The id.</value>
         public Guid Id { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance is hd.
+        /// </summary>
+        /// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value>
+        public bool? IsHD { 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.
@@ -162,6 +172,26 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        /// <summary>
+        /// Id of the program.
+        /// </summary>
+        [IgnoreDataMember]
+        public string ExternalId
+        {
+            get { return this.GetProviderId("ProviderExternalId"); }
+            set
+            {
+                this.SetProviderId("ProviderExternalId", value);
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the etag.
+        /// </summary>
+        /// <value>The etag.</value>
+        [IgnoreDataMember]
+        public string ExternalEtag { get; set; }
+
         [IgnoreDataMember]
         public virtual bool IsHidden
         {
@@ -183,7 +213,7 @@ namespace MediaBrowser.Controller.Entities
             {
                 // Local trailer, special feature, theme video, etc.
                 // An item that belongs to another item but is not part of the Parent-Child tree
-                return !IsFolder && Parent == null && LocationType == LocationType.FileSystem;
+                return !IsFolder && ParentId == Guid.Empty && LocationType == LocationType.FileSystem;
             }
         }
 
@@ -305,6 +335,9 @@ namespace MediaBrowser.Controller.Entities
 
         public DateTime DateLastSaved { get; set; }
 
+        [IgnoreDataMember]
+        public DateTime DateLastRefreshed { get; set; }
+
         /// <summary>
         /// The logger
         /// </summary>
@@ -331,30 +364,8 @@ namespace MediaBrowser.Controller.Entities
             return Name;
         }
 
-        /// <summary>
-        /// Returns true if this item should not attempt to fetch metadata
-        /// </summary>
-        /// <value><c>true</c> if [dont fetch meta]; otherwise, <c>false</c>.</value>
-        [Obsolete("Please use IsLocked instead of DontFetchMeta")]
-        public bool DontFetchMeta { get; set; }
-
-        [IgnoreDataMember]
-        public bool IsLocked
-        {
-            get
-            {
-                return DontFetchMeta;
-            }
-            set
-            {
-                DontFetchMeta = value;
-            }
-        }
-
-        public bool IsUnidentified { get; set; }
-
         [IgnoreDataMember]
-        public List<IItemIdentity> Identities { get; set; }
+        public bool IsLocked { get; set; }
 
         /// <summary>
         /// Gets or sets the locked fields.
@@ -484,7 +495,6 @@ namespace MediaBrowser.Controller.Entities
 
         public Guid ParentId { get; set; }
 
-        private Folder _parent;
         /// <summary>
         /// Gets or sets the parent.
         /// </summary>
@@ -494,11 +504,6 @@ namespace MediaBrowser.Controller.Entities
         {
             get
             {
-                if (_parent != null)
-                {
-                    return _parent;
-                }
-
                 if (ParentId != Guid.Empty)
                 {
                     return LibraryManager.GetItemById(ParentId) as Folder;
@@ -506,12 +511,14 @@ namespace MediaBrowser.Controller.Entities
 
                 return null;
             }
-            set { _parent = value; }
+            set
+            {
+
+            }
         }
 
         public void SetParent(Folder parent)
         {
-            Parent = parent;
             ParentId = parent == null ? Guid.Empty : parent.Id;
         }
 
@@ -558,6 +565,7 @@ namespace MediaBrowser.Controller.Entities
         /// Gets or sets the end date.
         /// </summary>
         /// <value>The end date.</value>
+        [IgnoreDataMember]
         public DateTime? EndDate { get; set; }
 
         /// <summary>
@@ -582,6 +590,7 @@ namespace MediaBrowser.Controller.Entities
         /// Gets or sets the custom rating.
         /// </summary>
         /// <value>The custom rating.</value>
+        //[IgnoreDataMember]
         public string CustomRating { get; set; }
 
         /// <summary>
@@ -590,12 +599,6 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The overview.</value>
         public string Overview { get; set; }
 
-        /// <summary>
-        /// Gets or sets the people.
-        /// </summary>
-        /// <value>The people.</value>
-        public List<PersonInfo> People { get; set; }
-
         /// <summary>
         /// Gets or sets the studios.
         /// </summary>
@@ -618,6 +621,7 @@ namespace MediaBrowser.Controller.Entities
         /// Gets or sets the community rating.
         /// </summary>
         /// <value>The community rating.</value>
+        //[IgnoreDataMember]
         public float? CommunityRating { get; set; }
 
         /// <summary>
@@ -643,6 +647,7 @@ namespace MediaBrowser.Controller.Entities
         /// This could be episode number, album track number, etc.
         /// </summary>
         /// <value>The index number.</value>
+        //[IgnoreDataMember]
         public int? IndexNumber { get; set; }
 
         /// <summary>
@@ -662,7 +667,7 @@ namespace MediaBrowser.Controller.Entities
         {
             get
             {
-                if (!string.IsNullOrEmpty(CustomRating))
+                if (!string.IsNullOrWhiteSpace(CustomRating))
                 {
                     return CustomRating;
                 }
@@ -701,16 +706,16 @@ namespace MediaBrowser.Controller.Entities
         /// Loads the theme songs.
         /// </summary>
         /// <returns>List{Audio.Audio}.</returns>
-        private IEnumerable<Audio.Audio> LoadThemeSongs(List<FileSystemInfo> fileSystemChildren, IDirectoryService directoryService)
+        private IEnumerable<Audio.Audio> LoadThemeSongs(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
         {
-            var files = fileSystemChildren.OfType<DirectoryInfo>()
+            var files = fileSystemChildren.Where(i => i.IsDirectory)
                 .Where(i => string.Equals(i.Name, ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase))
-                .SelectMany(i => i.EnumerateFiles("*", SearchOption.TopDirectoryOnly))
+                .SelectMany(i => directoryService.GetFiles(i.FullName))
                 .ToList();
 
             // Support plex/xbmc convention
-            files.AddRange(fileSystemChildren.OfType<FileInfo>()
-                .Where(i => string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase))
+            files.AddRange(fileSystemChildren
+                .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase))
                 );
 
             return LibraryManager.ResolvePaths(files, directoryService, null)
@@ -737,11 +742,11 @@ namespace MediaBrowser.Controller.Entities
         /// Loads the video backdrops.
         /// </summary>
         /// <returns>List{Video}.</returns>
-        private IEnumerable<Video> LoadThemeVideos(IEnumerable<FileSystemInfo> fileSystemChildren, IDirectoryService directoryService)
+        private IEnumerable<Video> LoadThemeVideos(IEnumerable<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
         {
-            var files = fileSystemChildren.OfType<DirectoryInfo>()
+            var files = fileSystemChildren.Where(i => i.IsDirectory)
                 .Where(i => string.Equals(i.Name, ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
-                .SelectMany(i => i.EnumerateFiles("*", SearchOption.TopDirectoryOnly));
+                .SelectMany(i => directoryService.GetFiles(i.FullName));
 
             return LibraryManager.ResolvePaths(files, directoryService, null)
                 .OfType<Video>()
@@ -765,7 +770,7 @@ namespace MediaBrowser.Controller.Entities
 
         public Task RefreshMetadata(CancellationToken cancellationToken)
         {
-            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService()), cancellationToken);
+            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
         }
 
         /// <summary>
@@ -786,7 +791,7 @@ namespace MediaBrowser.Controller.Entities
                 {
                     var files = locationType != LocationType.Remote && locationType != LocationType.Virtual ?
                         GetFileSystemChildren(options.DirectoryService).ToList() :
-                        new List<FileSystemInfo>();
+                        new List<FileSystemMetadata>();
 
                     var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
 
@@ -833,7 +838,7 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="fileSystemChildren"></param>
         /// <param name="cancellationToken"></param>
         /// <returns></returns>
-        protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
+        protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
         {
             var themeSongsChanged = false;
 
@@ -864,14 +869,14 @@ namespace MediaBrowser.Controller.Entities
             return themeSongsChanged || themeVideosChanged || localTrailersChanged;
         }
 
-        protected virtual IEnumerable<FileSystemInfo> GetFileSystemChildren(IDirectoryService directoryService)
+        protected virtual IEnumerable<FileSystemMetadata> GetFileSystemChildren(IDirectoryService directoryService)
         {
             var path = ContainingFolderPath;
 
             return directoryService.GetFileSystemEntries(path);
         }
 
-        private async Task<bool> RefreshLocalTrailers(IHasTrailers item, MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
+        private async Task<bool> RefreshLocalTrailers(IHasTrailers item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
         {
             var newItems = LibraryManager.FindTrailers(this, fileSystemChildren, options.DirectoryService).ToList();
 
@@ -888,7 +893,7 @@ namespace MediaBrowser.Controller.Entities
             return itemsChanged;
         }
 
-        private async Task<bool> RefreshThemeVideos(IHasThemeMedia item, MetadataRefreshOptions options, IEnumerable<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
+        private async Task<bool> RefreshThemeVideos(IHasThemeMedia item, MetadataRefreshOptions options, IEnumerable<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
         {
             var newThemeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService).ToList();
 
@@ -902,7 +907,7 @@ namespace MediaBrowser.Controller.Entities
 
                 if (!i.IsThemeMedia)
                 {
-                    i.IsThemeMedia = true;
+                    i.ExtraType = ExtraType.ThemeVideo;
                     subOptions.ForceSave = true;
                 }
 
@@ -919,7 +924,7 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// Refreshes the theme songs.
         /// </summary>
-        private async Task<bool> RefreshThemeSongs(IHasThemeMedia item, MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
+        private async Task<bool> RefreshThemeSongs(IHasThemeMedia item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
         {
             var newThemeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService).ToList();
             var newThemeSongIds = newThemeSongs.Select(i => i.Id).ToList();
@@ -932,7 +937,7 @@ namespace MediaBrowser.Controller.Entities
 
                 if (!i.IsThemeMedia)
                 {
-                    i.IsThemeMedia = true;
+                    i.ExtraType = ExtraType.ThemeSong;
                     subOptions.ForceSave = true;
                 }
 
@@ -999,18 +1004,11 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>System.String.</returns>
         public string GetPreferredMetadataLanguage()
         {
-            string lang = null;
-
-            var hasLang = this as IHasPreferredMetadataLanguage;
-
-            if (hasLang != null)
-            {
-                lang = hasLang.PreferredMetadataLanguage;
-            }
+            string lang = PreferredMetadataLanguage;
 
             if (string.IsNullOrWhiteSpace(lang))
             {
-                lang = Parents.OfType<IHasPreferredMetadataLanguage>()
+                lang = Parents
                     .Select(i => i.PreferredMetadataLanguage)
                     .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
             }
@@ -1036,18 +1034,11 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>System.String.</returns>
         public string GetPreferredMetadataCountryCode()
         {
-            string lang = null;
-
-            var hasLang = this as IHasPreferredMetadataLanguage;
-
-            if (hasLang != null)
-            {
-                lang = hasLang.PreferredMetadataCountryCode;
-            }
+            string lang = PreferredMetadataCountryCode;
 
             if (string.IsNullOrWhiteSpace(lang))
             {
-                lang = Parents.OfType<IHasPreferredMetadataLanguage>()
+                lang = Parents
                     .Select(i => i.PreferredMetadataCountryCode)
                     .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
             }
@@ -1114,7 +1105,14 @@ namespace MediaBrowser.Controller.Entities
             // Could not determine the integer value
             if (!value.HasValue)
             {
-                return true;
+                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;
@@ -1165,6 +1163,17 @@ namespace MediaBrowser.Controller.Entities
         /// <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(UnratedItem.Other);
         }
 
@@ -1418,7 +1427,7 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>Task.</returns>
         public virtual Task ChangedExternally()
         {
-            ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions());
+            ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(FileSystem));
             return Task.FromResult(true);
         }
 
@@ -1434,7 +1443,24 @@ namespace MediaBrowser.Controller.Entities
             return GetImageInfo(type, imageIndex) != null;
         }
 
-        public void SetImagePath(ImageType type, int index, FileSystemInfo file)
+        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)
+            {
+                ImageInfos.Remove(existingImage);
+            }
+
+            ImageInfos.Add(image);
+        }
+
+        public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
         {
             if (type == ImageType.Chapter)
             {
@@ -1475,18 +1501,21 @@ namespace MediaBrowser.Controller.Entities
             // Remove it from the item
             RemoveImage(info);
 
-            // Delete the source file
-            var currentFile = new FileInfo(info.Path);
-
-            // Deletion will fail if the file is hidden so remove the attribute first
-            if (currentFile.Exists)
+            if (info.IsLocalFile)
             {
-                if ((currentFile.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
+                // Delete the source file
+                var currentFile = new FileInfo(info.Path);
+
+                // Deletion will fail if the file is hidden so remove the attribute first
+                if (currentFile.Exists)
                 {
-                    currentFile.Attributes &= ~FileAttributes.Hidden;
-                }
+                    if ((currentFile.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
+                    {
+                        currentFile.Attributes &= ~FileAttributes.Hidden;
+                    }
 
-                FileSystem.DeleteFile(currentFile.FullName);
+                    FileSystem.DeleteFile(currentFile.FullName);
+                }
             }
 
             return UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
@@ -1507,11 +1536,16 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         public bool ValidateImages(IDirectoryService directoryService)
         {
-            var allDirectories = ImageInfos.Select(i => System.IO.Path.GetDirectoryName(i.Path)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
-            var allFiles = allDirectories.SelectMany(directoryService.GetFiles).Select(i => i.FullName).ToList();
+            var allFiles = ImageInfos
+                .Where(i => i.IsLocalFile)
+                .Select(i => System.IO.Path.GetDirectoryName(i.Path))
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .SelectMany(directoryService.GetFiles)
+                .Select(i => i.FullName)
+                .ToList();
 
             var deletedImages = ImageInfos
-                .Where(image => !allFiles.Contains(image.Path, StringComparer.OrdinalIgnoreCase))
+                .Where(image => image.IsLocalFile && !allFiles.Contains(image.Path, StringComparer.OrdinalIgnoreCase))
                 .ToList();
 
             if (deletedImages.Count > 0)
@@ -1584,11 +1618,6 @@ namespace MediaBrowser.Controller.Entities
             return ImageInfos.Where(i => i.Type == imageType);
         }
 
-        public bool AddImages(ImageType imageType, IEnumerable<FileInfo> images)
-        {
-            return AddImages(imageType, images.Cast<FileSystemInfo>().ToList());
-        }
-
         /// <summary>
         /// Adds the images.
         /// </summary>
@@ -1596,7 +1625,7 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="images">The images.</param>
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
         /// <exception cref="System.ArgumentException">Cannot call AddImages with chapter images</exception>
-        public bool AddImages(ImageType imageType, List<FileSystemInfo> images)
+        public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
         {
             if (imageType == ImageType.Chapter)
             {
@@ -1606,7 +1635,7 @@ namespace MediaBrowser.Controller.Entities
             var existingImages = GetImages(imageType)
                 .ToList();
 
-            var newImageList = new List<FileSystemInfo>();
+            var newImageList = new List<FileSystemMetadata>();
             var imageAdded = false;
 
             foreach (var newImage in images)
@@ -1626,7 +1655,10 @@ namespace MediaBrowser.Controller.Entities
                 }
                 else
                 {
-                    existing.DateModified = FileSystem.GetLastWriteTimeUtc(newImage);
+                    if (existing.IsLocalFile)
+                    {
+                        existing.DateModified = FileSystem.GetLastWriteTimeUtc(newImage);
+                    }
                 }
             }
 
@@ -1635,7 +1667,7 @@ namespace MediaBrowser.Controller.Entities
                 var newImagePaths = images.Select(i => i.FullName).ToList();
 
                 var deleted = existingImages
-                    .Where(i => !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !File.Exists(i.Path))
+                    .Where(i => i.IsLocalFile && !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !FileSystem.FileExists(i.Path))
                     .ToList();
 
                 ImageInfos = ImageInfos.Except(deleted).ToList();
@@ -1646,7 +1678,7 @@ namespace MediaBrowser.Controller.Entities
             return newImageList.Count > 0;
         }
 
-        private ItemImageInfo GetImageInfo(FileSystemInfo file, ImageType type)
+        private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type)
         {
             return new ItemImageInfo
             {
@@ -1686,6 +1718,12 @@ namespace MediaBrowser.Controller.Entities
                 return Task.FromResult(true);
             }
 
+            if (!info1.IsLocalFile || !info2.IsLocalFile)
+            {
+                // TODO: Not supported  yet
+                return Task.FromResult(true);
+            }
+
             var path1 = info1.Path;
             var path2 = info2.Path;
 
@@ -1769,7 +1807,7 @@ namespace MediaBrowser.Controller.Entities
             {
                 foreach (var map in ConfigurationManager.Configuration.PathSubstitutions)
                 {
-                    path = FileSystem.SubstitutePath(path, map.From, map.To);
+                    path = LibraryManager.SubstitutePath(path, map.From, map.To);
                 }
             }
 
@@ -1810,7 +1848,7 @@ namespace MediaBrowser.Controller.Entities
 
             if (video == null)
             {
-                video = LibraryManager.ResolvePath(new FileInfo(path)) as Video;
+                video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
 
                 newOptions.ForceSave = true;
             }

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

@@ -7,7 +7,7 @@ using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Entities
 {
-    public class Book : BaseItem, IHasTags, IHasPreferredMetadataLanguage, IHasLookupInfo<BookInfo>, IHasSeries
+    public class Book : BaseItem, IHasTags, IHasLookupInfo<BookInfo>, IHasSeries
     {
         public override string MediaType
         {
@@ -25,14 +25,6 @@ namespace MediaBrowser.Controller.Entities
 
         public string SeriesName { get; set; }
 
-        public string PreferredMetadataLanguage { get; set; }
-
-        /// <summary>
-        /// Gets or sets the preferred metadata country code.
-        /// </summary>
-        /// <value>The preferred metadata country code.</value>
-        public string PreferredMetadataCountryCode { get; set; }
-
         public Book()
         {
             Tags = new List<string>();

+ 5 - 3
MediaBrowser.Controller/Entities/CollectionFolder.cs

@@ -8,6 +8,8 @@ using System.Linq;
 using System.Runtime.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
+using MediaBrowser.Common.IO;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -80,7 +82,7 @@ namespace MediaBrowser.Controller.Entities
 
         public List<string> PhysicalLocationsList { get; set; }
 
-        protected override IEnumerable<FileSystemInfo> GetFileSystemChildren(IDirectoryService directoryService)
+        protected override IEnumerable<FileSystemMetadata> GetFileSystemChildren(IDirectoryService directoryService)
         {
             return CreateResolveArgs(directoryService).FileSystemChildren;
         }
@@ -107,7 +109,7 @@ namespace MediaBrowser.Controller.Entities
 
             var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
             {
-                FileInfo = new DirectoryInfo(path),
+                FileInfo = FileSystem.GetDirectoryInfo(path),
                 Path = path,
                 Parent = Parent,
                 CollectionType = CollectionType
@@ -129,7 +131,7 @@ namespace MediaBrowser.Controller.Entities
                 {
                     var paths = LibraryManager.NormalizeRootPathList(fileSystemDictionary.Keys);
 
-                    fileSystemDictionary = paths.Select(i => (FileSystemInfo)new DirectoryInfo(i)).ToDictionary(i => i.FullName);
+                    fileSystemDictionary = paths.Select(FileSystem.GetDirectoryInfo).ToDictionary(i => i.FullName);
                 }
 
                 args.FileSystemDictionary = fileSystemDictionary;

+ 60 - 38
MediaBrowser.Controller/Entities/Folder.cs

@@ -13,13 +13,15 @@ using System.Linq;
 using System.Runtime.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
+using MediaBrowser.Common.IO;
 
 namespace MediaBrowser.Controller.Entities
 {
     /// <summary>
     /// Class Folder
     /// </summary>
-    public class Folder : BaseItem, IHasThemeMedia, IHasTags, IHasPreferredMetadataLanguage
+    public class Folder : BaseItem, IHasThemeMedia, IHasTags
     {
         public static IUserManager UserManager { get; set; }
         public static IUserViewManager UserViewManager { get; set; }
@@ -28,14 +30,6 @@ namespace MediaBrowser.Controller.Entities
         public List<Guid> ThemeVideoIds { get; set; }
         public List<string> Tags { get; set; }
 
-        public string PreferredMetadataLanguage { get; set; }
-
-        /// <summary>
-        /// Gets or sets the preferred metadata country code.
-        /// </summary>
-        /// <value>The preferred metadata country code.</value>
-        public string PreferredMetadataCountryCode { get; set; }
-
         public Folder()
         {
             LinkedChildren = new List<LinkedChild>();
@@ -48,7 +42,7 @@ namespace MediaBrowser.Controller.Entities
         [IgnoreDataMember]
         public virtual bool IsPreSorted
         {
-            get { return ConfigurationManager.Configuration.EnableWindowsShortcuts; }
+            get { return false; }
         }
 
         /// <summary>
@@ -120,7 +114,7 @@ namespace MediaBrowser.Controller.Entities
         [IgnoreDataMember]
         protected virtual bool SupportsShortcutChildren
         {
-            get { return false; }
+            get { return ConfigurationManager.Configuration.EnableWindowsShortcuts; }
         }
 
         /// <summary>
@@ -213,7 +207,7 @@ namespace MediaBrowser.Controller.Entities
                     return base.OfficialRatingForComparison;
                 }
 
-                return !string.IsNullOrEmpty(base.OfficialRatingForComparison) ? base.OfficialRatingForComparison : "None";
+                return !string.IsNullOrWhiteSpace(base.OfficialRatingForComparison) ? base.OfficialRatingForComparison : "None";
             }
         }
 
@@ -320,7 +314,7 @@ namespace MediaBrowser.Controller.Entities
         [IgnoreDataMember]
         public IEnumerable<BaseItem> Children
         {
-            get { return ActualChildren; }
+            get { return ActualChildren.ToList(); }
         }
 
         /// <summary>
@@ -371,7 +365,7 @@ namespace MediaBrowser.Controller.Entities
 
         public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
         {
-            return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService()));
+            return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(FileSystem)));
         }
 
         /// <summary>
@@ -474,7 +468,7 @@ namespace MediaBrowser.Controller.Entities
                                 currentChild.DateModified = child.DateModified;
                             }
 
-                            currentChild.IsOffline = false;
+                            await UpdateIsOffline(currentChild, false).ConfigureAwait(false);
                             validChildren.Add(currentChild);
                         }
                         else
@@ -509,12 +503,12 @@ namespace MediaBrowser.Controller.Entities
 
                         else if (!string.IsNullOrEmpty(item.Path) && IsPathOffline(item.Path))
                         {
-                            item.IsOffline = true;
+                            await UpdateIsOffline(item, true).ConfigureAwait(false);
                             validChildren.Add(item);
                         }
                         else
                         {
-                            item.IsOffline = false;
+                            await UpdateIsOffline(item, false).ConfigureAwait(false);
                             actualRemovals.Add(item);
                         }
                     }
@@ -569,6 +563,17 @@ namespace MediaBrowser.Controller.Entities
             progress.Report(100);
         }
 
+        private Task UpdateIsOffline(BaseItem item, bool newValue)
+        {
+            if (item.IsOffline != newValue)
+            {
+                item.IsOffline = newValue;
+                return item.UpdateToRepository(ItemUpdateType.None, CancellationToken.None);
+            }
+
+            return Task.FromResult(true);
+        }
+
         private async Task RefreshMetadataRecursive(MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
         {
             var children = ActualChildren.ToList();
@@ -691,9 +696,9 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         /// <param name="path">The path.</param>
         /// <returns><c>true</c> if the specified path is offline; otherwise, <c>false</c>.</returns>
-        private bool IsPathOffline(string path)
+        public static bool IsPathOffline(string path)
         {
-            if (File.Exists(path))
+            if (FileSystem.FileExists(path))
             {
                 return false;
             }
@@ -703,7 +708,7 @@ namespace MediaBrowser.Controller.Entities
             // Depending on whether the path is local or unc, it may return either null or '\' at the top
             while (!string.IsNullOrEmpty(path) && path.Length > 1)
             {
-                if (Directory.Exists(path))
+                if (FileSystem.DirectoryExists(path))
                 {
                     return false;
                 }
@@ -725,12 +730,12 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="folders">The folders.</param>
         /// <param name="path">The path.</param>
         /// <returns><c>true</c> if the specified folders contains path; otherwise, <c>false</c>.</returns>
-        private bool ContainsPath(IEnumerable<VirtualFolderInfo> folders, string path)
+        private static bool ContainsPath(IEnumerable<VirtualFolderInfo> folders, string path)
         {
             return folders.SelectMany(i => i.Locations).Any(i => ContainsPath(i, path));
         }
 
-        private bool ContainsPath(string parent, string path)
+        private static bool ContainsPath(string parent, string path)
         {
             return string.Equals(parent, path, StringComparison.OrdinalIgnoreCase) || FileSystem.ContainsSubPath(parent, path);
         }
@@ -752,21 +757,24 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>IEnumerable{BaseItem}.</returns>
         protected IEnumerable<BaseItem> GetCachedChildren()
         {
-            var childrenItems = ItemRepository.GetChildrenItems(Id).Select(RetrieveChild).Where(i => i != null);
-
-            //var children = ItemRepository.GetChildren(Id).Select(RetrieveChild).Where(i => i != null).ToList();
-
-            //if (children.Count != childrenItems.Count)
-            //{
-            //    var b = this;
-            //}
+            if (ConfigurationManager.Configuration.DisableStartupScan)
+            {
+                return ItemRepository.GetChildrenItems(Id).Select(RetrieveChild).Where(i => i != null);
+                //return ItemRepository.GetItems(new InternalItemsQuery
+                //{
+                //    ParentId = Id
 
-            return childrenItems;
+                //}).Items.Select(RetrieveChild).Where(i => i != null);
+            }
+            else
+            {
+                return ItemRepository.GetChildrenItems(Id).Select(RetrieveChild).Where(i => i != null);
+            }
         }
 
         private BaseItem RetrieveChild(BaseItem child)
         {
-            if (child.Id == Guid.Empty)
+            if (child == null || child.Id == Guid.Empty)
             {
                 Logger.Error("Item found with empty Id: " + (child.Path ?? child.Name));
                 return null;
@@ -1064,7 +1072,7 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
-        protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
+        protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
         {
             var changesFound = false;
 
@@ -1085,7 +1093,7 @@ namespace MediaBrowser.Controller.Entities
         /// Refreshes the linked children.
         /// </summary>
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
-        private bool RefreshLinkedChildren(IEnumerable<FileSystemInfo> fileSystemChildren)
+        private bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
         {
             var currentManualLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Manual).ToList();
             var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList();
@@ -1170,9 +1178,16 @@ namespace MediaBrowser.Controller.Entities
             DateTime? datePlayed,
             bool resetPosition)
         {
+            var itemsResult = await GetItems(new InternalItemsQuery
+            {
+                User = user,
+                Recursive = true,
+                IsFolder = false
+
+            }).ConfigureAwait(false);
+
             // Sweep through recursively and update status
-            var tasks = GetRecursiveChildren(user, i => !i.IsFolder && i.LocationType != LocationType.Virtual)
-                .Select(c => c.MarkPlayed(user, datePlayed, resetPosition));
+            var tasks = itemsResult.Items.Select(c => c.MarkPlayed(user, datePlayed, resetPosition));
 
             await Task.WhenAll(tasks).ConfigureAwait(false);
         }
@@ -1184,9 +1199,16 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>Task.</returns>
         public override async Task MarkUnplayed(User user)
         {
+            var itemsResult = await GetItems(new InternalItemsQuery
+            {
+                User = user,
+                Recursive = true,
+                IsFolder = false
+
+            }).ConfigureAwait(false);
+
             // Sweep through recursively and update status
-            var tasks = GetRecursiveChildren(user, i => !i.IsFolder && i.LocationType != LocationType.Virtual)
-                .Select(c => c.MarkUnplayed(user));
+            var tasks = itemsResult.Items.Select(c => c.MarkUnplayed(user));
 
             await Task.WhenAll(tasks).ConfigureAwait(false);
         }

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

@@ -8,19 +8,11 @@ using System.Linq;
 
 namespace MediaBrowser.Controller.Entities
 {
-    public class Game : BaseItem, IHasTrailers, IHasThemeMedia, IHasTags, IHasScreenshots, ISupportsPlaceHolders, IHasPreferredMetadataLanguage, IHasLookupInfo<GameInfo>
+    public class Game : BaseItem, IHasTrailers, IHasThemeMedia, IHasTags, IHasScreenshots, ISupportsPlaceHolders, IHasLookupInfo<GameInfo>
     {
         public List<Guid> ThemeSongIds { get; set; }
         public List<Guid> ThemeVideoIds { get; set; }
 
-        public string PreferredMetadataLanguage { get; set; }
-
-        /// <summary>
-        /// Gets or sets the preferred metadata country code.
-        /// </summary>
-        /// <value>The preferred metadata country code.</value>
-        public string PreferredMetadataCountryCode { get; set; }
-
         public Game()
         {
             MultiPartGameFiles = new List<string>();

Some files were not shown because too many files changed in this diff