DidlBuilder.cs 41 KB

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