jellyfin/Emby.Dlna/DlnaManager.cs

492 lines
18 KiB
C#
Raw Normal View History

#pragma warning disable CS1591
2022-03-18 19:33:32 -04:00
using System;
2019-01-13 14:16:19 -05:00
using System.Collections.Generic;
using System.Globalization;
2019-01-13 14:16:19 -05:00
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
2019-01-13 14:16:19 -05:00
using System.Text.RegularExpressions;
2019-01-27 09:40:37 -05:00
using System.Threading.Tasks;
2019-01-13 14:16:19 -05:00
using Emby.Dlna.Profiles;
using Emby.Dlna.Server;
using Jellyfin.Extensions.Json;
2019-01-13 14:16:19 -05:00
using MediaBrowser.Common.Configuration;
2016-10-29 18:22:20 -04:00
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;
2019-01-13 14:16:19 -05:00
using MediaBrowser.Model.Serialization;
using Microsoft.AspNetCore.Http;
2019-01-13 14:16:19 -05:00
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
2016-10-29 18:22:20 -04:00
2016-10-29 18:34:54 -04:00
namespace Emby.Dlna
2016-10-29 18:22:20 -04:00
{
public class DlnaManager : IDlnaManager
{
private readonly IApplicationPaths _appPaths;
private readonly IXmlSerializer _xmlSerializer;
private readonly IFileSystem _fileSystem;
2020-06-05 20:15:56 -04:00
private readonly ILogger<DlnaManager> _logger;
2016-10-29 18:22:20 -04:00
private readonly IServerApplicationHost _appHost;
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
2021-03-08 23:57:38 -05:00
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
2016-10-29 18:22:20 -04:00
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
public DlnaManager(
IXmlSerializer xmlSerializer,
2016-10-29 18:22:20 -04:00
IFileSystem fileSystem,
IApplicationPaths appPaths,
ILoggerFactory loggerFactory,
IServerApplicationHost appHost)
2016-10-29 18:22:20 -04:00
{
_xmlSerializer = xmlSerializer;
_fileSystem = fileSystem;
_appPaths = appPaths;
2020-06-05 20:15:56 -04:00
_logger = loggerFactory.CreateLogger<DlnaManager>();
2016-10-29 18:22:20 -04:00
_appHost = appHost;
}
2020-08-07 13:26:28 -04:00
private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
2019-01-27 09:40:37 -05:00
public async Task InitProfilesAsync()
2016-10-29 18:22:20 -04:00
{
try
{
2020-08-07 13:26:28 -04:00
await ExtractSystemProfilesAsync().ConfigureAwait(false);
2022-03-18 19:33:32 -04:00
Directory.CreateDirectory(UserProfilesPath);
2016-10-29 18:22:20 -04:00
LoadProfiles();
}
catch (Exception ex)
{
2018-12-20 07:11:26 -05:00
_logger.LogError(ex, "Error extracting DLNA profiles.");
2016-10-29 18:22:20 -04:00
}
}
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
2016-10-29 18:22:20 -04:00
.OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
.ThenBy(i => i.Item1.Info.Name)
.Select(i => i.Item2)
.ToList();
}
}
2021-08-03 11:54:55 -04:00
/// <inheritdoc />
2016-10-29 18:22:20 -04:00
public DeviceProfile GetDefaultProfile()
{
return new DefaultProfile();
}
2021-08-03 11:54:55 -04:00
/// <inheritdoc />
public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
2016-10-29 18:22:20 -04:00
{
ArgumentNullException.ThrowIfNull(deviceInfo);
2016-10-29 18:22:20 -04:00
2021-04-20 13:08:19 -04:00
var profile = GetProfiles()
2022-12-05 09:01:13 -05:00
.FirstOrDefault(i => i.Identification is not null && IsMatch(deviceInfo, i.Identification));
2021-04-20 13:08:19 -04:00
2022-12-05 09:00:20 -05:00
if (profile is null)
2016-10-29 18:22:20 -04:00
{
2021-11-09 16:29:33 -05:00
_logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
2016-10-29 18:22:20 -04:00
}
2021-04-20 13:08:19 -04:00
else
2016-10-29 18:22:20 -04:00
{
2021-08-03 11:54:55 -04:00
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
2016-10-29 18:22:20 -04:00
}
2021-04-20 13:08:19 -04:00
return profile;
2016-10-29 18:22:20 -04:00
}
2021-04-19 09:07:14 -04:00
/// <summary>
/// Attempts to match a device with a profile.
/// Rules:
/// - If the profile field has no value, the field matches regardless of its contents.
2021-04-19 09:07:14 -04:00
/// - 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>
2021-04-20 13:04:16 -04:00
public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
2016-10-29 18:22:20 -04:00
{
2021-04-19 09:24:58 -04:00
return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
2021-04-21 05:18:29 -04:00
&& 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);
2016-10-29 18:22:20 -04:00
}
2021-04-20 13:04:16 -04:00
private bool IsRegexOrSubstringMatch(string input, string pattern)
2016-10-29 18:22:20 -04:00
{
2021-04-19 09:07:14 -04:00
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;
}
2021-04-20 13:14:23 -04:00
try
{
return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
2021-04-20 13:17:48 -04:00
|| Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
2021-04-20 13:14:23 -04:00
}
catch (ArgumentException ex)
{
_logger.LogError(ex, "Error evaluating regex pattern {Pattern}", pattern);
return false;
}
2016-10-29 18:22:20 -04:00
}
2021-08-03 11:54:55 -04:00
/// <inheritdoc />
public DeviceProfile? GetProfile(IHeaderDictionary headers)
2016-10-29 18:22:20 -04:00
{
ArgumentNullException.ThrowIfNull(headers);
2016-10-29 18:22:20 -04:00
2022-12-05 09:01:13 -05:00
var profile = GetProfiles().FirstOrDefault(i => i.Identification is not null && IsMatch(headers, i.Identification));
2022-12-05 09:00:20 -05:00
if (profile is null)
2016-10-29 18:22:20 -04:00
{
2021-08-03 11:54:55 -04:00
_logger.LogDebug("No matching device profile found. {@Headers}", headers);
2016-10-29 18:22:20 -04:00
}
else
{
2021-08-03 11:54:55 -04:00
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
2016-10-29 18:22:20 -04:00
}
return profile;
}
private bool IsMatch(IHeaderDictionary headers, DeviceIdentification profileInfo)
2016-10-29 18:22:20 -04:00
{
return profileInfo.Headers.Any(i => IsMatch(headers, i));
}
private bool IsMatch(IHeaderDictionary headers, HttpHeaderInfo header)
2016-10-29 18:22:20 -04:00
{
2016-11-20 14:40:35 -05:00
// Handle invalid user setup
2018-09-12 13:26:21 -04:00
if (string.IsNullOrEmpty(header.Name))
2016-11-20 14:40:35 -05:00
{
return false;
}
if (headers.TryGetValue(header.Name, out StringValues value))
2016-10-29 18:22:20 -04:00
{
2022-11-08 15:13:57 -05:00
if (StringValues.IsNullOrEmpty(value))
{
return false;
}
2016-10-29 18:22:20 -04:00
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;
2020-06-14 05:11:11 -04:00
// _logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
2016-10-29 18:22:20 -04:00
return isMatch;
case HeaderMatchType.Regex:
2022-11-08 15:13:57 -05:00
// Can't be null, we checked above the switch statement
return Regex.IsMatch(value!, header.Value, RegexOptions.IgnoreCase);
2016-10-29 18:22:20 -04:00
default:
throw new ArgumentException("Unrecognized HeaderMatchType");
}
}
return false;
}
private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
{
try
{
return _fileSystem.GetFilePaths(path)
.Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
2017-03-29 02:26:48 -04:00
.Select(i => ParseProfileFile(i, type))
2022-12-05 09:01:13 -05:00
.Where(i => i is not null)
2021-08-03 11:54:55 -04:00
.ToList()!; // We just filtered out all the nulls
2016-10-29 18:22:20 -04:00
}
2016-11-04 04:31:05 -04:00
catch (IOException)
2016-10-29 18:22:20 -04:00
{
2021-08-03 11:54:55 -04:00
return Array.Empty<DeviceProfile>();
2016-10-29 18:22:20 -04:00
}
}
2021-08-03 11:54:55 -04:00
private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
2016-10-29 18:22:20 -04:00
{
lock (_profiles)
{
2021-08-03 11:54:55 -04:00
if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
2016-10-29 18:22:20 -04:00
{
return profileTuple.Item2;
}
try
{
2017-03-29 02:26:48 -04:00
var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
var profile = ReserializeProfile(tempProfile);
2016-10-29 18:22:20 -04:00
profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
2016-10-29 18:22:20 -04:00
_profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
return profile;
}
catch (Exception ex)
{
2018-12-20 07:39:58 -05:00
_logger.LogError(ex, "Error parsing profile file: {Path}", path);
2016-10-29 18:22:20 -04:00
return null;
}
}
}
2021-08-03 11:54:55 -04:00
/// <inheritdoc />
public DeviceProfile? GetProfile(string id)
2016-10-29 18:22:20 -04:00
{
ArgumentException.ThrowIfNullOrEmpty(id);
2016-10-29 18:22:20 -04:00
2021-03-12 17:20:13 -05:00
var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
2022-12-05 09:00:20 -05:00
if (info is null)
2021-03-12 17:20:13 -05:00
{
return null;
}
2016-10-29 18:22:20 -04:00
return ParseProfileFile(info.Path, info.Info.Type);
}
private IEnumerable<InternalProfileInfo> GetProfileInfosInternal()
{
lock (_profiles)
{
return _profiles.Values
2016-10-29 18:22:20 -04:00
.Select(i => i.Item1)
.OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
.ThenBy(i => i.Info.Name);
}
}
2021-08-03 11:54:55 -04:00
/// <inheritdoc />
2016-10-29 18:22:20 -04:00
public IEnumerable<DeviceProfileInfo> GetProfileInfos()
{
return GetProfileInfosInternal().Select(i => i.Info);
}
private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
{
2021-08-03 11:54:55 -04:00
return new InternalProfileInfo(
new DeviceProfileInfo
2016-10-29 18:22:20 -04:00
{
Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
2016-10-29 18:22:20 -04:00
Name = _fileSystem.GetFileNameWithoutExtension(file),
Type = type
2021-08-03 11:54:55 -04:00
},
file.FullName);
2016-10-29 18:22:20 -04:00
}
2019-01-27 09:40:37 -05:00
private async Task ExtractSystemProfilesAsync()
2016-10-29 18:22:20 -04:00
{
2016-12-09 02:23:09 -05:00
var namespaceName = GetType().Namespace + ".Profiles.Xml.";
2016-10-29 18:22:20 -04:00
var systemProfilesPath = SystemProfilesPath;
foreach (var name in _assembly.GetManifestResourceNames())
2016-10-29 18:22:20 -04:00
{
if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
{
continue;
}
2020-11-06 10:15:30 -05:00
var path = Path.Join(
systemProfilesPath,
2022-03-18 19:33:32 -04:00
Path.GetFileName(name.AsSpan())[namespaceName.Length..]);
if (File.Exists(path))
{
continue;
}
2016-10-29 18:22:20 -04:00
2021-08-03 11:54:55 -04:00
// The stream should exist as we just got its name from GetManifestResourceNames
using (var stream = _assembly.GetManifestResourceStream(name)!)
2016-10-29 18:22:20 -04:00
{
2022-03-18 19:33:32 -04:00
Directory.CreateDirectory(systemProfilesPath);
2016-10-29 18:22:20 -04:00
2022-03-18 19:33:32 -04:00
var fileOptions = AsyncFile.WriteOptions;
fileOptions.Mode = FileMode.CreateNew;
fileOptions.PreallocationSize = stream.Length;
var fileStream = new FileStream(path, fileOptions);
await using (fileStream.ConfigureAwait(false))
2016-10-29 18:22:20 -04:00
{
2022-03-18 19:33:32 -04:00
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
2016-10-29 18:22:20 -04:00
}
}
}
}
2021-08-03 11:54:55 -04:00
/// <inheritdoc />
2016-10-29 18:22:20 -04:00
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);
}
}
2021-08-03 11:54:55 -04:00
/// <inheritdoc />
2016-10-29 18:22:20 -04:00
public void CreateProfile(DeviceProfile profile)
{
profile = ReserializeProfile(profile);
ArgumentException.ThrowIfNullOrEmpty(profile.Name);
2016-10-29 18:22:20 -04:00
2016-12-09 02:23:09 -05:00
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
2016-10-29 18:22:20 -04:00
var path = Path.Combine(UserProfilesPath, newFilename);
SaveProfile(profile, path, DeviceProfileType.User);
}
2021-08-03 11:54:55 -04:00
/// <inheritdoc />
public void UpdateProfile(string profileId, DeviceProfile profile)
2016-10-29 18:22:20 -04:00
{
profile = ReserializeProfile(profile);
ArgumentException.ThrowIfNullOrEmpty(profile.Id);
2020-06-15 17:43:52 -04:00
ArgumentException.ThrowIfNullOrEmpty(profile.Name);
2016-10-29 18:22:20 -04:00
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
2022-03-18 19:33:32 -04:00
if (current.Info.Type == DeviceProfileType.System)
{
throw new ArgumentException("System profiles can't be edited");
}
2016-10-29 18:22:20 -04:00
2016-12-09 02:23:09 -05:00
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
2022-03-18 19:33:32 -04:00
var path = Path.Join(UserProfilesPath, newFilename);
2016-10-29 18:22:20 -04:00
2022-03-18 19:33:32 -04:00
if (!string.Equals(path, current.Path, StringComparison.Ordinal))
2016-10-29 18:22:20 -04:00
{
2022-03-18 19:33:32 -04:00
lock (_profiles)
{
_profiles.Remove(current.Path);
}
2016-10-29 18:22:20 -04:00
}
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);
}
2020-06-15 17:43:52 -04:00
2016-12-09 02:23:09 -05:00
SerializeToXml(profile, path);
2016-10-29 18:22:20 -04:00
}
2016-12-09 02:23:09 -05:00
internal void SerializeToXml(DeviceProfile profile, string path)
2016-10-29 18:22:20 -04:00
{
2016-12-09 02:23:09 -05:00
_xmlSerializer.SerializeToFile(profile, path);
2016-10-29 18:22:20 -04:00
}
/// <summary>
/// Recreates the object using serialization, to ensure it's not a subclass.
2020-11-18 08:23:45 -05:00
/// If it's a subclass it may not serialize properly to xml (different root element tag name).
2016-10-29 18:22:20 -04:00
/// </summary>
2020-08-20 15:04:57 -04:00
/// <param name="profile">The device profile.</param>
2020-11-18 08:23:45 -05:00
/// <returns>The re-serialized device profile.</returns>
2016-10-29 18:22:20 -04:00
private DeviceProfile ReserializeProfile(DeviceProfile profile)
{
if (profile.GetType() == typeof(DeviceProfile))
{
return profile;
}
var json = JsonSerializer.Serialize(profile, _jsonOptions);
2016-10-29 18:22:20 -04:00
2021-08-03 11:54:55 -04:00
// Output can't be null if the input isn't null
return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
2016-10-29 18:22:20 -04:00
}
2021-08-03 11:54:55 -04:00
/// <inheritdoc />
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
2016-10-29 18:22:20 -04:00
{
var profile = GetProfile(headers) ?? GetDefaultProfile();
2016-10-29 18:22:20 -04:00
var serverId = _appHost.SystemId;
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
}
2021-08-03 11:54:55 -04:00
/// <inheritdoc />
2021-02-14 09:11:46 -05:00
public ImageStream? GetIcon(string filename)
2016-10-29 18:22:20 -04:00
{
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
? ImageFormat.Png
: ImageFormat.Jpg;
2019-01-27 06:03:43 -05:00
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
var stream = _assembly.GetManifestResourceStream(resource);
2022-12-05 09:00:20 -05:00
if (stream is null)
{
return null;
}
2016-11-03 18:53:02 -04:00
return new ImageStream(stream)
2016-10-29 18:22:20 -04:00
{
Format = format
2016-10-29 18:22:20 -04:00
};
}
2020-08-20 15:04:57 -04:00
private class InternalProfileInfo
{
2021-08-03 11:54:55 -04:00
internal InternalProfileInfo(DeviceProfileInfo info, string path)
{
Info = info;
Path = path;
}
internal DeviceProfileInfo Info { get; }
2020-08-20 15:04:57 -04:00
2021-08-03 11:54:55 -04:00
internal string Path { get; }
2020-08-20 15:04:57 -04:00
}
2016-10-29 18:22:20 -04:00
}
2018-12-30 07:18:38 -05:00
}