DidlBuilder.cs 41 KB

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