Explorar o código

Merge pull request #1079 from MediaBrowser/dev

3.0.5582.0
Luke %!s(int64=10) %!d(string=hai) anos
pai
achega
935de313d5
Modificáronse 100 ficheiros con 3815 adicións e 924 borrados
  1. 1 1
      Emby.Drawing/Common/ImageHeader.cs
  2. 98 0
      Emby.Drawing/Emby.Drawing.csproj
  3. 138 0
      Emby.Drawing/GDI/DynamicImageHelpers.cs
  4. 254 0
      Emby.Drawing/GDI/GDIImageEncoder.cs
  5. 217 0
      Emby.Drawing/GDI/ImageExtensions.cs
  6. 34 0
      Emby.Drawing/GDI/PercentPlayedDrawer.cs
  7. 32 0
      Emby.Drawing/GDI/PlayedIndicatorDrawer.cs
  8. 50 0
      Emby.Drawing/GDI/UnplayedCountIndicator.cs
  9. 53 0
      Emby.Drawing/IImageEncoder.cs
  10. 229 0
      Emby.Drawing/ImageMagick/ImageMagickEncoder.cs
  11. 1 1
      Emby.Drawing/ImageMagick/PercentPlayedDrawer.cs
  12. 1 1
      Emby.Drawing/ImageMagick/PlayedIndicatorDrawer.cs
  13. 518 0
      Emby.Drawing/ImageMagick/StripCollageBuilder.cs
  14. 1 1
      Emby.Drawing/ImageMagick/UnplayedCountIndicator.cs
  15. 23 130
      Emby.Drawing/ImageProcessor.cs
  16. 31 0
      Emby.Drawing/Properties/AssemblyInfo.cs
  17. 4 0
      Emby.Drawing/packages.config
  18. 142 45
      MediaBrowser.Api/ApiEntryPoint.cs
  19. 5 5
      MediaBrowser.Api/BaseApiService.cs
  20. 1 1
      MediaBrowser.Api/ConfigurationService.cs
  21. 1 1
      MediaBrowser.Api/FilterService.cs
  22. 22 7
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  23. 2 2
      MediaBrowser.Api/Movies/MoviesService.cs
  24. 2 2
      MediaBrowser.Api/Music/AlbumsService.cs
  25. 109 4
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  26. 1 13
      MediaBrowser.Api/Playback/Dash/MpegDashService.cs
  27. 30 3
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  28. 31 7
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  29. 0 2
      MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
  30. 38 13
      MediaBrowser.Api/Playback/MediaInfoService.cs
  31. 7 0
      MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
  32. 0 1
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  33. 1 1
      MediaBrowser.Api/Playback/TranscodingThrottler.cs
  34. 2 2
      MediaBrowser.Api/Session/SessionsService.cs
  35. 1 1
      MediaBrowser.Api/SimilarItemsHelper.cs
  36. 2 2
      MediaBrowser.Api/Subtitles/SubtitleService.cs
  37. 5 6
      MediaBrowser.Api/Sync/SyncService.cs
  38. 2 2
      MediaBrowser.Api/UserLibrary/ArtistsService.cs
  39. 1 2
      MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
  40. 1 1
      MediaBrowser.Api/UserLibrary/GameGenresService.cs
  41. 1 1
      MediaBrowser.Api/UserLibrary/GenresService.cs
  42. 1 1
      MediaBrowser.Api/UserLibrary/MusicGenresService.cs
  43. 1 1
      MediaBrowser.Api/UserLibrary/PersonsService.cs
  44. 45 3
      MediaBrowser.Api/UserLibrary/PlaystateService.cs
  45. 1 1
      MediaBrowser.Api/UserLibrary/StudiosService.cs
  46. 4 9
      MediaBrowser.Common.Implementations/BaseApplicationHost.cs
  47. 8 4
      MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs
  48. 10 7
      MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  49. 2 13
      MediaBrowser.Controller/Channels/Channel.cs
  50. 7 1
      MediaBrowser.Controller/Channels/ChannelAudioItem.cs
  51. 5 0
      MediaBrowser.Controller/Channels/ChannelFolderItem.cs
  52. 12 0
      MediaBrowser.Controller/Channels/ChannelVideoItem.cs
  53. 12 0
      MediaBrowser.Controller/Drawing/IImageProcessor.cs
  54. 32 0
      MediaBrowser.Controller/Drawing/ImageCollageOptions.cs
  55. 8 0
      MediaBrowser.Controller/Dto/IDtoService.cs
  56. 3 4
      MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
  57. 60 36
      MediaBrowser.Controller/Entities/BaseItem.cs
  58. 4 16
      MediaBrowser.Controller/Entities/Folder.cs
  59. 7 1
      MediaBrowser.Controller/Entities/IHasImages.cs
  60. 0 3
      MediaBrowser.Controller/Entities/LinkedChild.cs
  61. 9 9
      MediaBrowser.Controller/Entities/Movies/BoxSet.cs
  62. 33 3
      MediaBrowser.Controller/Entities/PhotoAlbum.cs
  63. 62 23
      MediaBrowser.Controller/Entities/UserViewBuilder.cs
  64. 1 13
      MediaBrowser.Controller/IO/ThrottledStream.cs
  65. 4 20
      MediaBrowser.Controller/Library/IMediaSourceManager.cs
  66. 41 0
      MediaBrowser.Controller/Library/NameExtensions.cs
  67. 7 4
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  68. 3 4
      MediaBrowser.Controller/LiveTv/ILiveTvService.cs
  69. 9 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  70. 2 0
      MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
  71. 3 5
      MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
  72. 1 289
      MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs
  73. 25 0
      MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs
  74. 1 13
      MediaBrowser.Controller/Providers/BaseItemXmlParser.cs
  75. 1 2
      MediaBrowser.Controller/Providers/IImageEnhancer.cs
  76. 13 0
      MediaBrowser.Controller/Providers/IProviderManager.cs
  77. 2 2
      MediaBrowser.Controller/Sync/IHasDynamicAccess.cs
  78. 10 0
      MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs
  79. 14 20
      MediaBrowser.Controller/Sync/IServerSyncProvider.cs
  80. 3 11
      MediaBrowser.Controller/Sync/ISyncDataProvider.cs
  81. 14 0
      MediaBrowser.Controller/Sync/ISyncManager.cs
  82. 5 0
      MediaBrowser.Controller/Sync/SyncedFileInfo.cs
  83. 1 0
      MediaBrowser.Controller/packages.config
  84. 4 4
      MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs
  85. 5 4
      MediaBrowser.Dlna/Didl/DidlBuilder.cs
  86. 4 4
      MediaBrowser.Dlna/PlayTo/PlayToController.cs
  87. 9 3
      MediaBrowser.Dlna/Ssdp/SsdpHandler.cs
  88. 1 1
      MediaBrowser.LocalMetadata/BaseXmlProvider.cs
  89. 6 1
      MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
  90. 5 0
      MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs
  91. 1 1
      MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
  92. 0 5
      MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs
  93. 2 17
      MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs
  94. 6 3
      MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs
  95. 293 99
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  96. 10 1
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  97. 1 1
      MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
  98. 3 3
      MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs
  99. 887 0
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  100. 0 0
      MediaBrowser.MediaEncoding/Probing/whitelist.txt

+ 1 - 1
MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs → Emby.Drawing/Common/ImageHeader.cs

@@ -6,7 +6,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 
-namespace MediaBrowser.Server.Implementations.Drawing
+namespace Emby.Drawing.Common
 {
     /// <summary>
     /// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349

+ 98 - 0
Emby.Drawing/Emby.Drawing.csproj

@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{08FFF49B-F175-4807-A2B5-73B0EBD9F716}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>Emby.Drawing</RootNamespace>
+    <AssemblyName>Emby.Drawing</AssemblyName>
+    <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>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="ImageMagickSharp, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ImageMagickSharp.1.0.0.14\lib\net45\ImageMagickSharp.dll</HintPath>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Drawing" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="..\SharedVersion.cs">
+      <Link>Properties\SharedVersion.cs</Link>
+    </Compile>
+    <Compile Include="GDI\DynamicImageHelpers.cs" />
+    <Compile Include="GDI\GDIImageEncoder.cs" />
+    <Compile Include="GDI\ImageExtensions.cs" />
+    <Compile Include="GDI\PercentPlayedDrawer.cs" />
+    <Compile Include="GDI\PlayedIndicatorDrawer.cs" />
+    <Compile Include="GDI\UnplayedCountIndicator.cs" />
+    <Compile Include="IImageEncoder.cs" />
+    <Compile Include="Common\ImageHeader.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="Properties\AssemblyInfo.cs" />
+    <Compile Include="ImageMagick\UnplayedCountIndicator.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="ImageMagick\fonts\MontserratLight.otf" />
+    <EmbeddedResource Include="ImageMagick\fonts\robotoregular.ttf" />
+    <EmbeddedResource Include="ImageMagick\fonts\webdings.ttf" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
+      <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
+      <Name>MediaBrowser.Common</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj">
+      <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
+      <Name>MediaBrowser.Controller</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+      <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+      <Name>MediaBrowser.Model</Name>
+    </ProjectReference>
+  </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.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

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

@@ -0,0 +1,138 @@
+using Emby.Drawing.ImageMagick;
+using MediaBrowser.Common.IO;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Linq;
+
+namespace Emby.Drawing.GDI
+{
+    public static class DynamicImageHelpers
+    {
+        public static void CreateThumbCollage(List<string> files,
+            IFileSystem fileSystem,
+            string file,
+            int width,
+            int height)
+        {
+            const int numStrips = 4;
+            files = StripCollageBuilder.ProjectPaths(files, numStrips).ToList();
+            
+            const int rows = 1;
+             int cols = numStrips;
+
+            int cellWidth = 2 * (width / 3);
+            int cellHeight = height;
+            var index = 0;
+
+            using (var img = new Bitmap(width, height, PixelFormat.Format32bppPArgb))
+            {
+                using (var graphics = Graphics.FromImage(img))
+                {
+                    graphics.CompositingQuality = CompositingQuality.HighQuality;
+                    graphics.SmoothingMode = SmoothingMode.HighQuality;
+                    graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
+                    graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
+                    graphics.CompositingMode = CompositingMode.SourceCopy;
+
+                    for (var row = 0; row < rows; row++)
+                    {
+                        for (var col = 0; col < cols; col++)
+                        {
+                            var x = col * (cellWidth / 2);
+                            var y = row * cellHeight;
+
+                            if (files.Count > index)
+                            {
+                                using (var fileStream = fileSystem.GetFileStream(files[index], FileMode.Open, FileAccess.Read, FileShare.Read, true))
+                                {
+                                    using (var memoryStream = new MemoryStream())
+                                    {
+                                        fileStream.CopyTo(memoryStream);
+
+                                        memoryStream.Position = 0;
+
+                                        using (var imgtemp = Image.FromStream(memoryStream, true, false))
+                                        {
+                                            graphics.DrawImage(imgtemp, x, y, cellWidth, cellHeight);
+                                        }
+                                    }
+                                }
+                            }
+
+                            index++;
+                        }
+                    }
+                    img.Save(file);
+                }
+            }
+        }
+
+        public static void CreateSquareCollage(List<string> files,
+            IFileSystem fileSystem,
+            string file,
+            int width,
+            int height)
+        {
+            files = StripCollageBuilder.ProjectPaths(files, 4).ToList();
+            
+            const int rows = 2;
+            const int cols = 2;
+
+            int singleSize = width / 2;
+            var index = 0;
+
+            using (var img = new Bitmap(width, height, PixelFormat.Format32bppPArgb))
+            {
+                using (var graphics = Graphics.FromImage(img))
+                {
+                    graphics.CompositingQuality = CompositingQuality.HighQuality;
+                    graphics.SmoothingMode = SmoothingMode.HighQuality;
+                    graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
+                    graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
+                    graphics.CompositingMode = CompositingMode.SourceCopy;
+
+                    for (var row = 0; row < rows; row++)
+                    {
+                        for (var col = 0; col < cols; col++)
+                        {
+                            var x = col * singleSize;
+                            var y = row * singleSize;
+
+                            using (var fileStream = fileSystem.GetFileStream(files[index], FileMode.Open, FileAccess.Read, FileShare.Read, true))
+                            {
+                                using (var memoryStream = new MemoryStream())
+                                {
+                                    fileStream.CopyTo(memoryStream);
+
+                                    memoryStream.Position = 0;
+
+                                    using (var imgtemp = Image.FromStream(memoryStream, true, false))
+                                    {
+                                        graphics.DrawImage(imgtemp, x, y, singleSize, singleSize);
+                                    }
+                                }
+                            }
+
+                            index++;
+                        }
+                    }
+                    img.Save(file);
+                }
+            }
+        }
+
+        private static Stream GetStream(Image image)
+        {
+            var ms = new MemoryStream();
+
+            image.Save(ms, ImageFormat.Png);
+
+            ms.Position = 0;
+
+            return ms;
+        }
+    }
+}

+ 254 - 0
Emby.Drawing/GDI/GDIImageEncoder.cs

@@ -0,0 +1,254 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Linq;
+using ImageFormat = MediaBrowser.Model.Drawing.ImageFormat;
+
+namespace Emby.Drawing.GDI
+{
+    public class GDIImageEncoder : IImageEncoder
+    {
+        private readonly IFileSystem _fileSystem;
+        private readonly ILogger _logger;
+
+        public GDIImageEncoder(IFileSystem fileSystem, ILogger logger)
+        {
+            _fileSystem = fileSystem;
+            _logger = logger;
+        }
+
+        public string[] SupportedInputFormats
+        {
+            get
+            {
+                return new[]
+                {
+                    "png",
+                    "jpeg",
+                    "jpg",
+                    "gif",
+                    "bmp"
+                };
+            }
+        }
+
+        public ImageFormat[] SupportedOutputFormats
+        {
+            get
+            {
+                return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
+            }
+        }
+
+        public ImageSize GetImageSize(string path)
+        {
+            using (var image = Image.FromFile(path))
+            {
+                return new ImageSize
+                {
+                    Width = image.Width,
+                    Height = image.Height
+                };
+            }
+        }
+
+        public void CropWhiteSpace(string inputPath, string outputPath)
+        {
+            using (var image = (Bitmap)Image.FromFile(inputPath))
+            {
+                using (var croppedImage = image.CropWhitespace())
+                {
+                    Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+
+                    using (var outputStream = _fileSystem.GetFileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.Read, false))
+                    {
+                        croppedImage.Save(System.Drawing.Imaging.ImageFormat.Png, outputStream, 100);
+                    }
+                } 
+            }
+        }
+
+        public void EncodeImage(string inputPath, string cacheFilePath, int width, int height, int quality, ImageProcessingOptions options)
+        {
+            var hasPostProcessing = !string.IsNullOrEmpty(options.BackgroundColor) || options.UnplayedCount.HasValue || options.AddPlayedIndicator || options.PercentPlayed > 0;
+
+            using (var originalImage = Image.FromFile(inputPath))
+            {
+                var newWidth = Convert.ToInt32(width);
+                var newHeight = Convert.ToInt32(height);
+
+                var selectedOutputFormat = options.OutputFormat;
+
+                // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
+                // Also, Webp only supports Format32bppArgb and Format32bppRgb
+                var pixelFormat = selectedOutputFormat == ImageFormat.Webp
+                    ? PixelFormat.Format32bppArgb
+                    : PixelFormat.Format32bppPArgb;
+
+                using (var thumbnail = new Bitmap(newWidth, newHeight, pixelFormat))
+                {
+                    // Mono throw an exeception if assign 0 to SetResolution
+                    if (originalImage.HorizontalResolution > 0 && originalImage.VerticalResolution > 0)
+                    {
+                        // Preserve the original resolution
+                        thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
+                    }
+
+                    using (var thumbnailGraph = Graphics.FromImage(thumbnail))
+                    {
+                        thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
+                        thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
+                        thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
+                        thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
+                        thumbnailGraph.CompositingMode = !hasPostProcessing ?
+                            CompositingMode.SourceCopy :
+                            CompositingMode.SourceOver;
+
+                        SetBackgroundColor(thumbnailGraph, options);
+
+                        thumbnailGraph.DrawImage(originalImage, 0, 0, newWidth, newHeight);
+
+                        DrawIndicator(thumbnailGraph, newWidth, newHeight, options);
+
+                        var outputFormat = GetOutputFormat(originalImage, selectedOutputFormat);
+
+                        Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
+
+                        // Save to the cache location
+                        using (var cacheFileStream = _fileSystem.GetFileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, false))
+                        {
+                            // Save to the memory stream
+                            thumbnail.Save(outputFormat, cacheFileStream, quality);
+                        }
+                    }
+                }
+
+            }
+        }
+
+        /// <summary>
+        /// Sets the color of the background.
+        /// </summary>
+        /// <param name="graphics">The graphics.</param>
+        /// <param name="options">The options.</param>
+        private void SetBackgroundColor(Graphics graphics, ImageProcessingOptions options)
+        {
+            var color = options.BackgroundColor;
+
+            if (!string.IsNullOrEmpty(color))
+            {
+                Color drawingColor;
+
+                try
+                {
+                    drawingColor = ColorTranslator.FromHtml(color);
+                }
+                catch
+                {
+                    drawingColor = ColorTranslator.FromHtml("#" + color);
+                }
+
+                graphics.Clear(drawingColor);
+            }
+        }
+
+        /// <summary>
+        /// Draws the indicator.
+        /// </summary>
+        /// <param name="graphics">The graphics.</param>
+        /// <param name="imageWidth">Width of the image.</param>
+        /// <param name="imageHeight">Height of the image.</param>
+        /// <param name="options">The options.</param>
+        private void DrawIndicator(Graphics graphics, int imageWidth, int imageHeight, ImageProcessingOptions options)
+        {
+            if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0))
+            {
+                return;
+            }
+
+            try
+            {
+                if (options.AddPlayedIndicator)
+                {
+                    var currentImageSize = new Size(imageWidth, imageHeight);
+
+                    new PlayedIndicatorDrawer().DrawPlayedIndicator(graphics, currentImageSize);
+                }
+                else if (options.UnplayedCount.HasValue)
+                {
+                    var currentImageSize = new Size(imageWidth, imageHeight);
+
+                    new UnplayedCountIndicator().DrawUnplayedCountIndicator(graphics, currentImageSize, options.UnplayedCount.Value);
+                }
+
+                if (options.PercentPlayed > 0)
+                {
+                    var currentImageSize = new Size(imageWidth, imageHeight);
+
+                    new PercentPlayedDrawer().Process(graphics, currentImageSize, options.PercentPlayed);
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error drawing indicator overlay", ex);
+            }
+        }
+
+        /// <summary>
+        /// Gets the output format.
+        /// </summary>
+        /// <param name="image">The image.</param>
+        /// <param name="outputFormat">The output format.</param>
+        /// <returns>ImageFormat.</returns>
+        private System.Drawing.Imaging.ImageFormat GetOutputFormat(Image image, ImageFormat outputFormat)
+        {
+            switch (outputFormat)
+            {
+                case ImageFormat.Bmp:
+                    return System.Drawing.Imaging.ImageFormat.Bmp;
+                case ImageFormat.Gif:
+                    return System.Drawing.Imaging.ImageFormat.Gif;
+                case ImageFormat.Jpg:
+                    return System.Drawing.Imaging.ImageFormat.Jpeg;
+                case ImageFormat.Png:
+                    return System.Drawing.Imaging.ImageFormat.Png;
+                default:
+                    return image.RawFormat;
+            }
+        }
+
+        public void CreateImageCollage(ImageCollageOptions options)
+        {
+            double ratio = options.Width;
+            ratio /= options.Height;
+
+            if (ratio >= 1.4)
+            {
+                DynamicImageHelpers.CreateThumbCollage(options.InputPaths.ToList(), _fileSystem, options.OutputPath, options.Width, options.Height);
+            }
+            else if (ratio >= .9)
+            {
+                DynamicImageHelpers.CreateSquareCollage(options.InputPaths.ToList(), _fileSystem, options.OutputPath, options.Width, options.Height);
+            }
+            else
+            {
+                DynamicImageHelpers.CreateSquareCollage(options.InputPaths.ToList(), _fileSystem, options.OutputPath, options.Width, options.Width);
+            }
+        }
+
+        public void Dispose()
+        {
+        }
+
+        public string Name
+        {
+            get { return "GDI"; }
+        }
+    }
+}

+ 217 - 0
Emby.Drawing/GDI/ImageExtensions.cs

@@ -0,0 +1,217 @@
+using System;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.IO;
+
+namespace Emby.Drawing.GDI
+{
+    public static class ImageExtensions
+    {
+        /// <summary>
+        /// Saves the image.
+        /// </summary>
+        /// <param name="outputFormat">The output format.</param>
+        /// <param name="image">The image.</param>
+        /// <param name="toStream">To stream.</param>
+        /// <param name="quality">The quality.</param>
+        public static void Save(this Image image, ImageFormat outputFormat, Stream toStream, int quality)
+        {
+            // Use special save methods for jpeg and png that will result in a much higher quality image
+            // All other formats use the generic Image.Save
+            if (ImageFormat.Jpeg.Equals(outputFormat))
+            {
+                SaveAsJpeg(image, toStream, quality);
+            }
+            else if (ImageFormat.Png.Equals(outputFormat))
+            {
+                image.Save(toStream, ImageFormat.Png);
+            }
+            else
+            {
+                image.Save(toStream, outputFormat);
+            }
+        }
+
+        /// <summary>
+        /// Saves the JPEG.
+        /// </summary>
+        /// <param name="image">The image.</param>
+        /// <param name="target">The target.</param>
+        /// <param name="quality">The quality.</param>
+        public static void SaveAsJpeg(this Image image, Stream target, int quality)
+        {
+            using (var encoderParameters = new EncoderParameters(1))
+            {
+                encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality);
+                image.Save(target, GetImageCodecInfo("image/jpeg"), encoderParameters);
+            }
+        }
+
+        private static readonly ImageCodecInfo[] Encoders = ImageCodecInfo.GetImageEncoders();
+
+        /// <summary>
+        /// Gets the image codec info.
+        /// </summary>
+        /// <param name="mimeType">Type of the MIME.</param>
+        /// <returns>ImageCodecInfo.</returns>
+        private static ImageCodecInfo GetImageCodecInfo(string mimeType)
+        {
+            foreach (var encoder in Encoders)
+            {
+                if (string.Equals(encoder.MimeType, mimeType, StringComparison.OrdinalIgnoreCase))
+                {
+                    return encoder;
+                }
+            }
+
+            return Encoders.Length == 0 ? null : Encoders[0];
+        }
+
+        /// <summary>
+        /// Crops an image by removing whitespace and transparency from the edges
+        /// </summary>
+        /// <param name="bmp">The BMP.</param>
+        /// <returns>Bitmap.</returns>
+        /// <exception cref="System.Exception"></exception>
+        public static Bitmap CropWhitespace(this Bitmap bmp)
+        {
+            var width = bmp.Width;
+            var height = bmp.Height;
+
+            var topmost = 0;
+            for (int row = 0; row < height; ++row)
+            {
+                if (IsAllWhiteRow(bmp, row, width))
+                    topmost = row;
+                else break;
+            }
+
+            int bottommost = 0;
+            for (int row = height - 1; row >= 0; --row)
+            {
+                if (IsAllWhiteRow(bmp, row, width))
+                    bottommost = row;
+                else break;
+            }
+
+            int leftmost = 0, rightmost = 0;
+            for (int col = 0; col < width; ++col)
+            {
+                if (IsAllWhiteColumn(bmp, col, height))
+                    leftmost = col;
+                else
+                    break;
+            }
+
+            for (int col = width - 1; col >= 0; --col)
+            {
+                if (IsAllWhiteColumn(bmp, col, height))
+                    rightmost = col;
+                else
+                    break;
+            }
+
+            if (rightmost == 0) rightmost = width; // As reached left
+            if (bottommost == 0) bottommost = height; // As reached top.
+
+            var croppedWidth = rightmost - leftmost;
+            var croppedHeight = bottommost - topmost;
+
+            if (croppedWidth == 0) // No border on left or right
+            {
+                leftmost = 0;
+                croppedWidth = width;
+            }
+
+            if (croppedHeight == 0) // No border on top or bottom
+            {
+                topmost = 0;
+                croppedHeight = height;
+            }
+
+            // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
+            var thumbnail = new Bitmap(croppedWidth, croppedHeight, PixelFormat.Format32bppPArgb);
+
+            // Preserve the original resolution
+            TrySetResolution(thumbnail, bmp.HorizontalResolution, bmp.VerticalResolution);
+
+            using (var thumbnailGraph = Graphics.FromImage(thumbnail))
+            {
+                thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
+                thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
+                thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
+                thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
+                thumbnailGraph.CompositingMode = CompositingMode.SourceCopy;
+
+                thumbnailGraph.DrawImage(bmp,
+                  new RectangleF(0, 0, croppedWidth, croppedHeight),
+                  new RectangleF(leftmost, topmost, croppedWidth, croppedHeight),
+                  GraphicsUnit.Pixel);
+            }
+            return thumbnail;
+        }
+
+        /// <summary>
+        /// Tries the set resolution.
+        /// </summary>
+        /// <param name="bmp">The BMP.</param>
+        /// <param name="x">The x.</param>
+        /// <param name="y">The y.</param>
+        private static void TrySetResolution(Bitmap bmp, float x, float y)
+        {
+            if (x > 0 && y > 0)
+            {
+                bmp.SetResolution(x, y);
+            }
+        }
+
+        /// <summary>
+        /// Determines whether or not a row of pixels is all whitespace
+        /// </summary>
+        /// <param name="bmp">The BMP.</param>
+        /// <param name="row">The row.</param>
+        /// <param name="width">The width.</param>
+        /// <returns><c>true</c> if [is all white row] [the specified BMP]; otherwise, <c>false</c>.</returns>
+        private static bool IsAllWhiteRow(Bitmap bmp, int row, int width)
+        {
+            for (var i = 0; i < width; ++i)
+            {
+                if (!IsWhiteSpace(bmp.GetPixel(i, row)))
+                {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /// <summary>
+        /// Determines whether or not a column of pixels is all whitespace
+        /// </summary>
+        /// <param name="bmp">The BMP.</param>
+        /// <param name="col">The col.</param>
+        /// <param name="height">The height.</param>
+        /// <returns><c>true</c> if [is all white column] [the specified BMP]; otherwise, <c>false</c>.</returns>
+        private static bool IsAllWhiteColumn(Bitmap bmp, int col, int height)
+        {
+            for (var i = 0; i < height; ++i)
+            {
+                if (!IsWhiteSpace(bmp.GetPixel(col, i)))
+                {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /// <summary>
+        /// Determines if a color is whitespace
+        /// </summary>
+        /// <param name="color">The color.</param>
+        /// <returns><c>true</c> if [is white space] [the specified color]; otherwise, <c>false</c>.</returns>
+        private static bool IsWhiteSpace(Color color)
+        {
+            return (color.R == 255 && color.G == 255 && color.B == 255) || color.A == 0;
+        }
+    }
+}

+ 34 - 0
Emby.Drawing/GDI/PercentPlayedDrawer.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Drawing;
+
+namespace Emby.Drawing.GDI
+{
+    public class PercentPlayedDrawer
+    {
+        private const int IndicatorHeight = 8;
+
+        public void Process(Graphics graphics, Size imageSize, double percent)
+        {
+            var y = imageSize.Height - IndicatorHeight;
+
+            using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 0, 0, 0)))
+            {
+                const int innerX = 0;
+                var innerY = y;
+                var innerWidth = imageSize.Width;
+                var innerHeight = imageSize.Height;
+
+                graphics.FillRectangle(backdroundBrush, innerX, innerY, innerWidth, innerHeight);
+
+                using (var foregroundBrush = new SolidBrush(Color.FromArgb(82, 181, 75)))
+                {
+                    double foregroundWidth = innerWidth;
+                    foregroundWidth *= percent;
+                    foregroundWidth /= 100;
+
+                    graphics.FillRectangle(foregroundBrush, innerX, innerY, Convert.ToInt32(Math.Round(foregroundWidth)), innerHeight);
+                }
+            }
+        }
+    }
+}

+ 32 - 0
Emby.Drawing/GDI/PlayedIndicatorDrawer.cs

@@ -0,0 +1,32 @@
+using System.Drawing;
+
+namespace Emby.Drawing.GDI
+{
+    public class PlayedIndicatorDrawer
+    {
+        private const int IndicatorHeight = 40;
+        public const int IndicatorWidth = 40;
+        private const int FontSize = 40;
+        private const int OffsetFromTopRightCorner = 10;
+
+        public void DrawPlayedIndicator(Graphics graphics, Size imageSize)
+        {
+            var x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner;
+
+            using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 82, 181, 75)))
+            {
+                graphics.FillEllipse(backdroundBrush, x, OffsetFromTopRightCorner, IndicatorWidth, IndicatorHeight);
+
+                x = imageSize.Width - 45 - OffsetFromTopRightCorner;
+
+                using (var font = new Font("Webdings", FontSize, FontStyle.Regular, GraphicsUnit.Pixel))
+                {
+                    using (var fontBrush = new SolidBrush(Color.White))
+                    {
+                        graphics.DrawString("a", font, fontBrush, x, OffsetFromTopRightCorner - 2);
+                    }
+                }
+            }
+        }
+    }
+}

+ 50 - 0
Emby.Drawing/GDI/UnplayedCountIndicator.cs

@@ -0,0 +1,50 @@
+using System.Drawing;
+
+namespace Emby.Drawing.GDI
+{
+    public class UnplayedCountIndicator
+    {
+        private const int IndicatorHeight = 41;
+        public const int IndicatorWidth = 41;
+        private const int OffsetFromTopRightCorner = 10;
+
+        public void DrawUnplayedCountIndicator(Graphics graphics, Size imageSize, int count)
+        {
+            var x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner;
+
+            using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 82, 181, 75)))
+            {
+                graphics.FillEllipse(backdroundBrush, x, OffsetFromTopRightCorner, IndicatorWidth, IndicatorHeight);
+
+                var text = count.ToString();
+
+                x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner;
+                var y = OffsetFromTopRightCorner + 6;
+                var fontSize = 24;
+
+                if (text.Length == 1)
+                {
+                    x += 10;
+                }
+                else if (text.Length == 2)
+                {
+                    x += 3;
+                }
+                else if (text.Length == 3)
+                {
+                    x += 1;
+                    y += 1;
+                    fontSize = 20;
+                }
+
+                using (var font = new Font("Sans-Serif", fontSize, FontStyle.Regular, GraphicsUnit.Pixel))
+                {
+                    using (var fontBrush = new SolidBrush(Color.White))
+                    {
+                        graphics.DrawString(text, font, fontBrush, x, y);
+                    }
+                }
+            }
+        }
+    }
+}

+ 53 - 0
Emby.Drawing/IImageEncoder.cs

@@ -0,0 +1,53 @@
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Drawing;
+using System;
+
+namespace Emby.Drawing
+{
+    public interface IImageEncoder : IDisposable
+    {
+        /// <summary>
+        /// Gets the supported input formats.
+        /// </summary>
+        /// <value>The supported input formats.</value>
+        string[] SupportedInputFormats { get; }
+        /// <summary>
+        /// Gets the supported output formats.
+        /// </summary>
+        /// <value>The supported output formats.</value>
+        ImageFormat[] SupportedOutputFormats { get; }
+        /// <summary>
+        /// 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>
+        /// <param name="outputPath">The output path.</param>
+        void CropWhiteSpace(string inputPath, string outputPath);
+        /// <summary>
+        /// Encodes the image.
+        /// </summary>
+        /// <param name="inputPath">The input path.</param>
+        /// <param name="outputPath">The output path.</param>
+        /// <param name="width">The width.</param>
+        /// <param name="height">The height.</param>
+        /// <param name="quality">The quality.</param>
+        /// <param name="options">The options.</param>
+        void EncodeImage(string inputPath, string outputPath, int width, int height, int quality, ImageProcessingOptions options);
+
+        /// <summary>
+        /// Creates the image collage.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        void CreateImageCollage(ImageCollageOptions options);
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        string Name { get; }
+    }
+}

+ 229 - 0
Emby.Drawing/ImageMagick/ImageMagickEncoder.cs

@@ -0,0 +1,229 @@
+using ImageMagickSharp;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Logging;
+using System;
+using System.IO;
+
+namespace Emby.Drawing.ImageMagick
+{
+    public class ImageMagickEncoder : IImageEncoder
+    {
+        private readonly ILogger _logger;
+        private readonly IApplicationPaths _appPaths;
+
+        public ImageMagickEncoder(ILogger logger, IApplicationPaths appPaths)
+        {
+            _logger = logger;
+            _appPaths = appPaths;
+
+            LogImageMagickVersion();
+        }
+
+        public string[] SupportedInputFormats
+        {
+            get
+            {
+                // Some common file name extensions for RAW picture files include: .cr2, .crw, .dng, .nef, .orf, .rw2, .pef, .arw, .sr2, .srf, and .tif.
+                return new[]
+                {
+                    "tiff", 
+                    "jpeg", 
+                    "jpg", 
+                    "png", 
+                    "aiff", 
+                    "cr2", 
+                    "crw", 
+                    "dng", 
+                    "nef", 
+                    "orf", 
+                    "pef", 
+                    "arw", 
+                    "webp",
+                    "gif",
+                    "bmp"
+                };
+            }
+        }
+
+        public ImageFormat[] SupportedOutputFormats
+        {
+            get
+            {
+                if (_webpAvailable)
+                {
+                    return new[] { ImageFormat.Webp, ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
+                }
+                return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
+            }
+        }
+
+        private void LogImageMagickVersion()
+        {
+            _logger.Info("ImageMagick version: " + Wand.VersionString);
+            TestWebp();
+        }
+
+        private bool _webpAvailable = true;
+        private void TestWebp()
+        {
+            try
+            {
+                var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".webp");
+                Directory.CreateDirectory(Path.GetDirectoryName(tmpPath));
+
+                using (var wand = new MagickWand(1, 1, new PixelWand("none", 1)))
+                {
+                    wand.SaveImage(tmpPath);
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error loading webp: ", ex);
+                _webpAvailable = false;
+            }
+        }
+
+        public void CropWhiteSpace(string inputPath, string outputPath)
+        {
+            CheckDisposed();
+
+            using (var wand = new MagickWand(inputPath))
+            {
+                wand.CurrentImage.TrimImage(10);
+                wand.SaveImage(outputPath);
+            }
+        }
+
+        public ImageSize GetImageSize(string path)
+        {
+            CheckDisposed();
+
+            using (var wand = new MagickWand())
+            {
+                wand.PingImage(path);
+                var img = wand.CurrentImage;
+
+                return new ImageSize
+                {
+                    Width = img.Width,
+                    Height = img.Height
+                };
+            }
+        }
+
+        public void EncodeImage(string inputPath, string outputPath, int width, int height, int quality, ImageProcessingOptions options)
+        {
+            if (string.IsNullOrWhiteSpace(options.BackgroundColor))
+            {
+                using (var originalImage = new MagickWand(inputPath))
+                {
+                    originalImage.CurrentImage.ResizeImage(width, height);
+
+                    DrawIndicator(originalImage, width, height, options);
+
+                    originalImage.CurrentImage.CompressionQuality = quality;
+
+                    originalImage.SaveImage(outputPath);
+                }
+            }
+            else
+            {
+                using (var wand = new MagickWand(width, height, options.BackgroundColor))
+                {
+                    using (var originalImage = new MagickWand(inputPath))
+                    {
+                        originalImage.CurrentImage.ResizeImage(width, height);
+
+                        wand.CurrentImage.CompositeImage(originalImage, CompositeOperator.OverCompositeOp, 0, 0);
+                        DrawIndicator(wand, width, height, options);
+
+                        wand.CurrentImage.CompressionQuality = quality;
+
+                        wand.SaveImage(outputPath);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Draws the indicator.
+        /// </summary>
+        /// <param name="wand">The wand.</param>
+        /// <param name="imageWidth">Width of the image.</param>
+        /// <param name="imageHeight">Height of the image.</param>
+        /// <param name="options">The options.</param>
+        private void DrawIndicator(MagickWand wand, int imageWidth, int imageHeight, ImageProcessingOptions options)
+        {
+            if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0))
+            {
+                return;
+            }
+
+            try
+            {
+                if (options.AddPlayedIndicator)
+                {
+                    var currentImageSize = new ImageSize(imageWidth, imageHeight);
+
+                    new PlayedIndicatorDrawer(_appPaths).DrawPlayedIndicator(wand, currentImageSize);
+                }
+                else if (options.UnplayedCount.HasValue)
+                {
+                    var currentImageSize = new ImageSize(imageWidth, imageHeight);
+
+                    new UnplayedCountIndicator(_appPaths).DrawUnplayedCountIndicator(wand, currentImageSize, options.UnplayedCount.Value);
+                }
+
+                if (options.PercentPlayed > 0)
+                {
+                    new PercentPlayedDrawer().Process(wand, options.PercentPlayed);
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error drawing indicator overlay", ex);
+            }
+        }
+
+        public void CreateImageCollage(ImageCollageOptions options)
+        {
+            double ratio = options.Width;
+            ratio /= options.Height;
+
+            if (ratio >= 1.4)
+            {
+                new StripCollageBuilder(_appPaths).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, options.Text);
+            }
+            else if (ratio >= .9)
+            {
+                new StripCollageBuilder(_appPaths).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, options.Text);
+            }
+            else
+            {
+                new StripCollageBuilder(_appPaths).BuildPosterCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, options.Text);
+            }
+        }
+
+        public string Name
+        {
+            get { return "ImageMagick"; }
+        }
+
+        private bool _disposed;
+        public void Dispose()
+        {
+            _disposed = true;
+            Wand.CloseEnvironment();
+        }
+
+        private void CheckDisposed()
+        {
+            if (_disposed)
+            {
+                throw new ObjectDisposedException(GetType().Name);
+            }
+        }
+    }
+}

+ 1 - 1
MediaBrowser.Server.Implementations/Drawing/PercentPlayedDrawer.cs → Emby.Drawing/ImageMagick/PercentPlayedDrawer.cs

@@ -1,7 +1,7 @@
 using ImageMagickSharp;
 using System;
 
-namespace MediaBrowser.Server.Implementations.Drawing
+namespace Emby.Drawing.ImageMagick
 {
     public class PercentPlayedDrawer
     {

+ 1 - 1
MediaBrowser.Server.Implementations/Drawing/PlayedIndicatorDrawer.cs → Emby.Drawing/ImageMagick/PlayedIndicatorDrawer.cs

@@ -4,7 +4,7 @@ using MediaBrowser.Model.Drawing;
 using System;
 using System.IO;
 
-namespace MediaBrowser.Server.Implementations.Drawing
+namespace Emby.Drawing.ImageMagick
 {
     public class PlayedIndicatorDrawer
     {

+ 518 - 0
Emby.Drawing/ImageMagick/StripCollageBuilder.cs

@@ -0,0 +1,518 @@
+using ImageMagickSharp;
+using MediaBrowser.Common.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Emby.Drawing.ImageMagick
+{
+    public class StripCollageBuilder
+    {
+        private readonly IApplicationPaths _appPaths;
+
+        public StripCollageBuilder(IApplicationPaths appPaths)
+        {
+            _appPaths = appPaths;
+        }
+
+        public void BuildPosterCollage(IEnumerable<string> paths, string outputPath, int width, int height, string text)
+        {
+            if (!string.IsNullOrWhiteSpace(text))
+            {
+                using (var wand = BuildPosterCollageWandWithText(paths, text, width, height))
+                {
+                    wand.SaveImage(outputPath);
+                }
+            }
+            else
+            {
+                using (var wand = BuildPosterCollageWand(paths, width, height))
+                {
+                    wand.SaveImage(outputPath);
+                }
+            }
+        }
+
+        public void BuildSquareCollage(IEnumerable<string> paths, string outputPath, int width, int height, string text)
+        {
+            if (!string.IsNullOrWhiteSpace(text))
+            {
+                using (var wand = BuildSquareCollageWandWithText(paths, text, width, height))
+                {
+                    wand.SaveImage(outputPath);
+                }
+            }
+            else
+            {
+                using (var wand = BuildSquareCollageWand(paths, width, height))
+                {
+                    wand.SaveImage(outputPath);
+                }
+            }
+        }
+
+        public void BuildThumbCollage(IEnumerable<string> paths, string outputPath, int width, int height, string text)
+        {
+            if (!string.IsNullOrWhiteSpace(text))
+            {
+                using (var wand = BuildThumbCollageWandWithText(paths, text, width, height))
+                {
+                    wand.SaveImage(outputPath);
+                }
+            }
+            else
+            {
+                using (var wand = BuildThumbCollageWand(paths, width, height))
+                {
+                    wand.SaveImage(outputPath);
+                }
+            }
+        }
+
+        internal static string[] ProjectPaths(IEnumerable<string> paths, int count)
+        {
+            var clone = paths.ToList();
+            var list = new List<string>();
+
+            while (list.Count < count)
+            {
+                foreach (var path in clone)
+                {
+                    list.Add(path);
+
+                    if (list.Count >= count)
+                    {
+                        break;
+                    }
+                }
+            }
+
+            return list.Take(count).ToArray();
+        }
+
+        private MagickWand BuildThumbCollageWandWithText(IEnumerable<string> paths, string text, int width, int height)
+        {
+            var inputPaths = ProjectPaths(paths, 8);
+            using (var wandImages = new MagickWand(inputPaths))
+            {
+                var wand = new MagickWand(width, height);
+                wand.OpenImage("gradient:#111111-#111111");
+                using (var draw = new DrawingWand())
+                {
+                    using (var fcolor = new PixelWand(ColorName.White))
+                    {
+                        draw.FillColor = fcolor;
+                        draw.Font = MontserratLightFont;
+                        draw.FontSize = 60;
+                        draw.FontWeight = FontWeightType.LightStyle;
+                        draw.TextAntialias = true;
+                    }
+
+                    var fontMetrics = wand.QueryFontMetrics(draw, text);
+                    var textContainerY = Convert.ToInt32(height * .165);
+                    wand.CurrentImage.AnnotateImage(draw, (width - fontMetrics.TextWidth) / 2, textContainerY, 0.0, text);
+
+                    var iSlice = Convert.ToInt32(width * .1166666667);
+                    int iTrans = Convert.ToInt32(height * 0.2);
+                    int iHeight = Convert.ToInt32(height * 0.46296296296296296296296296296296);
+                    var horizontalImagePadding = Convert.ToInt32(width * 0.0125);
+
+                    foreach (var element in wandImages.ImageList)
+                    {
+                        int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height);
+                        element.Gravity = GravityType.CenterGravity;
+                        element.BackgroundColor = new PixelWand("none", 1);
+                        element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter);
+                        int ix = (int)Math.Abs((iWidth - iSlice) / 2);
+                        element.CropImage(iSlice, iHeight, ix, 0);
+
+                        element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0);
+                    }
+
+                    wandImages.SetFirstIterator();
+                    using (var wandList = wandImages.AppendImages())
+                    {
+                        wandList.CurrentImage.TrimImage(1);
+                        using (var mwr = wandList.CloneMagickWand())
+                        {
+                            using (var blackPixelWand = new PixelWand(ColorName.Black))
+                            {
+                                using (var greyPixelWand = new PixelWand(ColorName.Grey70))
+                                {
+                                    mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1);
+                                    mwr.CurrentImage.FlipImage();
+
+                                    mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel;
+                                    mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand);
+
+                                    using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans))
+                                    {
+                                        mwg.OpenImage("gradient:black-none");
+                                        var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
+                                        mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.DstInCompositeOp, 0, verticalSpacing);
+
+                                        wandList.AddImage(mwr);
+                                        int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2;
+                                        wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * 0.26851851851851851851851851851852));
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                return wand;
+            }
+        }
+
+        private MagickWand BuildPosterCollageWand(IEnumerable<string> paths, int width, int height)
+        {
+            var inputPaths = ProjectPaths(paths, 4);
+            using (var wandImages = new MagickWand(inputPaths))
+            {
+                var wand = new MagickWand(width, height);
+                wand.OpenImage("gradient:#111111-#111111");
+                using (var draw = new DrawingWand())
+                {
+                    var iSlice = Convert.ToInt32(width * 0.225);
+                    int iTrans = Convert.ToInt32(height * .25);
+                    int iHeight = Convert.ToInt32(height * .65);
+                    var horizontalImagePadding = Convert.ToInt32(width * 0.0275);
+
+                    foreach (var element in wandImages.ImageList)
+                    {
+                        using (var blackPixelWand = new PixelWand(ColorName.Black))
+                        {
+                            int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height);
+                            element.Gravity = GravityType.CenterGravity;
+                            element.BackgroundColor = blackPixelWand;
+                            element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter);
+                            int ix = (int)Math.Abs((iWidth - iSlice) / 2);
+                            element.CropImage(iSlice, iHeight, ix, 0);
+
+                            element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0);
+                        }
+                    }
+
+                    wandImages.SetFirstIterator();
+                    using (var wandList = wandImages.AppendImages())
+                    {
+                        wandList.CurrentImage.TrimImage(1);
+                        using (var mwr = wandList.CloneMagickWand())
+                        {
+                            using (var blackPixelWand = new PixelWand(ColorName.Black))
+                            {
+                                using (var greyPixelWand = new PixelWand(ColorName.Grey70))
+                                {
+                                    mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1);
+                                    mwr.CurrentImage.FlipImage();
+
+                                    mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel;
+                                    mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand);
+
+                                    using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans))
+                                    {
+                                        mwg.OpenImage("gradient:black-none");
+                                        var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
+                                        mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing);
+
+                                        wandList.AddImage(mwr);
+                                        int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2;
+                                        wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .05));
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                return wand;
+            }
+        }
+
+        private MagickWand BuildPosterCollageWandWithText(IEnumerable<string> paths, string label, int width, int height)
+        {
+            var inputPaths = ProjectPaths(paths, 4);
+            using (var wandImages = new MagickWand(inputPaths))
+            {
+                var wand = new MagickWand(width, height);
+                wand.OpenImage("gradient:#111111-#111111");
+                using (var draw = new DrawingWand())
+                {
+                    using (var fcolor = new PixelWand(ColorName.White))
+                    {
+                        draw.FillColor = fcolor;
+                        draw.Font = MontserratLightFont;
+                        draw.FontSize = 60;
+                        draw.FontWeight = FontWeightType.LightStyle;
+                        draw.TextAntialias = true;
+                    }
+
+                    var fontMetrics = wand.QueryFontMetrics(draw, label);
+                    var textContainerY = Convert.ToInt32(height * .165);
+                    wand.CurrentImage.AnnotateImage(draw, (width - fontMetrics.TextWidth) / 2, textContainerY, 0.0, label);
+
+                    var iSlice = Convert.ToInt32(width * 0.225);
+                    int iTrans = Convert.ToInt32(height * 0.2);
+                    int iHeight = Convert.ToInt32(height * 0.46296296296296296296296296296296);
+                    var horizontalImagePadding = Convert.ToInt32(width * 0.0275);
+
+                    foreach (var element in wandImages.ImageList)
+                    {
+                        int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height);
+                        element.Gravity = GravityType.CenterGravity;
+                        element.BackgroundColor = new PixelWand("none", 1);
+                        element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter);
+                        int ix = (int)Math.Abs((iWidth - iSlice) / 2);
+                        element.CropImage(iSlice, iHeight, ix, 0);
+
+                        element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0);
+                    }
+
+                    wandImages.SetFirstIterator();
+                    using (var wandList = wandImages.AppendImages())
+                    {
+                        wandList.CurrentImage.TrimImage(1);
+                        using (var mwr = wandList.CloneMagickWand())
+                        {
+                            using (var blackPixelWand = new PixelWand(ColorName.Black))
+                            {
+                                using (var greyPixelWand = new PixelWand(ColorName.Grey70))
+                                {
+                                    mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1);
+                                    mwr.CurrentImage.FlipImage();
+
+                                    mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel;
+                                    mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand);
+
+                                    using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans))
+                                    {
+                                        mwg.OpenImage("gradient:black-none");
+                                        var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
+                                        mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.DstInCompositeOp, 0, verticalSpacing);
+
+                                        wandList.AddImage(mwr);
+                                        int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2;
+                                        wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * 0.26851851851851851851851851851852));
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                return wand;
+            }
+        }
+
+        private MagickWand BuildThumbCollageWand(IEnumerable<string> paths, int width, int height)
+        {
+            var inputPaths = ProjectPaths(paths, 8);
+            using (var wandImages = new MagickWand(inputPaths))
+            {
+                var wand = new MagickWand(width, height);
+                wand.OpenImage("gradient:#111111-#111111");
+                using (var draw = new DrawingWand())
+                {
+                    var iSlice = Convert.ToInt32(width * .1166666667);
+                    int iTrans = Convert.ToInt32(height * .25);
+                    int iHeight = Convert.ToInt32(height * .62);
+                    var horizontalImagePadding = Convert.ToInt32(width * 0.0125);
+
+                    foreach (var element in wandImages.ImageList)
+                    {
+                        using (var blackPixelWand = new PixelWand(ColorName.Black))
+                        {
+                            int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height);
+                            element.Gravity = GravityType.CenterGravity;
+                            element.BackgroundColor = blackPixelWand;
+                            element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter);
+                            int ix = (int)Math.Abs((iWidth - iSlice) / 2);
+                            element.CropImage(iSlice, iHeight, ix, 0);
+
+                            element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0);
+                        }
+                    }
+
+                    wandImages.SetFirstIterator();
+                    using (var wandList = wandImages.AppendImages())
+                    {
+                        wandList.CurrentImage.TrimImage(1);
+                        using (var mwr = wandList.CloneMagickWand())
+                        {
+                            using (var blackPixelWand = new PixelWand(ColorName.Black))
+                            {
+                                using (var greyPixelWand = new PixelWand(ColorName.Grey70))
+                                {
+                                    mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1);
+                                    mwr.CurrentImage.FlipImage();
+
+                                    mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel;
+                                    mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand);
+
+                                    using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans))
+                                    {
+                                        mwg.OpenImage("gradient:black-none");
+                                        var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
+                                        mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing);
+
+                                        wandList.AddImage(mwr);
+                                        int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2;
+                                        wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .085));
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                return wand;
+            }
+        }
+
+        private MagickWand BuildSquareCollageWand(IEnumerable<string> paths, int width, int height)
+        {
+            var inputPaths = ProjectPaths(paths, 4);
+            using (var wandImages = new MagickWand(inputPaths))
+            {
+                var wand = new MagickWand(width, height);
+                wand.OpenImage("gradient:#111111-#111111");
+                using (var draw = new DrawingWand())
+                {
+                    var iSlice = Convert.ToInt32(width * .225);
+                    int iTrans = Convert.ToInt32(height * .25);
+                    int iHeight = Convert.ToInt32(height * .63);
+                    var horizontalImagePadding = Convert.ToInt32(width * 0.02);
+
+                    foreach (var element in wandImages.ImageList)
+                    {
+                        using (var blackPixelWand = new PixelWand(ColorName.Black))
+                        {
+                            int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height);
+                            element.Gravity = GravityType.CenterGravity;
+                            element.BackgroundColor = blackPixelWand;
+                            element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter);
+                            int ix = (int)Math.Abs((iWidth - iSlice) / 2);
+                            element.CropImage(iSlice, iHeight, ix, 0);
+
+                            element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0);
+                        } 
+                    }
+
+                    wandImages.SetFirstIterator();
+                    using (var wandList = wandImages.AppendImages())
+                    {
+                        wandList.CurrentImage.TrimImage(1);
+                        using (var mwr = wandList.CloneMagickWand())
+                        {
+                            using (var blackPixelWand = new PixelWand(ColorName.Black))
+                            {
+                                using (var greyPixelWand = new PixelWand(ColorName.Grey70))
+                                {
+                                    mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1);
+                                    mwr.CurrentImage.FlipImage();
+
+                                    mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel;
+                                    mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand);
+
+                                    using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans))
+                                    {
+                                        mwg.OpenImage("gradient:black-none");
+                                        var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
+                                        mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing);
+
+                                        wandList.AddImage(mwr);
+                                        int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2;
+                                        wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .07));
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                return wand;
+            }
+        }
+
+        private MagickWand BuildSquareCollageWandWithText(IEnumerable<string> paths, string label, int width, int height)
+        {
+            var inputPaths = ProjectPaths(paths, 4);
+            using (var wandImages = new MagickWand(inputPaths))
+            {
+                var wand = new MagickWand(width, height);
+                wand.OpenImage("gradient:#111111-#111111");
+                using (var draw = new DrawingWand())
+                {
+                    using (var fcolor = new PixelWand(ColorName.White))
+                    {
+                        draw.FillColor = fcolor;
+                        draw.Font = MontserratLightFont;
+                        draw.FontSize = 60;
+                        draw.FontWeight = FontWeightType.LightStyle;
+                        draw.TextAntialias = true;
+                    }
+
+                    var fontMetrics = wand.QueryFontMetrics(draw, label);
+                    var textContainerY = Convert.ToInt32(height * .165);
+                    wand.CurrentImage.AnnotateImage(draw, (width - fontMetrics.TextWidth) / 2, textContainerY, 0.0, label);
+
+                    var iSlice = Convert.ToInt32(width * .225);
+                    int iTrans = Convert.ToInt32(height * 0.2);
+                    int iHeight = Convert.ToInt32(height * 0.46296296296296296296296296296296);
+                    var horizontalImagePadding = Convert.ToInt32(width * 0.02);
+
+                    foreach (var element in wandImages.ImageList)
+                    {
+                        int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height);
+                        element.Gravity = GravityType.CenterGravity;
+                        element.BackgroundColor = new PixelWand("none", 1);
+                        element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter);
+                        int ix = (int)Math.Abs((iWidth - iSlice) / 2);
+                        element.CropImage(iSlice, iHeight, ix, 0);
+
+                        element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0);
+                    }
+
+                    wandImages.SetFirstIterator();
+                    using (var wandList = wandImages.AppendImages())
+                    {
+                        wandList.CurrentImage.TrimImage(1);
+                        using (var mwr = wandList.CloneMagickWand())
+                        {
+                            using (var blackPixelWand = new PixelWand(ColorName.Black))
+                            {
+                                using (var greyPixelWand = new PixelWand(ColorName.Grey70))
+                                {
+                                    mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1);
+                                    mwr.CurrentImage.FlipImage();
+
+                                    mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel;
+                                    mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand);
+
+                                    using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans))
+                                    {
+                                        mwg.OpenImage("gradient:black-none");
+                                        var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
+                                        mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.DstInCompositeOp, 0, verticalSpacing);
+
+                                        wandList.AddImage(mwr);
+                                        int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2;
+                                        wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * 0.26851851851851851851851851851852));
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                return wand;
+            }
+        }
+
+        private string MontserratLightFont
+        {
+            get { return PlayedIndicatorDrawer.ExtractFont("MontserratLight.otf", _appPaths); }
+        }
+    }
+}

+ 1 - 1
MediaBrowser.Server.Implementations/Drawing/UnplayedCountIndicator.cs → Emby.Drawing/ImageMagick/UnplayedCountIndicator.cs

@@ -3,7 +3,7 @@ using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.Drawing;
 using System.Globalization;
 
-namespace MediaBrowser.Server.Implementations.Drawing
+namespace Emby.Drawing.ImageMagick
 {
     public class UnplayedCountIndicator
     {

+ 23 - 130
MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs → Emby.Drawing/ImageProcessor.cs

@@ -1,4 +1,4 @@
-using ImageMagickSharp;
+using Emby.Drawing.Common;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller;
@@ -18,7 +18,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Server.Implementations.Drawing
+namespace Emby.Drawing
 {
     /// <summary>
     /// Class ImageProcessor
@@ -50,12 +50,14 @@ namespace MediaBrowser.Server.Implementations.Drawing
         private readonly IFileSystem _fileSystem;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IServerApplicationPaths _appPaths;
+        private readonly IImageEncoder _imageEncoder;
 
-        public ImageProcessor(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer jsonSerializer)
+        public ImageProcessor(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IImageEncoder imageEncoder)
         {
             _logger = logger;
             _fileSystem = fileSystem;
             _jsonSerializer = jsonSerializer;
+            _imageEncoder = imageEncoder;
             _appPaths = appPaths;
 
             _saveImageSizeTimer = new Timer(SaveImageSizeCallback, null, Timeout.Infinite, Timeout.Infinite);
@@ -85,8 +87,14 @@ namespace MediaBrowser.Server.Implementations.Drawing
             }
 
             _cachedImagedSizes = new ConcurrentDictionary<Guid, ImageSize>(sizeDictionary);
+        }
 
-            LogImageMagickVersionVersion();
+        public string[] SupportedInputFormats
+        {
+            get
+            {
+                return _imageEncoder.SupportedInputFormats;
+            }
         }
 
         private string ResizedImageCachePath
@@ -130,44 +138,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
 
         public ImageFormat[] GetSupportedImageOutputFormats()
         {
-            if (_webpAvailable)
-            {
-                return new[] { ImageFormat.Webp, ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
-            }
-            return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
-        }
-
-        private bool _webpAvailable = true;
-        private void TestWebp()
-        {
-            try
-            {
-                var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".webp");
-                Directory.CreateDirectory(Path.GetDirectoryName(tmpPath));
-
-                using (var wand = new MagickWand(1, 1, new PixelWand("none", 1)))
-                {
-                    wand.SaveImage(tmpPath);
-                }
-            }
-            catch (Exception ex)
-            {
-                _logger.ErrorException("Error loading webp: ", ex);
-                _webpAvailable = false;
-            }
-        }
-
-        private void LogImageMagickVersionVersion()
-        {
-            try
-            {
-                _logger.Info("ImageMagick version: " + Wand.VersionString);
-            }
-            catch (Exception ex)
-            {
-                _logger.ErrorException("Error loading ImageMagick: ", ex);
-            }
-            TestWebp();
+            return _imageEncoder.SupportedOutputFormats;
         }
 
         public async Task<string> ProcessImage(ImageProcessingOptions options)
@@ -244,36 +215,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
 
                     Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
 
-                    if (string.IsNullOrWhiteSpace(options.BackgroundColor))
-                    {
-                        using (var originalImage = new MagickWand(originalImagePath))
-                        {
-                            originalImage.CurrentImage.ResizeImage(newWidth, newHeight);
-
-                            DrawIndicator(originalImage, newWidth, newHeight, options);
-
-                            originalImage.CurrentImage.CompressionQuality = quality;
-
-                            originalImage.SaveImage(cacheFilePath);
-                        }
-                    }
-                    else
-                    {
-                        using (var wand = new MagickWand(newWidth, newHeight, options.BackgroundColor))
-                        {
-                            using (var originalImage = new MagickWand(originalImagePath))
-                            {
-                                originalImage.CurrentImage.ResizeImage(newWidth, newHeight);
-
-                                wand.CurrentImage.CompositeImage(originalImage, CompositeOperator.OverCompositeOp, 0, 0);
-                                DrawIndicator(wand, newWidth, newHeight, options);
-
-                                wand.CurrentImage.CompressionQuality = quality;
-
-                                wand.SaveImage(cacheFilePath);
-                            }
-                        }
-                    }
+                    _imageEncoder.EncodeImage(originalImagePath, cacheFilePath, newWidth, newHeight, quality, options);
                 }
             }
             finally
@@ -286,7 +228,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
 
         private ImageFormat GetOutputFormat(ImageFormat requestedFormat)
         {
-            if (requestedFormat == ImageFormat.Webp && !_webpAvailable)
+            if (requestedFormat == ImageFormat.Webp && !_imageEncoder.SupportedOutputFormats.Contains(ImageFormat.Webp))
             {
                 return ImageFormat.Png;
             }
@@ -294,46 +236,6 @@ namespace MediaBrowser.Server.Implementations.Drawing
             return requestedFormat;
         }
 
-        /// <summary>
-        /// Draws the indicator.
-        /// </summary>
-        /// <param name="wand">The wand.</param>
-        /// <param name="imageWidth">Width of the image.</param>
-        /// <param name="imageHeight">Height of the image.</param>
-        /// <param name="options">The options.</param>
-        private void DrawIndicator(MagickWand wand, int imageWidth, int imageHeight, ImageProcessingOptions options)
-        {
-            if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0))
-            {
-                return;
-            }
-
-            try
-            {
-                if (options.AddPlayedIndicator)
-                {
-                    var currentImageSize = new ImageSize(imageWidth, imageHeight);
-
-                    new PlayedIndicatorDrawer(_appPaths).DrawPlayedIndicator(wand, currentImageSize);
-                }
-                else if (options.UnplayedCount.HasValue)
-                {
-                    var currentImageSize = new ImageSize(imageWidth, imageHeight);
-
-                    new UnplayedCountIndicator(_appPaths).DrawUnplayedCountIndicator(wand, currentImageSize, options.UnplayedCount.Value);
-                }
-
-                if (options.PercentPlayed > 0)
-                {
-                    new PercentPlayedDrawer().Process(wand, options.PercentPlayed);
-                }
-            }
-            catch (Exception ex)
-            {
-                _logger.ErrorException("Error drawing indicator overlay", ex);
-            }
-        }
-
         /// <summary>
         /// Crops whitespace from an image, caches the result, and returns the cached path
         /// </summary>
@@ -360,11 +262,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
             {
                 Directory.CreateDirectory(Path.GetDirectoryName(croppedImagePath));
 
-                using (var wand = new MagickWand(originalImagePath))
-                {
-                    wand.CurrentImage.TrimImage(10);
-                    wand.SaveImage(croppedImagePath);
-                }
+                _imageEncoder.CropWhiteSpace(originalImagePath, croppedImagePath);
             }
             catch (Exception ex)
             {
@@ -500,17 +398,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
 
                 CheckDisposed();
 
-                using (var wand = new MagickWand())
-                {
-                    wand.PingImage(path);
-                    var img = wand.CurrentImage;
-
-                    size = new ImageSize
-                    {
-                        Width = img.Width,
-                        Height = img.Height
-                    };
-                }
+                size = _imageEncoder.GetImageSize(path);
             }
 
             StartSaveImageSizeTimer();
@@ -838,6 +726,11 @@ namespace MediaBrowser.Server.Implementations.Drawing
             return Path.Combine(path, filename);
         }
 
+        public void CreateImageCollage(ImageCollageOptions options)
+        {
+            _imageEncoder.CreateImageCollage(options);
+        }
+
         public IEnumerable<IImageEnhancer> GetSupportedEnhancers(IHasImages item, ImageType imageType)
         {
             return ImageEnhancers.Where(i =>
@@ -860,7 +753,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
         public void Dispose()
         {
             _disposed = true;
-            Wand.CloseEnvironment();
+            _imageEncoder.Dispose();
             _saveImageSizeTimer.Dispose();
         }
 

+ 31 - 0
Emby.Drawing/Properties/AssemblyInfo.cs

@@ -0,0 +1,31 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Emby.Drawing")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Emby.Drawing")]
+[assembly: AssemblyCopyright("Copyright ©  2015")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("87b6f14e-16d8-4a58-a553-fd9945e47458")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//

+ 4 - 0
Emby.Drawing/packages.config

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="ImageMagickSharp" version="1.0.0.14" targetFramework="net45" />
+</packages>

+ 142 - 45
MediaBrowser.Api/ApiEntryPoint.cs

@@ -151,7 +151,7 @@ namespace MediaBrowser.Api
         {
             lock (_activeTranscodingJobs)
             {
-                var job = new TranscodingJob
+                var job = new TranscodingJob(Logger)
                 {
                     Type = type,
                     Path = path,
@@ -284,28 +284,72 @@ namespace MediaBrowser.Api
         {
             job.ActiveRequestCount++;
 
-            job.DisposeKillTimer();
+            if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
+            {
+                job.StopKillTimer();
+            }
         }
-        
+
         public void OnTranscodeEndRequest(TranscodingJob job)
         {
             job.ActiveRequestCount--;
+            Logger.Debug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
+            if (job.ActiveRequestCount <= 0)
+            {
+                PingTimer(job, false);
+            }
+        }
+        internal void PingTranscodingJob(string playSessionId)
+        {
+            if (string.IsNullOrEmpty(playSessionId))
+            {
+                throw new ArgumentNullException("playSessionId");
+            }
+
+            Logger.Debug("PingTranscodingJob PlaySessionId={0}", playSessionId);
+
+            var jobs = new List<TranscodingJob>();
 
-            if (job.ActiveRequestCount == 0)
+            lock (_activeTranscodingJobs)
             {
-                // TODO: Lower this hls timeout
-                var timerDuration = job.Type == TranscodingJobType.Progressive ?
-                    1000 :
-                    7200000;
+                // This is really only needed for HLS. 
+                // Progressive streams can stop on their own reliably
+                jobs = jobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
+            }
 
-                if (job.KillTimer == null)
-                {
-                    job.KillTimer = new Timer(OnTranscodeKillTimerStopped, job, timerDuration, Timeout.Infinite);
-                }
-                else
-                {
-                    job.KillTimer.Change(timerDuration, Timeout.Infinite);
-                }
+            foreach (var job in jobs)
+            {
+                PingTimer(job, true);
+            }
+        }
+
+        private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
+        {
+            if (job.HasExited)
+            {
+                job.StopKillTimer();
+                return;
+            }
+
+            // TODO: Lower this hls timeout
+            var timerDuration = job.Type == TranscodingJobType.Progressive ?
+                1000 :
+                1800000;
+
+            // We can really reduce the timeout for apps that are using the newer api
+            if (!string.IsNullOrWhiteSpace(job.PlaySessionId) && job.Type != TranscodingJobType.Progressive)
+            {
+                timerDuration = 20000;
+            }
+
+            // Don't start the timer for playback checkins with progressive streaming
+            if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
+            {
+                job.StartKillTimer(timerDuration, OnTranscodeKillTimerStopped);
+            }
+            else
+            {
+                job.ChangeKillTimerIfStarted(timerDuration);
             }
         }
 
@@ -317,6 +361,8 @@ namespace MediaBrowser.Api
         {
             var job = (TranscodingJob)state;
 
+            Logger.Debug("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
+
             KillTranscodingJob(job, path => true);
         }
 
@@ -329,19 +375,14 @@ namespace MediaBrowser.Api
         /// <returns>Task.</returns>
         internal void KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
         {
-            if (string.IsNullOrEmpty(deviceId))
-            {
-                throw new ArgumentNullException("deviceId");
-            }
-
             KillTranscodingJobs(j =>
             {
-                if (string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase))
+                if (!string.IsNullOrWhiteSpace(playSessionId))
                 {
-                    return string.IsNullOrWhiteSpace(playSessionId) || string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase);
+                    return string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase);
                 }
 
-                return false;
+                return string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase);
 
             }, deleteFiles);
         }
@@ -381,6 +422,10 @@ namespace MediaBrowser.Api
         /// <param name="delete">The delete.</param>
         private void KillTranscodingJob(TranscodingJob job, Func<string, bool> delete)
         {
+            job.DisposeKillTimer();
+
+            Logger.Debug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
+            
             lock (_activeTranscodingJobs)
             {
                 _activeTranscodingJobs.Remove(job);
@@ -389,34 +434,23 @@ namespace MediaBrowser.Api
                 {
                     job.CancellationTokenSource.Cancel();
                 }
-
-                job.DisposeKillTimer();
             }
 
             lock (job.ProcessLock)
             {
-                var process = job.Process;
-
-                var hasExited = true;
-
-                try
+                if (job.TranscodingThrottler != null)
                 {
-                    hasExited = process.HasExited;
-                }
-                catch (Exception ex)
-                {
-                    Logger.ErrorException("Error determining if ffmpeg process has exited for {0}", ex, job.Path);
+                    job.TranscodingThrottler.Stop();
                 }
 
+                var process = job.Process;
+
+                var hasExited = job.HasExited;
+
                 if (!hasExited)
                 {
                     try
                     {
-                        if (job.TranscodingThrottler != null)
-                        {
-                            job.TranscodingThrottler.Stop();
-                        }
-
                         Logger.Info("Killing ffmpeg process for {0}", job.Path);
 
                         //process.Kill();
@@ -558,6 +592,7 @@ namespace MediaBrowser.Api
         /// </summary>
         /// <value>The process.</value>
         public Process Process { get; set; }
+        public ILogger Logger { get; private set; }
         /// <summary>
         /// Gets or sets the active request count.
         /// </summary>
@@ -567,7 +602,7 @@ namespace MediaBrowser.Api
         /// Gets or sets the kill timer.
         /// </summary>
         /// <value>The kill timer.</value>
-        public Timer KillTimer { get; set; }
+        private Timer KillTimer { get; set; }
 
         public string DeviceId { get; set; }
 
@@ -590,12 +625,74 @@ namespace MediaBrowser.Api
 
         public TranscodingThrottler TranscodingThrottler { get; set; }
 
+        private readonly object _timerLock = new object();
+
+        public TranscodingJob(ILogger logger)
+        {
+            Logger = logger;
+        }
+
+        public void StopKillTimer()
+        {
+            lock (_timerLock)
+            {
+                if (KillTimer != null)
+                {
+                    KillTimer.Change(Timeout.Infinite, Timeout.Infinite);
+                }
+            }
+        }
+
         public void DisposeKillTimer()
         {
-            if (KillTimer != null)
+            lock (_timerLock)
+            {
+                if (KillTimer != null)
+                {
+                    KillTimer.Dispose();
+                    KillTimer = null;
+                }
+            }
+        }
+
+        public void StartKillTimer(int intervalMs, TimerCallback callback)
+        {
+            CheckHasExited();
+
+            lock (_timerLock)
+            {
+                if (KillTimer == null)
+                {
+                    Logger.Debug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
+                    KillTimer = new Timer(callback, this, intervalMs, Timeout.Infinite);
+                }
+                else
+                {
+                    Logger.Debug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
+                    KillTimer.Change(intervalMs, Timeout.Infinite);
+                }
+            }
+        }
+
+        public void ChangeKillTimerIfStarted(int intervalMs)
+        {
+            CheckHasExited();
+
+            lock (_timerLock)
+            {
+                if (KillTimer != null)
+                {
+                    Logger.Debug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
+                    KillTimer.Change(intervalMs, Timeout.Infinite);
+                }
+            }
+        }
+
+        private void CheckHasExited()
+        {
+            if (HasExited)
             {
-                KillTimer.Dispose();
-                KillTimer = null;
+                throw new ObjectDisposedException("Job");
             }
         }
     }

+ 5 - 5
MediaBrowser.Api/BaseApiService.cs

@@ -259,7 +259,7 @@ namespace MediaBrowser.Api
                 .GetRecursiveChildren(i => i is IHasArtist)
                 .Cast<IHasArtist>()
                 .SelectMany(i => i.AllArtists)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .FirstOrDefault(i =>
                 {
                     i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
@@ -281,7 +281,7 @@ namespace MediaBrowser.Api
 
             return libraryManager.RootFolder.GetRecursiveChildren()
                 .SelectMany(i => i.Genres)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .FirstOrDefault(i =>
                 {
                     i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
@@ -301,7 +301,7 @@ namespace MediaBrowser.Api
             return libraryManager.RootFolder
                 .GetRecursiveChildren(i => i is Game)
                 .SelectMany(i => i.Genres)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .FirstOrDefault(i =>
                 {
                     i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
@@ -324,7 +324,7 @@ namespace MediaBrowser.Api
             return libraryManager.RootFolder
                 .GetRecursiveChildren()
                 .SelectMany(i => i.Studios)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .FirstOrDefault(i =>
                 {
                     i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
@@ -348,7 +348,7 @@ namespace MediaBrowser.Api
                 .GetRecursiveChildren()
                 .SelectMany(i => i.People)
                 .Select(i => i.Name)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .FirstOrDefault(i =>
                 {
                     i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));

+ 1 - 1
MediaBrowser.Api/ConfigurationService.cs

@@ -123,7 +123,7 @@ namespace MediaBrowser.Api
 
         public void Post(AutoSetMetadataOptions request)
         {
-            _configurationManager.DisableMetadataService("Media Browser Xml");
+            _configurationManager.DisableMetadataService("Emby Xml");
             _configurationManager.SaveConfiguration();
         }
 

+ 1 - 1
MediaBrowser.Api/FilterService.cs

@@ -76,7 +76,7 @@ namespace MediaBrowser.Api
                 .ToArray();
 
             result.Genres = items.SelectMany(i => i.Genres)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .OrderBy(i => i)
                 .ToArray();
 

+ 22 - 7
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dto;
@@ -186,6 +187,9 @@ namespace MediaBrowser.Api.LiveTv
         [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
         public bool? IsMovie { get; set; }
 
+        [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
+        public bool? IsSports { get; set; }
+
         [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
         public int? StartIndex { get; set; }
 
@@ -218,6 +222,9 @@ namespace MediaBrowser.Api.LiveTv
         [ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
         public bool? HasAired { get; set; }
 
+        [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
+        public bool? IsSports { get; set; }
+
         [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
         public bool? IsMovie { get; set; }
     }
@@ -422,11 +429,12 @@ namespace MediaBrowser.Api.LiveTv
             query.SortBy = (request.SortBy ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
             query.SortOrder = request.SortOrder;
             query.IsMovie = request.IsMovie;
+            query.IsSports = request.IsSports;
             query.Genres = (request.Genres ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
 
             var result = await _liveTvManager.GetPrograms(query, CancellationToken.None).ConfigureAwait(false);
 
-            return ToOptimizedSerializedResultUsingCache(result);
+            return ToOptimizedResult(result);
         }
 
         public async Task<object> Get(GetRecommendedPrograms request)
@@ -437,12 +445,13 @@ namespace MediaBrowser.Api.LiveTv
                 IsAiring = request.IsAiring,
                 Limit = request.Limit,
                 HasAired = request.HasAired,
-                IsMovie = request.IsMovie
+                IsMovie = request.IsMovie,
+                IsSports = request.IsSports
             };
 
             var result = await _liveTvManager.GetRecommendedPrograms(query, CancellationToken.None).ConfigureAwait(false);
 
-            return ToOptimizedSerializedResultUsingCache(result);
+            return ToOptimizedResult(result);
         }
 
         public object Post(GetPrograms request)
@@ -452,6 +461,9 @@ namespace MediaBrowser.Api.LiveTv
 
         public async Task<object> Get(GetRecordings request)
         {
+            var options = new DtoOptions();
+            options.DeviceId = AuthorizationContext.GetAuthorizationInfo(Request).DeviceId;
+
             var result = await _liveTvManager.GetRecordings(new RecordingQuery
             {
                 ChannelId = request.ChannelId,
@@ -463,16 +475,19 @@ namespace MediaBrowser.Api.LiveTv
                 SeriesTimerId = request.SeriesTimerId,
                 IsInProgress = request.IsInProgress
 
-            }, CancellationToken.None).ConfigureAwait(false);
+            }, options, CancellationToken.None).ConfigureAwait(false);
 
-            return ToOptimizedSerializedResultUsingCache(result);
+            return ToOptimizedResult(result);
         }
 
         public async Task<object> Get(GetRecording request)
         {
             var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(request.UserId);
 
-            var result = await _liveTvManager.GetRecording(request.Id, CancellationToken.None, user).ConfigureAwait(false);
+            var options = new DtoOptions();
+            options.DeviceId = AuthorizationContext.GetAuthorizationInfo(Request).DeviceId;
+
+            var result = await _liveTvManager.GetRecording(request.Id, options, CancellationToken.None, user).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }

+ 2 - 2
MediaBrowser.Api/Movies/MoviesService.cs

@@ -410,7 +410,7 @@ namespace MediaBrowser.Api.Movies
             return items
                 .SelectMany(i => i.People.Where(p => !string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)).Take(2))
                 .Select(i => i.Name)
-                .Distinct(StringComparer.OrdinalIgnoreCase);
+                .DistinctNames();
         }
 
         private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
@@ -419,7 +419,7 @@ namespace MediaBrowser.Api.Movies
                 .Select(i => i.People.FirstOrDefault(p => string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)))
                 .Where(i => i != null)
                 .Select(i => i.Name)
-                .Distinct(StringComparer.OrdinalIgnoreCase);
+                .DistinctNames();
         }
     }
 }

+ 2 - 2
MediaBrowser.Api/Music/AlbumsService.cs

@@ -79,12 +79,12 @@ namespace MediaBrowser.Api.Music
 
             var artists1 = album1
                 .AllArtists
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .ToList();
 
             var artists2 = album2
                 .AllArtists
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
 
             return points + artists1.Where(artists2.ContainsKey).Sum(i => 5);

+ 109 - 4
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -1026,7 +1026,7 @@ namespace MediaBrowser.Api.Playback
             // 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);
 
-            var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(commandLineLogMessage + Environment.NewLine + Environment.NewLine);
+            var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(Request.AbsoluteUri + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
             await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
 
             process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
@@ -1514,6 +1514,10 @@ namespace MediaBrowser.Api.Playback
                     request.PlaySessionId = val;
                 }
                 else if (i == 22)
+                {
+                    // api_key
+                }
+                else if (i == 23)
                 {
                     request.LiveStreamId = val;
                 }
@@ -1624,14 +1628,19 @@ namespace MediaBrowser.Api.Playback
             var archivable = item as IArchivable;
             state.IsInputArchive = archivable != null && archivable.IsArchive;
 
-            MediaSourceInfo mediaSource = null;
+            MediaSourceInfo mediaSource;
             if (string.IsNullOrWhiteSpace(request.LiveStreamId))
             {
-                var mediaSources = await MediaSourceManager.GetPlayackMediaSources(request.Id, false, cancellationToken).ConfigureAwait(false);
+                var mediaSources = (await MediaSourceManager.GetPlayackMediaSources(request.Id, null, false, new[] { MediaType.Audio, MediaType.Video }, cancellationToken).ConfigureAwait(false)).ToList();
 
                 mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
                    ? mediaSources.First()
-                   : mediaSources.First(i => string.Equals(i.Id, request.MediaSourceId));
+                   : mediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId));
+
+                if (mediaSource == null && string.Equals(request.Id, request.MediaSourceId, StringComparison.OrdinalIgnoreCase))
+                {
+                    mediaSource = mediaSources.First();
+                }
             }
             else
             {
@@ -1700,6 +1709,102 @@ namespace MediaBrowser.Api.Playback
             {
                 state.OutputAudioCodec = "copy";
             }
+
+            if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && TranscodingJobType == TranscodingJobType.Hls)
+            {
+                var segmentLength = GetSegmentLength(state);
+                if (segmentLength.HasValue)
+                {
+                    state.SegmentLength = segmentLength.Value;
+                }
+            }
+        }
+
+        private int? GetSegmentLength(StreamState state)
+        {
+            var stream = state.VideoStream;
+
+            if (stream == null)
+            {
+                return null;
+            }
+
+            var frames = stream.KeyFrames;
+
+            if (frames == null || frames.Count < 2)
+            {
+                return null;
+            }
+
+            Logger.Debug("Found keyframes at {0}", string.Join(",", frames.ToArray()));
+
+            var intervals = new List<int>();
+            for (var i = 1; i < frames.Count; i++)
+            {
+                var start = frames[i - 1];
+                var end = frames[i];
+                intervals.Add(end - start);
+            }
+
+            Logger.Debug("Found keyframes intervals {0}", string.Join(",", intervals.ToArray()));
+
+            var results = new List<Tuple<int, int>>();
+
+            for (var i = 1; i <= 10; i++)
+            {
+                var idealMs = i*1000;
+
+                if (intervals.Max() < idealMs - 1000)
+                {
+                    break;
+                }
+
+                var segments = PredictStreamCopySegments(intervals, idealMs);
+                var variance = segments.Select(s => Math.Abs(idealMs - s)).Sum();
+
+                results.Add(new Tuple<int, int>(i, variance));
+            }
+
+            if (results.Count == 0)
+            {
+                return null;
+            }
+
+            return results.OrderBy(i => i.Item2).ThenBy(i => i.Item1).Select(i => i.Item1).First();
+        }
+
+        private List<int> PredictStreamCopySegments(List<int> intervals, int idealMs)
+        {
+            var segments = new List<int>();
+            var currentLength = 0;
+
+            foreach (var interval in intervals)
+            {
+                if (currentLength == 0 || (currentLength + interval) <= idealMs)
+                {
+                    currentLength += interval;
+                }
+
+                else
+                {
+                    // The segment will either be above or below the ideal. 
+                    // Need to figure out which is preferable
+                    var offset1 = Math.Abs(idealMs - currentLength);
+                    var offset2 = Math.Abs(idealMs - (currentLength + interval));
+
+                    if (offset1 <= offset2)
+                    {
+                        segments.Add(currentLength);
+                        currentLength = interval;
+                    }
+                    else
+                    {
+                        currentLength += interval;
+                    }
+                }
+            }
+            Logger.Debug("Predicted actual segment lengths for length {0}: {1}", idealMs, string.Join(",", segments.ToArray()));
+            return segments;
         }
 
         private void AttachMediaSourceInfo(StreamState state,

+ 1 - 13
MediaBrowser.Api/Playback/Dash/MpegDashService.cs

@@ -5,7 +5,6 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.IO;
@@ -518,25 +517,14 @@ namespace MediaBrowser.Api.Playback.Dash
 
         private async Task WaitForSegment(string playlist, string segment, CancellationToken cancellationToken)
         {
-            var tmpPath = playlist + ".tmp";
-
             var segmentFilename = Path.GetFileName(segment);
 
             Logger.Debug("Waiting for {0} in {1}", segmentFilename, playlist);
 
             while (true)
             {
-                FileStream fileStream;
-                try
-                {
-                    fileStream = FileSystem.GetFileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
-                }
-                catch (IOException)
-                {
-                    fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
-                }
                 // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
-                using (fileStream)
+                using (var fileStream = GetPlaylistFileStream(playlist))
                 {
                     using (var reader = new StreamReader(fileStream))
                     {

+ 30 - 3
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -3,7 +3,6 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.IO;
@@ -86,6 +85,7 @@ namespace MediaBrowser.Api.Playback.Hls
                 state.Request.StartTimeTicks = null;
             }
 
+            TranscodingJob job = null;
             var playlist = state.OutputFilePath;
 
             if (!File.Exists(playlist))
@@ -98,7 +98,7 @@ namespace MediaBrowser.Api.Playback.Hls
                         // If the playlist doesn't already exist, startup ffmpeg
                         try
                         {
-                            await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
+                            job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
                         }
                         catch
                         {
@@ -117,6 +117,12 @@ namespace MediaBrowser.Api.Playback.Hls
 
             if (isLive)
             {
+                job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
+
+                if (job != null)
+                {
+                    ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
+                }
                 return ResultFactory.GetResult(GetLivePlaylistText(playlist, state.SegmentLength), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
             }
 
@@ -135,6 +141,13 @@ namespace MediaBrowser.Api.Playback.Hls
 
             var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate);
 
+            job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
+
+            if (job != null)
+            {
+                ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
+            }
+            
             return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
         }
 
@@ -186,7 +199,7 @@ namespace MediaBrowser.Api.Playback.Hls
             while (true)
             {
                 // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
-                using (var fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
+                using (var fileStream = GetPlaylistFileStream(playlist))
                 {
                     using (var reader = new StreamReader(fileStream))
                     {
@@ -212,6 +225,20 @@ namespace MediaBrowser.Api.Playback.Hls
             }
         }
 
+        protected Stream GetPlaylistFileStream(string path)
+        {
+            var tmpPath = path + ".tmp";
+
+            try
+            {
+                return FileSystem.GetFileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
+            }
+            catch (IOException)
+            {
+                return FileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
+            }
+        }
+
         protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
         {
             var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream;

+ 31 - 7
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
@@ -128,9 +127,27 @@ namespace MediaBrowser.Api.Playback.Hls
                 }
                 else
                 {
+                    var startTranscoding = false;
+
                     var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
-                    var segmentGapRequiringTranscodingChange = 24/state.SegmentLength;
-                    if (currentTranscodingIndex == null || requestedIndex < currentTranscodingIndex.Value || (requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange)
+                    var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
+
+                    if (currentTranscodingIndex == null)
+                    {
+                        Logger.Debug("Starting transcoding because currentTranscodingIndex=null");
+                        startTranscoding = true;
+                    }
+                    else if (requestedIndex < currentTranscodingIndex.Value)
+                    {
+                        Logger.Debug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", requestedIndex, currentTranscodingIndex);
+                        startTranscoding = true;
+                    }
+                    else if ((requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange)
+                    {
+                        Logger.Debug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", (requestedIndex - currentTranscodingIndex.Value), segmentGapRequiringTranscodingChange, requestedIndex);
+                        startTranscoding = true;
+                    }
+                    if (startTranscoding)
                     {
                         // If the playlist doesn't already exist, startup ffmpeg
                         try
@@ -145,7 +162,6 @@ namespace MediaBrowser.Api.Playback.Hls
                             request.StartTimeTicks = GetSeekPositionTicks(state, requestedIndex);
 
                             job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
-                            ApiEntryPoint.Instance.OnTranscodeBeginRequest(job);
                         }
                         catch
                         {
@@ -153,7 +169,15 @@ namespace MediaBrowser.Api.Playback.Hls
                             throw;
                         }
 
-                        await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
+                        //await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
+                    }
+                    else
+                    {
+                        job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+                        if (job.TranscodingThrottler != null)
+                        {
+                            job.TranscodingThrottler.UnpauseTranscoding();
+                        }
                     }
                 }
             }
@@ -300,7 +324,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             var segmentFilename = Path.GetFileName(segmentPath);
 
-            using (var fileStream = FileSystem.GetFileStream(playlistPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
+            using (var fileStream = GetPlaylistFileStream(playlistPath))
             {
                 using (var reader = new StreamReader(fileStream))
                 {
@@ -712,7 +736,7 @@ namespace MediaBrowser.Api.Playback.Hls
                     ).Trim();
             }
 
-            return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header -sc_threshold 0 {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
+            return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header -copyts -sc_threshold 0 {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
                             inputModifier,
                             GetInputArgument(state),
                             threads,

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

@@ -3,12 +3,10 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.IO;
 using ServiceStack;
 using System;
-using System.IO;
 
 namespace MediaBrowser.Api.Playback.Hls
 {

+ 38 - 13
MediaBrowser.Api/Playback/MediaInfoService.cs

@@ -1,4 +1,6 @@
-using MediaBrowser.Controller.Devices;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
@@ -59,23 +61,27 @@ namespace MediaBrowser.Api.Playback
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IDeviceManager _deviceManager;
         private readonly ILibraryManager _libraryManager;
+        private readonly IServerConfigurationManager _config;
+        private readonly INetworkManager _networkManager;
 
-        public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager)
+        public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager, IServerConfigurationManager config, INetworkManager networkManager)
         {
             _mediaSourceManager = mediaSourceManager;
             _deviceManager = deviceManager;
             _libraryManager = libraryManager;
+            _config = config;
+            _networkManager = networkManager;
         }
 
         public async Task<object> Get(GetPlaybackInfo request)
         {
-            var result = await GetPlaybackInfo(request.Id, request.UserId).ConfigureAwait(false);
+            var result = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }).ConfigureAwait(false);
             return ToOptimizedResult(result);
         }
 
         public async Task<object> Get(GetLiveMediaInfo request)
         {
-            var result = await GetPlaybackInfo(request.Id, request.UserId).ConfigureAwait(false);
+            var result = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }).ConfigureAwait(false);
             return ToOptimizedResult(result);
         }
 
@@ -122,29 +128,32 @@ namespace MediaBrowser.Api.Playback
 
         public async Task<object> Post(GetPostedPlaybackInfo request)
         {
-            var info = await GetPlaybackInfo(request.Id, request.UserId, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false);
             var authInfo = AuthorizationContext.GetAuthorizationInfo(Request);
 
             var profile = request.DeviceProfile;
-            if (profile == null)
+
+            var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
+            if (caps != null)
             {
-                var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
-                if (caps != null)
+                if (profile == null)
                 {
                     profile = caps.DeviceProfile;
                 }
             }
 
+            var info = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false);
+
             if (profile != null)
             {
                 var mediaSourceId = request.MediaSourceId;
+
                 SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex);
             }
 
             return ToOptimizedResult(info);
         }
 
-        private async Task<PlaybackInfoResponse> GetPlaybackInfo(string id, string userId, string mediaSourceId = null, string liveStreamId = null)
+        private async Task<PlaybackInfoResponse> GetPlaybackInfo(string id, string userId, string[] supportedLiveMediaTypes, string mediaSourceId = null, string liveStreamId = null)
         {
             var result = new PlaybackInfoResponse();
 
@@ -153,7 +162,7 @@ namespace MediaBrowser.Api.Playback
                 IEnumerable<MediaSourceInfo> mediaSources;
                 try
                 {
-                    mediaSources = await _mediaSourceManager.GetPlayackMediaSources(id, userId, true, CancellationToken.None).ConfigureAwait(false);
+                    mediaSources = await _mediaSourceManager.GetPlayackMediaSources(id, userId, true, supportedLiveMediaTypes, CancellationToken.None).ConfigureAwait(false);
                 }
                 catch (PlaybackException ex)
                 {
@@ -223,7 +232,7 @@ namespace MediaBrowser.Api.Playback
             int? subtitleStreamIndex,
             string playSessionId)
         {
-            var streamBuilder = new StreamBuilder();
+            var streamBuilder = new StreamBuilder(Logger);
 
             var options = new VideoOptions
             {
@@ -231,8 +240,7 @@ namespace MediaBrowser.Api.Playback
                 Context = EncodingContext.Streaming,
                 DeviceId = auth.DeviceId,
                 ItemId = item.Id.ToString("N"),
-                Profile = profile,
-                MaxBitrate = maxBitrate
+                Profile = profile
             };
 
             if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
@@ -248,6 +256,7 @@ namespace MediaBrowser.Api.Playback
 
                 // Dummy this up to fool StreamBuilder
                 mediaSource.SupportsDirectStream = true;
+                options.MaxBitrate = 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) ?
@@ -270,6 +279,8 @@ 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) :
@@ -288,6 +299,8 @@ 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) :
@@ -309,6 +322,18 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
+        private int? GetMaxBitrate(int? clientMaxBitrate)
+        {
+            var maxBitrate = clientMaxBitrate;
+
+            if (_config.Configuration.RemoteClientBitrateLimit > 0 && !_networkManager.IsInLocalNetwork(Request.RemoteIp))
+            {
+                maxBitrate = Math.Min(maxBitrate ?? _config.Configuration.RemoteClientBitrateLimit, _config.Configuration.RemoteClientBitrateLimit);
+            }
+
+            return maxBitrate;
+        }
+
         private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
         {
             var profiles = info.GetSubtitleProfiles(false, "-", accessToken);

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

@@ -63,6 +63,13 @@ namespace MediaBrowser.Api.Playback.Progressive
                 new ProgressiveFileCopier(_fileSystem, _job)
                     .StreamFile(Path, responseStream);
             }
+            catch (IOException)
+            {
+                // These error are always the same so don't dump the whole stack trace
+                Logger.Error("Error streaming media. The client has most likely disconnected or transcoding has failed.");
+
+                throw;
+            }
             catch (Exception ex)
             {
                 Logger.ErrorException("Error streaming media. The client has most likely disconnected or transcoding has failed.", ex);

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

@@ -5,7 +5,6 @@ using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.IO;
 using ServiceStack;

+ 1 - 1
MediaBrowser.Api/Playback/TranscodingThrottler.cs

@@ -70,7 +70,7 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
-        private void UnpauseTranscoding()
+        public void UnpauseTranscoding()
         {
             if (_isPaused)
             {

+ 2 - 2
MediaBrowser.Api/Session/SessionsService.cs

@@ -383,12 +383,12 @@ namespace MediaBrowser.Api.Session
 
                 if (!user.Policy.EnableRemoteControlOfOtherUsers)
                 {
-                    result = result.Where(i => i.ContainsUser(request.ControllableByUserId.Value));
+                    result = result.Where(i => !i.UserId.HasValue || i.ContainsUser(request.ControllableByUserId.Value));
                 }
 
                 if (!user.Policy.EnableSharedDeviceControl)
                 {
-                    result = result.Where(i => !i.UserId.HasValue);
+                    result = result.Where(i => i.UserId.HasValue);
                 }
 
                 result = result.Where(i =>

+ 1 - 1
MediaBrowser.Api/SimilarItemsHelper.cs

@@ -170,7 +170,7 @@ namespace MediaBrowser.Api
             points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 3);
 
             var item2PeopleNames = item2.People.Select(i => i.Name)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
 
             points += item1.People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i =>

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

@@ -136,11 +136,11 @@ namespace MediaBrowser.Api.Subtitles
             _providerManager = providerManager;
         }
 
-        public object Get(GetSubtitlePlaylist request)
+        public async Task<object> Get(GetSubtitlePlaylist request)
         {
             var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
 
-            var mediaSource = _mediaSourceManager.GetStaticMediaSource(item, request.MediaSourceId, false);
+            var mediaSource = await _mediaSourceManager.GetMediaSource(item, request.MediaSourceId, false).ConfigureAwait(false);
 
             var builder = new StringBuilder();
 

+ 5 - 6
MediaBrowser.Api/Sync/SyncService.cs

@@ -248,6 +248,9 @@ namespace MediaBrowser.Api.Sync
             result.Targets = _syncManager.GetSyncTargets(request.UserId)
                 .ToList();
 
+            var auth = AuthorizationContext.GetAuthorizationInfo(Request);
+            var authenticatedUser = _userManager.GetUserById(auth.UserId);
+
             if (!string.IsNullOrWhiteSpace(request.TargetId))
             {
                 result.Targets = result.Targets
@@ -255,11 +258,11 @@ namespace MediaBrowser.Api.Sync
                     .ToList();
 
                 result.QualityOptions = _syncManager
-                    .GetQualityOptions(request.TargetId)
+                    .GetQualityOptions(request.TargetId, authenticatedUser)
                     .ToList();
 
                 result.ProfileOptions = _syncManager
-                    .GetProfileOptions(request.TargetId)
+                    .GetProfileOptions(request.TargetId, authenticatedUser)
                     .ToList();
             }
 
@@ -277,10 +280,6 @@ namespace MediaBrowser.Api.Sync
                     }
                 };
 
-                var auth = AuthorizationContext.GetAuthorizationInfo(Request);
-
-                var authenticatedUser = _userManager.GetUserById(auth.UserId);
-
                 var items = request.ItemIds.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                     .Select(_libraryManager.GetItemById)
                     .Where(i => i != null);

+ 2 - 2
MediaBrowser.Api/UserLibrary/ArtistsService.cs

@@ -132,7 +132,7 @@ namespace MediaBrowser.Api.UserLibrary
                     .Where(i => !i.IsFolder)
                     .OfType<IHasAlbumArtist>()
                     .SelectMany(i => i.AlbumArtists)
-                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .DistinctNames()
                     .Select(name =>
                     {
                         try
@@ -152,7 +152,7 @@ namespace MediaBrowser.Api.UserLibrary
                 .Where(i => !i.IsFolder)
                 .OfType<IHasArtist>()
                 .SelectMany(i => i.AllArtists)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .Select(name =>
                 {
                     try

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

@@ -142,7 +142,7 @@ namespace MediaBrowser.Api.UserLibrary
             }
 
             IEnumerable<Tuple<TItemType, List<BaseItem>>> tuples;
-            if (dtoOptions.Fields.Contains(ItemFields.ItemCounts) || true)
+            if (dtoOptions.Fields.Contains(ItemFields.ItemCounts))
             {
                 tuples = ibnItems.Select(i => new Tuple<TItemType, List<BaseItem>>(i, i.GetTaggedItems(libraryItems).ToList()));
             }
@@ -177,7 +177,6 @@ namespace MediaBrowser.Api.UserLibrary
                 return true;
             }
 
-            return true;
             return options.Fields.Contains(ItemFields.ItemCounts);
         }
 

+ 1 - 1
MediaBrowser.Api/UserLibrary/GameGenresService.cs

@@ -105,7 +105,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             return itemsList
                 .SelectMany(i => i.Genres)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .Select(name => LibraryManager.GetGameGenre(name));
         }
     }

+ 1 - 1
MediaBrowser.Api/UserLibrary/GenresService.cs

@@ -108,7 +108,7 @@ namespace MediaBrowser.Api.UserLibrary
         {
             return items
                 .SelectMany(i => i.Genres)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .Select(name =>
                 {
                     try

+ 1 - 1
MediaBrowser.Api/UserLibrary/MusicGenresService.cs

@@ -105,7 +105,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             return itemsList
                 .SelectMany(i => i.Genres)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .Select(name => LibraryManager.GetMusicGenre(name));
         }
     }

+ 1 - 1
MediaBrowser.Api/UserLibrary/PersonsService.cs

@@ -127,7 +127,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             return allPeople
                 .Select(i => i.Name)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
 
                 .Select(name =>
                 {

+ 45 - 3
MediaBrowser.Api/UserLibrary/PlaystateService.cs

@@ -114,6 +114,15 @@ namespace MediaBrowser.Api.UserLibrary
 
         [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
         public int? SubtitleStreamIndex { get; set; }
+
+        [ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public PlayMethod PlayMethod { get; set; }
+
+        [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string LiveStreamId { get; set; }
+
+        [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string PlaySessionId { get; set; }
     }
 
     /// <summary>
@@ -160,6 +169,15 @@ namespace MediaBrowser.Api.UserLibrary
 
         [ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
         public int? VolumeLevel { get; set; }
+
+        [ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public PlayMethod PlayMethod { get; set; }
+
+        [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string LiveStreamId { get; set; }
+
+        [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string PlaySessionId { get; set; }
     }
 
     /// <summary>
@@ -191,6 +209,12 @@ namespace MediaBrowser.Api.UserLibrary
         /// <value>The position ticks.</value>
         [ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")]
         public long? PositionTicks { get; set; }
+
+        [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string LiveStreamId { get; set; }
+
+        [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string PlaySessionId { get; set; }
     }
 
     [Authenticated]
@@ -260,7 +284,10 @@ namespace MediaBrowser.Api.UserLibrary
                 QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(),
                 MediaSourceId = request.MediaSourceId,
                 AudioStreamIndex = request.AudioStreamIndex,
-                SubtitleStreamIndex = request.SubtitleStreamIndex
+                SubtitleStreamIndex = request.SubtitleStreamIndex,
+                PlayMethod = request.PlayMethod,
+                PlaySessionId = request.PlaySessionId,
+                LiveStreamId = request.LiveStreamId
             });
         }
 
@@ -288,12 +315,20 @@ namespace MediaBrowser.Api.UserLibrary
                 MediaSourceId = request.MediaSourceId,
                 AudioStreamIndex = request.AudioStreamIndex,
                 SubtitleStreamIndex = request.SubtitleStreamIndex,
-                VolumeLevel = request.VolumeLevel
+                VolumeLevel = request.VolumeLevel,
+                PlayMethod = request.PlayMethod,
+                PlaySessionId = request.PlaySessionId,
+                LiveStreamId = request.LiveStreamId
             });
         }
 
         public void Post(ReportPlaybackProgress request)
         {
+            if (!string.IsNullOrWhiteSpace(request.PlaySessionId))
+            {
+                ApiEntryPoint.Instance.PingTranscodingJob(request.PlaySessionId);
+            }
+
             request.SessionId = GetSession().Result.Id;
 
             var task = _sessionManager.OnPlaybackProgress(request);
@@ -311,12 +346,19 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 ItemId = request.Id,
                 PositionTicks = request.PositionTicks,
-                MediaSourceId = request.MediaSourceId
+                MediaSourceId = request.MediaSourceId,
+                PlaySessionId = request.PlaySessionId,
+                LiveStreamId = request.LiveStreamId
             });
         }
 
         public void Post(ReportPlaybackStopped request)
         {
+            if (!string.IsNullOrWhiteSpace(request.PlaySessionId))
+            {
+                ApiEntryPoint.Instance.KillTranscodingJobs(AuthorizationContext.GetAuthorizationInfo(Request).DeviceId, request.PlaySessionId, s => true);
+            }
+
             request.SessionId = GetSession().Result.Id;
 
             var task = _sessionManager.OnPlaybackStopped(request);

+ 1 - 1
MediaBrowser.Api/UserLibrary/StudiosService.cs

@@ -109,7 +109,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             return itemsList
                 .SelectMany(i => i.Studios)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .Select(name => LibraryManager.GetStudio(name));
         }
     }

+ 4 - 9
MediaBrowser.Common.Implementations/BaseApplicationHost.cs

@@ -101,12 +101,6 @@ namespace MediaBrowser.Common.Implementations
         /// <value>The failed assemblies.</value>
         public List<string> FailedAssemblies { get; protected set; }
 
-        /// <summary>
-        /// Gets all types within all running assemblies
-        /// </summary>
-        /// <value>All types.</value>
-        public Type[] AllTypes { get; protected set; }
-
         /// <summary>
         /// Gets all concrete types.
         /// </summary>
@@ -438,9 +432,10 @@ namespace MediaBrowser.Common.Implementations
                 Logger.Info("Loading {0}", assembly.FullName);
             }
 
-            AllTypes = assemblies.SelectMany(GetTypes).ToArray();
-
-            AllConcreteTypes = AllTypes.Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType).ToArray();
+            AllConcreteTypes = assemblies
+                .SelectMany(GetTypes)
+                .Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType)
+                .ToArray();
         }
 
         /// <summary>

+ 8 - 4
MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs

@@ -172,11 +172,11 @@ namespace MediaBrowser.Common.Implementations.Networking
                 Uri uri;
                 if (Uri.TryCreate(endpoint, UriKind.RelativeOrAbsolute, out uri))
                 {
-                    var host = uri.DnsSafeHost;
-                    Logger.Debug("Resolving host {0}", host);
-
                     try
                     {
+                        var host = uri.DnsSafeHost;
+                        Logger.Debug("Resolving host {0}", host);
+
                         address = GetIpAddresses(host).FirstOrDefault();
 
                         if (address != null)
@@ -186,9 +186,13 @@ namespace MediaBrowser.Common.Implementations.Networking
                             return IsInLocalNetworkInternal(address.ToString(), false);
                         }
                     }
+                    catch (InvalidOperationException)
+                    {
+                        // Can happen with reverse proxy or IIS url rewriting
+                    }
                     catch (Exception ex)
                     {
-                        Logger.ErrorException("Error resovling hostname {0}", ex, host);
+                        Logger.ErrorException("Error resovling hostname", ex);
                     }
                 }
             }

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

@@ -121,12 +121,12 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
             {
                 if (_lastExecutionResult == null)
                 {
+                    var path = GetHistoryFilePath();
+
                     lock (_lastExecutionResultSyncLock)
                     {
                         if (_lastExecutionResult == null)
                         {
-                            var path = GetHistoryFilePath();
-
                             try
                             {
                                 return JsonSerializer.DeserializeFromFile<TaskResult>(path);
@@ -152,6 +152,14 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
             private set
             {
                 _lastExecutionResult = value;
+
+                var path = GetHistoryFilePath();
+                Directory.CreateDirectory(Path.GetDirectoryName(path));
+
+                lock (_lastExecutionResultSyncLock)
+                {
+                    JsonSerializer.SerializeToFile(value, path);
+                }
             }
         }
 
@@ -582,11 +590,6 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
                 result.LongErrorMessage = ex.StackTrace;
             }
 
-            var path = GetHistoryFilePath();
-            Directory.CreateDirectory(Path.GetDirectoryName(path));
-
-            JsonSerializer.SerializeToFile(result, path);
-
             LastExecutionResult = result;
 
             ((TaskManager)TaskManager).OnTaskCompleted(this, result);

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

@@ -5,7 +5,6 @@ using System;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Channels
 {
@@ -15,19 +14,9 @@ namespace MediaBrowser.Controller.Channels
 
         public override bool IsVisible(User user)
         {
-            if (user.Policy.BlockedChannels != null)
+            if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
             {
-                if (user.Policy.BlockedChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
-                {
-                    return false;
-                }
-            }
-            else
-            {
-                if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
-                {
-                    return false;
-                }
+                return false;
             }
             
             return base.IsVisible(user);

+ 7 - 1
MediaBrowser.Controller/Channels/ChannelAudioItem.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Model.Channels;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
@@ -100,5 +101,10 @@ namespace MediaBrowser.Controller.Channels
         {
             return false;
         }
+
+        public override bool IsVisibleStandalone(User user)
+        {
+            return IsVisibleStandaloneInternal(user, false) && ChannelVideoItem.IsChannelVisible(this, user);
+        }
     }
 }

+ 5 - 0
MediaBrowser.Controller/Channels/ChannelFolderItem.cs

@@ -80,5 +80,10 @@ namespace MediaBrowser.Controller.Channels
         {
             return false;
         }
+
+        public override bool IsVisibleStandalone(User user)
+        {
+            return IsVisibleStandaloneInternal(user, false) && ChannelVideoItem.IsChannelVisible(this, user);
+        }
     }
 }

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

@@ -130,5 +130,17 @@ namespace MediaBrowser.Controller.Channels
         {
             return false;
         }
+
+        public override bool IsVisibleStandalone(User user)
+        {
+            return IsVisibleStandaloneInternal(user, false) && IsChannelVisible(this, user);
+        }
+
+        internal static bool IsChannelVisible(IChannelItem item, User user)
+        {
+            var channel = ChannelManager.GetChannel(item.ChannelId);
+
+            return channel.IsVisible(user);
+        }
     }
 }

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

@@ -13,6 +13,12 @@ namespace MediaBrowser.Controller.Drawing
     /// </summary>
     public interface IImageProcessor
     {
+        /// <summary>
+        /// Gets the supported input formats.
+        /// </summary>
+        /// <value>The supported input formats.</value>
+        string[] SupportedInputFormats { get; }
+        
         /// <summary>
         /// Gets the image enhancers.
         /// </summary>
@@ -93,5 +99,11 @@ namespace MediaBrowser.Controller.Drawing
         /// </summary>
         /// <returns>ImageOutputFormat[].</returns>
         ImageFormat[] GetSupportedImageOutputFormats();
+
+        /// <summary>
+        /// Creates the image collage.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        void CreateImageCollage(ImageCollageOptions options);
     }
 }

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

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

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

@@ -35,6 +35,14 @@ namespace MediaBrowser.Controller.Dto
         /// <returns>Task{BaseItemDto}.</returns>
         BaseItemDto GetBaseItemDto(BaseItem item, List<ItemFields> fields, User user = null, BaseItem owner = null);
 
+        /// <summary>
+        /// Fills the synchronize information.
+        /// </summary>
+        /// <param name="dtos">The dtos.</param>
+        /// <param name="options">The options.</param>
+        /// <param name="user">The user.</param>
+        void FillSyncInfo(IEnumerable<IHasSyncInfo> dtos, DtoOptions options, User user);
+
         /// <summary>
         /// Gets the base item dto.
         /// </summary>

+ 3 - 4
MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs

@@ -1,6 +1,5 @@
-using System;
+using MediaBrowser.Controller.Library;
 using System.Collections.Generic;
-using System.Linq;
 
 namespace MediaBrowser.Controller.Entities.Audio
 {
@@ -20,11 +19,11 @@ namespace MediaBrowser.Controller.Entities.Audio
     {
         public static bool HasArtist(this IHasArtist hasArtist, string artist)
         {
-            return hasArtist.Artists.Contains(artist, StringComparer.OrdinalIgnoreCase);
+            return NameExtensions.EqualsAny(hasArtist.Artists, artist);
         }
         public static bool HasAnyArtist(this IHasArtist hasArtist, string artist)
         {
-            return hasArtist.AllArtists.Contains(artist, StringComparer.OrdinalIgnoreCase);
+            return NameExtensions.EqualsAny(hasArtist.AllArtists, artist);
         }
     }
 }

+ 60 - 36
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Extensions;
+using System.Globalization;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Collections;
@@ -44,7 +45,7 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// The supported image extensions
         /// </summary>
-        public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".tbn" };
+        public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg" };
 
         public static readonly List<string> SupportedImageExtensionsList = SupportedImageExtensions.ToList();
 
@@ -1143,6 +1144,11 @@ namespace MediaBrowser.Controller.Entities
         }
 
         public virtual bool IsVisibleStandalone(User user)
+        {
+            return IsVisibleStandaloneInternal(user, true);
+        }
+
+        protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
         {
             if (!IsVisible(user))
             {
@@ -1154,7 +1160,23 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
 
-            // TODO: Need some work here, e.g. is in user library, for channels, can user access channel, etc.
+            if (checkFolders)
+            {
+                var topParent = Parents.LastOrDefault() ?? this;
+
+                if (string.IsNullOrWhiteSpace(topParent.Path))
+                {
+                    return true;
+                }
+
+                var userCollectionFolders = user.RootFolder.GetChildren(user, true).Select(i => i.Id).ToList();
+                var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id);
+
+                if (!itemCollectionFolders.Any(userCollectionFolders.Contains))
+                {
+                    return false;
+                }
+            }
 
             return true;
         }
@@ -1219,18 +1241,6 @@ namespace MediaBrowser.Controller.Entities
 
         private BaseItem FindLinkedChild(LinkedChild info)
         {
-            if (!string.IsNullOrWhiteSpace(info.ItemName))
-            {
-                if (string.Equals(info.ItemType, "musicgenre", StringComparison.OrdinalIgnoreCase))
-                {
-                    return LibraryManager.GetMusicGenre(info.ItemName);
-                }
-                if (string.Equals(info.ItemType, "musicartist", StringComparison.OrdinalIgnoreCase))
-                {
-                    return LibraryManager.GetArtist(info.ItemName);
-                }
-            }
-
             if (!string.IsNullOrEmpty(info.Path))
             {
                 var itemByPath = LibraryManager.RootFolder.FindByPath(info.Path);
@@ -1243,23 +1253,6 @@ namespace MediaBrowser.Controller.Entities
                 return itemByPath;
             }
 
-            if (!string.IsNullOrWhiteSpace(info.ItemName) && !string.IsNullOrWhiteSpace(info.ItemType))
-            {
-                return LibraryManager.RootFolder.GetRecursiveChildren(i =>
-                {
-                    if (string.Equals(i.Name, info.ItemName, StringComparison.OrdinalIgnoreCase))
-                    {
-                        if (string.Equals(i.GetType().Name, info.ItemType, StringComparison.OrdinalIgnoreCase))
-                        {
-                            return true;
-                        }
-                    }
-
-                    return false;
-
-                }).FirstOrDefault();
-            }
-
             return null;
         }
 
@@ -1540,7 +1533,7 @@ namespace MediaBrowser.Controller.Entities
             }
 
             // Remove it from the item
-            ImageInfos.Remove(info);
+            RemoveImage(info);
 
             // Delete the source file
             var currentFile = new FileInfo(info.Path);
@@ -1559,6 +1552,11 @@ namespace MediaBrowser.Controller.Entities
             return UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
         }
 
+        public void RemoveImage(ItemImageInfo image)
+        {
+            ImageInfos.Remove(image);
+        }
+
         public virtual Task UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken)
         {
             return LibraryManager.UpdateItem(this, updateReason, cancellationToken);
@@ -1651,7 +1649,7 @@ namespace MediaBrowser.Controller.Entities
 
         public bool AddImages(ImageType imageType, IEnumerable<FileInfo> images)
         {
-            return AddImages(imageType, images.Cast<FileSystemInfo>());
+            return AddImages(imageType, images.Cast<FileSystemInfo>().ToList());
         }
 
         /// <summary>
@@ -1661,7 +1659,7 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="images">The images.</param>
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
         /// <exception cref="System.ArgumentException">Cannot call AddImages with chapter images</exception>
-        public bool AddImages(ImageType imageType, IEnumerable<FileSystemInfo> images)
+        public bool AddImages(ImageType imageType, List<FileSystemInfo> images)
         {
             if (imageType == ImageType.Chapter)
             {
@@ -1672,6 +1670,7 @@ namespace MediaBrowser.Controller.Entities
                 .ToList();
 
             var newImageList = new List<FileSystemInfo>();
+            var imageAdded = false;
 
             foreach (var newImage in images)
             {
@@ -1686,14 +1685,26 @@ namespace MediaBrowser.Controller.Entities
                 if (existing == null)
                 {
                     newImageList.Add(newImage);
+                    imageAdded = true;
                 }
                 else
                 {
                     existing.DateModified = FileSystem.GetLastWriteTimeUtc(newImage);
-                    existing.Length = ((FileInfo) newImage).Length;
+                    existing.Length = ((FileInfo)newImage).Length;
                 }
             }
 
+            if (imageAdded || images.Count != existingImages.Count)
+            {
+                var newImagePaths = images.Select(i => i.FullName).ToList();
+
+                var deleted = existingImages
+                    .Where(i => !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !File.Exists(i.Path))
+                    .ToList();
+
+                ImageInfos = ImageInfos.Except(deleted).ToList();
+            }
+
             ImageInfos.AddRange(newImageList.Select(i => GetImageInfo(i, imageType)));
 
             return newImageList.Count > 0;
@@ -1882,5 +1893,18 @@ namespace MediaBrowser.Controller.Entities
 
             return video.RefreshMetadata(newOptions, cancellationToken);
         }
+
+        public string GetEtag()
+        {
+            return string.Join("|", GetEtagValues().ToArray()).GetMD5().ToString("N");
+        }
+
+        protected virtual List<string> GetEtagValues()
+        {
+            return new List<string>
+            {
+                DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
+            };
+        }
     }
 }

+ 4 - 16
MediaBrowser.Controller/Entities/Folder.cs

@@ -334,22 +334,9 @@ namespace MediaBrowser.Controller.Entities
         {
             if (this is ICollectionFolder && !(this is BasePluginFolder))
             {
-                if (user.Policy.BlockedMediaFolders != null)
+                if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
                 {
-                    if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) ||
-
-                        // Backwards compatibility
-                        user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
-                    {
-                        return false;
-                    }
-                }
-                else
-                {
-                    if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
-                    {
-                        return false;
-                    }
+                    return false;
                 }
             }
 
@@ -1004,8 +991,9 @@ namespace MediaBrowser.Controller.Entities
             }
 
             var locations = user.RootFolder
-                .GetChildren(user, true)
+                .Children
                 .OfType<CollectionFolder>()
+                .Where(i => i.IsVisible(user))
                 .SelectMany(i => i.PhysicalLocations)
                 .ToList();
 

+ 7 - 1
MediaBrowser.Controller/Entities/IHasImages.cs

@@ -141,7 +141,7 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="imageType">Type of the image.</param>
         /// <param name="images">The images.</param>
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
-        bool AddImages(ImageType imageType, IEnumerable<FileSystemInfo> images);
+        bool AddImages(ImageType imageType, List<FileSystemInfo> images);
 
         /// <summary>
         /// Determines whether [is save local metadata enabled].
@@ -190,6 +190,12 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         /// <returns><c>true</c> if [is internet metadata enabled]; otherwise, <c>false</c>.</returns>
         bool IsInternetMetadataEnabled();
+
+        /// <summary>
+        /// Removes the image.
+        /// </summary>
+        /// <param name="image">The image.</param>
+        void RemoveImage(ItemImageInfo image);
     }
 
     public static class HasImagesExtensions

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

@@ -9,9 +9,6 @@ namespace MediaBrowser.Controller.Entities
         public string Path { get; set; }
         public LinkedChildType Type { get; set; }
 
-        public string ItemName { get; set; }
-        public string ItemType { get; set; }
-
         [IgnoreDataMember]
         public string Id { get; set; }
 

+ 9 - 9
MediaBrowser.Controller/Entities/Movies/BoxSet.cs

@@ -175,19 +175,19 @@ namespace MediaBrowser.Controller.Entities.Movies
 
         public override bool IsVisible(User user)
         {
-            if (base.IsVisible(user))
-            {
-                var userId = user.Id.ToString("N");
-
-                // Need to check Count > 0 for boxsets created prior to the introduction of Shares
-                if (Shares.Count > 0 && !Shares.Any(i => string.Equals(userId, i.UserId, StringComparison.OrdinalIgnoreCase)))
-                {
-                    //return false;
-                }
+            var userId = user.Id.ToString("N");
 
+            // Need to check Count > 0 for boxsets created prior to the introduction of Shares
+            if (Shares.Count > 0 && Shares.Any(i => string.Equals(userId, i.UserId, StringComparison.OrdinalIgnoreCase)))
+            {
                 return true;
             }
 
+            if (base.IsVisible(user))
+            {
+                return GetChildren(user, true).Any();
+            }
+
             return false;
         }
     }

+ 33 - 3
MediaBrowser.Controller/Entities/PhotoAlbum.cs

@@ -1,11 +1,15 @@
-using MediaBrowser.Model.Configuration;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Users;
+using System;
 using System.Linq;
 using System.Runtime.Serialization;
-using MediaBrowser.Model.Users;
+using System.Threading;
+using System.Threading.Tasks;
 
 namespace MediaBrowser.Controller.Entities
 {
-    public class PhotoAlbum : Folder
+    public class PhotoAlbum : Folder, IMetadataContainer
     {
         public override bool SupportsLocalMetadata
         {
@@ -28,5 +32,31 @@ namespace MediaBrowser.Controller.Entities
         {
             return config.BlockUnratedItems.Contains(UnratedItem.Other);
         }
+
+        public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var items = GetRecursiveChildren().ToList();
+
+            var totalItems = items.Count;
+            var numComplete = 0;
+
+            // Refresh songs
+            foreach (var item in items)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+
+                await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= totalItems;
+                progress.Report(percent * 100);
+            }
+
+            // Refresh current item
+            await RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+
+            progress.Report(100);
+        }
     }
 }

+ 62 - 23
MediaBrowser.Controller/Entities/UserViewBuilder.cs

@@ -50,6 +50,16 @@ namespace MediaBrowser.Controller.Entities
         {
             var user = query.User;
 
+            if (query.IncludeItemTypes != null &&
+                query.IncludeItemTypes.Length == 1 &&
+                string.Equals(query.IncludeItemTypes[0], "Playlist", StringComparison.OrdinalIgnoreCase))
+            {
+                if (!string.Equals(viewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+                {
+                    return await FindPlaylists(queryParent, user, query).ConfigureAwait(false);
+                }
+            }
+
             switch (viewType)
             {
                 case CollectionType.Channels:
@@ -107,9 +117,7 @@ namespace MediaBrowser.Controller.Entities
 
                 case CollectionType.LiveTv:
                     {
-                        var result = await GetLiveTvFolders(user).ConfigureAwait(false);
-
-                        return GetResult(result, queryParent, query);
+                        return await GetLiveTvView(queryParent, user, query).ConfigureAwait(false);
                     }
 
                 case CollectionType.Books:
@@ -205,6 +213,9 @@ namespace MediaBrowser.Controller.Entities
                 case SpecialFolder.MusicLatest:
                     return GetMusicLatest(queryParent, user, query);
 
+                case SpecialFolder.MusicPlaylists:
+                    return await GetMusicPlaylists(queryParent, user, query).ConfigureAwait(false);
+
                 case SpecialFolder.MusicAlbums:
                     return GetMusicAlbums(queryParent, user, query);
 
@@ -240,6 +251,16 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        private async Task<QueryResult<BaseItem>> FindPlaylists(Folder parent, User user, InternalItemsQuery query)
+        {
+            var collectionFolders = user.RootFolder.GetChildren(user, true).Select(i => i.Id).ToList();
+
+            var list = _playlistManager.GetPlaylists(user.Id.ToString("N"))
+                .Where(i => i.GetChildren(user, true).Any(media => _libraryManager.GetCollectionFolders(media).Select(c => c.Id).Any(collectionFolders.Contains)));
+
+            return GetResult(list, parent, query);
+        }
+
         private int GetSpecialItemsLimit()
         {
             return 50;
@@ -257,12 +278,13 @@ namespace MediaBrowser.Controller.Entities
             var list = new List<BaseItem>();
 
             list.Add(await GetUserView(SpecialFolder.MusicLatest, user, "0", parent).ConfigureAwait(false));
-            list.Add(await GetUserView(SpecialFolder.MusicAlbums, user, "1", parent).ConfigureAwait(false));
-            list.Add(await GetUserView(SpecialFolder.MusicAlbumArtists, user, "2", parent).ConfigureAwait(false));
-            list.Add(await GetUserView(SpecialFolder.MusicArtists, user, "3", parent).ConfigureAwait(false));
-            list.Add(await GetUserView(SpecialFolder.MusicSongs, user, "4", parent).ConfigureAwait(false));
-            list.Add(await GetUserView(SpecialFolder.MusicGenres, user, "5", parent).ConfigureAwait(false));
-            list.Add(await GetUserView(SpecialFolder.MusicFavorites, user, "6", parent).ConfigureAwait(false));
+            list.Add(await GetUserView(SpecialFolder.MusicPlaylists, user, "1", parent).ConfigureAwait(false));
+            list.Add(await GetUserView(SpecialFolder.MusicAlbums, user, "2", parent).ConfigureAwait(false));
+            list.Add(await GetUserView(SpecialFolder.MusicAlbumArtists, user, "3", parent).ConfigureAwait(false));
+            //list.Add(await GetUserView(SpecialFolder.MusicArtists, user, "4", parent).ConfigureAwait(false));
+            list.Add(await GetUserView(SpecialFolder.MusicSongs, user, "5", parent).ConfigureAwait(false));
+            list.Add(await GetUserView(SpecialFolder.MusicGenres, user, "6", parent).ConfigureAwait(false));
+            list.Add(await GetUserView(SpecialFolder.MusicFavorites, user, "7", parent).ConfigureAwait(false));
 
             return GetResult(list, parent, query);
         }
@@ -283,7 +305,7 @@ namespace MediaBrowser.Controller.Entities
             var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Music, CollectionType.MusicVideos })
                 .Where(i => !i.IsFolder)
                 .SelectMany(i => i.Genres)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .Select(i =>
                 {
                     try
@@ -313,7 +335,7 @@ namespace MediaBrowser.Controller.Entities
                 .Where(i => i.Genres.Contains(displayParent.Name, StringComparer.OrdinalIgnoreCase))
                 .OfType<IHasAlbumArtist>()
                 .SelectMany(i => i.AlbumArtists)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .Select(i =>
                 {
                     try
@@ -337,7 +359,7 @@ namespace MediaBrowser.Controller.Entities
                 .Where(i => !i.IsFolder)
                 .OfType<IHasAlbumArtist>()
                 .SelectMany(i => i.AlbumArtists)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .Select(i =>
                 {
                     try
@@ -361,7 +383,7 @@ namespace MediaBrowser.Controller.Entities
                 .Where(i => !i.IsFolder)
                 .OfType<IHasArtist>()
                 .SelectMany(i => i.Artists)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .Select(i =>
                 {
                     try
@@ -385,7 +407,7 @@ namespace MediaBrowser.Controller.Entities
                 .Where(i => !i.IsFolder)
                 .OfType<IHasAlbumArtist>()
                 .SelectMany(i => i.AlbumArtists)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .Select(i =>
                 {
                     try
@@ -403,6 +425,14 @@ namespace MediaBrowser.Controller.Entities
             return GetResult(artists, parent, query);
         }
 
+        private Task<QueryResult<BaseItem>> GetMusicPlaylists(Folder parent, User user, InternalItemsQuery query)
+        {
+            query.IncludeItemTypes = new[] { "Playlist" };
+            query.Recursive = true;
+
+            return parent.GetItems(query);
+        }
+
         private QueryResult<BaseItem> GetMusicAlbums(Folder parent, User user, InternalItemsQuery query)
         {
             var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Music, CollectionType.MusicVideos }, i => (i is MusicAlbum) && FilterItem(i, query));
@@ -552,7 +582,7 @@ namespace MediaBrowser.Controller.Entities
             var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Movies, CollectionType.BoxSets, string.Empty })
                 .Where(i => i is Movie)
                 .SelectMany(i => i.Genres)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .Select(i =>
                 {
                     try
@@ -724,7 +754,7 @@ namespace MediaBrowser.Controller.Entities
             var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.TvShows, string.Empty })
                 .OfType<Series>()
                 .SelectMany(i => i.Genres)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .Select(i =>
                 {
                     try
@@ -776,7 +806,7 @@ namespace MediaBrowser.Controller.Entities
             var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Games })
                 .OfType<Game>()
                 .SelectMany(i => i.Genres)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .DistinctNames()
                 .Select(i =>
                 {
                     try
@@ -1749,17 +1779,26 @@ namespace MediaBrowser.Controller.Entities
             return parent.GetRecursiveChildren(user, filter);
         }
 
-        private async Task<IEnumerable<BaseItem>> GetLiveTvFolders(User user)
+        private async Task<QueryResult<BaseItem>> GetLiveTvView(Folder queryParent, User user, InternalItemsQuery query)
         {
-            var list = new List<BaseItem>();
+            if (query.Recursive)
+            {
+                return await _liveTvManager.GetInternalRecordings(new RecordingQuery
+                {
+                    IsInProgress = false,
+                    Status = RecordingStatus.Completed,
+                    UserId = user.Id.ToString("N")
 
-            var parent = user.RootFolder;
+                }, CancellationToken.None).ConfigureAwait(false);
+            }
+
+            var list = new List<BaseItem>();
 
             //list.Add(await GetUserSubView(SpecialFolder.LiveTvNowPlaying, user, "0", parent).ConfigureAwait(false));
-            list.Add(await GetUserView(SpecialFolder.LiveTvChannels, user, string.Empty, parent).ConfigureAwait(false));
-            list.Add(await GetUserView(SpecialFolder.LiveTvRecordingGroups, user, string.Empty, parent).ConfigureAwait(false));
+            list.Add(await GetUserView(SpecialFolder.LiveTvChannels, user, string.Empty, user.RootFolder).ConfigureAwait(false));
+            list.Add(await GetUserView(SpecialFolder.LiveTvRecordingGroups, user, string.Empty, user.RootFolder).ConfigureAwait(false));
 
-            return list;
+            return GetResult(list, queryParent, query);
         }
 
         private async Task<UserView> GetUserView(string name, string type, User user, string sortName, BaseItem parent)

+ 1 - 13
MediaBrowser.Server.Implementations/HttpServer/ThrottledStream.cs → MediaBrowser.Controller/IO/ThrottledStream.cs

@@ -3,7 +3,7 @@ using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Server.Implementations.HttpServer
+namespace MediaBrowser.Controller.IO
 {
     /// <summary>
     /// Class for streaming data with throttling support.
@@ -15,8 +15,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
         /// </summary>
         public const long Infinite = 0;
 
-        public Func<long, long, long> ThrottleCallback { get; set; }
-        
         #region Private members
         /// <summary>
         /// The base stream.
@@ -293,16 +291,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
                 return false;
             }
 
-            if (ThrottleCallback != null)
-            {
-                var val = ThrottleCallback(_maximumBytesPerSecond, _bytesWritten);
-
-                if (val == 0)
-                {
-                    return false;
-                }
-            }
-
             return true;
         }
 

+ 4 - 20
MediaBrowser.Controller/Library/IMediaSourceManager.cs

@@ -43,18 +43,10 @@ namespace MediaBrowser.Controller.Library
         /// <param name="id">The identifier.</param>
         /// <param name="userId">The user identifier.</param>
         /// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
+        /// <param name="supportedLiveMediaTypes">The supported live media types.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>IEnumerable&lt;MediaSourceInfo&gt;.</returns>
-        Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Gets the playack media sources.
-        /// </summary>
-        /// <param name="id">The identifier.</param>
-        /// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task&lt;IEnumerable&lt;MediaSourceInfo&gt;&gt;.</returns>
-        Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, bool enablePathSubstitution, CancellationToken cancellationToken);
+        Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, string[] supportedLiveMediaTypes, CancellationToken cancellationToken);
 
         /// <summary>
         /// Gets the static media sources.
@@ -63,16 +55,8 @@ namespace MediaBrowser.Controller.Library
         /// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
         /// <param name="user">The user.</param>
         /// <returns>IEnumerable&lt;MediaSourceInfo&gt;.</returns>
-        IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user);
+        IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user = null);
 
-        /// <summary>
-        /// Gets the static media sources.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
-        /// <returns>IEnumerable&lt;MediaSourceInfo&gt;.</returns>
-        IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution);
-        
         /// <summary>
         /// Gets the static media source.
         /// </summary>
@@ -80,7 +64,7 @@ namespace MediaBrowser.Controller.Library
         /// <param name="mediaSourceId">The media source identifier.</param>
         /// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
         /// <returns>MediaSourceInfo.</returns>
-        MediaSourceInfo GetStaticMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution);
+        Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution);
 
         /// <summary>
         /// Opens the media source.

+ 41 - 0
MediaBrowser.Controller/Library/NameExtensions.cs

@@ -0,0 +1,41 @@
+using MediaBrowser.Common.Extensions;
+using MoreLinq;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Library
+{
+    public static class NameExtensions
+    {
+        public static bool AreEqual(string name1, string name2)
+        {
+            name1 = NormalizeForComparison(name1);
+            name2 = NormalizeForComparison(name2);
+
+            return string.Equals(name1, name2, StringComparison.OrdinalIgnoreCase);
+        }
+
+        public static bool EqualsAny(IEnumerable<string> names, string name)
+        {
+            name = NormalizeForComparison(name);
+
+            return names.Any(i => string.Equals(NormalizeForComparison(i), name, StringComparison.OrdinalIgnoreCase));
+        }
+
+        private static string NormalizeForComparison(string name)
+        {
+            if (string.IsNullOrWhiteSpace(name))
+            {
+                return string.Empty;
+            }
+
+            return name.RemoveDiacritics();
+        }
+
+        public static IEnumerable<string> DistinctNames(this IEnumerable<string> names)
+        {
+            return names.DistinctBy(NormalizeForComparison, StringComparer.OrdinalIgnoreCase);
+        }
+    }
+}

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

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.LiveTv;
@@ -74,10 +75,11 @@ namespace MediaBrowser.Controller.LiveTv
         /// Gets the recording.
         /// </summary>
         /// <param name="id">The identifier.</param>
-        /// <param name="user">The user.</param>
+        /// <param name="options">The options.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="user">The user.</param>
         /// <returns>Task{RecordingInfoDto}.</returns>
-        Task<RecordingInfoDto> GetRecording(string id, CancellationToken cancellationToken, User user = null);
+        Task<RecordingInfoDto> GetRecording(string id, DtoOptions options, CancellationToken cancellationToken, User user = null);
 
         /// <summary>
         /// Gets the channel.
@@ -103,14 +105,15 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{TimerInfoDto}.</returns>
         Task<SeriesTimerInfoDto> GetSeriesTimer(string id, CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Gets the recordings.
         /// </summary>
         /// <param name="query">The query.</param>
+        /// <param name="options">The options.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>QueryResult{RecordingInfoDto}.</returns>
-        Task<QueryResult<RecordingInfoDto>> GetRecordings(RecordingQuery query, CancellationToken cancellationToken);
+        Task<QueryResult<RecordingInfoDto>> GetRecordings(RecordingQuery query, DtoOptions options, CancellationToken cancellationToken);
 
         /// <summary>
         /// Gets the timers.

+ 3 - 4
MediaBrowser.Controller/LiveTv/ILiveTvService.cs

@@ -1,10 +1,9 @@
-using System;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Dto;
+using System;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Controller.Channels;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Model.Dto;
 
 namespace MediaBrowser.Controller.LiveTv
 {

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

@@ -52,6 +52,10 @@
       <SpecificVersion>False</SpecificVersion>
       <HintPath>..\packages\morelinq.1.1.0\lib\net35\MoreLinq.dll</HintPath>
     </Reference>
+    <Reference Include="Patterns.IO, Version=1.0.5580.36861, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\Patterns.IO.1.0.0.3\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.IO.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
     <Reference Include="System.Data" />
@@ -115,6 +119,7 @@
     <Compile Include="Dlna\IMediaReceiverRegistrar.cs" />
     <Compile Include="Dlna\IUpnpService.cs" />
     <Compile Include="Drawing\IImageProcessor.cs" />
+    <Compile Include="Drawing\ImageCollageOptions.cs" />
     <Compile Include="Drawing\ImageProcessingOptions.cs" />
     <Compile Include="Drawing\ImageProcessorExtensions.cs" />
     <Compile Include="Drawing\ImageStream.cs" />
@@ -171,6 +176,7 @@
     <Compile Include="Entities\UserView.cs" />
     <Compile Include="Entities\UserViewBuilder.cs" />
     <Compile Include="FileOrganization\IFileOrganizationService.cs" />
+    <Compile Include="IO\ThrottledStream.cs" />
     <Compile Include="Library\DeleteOptions.cs" />
     <Compile Include="Library\ILibraryPostScanTask.cs" />
     <Compile Include="Library\IMediaSourceManager.cs" />
@@ -184,6 +190,7 @@
     <Compile Include="Library\IUserViewManager.cs" />
     <Compile Include="Library\LibraryManagerExtensions.cs" />
     <Compile Include="Library\MetadataConfigurationStore.cs" />
+    <Compile Include="Library\NameExtensions.cs" />
     <Compile Include="Library\PlaybackStopEventArgs.cs" />
     <Compile Include="Library\UserDataSaveEventArgs.cs" />
     <Compile Include="LiveTv\ILiveTvItem.cs" />
@@ -211,8 +218,8 @@
     <Compile Include="MediaEncoding\IEncodingManager.cs" />
     <Compile Include="MediaEncoding\ImageEncodingOptions.cs" />
     <Compile Include="MediaEncoding\IMediaEncoder.cs" />
-    <Compile Include="MediaEncoding\InternalMediaInfoResult.cs" />
     <Compile Include="MediaEncoding\ISubtitleEncoder.cs" />
+    <Compile Include="MediaEncoding\MediaInfoRequest.cs" />
     <Compile Include="MediaEncoding\MediaStreamSelector.cs" />
     <Compile Include="Net\AuthenticatedAttribute.cs" />
     <Compile Include="Net\AuthorizationInfo.cs" />
@@ -394,6 +401,7 @@
     <Compile Include="Subtitles\SubtitleResponse.cs" />
     <Compile Include="Subtitles\SubtitleSearchRequest.cs" />
     <Compile Include="Sync\IHasDynamicAccess.cs" />
+    <Compile Include="Sync\IRemoteSyncProvider.cs" />
     <Compile Include="Sync\IServerSyncProvider.cs" />
     <Compile Include="Sync\ISyncDataProvider.cs" />
     <Compile Include="Sync\ISyncManager.cs" />

+ 2 - 0
MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs

@@ -41,6 +41,8 @@ namespace MediaBrowser.Controller.MediaEncoding
         public int? SubtitleStreamIndex { get; set; }
         public int? MaxRefFrames { get; set; }
         public int? MaxVideoBitDepth { get; set; }
+        public int? CpuCoreLimit { get; set; }
+        public bool ReadInputAtNativeFramerate { get; set; }
         public SubtitleDeliveryMethod SubtitleMethod { get; set; }
 
         /// <summary>

+ 3 - 5
MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs

@@ -63,16 +63,14 @@ namespace MediaBrowser.Controller.MediaEncoding
             string filenamePrefix, 
             int? maxWidth,
             CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Gets the media info.
         /// </summary>
-        /// <param name="inputFiles">The input files.</param>
-        /// <param name="protocol">The protocol.</param>
-        /// <param name="isAudio">if set to <c>true</c> [is audio].</param>
+        /// <param name="request">The request.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, MediaProtocol protocol, bool isAudio, CancellationToken cancellationToken);
+        Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken);
 
         /// <summary>
         /// Gets the probe size argument.

+ 1 - 289
MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs

@@ -1,9 +1,7 @@
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using System;
 using System.Collections.Generic;
-using System.Globalization;
 using System.IO;
 using System.Linq;
 
@@ -46,291 +44,5 @@ namespace MediaBrowser.Controller.MediaEncoding
                 .Where(f => !string.IsNullOrEmpty(f))
                 .ToList();
         }
-
-        public static MediaInfo GetMediaInfo(InternalMediaInfoResult data)
-        {
-            var internalStreams = data.streams ?? new MediaStreamInfo[] { };
-
-            var info = new MediaInfo
-            {
-                MediaStreams = internalStreams.Select(s => GetMediaStream(s, data.format))
-                    .Where(i => i != null)
-                    .ToList()
-            };
-
-            if (data.format != null)
-            {
-                info.Format = data.format.format_name;
-
-                if (!string.IsNullOrEmpty(data.format.bit_rate))
-                {
-                    info.TotalBitrate = int.Parse(data.format.bit_rate, UsCulture);
-                }
-            }
-
-            return info;
-        }
-
-        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
-        /// <summary>
-        /// Converts ffprobe stream info to our MediaStream class
-        /// </summary>
-        /// <param name="streamInfo">The stream info.</param>
-        /// <param name="formatInfo">The format info.</param>
-        /// <returns>MediaStream.</returns>
-        private static MediaStream GetMediaStream(MediaStreamInfo streamInfo, MediaFormatInfo formatInfo)
-        {
-            var stream = new MediaStream
-            {
-                Codec = streamInfo.codec_name,
-                Profile = streamInfo.profile,
-                Level = streamInfo.level,
-                Index = streamInfo.index,
-                PixelFormat = streamInfo.pix_fmt
-            };
-
-            if (streamInfo.tags != null)
-            {
-                stream.Language = GetDictionaryValue(streamInfo.tags, "language");
-            }
-
-            if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase))
-            {
-                stream.Type = MediaStreamType.Audio;
-
-                stream.Channels = streamInfo.channels;
-
-                if (!string.IsNullOrEmpty(streamInfo.sample_rate))
-                {
-                    stream.SampleRate = int.Parse(streamInfo.sample_rate, UsCulture);
-                }
-
-                stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout);
-            }
-            else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase))
-            {
-                stream.Type = MediaStreamType.Subtitle;
-            }
-            else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase))
-            {
-                stream.Type = (streamInfo.codec_name ?? string.Empty).IndexOf("mjpeg", StringComparison.OrdinalIgnoreCase) != -1
-                    ? MediaStreamType.EmbeddedImage
-                    : MediaStreamType.Video;
-
-                stream.Width = streamInfo.width;
-                stream.Height = streamInfo.height;
-                stream.AspectRatio = GetAspectRatio(streamInfo);
-
-                stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
-                stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
-
-                stream.BitDepth = GetBitDepth(stream.PixelFormat);
-
-                //stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
-                //    string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
-                //    string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
-
-                stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase);
-            }
-            else
-            {
-                return null;
-            }
-
-            // Get stream bitrate
-            var bitrate = 0;
-
-            if (!string.IsNullOrEmpty(streamInfo.bit_rate))
-            {
-                bitrate = int.Parse(streamInfo.bit_rate, UsCulture);
-            }
-            else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video)
-            {
-                // If the stream info doesn't have a bitrate get the value from the media format info
-                bitrate = int.Parse(formatInfo.bit_rate, UsCulture);
-            }
-
-            if (bitrate > 0)
-            {
-                stream.BitRate = bitrate;
-            }
-
-            if (streamInfo.disposition != null)
-            {
-                var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
-                var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
-
-                stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
-
-                stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
-            }
-
-            return stream;
-        }
-
-        private static int? GetBitDepth(string pixelFormat)
-        {
-            var eightBit = new List<string>
-            {
-                "yuv420p",
-                "yuv411p",
-                "yuvj420p",
-                "uyyvyy411",
-                "nv12",
-                "nv21",
-                "rgb444le",
-                "rgb444be",
-                "bgr444le",
-                "bgr444be",
-                "yuvj411p"            
-            };
-
-            if (!string.IsNullOrEmpty(pixelFormat))
-            {
-                if (eightBit.Contains(pixelFormat, StringComparer.OrdinalIgnoreCase))
-                {
-                    return 8;
-                }
-            }
-
-            return null;
-        }
-
-        /// <summary>
-        /// Gets a string from an FFProbeResult tags dictionary
-        /// </summary>
-        /// <param name="tags">The tags.</param>
-        /// <param name="key">The key.</param>
-        /// <returns>System.String.</returns>
-        private static string GetDictionaryValue(Dictionary<string, string> tags, string key)
-        {
-            if (tags == null)
-            {
-                return null;
-            }
-
-            string val;
-
-            tags.TryGetValue(key, out val);
-            return val;
-        }
-
-        private static string ParseChannelLayout(string input)
-        {
-            if (string.IsNullOrEmpty(input))
-            {
-                return input;
-            }
-
-            return input.Split('(').FirstOrDefault();
-        }
-
-        private static string GetAspectRatio(MediaStreamInfo info)
-        {
-            var original = info.display_aspect_ratio;
-
-            int height;
-            int width;
-
-            var parts = (original ?? string.Empty).Split(':');
-            if (!(parts.Length == 2 &&
-                int.TryParse(parts[0], NumberStyles.Any, UsCulture, out width) &&
-                int.TryParse(parts[1], NumberStyles.Any, UsCulture, out height) &&
-                width > 0 &&
-                height > 0))
-            {
-                width = info.width;
-                height = info.height;
-            }
-
-            if (width > 0 && height > 0)
-            {
-                double ratio = width;
-                ratio /= height;
-
-                if (IsClose(ratio, 1.777777778, .03))
-                {
-                    return "16:9";
-                }
-
-                if (IsClose(ratio, 1.3333333333, .05))
-                {
-                    return "4:3";
-                }
-
-                if (IsClose(ratio, 1.41))
-                {
-                    return "1.41:1";
-                }
-
-                if (IsClose(ratio, 1.5))
-                {
-                    return "1.5:1";
-                }
-
-                if (IsClose(ratio, 1.6))
-                {
-                    return "1.6:1";
-                }
-
-                if (IsClose(ratio, 1.66666666667))
-                {
-                    return "5:3";
-                }
-
-                if (IsClose(ratio, 1.85, .02))
-                {
-                    return "1.85:1";
-                }
-
-                if (IsClose(ratio, 2.35, .025))
-                {
-                    return "2.35:1";
-                }
-
-                if (IsClose(ratio, 2.4, .025))
-                {
-                    return "2.40:1";
-                }
-            }
-
-            return original;
-        }
-
-        private static bool IsClose(double d1, double d2, double variance = .005)
-        {
-            return Math.Abs(d1 - d2) <= variance;
-        }
-
-        /// <summary>
-        /// Gets a frame rate from a string value in ffprobe output
-        /// This could be a number or in the format of 2997/125.
-        /// </summary>
-        /// <param name="value">The value.</param>
-        /// <returns>System.Nullable{System.Single}.</returns>
-        private static float? GetFrameRate(string value)
-        {
-            if (!string.IsNullOrEmpty(value))
-            {
-                var parts = value.Split('/');
-
-                float result;
-
-                if (parts.Length == 2)
-                {
-                    result = float.Parse(parts[0], UsCulture) / float.Parse(parts[1], UsCulture);
-                }
-                else
-                {
-                    result = float.Parse(parts[0], UsCulture);
-                }
-
-                return float.IsNaN(result) ? (float?)null : result;
-            }
-
-            return null;
-        }
-
     }
 }

+ 25 - 0
MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs

@@ -0,0 +1,25 @@
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+    public class MediaInfoRequest
+    {
+        public string InputPath { get; set; }
+        public MediaProtocol Protocol { get; set; }
+        public bool ExtractChapters { get; set; }
+        public DlnaProfileType MediaType { get; set; }
+        public IIsoMount MountedIso { get; set; }
+        public VideoType VideoType { get; set; }
+        public List<string> PlayableStreamFileNames { get; set; }
+        public bool ExtractKeyFrameInterval { get; set; }
+
+        public MediaInfoRequest()
+        {
+            PlayableStreamFileNames = new List<string>();
+        }
+    }
+}

+ 1 - 13
MediaBrowser.Controller/Providers/BaseItemXmlParser.cs

@@ -1404,24 +1404,12 @@ namespace MediaBrowser.Controller.Providers
                 {
                     switch (reader.Name)
                     {
-                        case "Name":
-                            {
-                                linkedItem.ItemName = reader.ReadElementContentAsString();
-                                break;
-                            }
-
                         case "Path":
                             {
                                 linkedItem.Path = reader.ReadElementContentAsString();
                                 break;
                             }
 
-                        case "Type":
-                            {
-                                linkedItem.ItemType = reader.ReadElementContentAsString();
-                                break;
-                            }
-
                         default:
                             reader.Skip();
                             break;
@@ -1435,7 +1423,7 @@ namespace MediaBrowser.Controller.Providers
                 return linkedItem;
             }
 
-            return string.IsNullOrWhiteSpace(linkedItem.ItemName) || string.IsNullOrWhiteSpace(linkedItem.ItemType) ? null : linkedItem;
+            return null;
         }
 
 

+ 1 - 2
MediaBrowser.Controller/Providers/IImageEnhancer.cs

@@ -1,5 +1,4 @@
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Entities;
 using System.Threading.Tasks;

+ 13 - 0
MediaBrowser.Controller/Providers/IProviderManager.cs

@@ -78,6 +78,19 @@ namespace MediaBrowser.Controller.Providers
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         Task SaveImage(IHasImages item, Stream source, string mimeType, ImageType type, int? imageIndex, string internalCacheKey, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Saves the image.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="source">The source.</param>
+        /// <param name="mimeType">Type of the MIME.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="imageIndex">Index of the image.</param>
+        /// <param name="internalCacheKey">The internal cache key.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SaveImage(IHasImages item, string source, string mimeType, ImageType type, int? imageIndex, string internalCacheKey, CancellationToken cancellationToken);
         
         /// <summary>
         /// Adds the metadata providers.

+ 2 - 2
MediaBrowser.Controller/Sync/IHasDynamicAccess.cs

@@ -9,10 +9,10 @@ namespace MediaBrowser.Controller.Sync
         /// <summary>
         /// Gets the synced file information.
         /// </summary>
-        /// <param name="remotePath">The remote path.</param>
+        /// <param name="id">The identifier.</param>
         /// <param name="target">The target.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task&lt;SyncedFileInfo&gt;.</returns>
-        Task<SyncedFileInfo> GetSyncedFileInfo(string remotePath, SyncTarget target, CancellationToken cancellationToken);
+        Task<SyncedFileInfo> GetSyncedFileInfo(string id, SyncTarget target, CancellationToken cancellationToken);
     }
 }

+ 10 - 0
MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs

@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Controller.Sync
+{
+    /// <summary>
+    /// A marker interface
+    /// </summary>
+    public interface IRemoteSyncProvider
+    {
+    }
+}

+ 14 - 20
MediaBrowser.Controller/Sync/IServerSyncProvider.cs

@@ -1,6 +1,7 @@
-using MediaBrowser.Model.Sync;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Sync;
+using Patterns.IO;
 using System;
-using System.Collections.Generic;
 using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
@@ -13,46 +14,39 @@ namespace MediaBrowser.Controller.Sync
         /// Transfers the file.
         /// </summary>
         /// <param name="stream">The stream.</param>
-        /// <param name="remotePath">The remote path.</param>
+        /// <param name="pathParts">The path parts.</param>
         /// <param name="target">The target.</param>
         /// <param name="progress">The progress.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        Task<SyncedFileInfo> SendFile(Stream stream, string remotePath, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
+        Task<SyncedFileInfo> SendFile(Stream stream, string[] pathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
 
         /// <summary>
         /// Deletes the file.
         /// </summary>
-        /// <param name="path">The path.</param>
+        /// <param name="id">The identifier.</param>
         /// <param name="target">The target.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        Task DeleteFile(string path, SyncTarget target, CancellationToken cancellationToken);
+        Task DeleteFile(string id, SyncTarget target, CancellationToken cancellationToken);
 
         /// <summary>
         /// Gets the file.
         /// </summary>
-        /// <param name="path">The path.</param>
+        /// <param name="id">The identifier.</param>
         /// <param name="target">The target.</param>
         /// <param name="progress">The progress.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task&lt;Stream&gt;.</returns>
-        Task<Stream> GetFile(string path, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
+        Task<Stream> GetFile(string id, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
 
         /// <summary>
-        /// Gets the full path.
+        /// Gets the files.
         /// </summary>
-        /// <param name="path">The path.</param>
+        /// <param name="query">The query.</param>
         /// <param name="target">The target.</param>
-        /// <returns>System.String.</returns>
-        string GetFullPath(IEnumerable<string> path, SyncTarget target);
-
-        /// <summary>
-        /// Gets the parent directory path.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="target">The target.</param>
-        /// <returns>System.String.</returns>
-        string GetParentDirectoryPath(string path, SyncTarget target);
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task&lt;QueryResult&lt;FileMetadata&gt;&gt;.</returns>
+        Task<QueryResult<FileMetadata>> GetFiles(FileQuery query, SyncTarget target, CancellationToken cancellationToken);
     }
 }

+ 3 - 11
MediaBrowser.Controller/Sync/ISyncDataProvider.cs

@@ -7,20 +7,12 @@ namespace MediaBrowser.Controller.Sync
     public interface ISyncDataProvider
     {
         /// <summary>
-        /// Gets the server item ids.
+        /// Gets the local items.
         /// </summary>
         /// <param name="target">The target.</param>
         /// <param name="serverId">The server identifier.</param>
-        /// <returns>Task&lt;List&lt;System.String&gt;&gt;.</returns>
-        Task<List<string>> GetServerItemIds(SyncTarget target, string serverId);
-
-        /// <summary>
-        /// Gets the synchronize job item ids.
-        /// </summary>
-        /// <param name="target">The target.</param>
-        /// <param name="serverId">The server identifier.</param>
-        /// <returns>Task&lt;List&lt;System.String&gt;&gt;.</returns>
-        Task<List<string>> GetSyncJobItemIds(SyncTarget target, string serverId);
+        /// <returns>Task&lt;List&lt;LocalItem&gt;&gt;.</returns>
+        Task<List<LocalItem>> GetLocalItems(SyncTarget target, string serverId);
 
         /// <summary>
         /// Adds the or update.

+ 14 - 0
MediaBrowser.Controller/Sync/ISyncManager.cs

@@ -174,6 +174,13 @@ namespace MediaBrowser.Controller.Sync
         /// <param name="targetId">The target identifier.</param>
         /// <returns>IEnumerable&lt;SyncQualityOption&gt;.</returns>
         IEnumerable<SyncQualityOption> GetQualityOptions(string targetId);
+        /// <summary>
+        /// Gets the quality options.
+        /// </summary>
+        /// <param name="targetId">The target identifier.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable&lt;SyncQualityOption&gt;.</returns>
+        IEnumerable<SyncQualityOption> GetQualityOptions(string targetId, User user);
 
         /// <summary>
         /// Gets the profile options.
@@ -181,5 +188,12 @@ namespace MediaBrowser.Controller.Sync
         /// <param name="targetId">The target identifier.</param>
         /// <returns>IEnumerable&lt;SyncQualityOption&gt;.</returns>
         IEnumerable<SyncProfileOption> GetProfileOptions(string targetId);
+        /// <summary>
+        /// Gets the profile options.
+        /// </summary>
+        /// <param name="targetId">The target identifier.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable&lt;SyncProfileOption&gt;.</returns>
+        IEnumerable<SyncProfileOption> GetProfileOptions(string targetId, User user);
     }
 }

+ 5 - 0
MediaBrowser.Controller/Sync/SyncedFileInfo.cs

@@ -20,6 +20,11 @@ namespace MediaBrowser.Controller.Sync
         /// </summary>
         /// <value>The required HTTP headers.</value>
         public Dictionary<string, string> RequiredHttpHeaders { get; set; }
+        /// <summary>
+        /// Gets or sets the identifier.
+        /// </summary>
+        /// <value>The identifier.</value>
+        public string Id { get; set; }
 
         public SyncedFileInfo()
         {

+ 1 - 0
MediaBrowser.Controller/packages.config

@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
   <package id="morelinq" version="1.1.0" targetFramework="net45" />
+  <package id="Patterns.IO" version="1.0.0.3" targetFramework="net45" />
 </packages>

+ 4 - 4
MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs

@@ -223,7 +223,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
             if (string.Equals(flag, "BrowseMetadata"))
             {
                 totalCount = 1;
-                
+
                 if (item.IsFolder || serverItem.StubType.HasValue)
                 {
                     var childrenResult = (await GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount).ConfigureAwait(false));
@@ -350,7 +350,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
             };
         }
 
-        private async Task<QueryResult<BaseItem>> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit)
+        private Task<QueryResult<BaseItem>> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit)
         {
             var folder = (Folder)item;
 
@@ -389,7 +389,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
                 isFolder = true;
             }
 
-            return await folder.GetItems(new InternalItemsQuery
+            return folder.GetItems(new InternalItemsQuery
             {
                 Limit = limit,
                 StartIndex = startIndex,
@@ -401,7 +401,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
                 IsFolder = isFolder,
                 MediaTypes = mediaTypes.ToArray()
 
-            }).ConfigureAwait(false);
+            });
         }
 
         private async Task<QueryResult<ServerItem>> GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit)

+ 5 - 4
MediaBrowser.Dlna/Didl/DidlBuilder.cs

@@ -12,6 +12,7 @@ using MediaBrowser.Dlna.ContentDirectory;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Net;
 using System;
 using System.Globalization;
@@ -124,9 +125,9 @@ namespace MediaBrowser.Dlna.Didl
         {
             if (streamInfo == null)
             {
-                var sources = _user == null ? _mediaSourceManager.GetStaticMediaSources(video, true).ToList() : _mediaSourceManager.GetStaticMediaSources(video, true, _user).ToList();
+                var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user).ToList();
 
-                streamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
+                streamInfo = new StreamBuilder(new NullLogger()).BuildVideoItem(new VideoOptions
                 {
                     ItemId = GetClientId(video),
                     MediaSources = sources,
@@ -351,9 +352,9 @@ namespace MediaBrowser.Dlna.Didl
 
             if (streamInfo == null)
             {
-                var sources = _user == null ? _mediaSourceManager.GetStaticMediaSources(audio, true).ToList() : _mediaSourceManager.GetStaticMediaSources(audio, true, _user).ToList();
+                var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user).ToList();
 
-                streamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions
+                streamInfo = new StreamBuilder(new NullLogger()).BuildAudioItem(new AudioOptions
                {
                    ItemId = GetClientId(audio),
                    MediaSources = sources,

+ 4 - 4
MediaBrowser.Dlna/PlayTo/PlayToController.cs

@@ -470,7 +470,7 @@ namespace MediaBrowser.Dlna.PlayTo
             
             var hasMediaSources = item as IHasMediaSources;
             var mediaSources = hasMediaSources != null
-                ? (user == null ? _mediaSourceManager.GetStaticMediaSources(hasMediaSources, true) : _mediaSourceManager.GetStaticMediaSources(hasMediaSources, true, user)).ToList()
+                ? (_mediaSourceManager.GetStaticMediaSources(hasMediaSources, true, user)).ToList()
                 : new List<MediaSourceInfo>();
 
             var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
@@ -542,7 +542,7 @@ namespace MediaBrowser.Dlna.PlayTo
             {
                 return new PlaylistItem
                 {
-                    StreamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
+                    StreamInfo = new StreamBuilder(_logger).BuildVideoItem(new VideoOptions
                     {
                         ItemId = item.Id.ToString("N"),
                         MediaSources = mediaSources,
@@ -562,7 +562,7 @@ namespace MediaBrowser.Dlna.PlayTo
             {
                 return new PlaylistItem
                 {
-                    StreamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions
+                    StreamInfo = new StreamBuilder(_logger).BuildAudioItem(new AudioOptions
                     {
                         ItemId = item.Id.ToString("N"),
                         MediaSources = mediaSources,
@@ -892,7 +892,7 @@ namespace MediaBrowser.Dlna.PlayTo
 
                 request.MediaSource = hasMediaSources == null ?
                     null :
-                    mediaSourceManager.GetStaticMediaSource(hasMediaSources, request.MediaSourceId, false);
+                    mediaSourceManager.GetMediaSource(hasMediaSources, request.MediaSourceId, false).Result;
 
 
 

+ 9 - 3
MediaBrowser.Dlna/Ssdp/SsdpHandler.cs

@@ -62,16 +62,22 @@ namespace MediaBrowser.Dlna.Ssdp
         {
             if (string.Equals(args.Method, "M-SEARCH", StringComparison.OrdinalIgnoreCase))
             {
-                TimeSpan delay = GetSearchDelay(args.Headers);
+                var headers = args.Headers;
+
+                TimeSpan delay = GetSearchDelay(headers);
                 
                 if (_config.GetDlnaConfiguration().EnableDebugLogging)
                 {
                     _logger.Debug("Delaying search response by {0} seconds", delay.TotalSeconds);
                 }
                 
-                await Task.Delay(delay).ConfigureAwait(false);                
+                await Task.Delay(delay).ConfigureAwait(false);
 
-                RespondToSearch(args.EndPoint, args.Headers["st"]);
+                string st;
+                if (headers.TryGetValue("st", out st))
+                {
+                    RespondToSearch(args.EndPoint, st);
+                }
             }
 
             EventHelper.FireEventIfNotNull(MessageReceived, this, args, _logger);

+ 1 - 1
MediaBrowser.LocalMetadata/BaseXmlProvider.cs

@@ -92,7 +92,7 @@ namespace MediaBrowser.LocalMetadata
         {
             get
             {
-                return "Media Browser Xml";
+                return "Emby Xml";
             }
         }
         

+ 6 - 1
MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs

@@ -10,7 +10,7 @@ using System.Linq;
 
 namespace MediaBrowser.LocalMetadata.Images
 {
-    public class EpisodeLocalLocalImageProvider : ILocalImageFileProvider
+    public class EpisodeLocalLocalImageProvider : ILocalImageFileProvider, IHasOrder
     {
         private readonly IFileSystem _fileSystem;
 
@@ -24,6 +24,11 @@ namespace MediaBrowser.LocalMetadata.Images
             get { return "Local Images"; }
         }
 
+        public int Order
+        {
+            get { return 0; }
+        }
+
         public bool Supports(IHasImages item)
         {
             return item is Episode && item.SupportsLocalMetadata;

+ 5 - 0
MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs

@@ -26,6 +26,11 @@ namespace MediaBrowser.LocalMetadata.Images
 
         public bool Supports(IHasImages item)
         {
+            if (item is Photo)
+            {
+                return false;
+            }
+
             if (!item.IsSaveLocalMetadataEnabled())
             {
                 return true;

+ 1 - 1
MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs

@@ -12,7 +12,7 @@ using System.Linq;
 
 namespace MediaBrowser.LocalMetadata.Images
 {
-    public class LocalImageProvider : ILocalImageFileProvider
+    public class LocalImageProvider : ILocalImageFileProvider, IHasOrder
     {
         private readonly IFileSystem _fileSystem;
 

+ 0 - 5
MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs

@@ -756,11 +756,6 @@ namespace MediaBrowser.LocalMetadata.Savers
             {
                 builder.Append("<" + singularNodeName + ">");
 
-                if (!string.IsNullOrWhiteSpace(link.ItemType))
-                {
-                    builder.Append("<Type>" + SecurityElement.Escape(link.ItemType) + "</Type>");
-                }
-
                 if (!string.IsNullOrWhiteSpace(link.Path))
                 {
                     builder.Append("<Path>" + SecurityElement.Escape((link.Path)) + "</Path>");

+ 2 - 17
MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs

@@ -70,10 +70,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
             Directory.CreateDirectory(Path.GetDirectoryName(encodingJob.OutputFilePath));
 
-            if (options.Context == EncodingContext.Static && encodingJob.IsInputVideo)
-            {
-                encodingJob.ReadInputAtNativeFramerate = true;
-            }
+            encodingJob.ReadInputAtNativeFramerate = options.ReadInputAtNativeFramerate;
 
             await AcquireResources(encodingJob, cancellationToken).ConfigureAwait(false);
 
@@ -305,19 +302,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// <returns>System.Int32.</returns>
         protected int GetNumberOfThreads(EncodingJob job, bool isWebm)
         {
-            // Only need one thread for sync
-            if (job.Options.Context == EncodingContext.Static)
-            {
-                return 1;
-            }
-
-            if (isWebm)
-            {
-                // Recommended per docs
-                return Math.Max(Environment.ProcessorCount - 1, 2);
-            }
-
-            return 0;
+            return job.Options.CpuCoreLimit ?? 0;
         }
 
         protected EncodingQuality GetQualitySetting()

+ 6 - 3
MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs

@@ -59,7 +59,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
 
-            var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(request.ItemId, false, cancellationToken).ConfigureAwait(false);
+            var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(request.ItemId, null, false, new[] { MediaType.Audio, MediaType.Video }, cancellationToken).ConfigureAwait(false);
 
             var mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
                ? mediaSources.First()
@@ -124,10 +124,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
             state.InputContainer = mediaSource.Container;
             state.InputFileSize = mediaSource.Size;
             state.InputBitrate = mediaSource.Bitrate;
-            state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
             state.RunTimeTicks = mediaSource.RunTimeTicks;
             state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
 
+            if (mediaSource.ReadAtNativeFramerate)
+            {
+                state.ReadInputAtNativeFramerate = true;
+            }
+
             if (mediaSource.VideoType.HasValue)
             {
                 state.VideoType = mediaSource.VideoType.Value;
@@ -148,7 +152,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
             state.InputBitrate = mediaSource.Bitrate;
             state.InputFileSize = mediaSource.Size;
-            state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
 
             if (state.ReadInputAtNativeFramerate ||
                 mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))

+ 293 - 99
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -5,12 +5,15 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.MediaEncoding.Probing;
+using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Serialization;
 using System;
+using System.Collections.Generic;
 using System.Diagnostics;
 using System.Globalization;
 using System.IO;
@@ -72,6 +75,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
         protected readonly Func<ISubtitleEncoder> SubtitleEncoder;
         protected readonly Func<IMediaSourceManager> MediaSourceManager;
 
+        private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
+
         public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, Func<ISubtitleEncoder> subtitleEncoder, Func<IMediaSourceManager> mediaSourceManager)
         {
             _logger = logger;
@@ -102,16 +107,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// <summary>
         /// Gets the media info.
         /// </summary>
-        /// <param name="inputFiles">The input files.</param>
-        /// <param name="protocol">The protocol.</param>
-        /// <param name="isAudio">if set to <c>true</c> [is audio].</param>
+        /// <param name="request">The request.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        public Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, MediaProtocol protocol, bool isAudio,
-            CancellationToken cancellationToken)
+        public Task<Model.MediaInfo.MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
         {
-            return GetMediaInfoInternal(GetInputArgument(inputFiles, protocol), !isAudio,
-                GetProbeSizeArgument(inputFiles, protocol), cancellationToken);
+            var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
+
+            var inputFiles = MediaEncoderHelpers.GetInputArgument(request.InputPath, request.Protocol, request.MountedIso, request.PlayableStreamFileNames);
+
+            var extractKeyFrameInterval = request.ExtractKeyFrameInterval && request.Protocol == MediaProtocol.File && request.VideoType == VideoType.VideoFile;
+
+            return GetMediaInfoInternal(GetInputArgument(inputFiles, request.Protocol), request.InputPath, request.Protocol, extractChapters, extractKeyFrameInterval,
+                GetProbeSizeArgument(inputFiles, request.Protocol), request.MediaType == DlnaProfileType.Audio, cancellationToken);
         }
 
         /// <summary>
@@ -141,13 +149,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// Gets the media info internal.
         /// </summary>
         /// <param name="inputPath">The input path.</param>
+        /// <param name="primaryPath">The primary path.</param>
+        /// <param name="protocol">The protocol.</param>
         /// <param name="extractChapters">if set to <c>true</c> [extract chapters].</param>
+        /// <param name="extractKeyFrameInterval">if set to <c>true</c> [extract key frame interval].</param>
         /// <param name="probeSizeArgument">The probe size argument.</param>
+        /// <param name="isAudio">if set to <c>true</c> [is audio].</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{MediaInfoResult}.</returns>
         /// <exception cref="System.ApplicationException"></exception>
-        private async Task<InternalMediaInfoResult> GetMediaInfoInternal(string inputPath, bool extractChapters,
+        private async Task<Model.MediaInfo.MediaInfo> GetMediaInfoInternal(string inputPath,
+            string primaryPath,
+            MediaProtocol protocol,
+            bool extractChapters,
+            bool extractKeyFrameInterval,
             string probeSizeArgument,
+            bool isAudio,
             CancellationToken cancellationToken)
         {
             var args = extractChapters
@@ -164,6 +181,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                     // Must consume both or ffmpeg may hang due to deadlocks. See comments below.   
                     RedirectStandardOutput = true,
                     RedirectStandardError = true,
+                    RedirectStandardInput = true,
                     FileName = FFProbePath,
                     Arguments = string.Format(args,
                     probeSizeArgument, inputPath).Trim(),
@@ -177,15 +195,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
 
-            process.Exited += ProcessExited;
-
             await _ffProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
 
-            InternalMediaInfoResult result;
+            var processWrapper = new ProcessWrapper(process, this);
 
             try
             {
-                process.Start();
+                StartProcess(processWrapper);
             }
             catch (Exception ex)
             {
@@ -200,19 +216,57 @@ namespace MediaBrowser.MediaEncoding.Encoder
             {
                 process.BeginErrorReadLine();
 
-                result = _jsonSerializer.DeserializeFromStream<InternalMediaInfoResult>(process.StandardOutput.BaseStream);
+                var result = _jsonSerializer.DeserializeFromStream<InternalMediaInfoResult>(process.StandardOutput.BaseStream);
+
+                if (result != null)
+                {
+                    if (result.streams != null)
+                    {
+                        // Normalize aspect ratio if invalid
+                        foreach (var stream in result.streams)
+                        {
+                            if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
+                            {
+                                stream.display_aspect_ratio = string.Empty;
+                            }
+                            if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
+                            {
+                                stream.sample_aspect_ratio = string.Empty;
+                            }
+                        }
+                    }
+
+                    var mediaInfo = new ProbeResultNormalizer(_logger, FileSystem).GetMediaInfo(result, isAudio, primaryPath, protocol);
+
+                    if (extractKeyFrameInterval && mediaInfo.RunTimeTicks.HasValue)
+                    {
+                        foreach (var stream in mediaInfo.MediaStreams)
+                        {
+                            if (stream.Type == MediaStreamType.Video && string.Equals(stream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
+                            {
+                                try
+                                {
+                                    //stream.KeyFrames = await GetKeyFrames(inputPath, stream.Index, cancellationToken)
+                                    //            .ConfigureAwait(false);
+                                }
+                                catch (OperationCanceledException)
+                                {
+
+                                }
+                                catch (Exception ex)
+                                {
+                                    _logger.ErrorException("Error getting key frame interval", ex);
+                                }
+                            }
+                        }
+                    }
+
+                    return mediaInfo;
+                }
             }
             catch
             {
-                // Hate having to do this
-                try
-                {
-                    process.Kill();
-                }
-                catch (Exception ex1)
-                {
-                    _logger.ErrorException("Error killing ffprobe", ex1);
-                }
+                StopProcess(processWrapper, 100, true);
 
                 throw;
             }
@@ -221,30 +275,102 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 _ffProbeResourcePool.Release();
             }
 
-            if (result == null)
+            throw new ApplicationException(string.Format("FFProbe failed for {0}", inputPath));
+        }
+
+        private async Task<List<int>> GetKeyFrames(string inputPath, int videoStreamIndex, CancellationToken cancellationToken)
+        {
+            const string args = "-i {0} -select_streams v:{1} -show_frames -show_entries frame=pkt_dts,key_frame -print_format compact";
+
+            var process = new Process
+            {
+                StartInfo = new ProcessStartInfo
+                {
+                    CreateNoWindow = true,
+                    UseShellExecute = false,
+
+                    // Must consume both or ffmpeg may hang due to deadlocks. See comments below.   
+                    RedirectStandardOutput = true,
+                    RedirectStandardError = true,
+                    RedirectStandardInput = true,
+                    FileName = FFProbePath,
+                    Arguments = string.Format(args, inputPath, videoStreamIndex.ToString(CultureInfo.InvariantCulture)).Trim(),
+
+                    WindowStyle = ProcessWindowStyle.Hidden,
+                    ErrorDialog = false
+                },
+
+                EnableRaisingEvents = true
+            };
+
+            _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+            var processWrapper = new ProcessWrapper(process, this);
+
+            StartProcess(processWrapper);
+
+            var lines = new List<int>();
+
+            try
             {
-                throw new ApplicationException(string.Format("FFProbe failed for {0}", inputPath));
+                process.BeginErrorReadLine();
+
+                await StartReadingOutput(process.StandardOutput.BaseStream, lines, 120000, cancellationToken).ConfigureAwait(false);
+            }
+            catch (OperationCanceledException)
+            {
+                if (cancellationToken.IsCancellationRequested)
+                {
+                    throw;
+                }
+            }
+            finally
+            {
+                StopProcess(processWrapper, 100, true);
             }
 
-            cancellationToken.ThrowIfCancellationRequested();
+            return lines;
+        }
 
-            if (result.streams != null)
+        private async Task StartReadingOutput(Stream source, List<int> lines, int timeoutMs, CancellationToken cancellationToken)
+        {
+            try
             {
-                // Normalize aspect ratio if invalid
-                foreach (var stream in result.streams)
+                using (var reader = new StreamReader(source))
                 {
-                    if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
-                    {
-                        stream.display_aspect_ratio = string.Empty;
-                    }
-                    if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
+                    while (!reader.EndOfStream)
                     {
-                        stream.sample_aspect_ratio = string.Empty;
+                        cancellationToken.ThrowIfCancellationRequested();
+
+                        var line = await reader.ReadLineAsync().ConfigureAwait(false);
+
+                        var values = (line ?? string.Empty).Split('|')
+                            .Where(i => !string.IsNullOrWhiteSpace(i))
+                            .Select(i => i.Split('='))
+                            .Where(i => i.Length == 2)
+                            .ToDictionary(i => i[0], i => i[1]);
+
+                        string pktDts;
+                        int frameMs;
+                        if (values.TryGetValue("pkt_dts", out pktDts) && int.TryParse(pktDts, NumberStyles.Any, CultureInfo.InvariantCulture, out frameMs))
+                        {
+                            string keyFrame;
+                            if (values.TryGetValue("key_frame", out keyFrame) && string.Equals(keyFrame, "1", StringComparison.OrdinalIgnoreCase))
+                            {
+                                lines.Add(frameMs);
+                            }
+                        }
                     }
                 }
             }
-
-            return result;
+            catch (OperationCanceledException)
+            {
+                throw;
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error reading ffprobe output", ex);
+            }
         }
 
         /// <summary>
@@ -252,16 +378,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// </summary>
         protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
 
-        /// <summary>
-        /// Processes the exited.
-        /// </summary>
-        /// <param name="sender">The sender.</param>
-        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
-        private void ProcessExited(object sender, EventArgs e)
-        {
-            ((Process)sender).Dispose();
-        }
-
         public Task<Stream> ExtractAudioImage(string path, CancellationToken cancellationToken)
         {
             return ExtractImage(new[] { path }, MediaProtocol.File, true, null, null, cancellationToken);
@@ -286,6 +402,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 {
                     return await ExtractImageInternal(inputArgument, protocol, threedFormat, offset, true, resourcePool, cancellationToken).ConfigureAwait(false);
                 }
+                catch (ArgumentException)
+                {
+                    throw;
+                }
                 catch
                 {
                     _logger.Error("I-frame image extraction failed, will attempt standard way. Input: {0}", inputArgument);
@@ -368,7 +488,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
 
-            process.Start();
+            var processWrapper = new ProcessWrapper(process, this);
+
+            StartProcess(processWrapper);
 
             var memoryStream = new MemoryStream();
 
@@ -384,23 +506,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             if (!ranToCompletion)
             {
-                try
-                {
-                    _logger.Info("Killing ffmpeg process");
-
-                    process.StandardInput.WriteLine("q");
-
-                    process.WaitForExit(1000);
-                }
-                catch (Exception ex)
-                {
-                    _logger.ErrorException("Error killing process", ex);
-                }
+                StopProcess(processWrapper, 1000, false);
             }
 
             resourcePool.Release();
 
-            var exitCode = ranToCompletion ? process.ExitCode : -1;
+            var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
 
             process.Dispose();
 
@@ -419,31 +530,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return memoryStream;
         }
 
-        public Task<Stream> EncodeImage(ImageEncodingOptions options, CancellationToken cancellationToken)
-        {
-            throw new NotImplementedException();
-        }
-
-        /// <summary>
-        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
-        /// </summary>
-        public void Dispose()
-        {
-            Dispose(true);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        protected virtual void Dispose(bool dispose)
-        {
-            if (dispose)
-            {
-                _videoImageResourcePool.Dispose();
-            }
-        }
-
         public string GetTimeParameter(long ticks)
         {
             var time = TimeSpan.FromTicks(ticks);
@@ -510,9 +596,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             bool ranToCompletion;
 
+            var processWrapper = new ProcessWrapper(process, this);
+
             try
             {
-                process.Start();
+                StartProcess(processWrapper);
 
                 // Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
                 // but we still need to detect if the process hangs.
@@ -536,18 +624,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
                 if (!ranToCompletion)
                 {
-                    try
-                    {
-                        _logger.Info("Killing ffmpeg process");
-
-                        process.StandardInput.WriteLine("q");
-
-                        process.WaitForExit(1000);
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.ErrorException("Error killing process", ex);
-                    }
+                    StopProcess(processWrapper, 1000, false);
                 }
             }
             finally
@@ -555,7 +632,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 resourcePool.Release();
             }
 
-            var exitCode = ranToCompletion ? process.ExitCode : -1;
+            var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
 
             process.Dispose();
 
@@ -608,5 +685,122 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             return job.OutputFilePath;
         }
+
+        private void StartProcess(ProcessWrapper process)
+        {
+            process.Process.Start();
+
+            lock (_runningProcesses)
+            {
+                _runningProcesses.Add(process);
+            }
+        }
+        private void StopProcess(ProcessWrapper process, int waitTimeMs, bool enableForceKill)
+        {
+            try
+            {
+                _logger.Info("Killing ffmpeg process");
+
+                try
+                {
+                    process.Process.StandardInput.WriteLine("q");
+                }
+                catch (Exception)
+                {
+                    _logger.Error("Error sending q command to process");
+                }
+
+                try
+                {
+                    if (process.Process.WaitForExit(waitTimeMs))
+                    {
+                        return;
+                    }
+                }
+                catch (Exception ex)
+                {
+                    _logger.Error("Error in WaitForExit", ex);
+                }
+
+                if (enableForceKill)
+                {
+                    process.Process.Kill();
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error killing process", ex);
+            }
+        }
+
+        private void StopProcesses()
+        {
+            List<ProcessWrapper> proceses;
+            lock (_runningProcesses)
+            {
+                proceses = _runningProcesses.ToList();
+            }
+            _runningProcesses.Clear();
+
+            foreach (var process in proceses)
+            {
+                if (!process.HasExited)
+                {
+                    StopProcess(process, 500, true);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+        /// </summary>
+        public void Dispose()
+        {
+            Dispose(true);
+        }
+
+        /// <summary>
+        /// Releases unmanaged and - optionally - managed resources.
+        /// </summary>
+        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected virtual void Dispose(bool dispose)
+        {
+            if (dispose)
+            {
+                _videoImageResourcePool.Dispose();
+                StopProcesses();
+            }
+        }
+
+        private class ProcessWrapper
+        {
+            public readonly Process Process;
+            public bool HasExited;
+            public int? ExitCode;
+            private readonly MediaEncoder _mediaEncoder;
+
+            public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
+            {
+                Process = process;
+                this._mediaEncoder = mediaEncoder;
+                Process.Exited += Process_Exited;
+            }
+
+            void Process_Exited(object sender, EventArgs e)
+            {
+                var process = (Process)sender;
+
+                HasExited = true;
+
+                ExitCode = process.ExitCode;
+
+                lock (_mediaEncoder._runningProcesses)
+                {
+                    _mediaEncoder._runningProcesses.Remove(this);
+                }
+
+                process.Dispose();
+            }
+        }
     }
 }

+ 10 - 1
MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj

@@ -68,6 +68,9 @@
     <Compile Include="Encoder\JobLogger.cs" />
     <Compile Include="Encoder\MediaEncoder.cs" />
     <Compile Include="Encoder\VideoEncoder.cs" />
+    <Compile Include="Probing\FFProbeHelpers.cs" />
+    <Compile Include="Probing\InternalMediaInfoResult.cs" />
+    <Compile Include="Probing\ProbeResultNormalizer.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Subtitles\ISubtitleParser.cs" />
     <Compile Include="Subtitles\ISubtitleWriter.cs" />
@@ -91,6 +94,10 @@
       <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
       <Name>MediaBrowser.Controller</Name>
     </ProjectReference>
+    <ProjectReference Include="..\MediaBrowser.MediaInfo\MediaBrowser.MediaInfo.csproj">
+      <Project>{6e4145e4-c6d4-4e4d-94f2-87188db6e239}</Project>
+      <Name>MediaBrowser.MediaInfo</Name>
+    </ProjectReference>
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
       <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
       <Name>MediaBrowser.Model</Name>
@@ -99,7 +106,9 @@
   <ItemGroup>
     <None Include="packages.config" />
   </ItemGroup>
-  <ItemGroup />
+  <ItemGroup>
+    <EmbeddedResource Include="Probing\whitelist.txt" />
+  </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 - 1
MediaBrowser.Providers/MediaInfo/FFProbeHelpers.cs → MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs

@@ -2,7 +2,7 @@
 using System;
 using System.Collections.Generic;
 
-namespace MediaBrowser.Providers.MediaInfo
+namespace MediaBrowser.MediaEncoding.Probing
 {
     public static class FFProbeHelpers
     {

+ 3 - 3
MediaBrowser.Controller/MediaEncoding/InternalMediaInfoResult.cs → MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs

@@ -1,6 +1,6 @@
 using System.Collections.Generic;
 
-namespace MediaBrowser.Controller.MediaEncoding
+namespace MediaBrowser.MediaEncoding.Probing
 {
     /// <summary>
     /// Class MediaInfoResult
@@ -89,7 +89,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// </summary>
         /// <value>The channel_layout.</value>
         public string channel_layout { get; set; }
-        
+
         /// <summary>
         /// Gets or sets the avg_frame_rate.
         /// </summary>
@@ -317,7 +317,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// </summary>
         /// <value>The probe_score.</value>
         public int probe_score { get; set; }
-        
+
         /// <summary>
         /// Gets or sets the tags.
         /// </summary>

+ 887 - 0
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -0,0 +1,887 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.MediaInfo;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+
+namespace MediaBrowser.MediaEncoding.Probing
+{
+    public class ProbeResultNormalizer
+    {
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        private readonly ILogger _logger;
+        private readonly IFileSystem _fileSystem;
+
+        public ProbeResultNormalizer(ILogger logger, IFileSystem fileSystem)
+        {
+            _logger = logger;
+            _fileSystem = fileSystem;
+        }
+
+        public Model.MediaInfo.MediaInfo GetMediaInfo(InternalMediaInfoResult data, bool isAudio, string path, MediaProtocol protocol)
+        {
+            var info = new Model.MediaInfo.MediaInfo
+            {
+                Path = path,
+                Protocol = protocol
+            };
+
+            FFProbeHelpers.NormalizeFFProbeResult(data);
+            SetSize(data, info);
+
+            var internalStreams = data.streams ?? new MediaStreamInfo[] { };
+
+            info.MediaStreams = internalStreams.Select(s => GetMediaStream(s, data.format))
+                .Where(i => i != null)
+                .ToList();
+
+            if (data.format != null)
+            {
+                info.Container = data.format.format_name;
+
+                if (!string.IsNullOrEmpty(data.format.bit_rate))
+                {
+                    info.Bitrate = int.Parse(data.format.bit_rate, _usCulture);
+                }
+            }
+
+            if (isAudio)
+            {
+                SetAudioRuntimeTicks(data, info);
+
+                if (data.format != null && data.format.tags != null)
+                {
+                    SetAudioInfoFromTags(info, data.format.tags);
+                }
+            }
+            else
+            {
+                if (data.format != null && !string.IsNullOrEmpty(data.format.duration))
+                {
+                    info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks;
+                }
+
+                FetchWtvInfo(info, data);
+
+                if (data.Chapters != null)
+                {
+                    info.Chapters = data.Chapters.Select(GetChapterInfo).ToList();
+                }
+
+                ExtractTimestamp(info);
+
+                var videoStream = info.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
+
+                if (videoStream != null)
+                {
+                    UpdateFromMediaInfo(info, videoStream);
+                }
+            }
+
+            return info;
+        }
+
+        /// <summary>
+        /// Converts ffprobe stream info to our MediaStream class
+        /// </summary>
+        /// <param name="streamInfo">The stream info.</param>
+        /// <param name="formatInfo">The format info.</param>
+        /// <returns>MediaStream.</returns>
+        private MediaStream GetMediaStream(MediaStreamInfo streamInfo, MediaFormatInfo formatInfo)
+        {
+            var stream = new MediaStream
+            {
+                Codec = streamInfo.codec_name,
+                Profile = streamInfo.profile,
+                Level = streamInfo.level,
+                Index = streamInfo.index,
+                PixelFormat = streamInfo.pix_fmt
+            };
+
+            if (streamInfo.tags != null)
+            {
+                stream.Language = GetDictionaryValue(streamInfo.tags, "language");
+            }
+
+            if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase))
+            {
+                stream.Type = MediaStreamType.Audio;
+
+                stream.Channels = streamInfo.channels;
+
+                if (!string.IsNullOrEmpty(streamInfo.sample_rate))
+                {
+                    stream.SampleRate = int.Parse(streamInfo.sample_rate, _usCulture);
+                }
+
+                stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout);
+            }
+            else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase))
+            {
+                stream.Type = MediaStreamType.Subtitle;
+            }
+            else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase))
+            {
+                stream.Type = (streamInfo.codec_name ?? string.Empty).IndexOf("mjpeg", StringComparison.OrdinalIgnoreCase) != -1
+                    ? MediaStreamType.EmbeddedImage
+                    : MediaStreamType.Video;
+
+                stream.Width = streamInfo.width;
+                stream.Height = streamInfo.height;
+                stream.AspectRatio = GetAspectRatio(streamInfo);
+
+                stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
+                stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
+
+                stream.BitDepth = GetBitDepth(stream.PixelFormat);
+
+                //stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
+                //    string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
+                //    string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
+
+                stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase);
+            }
+            else
+            {
+                return null;
+            }
+
+            // Get stream bitrate
+            var bitrate = 0;
+
+            if (!string.IsNullOrEmpty(streamInfo.bit_rate))
+            {
+                bitrate = int.Parse(streamInfo.bit_rate, _usCulture);
+            }
+            else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video)
+            {
+                // If the stream info doesn't have a bitrate get the value from the media format info
+                bitrate = int.Parse(formatInfo.bit_rate, _usCulture);
+            }
+
+            if (bitrate > 0)
+            {
+                stream.BitRate = bitrate;
+            }
+
+            if (streamInfo.disposition != null)
+            {
+                var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
+                var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
+
+                stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
+
+                stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
+            }
+
+            return stream;
+        }
+
+        private int? GetBitDepth(string pixelFormat)
+        {
+            var eightBit = new List<string>
+            {
+                "yuv420p",
+                "yuv411p",
+                "yuvj420p",
+                "uyyvyy411",
+                "nv12",
+                "nv21",
+                "rgb444le",
+                "rgb444be",
+                "bgr444le",
+                "bgr444be",
+                "yuvj411p"            
+            };
+
+            if (!string.IsNullOrEmpty(pixelFormat))
+            {
+                if (eightBit.Contains(pixelFormat, StringComparer.OrdinalIgnoreCase))
+                {
+                    return 8;
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Gets a string from an FFProbeResult tags dictionary
+        /// </summary>
+        /// <param name="tags">The tags.</param>
+        /// <param name="key">The key.</param>
+        /// <returns>System.String.</returns>
+        private string GetDictionaryValue(Dictionary<string, string> tags, string key)
+        {
+            if (tags == null)
+            {
+                return null;
+            }
+
+            string val;
+
+            tags.TryGetValue(key, out val);
+            return val;
+        }
+
+        private string ParseChannelLayout(string input)
+        {
+            if (string.IsNullOrEmpty(input))
+            {
+                return input;
+            }
+
+            return input.Split('(').FirstOrDefault();
+        }
+
+        private string GetAspectRatio(MediaStreamInfo info)
+        {
+            var original = info.display_aspect_ratio;
+
+            int height;
+            int width;
+
+            var parts = (original ?? string.Empty).Split(':');
+            if (!(parts.Length == 2 &&
+                int.TryParse(parts[0], NumberStyles.Any, _usCulture, out width) &&
+                int.TryParse(parts[1], NumberStyles.Any, _usCulture, out height) &&
+                width > 0 &&
+                height > 0))
+            {
+                width = info.width;
+                height = info.height;
+            }
+
+            if (width > 0 && height > 0)
+            {
+                double ratio = width;
+                ratio /= height;
+
+                if (IsClose(ratio, 1.777777778, .03))
+                {
+                    return "16:9";
+                }
+
+                if (IsClose(ratio, 1.3333333333, .05))
+                {
+                    return "4:3";
+                }
+
+                if (IsClose(ratio, 1.41))
+                {
+                    return "1.41:1";
+                }
+
+                if (IsClose(ratio, 1.5))
+                {
+                    return "1.5:1";
+                }
+
+                if (IsClose(ratio, 1.6))
+                {
+                    return "1.6:1";
+                }
+
+                if (IsClose(ratio, 1.66666666667))
+                {
+                    return "5:3";
+                }
+
+                if (IsClose(ratio, 1.85, .02))
+                {
+                    return "1.85:1";
+                }
+
+                if (IsClose(ratio, 2.35, .025))
+                {
+                    return "2.35:1";
+                }
+
+                if (IsClose(ratio, 2.4, .025))
+                {
+                    return "2.40:1";
+                }
+            }
+
+            return original;
+        }
+
+        private bool IsClose(double d1, double d2, double variance = .005)
+        {
+            return Math.Abs(d1 - d2) <= variance;
+        }
+
+        /// <summary>
+        /// Gets a frame rate from a string value in ffprobe output
+        /// This could be a number or in the format of 2997/125.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        /// <returns>System.Nullable{System.Single}.</returns>
+        private float? GetFrameRate(string value)
+        {
+            if (!string.IsNullOrEmpty(value))
+            {
+                var parts = value.Split('/');
+
+                float result;
+
+                if (parts.Length == 2)
+                {
+                    result = float.Parse(parts[0], _usCulture) / float.Parse(parts[1], _usCulture);
+                }
+                else
+                {
+                    result = float.Parse(parts[0], _usCulture);
+                }
+
+                return float.IsNaN(result) ? (float?)null : result;
+            }
+
+            return null;
+        }
+
+        private void SetAudioRuntimeTicks(InternalMediaInfoResult result, Model.MediaInfo.MediaInfo data)
+        {
+            if (result.streams != null)
+            {
+                // Get the first audio stream
+                var stream = result.streams.FirstOrDefault(s => string.Equals(s.codec_type, "audio", StringComparison.OrdinalIgnoreCase));
+
+                if (stream != null)
+                {
+                    // Get duration from stream properties
+                    var duration = stream.duration;
+
+                    // If it's not there go into format properties
+                    if (string.IsNullOrEmpty(duration))
+                    {
+                        duration = result.format.duration;
+                    }
+
+                    // If we got something, parse it
+                    if (!string.IsNullOrEmpty(duration))
+                    {
+                        data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, _usCulture)).Ticks;
+                    }
+                }
+            }
+        }
+
+        private void SetSize(InternalMediaInfoResult data, Model.MediaInfo.MediaInfo info)
+        {
+            if (data.format != null)
+            {
+                if (!string.IsNullOrEmpty(data.format.size))
+                {
+                    info.Size = long.Parse(data.format.size, _usCulture);
+                }
+                else
+                {
+                    info.Size = null;
+                }
+            }
+        }
+
+        private void SetAudioInfoFromTags(Model.MediaInfo.MediaInfo audio, Dictionary<string, string> tags)
+        {
+            var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
+
+            // Only set Name if title was found in the dictionary
+            if (!string.IsNullOrEmpty(title))
+            {
+                audio.Title = title;
+            }
+
+            var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
+
+            if (!string.IsNullOrWhiteSpace(composer))
+            {
+                foreach (var person in Split(composer, false))
+                {
+                    audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer });
+                }
+            }
+
+            audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
+
+            var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
+
+            if (!string.IsNullOrWhiteSpace(artists))
+            {
+                audio.Artists = artists.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
+                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .ToList();
+            }
+            else
+            {
+                var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
+                if (string.IsNullOrWhiteSpace(artist))
+                {
+                    audio.Artists.Clear();
+                }
+                else
+                {
+                    audio.Artists = SplitArtists(artist)
+                        .Distinct(StringComparer.OrdinalIgnoreCase)
+                        .ToList();
+                }
+            }
+
+            var albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist");
+            if (string.IsNullOrWhiteSpace(albumArtist))
+            {
+                albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album artist");
+            }
+            if (string.IsNullOrWhiteSpace(albumArtist))
+            {
+                albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album_artist");
+            }
+
+            if (string.IsNullOrWhiteSpace(albumArtist))
+            {
+                audio.AlbumArtists = new List<string>();
+            }
+            else
+            {
+                audio.AlbumArtists = SplitArtists(albumArtist)
+                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .ToList();
+
+            }
+
+            // Track number
+            audio.IndexNumber = GetDictionaryDiscValue(tags, "track");
+
+            // Disc number
+            audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
+
+            audio.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
+
+            // Several different forms of retaildate
+            audio.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
+                FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
+                FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
+                FFProbeHelpers.GetDictionaryDateTime(tags, "date");
+
+            // If we don't have a ProductionYear try and get it from PremiereDate
+            if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
+            {
+                audio.ProductionYear = audio.PremiereDate.Value.ToLocalTime().Year;
+            }
+
+            FetchGenres(audio, tags);
+
+            // There's several values in tags may or may not be present
+            FetchStudios(audio, tags, "organization");
+            FetchStudios(audio, tags, "ensemble");
+            FetchStudios(audio, tags, "publisher");
+
+            // These support mulitple values, but for now we only store the first.
+            audio.SetProviderId(MetadataProviders.MusicBrainzAlbumArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id")));
+            audio.SetProviderId(MetadataProviders.MusicBrainzArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Artist Id")));
+
+            audio.SetProviderId(MetadataProviders.MusicBrainzAlbum, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Id")));
+            audio.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Group Id")));
+            audio.SetProviderId(MetadataProviders.MusicBrainzTrack, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Track Id")));
+        }
+
+        private string GetMultipleMusicBrainzId(string value)
+        {
+            if (string.IsNullOrWhiteSpace(value))
+            {
+                return null;
+            }
+
+            return value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
+                .Select(i => i.Trim())
+                .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
+        }
+
+        private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
+
+        /// <summary>
+        /// Splits the specified val.
+        /// </summary>
+        /// <param name="val">The val.</param>
+        /// <param name="allowCommaDelimiter">if set to <c>true</c> [allow comma delimiter].</param>
+        /// <returns>System.String[][].</returns>
+        private IEnumerable<string> Split(string val, bool allowCommaDelimiter)
+        {
+            // Only use the comma as a delimeter if there are no slashes or pipes. 
+            // We want to be careful not to split names that have commas in them
+            var delimeter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i) != -1) ?
+                _nameDelimiters :
+                new[] { ',' };
+
+            return val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
+                .Where(i => !string.IsNullOrWhiteSpace(i))
+                .Select(i => i.Trim());
+        }
+
+        private const string ArtistReplaceValue = " | ";
+
+        private IEnumerable<string> SplitArtists(string val)
+        {
+            val = val.Replace(" featuring ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase)
+                .Replace(" feat. ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase);
+
+            var artistsFound = new List<string>();
+
+            foreach (var whitelistArtist in GetSplitWhitelist())
+            {
+                var originalVal = val;
+                val = val.Replace(whitelistArtist, "|", StringComparison.OrdinalIgnoreCase);
+
+                if (!string.Equals(originalVal, val, StringComparison.OrdinalIgnoreCase))
+                {
+                    artistsFound.Add(whitelistArtist);
+                }
+            }
+
+            // Only use the comma as a delimeter if there are no slashes or pipes. 
+            // We want to be careful not to split names that have commas in them
+            var delimeter = _nameDelimiters;
+
+            var artists = val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
+                .Where(i => !string.IsNullOrWhiteSpace(i))
+                .Select(i => i.Trim());
+
+            artistsFound.AddRange(artists);
+            return artistsFound;
+        }
+
+
+        private List<string> _splitWhiteList = null;
+
+        private IEnumerable<string> GetSplitWhitelist()
+        {
+            if (_splitWhiteList == null)
+            {
+                var file = GetType().Namespace + ".whitelist.txt";
+
+                using (var stream = GetType().Assembly.GetManifestResourceStream(file))
+                {
+                    using (var reader = new StreamReader(stream))
+                    {
+                        var list = new List<string>();
+
+                        while (!reader.EndOfStream)
+                        {
+                            var val = reader.ReadLine();
+
+                            if (!string.IsNullOrWhiteSpace(val))
+                            {
+                                list.Add(val);
+                            }
+                        }
+
+                        _splitWhiteList = list;
+                    }
+                }
+            }
+
+            return _splitWhiteList;
+        }
+
+        /// <summary>
+        /// Gets the studios from the tags collection
+        /// </summary>
+        /// <param name="audio">The audio.</param>
+        /// <param name="tags">The tags.</param>
+        /// <param name="tagName">Name of the tag.</param>
+        private void FetchStudios(Model.MediaInfo.MediaInfo audio, Dictionary<string, string> tags, string tagName)
+        {
+            var val = FFProbeHelpers.GetDictionaryValue(tags, tagName);
+
+            if (!string.IsNullOrEmpty(val))
+            {
+                var studios = Split(val, true);
+
+                foreach (var studio in studios)
+                {
+                    // Sometimes the artist name is listed here, account for that
+                    if (audio.Artists.Contains(studio, StringComparer.OrdinalIgnoreCase))
+                    {
+                        continue;
+                    }
+                    if (audio.AlbumArtists.Contains(studio, StringComparer.OrdinalIgnoreCase))
+                    {
+                        continue;
+                    }
+
+                    audio.Studios.Add(studio);
+                }
+
+                audio.Studios = audio.Studios
+                    .Where(i => !string.IsNullOrWhiteSpace(i))
+                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .ToList();
+            }
+        }
+
+        /// <summary>
+        /// Gets the genres from the tags collection
+        /// </summary>
+        /// <param name="info">The information.</param>
+        /// <param name="tags">The tags.</param>
+        private void FetchGenres(Model.MediaInfo.MediaInfo info, Dictionary<string, string> tags)
+        {
+            var val = FFProbeHelpers.GetDictionaryValue(tags, "genre");
+
+            if (!string.IsNullOrEmpty(val))
+            {
+                foreach (var genre in Split(val, true))
+                {
+                    info.Genres.Add(genre);
+                }
+
+                info.Genres = info.Genres
+                    .Where(i => !string.IsNullOrWhiteSpace(i))
+                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .ToList();
+            }
+        }
+
+        /// <summary>
+        /// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'
+        /// </summary>
+        /// <param name="tags">The tags.</param>
+        /// <param name="tagName">Name of the tag.</param>
+        /// <returns>System.Nullable{System.Int32}.</returns>
+        private int? GetDictionaryDiscValue(Dictionary<string, string> tags, string tagName)
+        {
+            var disc = FFProbeHelpers.GetDictionaryValue(tags, tagName);
+
+            if (!string.IsNullOrEmpty(disc))
+            {
+                disc = disc.Split('/')[0];
+
+                int num;
+
+                if (int.TryParse(disc, out num))
+                {
+                    return num;
+                }
+            }
+
+            return null;
+        }
+
+        private ChapterInfo GetChapterInfo(MediaChapter chapter)
+        {
+            var info = new ChapterInfo();
+
+            if (chapter.tags != null)
+            {
+                string name;
+                if (chapter.tags.TryGetValue("title", out name))
+                {
+                    info.Name = name;
+                }
+            }
+
+            // Limit accuracy to milliseconds to match xml saving
+            var secondsString = chapter.start_time;
+            double seconds;
+
+            if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out seconds))
+            {
+                var ms = Math.Round(TimeSpan.FromSeconds(seconds).TotalMilliseconds);
+                info.StartPositionTicks = TimeSpan.FromMilliseconds(ms).Ticks;
+            }
+
+            return info;
+        }
+
+        private const int MaxSubtitleDescriptionExtractionLength = 100; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
+
+        private void FetchWtvInfo(Model.MediaInfo.MediaInfo video, InternalMediaInfoResult data)
+        {
+            if (data.format == null || data.format.tags == null)
+            {
+                return;
+            }
+
+            var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/Genre");
+
+            if (!string.IsNullOrWhiteSpace(genres))
+            {
+                //genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "genre");
+            }
+
+            if (!string.IsNullOrWhiteSpace(genres))
+            {
+                video.Genres = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries)
+                    .Where(i => !string.IsNullOrWhiteSpace(i))
+                    .Select(i => i.Trim())
+                    .ToList();
+            }
+
+            var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating");
+
+            if (!string.IsNullOrWhiteSpace(officialRating))
+            {
+                video.OfficialRating = officialRating;
+            }
+
+            var people = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaCredits");
+
+            if (!string.IsNullOrEmpty(people))
+            {
+                video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries)
+                    .Where(i => !string.IsNullOrWhiteSpace(i))
+                    .Select(i => new BaseItemPerson { Name = i.Trim(), Type = PersonType.Actor })
+                    .ToList();
+            }
+
+            var year = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime");
+            if (!string.IsNullOrWhiteSpace(year))
+            {
+                int val;
+
+                if (int.TryParse(year, NumberStyles.Integer, _usCulture, out val))
+                {
+                    video.ProductionYear = val;
+                }
+            }
+
+            var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaOriginalBroadcastDateTime");
+            if (!string.IsNullOrWhiteSpace(premiereDateString))
+            {
+                DateTime val;
+
+                // Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
+                // DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None)
+                if (DateTime.TryParse(year, null, DateTimeStyles.None, out val))
+                {
+                    video.PremiereDate = val.ToUniversalTime();
+                }
+            }
+
+            var description = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription");
+
+            var subTitle = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitle");
+
+            // For below code, credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
+
+            // Sometimes for TV Shows the Subtitle field is empty and the subtitle description contains the subtitle, extract if possible. See ticket https://mcebuddy2x.codeplex.com/workitem/1910
+            // The format is -> EPISODE/TOTAL_EPISODES_IN_SEASON. SUBTITLE: DESCRIPTION
+            // OR -> COMMENT. SUBTITLE: DESCRIPTION
+            // e.g. -> 4/13. The Doctor's Wife: Science fiction drama. When he follows a Time Lord distress signal, the Doctor puts Amy, Rory and his beloved TARDIS in grave danger. Also in HD. [AD,S]
+            // e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
+            if (String.IsNullOrWhiteSpace(subTitle) && !String.IsNullOrWhiteSpace(description) && description.Substring(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).Contains(":")) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
+            {
+                string[] parts = description.Split(':');
+                if (parts.Length > 0)
+                {
+                    string subtitle = parts[0];
+                    try
+                    {
+                        if (subtitle.Contains("/")) // It contains a episode number and season number
+                        {
+                            string[] numbers = subtitle.Split(' ');
+                            video.IndexNumber = int.Parse(numbers[0].Replace(".", "").Split('/')[0]);
+                            int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", "").Split('/')[1]);
+
+                            description = String.Join(" ", numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it
+                        }
+                        else
+                            throw new Exception(); // Switch to default parsing
+                    }
+                    catch // Default parsing
+                    {
+                        if (subtitle.Contains(".")) // skip the comment, keep the subtitle
+                            description = String.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
+                        else
+                            description = subtitle.Trim(); // Clean up whitespaces and save it
+                    }
+                }
+            }
+
+            if (!string.IsNullOrWhiteSpace(description))
+            {
+                video.Overview = description;
+            }
+        }
+
+        private void ExtractTimestamp(Model.MediaInfo.MediaInfo video)
+        {
+            if (video.VideoType == VideoType.VideoFile)
+            {
+                if (string.Equals(video.Container, "mpeg2ts", StringComparison.OrdinalIgnoreCase) ||
+                    string.Equals(video.Container, "m2ts", StringComparison.OrdinalIgnoreCase) ||
+                    string.Equals(video.Container, "ts", StringComparison.OrdinalIgnoreCase))
+                {
+                    try
+                    {
+                        video.Timestamp = GetMpegTimestamp(video.Path);
+
+                        _logger.Debug("Video has {0} timestamp", video.Timestamp);
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.ErrorException("Error extracting timestamp info from {0}", ex, video.Path);
+                        video.Timestamp = null;
+                    }
+                }
+            }
+        }
+
+        private TransportStreamTimestamp GetMpegTimestamp(string path)
+        {
+            var packetBuffer = new byte['Å'];
+
+            using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
+            {
+                fs.Read(packetBuffer, 0, packetBuffer.Length);
+            }
+
+            if (packetBuffer[0] == 71)
+            {
+                return TransportStreamTimestamp.None;
+            }
+
+            if ((packetBuffer[4] == 71) && (packetBuffer['Ä'] == 71))
+            {
+                if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0))
+                {
+                    return TransportStreamTimestamp.Zero;
+                }
+
+                return TransportStreamTimestamp.Valid;
+            }
+
+            return TransportStreamTimestamp.None;
+        }
+
+        private void UpdateFromMediaInfo(MediaSourceInfo video, MediaStream videoStream)
+        {
+            if (video.VideoType == VideoType.VideoFile && video.Protocol == MediaProtocol.File)
+            {
+                if (videoStream != null)
+                {
+                    try
+                    {
+                        var result = new MediaInfoLib().GetVideoInfo(video.Path);
+
+                        videoStream.IsCabac = result.IsCabac ?? videoStream.IsCabac;
+                        videoStream.IsInterlaced = result.IsInterlaced ?? videoStream.IsInterlaced;
+                        videoStream.BitDepth = result.BitDepth ?? videoStream.BitDepth;
+                        videoStream.RefFrames = result.RefFrames;
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.ErrorException("Error running MediaInfo on {0}", ex, video.Path);
+                    }
+                }
+            }
+        }
+    }
+}

+ 0 - 0
MediaBrowser.Providers/MediaInfo/whitelist.txt → MediaBrowser.MediaEncoding/Probing/whitelist.txt


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio