Moved discovery of loggers and weather providers to MEF. Also added support for third-party image processors, also discovered through MEF.

This commit is contained in:
LukePulverenti Luke Pulverenti luke pulverenti
2012-09-18 15:33:57 -04:00
parent 01a25c48a0
commit 8b7effd6ff
23 changed files with 370 additions and 334 deletions

View File

@@ -0,0 +1,33 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
using System.Drawing;
namespace MediaBrowser.Controller.Drawing
{
/// <summary>
/// Provides a base image processor class that plugins can use to process images as they are being writen to http responses
/// Since this is completely modular with MEF, a plugin only needs to have a subclass in their assembly with the following attribute on the class:
/// [Export(typeof(BaseImageProcessor))]
/// This will require a reference to System.ComponentModel.Composition
/// </summary>
public abstract class BaseImageProcessor
{
/// <summary>
/// Processes the primary image for a BaseEntity (Person, Studio, User, etc)
/// </summary>
/// <param name="bitmap">The bitmap holding the original image, after re-sizing</param>
/// <param name="graphics">The graphics surface on which the output is drawn</param>
/// <param name="entity">The entity that owns the image</param>
public abstract void ProcessImage(Bitmap bitmap, Graphics graphics, BaseEntity entity);
/// <summary>
/// Processes an image for a BaseItem
/// </summary>
/// <param name="bitmap">The bitmap holding the original image, after re-sizing</param>
/// <param name="graphics">The graphics surface on which the output is drawn</param>
/// <param name="entity">The entity that owns the image</param>
/// <param name="imageType">The image type</param>
/// <param name="imageIndex">The image index (currently only used with backdrops)</param>
public abstract void ProcessImage(Bitmap bitmap, Graphics graphics, BaseItem entity, ImageType imageType, int imageIndex);
}
}

View File

@@ -0,0 +1,81 @@
using System;
using System.Drawing;
namespace MediaBrowser.Controller.Drawing
{
public static class DrawingUtils
{
/// <summary>
/// Resizes a set of dimensions
/// </summary>
public static Size Resize(int currentWidth, int currentHeight, int? width, int? height, int? maxWidth, int? maxHeight)
{
return Resize(new Size(currentWidth, currentHeight), width, height, maxWidth, maxHeight);
}
/// <summary>
/// Resizes a set of dimensions
/// </summary>
/// <param name="size">The original size object</param>
/// <param name="width">A new fixed width, if desired</param>
/// <param name="height">A new fixed neight, if desired</param>
/// <param name="maxWidth">A max fixed width, if desired</param>
/// <param name="maxHeight">A max fixed height, if desired</param>
/// <returns>A new size object</returns>
public static Size Resize(Size size, int? width, int? height, int? maxWidth, int? maxHeight)
{
decimal newWidth = size.Width;
decimal newHeight = size.Height;
if (width.HasValue && height.HasValue)
{
newWidth = width.Value;
newHeight = height.Value;
}
else if (height.HasValue)
{
newWidth = GetNewWidth(newHeight, newWidth, height.Value);
newHeight = height.Value;
}
else if (width.HasValue)
{
newHeight = GetNewHeight(newHeight, newWidth, width.Value);
newWidth = width.Value;
}
if (maxHeight.HasValue && maxHeight < newHeight)
{
newWidth = GetNewWidth(newHeight, newWidth, maxHeight.Value);
newHeight = maxHeight.Value;
}
if (maxWidth.HasValue && maxWidth < newWidth)
{
newHeight = GetNewHeight(newHeight, newWidth, maxWidth.Value);
newWidth = maxWidth.Value;
}
return new Size(Convert.ToInt32(newWidth), Convert.ToInt32(newHeight));
}
private static decimal GetNewWidth(decimal currentHeight, decimal currentWidth, int newHeight)
{
decimal scaleFactor = newHeight;
scaleFactor /= currentHeight;
scaleFactor *= currentWidth;
return scaleFactor;
}
private static decimal GetNewHeight(decimal currentHeight, decimal currentWidth, int newWidth)
{
decimal scaleFactor = newWidth;
scaleFactor /= currentWidth;
scaleFactor *= currentHeight;
return scaleFactor;
}
}
}

View File

@@ -0,0 +1,148 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
namespace MediaBrowser.Controller.Drawing
{
public static class ImageProcessor
{
/// <summary>
/// Processes an image by resizing to target dimensions
/// </summary>
/// <param name="sourceImageStream">The stream containing the source image</param>
/// <param name="toStream">The stream to save the new image to</param>
/// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
/// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
/// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
/// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
/// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
/// <param name="entity">The entity that owns the image</param>
/// <param name="imageType">The image type</param>
/// <param name="imageIndex">The image index (currently only used with backdrops)</param>
public static void ProcessImage(Stream sourceImageStream, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality, BaseEntity entity, ImageType imageType, int imageIndex)
{
Image originalImage = Image.FromStream(sourceImageStream);
// Determine the output size based on incoming parameters
Size newSize = DrawingUtils.Resize(originalImage.Size, width, height, maxWidth, maxHeight);
Bitmap thumbnail;
// Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
if (originalImage.PixelFormat.HasFlag(PixelFormat.Indexed))
{
thumbnail = new Bitmap(originalImage, newSize.Width, newSize.Height);
}
else
{
thumbnail = new Bitmap(newSize.Width, newSize.Height, originalImage.PixelFormat);
}
// Preserve the original resolution
thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
Graphics thumbnailGraph = Graphics.FromImage(thumbnail);
thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
thumbnailGraph.CompositingMode = CompositingMode.SourceOver;
thumbnailGraph.DrawImage(originalImage, 0, 0, newSize.Width, newSize.Height);
// Run Kernel image processors
if (Kernel.Instance.ImageProcessors.Any())
{
ExecuteAdditionalImageProcessors(thumbnail, thumbnailGraph, entity, imageType, imageIndex);
}
// Write to the output stream
SaveImage(originalImage.RawFormat, thumbnail, toStream, quality);
thumbnailGraph.Dispose();
thumbnail.Dispose();
originalImage.Dispose();
}
/// <summary>
/// Executes additional image processors that are registered with the Kernel
/// </summary>
/// <param name="bitmap">The bitmap holding the original image, after re-sizing</param>
/// <param name="graphics">The graphics surface on which the output is drawn</param>
/// <param name="entity">The entity that owns the image</param>
/// <param name="imageType">The image type</param>
/// <param name="imageIndex">The image index (currently only used with backdrops)</param>
private static void ExecuteAdditionalImageProcessors(Bitmap bitmap, Graphics graphics, BaseEntity entity, ImageType imageType, int imageIndex)
{
var baseItem = entity as BaseItem;
if (baseItem != null)
{
foreach (var processor in Kernel.Instance.ImageProcessors)
{
processor.ProcessImage(bitmap, graphics, baseItem, imageType, imageIndex);
}
}
else
{
foreach (var processor in Kernel.Instance.ImageProcessors)
{
processor.ProcessImage(bitmap, graphics, entity);
}
}
}
public static void SaveImage(ImageFormat originalImageRawFormat, Image newImage, Stream toStream, int? quality)
{
// Use special save methods for jpeg and png that will result in a much higher quality image
// All other formats use the generic Image.Save
if (ImageFormat.Jpeg.Equals(originalImageRawFormat))
{
SaveJpeg(newImage, toStream, quality);
}
else if (ImageFormat.Png.Equals(originalImageRawFormat))
{
newImage.Save(toStream, ImageFormat.Png);
}
else
{
newImage.Save(toStream, originalImageRawFormat);
}
}
public static void SaveJpeg(Image image, Stream target, int? quality)
{
if (!quality.HasValue)
{
quality = 90;
}
using (var encoderParameters = new EncoderParameters(1))
{
encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality.Value);
image.Save(target, GetImageCodecInfo("image/jpeg"), encoderParameters);
}
}
public static ImageCodecInfo GetImageCodecInfo(string mimeType)
{
ImageCodecInfo[] info = ImageCodecInfo.GetImageEncoders();
for (int i = 0; i < info.Length; i++)
{
ImageCodecInfo ici = info[i];
if (ici.MimeType.Equals(mimeType, StringComparison.OrdinalIgnoreCase))
{
return ici;
}
}
return info[1];
}
}
}

View File

@@ -1,5 +1,6 @@
using MediaBrowser.Common.Kernel;
using MediaBrowser.Common.Logging;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.IO;
@@ -27,7 +28,6 @@ namespace MediaBrowser.Controller
public static Kernel Instance { get; private set; }
public ItemController ItemController { get; private set; }
public WeatherClient WeatherClient { get; private set; }
public IEnumerable<User> Users { get; private set; }
public Folder RootFolder { get; private set; }
@@ -47,6 +47,12 @@ namespace MediaBrowser.Controller
get { return KernelContext.Server; }
}
/// <summary>
/// Gets the list of currently registered weather prvoiders
/// </summary>
[ImportMany(typeof(BaseWeatherProvider))]
public IEnumerable<BaseWeatherProvider> WeatherProviders { get; private set; }
/// <summary>
/// Gets the list of currently registered metadata prvoiders
/// </summary>
@@ -71,6 +77,12 @@ namespace MediaBrowser.Controller
/// </summary>
internal IBaseItemResolver[] EntityResolvers { get; private set; }
/// <summary>
/// Gets the list of currently registered entity resolvers
/// </summary>
[ImportMany(typeof(BaseImageProcessor))]
internal IEnumerable<BaseImageProcessor> ImageProcessors { get; private set; }
/// <summary>
/// Creates a kernel based on a Data path, which is akin to our current programdata path
/// </summary>
@@ -85,13 +97,15 @@ namespace MediaBrowser.Controller
/// </summary>
protected override void InitializeInternal(IProgress<TaskProgress> progress)
{
base.InitializeInternal(progress);
ItemController = new ItemController();
DirectoryWatchers = new DirectoryWatchers();
ItemController.PreBeginResolvePath += ItemController_PreBeginResolvePath;
ItemController.BeginResolvePath += ItemController_BeginResolvePath;
base.InitializeInternal(progress);
ExtractFFMpeg();
}
/// <summary>
@@ -101,14 +115,11 @@ namespace MediaBrowser.Controller
{
await base.ReloadInternal(progress).ConfigureAwait(false);
ReloadWeatherClient();
ExtractFFMpeg();
ReportProgress(progress, "Loading Users");
ReloadUsers();
ReportProgress(progress, "Loading Media Library");
await ReloadRoot(allowInternetProviders: false).ConfigureAwait(false);
}
@@ -121,8 +132,6 @@ namespace MediaBrowser.Controller
DirectoryWatchers.Stop();
DisposeWeatherClient();
ItemController.PreBeginResolvePath -= ItemController_PreBeginResolvePath;
ItemController.BeginResolvePath -= ItemController_BeginResolvePath;
}
@@ -413,26 +422,5 @@ namespace MediaBrowser.Controller
}
}
}
/// <summary>
/// Disposes the current WeatherClient
/// </summary>
private void DisposeWeatherClient()
{
if (WeatherClient != null)
{
WeatherClient.Dispose();
}
}
/// <summary>
/// Disposes the current WeatherClient and creates a new one
/// </summary>
private void ReloadWeatherClient()
{
DisposeWeatherClient();
WeatherClient = new WeatherClient();
}
}
}

View File

@@ -36,6 +36,7 @@
<Reference Include="System" />
<Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Net.Http.WebRequest" />
<Reference Include="System.Reactive.Core, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
@@ -58,6 +59,9 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Drawing\BaseImageProcessor.cs" />
<Compile Include="Drawing\DrawingUtils.cs" />
<Compile Include="Drawing\ImageProcessor.cs" />
<Compile Include="Entities\Audio.cs" />
<Compile Include="Entities\BaseEntity.cs" />
<Compile Include="Entities\BaseItem.cs" />
@@ -107,7 +111,8 @@
<Compile Include="Resolvers\BaseItemResolver.cs" />
<Compile Include="Resolvers\FolderResolver.cs" />
<Compile Include="Resolvers\VideoResolver.cs" />
<Compile Include="Weather\WeatherClient.cs" />
<Compile Include="Weather\BaseWeatherProvider.cs" />
<Compile Include="Weather\WeatherProvider.cs" />
<Compile Include="Providers\BaseItemXmlParser.cs" />
<Compile Include="Xml\XmlExtensions.cs" />
</ItemGroup>

View File

@@ -0,0 +1,34 @@
using MediaBrowser.Common.Logging;
using MediaBrowser.Model.Weather;
using System;
using System.Net;
using System.Net.Cache;
using System.Net.Http;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.Weather
{
public abstract class BaseWeatherProvider : IDisposable
{
protected HttpClient HttpClient { get; private set; }
protected BaseWeatherProvider()
{
var handler = new WebRequestHandler { };
handler.AutomaticDecompression = DecompressionMethods.Deflate;
handler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.Revalidate);
HttpClient = new HttpClient(handler);
}
public virtual void Dispose()
{
Logger.LogInfo("Disposing " + GetType().Name);
HttpClient.Dispose();
}
public abstract Task<WeatherInfo> GetWeatherInfoAsync(string zipCode);
}
}

View File

@@ -2,11 +2,9 @@
using MediaBrowser.Common.Serialization;
using MediaBrowser.Model.Weather;
using System;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Cache;
using System.Net.Http;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.Weather
@@ -15,21 +13,10 @@ namespace MediaBrowser.Controller.Weather
/// Based on http://www.worldweatheronline.com/free-weather-feed.aspx
/// The classes in this file are a reproduction of the json output, which will then be converted to our weather model classes
/// </summary>
public class WeatherClient : IDisposable
[Export(typeof(BaseWeatherProvider))]
public class WeatherProvider : BaseWeatherProvider
{
private HttpClient HttpClient { get; set; }
public WeatherClient()
{
var handler = new WebRequestHandler { };
handler.AutomaticDecompression = DecompressionMethods.Deflate;
handler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.Revalidate);
HttpClient = new HttpClient(handler);
}
public async Task<WeatherInfo> GetWeatherInfoAsync(string zipCode)
public override async Task<WeatherInfo> GetWeatherInfoAsync(string zipCode)
{
if (string.IsNullOrWhiteSpace(zipCode))
{
@@ -73,11 +60,6 @@ namespace MediaBrowser.Controller.Weather
return info;
}
public void Dispose()
{
HttpClient.Dispose();
}
}
class WeatherResult