diff --git a/build.sh b/build.sh index 2f0bffaf2..40b13ae3c 100755 --- a/build.sh +++ b/build.sh @@ -192,6 +192,24 @@ PatchMono() fi done + + # Copy more stable version of Vectors for mono <5.12 + if [ -e $path/System.Numerics.Vectors.dll ]; then + packageDir="$HOME/.nuget/packages/system.numerics.vectors/4.5.0" + + if [ ! -d "$HOME/.nuget/packages/system.numerics.vectors/4.5.0" ]; then + # May reside in the NuGetFallback folder, which is harder to find + # Download somewhere to get the real cache populated + if [ $runtime = "dotnet" ] ; then + $nuget install System.Numerics.Vectors -Version 4.5.0 -Output ./_temp/System.Numerics.Vectors + else + mono $nuget install System.Numerics.Vectors -Version 4.5.0 -Output ./_temp/System.Numerics.Vectors + fi + rm -rf ./_temp/System.Numerics.Vectors + fi + # Copy the netstandard2.0 version rather than net46 + cp "$packageDir/lib/netstandard2.0/System.Numerics.Vectors.dll" $path/ + fi } PackageMono() diff --git a/src/NzbDrone.Core.Test/MediaCoverTests/ImageResizerFixture.cs b/src/NzbDrone.Core.Test/MediaCoverTests/ImageResizerFixture.cs index dd9ae13d0..e5a4c05f7 100644 --- a/src/NzbDrone.Core.Test/MediaCoverTests/ImageResizerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaCoverTests/ImageResizerFixture.cs @@ -4,7 +4,9 @@ using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Test.Framework; +using SixLabors.ImageSharp; namespace NzbDrone.Core.Test.MediaCoverTests { @@ -14,13 +16,10 @@ namespace NzbDrone.Core.Test.MediaCoverTests [SetUp] public void SetUp() { - Mocker.GetMock() - .Setup(v => v.OpenReadStream(It.IsAny())) - .Returns(s => new FileStream(s, FileMode.Open)); - - Mocker.GetMock() - .Setup(v => v.OpenWriteStream(It.IsAny())) - .Returns(s => new FileStream(s, FileMode.Create)); + if (PlatformInfo.GetVersion() < new Version(5, 8)) + { + Assert.Inconclusive("Not supported on Mono < 5.8"); + } Mocker.GetMock() .Setup(v => v.FileExists(It.IsAny())) @@ -29,6 +28,8 @@ namespace NzbDrone.Core.Test.MediaCoverTests Mocker.GetMock() .Setup(v => v.DeleteFile(It.IsAny())) .Callback(s => File.Delete(s)); + + Mocker.SetConstant(Mocker.Resolve()); } [Test] @@ -45,9 +46,11 @@ namespace NzbDrone.Core.Test.MediaCoverTests fileInfo.Exists.Should().BeTrue(); fileInfo.Length.Should().BeInRange(1000, 30000); - var image = System.Drawing.Image.FromFile(resizedFile); - image.Height.Should().Be(170); - image.Width.Should().Be(170); + using (var image = Image.Load(resizedFile)) + { + image.Height.Should().Be(170); + image.Width.Should().Be(170); + } } [Test] diff --git a/src/NzbDrone.Core/MediaCover/GdiPlusInterop.cs b/src/NzbDrone.Core/MediaCover/GdiPlusInterop.cs deleted file mode 100644 index 659a15d41..000000000 --- a/src/NzbDrone.Core/MediaCover/GdiPlusInterop.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Drawing; -using NzbDrone.Common.EnvironmentInfo; - -namespace NzbDrone.Core.MediaCover -{ - public static class GdiPlusInterop - { - private static Exception _gdiPlusException; - - static GdiPlusInterop() - { - TestLibrary(); - } - - private static void TestLibrary() - { - if (OsInfo.IsWindows) - { - return; - } - - try - { - // We use StringFormat as test coz it gets properly cleaned up by the finalizer even if gdiplus is absent and is relatively non-invasive. - var strFormat = new StringFormat(); - - strFormat.Dispose(); - } - catch (Exception ex) - { - _gdiPlusException = ex; - } - } - - public static void CheckGdiPlus() - { - if (_gdiPlusException != null) - { - throw new DllNotFoundException("Couldn't load GDIPlus library", _gdiPlusException); - } - } - } -} diff --git a/src/NzbDrone.Core/MediaCover/ImageResizer.cs b/src/NzbDrone.Core/MediaCover/ImageResizer.cs index 9673cbec6..c2509d8dd 100644 --- a/src/NzbDrone.Core/MediaCover/ImageResizer.cs +++ b/src/NzbDrone.Core/MediaCover/ImageResizer.cs @@ -1,5 +1,9 @@ -using ImageResizer; -using NzbDrone.Common.Disk; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Processing; +using SixLabors.Memory; namespace NzbDrone.Core.MediaCover { @@ -11,29 +15,40 @@ namespace NzbDrone.Core.MediaCover public class ImageResizer : IImageResizer { private readonly IDiskProvider _diskProvider; + private readonly bool _enabled; - public ImageResizer(IDiskProvider diskProvider) + public ImageResizer(IDiskProvider diskProvider, IPlatformInfo platformInfo) { _diskProvider = diskProvider; + + // Random segfaults on mono 5.0 and 5.4 + if (PlatformInfo.IsMono && platformInfo.Version < new System.Version(5, 8)) + { + return; + } + + _enabled = true; + + // More conservative memory allocation + SixLabors.ImageSharp.Configuration.Default.MemoryAllocator = new SimpleGcMemoryAllocator(); + + // Thumbnails don't need super high quality + SixLabors.ImageSharp.Configuration.Default.ImageFormatsManager.SetEncoder(JpegFormat.Instance, new JpegEncoder + { + Quality = 92 + }); } public void Resize(string source, string destination, int height) { + if (!_enabled) return; + try { - GdiPlusInterop.CheckGdiPlus(); - - using (var sourceStream = _diskProvider.OpenReadStream(source)) + using (var image = Image.Load(source)) { - using (var outputStream = _diskProvider.OpenWriteStream(destination)) - { - var settings = new Instructions(); - settings.Height = height; - - var job = new ImageJob(sourceStream, outputStream, settings); - - ImageBuilder.Current.Build(job); - } + image.Mutate(x => x.Resize(0, height)); + image.Save(destination); } } catch diff --git a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs index f8e7b652b..b9a6eb914 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Net; +using System.Threading; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -35,6 +36,10 @@ namespace NzbDrone.Core.MediaCover private readonly string _coverRootFolder; + // ImageSharp is slow on ARM (no hardware acceleration on mono yet) + // So limit the number of concurrent resizing tasks + private static SemaphoreSlim _semaphore = new SemaphoreSlim((int)Math.Ceiling(Environment.ProcessorCount / 2.0)); + public MediaCoverService(IImageResizer resizer, IHttpClient httpClient, IDiskProvider diskProvider, @@ -85,6 +90,8 @@ namespace NzbDrone.Core.MediaCover private void EnsureCovers(Series series) { + var toResize = new List>(); + foreach (var cover in series.Images) { var fileName = GetCoverPath(series.Id, cover.CoverType); @@ -106,7 +113,21 @@ namespace NzbDrone.Core.MediaCover _logger.Error(e, "Couldn't download media cover for {0}", series); } - EnsureResizedCovers(series, cover, !alreadyExists); + toResize.Add(Tuple.Create(cover, alreadyExists)); + } + + try + { + _semaphore.Wait(); + + foreach (var tuple in toResize) + { + EnsureResizedCovers(series, tuple.Item1, !tuple.Item2); + } + } + finally + { + _semaphore.Release(); } } diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index 33a82176e..48df7d8f2 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -6,7 +6,8 @@ - + + @@ -44,4 +45,4 @@ PreserveNewest - \ No newline at end of file + diff --git a/src/NzbDrone.Test.Common/NzbDroneRunner.cs b/src/NzbDrone.Test.Common/NzbDroneRunner.cs index 131b7b5d2..537df732d 100644 --- a/src/NzbDrone.Test.Common/NzbDroneRunner.cs +++ b/src/NzbDrone.Test.Common/NzbDroneRunner.cs @@ -76,7 +76,7 @@ namespace NzbDrone.Test.Common return; } - Console.WriteLine("Waiting for NzbDrone to start. Response Status : {0} [{1}] {2}", statusCall.ResponseStatus, statusCall.StatusDescription, statusCall.ErrorException); + Console.WriteLine("Waiting for NzbDrone to start. Response Status : {0} [{1}] {2}", statusCall.ResponseStatus, statusCall.StatusDescription, statusCall.ErrorException.Message); Thread.Sleep(500); }