diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
index b0e943b6e..f2812eb3a 100644
--- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
+++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
@@ -68,6 +68,7 @@
+
diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
new file mode 100644
index 000000000..eed846b7f
--- /dev/null
+++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
@@ -0,0 +1,346 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Xml;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Xml;
+
+namespace MediaBrowser.LocalMetadata.Savers
+{
+ public abstract class BaseNfoSaver : IMetadataFileSaver
+ {
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ private static readonly Dictionary CommonTags = new[] {
+
+ "plot",
+ "customrating",
+ "lockdata",
+ "type",
+ "dateadded",
+ "title",
+ "rating",
+ "year",
+ "sorttitle",
+ "mpaa",
+ "mpaadescription",
+ "aspectratio",
+ "website",
+ "collectionnumber",
+ "tmdbid",
+ "rottentomatoesid",
+ "language",
+ "tvcomid",
+ "budget",
+ "revenue",
+ "tagline",
+ "studio",
+ "genre",
+ "tag",
+ "runtime",
+ "actor",
+ "criticratingsummary",
+ "criticrating",
+ "fileinfo",
+ "director",
+ "writer",
+ "trailer",
+ "premiered",
+ "releasedate",
+ "outline",
+ "id",
+ "votes",
+ "credits",
+ "originaltitle",
+ "watched",
+ "playcount",
+ "lastplayed",
+ "art",
+ "resume",
+ "biography",
+ "formed",
+ "review",
+ "style",
+ "imdbid",
+ "imdb_id",
+ "plotkeyword",
+ "country",
+ "audiodbalbumid",
+ "audiodbartistid",
+ "awardsummary",
+ "enddate",
+ "lockedfields",
+ "metascore",
+ "zap2itid",
+ "tvrageid",
+ "gamesdbid",
+
+ "musicbrainzartistid",
+ "musicbrainzalbumartistid",
+ "musicbrainzalbumid",
+ "musicbrainzreleasegroupid",
+ "tvdbid",
+ "collectionitem",
+
+ "isuserfavorite",
+ "userrating",
+
+ "countrycode"
+
+ }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
+
+ protected BaseNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory)
+ {
+ Logger = logger;
+ XmlReaderSettingsFactory = xmlReaderSettingsFactory;
+ UserDataManager = userDataManager;
+ UserManager = userManager;
+ LibraryManager = libraryManager;
+ ConfigurationManager = configurationManager;
+ FileSystem = fileSystem;
+ }
+
+ protected IFileSystem FileSystem { get; private set; }
+ protected IServerConfigurationManager ConfigurationManager { get; private set; }
+ protected ILibraryManager LibraryManager { get; private set; }
+ protected IUserManager UserManager { get; private set; }
+ protected IUserDataManager UserDataManager { get; private set; }
+ protected ILogger Logger { get; private set; }
+ protected IXmlReaderSettingsFactory XmlReaderSettingsFactory { get; private set; }
+
+ protected ItemUpdateType MinimumUpdateType
+ {
+ get
+ {
+ return ItemUpdateType.MetadataDownload;
+ }
+ }
+
+ public string Name
+ {
+ get
+ {
+ return SaverName;
+ }
+ }
+
+ public static string SaverName
+ {
+ get
+ {
+ return "Emby Xml";
+ }
+ }
+
+ public string GetSavePath(IHasMetadata item)
+ {
+ return GetLocalSavePath(item);
+ }
+
+ ///
+ /// Gets the save path.
+ ///
+ /// The item.
+ /// System.String.
+ protected abstract string GetLocalSavePath(IHasMetadata item);
+
+ ///
+ /// Gets the name of the root element.
+ ///
+ /// The item.
+ /// System.String.
+ protected abstract string GetRootElementName(IHasMetadata item);
+
+ ///
+ /// Determines whether [is enabled for] [the specified item].
+ ///
+ /// The item.
+ /// Type of the update.
+ /// true if [is enabled for] [the specified item]; otherwise, false.
+ public abstract bool IsEnabledFor(IHasMetadata item, ItemUpdateType updateType);
+
+ protected virtual List GetTagsUsed()
+ {
+ return new List();
+ }
+
+ public void Save(IHasMetadata item, CancellationToken cancellationToken)
+ {
+ var path = GetSavePath(item);
+
+ using (var memoryStream = new MemoryStream())
+ {
+ Save(item, memoryStream, path);
+
+ memoryStream.Position = 0;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ SaveToFile(memoryStream, path);
+ }
+ }
+
+ private void SaveToFile(Stream stream, string path)
+ {
+ FileSystem.CreateDirectory(Path.GetDirectoryName(path));
+
+ var file = FileSystem.GetFileInfo(path);
+
+ var wasHidden = false;
+
+ // This will fail if the file is hidden
+ if (file.Exists)
+ {
+ if (file.IsHidden)
+ {
+ FileSystem.SetHidden(path, false);
+
+ wasHidden = true;
+ }
+ }
+
+ using (var filestream = FileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ {
+ stream.CopyTo(filestream);
+ }
+
+ if (wasHidden || ConfigurationManager.Configuration.SaveMetadataHidden)
+ {
+ FileSystem.SetHidden(path, true);
+ }
+ }
+
+ private void Save(IHasMetadata item, Stream stream, string xmlPath)
+ {
+ var settings = new XmlWriterSettings
+ {
+ Indent = true,
+ Encoding = Encoding.UTF8,
+ CloseOutput = false
+ };
+
+ using (XmlWriter writer = XmlWriter.Create(stream, settings))
+ {
+ var root = GetRootElementName(item);
+
+ writer.WriteStartDocument(true);
+
+ writer.WriteStartElement(root);
+
+ var baseItem = item as BaseItem;
+
+ if (baseItem != null)
+ {
+ AddCommonNodes(baseItem, writer, LibraryManager, UserManager, UserDataManager, FileSystem, ConfigurationManager);
+ }
+
+ WriteCustomElements(item, writer);
+
+ var tagsUsed = GetTagsUsed();
+
+ try
+ {
+ AddCustomTags(xmlPath, tagsUsed, writer, Logger, FileSystem);
+ }
+ catch (FileNotFoundException)
+ {
+
+ }
+ catch (IOException)
+ {
+
+ }
+ catch (XmlException ex)
+ {
+ Logger.ErrorException("Error reading existng xml", ex);
+ }
+
+ writer.WriteEndElement();
+
+ writer.WriteEndDocument();
+ }
+ }
+
+ protected abstract void WriteCustomElements(IHasMetadata item, XmlWriter writer);
+
+ public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
+
+ ///
+ /// Adds the common nodes.
+ ///
+ /// Task.
+ public static void AddCommonNodes(BaseItem item, XmlWriter writer, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataRepo, IFileSystem fileSystem, IServerConfigurationManager config)
+ {
+ var writtenProviderIds = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ }
+
+ private static bool IsPersonType(PersonInfo person, string type)
+ {
+ return string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private void AddCustomTags(string path, List xmlTagsUsed, XmlWriter writer, ILogger logger, IFileSystem fileSystem)
+ {
+ var settings = XmlReaderSettingsFactory.Create(false);
+
+ settings.CheckCharacters = false;
+ settings.IgnoreProcessingInstructions = true;
+ settings.IgnoreComments = true;
+
+ using (var fileStream = fileSystem.OpenRead(path))
+ {
+ using (var streamReader = new StreamReader(fileStream, Encoding.UTF8))
+ {
+ // Use XmlReader for best performance
+ using (var reader = XmlReader.Create(streamReader, settings))
+ {
+ try
+ {
+ reader.MoveToContent();
+ }
+ catch (Exception ex)
+ {
+ logger.ErrorException("Error reading existing xml tags from {0}.", ex, path);
+ return;
+ }
+
+ // Loop through each element
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ var name = reader.Name;
+
+ if (!CommonTags.ContainsKey(name) && !xmlTagsUsed.Contains(name, StringComparer.OrdinalIgnoreCase))
+ {
+ writer.WriteNode(reader, false);
+ }
+ else
+ {
+ reader.Skip();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}