XmlSaverHelpers.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. using MediaBrowser.Controller.Configuration;
  2. using MediaBrowser.Controller.Entities;
  3. using MediaBrowser.Controller.Entities.Movies;
  4. using MediaBrowser.Controller.Entities.TV;
  5. using MediaBrowser.Controller.Library;
  6. using MediaBrowser.Controller.Persistence;
  7. using MediaBrowser.Controller.Playlists;
  8. using MediaBrowser.Model.Entities;
  9. using System;
  10. using System.Collections.Generic;
  11. using System.Globalization;
  12. using System.IO;
  13. using System.Linq;
  14. using System.Security;
  15. using System.Text;
  16. using System.Xml;
  17. using CommonIO;
  18. namespace MediaBrowser.LocalMetadata.Savers
  19. {
  20. /// <summary>
  21. /// Class XmlHelpers
  22. /// </summary>
  23. public static class XmlSaverHelpers
  24. {
  25. private static readonly Dictionary<string, string> CommonTags = new[] {
  26. "Added",
  27. "AspectRatio",
  28. "AudioDbAlbumId",
  29. "AudioDbArtistId",
  30. "AwardSummary",
  31. "BirthDate",
  32. "Budget",
  33. // Deprecated. No longer saving in this field.
  34. "certification",
  35. "Chapters",
  36. "ContentRating",
  37. "Countries",
  38. "CustomRating",
  39. "CriticRating",
  40. "CriticRatingSummary",
  41. "DeathDate",
  42. "DisplayOrder",
  43. "EndDate",
  44. "Genres",
  45. "Genre",
  46. "GamesDbId",
  47. // Deprecated. No longer saving in this field.
  48. "IMDB_ID",
  49. "IMDB",
  50. // Deprecated. No longer saving in this field.
  51. "IMDbId",
  52. "Language",
  53. "LocalTitle",
  54. "OriginalTitle",
  55. "LockData",
  56. "LockedFields",
  57. "Format3D",
  58. "Metascore",
  59. // Deprecated. No longer saving in this field.
  60. "MPAARating",
  61. "MPAADescription",
  62. "MusicBrainzArtistId",
  63. "MusicBrainzAlbumArtistId",
  64. "MusicBrainzAlbumId",
  65. "MusicBrainzReleaseGroupId",
  66. // Deprecated. No longer saving in this field.
  67. "MusicbrainzId",
  68. "Overview",
  69. "ShortOverview",
  70. "Persons",
  71. "PlotKeywords",
  72. "PremiereDate",
  73. "ProductionYear",
  74. "Rating",
  75. "Revenue",
  76. "RottenTomatoesId",
  77. "RunningTime",
  78. // Deprecated. No longer saving in this field.
  79. "Runtime",
  80. "SortTitle",
  81. "Studios",
  82. "Tags",
  83. // Deprecated. No longer saving in this field.
  84. "TagLine",
  85. "Taglines",
  86. "TMDbCollectionId",
  87. "TMDbId",
  88. // Deprecated. No longer saving in this field.
  89. "Trailer",
  90. "Trailers",
  91. "TVcomId",
  92. "TvDbId",
  93. "Type",
  94. "TVRageId",
  95. "VoteCount",
  96. "Website",
  97. "Zap2ItId",
  98. "CollectionItems",
  99. "PlaylistItems",
  100. "Shares"
  101. }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
  102. /// <summary>
  103. /// The us culture
  104. /// </summary>
  105. private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
  106. /// <summary>
  107. /// Saves the specified XML.
  108. /// </summary>
  109. /// <param name="xml">The XML.</param>
  110. /// <param name="path">The path.</param>
  111. /// <param name="xmlTagsUsed">The XML tags used.</param>
  112. public static void Save(StringBuilder xml, string path, List<string> xmlTagsUsed, IServerConfigurationManager config, IFileSystem fileSystem)
  113. {
  114. if (fileSystem.FileExists(path))
  115. {
  116. var position = xml.ToString().LastIndexOf("</", StringComparison.OrdinalIgnoreCase);
  117. xml.Insert(position, GetCustomTags(path, xmlTagsUsed));
  118. }
  119. var xmlDocument = new XmlDocument();
  120. xmlDocument.LoadXml(xml.ToString());
  121. //Add the new node to the document.
  122. xmlDocument.InsertBefore(xmlDocument.CreateXmlDeclaration("1.0", "UTF-8", "yes"), xmlDocument.DocumentElement);
  123. fileSystem.CreateDirectory(Path.GetDirectoryName(path));
  124. var wasHidden = false;
  125. var file = new FileInfo(path);
  126. // This will fail if the file is hidden
  127. if (file.Exists)
  128. {
  129. if ((file.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
  130. {
  131. file.Attributes &= ~FileAttributes.Hidden;
  132. wasHidden = true;
  133. }
  134. }
  135. using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
  136. {
  137. using (var streamWriter = new StreamWriter(filestream, Encoding.UTF8))
  138. {
  139. xmlDocument.Save(streamWriter);
  140. }
  141. }
  142. if (wasHidden || config.Configuration.SaveMetadataHidden)
  143. {
  144. file.Refresh();
  145. // Add back the attribute
  146. file.Attributes |= FileAttributes.Hidden;
  147. }
  148. }
  149. /// <summary>
  150. /// Gets the custom tags.
  151. /// </summary>
  152. /// <param name="path">The path.</param>
  153. /// <param name="xmlTagsUsed">The XML tags used.</param>
  154. /// <returns>System.String.</returns>
  155. private static string GetCustomTags(string path, List<string> xmlTagsUsed)
  156. {
  157. var settings = new XmlReaderSettings
  158. {
  159. CheckCharacters = false,
  160. IgnoreProcessingInstructions = true,
  161. IgnoreComments = true,
  162. ValidationType = ValidationType.None
  163. };
  164. var builder = new StringBuilder();
  165. using (var streamReader = new StreamReader(path, Encoding.UTF8))
  166. {
  167. // Use XmlReader for best performance
  168. using (var reader = XmlReader.Create(streamReader, settings))
  169. {
  170. reader.MoveToContent();
  171. // Loop through each element
  172. while (reader.Read())
  173. {
  174. if (reader.NodeType == XmlNodeType.Element)
  175. {
  176. var name = reader.Name;
  177. if (!CommonTags.ContainsKey(name) && !xmlTagsUsed.Contains(name, StringComparer.OrdinalIgnoreCase))
  178. {
  179. builder.AppendLine(reader.ReadOuterXml());
  180. }
  181. else
  182. {
  183. reader.Skip();
  184. }
  185. }
  186. }
  187. }
  188. }
  189. return builder.ToString();
  190. }
  191. /// <summary>
  192. /// Adds the common nodes.
  193. /// </summary>
  194. /// <param name="item">The item.</param>
  195. /// <param name="builder">The builder.</param>
  196. public static void AddCommonNodes(BaseItem item, ILibraryManager libraryManager, StringBuilder builder)
  197. {
  198. if (!string.IsNullOrEmpty(item.OfficialRating))
  199. {
  200. builder.Append("<ContentRating>" + SecurityElement.Escape(item.OfficialRating) + "</ContentRating>");
  201. }
  202. if (!string.IsNullOrEmpty(item.OfficialRatingDescription))
  203. {
  204. builder.Append("<MPAADescription>" + SecurityElement.Escape(item.OfficialRatingDescription) + "</MPAADescription>");
  205. }
  206. builder.Append("<Added>" + SecurityElement.Escape(item.DateCreated.ToLocalTime().ToString("G")) + "</Added>");
  207. builder.Append("<LockData>" + item.IsLocked.ToString().ToLower() + "</LockData>");
  208. if (item.LockedFields.Count > 0)
  209. {
  210. builder.Append("<LockedFields>" + string.Join("|", item.LockedFields.Select(i => i.ToString()).ToArray()) + "</LockedFields>");
  211. }
  212. if (!string.IsNullOrEmpty(item.DisplayMediaType))
  213. {
  214. builder.Append("<Type>" + SecurityElement.Escape(item.DisplayMediaType) + "</Type>");
  215. }
  216. var hasCriticRating = item as IHasCriticRating;
  217. if (hasCriticRating != null)
  218. {
  219. if (hasCriticRating.CriticRating.HasValue)
  220. {
  221. builder.Append("<CriticRating>" + SecurityElement.Escape(hasCriticRating.CriticRating.Value.ToString(UsCulture)) + "</CriticRating>");
  222. }
  223. if (!string.IsNullOrEmpty(hasCriticRating.CriticRatingSummary))
  224. {
  225. builder.Append("<CriticRatingSummary><![CDATA[" + hasCriticRating.CriticRatingSummary + "]]></CriticRatingSummary>");
  226. }
  227. }
  228. if (!string.IsNullOrEmpty(item.Overview))
  229. {
  230. builder.Append("<Overview><![CDATA[" + item.Overview + "]]></Overview>");
  231. }
  232. var hasOriginalTitle = item as IHasOriginalTitle;
  233. if (hasOriginalTitle != null)
  234. {
  235. if (!string.IsNullOrEmpty(hasOriginalTitle.OriginalTitle))
  236. {
  237. builder.Append("<OriginalTitle>" + SecurityElement.Escape(hasOriginalTitle.OriginalTitle) + "</OriginalTitle>");
  238. }
  239. }
  240. var hasShortOverview = item as IHasShortOverview;
  241. if (hasShortOverview != null)
  242. {
  243. if (!string.IsNullOrEmpty(hasShortOverview.ShortOverview))
  244. {
  245. builder.Append("<ShortOverview><![CDATA[" + hasShortOverview.ShortOverview + "]]></ShortOverview>");
  246. }
  247. }
  248. if (!string.IsNullOrEmpty(item.CustomRating))
  249. {
  250. builder.Append("<CustomRating>" + SecurityElement.Escape(item.CustomRating) + "</CustomRating>");
  251. }
  252. if (!string.IsNullOrEmpty(item.Name) && !(item is Episode))
  253. {
  254. builder.Append("<LocalTitle>" + SecurityElement.Escape(item.Name) + "</LocalTitle>");
  255. }
  256. if (!string.IsNullOrEmpty(item.ForcedSortName))
  257. {
  258. builder.Append("<SortTitle>" + SecurityElement.Escape(item.ForcedSortName) + "</SortTitle>");
  259. }
  260. if (item.PremiereDate.HasValue)
  261. {
  262. if (item is Person)
  263. {
  264. builder.Append("<BirthDate>" + SecurityElement.Escape(item.PremiereDate.Value.ToLocalTime().ToString("yyyy-MM-dd")) + "</BirthDate>");
  265. }
  266. else if (!(item is Episode))
  267. {
  268. builder.Append("<PremiereDate>" + SecurityElement.Escape(item.PremiereDate.Value.ToLocalTime().ToString("yyyy-MM-dd")) + "</PremiereDate>");
  269. }
  270. }
  271. if (item.EndDate.HasValue)
  272. {
  273. if (item is Person)
  274. {
  275. builder.Append("<DeathDate>" + SecurityElement.Escape(item.EndDate.Value.ToString("yyyy-MM-dd")) + "</DeathDate>");
  276. }
  277. else if (!(item is Episode))
  278. {
  279. builder.Append("<EndDate>" + SecurityElement.Escape(item.EndDate.Value.ToString("yyyy-MM-dd")) + "</EndDate>");
  280. }
  281. }
  282. var hasTrailers = item as IHasTrailers;
  283. if (hasTrailers != null)
  284. {
  285. if (hasTrailers.RemoteTrailers.Count > 0)
  286. {
  287. builder.Append("<Trailers>");
  288. foreach (var trailer in hasTrailers.RemoteTrailers)
  289. {
  290. builder.Append("<Trailer>" + SecurityElement.Escape(trailer.Url) + "</Trailer>");
  291. }
  292. builder.Append("</Trailers>");
  293. }
  294. }
  295. var hasProductionLocations = item as IHasProductionLocations;
  296. if (hasProductionLocations != null)
  297. {
  298. if (hasProductionLocations.ProductionLocations.Count > 0)
  299. {
  300. builder.Append("<Countries>");
  301. foreach (var name in hasProductionLocations.ProductionLocations)
  302. {
  303. builder.Append("<Country>" + SecurityElement.Escape(name) + "</Country>");
  304. }
  305. builder.Append("</Countries>");
  306. }
  307. }
  308. var hasDisplayOrder = item as IHasDisplayOrder;
  309. if (hasDisplayOrder != null && !string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder))
  310. {
  311. builder.Append("<DisplayOrder>" + SecurityElement.Escape(hasDisplayOrder.DisplayOrder) + "</DisplayOrder>");
  312. }
  313. var hasMetascore = item as IHasMetascore;
  314. if (hasMetascore != null && hasMetascore.Metascore.HasValue)
  315. {
  316. builder.Append("<Metascore>" + SecurityElement.Escape(hasMetascore.Metascore.Value.ToString(UsCulture)) + "</Metascore>");
  317. }
  318. var hasAwards = item as IHasAwards;
  319. if (hasAwards != null && !string.IsNullOrEmpty(hasAwards.AwardSummary))
  320. {
  321. builder.Append("<AwardSummary>" + SecurityElement.Escape(hasAwards.AwardSummary) + "</AwardSummary>");
  322. }
  323. var hasBudget = item as IHasBudget;
  324. if (hasBudget != null)
  325. {
  326. if (hasBudget.Budget.HasValue)
  327. {
  328. builder.Append("<Budget>" + SecurityElement.Escape(hasBudget.Budget.Value.ToString(UsCulture)) + "</Budget>");
  329. }
  330. if (hasBudget.Revenue.HasValue)
  331. {
  332. builder.Append("<Revenue>" + SecurityElement.Escape(hasBudget.Revenue.Value.ToString(UsCulture)) + "</Revenue>");
  333. }
  334. }
  335. if (item.CommunityRating.HasValue)
  336. {
  337. builder.Append("<Rating>" + SecurityElement.Escape(item.CommunityRating.Value.ToString(UsCulture)) + "</Rating>");
  338. }
  339. if (item.VoteCount.HasValue)
  340. {
  341. builder.Append("<VoteCount>" + SecurityElement.Escape(item.VoteCount.Value.ToString(UsCulture)) + "</VoteCount>");
  342. }
  343. if (item.ProductionYear.HasValue && !(item is Person))
  344. {
  345. builder.Append("<ProductionYear>" + SecurityElement.Escape(item.ProductionYear.Value.ToString(UsCulture)) + "</ProductionYear>");
  346. }
  347. if (!string.IsNullOrEmpty(item.HomePageUrl))
  348. {
  349. builder.Append("<Website>" + SecurityElement.Escape(item.HomePageUrl) + "</Website>");
  350. }
  351. var hasAspectRatio = item as IHasAspectRatio;
  352. if (hasAspectRatio != null)
  353. {
  354. if (!string.IsNullOrEmpty(hasAspectRatio.AspectRatio))
  355. {
  356. builder.Append("<AspectRatio>" + SecurityElement.Escape(hasAspectRatio.AspectRatio) + "</AspectRatio>");
  357. }
  358. }
  359. if (!string.IsNullOrEmpty(item.PreferredMetadataLanguage))
  360. {
  361. builder.Append("<Language>" + SecurityElement.Escape(item.PreferredMetadataLanguage) + "</Language>");
  362. }
  363. if (!string.IsNullOrEmpty(item.PreferredMetadataCountryCode))
  364. {
  365. builder.Append("<CountryCode>" + SecurityElement.Escape(item.PreferredMetadataCountryCode) + "</CountryCode>");
  366. }
  367. // Use original runtime here, actual file runtime later in MediaInfo
  368. var runTimeTicks = item.RunTimeTicks;
  369. if (runTimeTicks.HasValue)
  370. {
  371. var timespan = TimeSpan.FromTicks(runTimeTicks.Value);
  372. builder.Append("<RunningTime>" + Convert.ToInt32(timespan.TotalMinutes).ToString(UsCulture) + "</RunningTime>");
  373. }
  374. if (item.ProviderIds != null)
  375. {
  376. foreach (var providerKey in item.ProviderIds.Keys)
  377. {
  378. var providerId = item.ProviderIds[providerKey];
  379. if (!string.IsNullOrEmpty(providerId))
  380. {
  381. builder.Append(string.Format("<{0}>{1}</{0}>", providerKey + "Id", SecurityElement.Escape(providerId)));
  382. }
  383. }
  384. }
  385. var hasTagline = item as IHasTaglines;
  386. if (hasTagline != null)
  387. {
  388. if (hasTagline.Taglines.Count > 0)
  389. {
  390. builder.Append("<Taglines>");
  391. foreach (var tagline in hasTagline.Taglines)
  392. {
  393. builder.Append("<Tagline>" + SecurityElement.Escape(tagline) + "</Tagline>");
  394. }
  395. builder.Append("</Taglines>");
  396. }
  397. }
  398. if (item.Genres.Count > 0)
  399. {
  400. builder.Append("<Genres>");
  401. foreach (var genre in item.Genres)
  402. {
  403. builder.Append("<Genre>" + SecurityElement.Escape(genre) + "</Genre>");
  404. }
  405. builder.Append("</Genres>");
  406. }
  407. if (item.Studios.Count > 0)
  408. {
  409. builder.Append("<Studios>");
  410. foreach (var studio in item.Studios)
  411. {
  412. builder.Append("<Studio>" + SecurityElement.Escape(studio) + "</Studio>");
  413. }
  414. builder.Append("</Studios>");
  415. }
  416. if (item.Tags.Count > 0)
  417. {
  418. builder.Append("<Tags>");
  419. foreach (var tag in item.Tags)
  420. {
  421. builder.Append("<Tag>" + SecurityElement.Escape(tag) + "</Tag>");
  422. }
  423. builder.Append("</Tags>");
  424. }
  425. if (item.Keywords.Count > 0)
  426. {
  427. builder.Append("<PlotKeywords>");
  428. foreach (var tag in item.Keywords)
  429. {
  430. builder.Append("<PlotKeyword>" + SecurityElement.Escape(tag) + "</PlotKeyword>");
  431. }
  432. builder.Append("</PlotKeywords>");
  433. }
  434. var people = libraryManager.GetPeople(item);
  435. if (people.Count > 0)
  436. {
  437. builder.Append("<Persons>");
  438. foreach (var person in people)
  439. {
  440. builder.Append("<Person>");
  441. builder.Append("<Name>" + SecurityElement.Escape(person.Name) + "</Name>");
  442. builder.Append("<Type>" + SecurityElement.Escape(person.Type) + "</Type>");
  443. builder.Append("<Role>" + SecurityElement.Escape(person.Role) + "</Role>");
  444. if (person.SortOrder.HasValue)
  445. {
  446. builder.Append("<SortOrder>" + SecurityElement.Escape(person.SortOrder.Value.ToString(UsCulture)) + "</SortOrder>");
  447. }
  448. builder.Append("</Person>");
  449. }
  450. builder.Append("</Persons>");
  451. }
  452. var boxset = item as BoxSet;
  453. if (boxset != null)
  454. {
  455. AddLinkedChildren(boxset, builder, "CollectionItems", "CollectionItem");
  456. }
  457. var playlist = item as Playlist;
  458. if (playlist != null)
  459. {
  460. AddLinkedChildren(playlist, builder, "PlaylistItems", "PlaylistItem");
  461. }
  462. var hasShares = item as IHasShares;
  463. if (hasShares != null)
  464. {
  465. AddShares(hasShares, builder);
  466. }
  467. }
  468. public static void AddShares(IHasShares item, StringBuilder builder)
  469. {
  470. builder.Append("<Shares>");
  471. foreach (var share in item.Shares)
  472. {
  473. builder.Append("<Share>");
  474. builder.Append("<UserId>" + SecurityElement.Escape(share.UserId) + "</UserId>");
  475. builder.Append("<CanEdit>" + SecurityElement.Escape(share.CanEdit.ToString().ToLower()) + "</CanEdit>");
  476. builder.Append("</Share>");
  477. }
  478. builder.Append("</Shares>");
  479. }
  480. public static void AddChapters(Video item, StringBuilder builder, IItemRepository repository)
  481. {
  482. var chapters = repository.GetChapters(item.Id);
  483. builder.Append("<Chapters>");
  484. foreach (var chapter in chapters)
  485. {
  486. builder.Append("<Chapter>");
  487. builder.Append("<Name>" + SecurityElement.Escape(chapter.Name) + "</Name>");
  488. var time = TimeSpan.FromTicks(chapter.StartPositionTicks);
  489. var ms = Convert.ToInt64(time.TotalMilliseconds);
  490. builder.Append("<StartPositionMs>" + SecurityElement.Escape(ms.ToString(UsCulture)) + "</StartPositionMs>");
  491. builder.Append("</Chapter>");
  492. }
  493. builder.Append("</Chapters>");
  494. }
  495. /// <summary>
  496. /// Appends the media info.
  497. /// </summary>
  498. /// <typeparam name="T"></typeparam>
  499. public static void AddMediaInfo<T>(T item, StringBuilder builder, IItemRepository itemRepository)
  500. where T : BaseItem
  501. {
  502. var video = item as Video;
  503. if (video != null)
  504. {
  505. //AddChapters(video, builder, itemRepository);
  506. if (video.Video3DFormat.HasValue)
  507. {
  508. switch (video.Video3DFormat.Value)
  509. {
  510. case Video3DFormat.FullSideBySide:
  511. builder.Append("<Format3D>FSBS</Format3D>");
  512. break;
  513. case Video3DFormat.FullTopAndBottom:
  514. builder.Append("<Format3D>FTAB</Format3D>");
  515. break;
  516. case Video3DFormat.HalfSideBySide:
  517. builder.Append("<Format3D>HSBS</Format3D>");
  518. break;
  519. case Video3DFormat.HalfTopAndBottom:
  520. builder.Append("<Format3D>HTAB</Format3D>");
  521. break;
  522. case Video3DFormat.MVC:
  523. builder.Append("<Format3D>MVC</Format3D>");
  524. break;
  525. }
  526. }
  527. }
  528. }
  529. public static void AddLinkedChildren(Folder item, StringBuilder builder, string pluralNodeName, string singularNodeName)
  530. {
  531. var items = item.LinkedChildren
  532. .Where(i => i.Type == LinkedChildType.Manual)
  533. .ToList();
  534. if (items.Count == 0)
  535. {
  536. return;
  537. }
  538. builder.Append("<" + pluralNodeName + ">");
  539. foreach (var link in items)
  540. {
  541. builder.Append("<" + singularNodeName + ">");
  542. if (!string.IsNullOrWhiteSpace(link.Path))
  543. {
  544. builder.Append("<Path>" + SecurityElement.Escape((link.Path)) + "</Path>");
  545. }
  546. builder.Append("</" + singularNodeName + ">");
  547. }
  548. builder.Append("</" + pluralNodeName + ">");
  549. }
  550. }
  551. }