DidlBuilder.cs 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143
  1. using MediaBrowser.Model.Extensions;
  2. using MediaBrowser.Controller.Channels;
  3. using MediaBrowser.Controller.Drawing;
  4. using MediaBrowser.Controller.Entities;
  5. using MediaBrowser.Controller.Entities.Audio;
  6. using MediaBrowser.Controller.Entities.Movies;
  7. using MediaBrowser.Controller.Entities.TV;
  8. using MediaBrowser.Controller.Library;
  9. using MediaBrowser.Controller.Localization;
  10. using MediaBrowser.Controller.Playlists;
  11. using MediaBrowser.Dlna.ContentDirectory;
  12. using MediaBrowser.Model.Dlna;
  13. using MediaBrowser.Model.Drawing;
  14. using MediaBrowser.Model.Entities;
  15. using MediaBrowser.Model.Logging;
  16. using MediaBrowser.Model.Net;
  17. using System;
  18. using System.Globalization;
  19. using System.IO;
  20. using System.Linq;
  21. using System.Xml;
  22. using MediaBrowser.Controller.MediaEncoding;
  23. using MediaBrowser.Model.Configuration;
  24. namespace MediaBrowser.Dlna.Didl
  25. {
  26. public class DidlBuilder
  27. {
  28. private readonly CultureInfo _usCulture = new CultureInfo("en-US");
  29. private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
  30. private const string NS_DC = "http://purl.org/dc/elements/1.1/";
  31. private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
  32. private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
  33. private readonly DeviceProfile _profile;
  34. private readonly IImageProcessor _imageProcessor;
  35. private readonly string _serverAddress;
  36. private readonly string _accessToken;
  37. private readonly User _user;
  38. private readonly IUserDataManager _userDataManager;
  39. private readonly ILocalizationManager _localization;
  40. private readonly IMediaSourceManager _mediaSourceManager;
  41. private readonly ILogger _logger;
  42. private readonly ILibraryManager _libraryManager;
  43. private readonly IMediaEncoder _mediaEncoder;
  44. public DidlBuilder(DeviceProfile profile, User user, IImageProcessor imageProcessor, string serverAddress, string accessToken, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, ILogger logger, ILibraryManager libraryManager, IMediaEncoder mediaEncoder)
  45. {
  46. _profile = profile;
  47. _imageProcessor = imageProcessor;
  48. _serverAddress = serverAddress;
  49. _userDataManager = userDataManager;
  50. _localization = localization;
  51. _mediaSourceManager = mediaSourceManager;
  52. _logger = logger;
  53. _libraryManager = libraryManager;
  54. _mediaEncoder = mediaEncoder;
  55. _accessToken = accessToken;
  56. _user = user;
  57. }
  58. public string GetItemDidl(DlnaOptions options, BaseItem item, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo)
  59. {
  60. var result = new XmlDocument();
  61. var didl = result.CreateElement(string.Empty, "DIDL-Lite", NS_DIDL);
  62. didl.SetAttribute("xmlns:dc", NS_DC);
  63. didl.SetAttribute("xmlns:dlna", NS_DLNA);
  64. didl.SetAttribute("xmlns:upnp", NS_UPNP);
  65. //didl.SetAttribute("xmlns:sec", NS_SEC);
  66. foreach (var att in _profile.XmlRootAttributes)
  67. {
  68. didl.SetAttribute(att.Name, att.Value);
  69. }
  70. result.AppendChild(didl);
  71. result.DocumentElement.AppendChild(GetItemElement(options, result, item, context, null, deviceId, filter, streamInfo));
  72. return result.DocumentElement.OuterXml;
  73. }
  74. public XmlElement GetItemElement(DlnaOptions options, XmlDocument doc, BaseItem item, BaseItem context, StubType? contextStubType, string deviceId, Filter filter, StreamInfo streamInfo = null)
  75. {
  76. var clientId = GetClientId(item, null);
  77. var element = doc.CreateElement(string.Empty, "item", NS_DIDL);
  78. element.SetAttribute("restricted", "1");
  79. element.SetAttribute("id", clientId);
  80. if (context != null)
  81. {
  82. element.SetAttribute("parentID", GetClientId(context, contextStubType));
  83. }
  84. else
  85. {
  86. var parent = item.DisplayParentId;
  87. if (parent.HasValue)
  88. {
  89. element.SetAttribute("parentID", GetClientId(parent.Value, null));
  90. }
  91. }
  92. //AddBookmarkInfo(item, user, element);
  93. AddGeneralProperties(item, null, context, element, filter);
  94. // refID?
  95. // storeAttribute(itemNode, object, ClassProperties.REF_ID, false);
  96. var hasMediaSources = item as IHasMediaSources;
  97. if (hasMediaSources != null)
  98. {
  99. if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
  100. {
  101. AddAudioResource(options, element, hasMediaSources, deviceId, filter, streamInfo);
  102. }
  103. else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
  104. {
  105. AddVideoResource(options, element, hasMediaSources, deviceId, filter, streamInfo);
  106. }
  107. }
  108. AddCover(item, context, null, element);
  109. return element;
  110. }
  111. private ILogger GetStreamBuilderLogger(DlnaOptions options)
  112. {
  113. if (options.EnableDebugLog)
  114. {
  115. return _logger;
  116. }
  117. return new NullLogger();
  118. }
  119. private void AddVideoResource(DlnaOptions options, XmlElement container, IHasMediaSources video, string deviceId, Filter filter, StreamInfo streamInfo = null)
  120. {
  121. if (streamInfo == null)
  122. {
  123. var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user).ToList();
  124. streamInfo = new StreamBuilder(_mediaEncoder, GetStreamBuilderLogger(options)).BuildVideoItem(new VideoOptions
  125. {
  126. ItemId = GetClientId(video),
  127. MediaSources = sources,
  128. Profile = _profile,
  129. DeviceId = deviceId,
  130. MaxBitrate = _profile.MaxStreamingBitrate
  131. });
  132. }
  133. var targetWidth = streamInfo.TargetWidth;
  134. var targetHeight = streamInfo.TargetHeight;
  135. var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(streamInfo.Container,
  136. streamInfo.VideoCodec,
  137. streamInfo.TargetAudioCodec,
  138. targetWidth,
  139. targetHeight,
  140. streamInfo.TargetVideoBitDepth,
  141. streamInfo.TargetVideoBitrate,
  142. streamInfo.TargetTimestamp,
  143. streamInfo.IsDirectStream,
  144. streamInfo.RunTimeTicks,
  145. streamInfo.TargetVideoProfile,
  146. streamInfo.TargetVideoLevel,
  147. streamInfo.TargetFramerate,
  148. streamInfo.TargetPacketLength,
  149. streamInfo.TranscodeSeekInfo,
  150. streamInfo.IsTargetAnamorphic,
  151. streamInfo.TargetRefFrames,
  152. streamInfo.TargetVideoStreamCount,
  153. streamInfo.TargetAudioStreamCount,
  154. streamInfo.TargetVideoCodecTag,
  155. streamInfo.IsTargetAVC);
  156. foreach (var contentFeature in contentFeatureList)
  157. {
  158. AddVideoResource(container, video, deviceId, filter, contentFeature, streamInfo);
  159. }
  160. foreach (var subtitle in streamInfo.GetSubtitleProfiles(false, _serverAddress, _accessToken))
  161. {
  162. if (subtitle.DeliveryMethod == SubtitleDeliveryMethod.External)
  163. {
  164. var subtitleAdded = AddSubtitleElement(container, subtitle);
  165. if (subtitleAdded && _profile.EnableSingleSubtitleLimit)
  166. {
  167. break;
  168. }
  169. }
  170. }
  171. }
  172. private bool AddSubtitleElement(XmlElement container, SubtitleStreamInfo info)
  173. {
  174. var subtitleProfile = _profile.SubtitleProfiles
  175. .FirstOrDefault(i => string.Equals(info.Format, i.Format, StringComparison.OrdinalIgnoreCase) && i.Method == SubtitleDeliveryMethod.External);
  176. if (subtitleProfile == null)
  177. {
  178. return false;
  179. }
  180. var subtitleMode = subtitleProfile.DidlMode;
  181. if (string.Equals(subtitleMode, "CaptionInfoEx", StringComparison.OrdinalIgnoreCase))
  182. {
  183. // <sec:CaptionInfoEx sec:type="srt">http://192.168.1.3:9999/video.srt</sec:CaptionInfoEx>
  184. // <sec:CaptionInfo sec:type="srt">http://192.168.1.3:9999/video.srt</sec:CaptionInfo>
  185. var res = container.OwnerDocument.CreateElement("CaptionInfoEx", "sec");
  186. res.InnerText = info.Url;
  187. //// TODO: attribute needs SEC:
  188. res.SetAttribute("type", "sec", info.Format.ToLower());
  189. container.AppendChild(res);
  190. }
  191. else if (string.Equals(subtitleMode, "smi", StringComparison.OrdinalIgnoreCase))
  192. {
  193. var res = container.OwnerDocument.CreateElement(string.Empty, "res", NS_DIDL);
  194. res.InnerText = info.Url;
  195. res.SetAttribute("protocolInfo", "http-get:*:smi/caption:*");
  196. container.AppendChild(res);
  197. }
  198. else
  199. {
  200. var res = container.OwnerDocument.CreateElement(string.Empty, "res", NS_DIDL);
  201. res.InnerText = info.Url;
  202. var protocolInfo = string.Format("http-get:*:text/{0}:*", info.Format.ToLower());
  203. res.SetAttribute("protocolInfo", protocolInfo);
  204. container.AppendChild(res);
  205. }
  206. return true;
  207. }
  208. private void AddVideoResource(XmlElement container, IHasMediaSources video, string deviceId, Filter filter, string contentFeatures, StreamInfo streamInfo)
  209. {
  210. var res = container.OwnerDocument.CreateElement(string.Empty, "res", NS_DIDL);
  211. var url = streamInfo.ToDlnaUrl(_serverAddress, _accessToken);
  212. res.InnerText = url;
  213. var mediaSource = streamInfo.MediaSource;
  214. if (mediaSource.RunTimeTicks.HasValue)
  215. {
  216. res.SetAttribute("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
  217. }
  218. if (filter.Contains("res@size"))
  219. {
  220. if (streamInfo.IsDirectStream || streamInfo.EstimateContentLength)
  221. {
  222. var size = streamInfo.TargetSize;
  223. if (size.HasValue)
  224. {
  225. res.SetAttribute("size", size.Value.ToString(_usCulture));
  226. }
  227. }
  228. }
  229. var totalBitrate = streamInfo.TargetTotalBitrate;
  230. var targetSampleRate = streamInfo.TargetAudioSampleRate;
  231. var targetChannels = streamInfo.TargetAudioChannels;
  232. var targetWidth = streamInfo.TargetWidth;
  233. var targetHeight = streamInfo.TargetHeight;
  234. if (targetChannels.HasValue)
  235. {
  236. res.SetAttribute("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
  237. }
  238. if (filter.Contains("res@resolution"))
  239. {
  240. if (targetWidth.HasValue && targetHeight.HasValue)
  241. {
  242. res.SetAttribute("resolution", string.Format("{0}x{1}", targetWidth.Value, targetHeight.Value));
  243. }
  244. }
  245. if (targetSampleRate.HasValue)
  246. {
  247. res.SetAttribute("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
  248. }
  249. if (totalBitrate.HasValue)
  250. {
  251. res.SetAttribute("bitrate", totalBitrate.Value.ToString(_usCulture));
  252. }
  253. var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container,
  254. streamInfo.TargetAudioCodec,
  255. streamInfo.VideoCodec,
  256. streamInfo.TargetAudioBitrate,
  257. targetWidth,
  258. targetHeight,
  259. streamInfo.TargetVideoBitDepth,
  260. streamInfo.TargetVideoProfile,
  261. streamInfo.TargetVideoLevel,
  262. streamInfo.TargetFramerate,
  263. streamInfo.TargetPacketLength,
  264. streamInfo.TargetTimestamp,
  265. streamInfo.IsTargetAnamorphic,
  266. streamInfo.TargetRefFrames,
  267. streamInfo.TargetVideoStreamCount,
  268. streamInfo.TargetAudioStreamCount,
  269. streamInfo.TargetVideoCodecTag,
  270. streamInfo.IsTargetAVC);
  271. var filename = url.Substring(0, url.IndexOf('?'));
  272. var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType)
  273. ? MimeTypes.GetMimeType(filename)
  274. : mediaProfile.MimeType;
  275. res.SetAttribute("protocolInfo", String.Format(
  276. "http-get:*:{0}:{1}",
  277. mimeType,
  278. contentFeatures
  279. ));
  280. container.AppendChild(res);
  281. }
  282. private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem context)
  283. {
  284. if (itemStubType.HasValue && itemStubType.Value == StubType.People)
  285. {
  286. if (item is Video)
  287. {
  288. return _localization.GetLocalizedString("HeaderCastCrew");
  289. }
  290. return _localization.GetLocalizedString("HeaderPeople");
  291. }
  292. var episode = item as Episode;
  293. var season = context as Season;
  294. if (episode != null && season != null)
  295. {
  296. // This is a special embedded within a season
  297. if (item.ParentIndexNumber.HasValue && item.ParentIndexNumber.Value == 0)
  298. {
  299. if (season.IndexNumber.HasValue && season.IndexNumber.Value != 0)
  300. {
  301. return string.Format(_localization.GetLocalizedString("ValueSpecialEpisodeName"), item.Name);
  302. }
  303. }
  304. if (item.IndexNumber.HasValue)
  305. {
  306. var number = item.IndexNumber.Value.ToString("00").ToString(CultureInfo.InvariantCulture);
  307. if (episode.IndexNumberEnd.HasValue)
  308. {
  309. number += "-" + episode.IndexNumberEnd.Value.ToString("00").ToString(CultureInfo.InvariantCulture);
  310. }
  311. return number + " - " + item.Name;
  312. }
  313. }
  314. return item.Name;
  315. }
  316. private void AddAudioResource(DlnaOptions options, XmlElement container, IHasMediaSources audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
  317. {
  318. var res = container.OwnerDocument.CreateElement(string.Empty, "res", NS_DIDL);
  319. if (streamInfo == null)
  320. {
  321. var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user).ToList();
  322. streamInfo = new StreamBuilder(_mediaEncoder, GetStreamBuilderLogger(options)).BuildAudioItem(new AudioOptions
  323. {
  324. ItemId = GetClientId(audio),
  325. MediaSources = sources,
  326. Profile = _profile,
  327. DeviceId = deviceId
  328. });
  329. }
  330. var url = streamInfo.ToDlnaUrl(_serverAddress, _accessToken);
  331. res.InnerText = url;
  332. var mediaSource = streamInfo.MediaSource;
  333. if (mediaSource.RunTimeTicks.HasValue)
  334. {
  335. res.SetAttribute("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
  336. }
  337. if (filter.Contains("res@size"))
  338. {
  339. if (streamInfo.IsDirectStream || streamInfo.EstimateContentLength)
  340. {
  341. var size = streamInfo.TargetSize;
  342. if (size.HasValue)
  343. {
  344. res.SetAttribute("size", size.Value.ToString(_usCulture));
  345. }
  346. }
  347. }
  348. var targetAudioBitrate = streamInfo.TargetAudioBitrate;
  349. var targetSampleRate = streamInfo.TargetAudioSampleRate;
  350. var targetChannels = streamInfo.TargetAudioChannels;
  351. if (targetChannels.HasValue)
  352. {
  353. res.SetAttribute("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
  354. }
  355. if (targetSampleRate.HasValue)
  356. {
  357. res.SetAttribute("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
  358. }
  359. if (targetAudioBitrate.HasValue)
  360. {
  361. res.SetAttribute("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
  362. }
  363. var mediaProfile = _profile.GetAudioMediaProfile(streamInfo.Container,
  364. streamInfo.TargetAudioCodec,
  365. targetChannels,
  366. targetAudioBitrate);
  367. var filename = url.Substring(0, url.IndexOf('?'));
  368. var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType)
  369. ? MimeTypes.GetMimeType(filename)
  370. : mediaProfile.MimeType;
  371. var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container,
  372. streamInfo.TargetAudioCodec,
  373. targetAudioBitrate,
  374. targetSampleRate,
  375. targetChannels,
  376. streamInfo.IsDirectStream,
  377. streamInfo.RunTimeTicks,
  378. streamInfo.TranscodeSeekInfo);
  379. res.SetAttribute("protocolInfo", String.Format(
  380. "http-get:*:{0}:{1}",
  381. mimeType,
  382. contentFeatures
  383. ));
  384. container.AppendChild(res);
  385. }
  386. public static bool IsIdRoot(string id)
  387. {
  388. if (string.IsNullOrWhiteSpace(id) ||
  389. string.Equals(id, "0", StringComparison.OrdinalIgnoreCase)
  390. // Samsung sometimes uses 1 as root
  391. || string.Equals(id, "1", StringComparison.OrdinalIgnoreCase))
  392. {
  393. return true;
  394. }
  395. return false;
  396. }
  397. public XmlElement GetFolderElement(XmlDocument doc, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
  398. {
  399. var container = doc.CreateElement(string.Empty, "container", NS_DIDL);
  400. container.SetAttribute("restricted", "0");
  401. container.SetAttribute("searchable", "1");
  402. container.SetAttribute("childCount", childCount.ToString(_usCulture));
  403. var clientId = GetClientId(folder, stubType);
  404. if (string.Equals(requestedId, "0"))
  405. {
  406. container.SetAttribute("id", "0");
  407. container.SetAttribute("parentID", "-1");
  408. }
  409. else
  410. {
  411. container.SetAttribute("id", clientId);
  412. if (context != null)
  413. {
  414. container.SetAttribute("parentID", GetClientId(context, null));
  415. }
  416. else
  417. {
  418. var parent = folder.DisplayParentId;
  419. if (!parent.HasValue)
  420. {
  421. container.SetAttribute("parentID", "0");
  422. }
  423. else
  424. {
  425. container.SetAttribute("parentID", GetClientId(parent.Value, null));
  426. }
  427. }
  428. }
  429. AddCommonFields(folder, stubType, null, container, filter);
  430. AddCover(folder, context, stubType, container);
  431. return container;
  432. }
  433. //private void AddBookmarkInfo(BaseItem item, User user, XmlElement element)
  434. //{
  435. // var userdata = _userDataManager.GetUserData(user.Id, item.GetUserDataKey());
  436. // if (userdata.PlaybackPositionTicks > 0)
  437. // {
  438. // var dcmInfo = element.OwnerDocument.CreateElement("sec", "dcmInfo", NS_SEC);
  439. // dcmInfo.InnerText = string.Format("BM={0}", Convert.ToInt32(TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds).ToString(_usCulture));
  440. // element.AppendChild(dcmInfo);
  441. // }
  442. //}
  443. /// <summary>
  444. /// Adds fields used by both items and folders
  445. /// </summary>
  446. /// <param name="item">The item.</param>
  447. /// <param name="itemStubType">Type of the item stub.</param>
  448. /// <param name="context">The context.</param>
  449. /// <param name="element">The element.</param>
  450. /// <param name="filter">The filter.</param>
  451. private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlElement element, Filter filter)
  452. {
  453. // Don't filter on dc:title because not all devices will include it in the filter
  454. // MediaMonkey for example won't display content without a title
  455. //if (filter.Contains("dc:title"))
  456. {
  457. AddValue(element, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC);
  458. }
  459. element.AppendChild(CreateObjectClass(element.OwnerDocument, item, itemStubType));
  460. if (filter.Contains("dc:date"))
  461. {
  462. if (item.PremiereDate.HasValue)
  463. {
  464. AddValue(element, "dc", "date", item.PremiereDate.Value.ToString("o"), NS_DC);
  465. }
  466. }
  467. if (filter.Contains("upnp:genre"))
  468. {
  469. foreach (var genre in item.Genres)
  470. {
  471. AddValue(element, "upnp", "genre", genre, NS_UPNP);
  472. }
  473. }
  474. foreach (var studio in item.Studios)
  475. {
  476. AddValue(element, "upnp", "publisher", studio, NS_UPNP);
  477. }
  478. if (filter.Contains("dc:description"))
  479. {
  480. var desc = item.Overview;
  481. if (!string.IsNullOrEmpty(item.ShortOverview))
  482. {
  483. desc = item.ShortOverview;
  484. }
  485. if (!string.IsNullOrWhiteSpace(desc))
  486. {
  487. AddValue(element, "dc", "description", desc, NS_DC);
  488. }
  489. }
  490. if (filter.Contains("upnp:longDescription"))
  491. {
  492. if (!string.IsNullOrWhiteSpace(item.Overview))
  493. {
  494. AddValue(element, "upnp", "longDescription", item.Overview, NS_UPNP);
  495. }
  496. }
  497. if (!string.IsNullOrEmpty(item.OfficialRating))
  498. {
  499. if (filter.Contains("dc:rating"))
  500. {
  501. AddValue(element, "dc", "rating", item.OfficialRating, NS_DC);
  502. }
  503. if (filter.Contains("upnp:rating"))
  504. {
  505. AddValue(element, "upnp", "rating", item.OfficialRating, NS_UPNP);
  506. }
  507. }
  508. AddPeople(item, element);
  509. }
  510. private XmlElement CreateObjectClass(XmlDocument result, BaseItem item, StubType? stubType)
  511. {
  512. // More types here
  513. // http://oss.linn.co.uk/repos/Public/LibUpnpCil/DidlLite/UpnpAv/Test/TestDidlLite.cs
  514. var objectClass = result.CreateElement("upnp", "class", NS_UPNP);
  515. if (item.IsFolder || stubType.HasValue)
  516. {
  517. string classType = null;
  518. if (!_profile.RequiresPlainFolders)
  519. {
  520. if (item is MusicAlbum)
  521. {
  522. classType = "object.container.album.musicAlbum";
  523. }
  524. else if (item is MusicArtist)
  525. {
  526. classType = "object.container.person.musicArtist";
  527. }
  528. else if (item is Series || item is Season || item is BoxSet || item is Video)
  529. {
  530. classType = "object.container.album.videoAlbum";
  531. }
  532. else if (item is Playlist)
  533. {
  534. classType = "object.container.playlistContainer";
  535. }
  536. else if (item is PhotoAlbum)
  537. {
  538. classType = "object.container.album.photoAlbum";
  539. }
  540. }
  541. objectClass.InnerText = classType ?? "object.container.storageFolder";
  542. }
  543. else if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
  544. {
  545. objectClass.InnerText = "object.item.audioItem.musicTrack";
  546. }
  547. else if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
  548. {
  549. objectClass.InnerText = "object.item.imageItem.photo";
  550. }
  551. else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
  552. {
  553. if (!_profile.RequiresPlainVideoItems && item is Movie)
  554. {
  555. objectClass.InnerText = "object.item.videoItem.movie";
  556. }
  557. else if (!_profile.RequiresPlainVideoItems && item is MusicVideo)
  558. {
  559. objectClass.InnerText = "object.item.videoItem.musicVideoClip";
  560. }
  561. else
  562. {
  563. objectClass.InnerText = "object.item.videoItem";
  564. }
  565. }
  566. else if (item is MusicGenre)
  567. {
  568. objectClass.InnerText = _profile.RequiresPlainFolders ? "object.container.storageFolder" : "object.container.genre.musicGenre";
  569. }
  570. else if (item is Genre || item is GameGenre)
  571. {
  572. objectClass.InnerText = _profile.RequiresPlainFolders ? "object.container.storageFolder" : "object.container.genre";
  573. }
  574. else
  575. {
  576. objectClass.InnerText = "object.item";
  577. }
  578. return objectClass;
  579. }
  580. private void AddPeople(BaseItem item, XmlElement element)
  581. {
  582. var types = new[] { PersonType.Director, PersonType.Writer, PersonType.Producer, PersonType.Composer, "Creator" };
  583. var people = _libraryManager.GetPeople(item);
  584. foreach (var actor in people)
  585. {
  586. var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
  587. ?? PersonType.Actor;
  588. AddValue(element, "upnp", type.ToLower(), actor.Name, NS_UPNP);
  589. }
  590. }
  591. private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlElement element, Filter filter)
  592. {
  593. AddCommonFields(item, itemStubType, context, element, filter);
  594. var audio = item as Audio;
  595. if (audio != null)
  596. {
  597. foreach (var artist in audio.Artists)
  598. {
  599. AddValue(element, "upnp", "artist", artist, NS_UPNP);
  600. }
  601. if (!string.IsNullOrEmpty(audio.Album))
  602. {
  603. AddValue(element, "upnp", "album", audio.Album, NS_UPNP);
  604. }
  605. foreach (var artist in audio.AlbumArtists)
  606. {
  607. AddAlbumArtist(element, artist);
  608. }
  609. }
  610. var album = item as MusicAlbum;
  611. if (album != null)
  612. {
  613. foreach (var artist in album.AlbumArtists)
  614. {
  615. AddAlbumArtist(element, artist);
  616. AddValue(element, "upnp", "artist", artist, NS_UPNP);
  617. }
  618. foreach (var artist in album.Artists)
  619. {
  620. AddValue(element, "upnp", "artist", artist, NS_UPNP);
  621. }
  622. }
  623. var musicVideo = item as MusicVideo;
  624. if (musicVideo != null)
  625. {
  626. foreach (var artist in musicVideo.Artists)
  627. {
  628. AddValue(element, "upnp", "artist", artist, NS_UPNP);
  629. AddAlbumArtist(element, artist);
  630. }
  631. if (!string.IsNullOrEmpty(musicVideo.Album))
  632. {
  633. AddValue(element, "upnp", "album", musicVideo.Album, NS_UPNP);
  634. }
  635. }
  636. if (item.IndexNumber.HasValue)
  637. {
  638. AddValue(element, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP);
  639. if (item is Episode)
  640. {
  641. AddValue(element, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP);
  642. }
  643. }
  644. }
  645. private void AddAlbumArtist(XmlElement elem, string name)
  646. {
  647. try
  648. {
  649. var newNode = elem.OwnerDocument.CreateElement("upnp", "artist", NS_UPNP);
  650. newNode.InnerText = name;
  651. newNode.SetAttribute("role", "AlbumArtist");
  652. elem.AppendChild(newNode);
  653. }
  654. catch (XmlException)
  655. {
  656. //_logger.Error("Error adding xml value: " + value);
  657. }
  658. }
  659. private void AddValue(XmlElement elem, string prefix, string name, string value, string namespaceUri)
  660. {
  661. try
  662. {
  663. var date = elem.OwnerDocument.CreateElement(prefix, name, namespaceUri);
  664. date.InnerText = value;
  665. elem.AppendChild(date);
  666. }
  667. catch (XmlException)
  668. {
  669. //_logger.Error("Error adding xml value: " + value);
  670. }
  671. }
  672. private void AddCover(BaseItem item, BaseItem context, StubType? stubType, XmlElement element)
  673. {
  674. if (stubType.HasValue && stubType.Value == StubType.People)
  675. {
  676. AddEmbeddedImageAsCover("people", element);
  677. return;
  678. }
  679. ImageDownloadInfo imageInfo = null;
  680. if (context is UserView)
  681. {
  682. var episode = item as Episode;
  683. if (episode != null)
  684. {
  685. var parent = episode.Series;
  686. if (parent != null)
  687. {
  688. imageInfo = GetImageInfo(parent);
  689. }
  690. }
  691. }
  692. // Finally, just use the image from the item
  693. if (imageInfo == null)
  694. {
  695. imageInfo = GetImageInfo(item);
  696. }
  697. if (imageInfo == null)
  698. {
  699. return;
  700. }
  701. var result = element.OwnerDocument;
  702. var playbackPercentage = 0;
  703. var unplayedCount = 0;
  704. if (item is Video)
  705. {
  706. var userData = _userDataManager.GetUserDataDto(item, _user).Result;
  707. playbackPercentage = Convert.ToInt32(userData.PlayedPercentage ?? 0);
  708. if (playbackPercentage >= 100 || userData.Played)
  709. {
  710. playbackPercentage = 100;
  711. }
  712. }
  713. else if (item is Series || item is Season || item is BoxSet)
  714. {
  715. var userData = _userDataManager.GetUserDataDto(item, _user).Result;
  716. if (userData.Played)
  717. {
  718. playbackPercentage = 100;
  719. }
  720. else
  721. {
  722. unplayedCount = userData.UnplayedItemCount ?? 0;
  723. }
  724. }
  725. var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, playbackPercentage, unplayedCount, "jpg");
  726. var icon = result.CreateElement("upnp", "albumArtURI", NS_UPNP);
  727. var profile = result.CreateAttribute("dlna", "profileID", NS_DLNA);
  728. profile.InnerText = _profile.AlbumArtPn;
  729. icon.SetAttributeNode(profile);
  730. icon.InnerText = albumartUrlInfo.Url;
  731. element.AppendChild(icon);
  732. // TOOD: Remove these default values
  733. var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, playbackPercentage, unplayedCount, "jpg");
  734. icon = result.CreateElement("upnp", "icon", NS_UPNP);
  735. icon.InnerText = iconUrlInfo.Url;
  736. element.AppendChild(icon);
  737. if (!_profile.EnableAlbumArtInDidl)
  738. {
  739. if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
  740. || string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
  741. {
  742. if (!stubType.HasValue)
  743. {
  744. return;
  745. }
  746. }
  747. }
  748. AddImageResElement(item, element, 160, 160, playbackPercentage, unplayedCount, "jpg", "JPEG_TN");
  749. if (!_profile.EnableSingleAlbumArtLimit)
  750. {
  751. AddImageResElement(item, element, 4096, 4096, playbackPercentage, unplayedCount, "jpg", "JPEG_LRG");
  752. AddImageResElement(item, element, 1024, 768, playbackPercentage, unplayedCount, "jpg", "JPEG_MED");
  753. AddImageResElement(item, element, 640, 480, playbackPercentage, unplayedCount, "jpg", "JPEG_SM");
  754. AddImageResElement(item, element, 4096, 4096, playbackPercentage, unplayedCount, "png", "PNG_LRG");
  755. AddImageResElement(item, element, 160, 160, playbackPercentage, unplayedCount, "png", "PNG_TN");
  756. }
  757. }
  758. private void AddEmbeddedImageAsCover(string name, XmlElement element)
  759. {
  760. var result = element.OwnerDocument;
  761. var icon = result.CreateElement("upnp", "albumArtURI", NS_UPNP);
  762. var profile = result.CreateAttribute("dlna", "profileID", NS_DLNA);
  763. profile.InnerText = _profile.AlbumArtPn;
  764. icon.SetAttributeNode(profile);
  765. icon.InnerText = _serverAddress + "/Dlna/icons/people480.jpg";
  766. element.AppendChild(icon);
  767. icon = result.CreateElement("upnp", "icon", NS_UPNP);
  768. icon.InnerText = _serverAddress + "/Dlna/icons/people48.jpg";
  769. element.AppendChild(icon);
  770. }
  771. private void AddImageResElement(BaseItem item,
  772. XmlElement element,
  773. int maxWidth,
  774. int maxHeight,
  775. int playbackPercentage,
  776. int unplayedCount,
  777. string format,
  778. string org_Pn)
  779. {
  780. var imageInfo = GetImageInfo(item);
  781. if (imageInfo == null)
  782. {
  783. return;
  784. }
  785. var result = element.OwnerDocument;
  786. var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, playbackPercentage, unplayedCount, format);
  787. var res = result.CreateElement(string.Empty, "res", NS_DIDL);
  788. res.InnerText = albumartUrlInfo.Url;
  789. var width = albumartUrlInfo.Width;
  790. var height = albumartUrlInfo.Height;
  791. var contentFeatures = new ContentFeatureBuilder(_profile)
  792. .BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
  793. res.SetAttribute("protocolInfo", String.Format(
  794. "http-get:*:{0}:{1}",
  795. MimeTypes.GetMimeType("file." + format),
  796. contentFeatures
  797. ));
  798. if (width.HasValue && height.HasValue)
  799. {
  800. res.SetAttribute("resolution", string.Format("{0}x{1}", width.Value, height.Value));
  801. }
  802. element.AppendChild(res);
  803. }
  804. private ImageDownloadInfo GetImageInfo(BaseItem item)
  805. {
  806. if (item.HasImage(ImageType.Primary))
  807. {
  808. return GetImageInfo(item, ImageType.Primary);
  809. }
  810. if (item.HasImage(ImageType.Thumb))
  811. {
  812. return GetImageInfo(item, ImageType.Thumb);
  813. }
  814. if (item.HasImage(ImageType.Backdrop))
  815. {
  816. if (item is Channel)
  817. {
  818. return GetImageInfo(item, ImageType.Backdrop);
  819. }
  820. }
  821. item = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary));
  822. if (item != null)
  823. {
  824. if (item.HasImage(ImageType.Primary))
  825. {
  826. return GetImageInfo(item, ImageType.Primary);
  827. }
  828. }
  829. return null;
  830. }
  831. private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
  832. {
  833. var imageInfo = item.GetImageInfo(type, 0);
  834. string tag = null;
  835. try
  836. {
  837. tag = _imageProcessor.GetImageCacheTag(item, type);
  838. }
  839. catch
  840. {
  841. }
  842. int? width = null;
  843. int? height = null;
  844. try
  845. {
  846. var size = _imageProcessor.GetImageSize(imageInfo);
  847. width = Convert.ToInt32(size.Width);
  848. height = Convert.ToInt32(size.Height);
  849. }
  850. catch
  851. {
  852. }
  853. var inputFormat = (Path.GetExtension(imageInfo.Path) ?? string.Empty)
  854. .TrimStart('.')
  855. .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
  856. return new ImageDownloadInfo
  857. {
  858. ItemId = item.Id.ToString("N"),
  859. Type = type,
  860. ImageTag = tag,
  861. Width = width,
  862. Height = height,
  863. Format = inputFormat,
  864. ItemImageInfo = imageInfo
  865. };
  866. }
  867. class ImageDownloadInfo
  868. {
  869. internal string ItemId;
  870. internal string ImageTag;
  871. internal ImageType Type;
  872. internal int? Width;
  873. internal int? Height;
  874. internal bool IsDirectStream;
  875. internal string Format;
  876. internal ItemImageInfo ItemImageInfo;
  877. }
  878. class ImageUrlInfo
  879. {
  880. internal string Url;
  881. internal int? Width;
  882. internal int? Height;
  883. }
  884. public static string GetClientId(BaseItem item, StubType? stubType)
  885. {
  886. return GetClientId(item.Id, stubType);
  887. }
  888. public static string GetClientId(Guid idValue, StubType? stubType)
  889. {
  890. var id = idValue.ToString("N");
  891. if (stubType.HasValue)
  892. {
  893. id = stubType.Value.ToString().ToLower() + "_" + id;
  894. }
  895. return id;
  896. }
  897. public static string GetClientId(IHasMediaSources item)
  898. {
  899. var id = item.Id.ToString("N");
  900. return id;
  901. }
  902. private ImageUrlInfo GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, int playbackPercentage, int unplayedCount, string format)
  903. {
  904. var url = string.Format("{0}/Items/{1}/Images/{2}/0/{3}/{4}/{5}/{6}/{7}/{8}",
  905. _serverAddress,
  906. info.ItemId,
  907. info.Type,
  908. info.ImageTag,
  909. format,
  910. maxWidth.ToString(CultureInfo.InvariantCulture),
  911. maxHeight.ToString(CultureInfo.InvariantCulture),
  912. playbackPercentage.ToString(CultureInfo.InvariantCulture),
  913. unplayedCount.ToString(CultureInfo.InvariantCulture)
  914. );
  915. var width = info.Width;
  916. var height = info.Height;
  917. info.IsDirectStream = false;
  918. if (width.HasValue && height.HasValue)
  919. {
  920. var newSize = DrawingUtils.Resize(new ImageSize
  921. {
  922. Height = height.Value,
  923. Width = width.Value
  924. }, null, null, maxWidth, maxHeight);
  925. width = Convert.ToInt32(newSize.Width);
  926. height = Convert.ToInt32(newSize.Height);
  927. var normalizedFormat = format
  928. .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
  929. if (string.Equals(info.Format, normalizedFormat, StringComparison.OrdinalIgnoreCase))
  930. {
  931. info.IsDirectStream = maxWidth >= width.Value && maxHeight >= height.Value;
  932. }
  933. }
  934. return new ImageUrlInfo
  935. {
  936. Url = url,
  937. Width = width,
  938. Height = height
  939. };
  940. }
  941. }
  942. }