DlnaManager.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. #pragma warning disable CS1591
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Globalization;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Reflection;
  8. using System.Text.Json;
  9. using System.Text.RegularExpressions;
  10. using System.Threading.Tasks;
  11. using Emby.Dlna.Profiles;
  12. using Emby.Dlna.Server;
  13. using Jellyfin.Extensions.Json;
  14. using MediaBrowser.Common.Configuration;
  15. using MediaBrowser.Common.Extensions;
  16. using MediaBrowser.Controller;
  17. using MediaBrowser.Controller.Dlna;
  18. using MediaBrowser.Controller.Drawing;
  19. using MediaBrowser.Model.Dlna;
  20. using MediaBrowser.Model.Drawing;
  21. using MediaBrowser.Model.IO;
  22. using MediaBrowser.Model.Serialization;
  23. using Microsoft.AspNetCore.Http;
  24. using Microsoft.Extensions.Logging;
  25. using Microsoft.Extensions.Primitives;
  26. namespace Emby.Dlna
  27. {
  28. public class DlnaManager : IDlnaManager
  29. {
  30. private readonly IApplicationPaths _appPaths;
  31. private readonly IXmlSerializer _xmlSerializer;
  32. private readonly IFileSystem _fileSystem;
  33. private readonly ILogger<DlnaManager> _logger;
  34. private readonly IServerApplicationHost _appHost;
  35. private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
  36. private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
  37. private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
  38. public DlnaManager(
  39. IXmlSerializer xmlSerializer,
  40. IFileSystem fileSystem,
  41. IApplicationPaths appPaths,
  42. ILoggerFactory loggerFactory,
  43. IServerApplicationHost appHost)
  44. {
  45. _xmlSerializer = xmlSerializer;
  46. _fileSystem = fileSystem;
  47. _appPaths = appPaths;
  48. _logger = loggerFactory.CreateLogger<DlnaManager>();
  49. _appHost = appHost;
  50. }
  51. private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
  52. private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
  53. public async Task InitProfilesAsync()
  54. {
  55. try
  56. {
  57. await ExtractSystemProfilesAsync().ConfigureAwait(false);
  58. Directory.CreateDirectory(UserProfilesPath);
  59. LoadProfiles();
  60. }
  61. catch (Exception ex)
  62. {
  63. _logger.LogError(ex, "Error extracting DLNA profiles.");
  64. }
  65. }
  66. private void LoadProfiles()
  67. {
  68. var list = GetProfiles(UserProfilesPath, DeviceProfileType.User)
  69. .OrderBy(i => i.Name)
  70. .ToList();
  71. list.AddRange(GetProfiles(SystemProfilesPath, DeviceProfileType.System)
  72. .OrderBy(i => i.Name));
  73. }
  74. public IEnumerable<DeviceProfile> GetProfiles()
  75. {
  76. lock (_profiles)
  77. {
  78. return _profiles.Values
  79. .OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
  80. .ThenBy(i => i.Item1.Info.Name)
  81. .Select(i => i.Item2)
  82. .ToList();
  83. }
  84. }
  85. /// <inheritdoc />
  86. public DeviceProfile GetDefaultProfile()
  87. {
  88. return new DefaultProfile();
  89. }
  90. /// <inheritdoc />
  91. public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
  92. {
  93. ArgumentNullException.ThrowIfNull(deviceInfo);
  94. var profile = GetProfiles()
  95. .FirstOrDefault(i => i.Identification is not null && IsMatch(deviceInfo, i.Identification));
  96. if (profile is null)
  97. {
  98. _logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
  99. }
  100. else
  101. {
  102. _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
  103. }
  104. return profile;
  105. }
  106. /// <summary>
  107. /// Attempts to match a device with a profile.
  108. /// Rules:
  109. /// - If the profile field has no value, the field matches regardless of its contents.
  110. /// - the profile field can be an exact match, or a reg exp.
  111. /// </summary>
  112. /// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
  113. /// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
  114. /// <returns><b>True</b> if they match.</returns>
  115. public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
  116. {
  117. return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
  118. && IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
  119. && IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
  120. && IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
  121. && IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
  122. && IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
  123. && IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
  124. && IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
  125. }
  126. private bool IsRegexOrSubstringMatch(string input, string pattern)
  127. {
  128. if (string.IsNullOrEmpty(pattern))
  129. {
  130. // In profile identification: An empty pattern matches anything.
  131. return true;
  132. }
  133. if (string.IsNullOrEmpty(input))
  134. {
  135. // The profile contains a value, and the device doesn't.
  136. return false;
  137. }
  138. try
  139. {
  140. return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
  141. || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
  142. }
  143. catch (ArgumentException ex)
  144. {
  145. _logger.LogError(ex, "Error evaluating regex pattern {Pattern}", pattern);
  146. return false;
  147. }
  148. }
  149. /// <inheritdoc />
  150. public DeviceProfile? GetProfile(IHeaderDictionary headers)
  151. {
  152. ArgumentNullException.ThrowIfNull(headers);
  153. var profile = GetProfiles().FirstOrDefault(i => i.Identification is not null && IsMatch(headers, i.Identification));
  154. if (profile is null)
  155. {
  156. _logger.LogDebug("No matching device profile found. {@Headers}", headers);
  157. }
  158. else
  159. {
  160. _logger.LogDebug("Found matching device profile: {0}", profile.Name);
  161. }
  162. return profile;
  163. }
  164. private bool IsMatch(IHeaderDictionary headers, DeviceIdentification profileInfo)
  165. {
  166. return profileInfo.Headers.Any(i => IsMatch(headers, i));
  167. }
  168. private bool IsMatch(IHeaderDictionary headers, HttpHeaderInfo header)
  169. {
  170. // Handle invalid user setup
  171. if (string.IsNullOrEmpty(header.Name))
  172. {
  173. return false;
  174. }
  175. if (headers.TryGetValue(header.Name, out StringValues value))
  176. {
  177. switch (header.Match)
  178. {
  179. case HeaderMatchType.Equals:
  180. return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
  181. case HeaderMatchType.Substring:
  182. var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
  183. // _logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
  184. return isMatch;
  185. case HeaderMatchType.Regex:
  186. return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase);
  187. default:
  188. throw new ArgumentException("Unrecognized HeaderMatchType");
  189. }
  190. }
  191. return false;
  192. }
  193. private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
  194. {
  195. try
  196. {
  197. return _fileSystem.GetFilePaths(path)
  198. .Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
  199. .Select(i => ParseProfileFile(i, type))
  200. .Where(i => i is not null)
  201. .ToList()!; // We just filtered out all the nulls
  202. }
  203. catch (IOException)
  204. {
  205. return Array.Empty<DeviceProfile>();
  206. }
  207. }
  208. private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
  209. {
  210. lock (_profiles)
  211. {
  212. if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
  213. {
  214. return profileTuple.Item2;
  215. }
  216. try
  217. {
  218. var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
  219. var profile = ReserializeProfile(tempProfile);
  220. profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
  221. _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
  222. return profile;
  223. }
  224. catch (Exception ex)
  225. {
  226. _logger.LogError(ex, "Error parsing profile file: {Path}", path);
  227. return null;
  228. }
  229. }
  230. }
  231. /// <inheritdoc />
  232. public DeviceProfile? GetProfile(string id)
  233. {
  234. if (string.IsNullOrEmpty(id))
  235. {
  236. throw new ArgumentNullException(nameof(id));
  237. }
  238. var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
  239. if (info is null)
  240. {
  241. return null;
  242. }
  243. return ParseProfileFile(info.Path, info.Info.Type);
  244. }
  245. private IEnumerable<InternalProfileInfo> GetProfileInfosInternal()
  246. {
  247. lock (_profiles)
  248. {
  249. return _profiles.Values
  250. .Select(i => i.Item1)
  251. .OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
  252. .ThenBy(i => i.Info.Name);
  253. }
  254. }
  255. /// <inheritdoc />
  256. public IEnumerable<DeviceProfileInfo> GetProfileInfos()
  257. {
  258. return GetProfileInfosInternal().Select(i => i.Info);
  259. }
  260. private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
  261. {
  262. return new InternalProfileInfo(
  263. new DeviceProfileInfo
  264. {
  265. Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
  266. Name = _fileSystem.GetFileNameWithoutExtension(file),
  267. Type = type
  268. },
  269. file.FullName);
  270. }
  271. private async Task ExtractSystemProfilesAsync()
  272. {
  273. var namespaceName = GetType().Namespace + ".Profiles.Xml.";
  274. var systemProfilesPath = SystemProfilesPath;
  275. foreach (var name in _assembly.GetManifestResourceNames())
  276. {
  277. if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
  278. {
  279. continue;
  280. }
  281. var path = Path.Join(
  282. systemProfilesPath,
  283. Path.GetFileName(name.AsSpan())[namespaceName.Length..]);
  284. if (File.Exists(path))
  285. {
  286. continue;
  287. }
  288. // The stream should exist as we just got its name from GetManifestResourceNames
  289. using (var stream = _assembly.GetManifestResourceStream(name)!)
  290. {
  291. Directory.CreateDirectory(systemProfilesPath);
  292. var fileOptions = AsyncFile.WriteOptions;
  293. fileOptions.Mode = FileMode.CreateNew;
  294. fileOptions.PreallocationSize = stream.Length;
  295. var fileStream = new FileStream(path, fileOptions);
  296. await using (fileStream.ConfigureAwait(false))
  297. {
  298. await stream.CopyToAsync(fileStream).ConfigureAwait(false);
  299. }
  300. }
  301. }
  302. }
  303. /// <inheritdoc />
  304. public void DeleteProfile(string id)
  305. {
  306. var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
  307. if (info.Info.Type == DeviceProfileType.System)
  308. {
  309. throw new ArgumentException("System profiles cannot be deleted.");
  310. }
  311. _fileSystem.DeleteFile(info.Path);
  312. lock (_profiles)
  313. {
  314. _profiles.Remove(info.Path);
  315. }
  316. }
  317. /// <inheritdoc />
  318. public void CreateProfile(DeviceProfile profile)
  319. {
  320. profile = ReserializeProfile(profile);
  321. if (string.IsNullOrEmpty(profile.Name))
  322. {
  323. throw new ArgumentException("Profile is missing Name");
  324. }
  325. var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
  326. var path = Path.Combine(UserProfilesPath, newFilename);
  327. SaveProfile(profile, path, DeviceProfileType.User);
  328. }
  329. /// <inheritdoc />
  330. public void UpdateProfile(string profileId, DeviceProfile profile)
  331. {
  332. profile = ReserializeProfile(profile);
  333. if (string.IsNullOrEmpty(profile.Id))
  334. {
  335. throw new ArgumentException("Profile is missing Id");
  336. }
  337. if (string.IsNullOrEmpty(profile.Name))
  338. {
  339. throw new ArgumentException("Profile is missing Name");
  340. }
  341. var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
  342. if (current.Info.Type == DeviceProfileType.System)
  343. {
  344. throw new ArgumentException("System profiles can't be edited");
  345. }
  346. var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
  347. var path = Path.Join(UserProfilesPath, newFilename);
  348. if (!string.Equals(path, current.Path, StringComparison.Ordinal))
  349. {
  350. lock (_profiles)
  351. {
  352. _profiles.Remove(current.Path);
  353. }
  354. }
  355. SaveProfile(profile, path, DeviceProfileType.User);
  356. }
  357. private void SaveProfile(DeviceProfile profile, string path, DeviceProfileType type)
  358. {
  359. lock (_profiles)
  360. {
  361. _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
  362. }
  363. SerializeToXml(profile, path);
  364. }
  365. internal void SerializeToXml(DeviceProfile profile, string path)
  366. {
  367. _xmlSerializer.SerializeToFile(profile, path);
  368. }
  369. /// <summary>
  370. /// Recreates the object using serialization, to ensure it's not a subclass.
  371. /// If it's a subclass it may not serialize properly to xml (different root element tag name).
  372. /// </summary>
  373. /// <param name="profile">The device profile.</param>
  374. /// <returns>The re-serialized device profile.</returns>
  375. private DeviceProfile ReserializeProfile(DeviceProfile profile)
  376. {
  377. if (profile.GetType() == typeof(DeviceProfile))
  378. {
  379. return profile;
  380. }
  381. var json = JsonSerializer.Serialize(profile, _jsonOptions);
  382. // Output can't be null if the input isn't null
  383. return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
  384. }
  385. /// <inheritdoc />
  386. public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
  387. {
  388. var profile = GetProfile(headers) ?? GetDefaultProfile();
  389. var serverId = _appHost.SystemId;
  390. return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
  391. }
  392. /// <inheritdoc />
  393. public ImageStream? GetIcon(string filename)
  394. {
  395. var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
  396. ? ImageFormat.Png
  397. : ImageFormat.Jpg;
  398. var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
  399. var stream = _assembly.GetManifestResourceStream(resource);
  400. if (stream is null)
  401. {
  402. return null;
  403. }
  404. return new ImageStream(stream)
  405. {
  406. Format = format
  407. };
  408. }
  409. private class InternalProfileInfo
  410. {
  411. internal InternalProfileInfo(DeviceProfileInfo info, string path)
  412. {
  413. Info = info;
  414. Path = path;
  415. }
  416. internal DeviceProfileInfo Info { get; }
  417. internal string Path { get; }
  418. }
  419. }
  420. }