diff --git a/src/NzbDrone.Common/IEnumerableExtensions.cs b/src/NzbDrone.Common/IEnumerableExtensions.cs
index 11626292a..e4d3f5bfe 100644
--- a/src/NzbDrone.Common/IEnumerableExtensions.cs
+++ b/src/NzbDrone.Common/IEnumerableExtensions.cs
@@ -12,5 +12,15 @@ namespace NzbDrone.Common
 
             return source.Where(element => knownKeys.Add(keySelector(element)));
         }
+
+        public static void AddIfNotNull<TSource>(this List<TSource> source, TSource item)
+        {
+            if (item == null)
+            {
+                return;
+            }
+
+            source.Add(item);
+        }
     }
 }
\ No newline at end of file
diff --git a/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs
index b6b396d69..cb4c98a57 100644
--- a/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs
+++ b/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs
@@ -21,117 +21,23 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox
 {
     public class RoksboxMetadata : MetadataBase<RoksboxMetadataSettings>
     {
-        private readonly IEventAggregator _eventAggregator;
         private readonly IMapCoversToLocal _mediaCoverService;
-        private readonly IMediaFileService _mediaFileService;
-        private readonly IMetadataFileService _metadataFileService;
         private readonly IDiskProvider _diskProvider;
-        private readonly IHttpProvider _httpProvider;
-        private readonly IEpisodeService _episodeService;
         private readonly Logger _logger;
 
-        public RoksboxMetadata(IEventAggregator eventAggregator,
-                            IMapCoversToLocal mediaCoverService,
-                            IMediaFileService mediaFileService,
-                            IMetadataFileService metadataFileService,
+        public RoksboxMetadata(IMapCoversToLocal mediaCoverService,
                             IDiskProvider diskProvider,
-                            IHttpProvider httpProvider,
-                            IEpisodeService episodeService,
                             Logger logger)
-            : base(diskProvider, httpProvider, logger)
         {
-            _eventAggregator = eventAggregator;
             _mediaCoverService = mediaCoverService;
-            _mediaFileService = mediaFileService;
-            _metadataFileService = metadataFileService;
             _diskProvider = diskProvider;
-            _httpProvider = httpProvider;
-            _episodeService = episodeService;
             _logger = logger;
         }
 
         private static List<string> ValidCertification = new List<string> { "G", "NC-17", "PG", "PG-13", "R", "UR", "UNRATED", "NR", "TV-Y", "TV-Y7", "TV-Y7-FV", "TV-G", "TV-PG", "TV-14", "TV-MA" };
         private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?<season>\d+))|(?<season>specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
 
-        public override void OnSeriesUpdated(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
-        {
-            var metadataFiles = new List<MetadataFile>();
-
-            if (!_diskProvider.FolderExists(series.Path))
-            {
-                _logger.Info("Series folder ({0}) does not exist, skipping metadata creation", series.Path);
-                return;
-            }
-
-            if (Settings.SeriesImages)
-            {
-                var metadata = WriteSeriesImages(series, existingMetadataFiles);
-                if (metadata != null)
-                {
-                    metadataFiles.Add(metadata);
-                }
-            }
-
-            if (Settings.SeasonImages)
-            {
-                var metadata = WriteSeasonImages(series, existingMetadataFiles);
-                if (metadata != null)
-                {
-                    metadataFiles.AddRange(metadata);
-                }
-            }
-
-            foreach (var episodeFile in episodeFiles)
-            {
-                if (Settings.EpisodeMetadata)
-                {
-                    var metadata = WriteEpisodeMetadata(series, episodeFile, existingMetadataFiles);
-                    if (metadata != null)
-                    {
-                        metadataFiles.Add(metadata);
-                    }
-                }
-            }
-
-            foreach (var episodeFile in episodeFiles)
-            {
-                if (Settings.EpisodeImages)
-                {
-                    var metadataFile = WriteEpisodeImages(series, episodeFile, existingMetadataFiles);
-
-                    if (metadataFile != null)
-                    {
-                        metadataFiles.Add(metadataFile);
-                    }
-                }
-            }
-            metadataFiles.RemoveAll(c => c == null);
-            _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles));
-        }
-
-        public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload)
-        {
-            var metadataFiles = new List<MetadataFile>();
-
-            if (Settings.EpisodeMetadata)
-            {
-                metadataFiles.Add(WriteEpisodeMetadata(series, episodeFile, new List<MetadataFile>()));
-            }
-
-            if (Settings.EpisodeImages)
-            {
-                var metadataFile = WriteEpisodeImages(series, episodeFile, new List<MetadataFile>());
-
-                if (metadataFile != null)
-                {
-                    metadataFiles.Add(metadataFile);
-                }
-            }
-
-            _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles));
-        }
-
-        public override void AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
+        public override List<MetadataFile> AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
         {
             var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList();
             var updatedMetadataFiles = new List<MetadataFile>();
@@ -172,7 +78,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox
                 }
             }
 
-            _eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles));
+            return updatedMetadataFiles;
         }
 
         public override MetadataFile FindMetadataFile(Series series, string path)
@@ -237,137 +143,20 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox
             return null;
         }
 
-        private MetadataFile WriteSeriesImages(Series series, List<MetadataFile> existingMetadataFiles)
+        public override MetadataFileResult SeriesMetadata(Series series)
         {
-            //Because we only support one image, attempt to get the Poster type, then if that fails grab the first
-            var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault();
-            if (image == null)
-            {
-                _logger.Trace("Failed to find suitable Series image for series {0}.", series.Title);
-                return null;
-            }
-
-            var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
-            var destination = Path.Combine(series.Path, Path.GetFileName(series.Path) + Path.GetExtension(source));
-
-            //TODO: Do we want to overwrite the file if it exists?
-            if (_diskProvider.FileExists(destination))
-            {
-                _logger.Debug("Series image: {0} already exists.", image.CoverType);
-                return null;
-            }
-            else
-            {
-
-                _diskProvider.CopyFile(source, destination, false);
-
-                var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage) ??
-                                new MetadataFile
-                                {
-                                    SeriesId = series.Id,
-                                    Consumer = GetType().Name,
-                                    Type = MetadataType.SeriesImage,
-                                    RelativePath = DiskProviderBase.GetRelativePath(series.Path, destination)
-                                };
-
-                return metadata;
-            }
-        }
-
-        private IEnumerable<MetadataFile> WriteSeasonImages(Series series, List<MetadataFile> existingMetadataFiles)
-        {
-            _logger.Debug("Writing season images for {0}.", series.Title);
-            //Create a dictionary between season number and output folder
-            var seasonFolderMap = new Dictionary<int, string>();
-            foreach (var folder in Directory.EnumerateDirectories(series.Path))
-            {
-                var directoryinfo = new DirectoryInfo(folder);
-                var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name);
-                if (seasonMatch.Success)
-                {
-                    var seasonNumber = seasonMatch.Groups["season"].Value;
-
-                    if (seasonNumber.Contains("specials"))
-                    {
-                        seasonFolderMap[0] = folder;
-                    }
-                    else
-                    {
-                        int matchedSeason;
-                        if (Int32.TryParse(seasonNumber, out matchedSeason))
-                        {
-                            seasonFolderMap[matchedSeason] = folder;
-                        }
-                        else
-                        {
-                            _logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title);
-                        }
-                    }
-                }
-                else
-                {
-                    _logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title);
-                }
-            }
-            foreach (var season in series.Seasons)
-            {
-                //Work out the path to this season - if we don't have a matching path then skip this season.
-                string seasonFolder;
-                if (!seasonFolderMap.TryGetValue(season.SeasonNumber, out seasonFolder))
-                {
-                    _logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber);
-                    continue;
-                }
-
-                //Roksbox only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection
-                var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault();
-                if (image == null)
-                {
-                    _logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber);
-                    continue;
-                }
-
-
-                var filename = Path.GetFileName(seasonFolder) + ".jpg";
-
-                var path = Path.Combine(series.Path, seasonFolder, filename);
-                _logger.Debug("Writing season image for series {0}, season {1} to {2}.", series.Title, season.SeasonNumber, path);
-                DownloadImage(series, image.Url, path);
-
-                var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage &&
-                                                                            c.SeasonNumber == season.SeasonNumber) ??
-                                new MetadataFile
-                                {
-                                    SeriesId = series.Id,
-                                    SeasonNumber = season.SeasonNumber,
-                                    Consumer = GetType().Name,
-                                    Type = MetadataType.SeasonImage,
-                                    RelativePath = DiskProviderBase.GetRelativePath(series.Path, path)
-                                };
-
-                yield return metadata;
-            }
+            //Series metadata is not supported
+            return null;
         }
 
-        private MetadataFile WriteEpisodeMetadata(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
+        public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile)
         {
-            var filename = GetEpisodeMetadataFilename(episodeFile.Path);
-            var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
-
-            var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata &&
-                                                                              c.EpisodeFileId == episodeFile.Id);
-
-            if (existingMetadata != null)
+            if (!Settings.EpisodeMetadata)
             {
-                var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
-                if (!filename.PathEquals(fullPath))
-                {
-                    _diskProvider.MoveFile(fullPath, filename);
-                    existingMetadata.RelativePath = relativePath;
-                }
+                return null;
             }
 
-            _logger.Debug("Generating {0} for: {1}", filename, episodeFile.Path);
+            _logger.Debug("Generating Episode Metadata for: {0}", episodeFile.Path);
 
             var xmlResult = String.Empty;
             foreach (var episode in episodeFile.Episodes.Value)
@@ -389,7 +178,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox
                     details.Add(new XElement("actors", actors));
                     details.Add(new XElement("description", episode.Overview));
                     details.Add(new XElement("length", series.Runtime));
-                    details.Add(new XElement("mpaa", ValidCertification.Contains( series.Certification.ToUpperInvariant() ) ? series.Certification.ToUpperInvariant() : "UNRATED" ) );
+                    details.Add(new XElement("mpaa", ValidCertification.Contains(series.Certification.ToUpperInvariant()) ? series.Certification.ToUpperInvariant() : "UNRATED"));
                     doc.Add(details);
                     doc.Save(xw);
 
@@ -397,62 +186,61 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox
                     xmlResult += Environment.NewLine;
                 }
             }
-            
-            _logger.Debug("Saving episodedetails to: {0}", filename);
-            _diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray()));
-
-            var metadata = existingMetadata ??
-                           new MetadataFile
-                           {
-                               SeriesId = series.Id,
-                               EpisodeFileId = episodeFile.Id,
-                               Consumer = GetType().Name,
-                               Type = MetadataType.EpisodeMetadata,
-                               RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
-                           };
 
-            return metadata;
+            return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.Path), xmlResult.Trim(Environment.NewLine.ToCharArray()));
         }
 
-        private MetadataFile WriteEpisodeImages(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
+        public override List<ImageFileResult> SeriesImages(Series series)
         {
-            var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
-
-            if (screenshot == null)
+            var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault();
+            if (image == null)
             {
-                _logger.Trace("Episode screenshot not available");
+                _logger.Trace("Failed to find suitable Series image for series {0}.", series.Title);
                 return null;
             }
 
-            var filename = GetEpisodeImageFilename(episodeFile.Path);
-            var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
+            var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
+            var destination = Path.Combine(series.Path, Path.GetFileName(series.Path) + Path.GetExtension(source));
 
-            var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeImage &&
-                                                                              c.EpisodeFileId == episodeFile.Id);
+            return new List<ImageFileResult>{ new ImageFileResult(destination, source) };
+        }
+
+        public override List<ImageFileResult> SeasonImages(Series series, Season season)
+        {
+            var seasonFolders = GetSeasonFolders(series);
 
-            if (existingMetadata != null)
+            string seasonFolder;
+            if (!seasonFolders.TryGetValue(season.SeasonNumber, out seasonFolder))
             {
-                var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
-                if (!filename.PathEquals(fullPath))
-                {
-                    _diskProvider.MoveFile(fullPath, filename);
-                    existingMetadata.RelativePath = relativePath;
-                }
+                _logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber);
+                return new List<ImageFileResult>();
             }
 
-            DownloadImage(series, screenshot.Url, filename);
+            //Roksbox only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection
+            var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault();
+            if (image == null)
+            {
+                _logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber);
+                return new List<ImageFileResult>();
+            }
 
-            var metadata = existingMetadata ??
-                           new MetadataFile
-                           {
-                               SeriesId = series.Id,
-                               EpisodeFileId = episodeFile.Id,
-                               Consumer = GetType().Name,
-                               Type = MetadataType.EpisodeImage,
-                               RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
-                           };
+            var filename = Path.GetFileName(seasonFolder) + ".jpg";
+            var path = Path.Combine(series.Path, seasonFolder, filename);
+
+            return new List<ImageFileResult> { new ImageFileResult(path, image.Url) };
+        }
+
+        public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile)
+        {
+            var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
 
-            return metadata;
+            if (screenshot == null)
+            {
+                _logger.Trace("Episode screenshot not available");
+                return new List<ImageFileResult>();
+            }
+
+            return new List<ImageFileResult> {new ImageFileResult(GetEpisodeImageFilename(episodeFile.Path), screenshot.Url)};
         }
 
         private string GetEpisodeMetadataFilename(string episodeFilePath)
@@ -464,5 +252,44 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox
         {
             return Path.ChangeExtension(episodeFilePath, "jpg");
         }
+
+        private Dictionary<Int32, String> GetSeasonFolders(Series series)
+        {
+            var seasonFolderMap = new Dictionary<Int32, String>();
+
+            foreach (var folder in _diskProvider.GetDirectories(series.Path))
+            {
+                var directoryinfo = new DirectoryInfo(folder);
+                var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name);
+
+                if (seasonMatch.Success)
+                {
+                    var seasonNumber = seasonMatch.Groups["season"].Value;
+
+                    if (seasonNumber.Contains("specials"))
+                    {
+                        seasonFolderMap[0] = folder;
+                    }
+                    else
+                    {
+                        int matchedSeason;
+                        if (Int32.TryParse(seasonNumber, out matchedSeason))
+                        {
+                            seasonFolderMap[matchedSeason] = folder;
+                        }
+                        else
+                        {
+                            _logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title);
+                        }
+                    }
+                }
+                else
+                {
+                    _logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title);
+                }
+            }
+
+            return seasonFolderMap;
+        }
     }
 }
diff --git a/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs
index e76a341bc..37ca71d0f 100644
--- a/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs
+++ b/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.IO;
 using System.Linq;
 using System.Runtime.Remoting.Messaging;
@@ -21,116 +22,22 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv
 {
     public class WdtvMetadata : MetadataBase<WdtvMetadataSettings>
     {
-        private readonly IEventAggregator _eventAggregator;
         private readonly IMapCoversToLocal _mediaCoverService;
-        private readonly IMediaFileService _mediaFileService;
-        private readonly IMetadataFileService _metadataFileService;
         private readonly IDiskProvider _diskProvider;
-        private readonly IHttpProvider _httpProvider;
-        private readonly IEpisodeService _episodeService;
         private readonly Logger _logger;
 
-        public WdtvMetadata(IEventAggregator eventAggregator,
-                            IMapCoversToLocal mediaCoverService,
-                            IMediaFileService mediaFileService,
-                            IMetadataFileService metadataFileService,
+        public WdtvMetadata(IMapCoversToLocal mediaCoverService,
                             IDiskProvider diskProvider,
-                            IHttpProvider httpProvider,
-                            IEpisodeService episodeService,
                             Logger logger)
-            : base(diskProvider, httpProvider, logger)
         {
-            _eventAggregator = eventAggregator;
             _mediaCoverService = mediaCoverService;
-            _mediaFileService = mediaFileService;
-            _metadataFileService = metadataFileService;
             _diskProvider = diskProvider;
-            _httpProvider = httpProvider;
-            _episodeService = episodeService;
             _logger = logger;
         }
 
         private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?<season>\d+))|(?<season>specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
 
-        public override void OnSeriesUpdated(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
-        {
-            var metadataFiles = new List<MetadataFile>();
-
-            if (!_diskProvider.FolderExists(series.Path))
-            {
-                _logger.Info("Series folder ({0}) does not exist, skipping metadata creation", series.Path);
-                return;
-            }
-
-            if (Settings.SeriesImages)
-            {
-                var metadata = WriteSeriesImages(series, existingMetadataFiles);
-                if (metadata != null)
-                {
-                    metadataFiles.Add(metadata);
-                }
-            }
-
-            if (Settings.SeasonImages)
-            {
-                var metadata = WriteSeasonImages(series, existingMetadataFiles);
-                if (metadata != null)
-                {
-                    metadataFiles.AddRange(metadata);
-                }
-            }
-
-            foreach (var episodeFile in episodeFiles)
-            {
-                if (Settings.EpisodeMetadata)
-                {
-                    var metadata = WriteEpisodeMetadata(series, episodeFile, existingMetadataFiles);
-                    if (metadata != null)
-                    {
-                        metadataFiles.Add(metadata);
-                    }
-                }
-            }
-
-            foreach (var episodeFile in episodeFiles)
-            {
-                if (Settings.EpisodeImages)
-                {
-                    var metadataFile = WriteEpisodeImages(series, episodeFile, existingMetadataFiles);
-
-                    if (metadataFile != null)
-                    {
-                        metadataFiles.Add(metadataFile);
-                    }
-                }
-            }
-            metadataFiles.RemoveAll(c => c == null);
-            _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles));
-        }
-
-        public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload)
-        {
-            var metadataFiles = new List<MetadataFile>();
-
-            if (Settings.EpisodeMetadata)
-            {
-                metadataFiles.Add(WriteEpisodeMetadata(series, episodeFile, new List<MetadataFile>()));
-            }
-
-            if (Settings.EpisodeImages)
-            {
-                var metadataFile = WriteEpisodeImages(series, episodeFile, new List<MetadataFile>());
-
-                if (metadataFile != null)
-                {
-                    metadataFiles.Add(metadataFile);
-                }
-            }
-
-            _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles));
-        }
-
-        public override void AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
+        public override List<MetadataFile> AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
         {
             var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList();
             var updatedMetadataFiles = new List<MetadataFile>();
@@ -170,8 +77,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv
                     }
                 }
             }
-
-            _eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles));
+            return updatedMetadataFiles;
         }
 
         public override MetadataFile FindMetadataFile(Series series, string path)
@@ -236,137 +142,20 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv
             return null;
         }
 
-        private MetadataFile WriteSeriesImages(Series series, List<MetadataFile> existingMetadataFiles)
-        {
-            //Because we only support one image, attempt to get the Poster type, then if that fails grab the first
-            var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault();
-            if (image == null)
-            {
-                _logger.Trace("Failed to find suitable Series image for series {0}.", series.Title);
-                return null;
-            }
-
-            var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
-            var destination = Path.Combine(series.Path, "folder" + Path.GetExtension(source));
-
-            //TODO: Do we want to overwrite the file if it exists?
-            if (_diskProvider.FileExists(destination))
-            {
-                _logger.Debug("Series image: {0} already exists.", image.CoverType);
-                return null;
-            }
-            else
-            {
-
-                _diskProvider.CopyFile(source, destination, false);
-
-                var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage) ??
-                                new MetadataFile
-                                {
-                                    SeriesId = series.Id,
-                                    Consumer = GetType().Name,
-                                    Type = MetadataType.SeriesImage,
-                                    RelativePath = DiskProviderBase.GetRelativePath(series.Path, destination)
-                                };
-
-                return metadata;
-            }
-        }
-
-        private IEnumerable<MetadataFile> WriteSeasonImages(Series series, List<MetadataFile> existingMetadataFiles)
+        public override MetadataFileResult SeriesMetadata(Series series)
         {
-            _logger.Debug("Writing season images for {0}.", series.Title);
-            //Create a dictionary between season number and output folder
-            var seasonFolderMap = new Dictionary<int, string>();
-            foreach (var folder in Directory.EnumerateDirectories(series.Path))
-            {
-                var directoryinfo = new DirectoryInfo(folder);
-                var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name);
-                if (seasonMatch.Success)
-                {
-                    var seasonNumber = seasonMatch.Groups["season"].Value;
-
-                    if (seasonNumber.Contains("specials"))
-                    {
-                        seasonFolderMap[0] = folder;
-                    }
-                    else
-                    {
-                        int matchedSeason;
-                        if (Int32.TryParse(seasonNumber, out matchedSeason))
-                        {
-                            seasonFolderMap[matchedSeason] = folder;
-                        }
-                        else
-                        {
-                            _logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title);
-                        }
-                    }
-                }
-                else
-                {
-                    _logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title);
-                }
-            }
-            foreach (var season in series.Seasons)
-            {
-                //Work out the path to this season - if we don't have a matching path then skip this season.
-                string seasonFolder;
-                if (!seasonFolderMap.TryGetValue(season.SeasonNumber, out seasonFolder))
-                {
-                    _logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber);
-                    continue;
-                }
-
-                //WDTV only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection
-                var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault();
-                if (image == null)
-                {
-                    _logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber);
-                    continue;
-                }
-
-                    
-                var filename = "folder.jpg";
-
-                var path = Path.Combine(series.Path, seasonFolder, filename);
-                _logger.Debug("Writing season image for series {0}, season {1} to {2}.", series.Title, season.SeasonNumber, path);
-                DownloadImage(series, image.Url, path);
-
-                var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage &&
-                                                                            c.SeasonNumber == season.SeasonNumber) ??
-                                new MetadataFile
-                                {
-                                    SeriesId = series.Id,
-                                    SeasonNumber = season.SeasonNumber,
-                                    Consumer = GetType().Name,
-                                    Type = MetadataType.SeasonImage,
-                                    RelativePath = DiskProviderBase.GetRelativePath(series.Path, path)
-                                };
-
-                yield return metadata;
-            }
+            //Series metadata is not supported
+            return null;
         }
 
-        private MetadataFile WriteEpisodeMetadata(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
+        public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile)
         {
-            var filename = GetEpisodeMetadataFilename(episodeFile.Path);
-            var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
-
-            var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata &&
-                                                                              c.EpisodeFileId == episodeFile.Id);
-
-            if (existingMetadata != null)
+            if (!Settings.EpisodeMetadata)
             {
-                var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
-                if (!filename.PathEquals(fullPath))
-                {
-                    _diskProvider.MoveFile(fullPath, filename);
-                    existingMetadata.RelativePath = relativePath;
-                }
+                return null;
             }
 
-            _logger.Debug("Generating {0} for: {1}", filename, episodeFile.Path);
+            _logger.Debug("Generating Episode Metadata for: {0}", episodeFile.Path);
 
             var xmlResult = String.Empty;
             foreach (var episode in episodeFile.Episodes.Value)
@@ -404,62 +193,82 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv
                     xmlResult += Environment.NewLine;
                 }
             }
-            
-            _logger.Debug("Saving episodedetails to: {0}", filename);
-            _diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray()));
 
-            var metadata = existingMetadata ??
-                           new MetadataFile
-                           {
-                               SeriesId = series.Id,
-                               EpisodeFileId = episodeFile.Id,
-                               Consumer = GetType().Name,
-                               Type = MetadataType.EpisodeMetadata,
-                               RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
-                           };
+            var filename = GetEpisodeMetadataFilename(episodeFile.Path);
 
-            return metadata;
+            return new MetadataFileResult(filename, xmlResult.Trim(Environment.NewLine.ToCharArray()));
         }
 
-        private MetadataFile WriteEpisodeImages(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
+        public override List<ImageFileResult> SeriesImages(Series series)
         {
-            var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
+            if (!Settings.SeriesImages)
+            {
+                return new List<ImageFileResult>();
+            }
 
-            if (screenshot == null)
+            //Because we only support one image, attempt to get the Poster type, then if that fails grab the first
+            var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault();
+            if (image == null)
             {
-                _logger.Trace("Episode screenshot not available");
-                return null;
+                _logger.Trace("Failed to find suitable Series image for series {0}.", series.Title);
+                return new List<ImageFileResult>();
             }
 
-            var filename = GetEpisodeImageFilename(episodeFile.Path);
-            var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
+            var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
+            var destination = Path.Combine(series.Path, "folder" + Path.GetExtension(source));
 
-            var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeImage &&
-                                                                              c.EpisodeFileId == episodeFile.Id);
+            return new List<ImageFileResult>
+                   {
+                       new ImageFileResult(destination, source)
+                   };
+        }
 
-            if (existingMetadata != null)
+        public override List<ImageFileResult> SeasonImages(Series series, Season season)
+        {
+            if (!Settings.SeasonImages)
             {
-                var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
-                if (!filename.PathEquals(fullPath))
-                {
-                    _diskProvider.MoveFile(fullPath, filename);
-                    existingMetadata.RelativePath = relativePath;
-                }
+                return new List<ImageFileResult>();
             }
+            
+            var seasonFolders = GetSeasonFolders(series);
 
-            DownloadImage(series, screenshot.Url, filename);
+            //Work out the path to this season - if we don't have a matching path then skip this season.
+            string seasonFolder;
+            if (!seasonFolders.TryGetValue(season.SeasonNumber, out seasonFolder))
+            {
+                _logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber);
+                return new List<ImageFileResult>();
+            }
 
-            var metadata = existingMetadata ??
-                           new MetadataFile
-                           {
-                               SeriesId = series.Id,
-                               EpisodeFileId = episodeFile.Id,
-                               Consumer = GetType().Name,
-                               Type = MetadataType.EpisodeImage,
-                               RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
-                           };
+            //WDTV only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection
+            var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault();
+            if (image == null)
+            {
+                _logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber);
+                return new List<ImageFileResult>();
+            }
+
+            var path = Path.Combine(series.Path, seasonFolder, "folder.jpg");
 
-            return metadata;
+            return new List<ImageFileResult>{ new ImageFileResult(path, image.Url) };
+        }
+
+        public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile)
+        {
+            if (!Settings.EpisodeImages)
+            {
+                return new List<ImageFileResult>();
+            }
+
+            var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
+
+            if (screenshot == null)
+            {
+                _logger.Trace("Episode screenshot not available");
+                return new List<ImageFileResult>();
+            }
+
+            return new List<ImageFileResult>{ new ImageFileResult(GetEpisodeImageFilename(episodeFile.Path), screenshot.Url) };
         }
 
         private string GetEpisodeMetadataFilename(string episodeFilePath)
@@ -471,5 +280,45 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv
         {
             return Path.ChangeExtension(episodeFilePath, "metathumb");
         }
+
+        private Dictionary<Int32, String> GetSeasonFolders(Series series)
+        {
+            var seasonFolderMap = new Dictionary<Int32, String>();
+
+            foreach (var folder in _diskProvider.GetDirectories(series.Path))
+            {
+                var directoryinfo = new DirectoryInfo(folder);
+                var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name);
+
+                if (seasonMatch.Success)
+                {
+                    var seasonNumber = seasonMatch.Groups["season"].Value;
+
+                    if (seasonNumber.Contains("specials"))
+                    {
+                        seasonFolderMap[0] = folder;
+                    }
+                    else
+                    {
+                        int matchedSeason;
+                        if (Int32.TryParse(seasonNumber, out matchedSeason))
+                        {
+                            seasonFolderMap[matchedSeason] = folder;
+                        }
+                        else
+                        {
+                            _logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title);
+                        }
+                    }
+                }
+
+                else
+                {
+                    _logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title);
+                }
+            }
+
+            return seasonFolderMap;
+        }
     }
 }
diff --git a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs
index 4089c6863..806843df9 100644
--- a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs
+++ b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs
@@ -19,32 +19,16 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
 {
     public class XbmcMetadata : MetadataBase<XbmcMetadataSettings>
     {
-        private readonly IEventAggregator _eventAggregator;
         private readonly IMapCoversToLocal _mediaCoverService;
-        private readonly IMediaFileService _mediaFileService;
-        private readonly IMetadataFileService _metadataFileService;
         private readonly IDiskProvider _diskProvider;
-        private readonly IHttpProvider _httpProvider;
-        private readonly IEpisodeService _episodeService;
         private readonly Logger _logger;
 
-        public XbmcMetadata(IEventAggregator eventAggregator,
-                            IMapCoversToLocal mediaCoverService,
-                            IMediaFileService mediaFileService,
-                            IMetadataFileService metadataFileService,
+        public XbmcMetadata(IMapCoversToLocal mediaCoverService,
                             IDiskProvider diskProvider,
-                            IHttpProvider httpProvider,
-                            IEpisodeService episodeService,
                             Logger logger)
-            : base(diskProvider, httpProvider, logger)
         {
-            _eventAggregator = eventAggregator;
             _mediaCoverService = mediaCoverService;
-            _mediaFileService = mediaFileService;
-            _metadataFileService = metadataFileService;
             _diskProvider = diskProvider;
-            _httpProvider = httpProvider;
-            _episodeService = episodeService;
             _logger = logger;
         }
 
@@ -52,79 +36,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
         private static readonly Regex SeasonImagesRegex = new Regex(@"^season(?<season>\d{2,}|-all|-specials)-(?<type>poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
         private static readonly Regex EpisodeImageRegex = new Regex(@"-thumb\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
 
-        public override void OnSeriesUpdated(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
-        {
-            var metadataFiles = new List<MetadataFile>();
-
-            if (!_diskProvider.FolderExists(series.Path))
-            {
-                _logger.Info("Series folder does not exist, skipping metadata creation");
-                return;
-            }
-
-            if (Settings.SeriesMetadata)
-            {
-                metadataFiles.Add(WriteTvShowNfo(series, existingMetadataFiles));
-            }
-
-            if (Settings.SeriesImages)
-            {
-                metadataFiles.AddRange(WriteSeriesImages(series, existingMetadataFiles));
-            }
-
-            if (Settings.SeasonImages)
-            {
-                metadataFiles.AddRange(WriteSeasonImages(series, existingMetadataFiles));
-            }
-
-            foreach (var episodeFile in episodeFiles)
-            {
-                if (Settings.EpisodeMetadata)
-                {
-                    metadataFiles.Add(WriteEpisodeNfo(series, episodeFile, existingMetadataFiles));
-                }
-            }
-
-            foreach (var episodeFile in episodeFiles)
-            {
-                if (Settings.EpisodeImages)
-                {
-                    var metadataFile = WriteEpisodeImages(series, episodeFile, existingMetadataFiles);
-
-                    if (metadataFile != null)
-                    {
-                        metadataFiles.Add(metadataFile);
-                    }
-                }
-            }
-
-            _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles));
-        }
-
-        public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload)
-        {
-            var metadataFiles = new List<MetadataFile>();
-
-            if (Settings.EpisodeMetadata)
-            {
-                metadataFiles.Add(WriteEpisodeNfo(series, episodeFile, new List<MetadataFile>()));
-            }
-
-            if (Settings.EpisodeImages)
-            {
-                var metadataFile = WriteEpisodeImages(series, episodeFile, new List<MetadataFile>());
-
-                if (metadataFile != null)
-                {
-                    metadataFiles.Add(metadataFile);
-                }
-                WriteEpisodeImages(series, episodeFile, new List<MetadataFile>());
-            }
-
-            _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles));
-        }
-
-        public override void AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
+        public override List<MetadataFile> AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
         {
             var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList();
             var updatedMetadataFiles = new List<MetadataFile>();
@@ -165,7 +77,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
                 }
             }
 
-            _eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles));
+            return updatedMetadataFiles;
         }
 
         public override MetadataFile FindMetadataFile(Series series, string path)
@@ -239,8 +151,13 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
             return null;
         }
 
-        private MetadataFile WriteTvShowNfo(Series series, List<MetadataFile> existingMetadataFiles)
+        public override MetadataFileResult SeriesMetadata(Series series)
         {
+            if (!Settings.SeriesMetadata)
+            {
+                return null;
+            }
+
             _logger.Debug("Generating tvshow.nfo for: {0}", series.Title);
             var sb = new StringBuilder();
             var xws = new XmlWriterSettings();
@@ -254,7 +171,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
                 var tvShow = new XElement("tvshow");
 
                 tvShow.Add(new XElement("title", series.Title));
-                tvShow.Add(new XElement("rating", (decimal)series.Ratings.Percentage/10));
+                tvShow.Add(new XElement("rating", (decimal) series.Ratings.Percentage/10));
                 tvShow.Add(new XElement("plot", series.Overview));
                 tvShow.Add(new XElement("episodeguide", new XElement("url", episodeGuideUrl)));
                 tvShow.Add(new XElement("episodeguideurl", episodeGuideUrl));
@@ -276,10 +193,10 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
                 foreach (var actor in series.Actors)
                 {
                     tvShow.Add(new XElement("actor",
-                                    new XElement("name", actor.Name),
-                                    new XElement("role", actor.Character),
-                                    new XElement("thumb", actor.Images.First().Url)
-                            ));
+                        new XElement("name", actor.Name),
+                        new XElement("role", actor.Character),
+                        new XElement("thumb", actor.Images.First().Url)
+                        ));
                 }
 
                 var doc = new XDocument(tvShow);
@@ -287,108 +204,13 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
 
                 _logger.Debug("Saving tvshow.nfo for {0}", series.Title);
 
-                var path = Path.Combine(series.Path, "tvshow.nfo");
-
-                _diskProvider.WriteAllText(path, doc.ToString());
-
-                var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesMetadata) ??
-                               new MetadataFile
-                               {
-                                   SeriesId = series.Id,
-                                   Consumer = GetType().Name,
-                                   Type = MetadataType.SeriesMetadata,
-                                   RelativePath = DiskProviderBase.GetRelativePath(series.Path, path)
-                               };
-
-                return metadata;
-            }
-        }
-
-        private IEnumerable<MetadataFile> WriteSeriesImages(Series series, List<MetadataFile> existingMetadataFiles)
-        {
-            foreach (var image in series.Images)
-            {
-                var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
-                var destination = Path.Combine(series.Path, image.CoverType.ToString().ToLowerInvariant() + Path.GetExtension(source));
-
-                //TODO: Do we want to overwrite the file if it exists?
-                if (_diskProvider.FileExists(destination))
-                {
-                    _logger.Debug("Series image: {0} already exists.", image.CoverType);
-                    continue;
-                }
-
-                _diskProvider.CopyFile(source, destination, false);
-                var relativePath = DiskProviderBase.GetRelativePath(series.Path, destination);
-
-                var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage &&
-                                                                          c.RelativePath == relativePath) ??
-                               new MetadataFile
-                               {
-                                   SeriesId = series.Id,
-                                   Consumer = GetType().Name,
-                                   Type = MetadataType.SeriesImage,
-                                   RelativePath = relativePath
-                               };
-
-                yield return metadata;
+                return new MetadataFileResult(Path.Combine(series.Path, "tvshow.nfo"), doc.ToString());
             }
         }
 
-        private IEnumerable<MetadataFile> WriteSeasonImages(Series series, List<MetadataFile> existingMetadataFiles)
-        {
-            foreach (var season in series.Seasons)
-            {
-                foreach (var image in season.Images)
-                {
-                    var filename = String.Format("season{0:00}-{1}.jpg", season.SeasonNumber, image.CoverType.ToString().ToLower());
-
-                    if (season.SeasonNumber == 0)
-                    {
-                        filename = String.Format("season-specials-{0}.jpg", image.CoverType.ToString().ToLower());
-                    }
-
-                    var path = Path.Combine(series.Path, filename);
-                    var relativePath = DiskProviderBase.GetRelativePath(series.Path, path);
-
-                    DownloadImage(series, image.Url, path);
-                    
-                    var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage &&
-                                                                              c.SeasonNumber == season.SeasonNumber &&
-                                                                              c.RelativePath == relativePath) ??
-                                   new MetadataFile
-                                   {
-                                       SeriesId = series.Id,
-                                       SeasonNumber = season.SeasonNumber,
-                                       Consumer = GetType().Name,
-                                       Type = MetadataType.SeasonImage,
-                                       RelativePath = relativePath
-                                   };
-
-                    yield return metadata;
-                }
-            }
-        }
-
-        private MetadataFile WriteEpisodeNfo(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
-        {
-            var filename = GetEpisodeNfoFilename(episodeFile.Path);
-            var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
-
-            var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata &&
-                                                                              c.EpisodeFileId == episodeFile.Id);
-
-            if (existingMetadata != null)
-            {
-                var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
-                if (!filename.PathEquals(fullPath))
-                {
-                    _diskProvider.MoveFile(fullPath, filename);
-                    existingMetadata.RelativePath = relativePath;
-                }
-            }
-
-            _logger.Debug("Generating {0} for: {1}", filename, episodeFile.Path);
+        public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile)
+        {          
+            _logger.Debug("Generating Episode Metadata for: {0}", episodeFile.Path);
 
             var xmlResult = String.Empty;
             foreach (var episode in episodeFile.Episodes.Value)
@@ -423,9 +245,9 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
                     {
                         details.Add(new XElement("thumb", image.Url));
                     }
-                    
+
                     details.Add(new XElement("watched", "false"));
-                    details.Add(new XElement("rating", (decimal)episode.Ratings.Percentage/10));
+                    details.Add(new XElement("rating", (decimal)episode.Ratings.Percentage / 10));
 
                     //Todo: get guest stars, writer and director
                     //details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault()));
@@ -438,25 +260,37 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
                     xmlResult += Environment.NewLine;
                 }
             }
-            
-            _logger.Debug("Saving episodedetails to: {0}", filename);
-            _diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray()));
 
-            var metadata = existingMetadata ??
-                           new MetadataFile
-                           {
-                               SeriesId = series.Id,
-                               EpisodeFileId = episodeFile.Id,
-                               Consumer = GetType().Name,
-                               Type = MetadataType.EpisodeMetadata,
-                               RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
-                           };
+            return new MetadataFileResult(GetEpisodeNfoFilename(episodeFile.Path), xmlResult.Trim(Environment.NewLine.ToCharArray()));
+        }
 
-            return metadata;
+        public override List<ImageFileResult> SeriesImages(Series series)
+        {
+            if (!Settings.SeriesImages)
+            {
+                return new List<ImageFileResult>();
+            }
+
+            return ProcessSeriesImages(series).ToList();
         }
 
-        private MetadataFile WriteEpisodeImages(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
+        public override List<ImageFileResult> SeasonImages(Series series, Season season)
         {
+            if (!Settings.SeasonImages)
+            {
+                return new List<ImageFileResult>();
+            }
+
+            return ProcessSeasonImages(series, season).ToList();
+        }
+
+        public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile)
+        {
+            if (!Settings.EpisodeImages)
+            {
+                return new List<ImageFileResult>();
+            }
+
             var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
 
             if (screenshot == null)
@@ -465,35 +299,38 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
                 return null;
             }
 
-            var filename = GetEpisodeImageFilename(episodeFile.Path);
-            var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
+            return new List<ImageFileResult>
+                   {
+                       new ImageFileResult(GetEpisodeImageFilename(episodeFile.Path), screenshot.Url)
+                   };
+        }
+
+        private IEnumerable<ImageFileResult> ProcessSeriesImages(Series series)
+        {
+            foreach (var image in series.Images)
+            {
+                var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
+                var destination = Path.Combine(series.Path, image.CoverType.ToString().ToLowerInvariant() + Path.GetExtension(source));
 
-            var existingMetadata = existingMetadataFiles.FirstOrDefault(c => c.Type == MetadataType.EpisodeImage &&
-                                                                              c.EpisodeFileId == episodeFile.Id);
+                yield return new ImageFileResult(destination, source);
+            }
+        }
 
-            if (existingMetadata != null)
+        private IEnumerable<ImageFileResult> ProcessSeasonImages(Series series, Season season)
+        {
+            foreach (var image in season.Images)
             {
-                var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
-                if (!filename.PathEquals(fullPath))
+                var filename = String.Format("season{0:00}-{1}.jpg", season.SeasonNumber, image.CoverType.ToString().ToLower());
+
+                if (season.SeasonNumber == 0)
                 {
-                    _diskProvider.MoveFile(fullPath, filename);
-                    existingMetadata.RelativePath = relativePath;
+                    filename = String.Format("season-specials-{0}.jpg", image.CoverType.ToString().ToLower());
                 }
-            }
 
-            DownloadImage(series, screenshot.Url, filename);
+                var path = Path.Combine(series.Path, filename);
 
-            var metadata = existingMetadata ??
-                           new MetadataFile
-                           {
-                               SeriesId = series.Id,
-                               EpisodeFileId = episodeFile.Id,
-                               Consumer = GetType().Name,
-                               Type = MetadataType.EpisodeImage,
-                               RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
-                           };
-
-            return metadata;
+                yield return new ImageFileResult(Path.Combine(series.Path, filename), image.Url);
+            }
         }
 
         private string GetEpisodeNfoFilename(string episodeFilePath)
diff --git a/src/NzbDrone.Core/MetaData/Files/ImageFileResult.cs b/src/NzbDrone.Core/MetaData/Files/ImageFileResult.cs
new file mode 100644
index 000000000..e0330be8a
--- /dev/null
+++ b/src/NzbDrone.Core/MetaData/Files/ImageFileResult.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace NzbDrone.Core.Metadata.Files
+{
+    public class ImageFileResult
+    {
+        public String Path { get; set; }
+        public String Url { get; set; }
+
+        public ImageFileResult(string path, string url)
+        {
+            Path = path;
+            Url = url;
+        }
+    }
+}
diff --git a/src/NzbDrone.Core/MetaData/Files/MetadataFileResult.cs b/src/NzbDrone.Core/MetaData/Files/MetadataFileResult.cs
new file mode 100644
index 000000000..1152bd487
--- /dev/null
+++ b/src/NzbDrone.Core/MetaData/Files/MetadataFileResult.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace NzbDrone.Core.Metadata.Files
+{
+    public class MetadataFileResult
+    {
+        public String Path { get; set; }
+        public String Contents { get; set; }
+
+        public MetadataFileResult(string path, string contents)
+        {
+            Path = path;
+            Contents = contents;
+        }
+    }
+}
diff --git a/src/NzbDrone.Core/MetaData/IMetadata.cs b/src/NzbDrone.Core/MetaData/IMetadata.cs
index 63fa19d73..f9c1feae3 100644
--- a/src/NzbDrone.Core/MetaData/IMetadata.cs
+++ b/src/NzbDrone.Core/MetaData/IMetadata.cs
@@ -8,9 +8,14 @@ namespace NzbDrone.Core.Metadata
 {
     public interface IMetadata : IProvider
     {
-        void OnSeriesUpdated(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles);
-        void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload);
-        void AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles);
+        List<MetadataFile> AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles);
         MetadataFile FindMetadataFile(Series series, string path);
+
+        MetadataFileResult SeriesMetadata(Series series);
+        MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile);
+        List<ImageFileResult> SeriesImages(Series series);
+        List<ImageFileResult> SeasonImages(Series series, Season season);
+        List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile);
+
     }
 }
diff --git a/src/NzbDrone.Core/MetaData/MetadataService.cs b/src/NzbDrone.Core/MetaData/MetadataService.cs
index 681eed8d0..ff44e044d 100644
--- a/src/NzbDrone.Core/MetaData/MetadataService.cs
+++ b/src/NzbDrone.Core/MetaData/MetadataService.cs
@@ -1,6 +1,12 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.IO;
 using System.Linq;
+using System.Linq.Expressions;
+using System.Net;
 using NLog;
+using NzbDrone.Common;
+using NzbDrone.Common.Disk;
 using NzbDrone.Core.Datastore;
 using NzbDrone.Core.MediaCover;
 using NzbDrone.Core.MediaFiles;
@@ -21,6 +27,9 @@ namespace NzbDrone.Core.Metadata
         private readonly ICleanMetadataService _cleanMetadataService;
         private readonly IMediaFileService _mediaFileService;
         private readonly IEpisodeService _episodeService;
+        private readonly IDiskProvider _diskProvider;
+        private readonly IHttpProvider _httpProvider;
+        private readonly IEventAggregator _eventAggregator;
         private readonly Logger _logger;
 
         public MetadataService(IMetadataFactory metadataFactory,
@@ -28,6 +37,9 @@ namespace NzbDrone.Core.Metadata
                                ICleanMetadataService cleanMetadataService,
                                IMediaFileService mediaFileService,
                                IEpisodeService episodeService,
+                               IDiskProvider diskProvider,
+                               IHttpProvider httpProvider,
+                               IEventAggregator eventAggregator,
                                Logger logger)
         {
             _metadataFactory = metadataFactory;
@@ -35,17 +47,41 @@ namespace NzbDrone.Core.Metadata
             _cleanMetadataService = cleanMetadataService;
             _mediaFileService = mediaFileService;
             _episodeService = episodeService;
+            _diskProvider = diskProvider;
+            _httpProvider = httpProvider;
+            _eventAggregator = eventAggregator;
             _logger = logger;
         }
 
         public void Handle(MediaCoversUpdatedEvent message)
         {
             _cleanMetadataService.Clean(message.Series);
-            var seriesMetadata = _metadataFileService.GetFilesBySeries(message.Series.Id);
+
+            if (!_diskProvider.FolderExists(message.Series.Path))
+            {
+                _logger.Info("Series folder does not exist, skipping metadata creation");
+                return;
+            }
+
+            var seriesMetadataFiles = _metadataFileService.GetFilesBySeries(message.Series.Id);
+            var episodeFiles = GetEpisodeFiles(message.Series.Id);
 
             foreach (var consumer in _metadataFactory.Enabled())
             {
-                consumer.OnSeriesUpdated(message.Series, GetMetadataFilesForConsumer(consumer, seriesMetadata), GetEpisodeFiles(message.Series.Id));
+                var consumerFiles = GetMetadataFilesForConsumer(consumer, seriesMetadataFiles);
+                var files = new List<MetadataFile>();
+
+                files.AddIfNotNull(ProcessSeriesMetadata(consumer, message.Series, consumerFiles));
+                files.AddRange(ProcessSeriesImages(consumer, message.Series, consumerFiles));
+                files.AddRange(ProcessSeasonImages(consumer, message.Series, consumerFiles));
+
+                foreach (var episodeFile in episodeFiles)
+                {
+                    files.AddIfNotNull(ProcessEpisodeMetadata(consumer, message.Series, episodeFile, consumerFiles));
+                    files.AddRange(ProcessEpisodeImages(consumer, message.Series, episodeFile, consumerFiles));
+                }
+
+                _eventAggregator.PublishEvent(new MetadataFilesUpdated(files));
             }
         }
 
@@ -53,17 +89,27 @@ namespace NzbDrone.Core.Metadata
         {
             foreach (var consumer in _metadataFactory.Enabled())
             {
-                consumer.OnEpisodeImport(message.EpisodeInfo.Series, message.ImportedEpisode, message.NewDownload);
+                var files = new List<MetadataFile>();
+
+                files.AddIfNotNull(ProcessEpisodeMetadata(consumer, message.EpisodeInfo.Series, message.ImportedEpisode, new List<MetadataFile>()));
+                files.AddRange(ProcessEpisodeImages(consumer, message.EpisodeInfo.Series, message.ImportedEpisode, new List<MetadataFile>()));
+
+                _eventAggregator.PublishEvent(new MetadataFilesUpdated(files));
             }
         }
 
         public void Handle(SeriesRenamedEvent message)
         {
             var seriesMetadata = _metadataFileService.GetFilesBySeries(message.Series.Id);
+            var episodeFiles = GetEpisodeFiles(message.Series.Id);
 
             foreach (var consumer in _metadataFactory.Enabled())
             {
-                consumer.AfterRename(message.Series, GetMetadataFilesForConsumer(consumer, seriesMetadata), GetEpisodeFiles(message.Series.Id));
+                var updatedMetadataFiles = consumer.AfterRename(message.Series,
+                                                                GetMetadataFilesForConsumer(consumer, seriesMetadata),
+                                                                episodeFiles);
+
+                _eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles));
             }
         }
 
@@ -85,5 +131,219 @@ namespace NzbDrone.Core.Metadata
         {
             return seriesMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList();
         }
+
+        private MetadataFile ProcessSeriesMetadata(IMetadata consumer, Series series, List<MetadataFile> existingMetadataFiles)
+        {
+            var seriesMetadata = consumer.SeriesMetadata(series);
+
+            if (seriesMetadata == null)
+            {
+                return null;
+            }
+
+            var hash = seriesMetadata.Contents.SHA256Hash();
+
+            var metadata = existingMetadataFiles.SingleOrDefault(e => e.Type == MetadataType.SeriesMetadata) ??
+                               new MetadataFile
+                               {
+                                   SeriesId = series.Id,
+                                   Consumer = GetType().Name,
+                                   Type = MetadataType.SeriesMetadata,
+                               };
+
+            if (hash == metadata.Hash)
+            {
+                return null;
+            }
+
+            _logger.Debug("Writing Series Metadata to: {0}", seriesMetadata.Path);
+            _diskProvider.WriteAllText(seriesMetadata.Path, seriesMetadata.Contents);
+
+            metadata.Hash = hash;
+            metadata.RelativePath = DiskProviderBase.GetRelativePath(series.Path, seriesMetadata.Path);
+
+            return metadata;
+        }
+
+        private MetadataFile ProcessEpisodeMetadata(IMetadata consumer, Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
+        {
+            var episodeMetadata = consumer.EpisodeMetadata(series, episodeFile);
+
+            if (episodeMetadata == null)
+            {
+                return null;
+            }
+
+            var relativePath = DiskProviderBase.GetRelativePath(series.Path, episodeMetadata.Path);
+
+            var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata &&
+                                                                              c.EpisodeFileId == episodeFile.Id);
+
+            if (existingMetadata != null)
+            {
+                var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
+                if (!episodeMetadata.Path.PathEquals(fullPath))
+                {
+                    _diskProvider.MoveFile(fullPath, episodeMetadata.Path);
+                    existingMetadata.RelativePath = relativePath;
+                }
+            }
+
+            var hash = episodeMetadata.Contents.SHA256Hash();
+
+            var metadata = existingMetadata ??
+                           new MetadataFile
+                           {
+                               SeriesId = series.Id,
+                               EpisodeFileId = episodeFile.Id,
+                               Consumer = GetType().Name,
+                               Type = MetadataType.EpisodeMetadata,
+                               RelativePath = relativePath
+                           };
+
+            if (hash == metadata.Hash)
+            {
+                return null;
+            }
+
+            _logger.Debug("Writing Episode Metadata to: {0}", episodeMetadata.Path);
+            _diskProvider.WriteAllText(episodeMetadata.Path, episodeMetadata.Contents);
+
+            metadata.Hash = hash;
+
+            return metadata;
+        }
+
+        private List<MetadataFile> ProcessSeriesImages(IMetadata consumer, Series series, List<MetadataFile> existingMetadataFiles)
+        {
+            var result = new List<MetadataFile>();
+
+            foreach (var image in consumer.SeriesImages(series))
+            {
+                if (_diskProvider.FileExists(image.Path))
+                {
+                    _logger.Debug("Series image already exists: {0}", image.Path);
+                    continue;
+                }
+
+                var relativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path);
+
+                var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage &&
+                                                                          c.RelativePath == relativePath) ??
+                               new MetadataFile
+                               {
+                                   SeriesId = series.Id,
+                                   Consumer = GetType().Name,
+                                   Type = MetadataType.SeriesImage,
+                                   RelativePath = relativePath
+                               };
+
+                _diskProvider.CopyFile(image.Url, image.Path);
+
+                result.Add(metadata);
+            }
+
+            return result;
+        }
+
+        private List<MetadataFile> ProcessSeasonImages(IMetadata consumer, Series series, List<MetadataFile> existingMetadataFiles)
+        {
+            var result = new List<MetadataFile>();
+
+            foreach (var season in series.Seasons)
+            {
+                foreach (var image in consumer.SeasonImages(series, season))
+                {
+                    if (_diskProvider.FileExists(image.Path))
+                    {
+                        _logger.Debug("Season image already exists: {0}", image.Path);
+                        continue;
+                    }
+
+                    var relativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path);
+
+                    var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage &&
+                                                                            c.SeasonNumber == season.SeasonNumber &&
+                                                                            c.RelativePath == relativePath) ??
+                                new MetadataFile
+                                {
+                                    SeriesId = series.Id,
+                                    SeasonNumber = season.SeasonNumber,
+                                    Consumer = GetType().Name,
+                                    Type = MetadataType.SeasonImage,
+                                    RelativePath = relativePath
+                                };
+
+                    DownloadImage(series, image.Url, image.Path);
+
+                    result.Add(metadata);
+                }
+            }
+
+            return result;
+        }
+
+        private List<MetadataFile> ProcessEpisodeImages(IMetadata consumer, Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
+        {
+            var result = new List<MetadataFile>();
+
+            foreach (var image in consumer.EpisodeImages(series, episodeFile))
+            {
+                if (_diskProvider.FileExists(image.Path))
+                {
+                    _logger.Debug("Episode image already exists: {0}", image.Path);
+                    continue;
+                }
+
+                var relativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path);
+
+                var existingMetadata = existingMetadataFiles.FirstOrDefault(c => c.Type == MetadataType.EpisodeImage &&
+                                                                                  c.EpisodeFileId == episodeFile.Id);
+
+                if (existingMetadata != null)
+                {
+                    var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
+                    if (!image.Path.PathEquals(fullPath))
+                    {
+                        _diskProvider.MoveFile(fullPath, image.Path);
+                        existingMetadata.RelativePath = relativePath;
+
+                        return new List<MetadataFile>{ existingMetadata };
+                    }
+                }
+
+                var metadata = existingMetadata ??
+                               new MetadataFile
+                               {
+                                   SeriesId = series.Id,
+                                   EpisodeFileId = episodeFile.Id,
+                                   Consumer = GetType().Name,
+                                   Type = MetadataType.EpisodeImage,
+                                   RelativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path)
+                               };
+
+                DownloadImage(series, image.Url, image.Path);
+
+                result.Add(metadata);
+            }
+
+            return result;
+        }
+
+        private void DownloadImage(Series series, string url, string path)
+        {
+            try
+            {
+                _httpProvider.DownloadFile(url, path);
+            }
+            catch (WebException e)
+            {
+                _logger.Warn(string.Format("Couldn't download image {0} for {1}. {2}", url, series, e.Message));
+            }
+            catch (Exception e)
+            {
+                _logger.ErrorException("Couldn't download image " + url + " for " + series, e);
+            }
+        }
     }
 }
diff --git a/src/NzbDrone.Core/Metadata/Files/MetadataFile.cs b/src/NzbDrone.Core/Metadata/Files/MetadataFile.cs
index df52e2bc4..c30e9064d 100644
--- a/src/NzbDrone.Core/Metadata/Files/MetadataFile.cs
+++ b/src/NzbDrone.Core/Metadata/Files/MetadataFile.cs
@@ -12,5 +12,6 @@ namespace NzbDrone.Core.Metadata.Files
         public DateTime LastUpdated { get; set; }
         public Int32? EpisodeFileId { get; set; }
         public Int32? SeasonNumber { get; set; }
+        public String Hash { get; set; }
     }
 }
diff --git a/src/NzbDrone.Core/Metadata/MetadataBase.cs b/src/NzbDrone.Core/Metadata/MetadataBase.cs
index 3bebd9d90..d674f2873 100644
--- a/src/NzbDrone.Core/Metadata/MetadataBase.cs
+++ b/src/NzbDrone.Core/Metadata/MetadataBase.cs
@@ -13,17 +13,6 @@ namespace NzbDrone.Core.Metadata
 {
     public abstract class MetadataBase<TSettings> : IMetadata where TSettings : IProviderConfig, new()
     {
-        private readonly IDiskProvider _diskProvider;
-        private readonly IHttpProvider _httpProvider;
-        private readonly Logger _logger;
-
-        protected MetadataBase(IDiskProvider diskProvider, IHttpProvider httpProvider, Logger logger)
-        {
-            _diskProvider = diskProvider;
-            _httpProvider = httpProvider;
-            _logger = logger;
-        }
-
         public Type ConfigContract
         {
             get
@@ -42,11 +31,15 @@ namespace NzbDrone.Core.Metadata
 
         public ProviderDefinition Definition { get; set; }
 
-        public abstract void OnSeriesUpdated(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles);
-        public abstract void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload);
-        public abstract void AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles);
+        public abstract List<MetadataFile> AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles);
         public abstract MetadataFile FindMetadataFile(Series series, string path);
 
+        public abstract MetadataFileResult SeriesMetadata(Series series);
+        public abstract MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile);
+        public abstract List<ImageFileResult> SeriesImages(Series series);
+        public abstract List<ImageFileResult> SeasonImages(Series series, Season season);
+        public abstract List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile);
+
         protected TSettings Settings
         {
             get
@@ -55,28 +48,6 @@ namespace NzbDrone.Core.Metadata
             }
         }
 
-        protected virtual void DownloadImage(Series series, string url, string path)
-        {
-            try
-            {
-                if (_diskProvider.FileExists(path))
-                {
-                    _logger.Debug("Image already exists: {0}, will not download again.", path);
-                    return;
-                }
-
-                _httpProvider.DownloadFile(url, path);
-            }
-            catch (WebException e)
-            {
-                _logger.Warn(string.Format("Couldn't download image {0} for {1}. {2}", url, series, e.Message));
-            }
-            catch (Exception e)
-            {
-                _logger.ErrorException("Couldn't download image " + url + " for " + series, e);
-            }
-        }
-
         public override string ToString()
         {
             return GetType().Name;
diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj
index b8938f15e..76f4cb5bb 100644
--- a/src/NzbDrone.Core/NzbDrone.Core.csproj
+++ b/src/NzbDrone.Core/NzbDrone.Core.csproj
@@ -361,6 +361,8 @@
     <Compile Include="Metadata\Consumers\Xbmc\XbmcMetadata.cs" />
     <Compile Include="Metadata\Consumers\Xbmc\XbmcMetadataSettings.cs" />
     <Compile Include="Metadata\ExistingMetadataService.cs" />
+    <Compile Include="Metadata\Files\ImageFileResult.cs" />
+    <Compile Include="Metadata\Files\MetadataFileResult.cs" />
     <Compile Include="Metadata\Files\MetadataFilesUpdated.cs" />
     <Compile Include="Metadata\Files\MetadataFile.cs" />
     <Compile Include="Metadata\Files\MetadataFileRepository.cs" />
@@ -539,6 +541,7 @@
     <Compile Include="Qualities\QualityProfileItem.cs" />
     <Compile Include="Rest\JsonNetSerializer.cs" />
     <Compile Include="RootFolders\RootFolderRepository.cs" />
+    <Compile Include="Security.cs" />
     <Compile Include="ThingiProvider\ConfigContractNotFoundException.cs" />
     <Compile Include="ThingiProvider\Events\ProviderUpdatedEvent.cs" />
     <Compile Include="ThingiProvider\IProvider.cs" />
diff --git a/src/NzbDrone.Core/Security.cs b/src/NzbDrone.Core/Security.cs
new file mode 100644
index 000000000..abc950809
--- /dev/null
+++ b/src/NzbDrone.Core/Security.cs
@@ -0,0 +1,26 @@
+using System.Security.Cryptography;
+using System.Text;
+
+namespace NzbDrone.Core
+{
+    public static class Security
+    {
+        public static string SHA256Hash(this string input)
+        {
+            var stringBuilder = new StringBuilder();
+
+            using (var hash = SHA256Managed.Create())
+            {
+                var enc = Encoding.UTF8;
+                var result = hash.ComputeHash(enc.GetBytes(input));
+
+                foreach (var b in result)
+                {
+                    stringBuilder.Append(b.ToString("x2"));
+                }
+            }
+
+            return stringBuilder.ToString();
+        }
+    }
+}