DlnaManager.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Common.Extensions;
  3. using MediaBrowser.Common.IO;
  4. using MediaBrowser.Controller.Dlna;
  5. using MediaBrowser.Controller.Drawing;
  6. using MediaBrowser.Controller.Dto;
  7. using MediaBrowser.Controller.Library;
  8. using MediaBrowser.Dlna.Profiles;
  9. using MediaBrowser.Dlna.Server;
  10. using MediaBrowser.Model.Dlna;
  11. using MediaBrowser.Model.Logging;
  12. using MediaBrowser.Model.Serialization;
  13. using System;
  14. using System.Collections.Generic;
  15. using System.IO;
  16. using System.Linq;
  17. using System.Text;
  18. using System.Text.RegularExpressions;
  19. namespace MediaBrowser.Dlna
  20. {
  21. public class DlnaManager : IDlnaManager
  22. {
  23. private readonly IApplicationPaths _appPaths;
  24. private readonly IXmlSerializer _xmlSerializer;
  25. private readonly IFileSystem _fileSystem;
  26. private readonly ILogger _logger;
  27. private readonly IJsonSerializer _jsonSerializer;
  28. private readonly IUserManager _userManager;
  29. private readonly ILibraryManager _libraryManager;
  30. private readonly IDtoService _dtoService;
  31. private readonly IImageProcessor _imageProcessor;
  32. private readonly IUserDataManager _userDataManager;
  33. public DlnaManager(IXmlSerializer xmlSerializer, IFileSystem fileSystem, IApplicationPaths appPaths, ILogger logger, IJsonSerializer jsonSerializer, IUserManager userManager, ILibraryManager libraryManager, IDtoService dtoService, IImageProcessor imageProcessor, IUserDataManager userDataManager)
  34. {
  35. _xmlSerializer = xmlSerializer;
  36. _fileSystem = fileSystem;
  37. _appPaths = appPaths;
  38. _logger = logger;
  39. _jsonSerializer = jsonSerializer;
  40. _userManager = userManager;
  41. _libraryManager = libraryManager;
  42. _dtoService = dtoService;
  43. _imageProcessor = imageProcessor;
  44. _userDataManager = userDataManager;
  45. //DumpProfiles();
  46. }
  47. public IEnumerable<DeviceProfile> GetProfiles()
  48. {
  49. ExtractProfilesIfNeeded();
  50. var list = GetProfiles(UserProfilesPath, DeviceProfileType.User)
  51. .OrderBy(i => i.Name)
  52. .ToList();
  53. list.AddRange(GetProfiles(SystemProfilesPath, DeviceProfileType.System)
  54. .OrderBy(i => i.Name));
  55. return list;
  56. }
  57. private void DumpProfiles()
  58. {
  59. var list = new List<DeviceProfile>
  60. {
  61. new SamsungSmartTvProfile(),
  62. new Xbox360Profile(),
  63. new XboxOneProfile(),
  64. new SonyPs3Profile(),
  65. new SonyBravia2010Profile(),
  66. new SonyBravia2011Profile(),
  67. new SonyBravia2012Profile(),
  68. new SonyBravia2013Profile(),
  69. new SonyBlurayPlayer2013Profile(),
  70. new SonyBlurayPlayerProfile(),
  71. new PanasonicVieraProfile(),
  72. new WdtvLiveProfile(),
  73. new DenonAvrProfile(),
  74. new LinksysDMA2100Profile(),
  75. new LgTvProfile(),
  76. new Foobar2000Profile(),
  77. new DefaultProfile()
  78. };
  79. foreach (var item in list)
  80. {
  81. var path = Path.Combine(_appPaths.ProgramDataPath, _fileSystem.GetValidFilename(item.Name) + ".xml");
  82. _xmlSerializer.SerializeToFile(item, path);
  83. }
  84. }
  85. private bool _extracted;
  86. private readonly object _syncLock = new object();
  87. private void ExtractProfilesIfNeeded()
  88. {
  89. if (!_extracted)
  90. {
  91. lock (_syncLock)
  92. {
  93. if (!_extracted)
  94. {
  95. try
  96. {
  97. ExtractSystemProfiles();
  98. }
  99. catch (Exception ex)
  100. {
  101. _logger.ErrorException("Error extracting DLNA profiles.", ex);
  102. }
  103. _extracted = true;
  104. }
  105. }
  106. }
  107. }
  108. public DeviceProfile GetDefaultProfile()
  109. {
  110. ExtractProfilesIfNeeded();
  111. return new DefaultProfile();
  112. }
  113. public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
  114. {
  115. if (deviceInfo == null)
  116. {
  117. throw new ArgumentNullException("deviceInfo");
  118. }
  119. var profile = GetProfiles()
  120. .FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
  121. if (profile != null)
  122. {
  123. _logger.Debug("Found matching device profile: {0}", profile.Name);
  124. }
  125. else
  126. {
  127. _logger.Debug("No matching device profile found. The default will need to be used.");
  128. LogUnmatchedProfile(deviceInfo);
  129. }
  130. return profile;
  131. }
  132. private void LogUnmatchedProfile(DeviceIdentification profile)
  133. {
  134. var builder = new StringBuilder();
  135. builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty));
  136. builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
  137. builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
  138. builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
  139. builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
  140. builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
  141. builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
  142. builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
  143. builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
  144. _logger.LogMultiline("No matching device profile found. The default will need to be used.", LogSeverity.Info, builder);
  145. }
  146. private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
  147. {
  148. if (!string.IsNullOrWhiteSpace(profileInfo.DeviceDescription))
  149. {
  150. if (deviceInfo.DeviceDescription == null || !Regex.IsMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
  151. return false;
  152. }
  153. if (!string.IsNullOrWhiteSpace(profileInfo.FriendlyName))
  154. {
  155. if (deviceInfo.FriendlyName == null || !Regex.IsMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
  156. return false;
  157. }
  158. if (!string.IsNullOrWhiteSpace(profileInfo.Manufacturer))
  159. {
  160. if (deviceInfo.Manufacturer == null || !Regex.IsMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
  161. return false;
  162. }
  163. if (!string.IsNullOrWhiteSpace(profileInfo.ManufacturerUrl))
  164. {
  165. if (deviceInfo.ManufacturerUrl == null || !Regex.IsMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
  166. return false;
  167. }
  168. if (!string.IsNullOrWhiteSpace(profileInfo.ModelDescription))
  169. {
  170. if (deviceInfo.ModelDescription == null || !Regex.IsMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
  171. return false;
  172. }
  173. if (!string.IsNullOrWhiteSpace(profileInfo.ModelName))
  174. {
  175. if (deviceInfo.ModelName == null || !Regex.IsMatch(deviceInfo.ModelName, profileInfo.ModelName))
  176. return false;
  177. }
  178. if (!string.IsNullOrWhiteSpace(profileInfo.ModelNumber))
  179. {
  180. if (deviceInfo.ModelNumber == null || !Regex.IsMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
  181. return false;
  182. }
  183. if (!string.IsNullOrWhiteSpace(profileInfo.ModelUrl))
  184. {
  185. if (deviceInfo.ModelUrl == null || !Regex.IsMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
  186. return false;
  187. }
  188. if (!string.IsNullOrWhiteSpace(profileInfo.SerialNumber))
  189. {
  190. if (deviceInfo.SerialNumber == null || !Regex.IsMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
  191. return false;
  192. }
  193. return true;
  194. }
  195. public DeviceProfile GetProfile(IDictionary<string, string> headers)
  196. {
  197. if (headers == null)
  198. {
  199. throw new ArgumentNullException("headers");
  200. }
  201. var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
  202. if (profile != null)
  203. {
  204. _logger.Debug("Found matching device profile: {0}", profile.Name);
  205. }
  206. return profile;
  207. }
  208. private bool IsMatch(IDictionary<string, string> headers, DeviceIdentification profileInfo)
  209. {
  210. return profileInfo.Headers.Any(i => IsMatch(headers, i));
  211. }
  212. private bool IsMatch(IDictionary<string, string> headers, HttpHeaderInfo header)
  213. {
  214. string value;
  215. if (headers.TryGetValue(header.Name, out value))
  216. {
  217. switch (header.Match)
  218. {
  219. case HeaderMatchType.Equals:
  220. return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
  221. case HeaderMatchType.Substring:
  222. return value.IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
  223. case HeaderMatchType.Regex:
  224. return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase);
  225. default:
  226. throw new ArgumentException("Unrecognized HeaderMatchType");
  227. }
  228. }
  229. return false;
  230. }
  231. private string UserProfilesPath
  232. {
  233. get
  234. {
  235. return Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
  236. }
  237. }
  238. private string SystemProfilesPath
  239. {
  240. get
  241. {
  242. return Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
  243. }
  244. }
  245. private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
  246. {
  247. try
  248. {
  249. return new DirectoryInfo(path)
  250. .EnumerateFiles("*", SearchOption.TopDirectoryOnly)
  251. .Where(i => string.Equals(i.Extension, ".xml", StringComparison.OrdinalIgnoreCase))
  252. .Select(i => ParseProfileXmlFile(i.FullName, type))
  253. .Where(i => i != null)
  254. .ToList();
  255. }
  256. catch (DirectoryNotFoundException)
  257. {
  258. return new List<DeviceProfile>();
  259. }
  260. }
  261. private DeviceProfile ParseProfileXmlFile(string path, DeviceProfileType type)
  262. {
  263. try
  264. {
  265. var profile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
  266. profile.Id = path.ToLower().GetMD5().ToString("N");
  267. profile.ProfileType = type;
  268. return profile;
  269. }
  270. catch (Exception ex)
  271. {
  272. _logger.ErrorException("Error parsing profile xml: {0}", ex, path);
  273. return null;
  274. }
  275. }
  276. public DeviceProfile GetProfile(string id)
  277. {
  278. if (string.IsNullOrWhiteSpace(id))
  279. {
  280. throw new ArgumentNullException("id");
  281. }
  282. var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id));
  283. return ParseProfileXmlFile(info.Path, info.Info.Type);
  284. }
  285. private IEnumerable<InternalProfileInfo> GetProfileInfosInternal()
  286. {
  287. ExtractProfilesIfNeeded();
  288. return GetProfileInfos(UserProfilesPath, DeviceProfileType.User)
  289. .Concat(GetProfileInfos(SystemProfilesPath, DeviceProfileType.System))
  290. .OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
  291. .ThenBy(i => i.Info.Name);
  292. }
  293. public IEnumerable<DeviceProfileInfo> GetProfileInfos()
  294. {
  295. return GetProfileInfosInternal().Select(i => i.Info);
  296. }
  297. private IEnumerable<InternalProfileInfo> GetProfileInfos(string path, DeviceProfileType type)
  298. {
  299. try
  300. {
  301. return new DirectoryInfo(path)
  302. .EnumerateFiles("*", SearchOption.TopDirectoryOnly)
  303. .Where(i => string.Equals(i.Extension, ".xml", StringComparison.OrdinalIgnoreCase))
  304. .Select(i => new InternalProfileInfo
  305. {
  306. Path = i.FullName,
  307. Info = new DeviceProfileInfo
  308. {
  309. Id = i.FullName.ToLower().GetMD5().ToString("N"),
  310. Name = Path.GetFileNameWithoutExtension(i.FullName),
  311. Type = type
  312. }
  313. })
  314. .ToList();
  315. }
  316. catch (DirectoryNotFoundException)
  317. {
  318. return new List<InternalProfileInfo>();
  319. }
  320. }
  321. private void ExtractSystemProfiles()
  322. {
  323. var assembly = GetType().Assembly;
  324. var namespaceName = GetType().Namespace + ".Profiles.Xml.";
  325. var systemProfilesPath = SystemProfilesPath;
  326. foreach (var name in assembly.GetManifestResourceNames()
  327. .Where(i => i.StartsWith(namespaceName))
  328. .ToList())
  329. {
  330. var filename = Path.GetFileName(name).Substring(namespaceName.Length);
  331. var path = Path.Combine(systemProfilesPath, filename);
  332. using (var stream = assembly.GetManifestResourceStream(name))
  333. {
  334. var fileInfo = new FileInfo(path);
  335. if (!fileInfo.Exists || fileInfo.Length != stream.Length)
  336. {
  337. Directory.CreateDirectory(systemProfilesPath);
  338. using (var fileStream = _fileSystem.GetFileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
  339. {
  340. stream.CopyTo(fileStream);
  341. }
  342. }
  343. }
  344. }
  345. // Not necessary, but just to make it easy to find
  346. Directory.CreateDirectory(UserProfilesPath);
  347. }
  348. public void DeleteProfile(string id)
  349. {
  350. var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id));
  351. if (info.Info.Type == DeviceProfileType.System)
  352. {
  353. throw new ArgumentException("System profiles cannot be deleted.");
  354. }
  355. File.Delete(info.Path);
  356. }
  357. public void CreateProfile(DeviceProfile profile)
  358. {
  359. profile = ReserializeProfile(profile);
  360. if (string.IsNullOrWhiteSpace(profile.Name))
  361. {
  362. throw new ArgumentException("Profile is missing Name");
  363. }
  364. var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
  365. var path = Path.Combine(UserProfilesPath, newFilename);
  366. _xmlSerializer.SerializeToFile(profile, path);
  367. }
  368. public void UpdateProfile(DeviceProfile profile)
  369. {
  370. profile = ReserializeProfile(profile);
  371. if (string.IsNullOrWhiteSpace(profile.Id))
  372. {
  373. throw new ArgumentException("Profile is missing Id");
  374. }
  375. if (string.IsNullOrWhiteSpace(profile.Name))
  376. {
  377. throw new ArgumentException("Profile is missing Name");
  378. }
  379. var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase));
  380. if (current.Info.Type == DeviceProfileType.System)
  381. {
  382. throw new ArgumentException("System profiles are readonly");
  383. }
  384. var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
  385. var path = Path.Combine(UserProfilesPath, newFilename);
  386. if (!string.Equals(path, current.Path, StringComparison.Ordinal))
  387. {
  388. File.Delete(current.Path);
  389. }
  390. _xmlSerializer.SerializeToFile(profile, path);
  391. }
  392. /// <summary>
  393. /// Recreates the object using serialization, to ensure it's not a subclass.
  394. /// If it's a subclass it may not serlialize properly to xml (different root element tag name)
  395. /// </summary>
  396. /// <param name="profile"></param>
  397. /// <returns></returns>
  398. private DeviceProfile ReserializeProfile(DeviceProfile profile)
  399. {
  400. if (profile.GetType() == typeof(DeviceProfile))
  401. {
  402. return profile;
  403. }
  404. var json = _jsonSerializer.SerializeToString(profile);
  405. return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
  406. }
  407. class InternalProfileInfo
  408. {
  409. internal DeviceProfileInfo Info { get; set; }
  410. internal string Path { get; set; }
  411. }
  412. public string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId)
  413. {
  414. var profile = GetProfile(headers) ??
  415. GetDefaultProfile();
  416. return new DescriptionXmlBuilder(profile, serverUuId).GetXml();
  417. }
  418. public string GetContentDirectoryXml(IDictionary<string, string> headers)
  419. {
  420. var profile = GetProfile(headers) ??
  421. GetDefaultProfile();
  422. return new ContentDirectoryXmlBuilder(profile).GetXml();
  423. }
  424. public ControlResponse ProcessControlRequest(ControlRequest request)
  425. {
  426. var profile = GetProfile(request.Headers)
  427. ?? GetDefaultProfile();
  428. var device = DlnaServerEntryPoint.Instance.GetServerUpnpDevice(request.TargetServerUuId);
  429. var serverAddress = device.Descriptor.ToString().Substring(0, device.Descriptor.ToString().IndexOf("/dlna", StringComparison.OrdinalIgnoreCase));
  430. return new ControlHandler(_logger, _userManager, _libraryManager, profile, serverAddress, _dtoService, _imageProcessor, _userDataManager)
  431. .ProcessControlRequest(request);
  432. }
  433. public DlnaIconResponse GetIcon(string filename)
  434. {
  435. var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
  436. ? ImageFormat.Png
  437. : ImageFormat.Jpg;
  438. return new DlnaIconResponse
  439. {
  440. Format = format,
  441. Stream = GetType().Assembly.GetManifestResourceStream("MediaBrowser.Dlna.Images." + filename.ToLower())
  442. };
  443. }
  444. }
  445. }