New: MediaCover api now includes several resized variants to save bandwidth for mobile apps.

banner-35.jpg (height)
banner-70.jpg
fanart-180.jpg (height)
fanart-360.jpg
poster-170.jpg (width)
poster-340.jpg
This commit is contained in:
Taloth Saldono 2015-01-22 23:12:35 +01:00
parent eb4bcf9331
commit 35ab3a28fd
15 changed files with 241 additions and 16 deletions

View File

@ -21,7 +21,7 @@ namespace NzbDrone.Common.Crypto
{
using (var md5 = MD5.Create())
{
using (var stream = _diskProvider.StreamFile(path))
using (var stream = _diskProvider.OpenReadStream(path))
{
return md5.ComputeHash(stream);
}

View File

@ -421,7 +421,7 @@ namespace NzbDrone.Common.Disk
return driveInfo.VolumeLabel;
}
public FileStream StreamFile(string path)
public FileStream OpenReadStream(string path)
{
if (!FileExists(path))
{
@ -431,6 +431,11 @@ namespace NzbDrone.Common.Disk
return new FileStream(path, FileMode.Open, FileAccess.Read);
}
public FileStream OpenWriteStream(string path)
{
return new FileStream(path, FileMode.Create);
}
public List<DriveInfo> GetDrives()
{
return DriveInfo.GetDrives()

View File

@ -45,7 +45,8 @@ namespace NzbDrone.Common.Disk
void EmptyFolder(string path);
string[] GetFixedDrives();
string GetVolumeLabel(string path);
FileStream StreamFile(string path);
FileStream OpenReadStream(string path);
FileStream OpenWriteStream(string path);
List<DriveInfo> GetDrives();
List<DirectoryInfo> GetDirectoryInfos(string path);
List<FileInfo> GetFileInfos(string path);

View File

@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
namespace NzbDrone.Common.Extensions
{

View File

@ -56,7 +56,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
Subject.Clean();
Mocker.GetMock<IDiskProvider>().Verify(c => c.StreamFile(It.IsAny<string>()), Times.Never());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenReadStream(It.IsAny<string>()), Times.Never());
}
@ -67,7 +67,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
Subject.Clean();
Mocker.GetMock<IDiskProvider>().Verify(c => c.StreamFile(It.IsAny<string>()), Times.Never());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenReadStream(It.IsAny<string>()), Times.Never());
}
@ -107,7 +107,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
_metaData.First().Type = MetadataType.SeriesImage;
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.StreamFile(imagePath))
.Setup(c => c.OpenReadStream(imagePath))
.Returns(new FileStream("Files\\html_image.jpg".AsOsAgnostic(), FileMode.Open, FileAccess.Read));
@ -129,7 +129,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
_metaData.First().RelativePath = "Season\\image.jpg".AsOsAgnostic();
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.StreamFile(imagePath))
.Setup(c => c.OpenReadStream(imagePath))
.Returns(new FileStream("Files\\emptyfile.txt".AsOsAgnostic(), FileMode.Open, FileAccess.Read));
@ -149,7 +149,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
_metaData.First().RelativePath = "Season\\image.jpg".AsOsAgnostic();
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.StreamFile(imagePath))
.Setup(c => c.OpenReadStream(imagePath))
.Returns(new FileStream("Files\\Queue.txt".AsOsAgnostic(), FileMode.Open, FileAccess.Read));

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Crypto;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MediaCoverTests
{
[TestFixture]
public class ImageResizerFixture : CoreTest<ImageResizer>
{
[SetUp]
public void SetUp()
{
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.OpenReadStream(It.IsAny<string>()))
.Returns<string>(s => new FileStream(s, FileMode.Open));
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.OpenWriteStream(It.IsAny<string>()))
.Returns<string>(s => new FileStream(s, FileMode.Create));
}
[Test]
public void should_resize_image()
{
var mainFile = Path.Combine(TempFolder, "logo.png");
var resizedFile = Path.Combine(TempFolder, "logo-170.png");
File.Copy(@"Files/1024.png", mainFile);
Subject.Resize(mainFile, resizedFile, 170);
var fileInfo = new FileInfo(resizedFile);
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);
}
}
}

View File

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@ -7,17 +9,25 @@ using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Test.Framework;
using System.Linq;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Core.Test.MediaCoverTests
{
[TestFixture]
public class MediaCoverServiceFixture : CoreTest<MediaCoverService>
{
Series _series;
[SetUp]
public void Setup()
{
Mocker.SetConstant<IAppFolderInfo>(new AppFolderInfo(Mocker.Resolve<IStartupContext>()));
_series = Builder<Series>.CreateNew()
.With(v => v.Id = 2)
.With(v => v.Images = new List<MediaCover.MediaCover> { new MediaCover.MediaCover(MediaCoverTypes.Poster, "") })
.Build();
}
[Test]
@ -55,5 +65,55 @@ namespace NzbDrone.Core.Test.MediaCoverTests
covers.Single().Url.Should().Be("/MediaCover/12/banner.jpg");
}
[Test]
public void should_resize_covers_if_main_downloaded()
{
Mocker.GetMock<ICoverExistsSpecification>()
.Setup(v => v.AlreadyExists(It.IsAny<string>(), It.IsAny<string>()))
.Returns(false);
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.IsAny<string>()))
.Returns(true);
Subject.HandleAsync(new SeriesUpdatedEvent(_series));
Mocker.GetMock<IImageResizer>()
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(2));
}
[Test]
public void should_resize_covers_if_missing()
{
Mocker.GetMock<ICoverExistsSpecification>()
.Setup(v => v.AlreadyExists(It.IsAny<string>(), It.IsAny<string>()))
.Returns(true);
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.IsAny<string>()))
.Returns(false);
Subject.HandleAsync(new SeriesUpdatedEvent(_series));
Mocker.GetMock<IImageResizer>()
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(2));
}
[Test]
public void should_not_resize_covers_if_exists()
{
Mocker.GetMock<ICoverExistsSpecification>()
.Setup(v => v.AlreadyExists(It.IsAny<string>(), It.IsAny<string>()))
.Returns(true);
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.IsAny<string>()))
.Returns(true);
Subject.HandleAsync(new SeriesUpdatedEvent(_series));
Mocker.GetMock<IImageResizer>()
.Verify(v => v.Resize(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Never());
}
}
}

View File

@ -74,6 +74,7 @@
</Reference>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="Microsoft.CSharp" />
@ -215,6 +216,7 @@
<Compile Include="JobTests\JobRepositoryFixture.cs" />
<Compile Include="JobTests\TestJobs.cs" />
<Compile Include="MediaCoverTests\CoverExistsSpecificationFixture.cs" />
<Compile Include="MediaCoverTests\ImageResizerFixture.cs" />
<Compile Include="MediaCoverTests\MediaCoverServiceFixture.cs" />
<Compile Include="MediaFiles\DiskScanServiceTests\ScanFixture.cs" />
<Compile Include="MediaFiles\DownloadedEpisodesCommandServiceFixture.cs" />
@ -348,6 +350,10 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="..\..\Logo\1024.png">
<Link>Files\1024.png</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libraries\Sqlite\sqlite3.dll">
<Link>sqlite3.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>

View File

@ -69,7 +69,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
var buffer = new byte[10];
using (var imageStream = _diskProvider.StreamFile(path))
using (var imageStream = _diskProvider.OpenReadStream(path))
{
if (imageStream.Length < buffer.Length) return false;
imageStream.Read(buffer, 0, buffer.Length);

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ImageResizer;
using NzbDrone.Common.Disk;
namespace NzbDrone.Core.MediaCover
{
public interface IImageResizer
{
void Resize(string source, string destination, int height);
}
public class ImageResizer : IImageResizer
{
private readonly IDiskProvider _diskProvider;
public ImageResizer(IDiskProvider diskProvider)
{
_diskProvider = diskProvider;
}
public void Resize(string source, string destination, int height)
{
using (var sourceStream = _diskProvider.OpenReadStream(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);
}
}
}
}
}

View File

@ -17,7 +17,7 @@ namespace NzbDrone.Core.MediaCover
public interface IMapCoversToLocal
{
void ConvertToLocalUrls(int seriesId, IEnumerable<MediaCover> covers);
string GetCoverPath(int seriesId, MediaCoverTypes mediaCoverTypes);
string GetCoverPath(int seriesId, MediaCoverTypes mediaCoverTypes, int? height = null);
}
public class MediaCoverService :
@ -25,6 +25,7 @@ namespace NzbDrone.Core.MediaCover
IHandleAsync<SeriesDeletedEvent>,
IMapCoversToLocal
{
private readonly IImageResizer _resizer;
private readonly IHttpClient _httpClient;
private readonly IDiskProvider _diskProvider;
private readonly ICoverExistsSpecification _coverExistsSpecification;
@ -34,7 +35,8 @@ namespace NzbDrone.Core.MediaCover
private readonly string _coverRootFolder;
public MediaCoverService(IHttpClient httpClient,
public MediaCoverService(IImageResizer resizer,
IHttpClient httpClient,
IDiskProvider diskProvider,
IAppFolderInfo appFolderInfo,
ICoverExistsSpecification coverExistsSpecification,
@ -42,6 +44,7 @@ namespace NzbDrone.Core.MediaCover
IEventAggregator eventAggregator,
Logger logger)
{
_resizer = resizer;
_httpClient = httpClient;
_diskProvider = diskProvider;
_coverExistsSpecification = coverExistsSpecification;
@ -52,9 +55,11 @@ namespace NzbDrone.Core.MediaCover
_coverRootFolder = appFolderInfo.GetMediaCoverPath();
}
public string GetCoverPath(int seriesId, MediaCoverTypes coverTypes)
public string GetCoverPath(int seriesId, MediaCoverTypes coverTypes, int? height = null)
{
return Path.Combine(GetSeriesCoverPath(seriesId), coverTypes.ToString().ToLower() + ".jpg");
var heightSuffix = height.HasValue ? "-" + height.ToString() : "";
return Path.Combine(GetSeriesCoverPath(seriesId), coverTypes.ToString().ToLower() + heightSuffix + ".jpg");
}
public void ConvertToLocalUrls(int seriesId, IEnumerable<MediaCover> covers)
@ -88,6 +93,11 @@ namespace NzbDrone.Core.MediaCover
if (!_coverExistsSpecification.AlreadyExists(cover.Url, fileName))
{
DownloadCover(series, cover);
EnsureResizedCovers(series, cover, true);
}
else
{
EnsureResizedCovers(series, cover, false);
}
}
catch (WebException e)
@ -109,6 +119,44 @@ namespace NzbDrone.Core.MediaCover
_httpClient.DownloadFile(cover.Url, fileName);
}
private void EnsureResizedCovers(Series series, MediaCover cover, bool forceResize)
{
int[] heights;
switch (cover.CoverType)
{
default:
return;
case MediaCoverTypes.Poster:
case MediaCoverTypes.Headshot:
heights = new[] { 500, 250 };
break;
case MediaCoverTypes.Banner:
heights = new[] { 70, 35 };
break;
case MediaCoverTypes.Fanart:
case MediaCoverTypes.Screenshot:
heights = new[] { 360, 180 };
break;
}
foreach (var height in heights)
{
var mainFileName = GetCoverPath(series.Id, cover.CoverType);
var resizeFileName = GetCoverPath(series.Id, cover.CoverType, height);
if (forceResize || !_diskProvider.FileExists(resizeFileName))
{
_logger.Debug("Resizing {0}-{1} for {2}", cover.CoverType, height, series);
_resizer.Resize(mainFileName, resizeFileName, height);
}
}
}
public void HandleAsync(SeriesUpdatedEvent message)
{
EnsureCovers(message.Series);

View File

@ -72,6 +72,10 @@
<SpecificVersion>False</SpecificVersion>
<HintPath>..\Libraries\Growl.CoreLibrary.dll</HintPath>
</Reference>
<Reference Include="ImageResizer, Version=3.4.3.103, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\ImageResizer.3.4.3\lib\ImageResizer.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Newtonsoft.Json.6.0.6\lib\net40\Newtonsoft.Json.dll</HintPath>
@ -522,6 +526,7 @@
<Compile Include="Lifecycle\LifecycleService.cs" />
<Compile Include="MediaCover\CoverAlreadyExistsSpecification.cs" />
<Compile Include="MediaCover\MediaCover.cs" />
<Compile Include="MediaCover\ImageResizer.cs" />
<Compile Include="MediaCover\MediaCoverService.cs" />
<Compile Include="MediaCover\MediaCoversUpdatedEvent.cs" />
<Compile Include="MediaFiles\Commands\BackendCommandAttribute.cs" />

View File

@ -19,7 +19,7 @@ namespace NzbDrone.Core.Update
public Boolean Verify(UpdatePackage updatePackage, String packagePath)
{
using (var fileStream = _diskProvider.StreamFile(packagePath))
using (var fileStream = _diskProvider.OpenReadStream(packagePath))
{
var hash = fileStream.SHA256Hash();

View File

@ -3,6 +3,7 @@
<package id="FluentMigrator" version="1.3.1.0" targetFramework="net40" />
<package id="FluentMigrator.Runner" version="1.3.1.0" targetFramework="net40" />
<package id="FluentValidation" version="5.5.0.0" targetFramework="net40" />
<package id="ImageResizer" version="3.4.3" targetFramework="net40" />
<package id="MediaInfoNet" version="0.3" targetFramework="net40" />
<package id="Newtonsoft.Json" version="6.0.6" targetFramework="net40" />
<package id="NLog" version="2.1.0" targetFramework="net40" />

View File

@ -34,10 +34,16 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceUninstall", "ServiceHelpers\ServiceUninstall\ServiceUninstall.csproj", "{700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Core", "NzbDrone.Core\NzbDrone.Core.csproj", "{FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}"
ProjectSection(ProjectDependencies) = postProject
{0CC493D7-0A9F-4199-9615-0A977945D716} = {0CC493D7-0A9F-4199-9615-0A977945D716}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Update", "NzbDrone.Update\NzbDrone.Update.csproj", "{4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Common", "NzbDrone.Common\NzbDrone.Common.csproj", "{F2BE0FDF-6E47-4827-A420-DD4EF82407F8}"
ProjectSection(ProjectDependencies) = postProject
{9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB} = {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{1E6B3CBE-1578-41C1-9BF9-78D818740BE9}"
ProjectSection(SolutionItems) = preProject
@ -81,6 +87,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogentriesCore", "LogentriesCore\LogentriesCore.csproj", "{90D6E9FC-7B88-4E1B-B018-8FA742274558}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogentriesNLog", "LogentriesNLog\LogentriesNLog.csproj", "{9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}"
ProjectSection(ProjectDependencies) = postProject
{90D6E9FC-7B88-4E1B-B018-8FA742274558} = {90D6E9FC-7B88-4E1B-B018-8FA742274558}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TVDBSharp", "TVDBSharp\TVDBSharp.csproj", "{0CC493D7-0A9F-4199-9615-0A977945D716}"
EndProject