DidlBuilder.cs 42 KB

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