DescriptionXmlBuilder.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Linq;
  5. using System.Text;
  6. using Emby.Dlna.Common;
  7. using MediaBrowser.Model.Dlna;
  8. namespace Emby.Dlna.Server
  9. {
  10. public class DescriptionXmlBuilder
  11. {
  12. private readonly DeviceProfile _profile;
  13. private readonly CultureInfo _usCulture = new CultureInfo("en-US");
  14. private readonly string _serverUdn;
  15. private readonly string _serverAddress;
  16. private readonly string _serverName;
  17. private readonly string _serverId;
  18. public DescriptionXmlBuilder(DeviceProfile profile, string serverUdn, string serverAddress, string serverName, string serverId)
  19. {
  20. if (string.IsNullOrEmpty(serverUdn))
  21. {
  22. throw new ArgumentNullException(nameof(serverUdn));
  23. }
  24. if (string.IsNullOrEmpty(serverAddress))
  25. {
  26. throw new ArgumentNullException(nameof(serverAddress));
  27. }
  28. _profile = profile;
  29. _serverUdn = serverUdn;
  30. _serverAddress = serverAddress;
  31. _serverName = serverName;
  32. _serverId = serverId;
  33. }
  34. private static bool EnableAbsoluteUrls => false;
  35. public string GetXml()
  36. {
  37. var builder = new StringBuilder();
  38. builder.Append("<?xml version=\"1.0\"?>");
  39. builder.Append("<root");
  40. var attributes = _profile.XmlRootAttributes.ToList();
  41. attributes.Insert(0, new XmlAttribute
  42. {
  43. Name = "xmlns:dlna",
  44. Value = "urn:schemas-dlna-org:device-1-0"
  45. });
  46. attributes.Insert(0, new XmlAttribute
  47. {
  48. Name = "xmlns",
  49. Value = "urn:schemas-upnp-org:device-1-0"
  50. });
  51. foreach (var att in attributes)
  52. {
  53. builder.AppendFormat(" {0}=\"{1}\"", att.Name, att.Value);
  54. }
  55. builder.Append(">");
  56. builder.Append("<specVersion>");
  57. builder.Append("<major>1</major>");
  58. builder.Append("<minor>0</minor>");
  59. builder.Append("</specVersion>");
  60. if (!EnableAbsoluteUrls)
  61. {
  62. builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>");
  63. }
  64. AppendDeviceInfo(builder);
  65. builder.Append("</root>");
  66. return builder.ToString();
  67. }
  68. private void AppendDeviceInfo(StringBuilder builder)
  69. {
  70. builder.Append("<device>");
  71. AppendDeviceProperties(builder);
  72. AppendIconList(builder);
  73. builder.Append("<presentationURL>" + Escape(_serverAddress) + "/web/index.html</presentationURL>");
  74. AppendServiceList(builder);
  75. builder.Append("</device>");
  76. }
  77. private static readonly char[] s_escapeChars = new char[]
  78. {
  79. '<',
  80. '>',
  81. '"',
  82. '\'',
  83. '&'
  84. };
  85. private static readonly string[] s_escapeStringPairs = new[]
  86. {
  87. "<",
  88. "&lt;",
  89. ">",
  90. "&gt;",
  91. "\"",
  92. "&quot;",
  93. "'",
  94. "&apos;",
  95. "&",
  96. "&amp;"
  97. };
  98. private static string GetEscapeSequence(char c)
  99. {
  100. int num = s_escapeStringPairs.Length;
  101. for (int i = 0; i < num; i += 2)
  102. {
  103. string text = s_escapeStringPairs[i];
  104. string result = s_escapeStringPairs[i + 1];
  105. if (text[0] == c)
  106. {
  107. return result;
  108. }
  109. }
  110. return c.ToString(CultureInfo.InvariantCulture);
  111. }
  112. /// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary>
  113. /// <returns>The input string with invalid characters replaced.</returns>
  114. /// <param name="str">The string within which to escape invalid characters. </param>
  115. public static string Escape(string str)
  116. {
  117. if (str == null)
  118. {
  119. return null;
  120. }
  121. StringBuilder stringBuilder = null;
  122. int length = str.Length;
  123. int num = 0;
  124. while (true)
  125. {
  126. int num2 = str.IndexOfAny(s_escapeChars, num);
  127. if (num2 == -1)
  128. {
  129. break;
  130. }
  131. if (stringBuilder == null)
  132. {
  133. stringBuilder = new StringBuilder();
  134. }
  135. stringBuilder.Append(str, num, num2 - num);
  136. stringBuilder.Append(GetEscapeSequence(str[num2]));
  137. num = num2 + 1;
  138. }
  139. if (stringBuilder == null)
  140. {
  141. return str;
  142. }
  143. stringBuilder.Append(str, num, length - num);
  144. return stringBuilder.ToString();
  145. }
  146. private void AppendDeviceProperties(StringBuilder builder)
  147. {
  148. builder.Append("<dlna:X_DLNACAP/>");
  149. builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">DMS-1.50</dlna:X_DLNADOC>");
  150. builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">M-DMS-1.50</dlna:X_DLNADOC>");
  151. builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>");
  152. builder.Append("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>");
  153. builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>");
  154. builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>");
  155. builder.Append("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>");
  156. builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>");
  157. builder.Append("<modelNumber>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>");
  158. builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>");
  159. if (string.IsNullOrEmpty(_profile.SerialNumber))
  160. {
  161. builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>");
  162. }
  163. else
  164. {
  165. builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>");
  166. }
  167. builder.Append("<UPC/>");
  168. builder.Append("<UDN>uuid:" + Escape(_serverUdn) + "</UDN>");
  169. if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags))
  170. {
  171. builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">" + Escape(_profile.SonyAggregationFlags) + "</av:aggregationFlags>");
  172. }
  173. }
  174. private string GetFriendlyName()
  175. {
  176. if (string.IsNullOrEmpty(_profile.FriendlyName))
  177. {
  178. return "Jellyfin - " + _serverName;
  179. }
  180. var characterList = new List<char>();
  181. foreach (var c in _serverName)
  182. {
  183. if (char.IsLetterOrDigit(c) || c == '-')
  184. {
  185. characterList.Add(c);
  186. }
  187. }
  188. var characters = characterList.ToArray();
  189. var serverName = new string(characters);
  190. var name = _profile.FriendlyName?.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
  191. return name ?? string.Empty;
  192. }
  193. private void AppendIconList(StringBuilder builder)
  194. {
  195. builder.Append("<iconList>");
  196. foreach (var icon in GetIcons())
  197. {
  198. builder.Append("<icon>");
  199. builder.Append("<mimetype>" + Escape(icon.MimeType ?? string.Empty) + "</mimetype>");
  200. builder.Append("<width>" + Escape(icon.Width.ToString(_usCulture)) + "</width>");
  201. builder.Append("<height>" + Escape(icon.Height.ToString(_usCulture)) + "</height>");
  202. builder.Append("<depth>" + Escape(icon.Depth ?? string.Empty) + "</depth>");
  203. builder.Append("<url>" + BuildUrl(icon.Url) + "</url>");
  204. builder.Append("</icon>");
  205. }
  206. builder.Append("</iconList>");
  207. }
  208. private void AppendServiceList(StringBuilder builder)
  209. {
  210. builder.Append("<serviceList>");
  211. foreach (var service in GetServices())
  212. {
  213. builder.Append("<service>");
  214. builder.Append("<serviceType>" + Escape(service.ServiceType ?? string.Empty) + "</serviceType>");
  215. builder.Append("<serviceId>" + Escape(service.ServiceId ?? string.Empty) + "</serviceId>");
  216. builder.Append("<SCPDURL>" + BuildUrl(service.ScpdUrl) + "</SCPDURL>");
  217. builder.Append("<controlURL>" + BuildUrl(service.ControlUrl) + "</controlURL>");
  218. builder.Append("<eventSubURL>" + BuildUrl(service.EventSubUrl) + "</eventSubURL>");
  219. builder.Append("</service>");
  220. }
  221. builder.Append("</serviceList>");
  222. }
  223. private string BuildUrl(string url)
  224. {
  225. if (string.IsNullOrEmpty(url))
  226. {
  227. return string.Empty;
  228. }
  229. url = url.TrimStart('/');
  230. url = "/dlna/" + _serverUdn + "/" + url;
  231. if (EnableAbsoluteUrls)
  232. {
  233. url = _serverAddress.TrimEnd('/') + url;
  234. }
  235. return Escape(url);
  236. }
  237. private IEnumerable<DeviceIcon> GetIcons()
  238. => new[]
  239. {
  240. new DeviceIcon
  241. {
  242. MimeType = "image/png",
  243. Depth = "24",
  244. Width = 240,
  245. Height = 240,
  246. Url = "icons/logo240.png"
  247. },
  248. new DeviceIcon
  249. {
  250. MimeType = "image/jpeg",
  251. Depth = "24",
  252. Width = 240,
  253. Height = 240,
  254. Url = "icons/logo240.jpg"
  255. },
  256. new DeviceIcon
  257. {
  258. MimeType = "image/png",
  259. Depth = "24",
  260. Width = 120,
  261. Height = 120,
  262. Url = "icons/logo120.png"
  263. },
  264. new DeviceIcon
  265. {
  266. MimeType = "image/jpeg",
  267. Depth = "24",
  268. Width = 120,
  269. Height = 120,
  270. Url = "icons/logo120.jpg"
  271. },
  272. new DeviceIcon
  273. {
  274. MimeType = "image/png",
  275. Depth = "24",
  276. Width = 48,
  277. Height = 48,
  278. Url = "icons/logo48.png"
  279. },
  280. new DeviceIcon
  281. {
  282. MimeType = "image/jpeg",
  283. Depth = "24",
  284. Width = 48,
  285. Height = 48,
  286. Url = "icons/logo48.jpg"
  287. }
  288. };
  289. private IEnumerable<DeviceService> GetServices()
  290. {
  291. var list = new List<DeviceService>();
  292. list.Add(new DeviceService
  293. {
  294. ServiceType = "urn:schemas-upnp-org:service:ContentDirectory:1",
  295. ServiceId = "urn:upnp-org:serviceId:ContentDirectory",
  296. ScpdUrl = "contentdirectory/contentdirectory.xml",
  297. ControlUrl = "contentdirectory/control",
  298. EventSubUrl = "contentdirectory/events"
  299. });
  300. list.Add(new DeviceService
  301. {
  302. ServiceType = "urn:schemas-upnp-org:service:ConnectionManager:1",
  303. ServiceId = "urn:upnp-org:serviceId:ConnectionManager",
  304. ScpdUrl = "connectionmanager/connectionmanager.xml",
  305. ControlUrl = "connectionmanager/control",
  306. EventSubUrl = "connectionmanager/events"
  307. });
  308. if (_profile.EnableMSMediaReceiverRegistrar)
  309. {
  310. list.Add(new DeviceService
  311. {
  312. ServiceType = "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1",
  313. ServiceId = "urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar",
  314. ScpdUrl = "mediareceiverregistrar/mediareceiverregistrar.xml",
  315. ControlUrl = "mediareceiverregistrar/control",
  316. EventSubUrl = "mediareceiverregistrar/events"
  317. });
  318. }
  319. return list;
  320. }
  321. public override string ToString()
  322. {
  323. return GetXml();
  324. }
  325. }
  326. }