mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-19 04:59:35 -07:00
New: Use ImageSharp for resizing (#934)
* New: Swap to ImageSharp for image resizing to avoid leaks Stop resizing album images also * Fixed: MediaCoverModule falls back to fullsize for png and gif too * Fixed: Look for all image extensions in DeleteBadMediaCovers.cs
This commit is contained in:
parent
070e50d39e
commit
ad4d7e4cfd
8 changed files with 25 additions and 97 deletions
|
@ -10,7 +10,7 @@ namespace Lidarr.Api.V1.MediaCovers
|
||||||
{
|
{
|
||||||
public class MediaCoverModule : LidarrV1Module
|
public class MediaCoverModule : LidarrV1Module
|
||||||
{
|
{
|
||||||
private static readonly Regex RegexResizedImage = new Regex(@"-\d+\.jpg$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex RegexResizedImage = new Regex(@"-\d+(?=\.(jpg|png|gif)$)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
private const string MEDIA_COVER_ARTIST_ROUTE = @"/Artist/(?<artistId>\d+)/(?<filename>(.+)\.(jpg|png|gif))";
|
private const string MEDIA_COVER_ARTIST_ROUTE = @"/Artist/(?<artistId>\d+)/(?<filename>(.+)\.(jpg|png|gif))";
|
||||||
private const string MEDIA_COVER_ALBUM_ROUTE = @"/Album/(?<artistId>\d+)/(?<filename>(.+)\.(jpg|png|gif))";
|
private const string MEDIA_COVER_ALBUM_ROUTE = @"/Album/(?<artistId>\d+)/(?<filename>(.+)\.(jpg|png|gif))";
|
||||||
|
@ -35,7 +35,7 @@ namespace Lidarr.Api.V1.MediaCovers
|
||||||
{
|
{
|
||||||
// Return the full sized image if someone requests a non-existing resized one.
|
// Return the full sized image if someone requests a non-existing resized one.
|
||||||
// TODO: This code can be removed later once everyone had the update for a while.
|
// TODO: This code can be removed later once everyone had the update for a while.
|
||||||
var basefilePath = RegexResizedImage.Replace(filePath, ".jpg");
|
var basefilePath = RegexResizedImage.Replace(filePath, "");
|
||||||
if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath))
|
if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath))
|
||||||
{
|
{
|
||||||
return new NotFoundResponse();
|
return new NotFoundResponse();
|
||||||
|
@ -54,7 +54,7 @@ namespace Lidarr.Api.V1.MediaCovers
|
||||||
{
|
{
|
||||||
// Return the full sized image if someone requests a non-existing resized one.
|
// Return the full sized image if someone requests a non-existing resized one.
|
||||||
// TODO: This code can be removed later once everyone had the update for a while.
|
// TODO: This code can be removed later once everyone had the update for a while.
|
||||||
var basefilePath = RegexResizedImage.Replace(filePath, ".jpg");
|
var basefilePath = RegexResizedImage.Replace(filePath, "");
|
||||||
if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath))
|
if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath))
|
||||||
{
|
{
|
||||||
return new NotFoundResponse();
|
return new NotFoundResponse();
|
||||||
|
|
|
@ -10,7 +10,7 @@ namespace Lidarr.Http.Frontend.Mappers
|
||||||
{
|
{
|
||||||
public class MediaCoverMapper : StaticResourceMapperBase
|
public class MediaCoverMapper : StaticResourceMapperBase
|
||||||
{
|
{
|
||||||
private static readonly Regex RegexResizedImage = new Regex(@"-\d+\.jpg($|\?)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex RegexResizedImage = new Regex(@"-\d+(?=\.(jpg|png|gif)($|\?))", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
private readonly IAppFolderInfo _appFolderInfo;
|
private readonly IAppFolderInfo _appFolderInfo;
|
||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
|
@ -31,7 +31,7 @@ namespace Lidarr.Http.Frontend.Mappers
|
||||||
|
|
||||||
if (!_diskProvider.FileExists(resourcePath) || _diskProvider.GetFileSize(resourcePath) == 0)
|
if (!_diskProvider.FileExists(resourcePath) || _diskProvider.GetFileSize(resourcePath) == 0)
|
||||||
{
|
{
|
||||||
var baseResourcePath = RegexResizedImage.Replace(resourcePath, ".jpg$1");
|
var baseResourcePath = RegexResizedImage.Replace(resourcePath, "");
|
||||||
if (baseResourcePath != resourcePath)
|
if (baseResourcePath != resourcePath)
|
||||||
{
|
{
|
||||||
return baseResourcePath;
|
return baseResourcePath;
|
||||||
|
|
|
@ -153,7 +153,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests
|
||||||
Subject.HandleAsync(new ArtistRefreshCompleteEvent(_artist));
|
Subject.HandleAsync(new ArtistRefreshCompleteEvent(_artist));
|
||||||
|
|
||||||
Mocker.GetMock<IImageResizer>()
|
Mocker.GetMock<IImageResizer>()
|
||||||
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(3));
|
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -174,7 +174,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests
|
||||||
Subject.HandleAsync(new ArtistRefreshCompleteEvent(_artist));
|
Subject.HandleAsync(new ArtistRefreshCompleteEvent(_artist));
|
||||||
|
|
||||||
Mocker.GetMock<IImageResizer>()
|
Mocker.GetMock<IImageResizer>()
|
||||||
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(3));
|
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -224,7 +224,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests
|
||||||
Subject.HandleAsync(new ArtistRefreshCompleteEvent(_artist));
|
Subject.HandleAsync(new ArtistRefreshCompleteEvent(_artist));
|
||||||
|
|
||||||
Mocker.GetMock<IImageResizer>()
|
Mocker.GetMock<IImageResizer>()
|
||||||
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(3));
|
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -249,7 +249,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests
|
||||||
Subject.HandleAsync(new ArtistRefreshCompleteEvent(_artist));
|
Subject.HandleAsync(new ArtistRefreshCompleteEvent(_artist));
|
||||||
|
|
||||||
Mocker.GetMock<IImageResizer>()
|
Mocker.GetMock<IImageResizer>()
|
||||||
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(3));
|
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
@ -35,11 +36,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||||
if (!_configService.CleanupMetadataImages) return;
|
if (!_configService.CleanupMetadataImages) return;
|
||||||
|
|
||||||
var artists = _artistService.GetAllArtists();
|
var artists = _artistService.GetAllArtists();
|
||||||
|
var imageExtensions = new List<string> { ".jpg", ".png", ".gif" };
|
||||||
|
|
||||||
foreach (var artist in artists)
|
foreach (var artist in artists)
|
||||||
{
|
{
|
||||||
var images = _metaFileService.GetFilesByArtist(artist.Id)
|
var images = _metaFileService.GetFilesByArtist(artist.Id)
|
||||||
.Where(c => c.LastUpdated > new DateTime(2014, 12, 27) && c.RelativePath.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase));
|
.Where(c => c.LastUpdated > new DateTime(2014, 12, 27) && imageExtensions.Any(x => c.RelativePath.EndsWith(x, StringComparison.InvariantCultureIgnoreCase)));
|
||||||
|
|
||||||
foreach (var image in images)
|
foreach (var image in images)
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentMigrator.Runner" Version="1.6.2" />
|
<PackageReference Include="FluentMigrator.Runner" Version="1.6.2" />
|
||||||
<PackageReference Include="FluentValidation" Version="6.2.1" />
|
<PackageReference Include="FluentValidation" Version="6.2.1" />
|
||||||
<PackageReference Include="ImageResizer" Version="4.2.5" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||||
<PackageReference Include="NLog" Version="4.5.4" />
|
<PackageReference Include="NLog" Version="4.5.4" />
|
||||||
<PackageReference Include="OAuth" Version="1.0.3" />
|
<PackageReference Include="OAuth" Version="1.0.3" />
|
||||||
|
@ -16,6 +15,7 @@
|
||||||
<PackageReference Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
|
<PackageReference Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
|
||||||
<PackageReference Include="xmlrpcnet" Version="2.5.0" />
|
<PackageReference Include="xmlrpcnet" Version="2.5.0" />
|
||||||
<PackageReference Include="SpotifyAPI.Web" Version="4.2.0" />
|
<PackageReference Include="SpotifyAPI.Web" Version="4.2.0" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0006" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Marr.Data\Marr.Data.csproj" />
|
<ProjectReference Include="..\Marr.Data\Marr.Data.csproj" />
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,8 @@
|
||||||
using ImageResizer;
|
using System;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using SixLabors.Memory;
|
||||||
|
|
||||||
namespace NzbDrone.Core.MediaCover
|
namespace NzbDrone.Core.MediaCover
|
||||||
{
|
{
|
||||||
|
@ -15,25 +18,20 @@ namespace NzbDrone.Core.MediaCover
|
||||||
public ImageResizer(IDiskProvider diskProvider)
|
public ImageResizer(IDiskProvider diskProvider)
|
||||||
{
|
{
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
|
|
||||||
|
// More conservative memory allocation
|
||||||
|
SixLabors.ImageSharp.Configuration.Default.MemoryAllocator = new SimpleGcMemoryAllocator();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Resize(string source, string destination, int height)
|
public void Resize(string source, string destination, int height)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
GdiPlusInterop.CheckGdiPlus();
|
using (var image = Image.Load(source))
|
||||||
|
|
||||||
using (var sourceStream = _diskProvider.OpenReadStream(source))
|
|
||||||
{
|
{
|
||||||
using (var outputStream = _diskProvider.OpenWriteStream(destination))
|
var width = (int)Math.Floor((double)image.Width * (double)height / (double)image.Height);
|
||||||
{
|
image.Mutate(x => x.Resize(width, height));
|
||||||
var settings = new Instructions();
|
image.Save(destination);
|
||||||
settings.Height = height;
|
|
||||||
|
|
||||||
var job = new ImageJob(sourceStream, outputStream, settings);
|
|
||||||
|
|
||||||
ImageBuilder.Current.Build(job);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|
|
@ -163,8 +163,6 @@ namespace NzbDrone.Core.MediaCover
|
||||||
{
|
{
|
||||||
_logger.Error(e, "Couldn't download media cover for {0}", album);
|
_logger.Error(e, "Couldn't download media cover for {0}", album);
|
||||||
}
|
}
|
||||||
|
|
||||||
EnsureResizedAlbumCovers(album, cover, !alreadyExists);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,31 +225,6 @@ namespace NzbDrone.Core.MediaCover
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureResizedAlbumCovers(Album album, MediaCover cover, bool forceResize)
|
|
||||||
{
|
|
||||||
int[] heights = GetDefaultHeights(cover.CoverType);
|
|
||||||
|
|
||||||
foreach (var height in heights)
|
|
||||||
{
|
|
||||||
var mainFileName = GetCoverPath(album.Id, MediaCoverEntity.Album, cover.CoverType, cover.Extension, null);
|
|
||||||
var resizeFileName = GetCoverPath(album.Id, MediaCoverEntity.Album, cover.CoverType, cover.Extension, height);
|
|
||||||
|
|
||||||
if (forceResize || !_diskProvider.FileExists(resizeFileName) || _diskProvider.GetFileSize(resizeFileName) == 0)
|
|
||||||
{
|
|
||||||
_logger.Debug("Resizing {0}-{1} for {2}", cover.CoverType, height, album);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_resizer.Resize(mainFileName, resizeFileName, height);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_logger.Debug("Couldn't resize media cover {0}-{1} for album {2}, using full size image instead.", cover.CoverType, height, album);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int[] GetDefaultHeights(MediaCoverTypes coverType)
|
private int[] GetDefaultHeights(MediaCoverTypes coverType)
|
||||||
{
|
{
|
||||||
switch (coverType)
|
switch (coverType)
|
||||||
|
@ -261,6 +234,7 @@ namespace NzbDrone.Core.MediaCover
|
||||||
|
|
||||||
case MediaCoverTypes.Poster:
|
case MediaCoverTypes.Poster:
|
||||||
case MediaCoverTypes.Disc:
|
case MediaCoverTypes.Disc:
|
||||||
|
case MediaCoverTypes.Cover:
|
||||||
case MediaCoverTypes.Logo:
|
case MediaCoverTypes.Logo:
|
||||||
case MediaCoverTypes.Headshot:
|
case MediaCoverTypes.Headshot:
|
||||||
return new[] { 500, 250 };
|
return new[] { 500, 250 };
|
||||||
|
@ -271,8 +245,6 @@ namespace NzbDrone.Core.MediaCover
|
||||||
case MediaCoverTypes.Fanart:
|
case MediaCoverTypes.Fanart:
|
||||||
case MediaCoverTypes.Screenshot:
|
case MediaCoverTypes.Screenshot:
|
||||||
return new[] { 360, 180 };
|
return new[] { 360, 180 };
|
||||||
case MediaCoverTypes.Cover:
|
|
||||||
return new[] { 250 };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue