Преглед изворни кода

Merge pull request #2628 from MediaBrowser/dev

Dev
Luke пре 8 година
родитељ
комит
6ee9da3717
30 измењених фајлова са 862 додато и 100 уклоњено
  1. 5 0
      Emby.Common.Implementations/IO/SharpCifsFileSystem.cs
  2. 5 11
      Emby.Drawing.ImageMagick/ImageMagickEncoder.cs
  3. 7 10
      Emby.Drawing.Net/GDIImageEncoder.cs
  4. 6 0
      Emby.Drawing.Skia/Emby.Drawing.Skia.csproj
  5. 31 0
      Emby.Drawing.Skia/PercentPlayedDrawer.cs
  6. 120 0
      Emby.Drawing.Skia/PlayedIndicatorDrawer.cs
  7. 380 0
      Emby.Drawing.Skia/SkiaEncoder.cs
  8. 164 0
      Emby.Drawing.Skia/StripCollageBuilder.cs
  9. 68 0
      Emby.Drawing.Skia/UnplayedCountIndicator.cs
  10. 0 56
      Emby.Drawing/ImageProcessor.cs
  11. 4 1
      Emby.Server.Core/ApplicationHost.cs
  12. 2 2
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  13. 1 1
      Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
  14. 1 1
      Emby.Server.Implementations/Devices/DeviceManager.cs
  15. 2 2
      Emby.Server.Implementations/Devices/DeviceRepository.cs
  16. 2 2
      Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs
  17. 1 0
      Emby.Server.Implementations/Library/LibraryManager.cs
  18. 1 1
      Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
  19. 1 1
      Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs
  20. 1 1
      Emby.Server.Implementations/Library/Validators/GenresValidator.cs
  21. 1 1
      Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
  22. 1 1
      Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
  23. 3 1
      Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs
  24. 0 6
      MediaBrowser.Controller/Drawing/IImageEncoder.cs
  25. 1 0
      MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
  26. 10 0
      MediaBrowser.ServerApplication/ImageEncoderHelper.cs
  27. 6 0
      MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj
  28. 24 1
      MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
  29. 13 0
      MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
  30. 1 1
      SharedVersion.cs

+ 5 - 0
Emby.Common.Implementations/IO/SharpCifsFileSystem.cs

@@ -53,6 +53,11 @@ namespace Emby.Common.Implementations.IO
             if (separator == '/')
             if (separator == '/')
             {
             {
                 result = result.Replace('\\', '/');
                 result = result.Replace('\\', '/');
+
+                if (result.StartsWith("smb:/", StringComparison.OrdinalIgnoreCase) && !result.StartsWith("smb://", StringComparison.OrdinalIgnoreCase))
+                {
+                    result = result.Replace("smb:/", "smb://");
+                }
             }
             }
 
 
             return result;
             return result;

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

@@ -105,17 +105,6 @@ namespace Emby.Drawing.ImageMagick
             }
             }
         }
         }
 
 
-        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)
         public ImageSize GetImageSize(string path)
         {
         {
             CheckDisposed();
             CheckDisposed();
@@ -150,6 +139,11 @@ namespace Emby.Drawing.ImageMagick
             {
             {
                 using (var originalImage = new MagickWand(inputPath))
                 using (var originalImage = new MagickWand(inputPath))
                 {
                 {
+                    if (options.CropWhiteSpace)
+                    {
+                        originalImage.CurrentImage.TrimImage(10);
+                    }
+
                     ScaleImage(originalImage, width, height, options.Blur ?? 0);
                     ScaleImage(originalImage, width, height, options.Blur ?? 0);
 
 
                     if (autoOrient)
                     if (autoOrient)

+ 7 - 10
Emby.Drawing.Net/GDIImageEncoder.cs

@@ -75,27 +75,24 @@ namespace Emby.Drawing.Net
             }
             }
         }
         }
 
 
-        public void CropWhiteSpace(string inputPath, string outputPath)
+        private Image GetImage(string path, bool cropWhitespace)
         {
         {
-            using (var image = (Bitmap)Image.FromFile(inputPath))
+            if (cropWhitespace)
             {
             {
-                using (var croppedImage = image.CropWhitespace())
+                using (var originalImage = (Bitmap)Image.FromFile(path))
                 {
                 {
-                    _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(outputPath));
-
-                    using (var outputStream = _fileSystem.GetFileStream(outputPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, false))
-                    {
-                        croppedImage.Save(System.Drawing.Imaging.ImageFormat.Png, outputStream, 100);
-                    }
+                    return originalImage.CropWhitespace();
                 }
                 }
             }
             }
+
+            return Image.FromFile(path);
         }
         }
 
 
         public void EncodeImage(string inputPath, string cacheFilePath, bool autoOrient, int width, int height, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
         public void EncodeImage(string inputPath, string cacheFilePath, bool autoOrient, int width, int height, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
         {
         {
             var hasPostProcessing = !string.IsNullOrEmpty(options.BackgroundColor) || options.UnplayedCount.HasValue || options.AddPlayedIndicator || options.PercentPlayed > 0;
             var hasPostProcessing = !string.IsNullOrEmpty(options.BackgroundColor) || options.UnplayedCount.HasValue || options.AddPlayedIndicator || options.PercentPlayed > 0;
 
 
-            using (var originalImage = Image.FromFile(inputPath))
+            using (var originalImage = GetImage(inputPath, options.CropWhiteSpace))
             {
             {
                 var newWidth = Convert.ToInt32(width);
                 var newWidth = Convert.ToInt32(width);
                 var newHeight = Convert.ToInt32(height);
                 var newHeight = Convert.ToInt32(height);

+ 6 - 0
Emby.Drawing.Skia/Emby.Drawing.Skia.csproj

@@ -52,7 +52,12 @@
     <Compile Include="..\SharedVersion.cs">
     <Compile Include="..\SharedVersion.cs">
       <Link>Properties\SharedVersion.cs</Link>
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
     </Compile>
+    <Compile Include="PercentPlayedDrawer.cs" />
+    <Compile Include="PlayedIndicatorDrawer.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="SkiaEncoder.cs" />
+    <Compile Include="StripCollageBuilder.cs" />
+    <Compile Include="UnplayedCountIndicator.cs" />
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>
     <Reference Include="SkiaSharp, Version=1.57.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
     <Reference Include="SkiaSharp, Version=1.57.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
@@ -61,6 +66,7 @@
     </Reference>
     </Reference>
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>
+    <EmbeddedResource Include="fonts\robotoregular.ttf" />
     <None Include="packages.config" />
     <None Include="packages.config" />
   </ItemGroup>
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />

+ 31 - 0
Emby.Drawing.Skia/PercentPlayedDrawer.cs

@@ -0,0 +1,31 @@
+using SkiaSharp;
+using MediaBrowser.Model.Drawing;
+using System;
+
+namespace Emby.Drawing.Skia
+{
+    public class PercentPlayedDrawer
+    {
+        private const int IndicatorHeight = 8;
+
+        public void Process(SKCanvas canvas, ImageSize imageSize, double percent)
+        {
+            using (var paint = new SKPaint())
+            {
+                var endX = imageSize.Width - 1;
+                var endY = imageSize.Height - 1;
+
+                paint.Color = SKColor.Parse("#99000000");
+                paint.Style = SKPaintStyle.Fill;
+                canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, (float)endX, (float)endY), paint);
+
+                double foregroundWidth = endX;
+                foregroundWidth *= percent;
+                foregroundWidth /= 100;
+
+                paint.Color = SKColor.Parse("#FF52B54B");
+                canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(Math.Round(foregroundWidth)), (float)endY), paint);
+            }
+        }
+    }
+}

+ 120 - 0
Emby.Drawing.Skia/PlayedIndicatorDrawer.cs

@@ -0,0 +1,120 @@
+using SkiaSharp;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Drawing;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+using System.Reflection;
+
+namespace Emby.Drawing.Skia
+{
+    public class PlayedIndicatorDrawer
+    {
+        private const int FontSize = 42;
+        private const int OffsetFromTopRightCorner = 38;
+
+        private readonly IApplicationPaths _appPaths;
+        private readonly IHttpClient _iHttpClient;
+        private readonly IFileSystem _fileSystem;
+
+        public PlayedIndicatorDrawer(IApplicationPaths appPaths, IHttpClient iHttpClient, IFileSystem fileSystem)
+        {
+            _appPaths = appPaths;
+            _iHttpClient = iHttpClient;
+            _fileSystem = fileSystem;
+        }
+
+        public async Task DrawPlayedIndicator(SKCanvas canvas, ImageSize imageSize)
+        {
+            var x = imageSize.Width - OffsetFromTopRightCorner;
+
+            using (var paint = new SKPaint())
+            {
+                paint.Color = SKColor.Parse("#CC52B54B");
+                paint.Style = SKPaintStyle.Fill;
+                canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
+            }
+
+            using (var paint = new SKPaint())
+            {
+                paint.Color = new SKColor(255, 255, 255, 255);
+                paint.Style = SKPaintStyle.Fill;
+                paint.Typeface = SKTypeface.FromFile(await DownloadFont("webdings.ttf", "https://github.com/MediaBrowser/Emby.Resources/raw/master/fonts/webdings.ttf",
+                    _appPaths, _iHttpClient, _fileSystem).ConfigureAwait(false));
+                paint.TextSize = FontSize;
+                paint.IsAntialias = true;
+
+                canvas.DrawText("a", (float)x-20, OffsetFromTopRightCorner + 12, paint);
+            }
+        }
+
+        internal static string ExtractFont(string name, IApplicationPaths paths, IFileSystem fileSystem)
+        {
+            var filePath = Path.Combine(paths.ProgramDataPath, "fonts", name);
+
+            if (fileSystem.FileExists(filePath))
+            {
+                return filePath;
+            }
+
+            var namespacePath = typeof(PlayedIndicatorDrawer).Namespace + ".fonts." + name;
+            var tempPath = Path.Combine(paths.TempDirectory, Guid.NewGuid().ToString("N") + ".ttf");
+            fileSystem.CreateDirectory(fileSystem.GetDirectoryName(tempPath));
+
+            using (var stream = typeof(PlayedIndicatorDrawer).GetTypeInfo().Assembly.GetManifestResourceStream(namespacePath))
+            {
+                using (var fileStream = fileSystem.GetFileStream(tempPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+                {
+                    stream.CopyTo(fileStream);
+                }
+            }
+
+            fileSystem.CreateDirectory(fileSystem.GetDirectoryName(filePath));
+
+            try
+            {
+                fileSystem.CopyFile(tempPath, filePath, false);
+            }
+            catch (IOException)
+            {
+
+            }
+
+            return tempPath;
+        }
+
+        internal static async Task<string> DownloadFont(string name, string url, IApplicationPaths paths, IHttpClient httpClient, IFileSystem fileSystem)
+        {
+            var filePath = Path.Combine(paths.ProgramDataPath, "fonts", name);
+
+            if (fileSystem.FileExists(filePath))
+            {
+                return filePath;
+            }
+
+            var tempPath = await httpClient.GetTempFile(new HttpRequestOptions
+            {
+                Url = url,
+                Progress = new Progress<double>()
+
+            }).ConfigureAwait(false);
+
+            fileSystem.CreateDirectory(fileSystem.GetDirectoryName(filePath));
+
+            try
+            {
+                fileSystem.CopyFile(tempPath, filePath, false);
+            }
+            catch (IOException)
+            {
+
+            }
+
+            return tempPath;
+        }
+    }
+}

+ 380 - 0
Emby.Drawing.Skia/SkiaEncoder.cs

@@ -0,0 +1,380 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using SkiaSharp;
+using System;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace Emby.Drawing.Skia
+{
+    public class SkiaEncoder : IImageEncoder
+    {
+        private readonly ILogger _logger;
+        private readonly IApplicationPaths _appPaths;
+        private readonly Func<IHttpClient> _httpClientFactory;
+        private readonly IFileSystem _fileSystem;
+
+        public SkiaEncoder(ILogger logger, IApplicationPaths appPaths, Func<IHttpClient> httpClientFactory, IFileSystem fileSystem)
+        {
+            _logger = logger;
+            _appPaths = appPaths;
+            _httpClientFactory = httpClientFactory;
+            _fileSystem = fileSystem;
+
+            LogVersion();
+        }
+
+        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[]
+                {
+                    "jpeg",
+                    "jpg",
+                    "png",
+                    "dng",
+                    "webp",
+                    "gif",
+                    "bmp",
+                    "ico",
+                    "astc",
+                    "ktx",
+                    "pkm",
+                    "wbmp"
+                };
+            }
+        }
+
+        public ImageFormat[] SupportedOutputFormats
+        {
+            get
+            {
+                return new[] { ImageFormat.Webp, ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png, ImageFormat.Bmp };
+            }
+        }
+
+        private void LogVersion()
+        {
+            _logger.Info("SkiaSharp version: " + GetVersion());
+        }
+
+        public static string GetVersion()
+        {
+            using (var bitmap = new SKBitmap())
+            {
+                return typeof(SKBitmap).GetTypeInfo().Assembly.GetName().Version.ToString();
+            }
+        }
+
+        private static bool IsWhiteSpace(SKColor color)
+        {
+            return (color.Red == 255 && color.Green == 255 && color.Blue == 255) || color.Alpha == 0;
+        }
+
+        public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
+        {
+            switch (selectedFormat)
+            {
+                case ImageFormat.Bmp:
+                    return SKEncodedImageFormat.Bmp;
+                case ImageFormat.Jpg:
+                    return SKEncodedImageFormat.Jpeg;
+                case ImageFormat.Gif:
+                    return SKEncodedImageFormat.Gif;
+                case ImageFormat.Webp:
+                    return SKEncodedImageFormat.Webp;
+                default:
+                    return SKEncodedImageFormat.Png;
+            }
+        }
+
+        private static bool IsAllWhiteRow(SKBitmap bmp, int row)
+        {
+            for (var i = 0; i < bmp.Width; ++i)
+            {
+                if (!IsWhiteSpace(bmp.GetPixel(i, row)))
+                {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        private static bool IsAllWhiteColumn(SKBitmap bmp, int col)
+        {
+            for (var i = 0; i < bmp.Height; ++i)
+            {
+                if (!IsWhiteSpace(bmp.GetPixel(col, i)))
+                {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        private SKBitmap CropWhiteSpace(SKBitmap bitmap)
+        {
+            CheckDisposed();
+
+            var topmost = 0;
+            for (int row = 0; row < bitmap.Height; ++row)
+            {
+                if (IsAllWhiteRow(bitmap, row))
+                    topmost = row;
+                else break;
+            }
+
+            int bottommost = 0;
+            for (int row = bitmap.Height - 1; row >= 0; --row)
+            {
+                if (IsAllWhiteRow(bitmap, row))
+                    bottommost = row;
+                else break;
+            }
+
+            int leftmost = 0, rightmost = 0;
+            for (int col = 0; col < bitmap.Width; ++col)
+            {
+                if (IsAllWhiteColumn(bitmap, col))
+                    leftmost = col;
+                else
+                    break;
+            }
+
+            for (int col = bitmap.Width - 1; col >= 0; --col)
+            {
+                if (IsAllWhiteColumn(bitmap, col))
+                    rightmost = col;
+                else
+                    break;
+            }
+
+            var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost);
+
+            using (var image = SKImage.FromBitmap(bitmap))
+            {
+                using (var subset = image.Subset(newRect))
+                {
+                    return SKBitmap.FromImage(subset);
+                    //using (var data = subset.Encode(StripCollageBuilder.GetEncodedFormat(outputPath), 90))
+                    //{
+                    //    using (var fileStream = _fileSystem.GetFileStream(outputPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+                    //    {
+                    //        data.AsStream().CopyTo(fileStream);
+                    //    }
+                    //}
+                }
+            }
+        }
+
+        public ImageSize GetImageSize(string path)
+        {
+            CheckDisposed();
+
+            using (var s = new SKFileStream(path))
+            {
+                using (var codec = SKCodec.Create(s))
+                {
+                    var info = codec.Info;
+
+                    return new ImageSize
+                    {
+                        Width = info.Width,
+                        Height = info.Height
+                    };
+                }
+            }
+        }
+
+        private SKBitmap GetBitmap(string path, bool cropWhitespace)
+        {
+            if (cropWhitespace)
+            {
+                using (var bitmap = SKBitmap.Decode(path))
+                {
+                    return CropWhiteSpace(bitmap);
+                }
+            } 
+
+            return SKBitmap.Decode(path);
+        }
+
+        public void EncodeImage(string inputPath, string outputPath, bool autoOrient, int width, int height, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
+        {
+            if (string.IsNullOrWhiteSpace(inputPath))
+            {
+                throw new ArgumentNullException("inputPath");
+            }
+            if (string.IsNullOrWhiteSpace(inputPath))
+            {
+                throw new ArgumentNullException("outputPath");
+            }
+
+            var skiaOutputFormat = GetImageFormat(selectedOutputFormat);
+
+            var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
+            var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
+            var blur = options.Blur ?? 0;
+            var hasIndicator = !options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0);
+
+            using (var bitmap = GetBitmap(inputPath, options.CropWhiteSpace))
+            {
+                using (var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType))
+                {
+                    // scale image
+                    var resizeMethod = options.Image.Type == MediaBrowser.Model.Entities.ImageType.Logo ||
+                                       options.Image.Type == MediaBrowser.Model.Entities.ImageType.Art
+                        ? SKBitmapResizeMethod.Lanczos3
+                        : SKBitmapResizeMethod.Lanczos3;
+
+                    bitmap.Resize(resizedBitmap, resizeMethod);
+
+                    // If all we're doing is resizing then we can stop now
+                    if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
+                    {
+                        using (var outputStream = new SKFileWStream(outputPath))
+                        {
+                            resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
+                            return;
+                        }
+                    }
+
+                    // create bitmap to use for canvas drawing
+                    using (var saveBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType))
+                    {
+                        // create canvas used to draw into bitmap
+                        using (var canvas = new SKCanvas(saveBitmap))
+                        {
+                            // set background color if present
+                            if (hasBackgroundColor)
+                            {
+                                canvas.Clear(SKColor.Parse(options.BackgroundColor));
+                            }
+
+                            // Add blur if option is present
+                            if (blur > 0)
+                            {
+                                using (var paint = new SKPaint())
+                                {
+                                    // create image from resized bitmap to apply blur
+                                    using (var filter = SKImageFilter.CreateBlur(5, 5))
+                                    {
+                                        paint.ImageFilter = filter;
+                                        canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
+                                    }
+                                }
+                            }
+                            else
+                            {
+                                // draw resized bitmap onto canvas
+                                canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
+                            }
+
+                            // If foreground layer present then draw
+                            if (hasForegroundColor)
+                            {
+                                Double opacity;
+                                if (!Double.TryParse(options.ForegroundLayer, out opacity)) opacity = .4;
+
+                                var foregroundColor = String.Format("#{0:X2}000000", (Byte)((1 - opacity) * 0xFF));
+                                canvas.DrawColor(SKColor.Parse(foregroundColor), SKBlendMode.SrcOver);
+                            }
+
+                            if (hasIndicator)
+                            {
+                                DrawIndicator(canvas, width, height, options);
+                            }
+
+                            using (var outputStream = new SKFileWStream(outputPath))
+                            {
+                                saveBitmap.Encode(outputStream, skiaOutputFormat, quality);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        public void CreateImageCollage(ImageCollageOptions options)
+        {
+            double ratio = options.Width;
+            ratio /= options.Height;
+
+            if (ratio >= 1.4)
+            {
+                new StripCollageBuilder(_appPaths, _fileSystem).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
+            }
+            else if (ratio >= .9)
+            {
+                new StripCollageBuilder(_appPaths, _fileSystem).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
+            }
+            else
+            {
+                // @todo create Poster collage capability
+                new StripCollageBuilder(_appPaths, _fileSystem).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
+            }
+        }
+
+        private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
+        {
+            try
+            {
+                var currentImageSize = new ImageSize(imageWidth, imageHeight);
+
+                if (options.AddPlayedIndicator)
+                {
+                    var task = new PlayedIndicatorDrawer(_appPaths, _httpClientFactory(), _fileSystem).DrawPlayedIndicator(canvas, currentImageSize);
+                    Task.WaitAll(task);
+                }
+                else if (options.UnplayedCount.HasValue)
+                {
+                    new UnplayedCountIndicator(_appPaths, _httpClientFactory(), _fileSystem).DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value);
+                }
+
+                if (options.PercentPlayed > 0)
+                {
+                    new PercentPlayedDrawer().Process(canvas, currentImageSize, options.PercentPlayed);
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error drawing indicator overlay", ex);
+            }
+        }
+
+        public string Name
+        {
+            get { return "Skia"; }
+        }
+
+        private bool _disposed;
+        public void Dispose()
+        {
+            _disposed = true;
+        }
+
+        private void CheckDisposed()
+        {
+            if (_disposed)
+            {
+                throw new ObjectDisposedException(GetType().Name);
+            }
+        }
+
+        public bool SupportsImageCollageCreation
+        {
+            get { return true; }
+        }
+
+        public bool SupportsImageEncoding
+        {
+            get { return true; }
+        }
+    }
+}

+ 164 - 0
Emby.Drawing.Skia/StripCollageBuilder.cs

@@ -0,0 +1,164 @@
+using SkiaSharp;
+using MediaBrowser.Common.Configuration;
+using System;
+using System.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Drawing.Skia
+{
+    public class StripCollageBuilder
+    {
+        private readonly IApplicationPaths _appPaths;
+        private readonly IFileSystem _fileSystem;
+
+        public StripCollageBuilder(IApplicationPaths appPaths, IFileSystem fileSystem)
+        {
+            _appPaths = appPaths;
+            _fileSystem = fileSystem;
+        }
+
+        public static SKEncodedImageFormat GetEncodedFormat(string outputPath)
+        {
+            var ext = Path.GetExtension(outputPath).ToLower();
+
+            if (ext == ".jpg" || ext == ".jpeg")
+                return SKEncodedImageFormat.Jpeg;
+
+            if (ext == ".webp")
+                return SKEncodedImageFormat.Webp;
+
+            if (ext == ".gif")
+                return SKEncodedImageFormat.Gif;
+
+            if (ext == ".bmp")
+                return SKEncodedImageFormat.Bmp;
+
+            // default to png
+            return SKEncodedImageFormat.Png;
+        }
+
+        public void BuildPosterCollage(string[] paths, string outputPath, int width, int height)
+        {
+            // @todo
+        }
+
+        public void BuildSquareCollage(string[] paths, string outputPath, int width, int height)
+        {
+            using (var bitmap = BuildSquareCollageBitmap(paths, width, height))
+            {
+                using (var outputStream = new SKFileWStream(outputPath))
+                {
+                    bitmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
+                }
+            }
+        }
+
+        public void BuildThumbCollage(string[] paths, string outputPath, int width, int height)
+        {
+            using (var bitmap = BuildThumbCollageBitmap(paths, width, height))
+            {
+                using (var outputStream = new SKFileWStream(outputPath))
+                {
+                    bitmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
+                }
+            }
+        }
+
+        private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height)
+        {
+            var bitmap = new SKBitmap(width, height);
+
+            using (var canvas = new SKCanvas(bitmap))
+            {
+                canvas.Clear(SKColors.Black);
+
+                var iSlice = Convert.ToInt32(width * 0.24125);
+                int iTrans = Convert.ToInt32(height * .25);
+                int iHeight = Convert.ToInt32(height * .70);
+                var horizontalImagePadding = Convert.ToInt32(width * 0.0125);
+                var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
+                int imageIndex = 0;
+
+                for (int i = 0; i < 4; i++)
+                {
+                    using (var currentBitmap = SKBitmap.Decode(paths[imageIndex]))
+                    {
+                        int iWidth = (int)Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
+                        using (var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
+                        {
+                            currentBitmap.Resize(resizeBitmap, SKBitmapResizeMethod.Lanczos3);
+                            int ix = (int)Math.Abs((iWidth - iSlice) / 2);
+                            using (var image = SKImage.FromBitmap(resizeBitmap))
+                            {
+                                using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)))
+                                {
+                                    canvas.DrawImage(subset, (horizontalImagePadding * (i + 1)) + (iSlice * i), 0);
+
+                                    using (var croppedBitmap = SKBitmap.FromImage(subset))
+                                    {
+                                        using (var flipped = new SKBitmap(croppedBitmap.Width, croppedBitmap.Height / 2, croppedBitmap.ColorType, croppedBitmap.AlphaType))
+                                        {
+                                            croppedBitmap.Resize(flipped, SKBitmapResizeMethod.Lanczos3);
+
+                                            using (var gradient = new SKPaint())
+                                            {
+                                                var matrix = SKMatrix.MakeScale(1, -1);
+                                                matrix.SetScaleTranslate(1, -1, 0, flipped.Height);
+                                                gradient.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0), new SKPoint(0, flipped.Height), new[] { new SKColor(0, 0, 0, 0), SKColors.Black }, null, SKShaderTileMode.Clamp, matrix);
+                                                canvas.DrawBitmap(flipped, (horizontalImagePadding * (i + 1)) + (iSlice * i), iHeight + verticalSpacing, gradient);
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+
+                    imageIndex++;
+
+                    if (imageIndex >= paths.Length)
+                        imageIndex = 0;
+                }
+            }
+
+            return bitmap;
+        }
+
+        private SKBitmap BuildSquareCollageBitmap(string[] paths, int width, int height)
+        {
+            var bitmap = new SKBitmap(width, height);
+            var imageIndex = 0;
+            var cellWidth = width / 2;
+            var cellHeight = height / 2;
+
+            using (var canvas = new SKCanvas(bitmap))
+            {
+                for (var x = 0; x < 2; x++)
+                {
+                    for (var y = 0; y < 2; y++)
+                    {
+                        using (var currentBitmap = SKBitmap.Decode(paths[imageIndex]))
+                        {
+                            using (var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
+                            {
+                                // scale image
+                                currentBitmap.Resize(resizedBitmap, SKBitmapResizeMethod.Lanczos3);
+
+                                // draw this image into the strip at the next position
+                                var xPos = x * cellWidth;
+                                var yPos = y * cellHeight;
+                                canvas.DrawBitmap(resizedBitmap, xPos, yPos);
+                            }
+                        }
+                        imageIndex++;
+
+                        if (imageIndex >= paths.Length)
+                            imageIndex = 0;
+                    }
+                }
+            }
+
+            return bitmap;
+        }
+    }
+}

+ 68 - 0
Emby.Drawing.Skia/UnplayedCountIndicator.cs

@@ -0,0 +1,68 @@
+using SkiaSharp;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Drawing;
+using System.Globalization;
+using System.Threading.Tasks;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Drawing.Skia
+{
+    public class UnplayedCountIndicator
+    {
+        private const int OffsetFromTopRightCorner = 38;
+
+        private readonly IApplicationPaths _appPaths;
+        private readonly IHttpClient _iHttpClient;
+        private readonly IFileSystem _fileSystem;
+
+        public UnplayedCountIndicator(IApplicationPaths appPaths, IHttpClient iHttpClient, IFileSystem fileSystem)
+        {
+            _appPaths = appPaths;
+            _iHttpClient = iHttpClient;
+            _fileSystem = fileSystem;
+        }
+
+        public void DrawUnplayedCountIndicator(SKCanvas canvas, ImageSize imageSize, int count)
+        {
+            var x = imageSize.Width - OffsetFromTopRightCorner;
+            var text = count.ToString(CultureInfo.InvariantCulture);
+
+            using (var paint = new SKPaint())
+            {
+                paint.Color = SKColor.Parse("#CC52B54B");
+                paint.Style = SKPaintStyle.Fill;
+                canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
+            }
+            using (var paint = new SKPaint())
+            {
+                paint.Color = new SKColor(255, 255, 255, 255);
+                paint.Style = SKPaintStyle.Fill;
+                paint.Typeface = SKTypeface.FromFile(PlayedIndicatorDrawer.ExtractFont("robotoregular.ttf", _appPaths, _fileSystem));
+                paint.TextSize = 24;
+                paint.IsAntialias = true;
+
+                var y = OffsetFromTopRightCorner + 9;
+
+                if (text.Length == 1)
+                {
+                    x -= 7;
+                }
+                if (text.Length == 2)
+                {
+                    x -= 13;
+                }
+                else if (text.Length >= 3)
+                {
+                    x -= 15;
+                    y -= 2;
+                    paint.TextSize = 18;
+                }
+
+                canvas.DrawText(text, (float)x, y, paint);
+            }
+        }
+    }
+}

+ 0 - 56
Emby.Drawing/ImageProcessor.cs

@@ -136,14 +136,6 @@ namespace Emby.Drawing
             }
             }
         }
         }
 
 
-        private string CroppedWhitespaceImageCachePath
-        {
-            get
-            {
-                return Path.Combine(_appPaths.ImageCachePath, "cropped-images");
-            }
-        }
-
         public void AddParts(IEnumerable<IImageEnhancer> enhancers)
         public void AddParts(IEnumerable<IImageEnhancer> enhancers)
         {
         {
             ImageEnhancers = enhancers.ToArray();
             ImageEnhancers = enhancers.ToArray();
@@ -186,14 +178,6 @@ namespace Emby.Drawing
                 return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
                 return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
             }
             }
 
 
-            if (options.CropWhiteSpace && _imageEncoder.SupportsImageEncoding)
-            {
-                var tuple = await GetWhitespaceCroppedImage(originalImagePath, dateModified).ConfigureAwait(false);
-
-                originalImagePath = tuple.Item1;
-                dateModified = tuple.Item2;
-            }
-
             if (options.Enhancers.Count > 0)
             if (options.Enhancers.Count > 0)
             {
             {
                 var tuple = await GetEnhancedImage(new ItemImageInfo
                 var tuple = await GetEnhancedImage(new ItemImageInfo
@@ -400,46 +384,6 @@ namespace Emby.Drawing
             return requestedFormat;
             return requestedFormat;
         }
         }
 
 
-        /// <summary>
-        /// Crops whitespace from an image, caches the result, and returns the cached path
-        /// </summary>
-        private async Task<Tuple<string, DateTime>> GetWhitespaceCroppedImage(string originalImagePath, DateTime dateModified)
-        {
-            var name = originalImagePath;
-            name += "datemodified=" + dateModified.Ticks;
-
-            var croppedImagePath = GetCachePath(CroppedWhitespaceImageCachePath, name, Path.GetExtension(originalImagePath));
-
-            // Check again in case of contention
-            if (_fileSystem.FileExists(croppedImagePath))
-            {
-                return GetResult(croppedImagePath);
-            }
-
-            try
-            {
-                _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(croppedImagePath));
-                var tmpPath = Path.ChangeExtension(Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N")), Path.GetExtension(croppedImagePath));
-                _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(tmpPath));
-
-                _imageEncoder.CropWhiteSpace(originalImagePath, tmpPath);
-                CopyFile(tmpPath, croppedImagePath);
-                return GetResult(tmpPath);
-            }
-            catch (NotImplementedException)
-            {
-                // No need to spam the log with an error message
-                return new Tuple<string, DateTime>(originalImagePath, dateModified);
-            }
-            catch (Exception ex)
-            {
-                // We have to have a catch-all here because some of the .net image methods throw a plain old Exception
-                _logger.ErrorException("Error cropping image {0}", ex, originalImagePath);
-
-                return new Tuple<string, DateTime>(originalImagePath, dateModified);
-            }
-        }
-
         private Tuple<string, DateTime> GetResult(string path)
         private Tuple<string, DateTime> GetResult(string path)
         {
         {
             return new Tuple<string, DateTime>(path, _fileSystem.GetLastWriteTimeUtc(path));
             return new Tuple<string, DateTime>(path, _fileSystem.GetLastWriteTimeUtc(path));

+ 4 - 1
Emby.Server.Core/ApplicationHost.cs

@@ -761,7 +761,10 @@ namespace Emby.Server.Core
                     return null;
                     return null;
                 }
                 }
 
 
-                X509Certificate2 localCert = new X509Certificate2(certificateLocation, info.Password);
+                // Don't use an empty string password
+                var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
+
+                X509Certificate2 localCert = new X509Certificate2(certificateLocation, password);
                 //localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
                 //localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
                 if (!localCert.HasPrivateKey)
                 if (!localCert.HasPrivateKey)
                 {
                 {

+ 2 - 2
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.AppBase
             Logger.Info("Saving system configuration");
             Logger.Info("Saving system configuration");
             var path = CommonApplicationPaths.SystemConfigurationFilePath;
             var path = CommonApplicationPaths.SystemConfigurationFilePath;
 
 
-            FileSystem.CreateDirectory(Path.GetDirectoryName(path));
+            FileSystem.CreateDirectory(FileSystem.GetDirectoryName(path));
 
 
             lock (_configurationSyncLock)
             lock (_configurationSyncLock)
             {
             {
@@ -293,7 +293,7 @@ namespace Emby.Server.Implementations.AppBase
             _configurations.AddOrUpdate(key, configuration, (k, v) => configuration);
             _configurations.AddOrUpdate(key, configuration, (k, v) => configuration);
 
 
             var path = GetConfigurationFile(key);
             var path = GetConfigurationFile(key);
-            FileSystem.CreateDirectory(Path.GetDirectoryName(path));
+            FileSystem.CreateDirectory(FileSystem.GetDirectoryName(path));
 
 
             lock (_configurationSyncLock)
             lock (_configurationSyncLock)
             {
             {

+ 1 - 1
Emby.Server.Implementations/AppBase/ConfigurationHelper.cs

@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.AppBase
                 // If the file didn't exist before, or if something has changed, re-save
                 // If the file didn't exist before, or if something has changed, re-save
                 if (buffer == null || !buffer.SequenceEqual(newBytes))
                 if (buffer == null || !buffer.SequenceEqual(newBytes))
                 {
                 {
-                    fileSystem.CreateDirectory(Path.GetDirectoryName(path));
+                    fileSystem.CreateDirectory(fileSystem.GetDirectoryName(path));
 
 
                     // Save it after load in case we got new items
                     // Save it after load in case we got new items
                     fileSystem.WriteAllBytes(path, newBytes);
                     fileSystem.WriteAllBytes(path, newBytes);

+ 1 - 1
Emby.Server.Implementations/Devices/DeviceManager.cs

@@ -158,7 +158,7 @@ namespace Emby.Server.Implementations.Devices
 
 
             _libraryMonitor.ReportFileSystemChangeBeginning(path);
             _libraryMonitor.ReportFileSystemChangeBeginning(path);
 
 
-            _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
+            _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path));
 
 
             try
             try
             {
             {

+ 2 - 2
Emby.Server.Implementations/Devices/DeviceRepository.cs

@@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.Devices
         public Task SaveDevice(DeviceInfo device)
         public Task SaveDevice(DeviceInfo device)
         {
         {
             var path = Path.Combine(GetDevicePath(device.Id), "device.json");
             var path = Path.Combine(GetDevicePath(device.Id), "device.json");
-            _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
+            _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path));
 
 
             lock (_syncLock)
             lock (_syncLock)
             {
             {
@@ -180,7 +180,7 @@ namespace Emby.Server.Implementations.Devices
         public void AddCameraUpload(string deviceId, LocalFileInfo file)
         public void AddCameraUpload(string deviceId, LocalFileInfo file)
         {
         {
             var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json");
             var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json");
-            _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
+            _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path));
 
 
             lock (_syncLock)
             lock (_syncLock)
             {
             {

+ 2 - 2
Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs

@@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.FFMpeg
                 else
                 else
                 {
                 {
                     info = existingVersion;
                     info = existingVersion;
-                    versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath);
+                    versionedDirectoryPath = _fileSystem.GetDirectoryName(info.EncoderPath);
                     excludeFromDeletions.Add(versionedDirectoryPath);
                     excludeFromDeletions.Add(versionedDirectoryPath);
                 }
                 }
             }
             }
@@ -135,7 +135,7 @@ namespace Emby.Server.Implementations.FFMpeg
                     {
                     {
                         EncoderPath = encoder,
                         EncoderPath = encoder,
                         ProbePath = probe,
                         ProbePath = probe,
-                        Version = Path.GetFileName(Path.GetDirectoryName(probe))
+                        Version = Path.GetFileName(_fileSystem.GetDirectoryName(probe))
                     };
                     };
                 }
                 }
             }
             }

+ 1 - 0
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -1197,6 +1197,7 @@ namespace Emby.Server.Implementations.Library
                 catch (OperationCanceledException)
                 catch (OperationCanceledException)
                 {
                 {
                     _logger.Info("Post-scan task cancelled: {0}", task.GetType().Name);
                     _logger.Info("Post-scan task cancelled: {0}", task.GetType().Name);
+                    throw;
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {

+ 1 - 1
Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs

@@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Validators
                 catch (OperationCanceledException)
                 catch (OperationCanceledException)
                 {
                 {
                     // Don't clutter the log
                     // Don't clutter the log
-                    break;
+                    throw;
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {

+ 1 - 1
Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs

@@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Validators
                 catch (OperationCanceledException)
                 catch (OperationCanceledException)
                 {
                 {
                     // Don't clutter the log
                     // Don't clutter the log
-                    break;
+                    throw;
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {

+ 1 - 1
Emby.Server.Implementations/Library/Validators/GenresValidator.cs

@@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Validators
                 catch (OperationCanceledException)
                 catch (OperationCanceledException)
                 {
                 {
                     // Don't clutter the log
                     // Don't clutter the log
-                    break;
+                    throw;
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {

+ 1 - 1
Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs

@@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Validators
                 catch (OperationCanceledException)
                 catch (OperationCanceledException)
                 {
                 {
                     // Don't clutter the log
                     // Don't clutter the log
-                    break;
+                    throw;
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {

+ 1 - 1
Emby.Server.Implementations/Library/Validators/StudiosValidator.cs

@@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Validators
                 catch (OperationCanceledException)
                 catch (OperationCanceledException)
                 {
                 {
                     // Don't clutter the log
                     // Don't clutter the log
-                    break;
+                    throw;
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {

+ 3 - 1
Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs

@@ -26,6 +26,8 @@ namespace Emby.Server.Implementations.Library.Validators
 
 
             while (yearNumber < maxYear)
             while (yearNumber < maxYear)
             {
             {
+                cancellationToken.ThrowIfCancellationRequested();
+
                 try
                 try
                 {
                 {
                     var year = _libraryManager.GetYear(yearNumber);
                     var year = _libraryManager.GetYear(yearNumber);
@@ -35,7 +37,7 @@ namespace Emby.Server.Implementations.Library.Validators
                 catch (OperationCanceledException)
                 catch (OperationCanceledException)
                 {
                 {
                     // Don't clutter the log
                     // Don't clutter the log
-                    break;
+                    throw;
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {

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

@@ -16,12 +16,6 @@ namespace MediaBrowser.Controller.Drawing
         /// <value>The supported output formats.</value>
         /// <value>The supported output formats.</value>
         ImageFormat[] SupportedOutputFormats { get; }
         ImageFormat[] SupportedOutputFormats { get; }
         /// <summary>
         /// <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.
         /// Encodes the image.
         /// </summary>
         /// </summary>
         /// <param name="inputPath">The input path.</param>
         /// <param name="inputPath">The input path.</param>

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

@@ -86,6 +86,7 @@ namespace MediaBrowser.Controller.Drawing
                 PercentPlayed.Equals(0) &&
                 PercentPlayed.Equals(0) &&
                 !UnplayedCount.HasValue &&
                 !UnplayedCount.HasValue &&
                 !Blur.HasValue &&
                 !Blur.HasValue &&
+                !CropWhiteSpace &&
                 string.IsNullOrEmpty(BackgroundColor) &&
                 string.IsNullOrEmpty(BackgroundColor) &&
                 string.IsNullOrEmpty(ForegroundLayer);
                 string.IsNullOrEmpty(ForegroundLayer);
         }
         }

+ 10 - 0
MediaBrowser.ServerApplication/ImageEncoderHelper.cs

@@ -2,6 +2,7 @@
 using Emby.Drawing;
 using Emby.Drawing;
 using Emby.Drawing.Net;
 using Emby.Drawing.Net;
 using Emby.Drawing.ImageMagick;
 using Emby.Drawing.ImageMagick;
+using Emby.Drawing.Skia;
 using Emby.Server.Core;
 using Emby.Server.Core;
 using Emby.Server.Implementations;
 using Emby.Server.Implementations;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
@@ -23,6 +24,15 @@ namespace MediaBrowser.Server.Startup.Common
         {
         {
             if (!startupOptions.ContainsOption("-enablegdi"))
             if (!startupOptions.ContainsOption("-enablegdi"))
             {
             {
+                try
+                {
+                    //return new SkiaEncoder(logManager.GetLogger("ImageMagick"), appPaths, httpClient, fileSystem);
+                }
+                catch
+                {
+                    logger.Error("Error loading ImageMagick. Will revert to GDI.");
+                }
+
                 try
                 try
                 {
                 {
                     return new ImageMagickEncoder(logManager.GetLogger("ImageMagick"), appPaths, httpClient, fileSystem);
                     return new ImageMagickEncoder(logManager.GetLogger("ImageMagick"), appPaths, httpClient, fileSystem);

+ 6 - 0
MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj

@@ -191,9 +191,11 @@
   <ItemGroup>
   <ItemGroup>
     <Content Include="..\packages\SkiaSharp.1.57.1\runtimes\win7-x64\native\libSkiaSharp.dll">
     <Content Include="..\packages\SkiaSharp.1.57.1\runtimes\win7-x64\native\libSkiaSharp.dll">
       <Link>x64\libSkiaSharp.dll</Link>
       <Link>x64\libSkiaSharp.dll</Link>
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
     <Content Include="..\packages\SkiaSharp.1.57.1\runtimes\win7-x86\native\libSkiaSharp.dll">
     <Content Include="..\packages\SkiaSharp.1.57.1\runtimes\win7-x86\native\libSkiaSharp.dll">
       <Link>x86\libSkiaSharp.dll</Link>
       <Link>x86\libSkiaSharp.dll</Link>
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
     <Content Include="..\Tools\Installation\MediaBrowser.InstallUtil.dll">
     <Content Include="..\Tools\Installation\MediaBrowser.InstallUtil.dll">
       <Link>MediaBrowser.InstallUtil.dll</Link>
       <Link>MediaBrowser.InstallUtil.dll</Link>
@@ -1110,6 +1112,10 @@
       <Project>{c97a239e-a96c-4d64-a844-ccf8cc30aecb}</Project>
       <Project>{c97a239e-a96c-4d64-a844-ccf8cc30aecb}</Project>
       <Name>Emby.Drawing.Net</Name>
       <Name>Emby.Drawing.Net</Name>
     </ProjectReference>
     </ProjectReference>
+    <ProjectReference Include="..\Emby.Drawing.Skia\Emby.Drawing.Skia.csproj">
+      <Project>{2312da6d-ff86-4597-9777-bceec32d96dd}</Project>
+      <Name>Emby.Drawing.Skia</Name>
+    </ProjectReference>
     <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj">
     <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj">
       <Project>{08fff49b-f175-4807-a2b5-73b0ebd9f716}</Project>
       <Project>{08fff49b-f175-4807-a2b5-73b0ebd9f716}</Project>
       <Name>Emby.Drawing</Name>
       <Name>Emby.Drawing</Name>

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

@@ -15,6 +15,7 @@ using System.Text;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading;
 using System.Xml;
 using System.Xml;
+using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Xml;
 using MediaBrowser.Model.Xml;
 
 
@@ -227,6 +228,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
             }
             }
         }
         }
 
 
+        protected virtual string MovieDbParserSearchString
+        {
+            get { return "themoviedb.org/movie/"; }
+        }
+
         private void ParseProviderLinks(T item, string xml)
         private void ParseProviderLinks(T item, string xml)
         {
         {
             //Look for a match for the Regex pattern "tt" followed by 7 digits
             //Look for a match for the Regex pattern "tt" followed by 7 digits
@@ -238,7 +244,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
 
 
             // Support Tmdb
             // Support Tmdb
             // http://www.themoviedb.org/movie/36557
             // http://www.themoviedb.org/movie/36557
-            var srch = "themoviedb.org/movie/";
+            var srch = MovieDbParserSearchString;
             var index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
             var index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
 
 
             if (index != -1)
             if (index != -1)
@@ -250,6 +256,23 @@ namespace MediaBrowser.XbmcMetadata.Parsers
                     item.SetProviderId(MetadataProviders.Tmdb, tmdbId);
                     item.SetProviderId(MetadataProviders.Tmdb, tmdbId);
                 }
                 }
             }
             }
+
+            if (item is Series)
+            {
+                srch = "thetvdb.com/?tab=series&id=";
+
+                index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+
+                if (index != -1)
+                {
+                    var tvdbId = xml.Substring(index + srch.Length).TrimEnd('/');
+                    int value;
+                    if (!string.IsNullOrWhiteSpace(tvdbId) && int.TryParse(tvdbId, NumberStyles.Any, CultureInfo.InvariantCulture, out value))
+                    {
+                        item.SetProviderId(MetadataProviders.Tvdb, tvdbId);
+                    }
+                }
+            }
         }
         }
 
 
         protected virtual void FetchDataFromXmlNode(XmlReader reader, MetadataResult<T> itemResult)
         protected virtual void FetchDataFromXmlNode(XmlReader reader, MetadataResult<T> itemResult)

+ 13 - 0
MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs

@@ -13,6 +13,19 @@ namespace MediaBrowser.XbmcMetadata.Parsers
 {
 {
     public class SeriesNfoParser : BaseNfoParser<Series>
     public class SeriesNfoParser : BaseNfoParser<Series>
     {
     {
+        protected override bool SupportsUrlAfterClosingXmlTag
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        protected override string MovieDbParserSearchString
+        {
+            get { return "themoviedb.org/tv/"; }
+        }
+
         /// <summary>
         /// <summary>
         /// Fetches the data from XML node.
         /// Fetches the data from XML node.
         /// </summary>
         /// </summary>

+ 1 - 1
SharedVersion.cs

@@ -1,3 +1,3 @@
 using System.Reflection;
 using System.Reflection;
 
 
-[assembly: AssemblyVersion("3.2.15.2")]
+[assembly: AssemblyVersion("3.2.15.3")]