using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Security; using System.Text; using System.Xml; namespace MediaBrowser.LocalMetadata.Savers { /// <summary> /// Class XmlHelpers /// </summary> public static class XmlSaverHelpers { private static readonly Dictionary<string, string> CommonTags = new[] { "Added", "AspectRatio", "AudioDbAlbumId", "AudioDbArtistId", "AwardSummary", "BirthDate", "Budget", // Deprecated. No longer saving in this field. "certification", "Chapters", "ContentRating", "Countries", "CustomRating", "CriticRating", "CriticRatingSummary", "DeathDate", "DisplayOrder", "EndDate", "Genres", "Genre", "GamesDbId", // Deprecated. No longer saving in this field. "IMDB_ID", "IMDB", // Deprecated. No longer saving in this field. "IMDbId", "Language", "LocalTitle", "OriginalTitle", "LockData", "LockedFields", "Format3D", "Metascore", // Deprecated. No longer saving in this field. "MPAARating", "MPAADescription", "MusicBrainzArtistId", "MusicBrainzAlbumArtistId", "MusicBrainzAlbumId", "MusicBrainzReleaseGroupId", // Deprecated. No longer saving in this field. "MusicbrainzId", "Overview", "ShortOverview", "Persons", "PlotKeywords", "PremiereDate", "ProductionYear", "Rating", "Revenue", "RottenTomatoesId", "RunningTime", // Deprecated. No longer saving in this field. "Runtime", "SortTitle", "Studios", "Tags", // Deprecated. No longer saving in this field. "TagLine", "Taglines", "TMDbCollectionId", "TMDbId", // Deprecated. No longer saving in this field. "Trailer", "Trailers", "TVcomId", "TvDbId", "Type", "TVRageId", "VoteCount", "Website", "Zap2ItId", "CollectionItems", "PlaylistItems", "Shares" }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); /// <summary> /// The us culture /// </summary> private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); /// <summary> /// Saves the specified XML. /// </summary> /// <param name="xml">The XML.</param> /// <param name="path">The path.</param> /// <param name="xmlTagsUsed">The XML tags used.</param> public static void Save(StringBuilder xml, string path, List<string> xmlTagsUsed, IServerConfigurationManager config) { if (File.Exists(path)) { var position = xml.ToString().LastIndexOf("</", StringComparison.OrdinalIgnoreCase); xml.Insert(position, GetCustomTags(path, xmlTagsUsed)); } var xmlDocument = new XmlDocument(); xmlDocument.LoadXml(xml.ToString()); //Add the new node to the document. xmlDocument.InsertBefore(xmlDocument.CreateXmlDeclaration("1.0", "UTF-8", "yes"), xmlDocument.DocumentElement); Directory.CreateDirectory(Path.GetDirectoryName(path)); var wasHidden = false; var file = new FileInfo(path); // This will fail if the file is hidden if (file.Exists) { if ((file.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) { file.Attributes &= ~FileAttributes.Hidden; wasHidden = true; } } using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) { using (var streamWriter = new StreamWriter(filestream, Encoding.UTF8)) { xmlDocument.Save(streamWriter); } } if (wasHidden || config.Configuration.SaveMetadataHidden) { file.Refresh(); // Add back the attribute file.Attributes |= FileAttributes.Hidden; } } /// <summary> /// Gets the custom tags. /// </summary> /// <param name="path">The path.</param> /// <param name="xmlTagsUsed">The XML tags used.</param> /// <returns>System.String.</returns> private static string GetCustomTags(string path, List<string> xmlTagsUsed) { var settings = new XmlReaderSettings { CheckCharacters = false, IgnoreProcessingInstructions = true, IgnoreComments = true, ValidationType = ValidationType.None }; var builder = new StringBuilder(); using (var streamReader = new StreamReader(path, Encoding.UTF8)) { // Use XmlReader for best performance using (var reader = XmlReader.Create(streamReader, settings)) { reader.MoveToContent(); // 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)) { builder.AppendLine(reader.ReadOuterXml()); } else { reader.Skip(); } } } } } return builder.ToString(); } /// <summary> /// Adds the common nodes. /// </summary> /// <param name="item">The item.</param> /// <param name="builder">The builder.</param> public static void AddCommonNodes(BaseItem item, StringBuilder builder) { if (!string.IsNullOrEmpty(item.OfficialRating)) { builder.Append("<ContentRating>" + SecurityElement.Escape(item.OfficialRating) + "</ContentRating>"); } if (!string.IsNullOrEmpty(item.OfficialRatingDescription)) { builder.Append("<MPAADescription>" + SecurityElement.Escape(item.OfficialRatingDescription) + "</MPAADescription>"); } builder.Append("<Added>" + SecurityElement.Escape(item.DateCreated.ToLocalTime().ToString("G")) + "</Added>"); builder.Append("<LockData>" + item.IsLocked.ToString().ToLower() + "</LockData>"); if (item.LockedFields.Count > 0) { builder.Append("<LockedFields>" + string.Join("|", item.LockedFields.Select(i => i.ToString()).ToArray()) + "</LockedFields>"); } if (!string.IsNullOrEmpty(item.DisplayMediaType)) { builder.Append("<Type>" + SecurityElement.Escape(item.DisplayMediaType) + "</Type>"); } var hasCriticRating = item as IHasCriticRating; if (hasCriticRating != null) { if (hasCriticRating.CriticRating.HasValue) { builder.Append("<CriticRating>" + SecurityElement.Escape(hasCriticRating.CriticRating.Value.ToString(UsCulture)) + "</CriticRating>"); } if (!string.IsNullOrEmpty(hasCriticRating.CriticRatingSummary)) { builder.Append("<CriticRatingSummary><![CDATA[" + hasCriticRating.CriticRatingSummary + "]]></CriticRatingSummary>"); } } if (!string.IsNullOrEmpty(item.Overview)) { builder.Append("<Overview><![CDATA[" + item.Overview + "]]></Overview>"); } var hasOriginalTitle = item as IHasOriginalTitle; if (hasOriginalTitle != null) { if (!string.IsNullOrEmpty(hasOriginalTitle.OriginalTitle)) { builder.Append("<OriginalTitle>" + SecurityElement.Escape(hasOriginalTitle.OriginalTitle) + "</OriginalTitle>"); } } var hasShortOverview = item as IHasShortOverview; if (hasShortOverview != null) { if (!string.IsNullOrEmpty(hasShortOverview.ShortOverview)) { builder.Append("<ShortOverview><![CDATA[" + hasShortOverview.ShortOverview + "]]></ShortOverview>"); } } if (!string.IsNullOrEmpty(item.CustomRating)) { builder.Append("<CustomRating>" + SecurityElement.Escape(item.CustomRating) + "</CustomRating>"); } if (!string.IsNullOrEmpty(item.Name) && !(item is Episode)) { builder.Append("<LocalTitle>" + SecurityElement.Escape(item.Name) + "</LocalTitle>"); } if (!string.IsNullOrEmpty(item.ForcedSortName)) { builder.Append("<SortTitle>" + SecurityElement.Escape(item.ForcedSortName) + "</SortTitle>"); } if (item.PremiereDate.HasValue) { if (item is Person) { builder.Append("<BirthDate>" + SecurityElement.Escape(item.PremiereDate.Value.ToLocalTime().ToString("yyyy-MM-dd")) + "</BirthDate>"); } else if (!(item is Episode)) { builder.Append("<PremiereDate>" + SecurityElement.Escape(item.PremiereDate.Value.ToLocalTime().ToString("yyyy-MM-dd")) + "</PremiereDate>"); } } if (item.EndDate.HasValue) { if (item is Person) { builder.Append("<DeathDate>" + SecurityElement.Escape(item.EndDate.Value.ToString("yyyy-MM-dd")) + "</DeathDate>"); } else if (!(item is Episode)) { builder.Append("<EndDate>" + SecurityElement.Escape(item.EndDate.Value.ToString("yyyy-MM-dd")) + "</EndDate>"); } } var hasTrailers = item as IHasTrailers; if (hasTrailers != null) { if (hasTrailers.RemoteTrailers.Count > 0) { builder.Append("<Trailers>"); foreach (var trailer in hasTrailers.RemoteTrailers) { builder.Append("<Trailer>" + SecurityElement.Escape(trailer.Url) + "</Trailer>"); } builder.Append("</Trailers>"); } } var hasProductionLocations = item as IHasProductionLocations; if (hasProductionLocations != null) { if (hasProductionLocations.ProductionLocations.Count > 0) { builder.Append("<Countries>"); foreach (var name in hasProductionLocations.ProductionLocations) { builder.Append("<Country>" + SecurityElement.Escape(name) + "</Country>"); } builder.Append("</Countries>"); } } var hasDisplayOrder = item as IHasDisplayOrder; if (hasDisplayOrder != null && !string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder)) { builder.Append("<DisplayOrder>" + SecurityElement.Escape(hasDisplayOrder.DisplayOrder) + "</DisplayOrder>"); } var hasMetascore = item as IHasMetascore; if (hasMetascore != null && hasMetascore.Metascore.HasValue) { builder.Append("<Metascore>" + SecurityElement.Escape(hasMetascore.Metascore.Value.ToString(UsCulture)) + "</Metascore>"); } var hasAwards = item as IHasAwards; if (hasAwards != null && !string.IsNullOrEmpty(hasAwards.AwardSummary)) { builder.Append("<AwardSummary>" + SecurityElement.Escape(hasAwards.AwardSummary) + "</AwardSummary>"); } var hasBudget = item as IHasBudget; if (hasBudget != null) { if (hasBudget.Budget.HasValue) { builder.Append("<Budget>" + SecurityElement.Escape(hasBudget.Budget.Value.ToString(UsCulture)) + "</Budget>"); } if (hasBudget.Revenue.HasValue) { builder.Append("<Revenue>" + SecurityElement.Escape(hasBudget.Revenue.Value.ToString(UsCulture)) + "</Revenue>"); } } if (item.CommunityRating.HasValue) { builder.Append("<Rating>" + SecurityElement.Escape(item.CommunityRating.Value.ToString(UsCulture)) + "</Rating>"); } if (item.VoteCount.HasValue) { builder.Append("<VoteCount>" + SecurityElement.Escape(item.VoteCount.Value.ToString(UsCulture)) + "</VoteCount>"); } if (item.ProductionYear.HasValue && !(item is Person)) { builder.Append("<ProductionYear>" + SecurityElement.Escape(item.ProductionYear.Value.ToString(UsCulture)) + "</ProductionYear>"); } if (!string.IsNullOrEmpty(item.HomePageUrl)) { builder.Append("<Website>" + SecurityElement.Escape(item.HomePageUrl) + "</Website>"); } var hasAspectRatio = item as IHasAspectRatio; if (hasAspectRatio != null) { if (!string.IsNullOrEmpty(hasAspectRatio.AspectRatio)) { builder.Append("<AspectRatio>" + SecurityElement.Escape(hasAspectRatio.AspectRatio) + "</AspectRatio>"); } } var hasLanguage = item as IHasPreferredMetadataLanguage; if (hasLanguage != null) { if (!string.IsNullOrEmpty(hasLanguage.PreferredMetadataLanguage)) { builder.Append("<Language>" + SecurityElement.Escape(hasLanguage.PreferredMetadataLanguage) + "</Language>"); } if (!string.IsNullOrEmpty(hasLanguage.PreferredMetadataCountryCode)) { builder.Append("<CountryCode>" + SecurityElement.Escape(hasLanguage.PreferredMetadataCountryCode) + "</CountryCode>"); } } // Use original runtime here, actual file runtime later in MediaInfo var runTimeTicks = item.RunTimeTicks; if (runTimeTicks.HasValue) { var timespan = TimeSpan.FromTicks(runTimeTicks.Value); builder.Append("<RunningTime>" + Convert.ToInt32(timespan.TotalMinutes).ToString(UsCulture) + "</RunningTime>"); } var imdb = item.GetProviderId(MetadataProviders.Imdb); if (!string.IsNullOrEmpty(imdb)) { builder.Append("<IMDB>" + SecurityElement.Escape(imdb) + "</IMDB>"); } var tmdb = item.GetProviderId(MetadataProviders.Tmdb); if (!string.IsNullOrEmpty(tmdb)) { builder.Append("<TMDbId>" + SecurityElement.Escape(tmdb) + "</TMDbId>"); } if (!(item is Series)) { var tvdb = item.GetProviderId(MetadataProviders.Tvdb); if (!string.IsNullOrEmpty(tvdb)) { builder.Append("<TvDbId>" + SecurityElement.Escape(tvdb) + "</TvDbId>"); } } var externalId = item.GetProviderId(MetadataProviders.Tvcom); if (!string.IsNullOrEmpty(externalId)) { builder.Append("<TVcomId>" + SecurityElement.Escape(externalId) + "</TVcomId>"); } externalId = item.GetProviderId(MetadataProviders.RottenTomatoes); if (!string.IsNullOrEmpty(externalId)) { builder.Append("<RottenTomatoesId>" + SecurityElement.Escape(externalId) + "</RottenTomatoesId>"); } externalId = item.GetProviderId(MetadataProviders.Zap2It); if (!string.IsNullOrEmpty(externalId)) { builder.Append("<Zap2ItId>" + SecurityElement.Escape(externalId) + "</Zap2ItId>"); } externalId = item.GetProviderId(MetadataProviders.MusicBrainzAlbum); if (!string.IsNullOrEmpty(externalId)) { builder.Append("<MusicBrainzAlbumId>" + SecurityElement.Escape(externalId) + "</MusicBrainzAlbumId>"); } externalId = item.GetProviderId(MetadataProviders.MusicBrainzAlbumArtist); if (!string.IsNullOrEmpty(externalId)) { builder.Append("<MusicBrainzAlbumArtistId>" + SecurityElement.Escape(externalId) + "</MusicBrainzAlbumArtistId>"); } externalId = item.GetProviderId(MetadataProviders.MusicBrainzArtist); if (!string.IsNullOrEmpty(externalId)) { builder.Append("<MusicBrainzArtistId>" + SecurityElement.Escape(externalId) + "</MusicBrainzArtistId>"); } externalId = item.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup); if (!string.IsNullOrEmpty(externalId)) { builder.Append("<MusicBrainzReleaseGroupId>" + SecurityElement.Escape(externalId) + "</MusicBrainzReleaseGroupId>"); } externalId = item.GetProviderId(MetadataProviders.Gamesdb); if (!string.IsNullOrEmpty(externalId)) { builder.Append("<GamesDbId>" + SecurityElement.Escape(externalId) + "</GamesDbId>"); } externalId = item.GetProviderId(MetadataProviders.TmdbCollection); if (!string.IsNullOrEmpty(externalId)) { builder.Append("<TMDbCollectionId>" + SecurityElement.Escape(externalId) + "</TMDbCollectionId>"); } externalId = item.GetProviderId(MetadataProviders.AudioDbArtist); if (!string.IsNullOrEmpty(externalId)) { builder.Append("<AudioDbArtistId>" + SecurityElement.Escape(externalId) + "</AudioDbArtistId>"); } externalId = item.GetProviderId(MetadataProviders.AudioDbAlbum); if (!string.IsNullOrEmpty(externalId)) { builder.Append("<AudioDbAlbumId>" + SecurityElement.Escape(externalId) + "</AudioDbAlbumId>"); } externalId = item.GetProviderId(MetadataProviders.TvRage); if (!string.IsNullOrEmpty(externalId)) { builder.Append("<TVRageId>" + SecurityElement.Escape(externalId) + "</TVRageId>"); } var hasTagline = item as IHasTaglines; if (hasTagline != null) { if (hasTagline.Taglines.Count > 0) { builder.Append("<Taglines>"); foreach (var tagline in hasTagline.Taglines) { builder.Append("<Tagline>" + SecurityElement.Escape(tagline) + "</Tagline>"); } builder.Append("</Taglines>"); } } if (item.Genres.Count > 0) { builder.Append("<Genres>"); foreach (var genre in item.Genres) { builder.Append("<Genre>" + SecurityElement.Escape(genre) + "</Genre>"); } builder.Append("</Genres>"); } if (item.Studios.Count > 0) { builder.Append("<Studios>"); foreach (var studio in item.Studios) { builder.Append("<Studio>" + SecurityElement.Escape(studio) + "</Studio>"); } builder.Append("</Studios>"); } var hasTags = item as IHasTags; if (hasTags != null) { if (hasTags.Tags.Count > 0) { builder.Append("<Tags>"); foreach (var tag in hasTags.Tags) { builder.Append("<Tag>" + SecurityElement.Escape(tag) + "</Tag>"); } builder.Append("</Tags>"); } } var hasKeywords = item as IHasKeywords; if (hasKeywords != null) { if (hasKeywords.Keywords.Count > 0) { builder.Append("<PlotKeywords>"); foreach (var tag in hasKeywords.Keywords) { builder.Append("<PlotKeyword>" + SecurityElement.Escape(tag) + "</PlotKeyword>"); } builder.Append("</PlotKeywords>"); } } if (item.People.Count > 0) { builder.Append("<Persons>"); foreach (var person in item.People) { builder.Append("<Person>"); builder.Append("<Name>" + SecurityElement.Escape(person.Name) + "</Name>"); builder.Append("<Type>" + SecurityElement.Escape(person.Type) + "</Type>"); builder.Append("<Role>" + SecurityElement.Escape(person.Role) + "</Role>"); if (person.SortOrder.HasValue) { builder.Append("<SortOrder>" + SecurityElement.Escape(person.SortOrder.Value.ToString(UsCulture)) + "</SortOrder>"); } builder.Append("</Person>"); } builder.Append("</Persons>"); } var boxset = item as BoxSet; if (boxset != null) { AddLinkedChildren(boxset, builder, "CollectionItems", "CollectionItem"); } var playlist = item as Playlist; if (playlist != null) { AddLinkedChildren(playlist, builder, "PlaylistItems", "PlaylistItem"); } var hasShares = item as IHasShares; if (hasShares != null) { AddShares(hasShares, builder); } } public static void AddShares(IHasShares item, StringBuilder builder) { builder.Append("<Shares>"); foreach (var share in item.Shares) { builder.Append("<Share>"); builder.Append("<UserId>" + SecurityElement.Escape(share.UserId) + "</UserId>"); builder.Append("<CanEdit>" + SecurityElement.Escape(share.CanEdit.ToString().ToLower()) + "</CanEdit>"); builder.Append("</Share>"); } builder.Append("</Shares>"); } public static void AddChapters(Video item, StringBuilder builder, IItemRepository repository) { var chapters = repository.GetChapters(item.Id); builder.Append("<Chapters>"); foreach (var chapter in chapters) { builder.Append("<Chapter>"); builder.Append("<Name>" + SecurityElement.Escape(chapter.Name) + "</Name>"); var time = TimeSpan.FromTicks(chapter.StartPositionTicks); var ms = Convert.ToInt64(time.TotalMilliseconds); builder.Append("<StartPositionMs>" + SecurityElement.Escape(ms.ToString(UsCulture)) + "</StartPositionMs>"); builder.Append("</Chapter>"); } builder.Append("</Chapters>"); } /// <summary> /// Appends the media info. /// </summary> /// <typeparam name="T"></typeparam> public static void AddMediaInfo<T>(T item, StringBuilder builder, IItemRepository itemRepository) where T : BaseItem { var video = item as Video; if (video != null) { //AddChapters(video, builder, itemRepository); if (video.Video3DFormat.HasValue) { switch (video.Video3DFormat.Value) { case Video3DFormat.FullSideBySide: builder.Append("<Format3D>FSBS</Format3D>"); break; case Video3DFormat.FullTopAndBottom: builder.Append("<Format3D>FTAB</Format3D>"); break; case Video3DFormat.HalfSideBySide: builder.Append("<Format3D>HSBS</Format3D>"); break; case Video3DFormat.HalfTopAndBottom: builder.Append("<Format3D>HTAB</Format3D>"); break; } } } } public static void AddLinkedChildren(Folder item, StringBuilder builder, string pluralNodeName, string singularNodeName) { var items = item.LinkedChildren .Where(i => i.Type == LinkedChildType.Manual) .ToList(); if (items.Count == 0) { return; } builder.Append("<" + pluralNodeName + ">"); foreach (var link in items) { builder.Append("<" + singularNodeName + ">"); if (!string.IsNullOrWhiteSpace(link.Path)) { builder.Append("<Path>" + SecurityElement.Escape((link.Path)) + "</Path>"); } builder.Append("</" + singularNodeName + ">"); } builder.Append("</" + pluralNodeName + ">"); } } }