Parcourir la source

Merge pull request #2628 from MediaBrowser/dev

Dev
Luke il y a 8 ans
Parent
commit
6ee9da3717
30 fichiers modifiés avec 862 ajouts et 100 suppressions
  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 == '/')
             {
                 result = result.Replace('\\', '/');
+
+                if (result.StartsWith("smb:/", StringComparison.OrdinalIgnoreCase) && !result.StartsWith("smb://", StringComparison.OrdinalIgnoreCase))
+                {
+                    result = result.Replace("smb:/", "smb://");
+                }
             }
 
             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)
         {
             CheckDisposed();
@@ -150,6 +139,11 @@ namespace Emby.Drawing.ImageMagick
             {
                 using (var originalImage = new MagickWand(inputPath))
                 {
+                    if (options.CropWhiteSpace)
+                    {
+                        originalImage.CurrentImage.TrimImage(10);
+                    }
+
                     ScaleImage(originalImage, width, height, options.Blur ?? 0);
 
                     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)
         {
             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 newHeight = Convert.ToInt32(height);

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

@@ -52,7 +52,12 @@
     <Compile Include="..\SharedVersion.cs">
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
+    <Compile Include="PercentPlayedDrawer.cs" />
+    <Compile Include="PlayedIndicatorDrawer.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="SkiaEncoder.cs" />
+    <Compile Include="StripCollageBuilder.cs" />
+    <Compile Include="UnplayedCountIndicator.cs" />
   </ItemGroup>
   <ItemGroup>
     <Reference Include="SkiaSharp, Version=1.57.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
@@ -61,6 +66,7 @@
     </Reference>
   </ItemGroup>
   <ItemGroup>
+    <EmbeddedResource Include="fonts\robotoregular.ttf" />
     <None Include="packages.config" />
   </ItemGroup>
   <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)
         {
             ImageEnhancers = enhancers.ToArray();
@@ -186,14 +178,6 @@ namespace Emby.Drawing
                 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)
             {
                 var tuple = await GetEnhancedImage(new ItemImageInfo
@@ -400,46 +384,6 @@ namespace Emby.Drawing
             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)
         {
             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;
                 }
 
-                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;
                 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");
             var path = CommonApplicationPaths.SystemConfigurationFilePath;
 
-            FileSystem.CreateDirectory(Path.GetDirectoryName(path));
+            FileSystem.CreateDirectory(FileSystem.GetDirectoryName(path));
 
             lock (_configurationSyncLock)
             {
@@ -293,7 +293,7 @@ namespace Emby.Server.Implementations.AppBase
             _configurations.AddOrUpdate(key, configuration, (k, v) => configuration);
 
             var path = GetConfigurationFile(key);
-            FileSystem.CreateDirectory(Path.GetDirectoryName(path));
+            FileSystem.CreateDirectory(FileSystem.GetDirectoryName(path));
 
             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 (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
                     fileSystem.WriteAllBytes(path, newBytes);

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

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

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

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

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

@@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.FFMpeg
                 else
                 {
                     info = existingVersion;
-                    versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath);
+                    versionedDirectoryPath = _fileSystem.GetDirectoryName(info.EncoderPath);
                     excludeFromDeletions.Add(versionedDirectoryPath);
                 }
             }
@@ -135,7 +135,7 @@ namespace Emby.Server.Implementations.FFMpeg
                     {
                         EncoderPath = encoder,
                         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)
                 {
                     _logger.Info("Post-scan task cancelled: {0}", task.GetType().Name);
+                    throw;
                 }
                 catch (Exception ex)
                 {

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

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

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

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

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

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

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

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

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

@@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Validators
                 catch (OperationCanceledException)
                 {
                     // Don't clutter the log
-                    break;
+                    throw;
                 }
                 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)
             {
+                cancellationToken.ThrowIfCancellationRequested();
+
                 try
                 {
                     var year = _libraryManager.GetYear(yearNumber);
@@ -35,7 +37,7 @@ namespace Emby.Server.Implementations.Library.Validators
                 catch (OperationCanceledException)
                 {
                     // Don't clutter the log
-                    break;
+                    throw;
                 }
                 catch (Exception ex)
                 {

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

@@ -16,12 +16,6 @@ namespace MediaBrowser.Controller.Drawing
         /// <value>The supported output formats.</value>
         ImageFormat[] SupportedOutputFormats { get; }
         /// <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>

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

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

+ 10 - 0
MediaBrowser.ServerApplication/ImageEncoderHelper.cs

@@ -2,6 +2,7 @@
 using Emby.Drawing;
 using Emby.Drawing.Net;
 using Emby.Drawing.ImageMagick;
+using Emby.Drawing.Skia;
 using Emby.Server.Core;
 using Emby.Server.Implementations;
 using MediaBrowser.Common.Configuration;
@@ -23,6 +24,15 @@ namespace MediaBrowser.Server.Startup.Common
         {
             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
                 {
                     return new ImageMagickEncoder(logManager.GetLogger("ImageMagick"), appPaths, httpClient, fileSystem);

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

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

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

@@ -15,6 +15,7 @@ using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Xml;
+using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Model.IO;
 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)
         {
             //Look for a match for the Regex pattern "tt" followed by 7 digits
@@ -238,7 +244,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
 
             // Support Tmdb
             // http://www.themoviedb.org/movie/36557
-            var srch = "themoviedb.org/movie/";
+            var srch = MovieDbParserSearchString;
             var index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
 
             if (index != -1)
@@ -250,6 +256,23 @@ namespace MediaBrowser.XbmcMetadata.Parsers
                     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)

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

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

+ 1 - 1
SharedVersion.cs

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