123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497 |
- #pragma warning disable CS1591
- using System;
- using System.Collections.Generic;
- using System.Globalization;
- using System.IO;
- using System.Linq;
- using System.Reflection;
- using System.Text.Json;
- using System.Text.RegularExpressions;
- using System.Threading.Tasks;
- using Emby.Dlna.Profiles;
- using Emby.Dlna.Server;
- using Jellyfin.Extensions.Json;
- using MediaBrowser.Common.Configuration;
- using MediaBrowser.Common.Extensions;
- using MediaBrowser.Controller;
- using MediaBrowser.Controller.Dlna;
- using MediaBrowser.Controller.Drawing;
- using MediaBrowser.Model.Dlna;
- using MediaBrowser.Model.Drawing;
- using MediaBrowser.Model.IO;
- using MediaBrowser.Model.Serialization;
- using Microsoft.AspNetCore.Http;
- using Microsoft.Extensions.Logging;
- using Microsoft.Extensions.Primitives;
- namespace Emby.Dlna
- {
- public class DlnaManager : IDlnaManager
- {
- private readonly IApplicationPaths _appPaths;
- private readonly IXmlSerializer _xmlSerializer;
- private readonly IFileSystem _fileSystem;
- private readonly ILogger<DlnaManager> _logger;
- private readonly IServerApplicationHost _appHost;
- private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
- private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
- public DlnaManager(
- IXmlSerializer xmlSerializer,
- IFileSystem fileSystem,
- IApplicationPaths appPaths,
- ILoggerFactory loggerFactory,
- IServerApplicationHost appHost)
- {
- _xmlSerializer = xmlSerializer;
- _fileSystem = fileSystem;
- _appPaths = appPaths;
- _logger = loggerFactory.CreateLogger<DlnaManager>();
- _appHost = appHost;
- }
- private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
- private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
- public async Task InitProfilesAsync()
- {
- try
- {
- await ExtractSystemProfilesAsync().ConfigureAwait(false);
- Directory.CreateDirectory(UserProfilesPath);
- LoadProfiles();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error extracting DLNA profiles.");
- }
- }
- private void LoadProfiles()
- {
- var list = GetProfiles(UserProfilesPath, DeviceProfileType.User)
- .OrderBy(i => i.Name)
- .ToList();
- list.AddRange(GetProfiles(SystemProfilesPath, DeviceProfileType.System)
- .OrderBy(i => i.Name));
- }
- public IEnumerable<DeviceProfile> GetProfiles()
- {
- lock (_profiles)
- {
- return _profiles.Values
- .OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
- .ThenBy(i => i.Item1.Info.Name)
- .Select(i => i.Item2)
- .ToList();
- }
- }
- /// <inheritdoc />
- public DeviceProfile GetDefaultProfile()
- {
- return new DefaultProfile();
- }
- /// <inheritdoc />
- public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
- {
- ArgumentNullException.ThrowIfNull(deviceInfo);
- var profile = GetProfiles()
- .FirstOrDefault(i => i.Identification is not null && IsMatch(deviceInfo, i.Identification));
- if (profile is null)
- {
- _logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
- }
- else
- {
- _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
- }
- return profile;
- }
- /// <summary>
- /// Attempts to match a device with a profile.
- /// Rules:
- /// - If the profile field has no value, the field matches regardless of its contents.
- /// - the profile field can be an exact match, or a reg exp.
- /// </summary>
- /// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
- /// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
- /// <returns><b>True</b> if they match.</returns>
- public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
- {
- return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
- && IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
- && IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
- && IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
- && IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
- && IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
- && IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
- && IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
- }
- private bool IsRegexOrSubstringMatch(string input, string pattern)
- {
- if (string.IsNullOrEmpty(pattern))
- {
- // In profile identification: An empty pattern matches anything.
- return true;
- }
- if (string.IsNullOrEmpty(input))
- {
- // The profile contains a value, and the device doesn't.
- return false;
- }
- try
- {
- return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
- || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
- }
- catch (ArgumentException ex)
- {
- _logger.LogError(ex, "Error evaluating regex pattern {Pattern}", pattern);
- return false;
- }
- }
- /// <inheritdoc />
- public DeviceProfile? GetProfile(IHeaderDictionary headers)
- {
- ArgumentNullException.ThrowIfNull(headers);
- var profile = GetProfiles().FirstOrDefault(i => i.Identification is not null && IsMatch(headers, i.Identification));
- if (profile is null)
- {
- _logger.LogDebug("No matching device profile found. {@Headers}", headers);
- }
- else
- {
- _logger.LogDebug("Found matching device profile: {0}", profile.Name);
- }
- return profile;
- }
- private bool IsMatch(IHeaderDictionary headers, DeviceIdentification profileInfo)
- {
- return profileInfo.Headers.Any(i => IsMatch(headers, i));
- }
- private bool IsMatch(IHeaderDictionary headers, HttpHeaderInfo header)
- {
- // Handle invalid user setup
- if (string.IsNullOrEmpty(header.Name))
- {
- return false;
- }
- if (headers.TryGetValue(header.Name, out StringValues value))
- {
- switch (header.Match)
- {
- case HeaderMatchType.Equals:
- return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
- case HeaderMatchType.Substring:
- var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
- // _logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
- return isMatch;
- case HeaderMatchType.Regex:
- return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase);
- default:
- throw new ArgumentException("Unrecognized HeaderMatchType");
- }
- }
- return false;
- }
- private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
- {
- try
- {
- return _fileSystem.GetFilePaths(path)
- .Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
- .Select(i => ParseProfileFile(i, type))
- .Where(i => i is not null)
- .ToList()!; // We just filtered out all the nulls
- }
- catch (IOException)
- {
- return Array.Empty<DeviceProfile>();
- }
- }
- private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
- {
- lock (_profiles)
- {
- if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
- {
- return profileTuple.Item2;
- }
- try
- {
- var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
- var profile = ReserializeProfile(tempProfile);
- profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
- _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
- return profile;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error parsing profile file: {Path}", path);
- return null;
- }
- }
- }
- /// <inheritdoc />
- public DeviceProfile? GetProfile(string id)
- {
- if (string.IsNullOrEmpty(id))
- {
- throw new ArgumentNullException(nameof(id));
- }
- var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
- if (info is null)
- {
- return null;
- }
- return ParseProfileFile(info.Path, info.Info.Type);
- }
- private IEnumerable<InternalProfileInfo> GetProfileInfosInternal()
- {
- lock (_profiles)
- {
- return _profiles.Values
- .Select(i => i.Item1)
- .OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
- .ThenBy(i => i.Info.Name);
- }
- }
- /// <inheritdoc />
- public IEnumerable<DeviceProfileInfo> GetProfileInfos()
- {
- return GetProfileInfosInternal().Select(i => i.Info);
- }
- private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
- {
- return new InternalProfileInfo(
- new DeviceProfileInfo
- {
- Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
- Name = _fileSystem.GetFileNameWithoutExtension(file),
- Type = type
- },
- file.FullName);
- }
- private async Task ExtractSystemProfilesAsync()
- {
- var namespaceName = GetType().Namespace + ".Profiles.Xml.";
- var systemProfilesPath = SystemProfilesPath;
- foreach (var name in _assembly.GetManifestResourceNames())
- {
- if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
- {
- continue;
- }
- var path = Path.Join(
- systemProfilesPath,
- Path.GetFileName(name.AsSpan())[namespaceName.Length..]);
- if (File.Exists(path))
- {
- continue;
- }
- // The stream should exist as we just got its name from GetManifestResourceNames
- using (var stream = _assembly.GetManifestResourceStream(name)!)
- {
- Directory.CreateDirectory(systemProfilesPath);
- var fileOptions = AsyncFile.WriteOptions;
- fileOptions.Mode = FileMode.CreateNew;
- fileOptions.PreallocationSize = stream.Length;
- var fileStream = new FileStream(path, fileOptions);
- await using (fileStream.ConfigureAwait(false))
- {
- await stream.CopyToAsync(fileStream).ConfigureAwait(false);
- }
- }
- }
- }
- /// <inheritdoc />
- public void DeleteProfile(string id)
- {
- var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
- if (info.Info.Type == DeviceProfileType.System)
- {
- throw new ArgumentException("System profiles cannot be deleted.");
- }
- _fileSystem.DeleteFile(info.Path);
- lock (_profiles)
- {
- _profiles.Remove(info.Path);
- }
- }
- /// <inheritdoc />
- public void CreateProfile(DeviceProfile profile)
- {
- profile = ReserializeProfile(profile);
- if (string.IsNullOrEmpty(profile.Name))
- {
- throw new ArgumentException("Profile is missing Name");
- }
- var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
- var path = Path.Combine(UserProfilesPath, newFilename);
- SaveProfile(profile, path, DeviceProfileType.User);
- }
- /// <inheritdoc />
- public void UpdateProfile(string profileId, DeviceProfile profile)
- {
- profile = ReserializeProfile(profile);
- if (string.IsNullOrEmpty(profile.Id))
- {
- throw new ArgumentException("Profile is missing Id");
- }
- if (string.IsNullOrEmpty(profile.Name))
- {
- throw new ArgumentException("Profile is missing Name");
- }
- var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
- if (current.Info.Type == DeviceProfileType.System)
- {
- throw new ArgumentException("System profiles can't be edited");
- }
- var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
- var path = Path.Join(UserProfilesPath, newFilename);
- if (!string.Equals(path, current.Path, StringComparison.Ordinal))
- {
- lock (_profiles)
- {
- _profiles.Remove(current.Path);
- }
- }
- SaveProfile(profile, path, DeviceProfileType.User);
- }
- private void SaveProfile(DeviceProfile profile, string path, DeviceProfileType type)
- {
- lock (_profiles)
- {
- _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
- }
- SerializeToXml(profile, path);
- }
- internal void SerializeToXml(DeviceProfile profile, string path)
- {
- _xmlSerializer.SerializeToFile(profile, path);
- }
- /// <summary>
- /// Recreates the object using serialization, to ensure it's not a subclass.
- /// If it's a subclass it may not serialize properly to xml (different root element tag name).
- /// </summary>
- /// <param name="profile">The device profile.</param>
- /// <returns>The re-serialized device profile.</returns>
- private DeviceProfile ReserializeProfile(DeviceProfile profile)
- {
- if (profile.GetType() == typeof(DeviceProfile))
- {
- return profile;
- }
- var json = JsonSerializer.Serialize(profile, _jsonOptions);
- // Output can't be null if the input isn't null
- return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
- }
- /// <inheritdoc />
- public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
- {
- var profile = GetProfile(headers) ?? GetDefaultProfile();
- var serverId = _appHost.SystemId;
- return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
- }
- /// <inheritdoc />
- public ImageStream? GetIcon(string filename)
- {
- var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
- ? ImageFormat.Png
- : ImageFormat.Jpg;
- var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
- var stream = _assembly.GetManifestResourceStream(resource);
- if (stream is null)
- {
- return null;
- }
- return new ImageStream(stream)
- {
- Format = format
- };
- }
- private class InternalProfileInfo
- {
- internal InternalProfileInfo(DeviceProfileInfo info, string path)
- {
- Info = info;
- Path = path;
- }
- internal DeviceProfileInfo Info { get; }
- internal string Path { get; }
- }
- }
- }
|