diff options
Diffstat (limited to 'Emby.Dlna')
127 files changed, 4245 insertions, 3401 deletions
diff --git a/Emby.Dlna/Api/DlnaServerService.cs b/Emby.Dlna/Api/DlnaServerService.cs deleted file mode 100644 index 68bf80163..000000000 --- a/Emby.Dlna/Api/DlnaServerService.cs +++ /dev/null @@ -1,306 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Emby.Dlna.Main; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; - -namespace Emby.Dlna.Api -{ - [Route("/Dlna/{UuId}/description.xml", "GET", Summary = "Gets dlna server info")] - [Route("/Dlna/{UuId}/description", "GET", Summary = "Gets dlna server info")] - public class GetDescriptionXml - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UuId { get; set; } - } - - [Route("/Dlna/{UuId}/contentdirectory/contentdirectory.xml", "GET", Summary = "Gets dlna content directory xml")] - [Route("/Dlna/{UuId}/contentdirectory/contentdirectory", "GET", Summary = "Gets dlna content directory xml")] - public class GetContentDirectory - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UuId { get; set; } - } - - [Route("/Dlna/{UuId}/connectionmanager/connectionmanager.xml", "GET", Summary = "Gets dlna connection manager xml")] - [Route("/Dlna/{UuId}/connectionmanager/connectionmanager", "GET", Summary = "Gets dlna connection manager xml")] - public class GetConnnectionManager - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UuId { get; set; } - } - - [Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar.xml", "GET", Summary = "Gets dlna mediareceiverregistrar xml")] - [Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar", "GET", Summary = "Gets dlna mediareceiverregistrar xml")] - public class GetMediaReceiverRegistrar - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UuId { get; set; } - } - - [Route("/Dlna/{UuId}/contentdirectory/control", "POST", Summary = "Processes a control request")] - public class ProcessContentDirectoryControlRequest : IRequiresRequestStream - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UuId { get; set; } - - public Stream RequestStream { get; set; } - } - - [Route("/Dlna/{UuId}/connectionmanager/control", "POST", Summary = "Processes a control request")] - public class ProcessConnectionManagerControlRequest : IRequiresRequestStream - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UuId { get; set; } - - public Stream RequestStream { get; set; } - } - - [Route("/Dlna/{UuId}/mediareceiverregistrar/control", "POST", Summary = "Processes a control request")] - public class ProcessMediaReceiverRegistrarControlRequest : IRequiresRequestStream - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UuId { get; set; } - - public Stream RequestStream { get; set; } - } - - [Route("/Dlna/{UuId}/mediareceiverregistrar/events", "SUBSCRIBE", Summary = "Processes an event subscription request")] - [Route("/Dlna/{UuId}/mediareceiverregistrar/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")] - public class ProcessMediaReceiverRegistrarEventRequest - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")] - public string UuId { get; set; } - } - - [Route("/Dlna/{UuId}/contentdirectory/events", "SUBSCRIBE", Summary = "Processes an event subscription request")] - [Route("/Dlna/{UuId}/contentdirectory/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")] - public class ProcessContentDirectoryEventRequest - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")] - public string UuId { get; set; } - } - - [Route("/Dlna/{UuId}/connectionmanager/events", "SUBSCRIBE", Summary = "Processes an event subscription request")] - [Route("/Dlna/{UuId}/connectionmanager/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")] - public class ProcessConnectionManagerEventRequest - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")] - public string UuId { get; set; } - } - - [Route("/Dlna/{UuId}/icons/{Filename}", "GET", Summary = "Gets a server icon")] - [Route("/Dlna/icons/{Filename}", "GET", Summary = "Gets a server icon")] - public class GetIcon - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string UuId { get; set; } - - [ApiMember(Name = "Filename", Description = "The icon filename", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Filename { get; set; } - } - - public class DlnaServerService : IService, IRequiresRequest - { - private readonly IDlnaManager _dlnaManager; - - private const string XMLContentType = "text/xml; charset=UTF-8"; - - public IRequest Request { get; set; } - private IHttpResultFactory _resultFactory; - - private IContentDirectory ContentDirectory => DlnaEntryPoint.Current.ContentDirectory; - - private IConnectionManager ConnectionManager => DlnaEntryPoint.Current.ConnectionManager; - - private IMediaReceiverRegistrar MediaReceiverRegistrar => DlnaEntryPoint.Current.MediaReceiverRegistrar; - - public DlnaServerService(IDlnaManager dlnaManager, IHttpResultFactory httpResultFactory) - { - _dlnaManager = dlnaManager; - _resultFactory = httpResultFactory; - } - - private string GetHeader(string name) - { - return Request.Headers[name]; - } - - public object Get(GetDescriptionXml request) - { - var url = Request.AbsoluteUri; - var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); - var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers.ToDictionary(), request.UuId, serverAddress); - - var cacheLength = TimeSpan.FromDays(1); - var cacheKey = Request.RawUrl.GetMD5(); - var bytes = Encoding.UTF8.GetBytes(xml); - - return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, XMLContentType, () => Task.FromResult<Stream>(new MemoryStream(bytes))); - } - - public object Get(GetContentDirectory request) - { - var xml = ContentDirectory.GetServiceXml(Request.Headers.ToDictionary()); - - return _resultFactory.GetResult(Request, xml, XMLContentType); - } - - public object Get(GetMediaReceiverRegistrar request) - { - var xml = MediaReceiverRegistrar.GetServiceXml(Request.Headers.ToDictionary()); - - return _resultFactory.GetResult(Request, xml, XMLContentType); - } - - public object Get(GetConnnectionManager request) - { - var xml = ConnectionManager.GetServiceXml(Request.Headers.ToDictionary()); - - return _resultFactory.GetResult(Request, xml, XMLContentType); - } - - public object Post(ProcessMediaReceiverRegistrarControlRequest request) - { - var response = PostAsync(request.RequestStream, MediaReceiverRegistrar); - - return _resultFactory.GetResult(Request, response.Xml, XMLContentType); - } - - public object Post(ProcessContentDirectoryControlRequest request) - { - var response = PostAsync(request.RequestStream, ContentDirectory); - - return _resultFactory.GetResult(Request, response.Xml, XMLContentType); - } - - public object Post(ProcessConnectionManagerControlRequest request) - { - var response = PostAsync(request.RequestStream, ConnectionManager); - - return _resultFactory.GetResult(Request, response.Xml, XMLContentType); - } - - private ControlResponse PostAsync(Stream requestStream, IUpnpService service) - { - var id = GetPathValue(2); - - return service.ProcessControlRequest(new ControlRequest - { - Headers = Request.Headers.ToDictionary(), - InputXml = requestStream, - TargetServerUuId = id, - RequestedUrl = Request.AbsoluteUri - }); - } - - protected string GetPathValue(int index) - { - var pathInfo = Parse(Request.PathInfo); - var first = pathInfo[0]; - - // backwards compatibility - // TODO: Work out what this is doing. - if (string.Equals(first, "mediabrowser", StringComparison.OrdinalIgnoreCase) || - string.Equals(first, "emby", StringComparison.OrdinalIgnoreCase) || - string.Equals(first, "jellyfin", StringComparison.OrdinalIgnoreCase)) - { - index++; - } - - return pathInfo[index]; - } - - private List<string> Parse(string pathUri) - { - var actionParts = pathUri.Split(new[] { "://" }, StringSplitOptions.None); - - var pathInfo = actionParts[actionParts.Length - 1]; - - var optionsPos = pathInfo.LastIndexOf('?'); - if (optionsPos != -1) - { - pathInfo = pathInfo.Substring(0, optionsPos); - } - - var args = pathInfo.Split('/'); - - return args.Skip(1).ToList(); - } - - public object Get(GetIcon request) - { - var contentType = "image/" + Path.GetExtension(request.Filename) - .TrimStart('.') - .ToLowerInvariant(); - - var cacheLength = TimeSpan.FromDays(365); - var cacheKey = Request.RawUrl.GetMD5(); - - return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, contentType, () => Task.FromResult(_dlnaManager.GetIcon(request.Filename).Stream)); - } - - public object Subscribe(ProcessContentDirectoryEventRequest request) - { - return ProcessEventRequest(ContentDirectory); - } - - public object Subscribe(ProcessConnectionManagerEventRequest request) - { - return ProcessEventRequest(ConnectionManager); - } - - public object Subscribe(ProcessMediaReceiverRegistrarEventRequest request) - { - return ProcessEventRequest(MediaReceiverRegistrar); - } - - public object Unsubscribe(ProcessContentDirectoryEventRequest request) - { - return ProcessEventRequest(ContentDirectory); - } - - public object Unsubscribe(ProcessConnectionManagerEventRequest request) - { - return ProcessEventRequest(ConnectionManager); - } - - public object Unsubscribe(ProcessMediaReceiverRegistrarEventRequest request) - { - return ProcessEventRequest(MediaReceiverRegistrar); - } - - private object ProcessEventRequest(IEventManager eventManager) - { - var subscriptionId = GetHeader("SID"); - - if (string.Equals(Request.Verb, "SUBSCRIBE", StringComparison.OrdinalIgnoreCase)) - { - var notificationType = GetHeader("NT"); - - var callback = GetHeader("CALLBACK"); - var timeoutString = GetHeader("TIMEOUT"); - - if (string.IsNullOrEmpty(notificationType)) - { - return GetSubscriptionResponse(eventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callback)); - } - - return GetSubscriptionResponse(eventManager.CreateEventSubscription(notificationType, timeoutString, callback)); - } - - return GetSubscriptionResponse(eventManager.CancelEventSubscription(subscriptionId)); - } - - private object GetSubscriptionResponse(EventSubscriptionResponse response) - { - return _resultFactory.GetResult(Request, response.Content, response.ContentType, response.Headers); - } - } -} diff --git a/Emby.Dlna/Api/DlnaService.cs b/Emby.Dlna/Api/DlnaService.cs deleted file mode 100644 index 7f51f477a..000000000 --- a/Emby.Dlna/Api/DlnaService.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Linq; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Services; - -namespace Emby.Dlna.Api -{ - [Route("/Dlna/ProfileInfos", "GET", Summary = "Gets a list of profiles")] - public class GetProfileInfos : IReturn<DeviceProfileInfo[]> - { - } - - [Route("/Dlna/Profiles/{Id}", "DELETE", Summary = "Deletes a profile")] - public class DeleteProfile : IReturnVoid - { - [ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Id { get; set; } - } - - [Route("/Dlna/Profiles/Default", "GET", Summary = "Gets the default profile")] - public class GetDefaultProfile : IReturn<DeviceProfile> - { - } - - [Route("/Dlna/Profiles/{Id}", "GET", Summary = "Gets a single profile")] - public class GetProfile : IReturn<DeviceProfile> - { - [ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - [Route("/Dlna/Profiles/{Id}", "POST", Summary = "Updates a profile")] - public class UpdateProfile : DeviceProfile, IReturnVoid - { - } - - [Route("/Dlna/Profiles", "POST", Summary = "Creates a profile")] - public class CreateProfile : DeviceProfile, IReturnVoid - { - } - - [Authenticated(Roles = "Admin")] - public class DlnaService : IService - { - private readonly IDlnaManager _dlnaManager; - - public DlnaService(IDlnaManager dlnaManager) - { - _dlnaManager = dlnaManager; - } - - public object Get(GetProfileInfos request) - { - return _dlnaManager.GetProfileInfos().ToArray(); - } - - public object Get(GetProfile request) - { - return _dlnaManager.GetProfile(request.Id); - } - - public object Get(GetDefaultProfile request) - { - return _dlnaManager.GetDefaultProfile(); - } - - public void Delete(DeleteProfile request) - { - _dlnaManager.DeleteProfile(request.Id); - } - - public void Post(UpdateProfile request) - { - _dlnaManager.UpdateProfile(request); - } - - public void Post(CreateProfile request) - { - _dlnaManager.CreateProfile(request); - } - } -} diff --git a/Emby.Dlna/Common/Argument.cs b/Emby.Dlna/Common/Argument.cs index 3e325c41c..e4e9c55e0 100644 --- a/Emby.Dlna/Common/Argument.cs +++ b/Emby.Dlna/Common/Argument.cs @@ -1,12 +1,23 @@ - namespace Emby.Dlna.Common { + /// <summary> + /// DLNA Query parameter type, used when querying DLNA devices via SOAP. + /// </summary> public class Argument { - public string Name { get; set; } + /// <summary> + /// Gets or sets name of the DLNA argument. + /// </summary> + public string Name { get; set; } = string.Empty; - public string Direction { get; set; } + /// <summary> + /// Gets or sets the direction of the parameter. + /// </summary> + public string Direction { get; set; } = string.Empty; - public string RelatedStateVariable { get; set; } + /// <summary> + /// Gets or sets the related DLNA state variable for this argument. + /// </summary> + public string RelatedStateVariable { get; set; } = string.Empty; } } diff --git a/Emby.Dlna/Common/DeviceIcon.cs b/Emby.Dlna/Common/DeviceIcon.cs index 3a91b952e..f9fd1dcec 100644 --- a/Emby.Dlna/Common/DeviceIcon.cs +++ b/Emby.Dlna/Common/DeviceIcon.cs @@ -1,21 +1,41 @@ +using System.Globalization; namespace Emby.Dlna.Common { + /// <summary> + /// Defines the <see cref="DeviceIcon" />. + /// </summary> public class DeviceIcon { - public string Url { get; set; } + /// <summary> + /// Gets or sets the Url. + /// </summary> + public string Url { get; set; } = string.Empty; - public string MimeType { get; set; } + /// <summary> + /// Gets or sets the MimeType. + /// </summary> + public string MimeType { get; set; } = string.Empty; + /// <summary> + /// Gets or sets the Width. + /// </summary> public int Width { get; set; } + /// <summary> + /// Gets or sets the Height. + /// </summary> public int Height { get; set; } - public string Depth { get; set; } + /// <summary> + /// Gets or sets the Depth. + /// </summary> + public string Depth { get; set; } = string.Empty; + /// <inheritdoc /> public override string ToString() { - return string.Format("{0}x{1}", Height, Width); + return string.Format(CultureInfo.InvariantCulture, "{0}x{1}", Height, Width); } } } diff --git a/Emby.Dlna/Common/DeviceService.cs b/Emby.Dlna/Common/DeviceService.cs index c60d65291..c1369558e 100644 --- a/Emby.Dlna/Common/DeviceService.cs +++ b/Emby.Dlna/Common/DeviceService.cs @@ -1,21 +1,36 @@ - namespace Emby.Dlna.Common { + /// <summary> + /// Defines the <see cref="DeviceService" />. + /// </summary> public class DeviceService { - public string ServiceType { get; set; } + /// <summary> + /// Gets or sets the Service Type. + /// </summary> + public string ServiceType { get; set; } = string.Empty; - public string ServiceId { get; set; } + /// <summary> + /// Gets or sets the Service Id. + /// </summary> + public string ServiceId { get; set; } = string.Empty; - public string ScpdUrl { get; set; } + /// <summary> + /// Gets or sets the Scpd Url. + /// </summary> + public string ScpdUrl { get; set; } = string.Empty; - public string ControlUrl { get; set; } + /// <summary> + /// Gets or sets the Control Url. + /// </summary> + public string ControlUrl { get; set; } = string.Empty; - public string EventSubUrl { get; set; } + /// <summary> + /// Gets or sets the EventSubUrl. + /// </summary> + public string EventSubUrl { get; set; } = string.Empty; - public override string ToString() - { - return string.Format("{0}", ServiceId); - } + /// <inheritdoc /> + public override string ToString() => ServiceId; } } diff --git a/Emby.Dlna/Common/ServiceAction.cs b/Emby.Dlna/Common/ServiceAction.cs index 5e030d396..02b81a0aa 100644 --- a/Emby.Dlna/Common/ServiceAction.cs +++ b/Emby.Dlna/Common/ServiceAction.cs @@ -2,20 +2,30 @@ using System.Collections.Generic; namespace Emby.Dlna.Common { + /// <summary> + /// Defines the <see cref="ServiceAction" />. + /// </summary> public class ServiceAction { - public string Name { get; set; } - - public List<Argument> ArgumentList { get; set; } - - public override string ToString() - { - return Name; - } - + /// <summary> + /// Initializes a new instance of the <see cref="ServiceAction"/> class. + /// </summary> public ServiceAction() { ArgumentList = new List<Argument>(); } + + /// <summary> + /// Gets or sets the name of the action. + /// </summary> + public string Name { get; set; } = string.Empty; + + /// <summary> + /// Gets the ArgumentList. + /// </summary> + public List<Argument> ArgumentList { get; } + + /// <inheritdoc /> + public override string ToString() => Name; } } diff --git a/Emby.Dlna/Common/StateVariable.cs b/Emby.Dlna/Common/StateVariable.cs index 4ca84bf51..fd733e085 100644 --- a/Emby.Dlna/Common/StateVariable.cs +++ b/Emby.Dlna/Common/StateVariable.cs @@ -1,25 +1,34 @@ using System; +using System.Collections.Generic; namespace Emby.Dlna.Common { + /// <summary> + /// Defines the <see cref="StateVariable" />. + /// </summary> public class StateVariable { - public string Name { get; set; } + /// <summary> + /// Gets or sets the name of the state variable. + /// </summary> + public string Name { get; set; } = string.Empty; - public string DataType { get; set; } + /// <summary> + /// Gets or sets the data type of the state variable. + /// </summary> + public string DataType { get; set; } = string.Empty; + /// <summary> + /// Gets or sets a value indicating whether it sends events. + /// </summary> public bool SendsEvents { get; set; } - public string[] AllowedValues { get; set; } + /// <summary> + /// Gets or sets the allowed values range. + /// </summary> + public IReadOnlyList<string> AllowedValues { get; set; } = Array.Empty<string>(); - public override string ToString() - { - return Name; - } - - public StateVariable() - { - AllowedValues = Array.Empty<string>(); - } + /// <inheritdoc /> + public override string ToString() => Name; } } diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs index 0ebb490a1..91fac4bef 100644 --- a/Emby.Dlna/Configuration/DlnaOptions.cs +++ b/Emby.Dlna/Configuration/DlnaOptions.cs @@ -1,23 +1,92 @@ +#pragma warning disable CS1591 namespace Emby.Dlna.Configuration { + /// <summary> + /// The DlnaOptions class contains the user definable parameters for the dlna subsystems. + /// </summary> public class DlnaOptions { - public bool EnablePlayTo { get; set; } - public bool EnableServer { get; set; } - public bool EnableDebugLog { get; set; } - public bool BlastAliveMessages { get; set; } - public int ClientDiscoveryIntervalSeconds { get; set; } - public int BlastAliveMessageIntervalSeconds { get; set; } - public string DefaultUserId { get; set; } - + /// <summary> + /// Initializes a new instance of the <see cref="DlnaOptions"/> class. + /// </summary> public DlnaOptions() { EnablePlayTo = true; EnableServer = true; BlastAliveMessages = true; + SendOnlyMatchedHost = true; ClientDiscoveryIntervalSeconds = 60; - BlastAliveMessageIntervalSeconds = 1800; + AliveMessageIntervalSeconds = 1800; } + + /// <summary> + /// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna playTo subsystem. + /// </summary> + public bool EnablePlayTo { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna server subsystem. + /// </summary> + public bool EnableServer { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether detailed dlna server logs are sent to the console/log. + /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. + /// </summary> + public bool EnableDebugLog { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether whether detailed playTo debug logs are sent to the console/log. + /// If the setting "Emby.Dlna.PlayTo": "Debug" msut be set in logging.default.json for this property to work. + /// </summary> + public bool EnablePlayToTracing { get; set; } + + /// <summary> + /// Gets or sets the ssdp client discovery interval time (in seconds). + /// This is the time after which the server will send a ssdp search request. + /// </summary> + public int ClientDiscoveryIntervalSeconds { get; set; } + + /// <summary> + /// Gets or sets the frequency at which ssdp alive notifications are transmitted. + /// </summary> + public int AliveMessageIntervalSeconds { get; set; } + + /// <summary> + /// Gets or sets the frequency at which ssdp alive notifications are transmitted. MIGRATING - TO BE REMOVED ONCE WEB HAS BEEN ALTERED. + /// </summary> + public int BlastAliveMessageIntervalSeconds + { + get + { + return AliveMessageIntervalSeconds; + } + + set + { + AliveMessageIntervalSeconds = value; + } + } + + /// <summary> + /// Gets or sets the default user account that the dlna server uses. + /// </summary> + public string? DefaultUserId { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether playTo device profiles should be created. + /// </summary> + public bool AutoCreatePlayToProfiles { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to blast alive messages. + /// </summary> + public bool BlastAliveMessages { get; set; } = true; + + /// <summary> + /// gets or sets a value indicating whether to send only matched host. + /// </summary> + public bool SendOnlyMatchedHost { get; set; } = true; } } diff --git a/Emby.Dlna/ConfigurationExtension.cs b/Emby.Dlna/ConfigurationExtension.cs index 82d726e01..3ca43052a 100644 --- a/Emby.Dlna/ConfigurationExtension.cs +++ b/Emby.Dlna/ConfigurationExtension.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +#pragma warning disable CS1591 + using Emby.Dlna.Configuration; using MediaBrowser.Common.Configuration; @@ -11,19 +12,4 @@ namespace Emby.Dlna return manager.GetConfiguration<DlnaOptions>("dlna"); } } - - public class DlnaConfigurationFactory : IConfigurationFactory - { - public IEnumerable<ConfigurationStore> GetConfigurations() - { - return new ConfigurationStore[] - { - new ConfigurationStore - { - Key = "dlna", - ConfigurationType = typeof (DlnaOptions) - } - }; - } - } } diff --git a/Emby.Dlna/ConnectionManager/ConnectionManager.cs b/Emby.Dlna/ConnectionManager/ConnectionManager.cs deleted file mode 100644 index cc427f2a1..000000000 --- a/Emby.Dlna/ConnectionManager/ConnectionManager.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using Emby.Dlna.Service; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Model.Xml; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.ConnectionManager -{ - public class ConnectionManager : BaseService, IConnectionManager - { - private readonly IDlnaManager _dlna; - private readonly ILogger _logger; - private readonly IServerConfigurationManager _config; - protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory; - - public ConnectionManager(IDlnaManager dlna, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient, IXmlReaderSettingsFactory xmlReaderSettingsFactory) - : base(logger, httpClient) - { - _dlna = dlna; - _config = config; - _logger = logger; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; - } - - public string GetServiceXml(IDictionary<string, string> headers) - { - return new ConnectionManagerXmlBuilder().GetXml(); - } - - public ControlResponse ProcessControlRequest(ControlRequest request) - { - var profile = _dlna.GetProfile(request.Headers) ?? - _dlna.GetDefaultProfile(); - - return new ControlHandler(_config, _logger, XmlReaderSettingsFactory, profile).ProcessControlRequest(request); - } - } -} diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs new file mode 100644 index 000000000..916044a0c --- /dev/null +++ b/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs @@ -0,0 +1,53 @@ +#pragma warning disable CS1591 + +using System.Net.Http; +using System.Threading.Tasks; +using Emby.Dlna.Service; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dlna; +using Microsoft.Extensions.Logging; + +namespace Emby.Dlna.ConnectionManager +{ + /// <summary> + /// Defines the <see cref="ConnectionManagerService" />. + /// </summary> + public class ConnectionManagerService : BaseService, IConnectionManager + { + private readonly IDlnaManager _dlna; + private readonly IServerConfigurationManager _config; + + /// <summary> + /// Initializes a new instance of the <see cref="ConnectionManagerService"/> class. + /// </summary> + /// <param name="dlna">The <see cref="IDlnaManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param> + /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param> + /// <param name="logger">The <see cref="ILogger{ConnectionManagerService}"/> for use with the <see cref="ConnectionManagerService"/> instance..</param> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="ConnectionManagerService"/> instance..</param> + public ConnectionManagerService( + IDlnaManager dlna, + IServerConfigurationManager config, + ILogger<ConnectionManagerService> logger, + IHttpClientFactory httpClientFactory) + : base(logger, httpClientFactory) + { + _dlna = dlna; + _config = config; + } + + /// <inheritdoc /> + public string GetServiceXml() + { + return ConnectionManagerXmlBuilder.GetXml(); + } + + /// <inheritdoc /> + public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request) + { + var profile = _dlna.GetProfile(request.Headers) ?? + _dlna.GetDefaultProfile(); + + return new ControlHandler(_config, Logger, profile).ProcessControlRequestAsync(request); + } + } +} diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs index f5873455a..c484dac54 100644 --- a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs +++ b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs @@ -1,48 +1,62 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using Emby.Dlna.Common; using Emby.Dlna.Service; namespace Emby.Dlna.ConnectionManager { - public class ConnectionManagerXmlBuilder + /// <summary> + /// Defines the <see cref="ConnectionManagerXmlBuilder" />. + /// </summary> + public static class ConnectionManagerXmlBuilder { - public string GetXml() + /// <summary> + /// Gets the ConnectionManager:1 service template. + /// See http://upnp.org/specs/av/UPnP-av-ConnectionManager-v1-Service.pdf. + /// </summary> + /// <returns>An XML description of this service.</returns> + public static string GetXml() { - return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(), GetStateVariables()); + return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables()); } + /// <summary> + /// Get the list of state variables for this invocation. + /// </summary> + /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns> private static IEnumerable<StateVariable> GetStateVariables() { - var list = new List<StateVariable>(); - - list.Add(new StateVariable + var list = new List<StateVariable> { - Name = "SourceProtocolInfo", - DataType = "string", - SendsEvents = true - }); + new StateVariable + { + Name = "SourceProtocolInfo", + DataType = "string", + SendsEvents = true + }, - list.Add(new StateVariable - { - Name = "SinkProtocolInfo", - DataType = "string", - SendsEvents = true - }); + new StateVariable + { + Name = "SinkProtocolInfo", + DataType = "string", + SendsEvents = true + }, - list.Add(new StateVariable - { - Name = "CurrentConnectionIDs", - DataType = "string", - SendsEvents = true - }); + new StateVariable + { + Name = "CurrentConnectionIDs", + DataType = "string", + SendsEvents = true + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_ConnectionStatus", - DataType = "string", - SendsEvents = false, + new StateVariable + { + Name = "A_ARG_TYPE_ConnectionStatus", + DataType = "string", + SendsEvents = false, - AllowedValues = new string[] + AllowedValues = new[] { "OK", "ContentFormatMismatch", @@ -50,55 +64,56 @@ namespace Emby.Dlna.ConnectionManager "UnreliableChannel", "Unknown" } - }); + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_ConnectionManager", - DataType = "string", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_ConnectionManager", + DataType = "string", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_Direction", - DataType = "string", - SendsEvents = false, + new StateVariable + { + Name = "A_ARG_TYPE_Direction", + DataType = "string", + SendsEvents = false, - AllowedValues = new string[] + AllowedValues = new[] { "Output", "Input" } - }); + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_ProtocolInfo", - DataType = "string", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_ProtocolInfo", + DataType = "string", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_ConnectionID", - DataType = "ui4", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_ConnectionID", + DataType = "ui4", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_AVTransportID", - DataType = "ui4", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_AVTransportID", + DataType = "ui4", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_RcsID", - DataType = "ui4", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_RcsID", + DataType = "ui4", + SendsEvents = false + } + }; return list; } diff --git a/Emby.Dlna/ConnectionManager/ControlHandler.cs b/Emby.Dlna/ConnectionManager/ControlHandler.cs index 16211c61f..1a1790ee6 100644 --- a/Emby.Dlna/ConnectionManager/ControlHandler.cs +++ b/Emby.Dlna/ConnectionManager/ControlHandler.cs @@ -1,40 +1,55 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Xml; using Emby.Dlna.Service; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace Emby.Dlna.ConnectionManager { + /// <summary> + /// Defines the <see cref="ControlHandler" />. + /// </summary> public class ControlHandler : BaseControlHandler { private readonly DeviceProfile _profile; - protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams) + /// <summary> + /// Initializes a new instance of the <see cref="ControlHandler"/> class. + /// </summary> + /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="profile">The <see cref="DeviceProfile"/> for use with the <see cref="ControlHandler"/> instance.</param> + public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile) + : base(config, logger) + { + _profile = profile; + } + + /// <inheritdoc /> + protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter) { if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase)) { - return HandleGetProtocolInfo(); + HandleGetProtocolInfo(xmlWriter); + return; } throw new ResourceNotFoundException("Unexpected control request name: " + methodName); } - private IEnumerable<KeyValuePair<string, string>> HandleGetProtocolInfo() + /// <summary> + /// Builds the response to the GetProtocolInfo request. + /// </summary> + /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> + private void HandleGetProtocolInfo(XmlWriter xmlWriter) { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) - { - { "Source", _profile.ProtocolInfo }, - { "Sink", "" } - }; - } - - public ControlHandler(IServerConfigurationManager config, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory, DeviceProfile profile) : base(config, logger, xmlReaderSettingsFactory) - { - _profile = profile; + xmlWriter.WriteElementString("Source", _profile.ProtocolInfo); + xmlWriter.WriteElementString("Sink", string.Empty); } } } diff --git a/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs b/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs index b7727b558..542c7bfb4 100644 --- a/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs +++ b/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs @@ -1,11 +1,20 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using Emby.Dlna.Common; namespace Emby.Dlna.ConnectionManager { - public class ServiceActionListBuilder + /// <summary> + /// Defines the <see cref="ServiceActionListBuilder" />. + /// </summary> + public static class ServiceActionListBuilder { - public IEnumerable<ServiceAction> GetActions() + /// <summary> + /// Returns an enumerable of the ConnectionManagar:1 DLNA actions. + /// </summary> + /// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns> + public static IEnumerable<ServiceAction> GetActions() { var list = new List<ServiceAction> { @@ -19,6 +28,10 @@ namespace Emby.Dlna.ConnectionManager return list; } + /// <summary> + /// Returns the action details for "PrepareForConnection". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> private static ServiceAction PrepareForConnection() { var action = new ServiceAction @@ -78,6 +91,10 @@ namespace Emby.Dlna.ConnectionManager return action; } + /// <summary> + /// Returns the action details for "GetCurrentConnectionInfo". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> private static ServiceAction GetCurrentConnectionInfo() { var action = new ServiceAction @@ -144,7 +161,11 @@ namespace Emby.Dlna.ConnectionManager return action; } - private ServiceAction GetProtocolInfo() + /// <summary> + /// Returns the action details for "GetProtocolInfo". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> + private static ServiceAction GetProtocolInfo() { var action = new ServiceAction { @@ -168,7 +189,11 @@ namespace Emby.Dlna.ConnectionManager return action; } - private ServiceAction GetCurrentConnectionIDs() + /// <summary> + /// Returns the action details for "GetCurrentConnectionIDs". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> + private static ServiceAction GetCurrentConnectionIDs() { var action = new ServiceAction { @@ -185,7 +210,11 @@ namespace Emby.Dlna.ConnectionManager return action; } - private ServiceAction ConnectionComplete() + /// <summary> + /// Returns the action details for "ConnectionComplete". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> + private static ServiceAction ConnectionComplete() { var action = new ServiceAction { diff --git a/Emby.Dlna/ContentDirectory/ContentDirectory.cs b/Emby.Dlna/ContentDirectory/ContentDirectory.cs deleted file mode 100644 index b0fec90e6..000000000 --- a/Emby.Dlna/ContentDirectory/ContentDirectory.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Collections.Generic; -using Emby.Dlna.Service; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.TV; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Xml; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.ContentDirectory -{ - public class ContentDirectory : BaseService, IContentDirectory - { - private readonly ILibraryManager _libraryManager; - private readonly IImageProcessor _imageProcessor; - private readonly IUserDataManager _userDataManager; - private readonly IDlnaManager _dlna; - private readonly IServerConfigurationManager _config; - private readonly IUserManager _userManager; - private readonly ILocalizationManager _localization; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IUserViewManager _userViewManager; - private readonly IMediaEncoder _mediaEncoder; - protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory; - private readonly ITVSeriesManager _tvSeriesManager; - - public ContentDirectory(IDlnaManager dlna, - IUserDataManager userDataManager, - IImageProcessor imageProcessor, - ILibraryManager libraryManager, - IServerConfigurationManager config, - IUserManager userManager, - ILogger logger, - IHttpClient httpClient, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager, IMediaEncoder mediaEncoder, IXmlReaderSettingsFactory xmlReaderSettingsFactory, ITVSeriesManager tvSeriesManager) - : base(logger, httpClient) - { - _dlna = dlna; - _userDataManager = userDataManager; - _imageProcessor = imageProcessor; - _libraryManager = libraryManager; - _config = config; - _userManager = userManager; - _localization = localization; - _mediaSourceManager = mediaSourceManager; - _userViewManager = userViewManager; - _mediaEncoder = mediaEncoder; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; - _tvSeriesManager = tvSeriesManager; - } - - private int SystemUpdateId - { - get - { - var now = DateTime.UtcNow; - - return now.Year + now.DayOfYear + now.Hour; - } - } - - public string GetServiceXml(IDictionary<string, string> headers) - { - return new ContentDirectoryXmlBuilder().GetXml(); - } - - public ControlResponse ProcessControlRequest(ControlRequest request) - { - var profile = _dlna.GetProfile(request.Headers) ?? - _dlna.GetDefaultProfile(); - - var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase)); - - var user = GetUser(profile); - - return new ControlHandler( - Logger, - _libraryManager, - profile, - serverAddress, - null, - _imageProcessor, - _userDataManager, - user, - SystemUpdateId, - _config, - _localization, - _mediaSourceManager, - _userViewManager, - _mediaEncoder, - XmlReaderSettingsFactory, - _tvSeriesManager) - .ProcessControlRequest(request); - } - - private User GetUser(DeviceProfile profile) - { - if (!string.IsNullOrEmpty(profile.UserId)) - { - var user = _userManager.GetUserById(profile.UserId); - - if (user != null) - { - return user; - } - } - - var userId = _config.GetDlnaConfiguration().DefaultUserId; - - if (!string.IsNullOrEmpty(userId)) - { - var user = _userManager.GetUserById(userId); - - if (user != null) - { - return user; - } - } - - foreach (var user in _userManager.Users) - { - if (user.Policy.IsAdministrator) - { - return user; - } - } - - foreach (var user in _userManager.Users) - { - return user; - } - - return null; - } - } -} diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs new file mode 100644 index 000000000..9020dea99 --- /dev/null +++ b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs @@ -0,0 +1,176 @@ +#pragma warning disable CS1591 + +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Emby.Dlna.Service; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.TV; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Globalization; +using Microsoft.Extensions.Logging; + +namespace Emby.Dlna.ContentDirectory +{ + /// <summary> + /// Defines the <see cref="ContentDirectoryService" />. + /// </summary> + public class ContentDirectoryService : BaseService, IContentDirectory + { + private readonly ILibraryManager _libraryManager; + private readonly IImageProcessor _imageProcessor; + private readonly IUserDataManager _userDataManager; + private readonly IDlnaManager _dlna; + private readonly IServerConfigurationManager _config; + private readonly IUserManager _userManager; + private readonly ILocalizationManager _localization; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IUserViewManager _userViewManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly ITVSeriesManager _tvSeriesManager; + + /// <summary> + /// Initializes a new instance of the <see cref="ContentDirectoryService"/> class. + /// </summary> + /// <param name="dlna">The <see cref="IDlnaManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> + /// <param name="userDataManager">The <see cref="IUserDataManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> + /// <param name="imageProcessor">The <see cref="IImageProcessor"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> + /// <param name="libraryManager">The <see cref="ILibraryManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> + /// <param name="config">The <see cref="IServerConfigurationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> + /// <param name="userManager">The <see cref="IUserManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> + /// <param name="logger">The <see cref="ILogger{ContentDirectoryService}"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> + /// <param name="httpClient">The <see cref="IHttpClientFactory"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> + /// <param name="localization">The <see cref="ILocalizationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> + /// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> + /// <param name="userViewManager">The <see cref="IUserViewManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> + /// <param name="mediaEncoder">The <see cref="IMediaEncoder"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> + /// <param name="tvSeriesManager">The <see cref="ITVSeriesManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param> + public ContentDirectoryService( + IDlnaManager dlna, + IUserDataManager userDataManager, + IImageProcessor imageProcessor, + ILibraryManager libraryManager, + IServerConfigurationManager config, + IUserManager userManager, + ILogger<ContentDirectoryService> logger, + IHttpClientFactory httpClient, + ILocalizationManager localization, + IMediaSourceManager mediaSourceManager, + IUserViewManager userViewManager, + IMediaEncoder mediaEncoder, + ITVSeriesManager tvSeriesManager) + : base(logger, httpClient) + { + _dlna = dlna; + _userDataManager = userDataManager; + _imageProcessor = imageProcessor; + _libraryManager = libraryManager; + _config = config; + _userManager = userManager; + _localization = localization; + _mediaSourceManager = mediaSourceManager; + _userViewManager = userViewManager; + _mediaEncoder = mediaEncoder; + _tvSeriesManager = tvSeriesManager; + } + + /// <summary> + /// Gets the system id. (A unique id which changes on when our definition changes.) + /// </summary> + private static int SystemUpdateId + { + get + { + var now = DateTime.UtcNow; + + return now.Year + now.DayOfYear + now.Hour; + } + } + + /// <inheritdoc /> + public string GetServiceXml() + { + return ContentDirectoryXmlBuilder.GetXml(); + } + + /// <inheritdoc /> + public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile(); + + var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase)); + + var user = GetUser(profile); + + return new ControlHandler( + Logger, + _libraryManager, + profile, + serverAddress, + null, + _imageProcessor, + _userDataManager, + user, + SystemUpdateId, + _config, + _localization, + _mediaSourceManager, + _userViewManager, + _mediaEncoder, + _tvSeriesManager) + .ProcessControlRequestAsync(request); + } + + /// <summary> + /// Get the user stored in the device profile. + /// </summary> + /// <param name="profile">The <see cref="DeviceProfile"/>.</param> + /// <returns>The <see cref="User"/>.</returns> + private User? GetUser(DeviceProfile profile) + { + if (!string.IsNullOrEmpty(profile.UserId)) + { + var user = _userManager.GetUserById(Guid.Parse(profile.UserId)); + + if (user != null) + { + return user; + } + } + + var userId = _config.GetDlnaConfiguration().DefaultUserId; + + if (!string.IsNullOrEmpty(userId)) + { + var user = _userManager.GetUserById(Guid.Parse(userId)); + + if (user != null) + { + return user; + } + } + + foreach (var user in _userManager.Users) + { + if (user.HasPermission(PermissionKind.IsAdministrator)) + { + return user; + } + } + + return _userManager.Users.FirstOrDefault(); + } + } +} diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs index 15fdb36c4..3edaabb70 100644 --- a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs @@ -1,145 +1,159 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using Emby.Dlna.Common; using Emby.Dlna.Service; namespace Emby.Dlna.ContentDirectory { - public class ContentDirectoryXmlBuilder + /// <summary> + /// Defines the <see cref="ContentDirectoryXmlBuilder" />. + /// </summary> + public static class ContentDirectoryXmlBuilder { - public string GetXml() + /// <summary> + /// Gets the ContentDirectory:1 service template. + /// See http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf. + /// </summary> + /// <returns>An XML description of this service.</returns> + public static string GetXml() { - return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(), - GetStateVariables()); + return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables()); } + /// <summary> + /// Get the list of state variables for this invocation. + /// </summary> + /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns> private static IEnumerable<StateVariable> GetStateVariables() { - var list = new List<StateVariable>(); - - list.Add(new StateVariable + var list = new List<StateVariable> { - Name = "A_ARG_TYPE_Filter", - DataType = "string", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_Filter", + DataType = "string", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_SortCriteria", - DataType = "string", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_SortCriteria", + DataType = "string", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_Index", - DataType = "ui4", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_Index", + DataType = "ui4", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_Count", - DataType = "ui4", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_Count", + DataType = "ui4", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_UpdateID", - DataType = "ui4", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_UpdateID", + DataType = "ui4", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "SearchCapabilities", - DataType = "string", - SendsEvents = false - }); + new StateVariable + { + Name = "SearchCapabilities", + DataType = "string", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "SortCapabilities", - DataType = "string", - SendsEvents = false - }); + new StateVariable + { + Name = "SortCapabilities", + DataType = "string", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "SystemUpdateID", - DataType = "ui4", - SendsEvents = true - }); + new StateVariable + { + Name = "SystemUpdateID", + DataType = "ui4", + SendsEvents = true + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_SearchCriteria", - DataType = "string", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_SearchCriteria", + DataType = "string", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_Result", - DataType = "string", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_Result", + DataType = "string", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_ObjectID", - DataType = "string", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_ObjectID", + DataType = "string", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_BrowseFlag", - DataType = "string", - SendsEvents = false, + new StateVariable + { + Name = "A_ARG_TYPE_BrowseFlag", + DataType = "string", + SendsEvents = false, - AllowedValues = new string[] + AllowedValues = new[] { "BrowseMetadata", "BrowseDirectChildren" } - }); + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_BrowseLetter", - DataType = "string", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_BrowseLetter", + DataType = "string", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_CategoryType", - DataType = "ui4", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_CategoryType", + DataType = "ui4", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_RID", - DataType = "ui4", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_RID", + DataType = "ui4", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_PosSec", - DataType = "ui4", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_PosSec", + DataType = "ui4", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_Featurelist", - DataType = "string", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_Featurelist", + DataType = "string", + SendsEvents = false + } + }; return list; } diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 1150afdba..ac336e5dc 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Globalization; @@ -8,6 +10,8 @@ using System.Threading; using System.Xml; using Emby.Dlna.Didl; using Emby.Dlna.Service; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; @@ -15,7 +19,6 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; @@ -25,13 +28,26 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; +using Book = MediaBrowser.Controller.Entities.Book; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; +using Genre = MediaBrowser.Controller.Entities.Genre; +using Movie = MediaBrowser.Controller.Entities.Movies.Movie; +using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; +using Series = MediaBrowser.Controller.Entities.TV.Series; namespace Emby.Dlna.ContentDirectory { + /// <summary> + /// Defines the <see cref="ControlHandler" />. + /// </summary> public class ControlHandler : BaseControlHandler { + private const string NsDc = "http://purl.org/dc/elements/1.1/"; + private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; + private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/"; + private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/"; + private readonly ILibraryManager _libraryManager; private readonly IUserDataManager _userDataManager; private readonly IServerConfigurationManager _config; @@ -39,20 +55,47 @@ namespace Emby.Dlna.ContentDirectory private readonly IUserViewManager _userViewManager; private readonly ITVSeriesManager _tvSeriesManager; - private const string NS_DC = "http://purl.org/dc/elements/1.1/"; - private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; - private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/"; - private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/"; - private readonly int _systemUpdateId; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly DidlBuilder _didlBuilder; private readonly DeviceProfile _profile; - public ControlHandler(ILogger logger, ILibraryManager libraryManager, DeviceProfile profile, string serverAddress, string accessToken, IImageProcessor imageProcessor, IUserDataManager userDataManager, User user, int systemUpdateId, IServerConfigurationManager config, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager, IMediaEncoder mediaEncoder, IXmlReaderSettingsFactory xmlReaderSettingsFactory, ITVSeriesManager tvSeriesManager) - : base(config, logger, xmlReaderSettingsFactory) + /// <summary> + /// Initializes a new instance of the <see cref="ControlHandler"/> class. + /// </summary> + /// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="libraryManager">The <see cref="ILibraryManager"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="profile">The <see cref="DeviceProfile"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="serverAddress">The server address to use in this instance> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="accessToken">The <see cref="string"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="imageProcessor">The <see cref="IImageProcessor"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="userDataManager">The <see cref="IUserDataManager"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="user">The <see cref="User"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="systemUpdateId">The system id for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="localization">The <see cref="ILocalizationManager"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="userViewManager">The <see cref="IUserViewManager"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="mediaEncoder">The <see cref="IMediaEncoder"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="tvSeriesManager">The <see cref="ITVSeriesManager"/> for use with the <see cref="ControlHandler"/> instance.</param> + public ControlHandler( + ILogger logger, + ILibraryManager libraryManager, + DeviceProfile profile, + string serverAddress, + string accessToken, + IImageProcessor imageProcessor, + IUserDataManager userDataManager, + User user, + int systemUpdateId, + IServerConfigurationManager config, + ILocalizationManager localization, + IMediaSourceManager mediaSourceManager, + IUserViewManager userViewManager, + IMediaEncoder mediaEncoder, + ITVSeriesManager tvSeriesManager) + : base(config, logger) { _libraryManager = libraryManager; _userDataManager = userDataManager; @@ -63,149 +106,210 @@ namespace Emby.Dlna.ContentDirectory _profile = profile; _config = config; - _didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, _logger, mediaEncoder); + _didlBuilder = new DidlBuilder( + profile, + user, + imageProcessor, + serverAddress, + accessToken, + userDataManager, + localization, + mediaSourceManager, + Logger, + mediaEncoder, + libraryManager); } - protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams) + /// <inheritdoc /> + protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter) { - var deviceId = "test"; + if (xmlWriter == null) + { + throw new ArgumentNullException(nameof(xmlWriter)); + } + + if (methodParams == null) + { + throw new ArgumentNullException(nameof(methodParams)); + } - var user = _user; + const string DeviceId = "test"; if (string.Equals(methodName, "GetSearchCapabilities", StringComparison.OrdinalIgnoreCase)) - return HandleGetSearchCapabilities(); + { + HandleGetSearchCapabilities(xmlWriter); + return; + } if (string.Equals(methodName, "GetSortCapabilities", StringComparison.OrdinalIgnoreCase)) - return HandleGetSortCapabilities(); + { + HandleGetSortCapabilities(xmlWriter); + return; + } if (string.Equals(methodName, "GetSortExtensionCapabilities", StringComparison.OrdinalIgnoreCase)) - return HandleGetSortExtensionCapabilities(); + { + HandleGetSortExtensionCapabilities(xmlWriter); + return; + } if (string.Equals(methodName, "GetSystemUpdateID", StringComparison.OrdinalIgnoreCase)) - return HandleGetSystemUpdateID(); + { + HandleGetSystemUpdateID(xmlWriter); + return; + } if (string.Equals(methodName, "Browse", StringComparison.OrdinalIgnoreCase)) - return HandleBrowse(methodParams, user, deviceId); + { + HandleBrowse(xmlWriter, methodParams, DeviceId); + return; + } if (string.Equals(methodName, "X_GetFeatureList", StringComparison.OrdinalIgnoreCase)) - return HandleXGetFeatureList(); + { + HandleXGetFeatureList(xmlWriter); + return; + } if (string.Equals(methodName, "GetFeatureList", StringComparison.OrdinalIgnoreCase)) - return HandleGetFeatureList(); + { + HandleGetFeatureList(xmlWriter); + return; + } if (string.Equals(methodName, "X_SetBookmark", StringComparison.OrdinalIgnoreCase)) - return HandleXSetBookmark(methodParams, user); + { + HandleXSetBookmark(methodParams); + return; + } if (string.Equals(methodName, "Search", StringComparison.OrdinalIgnoreCase)) - return HandleSearch(methodParams, user, deviceId); + { + HandleSearch(xmlWriter, methodParams, DeviceId); + return; + } if (string.Equals(methodName, "X_BrowseByLetter", StringComparison.OrdinalIgnoreCase)) - return HandleX_BrowseByLetter(methodParams, user, deviceId); + { + HandleXBrowseByLetter(xmlWriter, methodParams, DeviceId); + return; + } throw new ResourceNotFoundException("Unexpected control request name: " + methodName); } - private IEnumerable<KeyValuePair<string, string>> HandleXSetBookmark(IDictionary<string, string> sparams, User user) + /// <summary> + /// Adds a "XSetBookmark" element to the xml document. + /// </summary> + /// <param name="sparams">The method parameters.</param> + private void HandleXSetBookmark(IReadOnlyDictionary<string, string> sparams) { var id = sparams["ObjectID"]; - var serverItem = GetItemFromObjectId(id, user); + var serverItem = GetItemFromObjectId(id); var item = serverItem.Item; - var newbookmark = int.Parse(sparams["PosSecond"], _usCulture); + var newbookmark = int.Parse(sparams["PosSecond"], CultureInfo.InvariantCulture); - var userdata = _userDataManager.GetUserData(user, item); + var userdata = _userDataManager.GetUserData(_user, item); userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks; - _userDataManager.SaveUserData(user, item, userdata, UserDataSaveReason.TogglePlayed, + _userDataManager.SaveUserData( + _user, + item, + userdata, + UserDataSaveReason.TogglePlayed, CancellationToken.None); - - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } - private IEnumerable<KeyValuePair<string, string>> HandleGetSearchCapabilities() + /// <summary> + /// Adds the "SearchCaps" element to the xml document. + /// </summary> + /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> + private static void HandleGetSearchCapabilities(XmlWriter xmlWriter) { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) - { - { "SearchCaps", "res@resolution,res@size,res@duration,dc:title,dc:creator,upnp:actor,upnp:artist,upnp:genre,upnp:album,dc:date,upnp:class,@id,@refID,@protocolInfo,upnp:author,dc:description,pv:avKeywords" } - }; - } - - private IEnumerable<KeyValuePair<string, string>> HandleGetSortCapabilities() - { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) - { - { "SortCaps", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating" } - }; + xmlWriter.WriteElementString( + "SearchCaps", + "res@resolution,res@size,res@duration,dc:title,dc:creator,upnp:actor,upnp:artist,upnp:genre,upnp:album,dc:date,upnp:class,@id,@refID,@protocolInfo,upnp:author,dc:description,pv:avKeywords"); } - private IEnumerable<KeyValuePair<string, string>> HandleGetSortExtensionCapabilities() + /// <summary> + /// Adds the "SortCaps" element to the xml document. + /// </summary> + /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> + private static void HandleGetSortCapabilities(XmlWriter xmlWriter) { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) - { - { "SortExtensionCaps", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating" } - }; + xmlWriter.WriteElementString( + "SortCaps", + "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating"); } - private IEnumerable<KeyValuePair<string, string>> HandleGetSystemUpdateID() + /// <summary> + /// Adds the "SortExtensionCaps" element to the xml document. + /// </summary> + /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> + private static void HandleGetSortExtensionCapabilities(XmlWriter xmlWriter) { - var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - headers.Add("Id", _systemUpdateId.ToString(_usCulture)); - return headers; - } - - private IEnumerable<KeyValuePair<string, string>> HandleGetFeatureList() - { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) - { - { "FeatureList", GetFeatureListXml() } - }; + xmlWriter.WriteElementString( + "SortExtensionCaps", + "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating"); } - private IEnumerable<KeyValuePair<string, string>> HandleXGetFeatureList() + /// <summary> + /// Adds the "Id" element to the xml document. + /// </summary> + /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> + private void HandleGetSystemUpdateID(XmlWriter xmlWriter) { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) - { - { "FeatureList", GetFeatureListXml() } - }; + xmlWriter.WriteElementString("Id", _systemUpdateId.ToString(CultureInfo.InvariantCulture)); } - private string GetFeatureListXml() + /// <summary> + /// Adds the "FeatureList" element to the xml document. + /// </summary> + /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> + private static void HandleGetFeatureList(XmlWriter xmlWriter) { - var builder = new StringBuilder(); - - builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); - builder.Append("<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">"); - - builder.Append("<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">"); - builder.Append("<container id=\"I\" type=\"object.item.imageItem\"/>"); - builder.Append("<container id=\"A\" type=\"object.item.audioItem\"/>"); - builder.Append("<container id=\"V\" type=\"object.item.videoItem\"/>"); - builder.Append("</Feature>"); - - builder.Append("</Features>"); - - return builder.ToString(); + xmlWriter.WriteElementString("FeatureList", WriteFeatureListXml()); } - public string GetValueOrDefault(IDictionary<string, string> sparams, string key, string defaultValue) + /// <summary> + /// Adds the "FeatureList" element to the xml document. + /// </summary> + /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> + private static void HandleXGetFeatureList(XmlWriter xmlWriter) + => HandleGetFeatureList(xmlWriter); + + /// <summary> + /// Builds a static feature list. + /// </summary> + /// <returns>The xml feature list.</returns> + private static string WriteFeatureListXml() { - if (sparams.TryGetValue(key, out string val)) - { - return val; - } - - return defaultValue; + return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + + "<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">" + + "<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">" + + "<container id=\"I\" type=\"object.item.imageItem\"/>" + + "<container id=\"A\" type=\"object.item.audioItem\"/>" + + "<container id=\"V\" type=\"object.item.videoItem\"/>" + + "</Feature>" + + "</Features>"; } - private IEnumerable<KeyValuePair<string, string>> HandleBrowse(IDictionary<string, string> sparams, User user, string deviceId) + /// <summary> + /// Builds the "Browse" xml response. + /// </summary> + /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> + /// <param name="sparams">The method parameters.</param> + /// <param name="deviceId">The device Id to use.</param> + private void HandleBrowse(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId) { var id = sparams["ObjectID"]; var flag = sparams["BrowseFlag"]; - var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); - var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", "")); + var filter = new Filter(sparams.GetValueOrDefault("Filter", "*")); + var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty)); var provided = 0; @@ -224,104 +328,108 @@ namespace Emby.Dlna.ContentDirectory start = startVal; } - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CloseOutput = false, - OmitXmlDeclaration = true, - ConformanceLevel = ConformanceLevel.Fragment - }; - - StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8); - int totalCount; - var dlnaOptions = _config.GetDlnaConfiguration(); - - using (var writer = XmlWriter.Create(builder, settings)) + using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8)) { - //writer.WriteStartDocument(); - - writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); - - writer.WriteAttributeString("xmlns", "dc", null, NS_DC); - writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); - writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); - //didl.SetAttribute("xmlns:sec", NS_SEC); - - DidlBuilder.WriteXmlRootAttributes(_profile, writer); - - var serverItem = GetItemFromObjectId(id, user); - var item = serverItem.Item; + var settings = new XmlWriterSettings() + { + Encoding = Encoding.UTF8, + CloseOutput = false, + OmitXmlDeclaration = true, + ConformanceLevel = ConformanceLevel.Fragment + }; - if (string.Equals(flag, "BrowseMetadata")) + using (var writer = XmlWriter.Create(builder, settings)) { - totalCount = 1; + writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl); - if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue) - { - var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount)); + writer.WriteAttributeString("xmlns", "dc", null, NsDc); + writer.WriteAttributeString("xmlns", "dlna", null, NsDlna); + writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp); - _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id); - } - else - { - _didlBuilder.WriteItemElement(dlnaOptions, writer, item, user, null, null, deviceId, filter); - } + DidlBuilder.WriteXmlRootAttributes(_profile, writer); - provided++; - } - else - { - var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount)); - totalCount = childrenResult.TotalRecordCount; - - provided = childrenResult.Items.Length; + var serverItem = GetItemFromObjectId(id); + var item = serverItem.Item; - foreach (var i in childrenResult.Items) + if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal)) { - var childItem = i.Item; - var displayStubType = i.StubType; + totalCount = 1; - if (childItem.IsDisplayedAsFolder || displayStubType.HasValue) + if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue) { - var childCount = (GetUserItems(childItem, displayStubType, user, sortCriteria, null, 0)) - .TotalRecordCount; + var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount); - _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter); + _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id); } else { - _didlBuilder.WriteItemElement(dlnaOptions, writer, childItem, user, item, serverItem.StubType, deviceId, filter); + _didlBuilder.WriteItemElement(writer, item, _user, null, null, deviceId, filter); + } + + provided++; + } + else + { + var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount); + totalCount = childrenResult.TotalRecordCount; + + provided = childrenResult.Items.Count; + + foreach (var i in childrenResult.Items) + { + var childItem = i.Item; + var displayStubType = i.StubType; + + if (childItem.IsDisplayedAsFolder || displayStubType.HasValue) + { + var childCount = GetUserItems(childItem, displayStubType, _user, sortCriteria, null, 0) + .TotalRecordCount; + + _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter); + } + else + { + _didlBuilder.WriteItemElement(writer, childItem, _user, item, serverItem.StubType, deviceId, filter); + } } } + + writer.WriteFullEndElement(); } - writer.WriteFullEndElement(); - //writer.WriteEndDocument(); - } - var resXML = builder.ToString(); + xmlWriter.WriteElementString("Result", builder.ToString()); + } - return new[] - { - new KeyValuePair<string,string>("Result", resXML), - new KeyValuePair<string,string>("NumberReturned", provided.ToString(_usCulture)), - new KeyValuePair<string,string>("TotalMatches", totalCount.ToString(_usCulture)), - new KeyValuePair<string,string>("UpdateID", _systemUpdateId.ToString(_usCulture)) - }; + xmlWriter.WriteElementString("NumberReturned", provided.ToString(CultureInfo.InvariantCulture)); + xmlWriter.WriteElementString("TotalMatches", totalCount.ToString(CultureInfo.InvariantCulture)); + xmlWriter.WriteElementString("UpdateID", _systemUpdateId.ToString(CultureInfo.InvariantCulture)); } - private IEnumerable<KeyValuePair<string, string>> HandleX_BrowseByLetter(IDictionary<string, string> sparams, User user, string deviceId) + /// <summary> + /// Builds the response to the "X_BrowseByLetter request. + /// </summary> + /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> + /// <param name="sparams">The method parameters.</param> + /// <param name="deviceId">The device id.</param> + private void HandleXBrowseByLetter(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId) { // TODO: Implement this method - return HandleSearch(sparams, user, deviceId); + HandleSearch(xmlWriter, sparams, deviceId); } - private IEnumerable<KeyValuePair<string, string>> HandleSearch(IDictionary<string, string> sparams, User user, string deviceId) + /// <summary> + /// Builds a response to the "Search" request. + /// </summary> + /// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param> + /// <param name="sparams">The method parameters.</param> + /// <param name="deviceId">The deviceId<see cref="string"/>.</param> + private void HandleSearch(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId) { - var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", "")); - var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", "")); - var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); + var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", string.Empty)); + var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty)); + var filter = new Filter(sparams.GetValueOrDefault("Filter", "*")); // sort example: dc:title, dc:date @@ -340,109 +448,106 @@ namespace Emby.Dlna.ContentDirectory start = startVal; } - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CloseOutput = false, - OmitXmlDeclaration = true, - ConformanceLevel = ConformanceLevel.Fragment - }; + QueryResult<BaseItem> childrenResult; - StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8); - int totalCount = 0; - int provided = 0; - - using (var writer = XmlWriter.Create(builder, settings)) + using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8)) { - //writer.WriteStartDocument(); - - writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); - - writer.WriteAttributeString("xmlns", "dc", null, NS_DC); - writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); - writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); - //didl.SetAttribute("xmlns:sec", NS_SEC); + var settings = new XmlWriterSettings() + { + Encoding = Encoding.UTF8, + CloseOutput = false, + OmitXmlDeclaration = true, + ConformanceLevel = ConformanceLevel.Fragment + }; - DidlBuilder.WriteXmlRootAttributes(_profile, writer); + using (var writer = XmlWriter.Create(builder, settings)) + { + writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl); - var serverItem = GetItemFromObjectId(sparams["ContainerID"], user); + writer.WriteAttributeString("xmlns", "dc", null, NsDc); + writer.WriteAttributeString("xmlns", "dlna", null, NsDlna); + writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp); - var item = serverItem.Item; + DidlBuilder.WriteXmlRootAttributes(_profile, writer); - var childrenResult = (GetChildrenSorted(item, user, searchCriteria, sortCriteria, start, requestedCount)); + var serverItem = GetItemFromObjectId(sparams["ContainerID"]); - totalCount = childrenResult.TotalRecordCount; + var item = serverItem.Item; - provided = childrenResult.Items.Length; + childrenResult = GetChildrenSorted(item, _user, searchCriteria, sortCriteria, start, requestedCount); - var dlnaOptions = _config.GetDlnaConfiguration(); + var dlnaOptions = _config.GetDlnaConfiguration(); - foreach (var i in childrenResult.Items) - { - if (i.IsDisplayedAsFolder) + foreach (var i in childrenResult.Items) { - var childCount = (GetChildrenSorted(i, user, searchCriteria, sortCriteria, null, 0)) - .TotalRecordCount; + if (i.IsDisplayedAsFolder) + { + var childCount = GetChildrenSorted(i, _user, searchCriteria, sortCriteria, null, 0) + .TotalRecordCount; - _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter); - } - else - { - _didlBuilder.WriteItemElement(dlnaOptions, writer, i, user, item, serverItem.StubType, deviceId, filter); + _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter); + } + else + { + _didlBuilder.WriteItemElement(writer, i, _user, item, serverItem.StubType, deviceId, filter); + } } + + writer.WriteFullEndElement(); } - writer.WriteFullEndElement(); - //writer.WriteEndDocument(); + xmlWriter.WriteElementString("Result", builder.ToString()); } - var resXML = builder.ToString(); - - return new List<KeyValuePair<string, string>> - { - new KeyValuePair<string,string>("Result", resXML), - new KeyValuePair<string,string>("NumberReturned", provided.ToString(_usCulture)), - new KeyValuePair<string,string>("TotalMatches", totalCount.ToString(_usCulture)), - new KeyValuePair<string,string>("UpdateID", _systemUpdateId.ToString(_usCulture)) - }; + xmlWriter.WriteElementString("NumberReturned", childrenResult.Items.Count.ToString(CultureInfo.InvariantCulture)); + xmlWriter.WriteElementString("TotalMatches", childrenResult.TotalRecordCount.ToString(CultureInfo.InvariantCulture)); + xmlWriter.WriteElementString("UpdateID", _systemUpdateId.ToString(CultureInfo.InvariantCulture)); } - private QueryResult<BaseItem> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit) + /// <summary> + /// Returns the child items meeting the criteria. + /// </summary> + /// <param name="item">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="search">The <see cref="SearchCriteria"/>.</param> + /// <param name="sort">The <see cref="SortCriteria"/>.</param> + /// <param name="startIndex">The start index.</param> + /// <param name="limit">The maximum number to return.</param> + /// <returns>The <see cref="QueryResult{BaseItem}"/>.</returns> + private static QueryResult<BaseItem> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit) { var folder = (Folder)item; - var sortOrders = new List<string>(); - if (!folder.IsPreSorted) - { - sortOrders.Add(ItemSortBy.SortName); - } + var sortOrders = folder.IsPreSorted + ? Array.Empty<(string, SortOrder)>() + : new[] { (ItemSortBy.SortName, sort.SortOrder) }; - var mediaTypes = new List<string>(); + string[] mediaTypes = Array.Empty<string>(); bool? isFolder = null; if (search.SearchType == SearchType.Audio) { - mediaTypes.Add(MediaType.Audio); + mediaTypes = new[] { MediaType.Audio }; isFolder = false; } else if (search.SearchType == SearchType.Video) { - mediaTypes.Add(MediaType.Video); + mediaTypes = new[] { MediaType.Video }; isFolder = false; } else if (search.SearchType == SearchType.Image) { - mediaTypes.Add(MediaType.Photo); + mediaTypes = new[] { MediaType.Photo }; isFolder = false; } else if (search.SearchType == SearchType.Playlist) { - //items = items.OfType<Playlist>(); + // items = items.OfType<Playlist>(); isFolder = true; } else if (search.SearchType == SearchType.MusicAlbum) { - //items = items.OfType<MusicAlbum>(); + // items = items.OfType<MusicAlbum>(); isFolder = true; } @@ -450,22 +555,36 @@ namespace Emby.Dlna.ContentDirectory { Limit = limit, StartIndex = startIndex, - OrderBy = sortOrders.Select(i => new ValueTuple<string, SortOrder>(i, sort.SortOrder)).ToArray(), + OrderBy = sortOrders, User = user, Recursive = true, IsMissing = false, - ExcludeItemTypes = new[] { typeof(Book).Name }, + ExcludeItemTypes = new[] { nameof(Book) }, IsFolder = isFolder, - MediaTypes = mediaTypes.ToArray(), + MediaTypes = mediaTypes, DtoOptions = GetDtoOptions() }); } - private DtoOptions GetDtoOptions() + /// <summary> + /// Returns a new DtoOptions object. + /// </summary> + /// <returns>The <see cref="DtoOptions"/>.</returns> + private static DtoOptions GetDtoOptions() { return new DtoOptions(true); } + /// <summary> + /// Returns the User items meeting the criteria. + /// </summary> + /// <param name="item">The <see cref="BaseItem"/>.</param> + /// <param name="stubType">The <see cref="StubType"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="sort">The <see cref="SortCriteria"/>.</param> + /// <param name="startIndex">The start index.</param> + /// <param name="limit">The maximum number to return.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit) { if (item is MusicGenre) @@ -500,11 +619,11 @@ namespace Emby.Dlna.ContentDirectory } else if (string.Equals(CollectionType.Folders, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase)) { - return GetFolders(item, user, stubType, sort, startIndex, limit); + return GetFolders(user, startIndex, limit); } else if (string.Equals(CollectionType.LiveTv, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase)) { - return GetLiveTvChannels(item, user, stubType, sort, startIndex, limit); + return GetLiveTvChannels(user, sort, startIndex, limit); } } @@ -523,7 +642,7 @@ namespace Emby.Dlna.ContentDirectory Limit = limit, StartIndex = startIndex, IsVirtualItem = false, - ExcludeItemTypes = new[] { typeof(Book).Name }, + ExcludeItemTypes = new[] { nameof(Book) }, IsPlaceHolder = false, DtoOptions = GetDtoOptions() }; @@ -535,14 +654,22 @@ namespace Emby.Dlna.ContentDirectory return ToResult(queryResult); } - private QueryResult<ServerItem> GetLiveTvChannels(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) + /// <summary> + /// Returns the Live Tv Channels meeting the criteria. + /// </summary> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="sort">The <see cref="SortCriteria"/>.</param> + /// <param name="startIndex">The start index.</param> + /// <param name="limit">The maximum number to return.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> + private QueryResult<ServerItem> GetLiveTvChannels(User user, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) { StartIndex = startIndex, Limit = limit, }; - query.IncludeItemTypes = new[] { typeof(LiveTvChannel).Name }; + query.IncludeItemTypes = new[] { nameof(LiveTvChannel) }; SetSorting(query, sort, false); @@ -551,6 +678,16 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } + /// <summary> + /// Returns the music folders meeting the criteria. + /// </summary> + /// <param name="item">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="stubType">The <see cref="StubType"/>.</param> + /// <param name="sort">The <see cref="SortCriteria"/>.</param> + /// <param name="startIndex">The start index.</param> + /// <param name="limit">The maximum number to return.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetMusicFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) @@ -567,7 +704,7 @@ namespace Emby.Dlna.ContentDirectory if (stubType.HasValue && stubType.Value == StubType.Playlists) { - return GetMusicPlaylists(item, user, query); + return GetMusicPlaylists(user, query); } if (stubType.HasValue && stubType.Value == StubType.Albums) @@ -610,65 +747,76 @@ namespace Emby.Dlna.ContentDirectory return GetMusicGenres(item, user, query); } - var list = new List<ServerItem>(); - - list.Add(new ServerItem(item) + var list = new List<ServerItem> { - StubType = StubType.Latest - }); + new ServerItem(item) + { + StubType = StubType.Latest + }, - list.Add(new ServerItem(item) - { - StubType = StubType.Playlists - }); + new ServerItem(item) + { + StubType = StubType.Playlists + }, - list.Add(new ServerItem(item) - { - StubType = StubType.Albums - }); + new ServerItem(item) + { + StubType = StubType.Albums + }, - list.Add(new ServerItem(item) - { - StubType = StubType.AlbumArtists - }); + new ServerItem(item) + { + StubType = StubType.AlbumArtists + }, - list.Add(new ServerItem(item) - { - StubType = StubType.Artists - }); + new ServerItem(item) + { + StubType = StubType.Artists + }, - list.Add(new ServerItem(item) - { - StubType = StubType.Songs - }); + new ServerItem(item) + { + StubType = StubType.Songs + }, - list.Add(new ServerItem(item) - { - StubType = StubType.Genres - }); + new ServerItem(item) + { + StubType = StubType.Genres + }, - list.Add(new ServerItem(item) - { - StubType = StubType.FavoriteArtists - }); + new ServerItem(item) + { + StubType = StubType.FavoriteArtists + }, - list.Add(new ServerItem(item) - { - StubType = StubType.FavoriteAlbums - }); + new ServerItem(item) + { + StubType = StubType.FavoriteAlbums + }, - list.Add(new ServerItem(item) - { - StubType = StubType.FavoriteSongs - }); + new ServerItem(item) + { + StubType = StubType.FavoriteSongs + } + }; return new QueryResult<ServerItem> { - Items = list.ToArray(), + Items = list, TotalRecordCount = list.Count }; } + /// <summary> + /// Returns the movie folders meeting the criteria. + /// </summary> + /// <param name="item">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="stubType">The <see cref="StubType"/>.</param> + /// <param name="sort">The <see cref="SortCriteria"/>.</param> + /// <param name="startIndex">The start index.</param> + /// <param name="limit">The maximum number to return.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetMovieFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) @@ -695,7 +843,7 @@ namespace Emby.Dlna.ContentDirectory if (stubType.HasValue && stubType.Value == StubType.Collections) { - return GetMovieCollections(item, user, query); + return GetMovieCollections(user, query); } if (stubType.HasValue && stubType.Value == StubType.Favorites) @@ -708,46 +856,49 @@ namespace Emby.Dlna.ContentDirectory return GetGenres(item, user, query); } - var list = new List<ServerItem>(); - - list.Add(new ServerItem(item) - { - StubType = StubType.ContinueWatching - }); - - list.Add(new ServerItem(item) - { - StubType = StubType.Latest - }); - - list.Add(new ServerItem(item) - { - StubType = StubType.Movies - }); - - list.Add(new ServerItem(item) - { - StubType = StubType.Collections - }); - - list.Add(new ServerItem(item) - { - StubType = StubType.Favorites - }); - - list.Add(new ServerItem(item) + var array = new[] { - StubType = StubType.Genres - }); + new ServerItem(item) + { + StubType = StubType.ContinueWatching + }, + new ServerItem(item) + { + StubType = StubType.Latest + }, + new ServerItem(item) + { + StubType = StubType.Movies + }, + new ServerItem(item) + { + StubType = StubType.Collections + }, + new ServerItem(item) + { + StubType = StubType.Favorites + }, + new ServerItem(item) + { + StubType = StubType.Genres + } + }; return new QueryResult<ServerItem> { - Items = list.ToArray(), - TotalRecordCount = list.Count + Items = array, + TotalRecordCount = array.Length }; } - private QueryResult<ServerItem> GetFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) + /// <summary> + /// Returns the folders meeting the criteria. + /// </summary> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="startIndex">The start index.</param> + /// <param name="limit">The maximum number to return.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> + private QueryResult<ServerItem> GetFolders(User user, int? startIndex, int? limit) { var folders = _libraryManager.GetUserRootFolder().GetChildren(user, true) .OrderBy(i => i.SortName) @@ -757,13 +908,26 @@ namespace Emby.Dlna.ContentDirectory }) .ToArray(); - return new QueryResult<ServerItem> - { - Items = folders, - TotalRecordCount = folders.Length - }; + return ApplyPaging( + new QueryResult<ServerItem> + { + Items = folders, + TotalRecordCount = folders.Length + }, + startIndex, + limit); } + /// <summary> + /// Returns the TV folders meeting the criteria. + /// </summary> + /// <param name="item">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="stubType">The <see cref="StubType"/>.</param> + /// <param name="sort">The <see cref="SortCriteria"/>.</param> + /// <param name="startIndex">The start index.</param> + /// <param name="limit">The maximum number to return.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) @@ -780,7 +944,7 @@ namespace Emby.Dlna.ContentDirectory if (stubType.HasValue && stubType.Value == StubType.NextUp) { - return GetNextUp(item, user, query); + return GetNextUp(item, query); } if (stubType.HasValue && stubType.Value == StubType.Latest) @@ -808,60 +972,68 @@ namespace Emby.Dlna.ContentDirectory return GetGenres(item, user, query); } - var list = new List<ServerItem>(); - - list.Add(new ServerItem(item) + var list = new List<ServerItem> { - StubType = StubType.ContinueWatching - }); + new ServerItem(item) + { + StubType = StubType.ContinueWatching + }, - list.Add(new ServerItem(item) - { - StubType = StubType.NextUp - }); + new ServerItem(item) + { + StubType = StubType.NextUp + }, - list.Add(new ServerItem(item) - { - StubType = StubType.Latest - }); + new ServerItem(item) + { + StubType = StubType.Latest + }, - list.Add(new ServerItem(item) - { - StubType = StubType.Series - }); + new ServerItem(item) + { + StubType = StubType.Series + }, - list.Add(new ServerItem(item) - { - StubType = StubType.FavoriteSeries - }); + new ServerItem(item) + { + StubType = StubType.FavoriteSeries + }, - list.Add(new ServerItem(item) - { - StubType = StubType.FavoriteEpisodes - }); + new ServerItem(item) + { + StubType = StubType.FavoriteEpisodes + }, - list.Add(new ServerItem(item) - { - StubType = StubType.Genres - }); + new ServerItem(item) + { + StubType = StubType.Genres + } + }; return new QueryResult<ServerItem> { - Items = list.ToArray(), + Items = list, TotalRecordCount = list.Count }; } + /// <summary> + /// Returns the Movies that are part watched that meet the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetMovieContinueWatching(BaseItem parent, User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; query.SetUser(user); - query.OrderBy = new ValueTuple<string, SortOrder>[] + query.OrderBy = new[] { - new ValueTuple<string, SortOrder> (ItemSortBy.DatePlayed, SortOrder.Descending), - new ValueTuple<string, SortOrder> (ItemSortBy.SortName, SortOrder.Ascending) + (ItemSortBy.DatePlayed, SortOrder.Descending), + (ItemSortBy.SortName, SortOrder.Ascending) }; query.IsResumable = true; @@ -872,136 +1044,213 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } + /// <summary> + /// Returns the series meeting the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetSeries(BaseItem parent, User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; query.SetUser(user); - query.IncludeItemTypes = new[] { typeof(Series).Name }; + query.IncludeItemTypes = new[] { nameof(Series) }; var result = _libraryManager.GetItemsResult(query); return ToResult(result); } + /// <summary> + /// Returns the Movie folders meeting the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetMovieMovies(BaseItem parent, User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; query.SetUser(user); - query.IncludeItemTypes = new[] { typeof(Movie).Name }; + query.IncludeItemTypes = new[] { nameof(Movie) }; var result = _libraryManager.GetItemsResult(query); return ToResult(result); } - private QueryResult<ServerItem> GetMovieCollections(BaseItem parent, User user, InternalItemsQuery query) + /// <summary> + /// Returns the Movie collections meeting the criteria. + /// </summary> + /// <param name="user">The see cref="User"/>.</param> + /// <param name="query">The see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> + private QueryResult<ServerItem> GetMovieCollections(User user, InternalItemsQuery query) { query.Recursive = true; - //query.Parent = parent; + // query.Parent = parent; query.SetUser(user); - query.IncludeItemTypes = new[] { typeof(BoxSet).Name }; + query.IncludeItemTypes = new[] { nameof(BoxSet) }; var result = _libraryManager.GetItemsResult(query); return ToResult(result); } + /// <summary> + /// Returns the Music albums meeting the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetMusicAlbums(BaseItem parent, User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; query.SetUser(user); - query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name }; + query.IncludeItemTypes = new[] { nameof(MusicAlbum) }; var result = _libraryManager.GetItemsResult(query); return ToResult(result); } + /// <summary> + /// Returns the Music songs meeting the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetMusicSongs(BaseItem parent, User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; query.SetUser(user); - query.IncludeItemTypes = new[] { typeof(Audio).Name }; + query.IncludeItemTypes = new[] { nameof(Audio) }; var result = _libraryManager.GetItemsResult(query); return ToResult(result); } + /// <summary> + /// Returns the songs tagged as favourite that meet the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetFavoriteSongs(BaseItem parent, User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; query.SetUser(user); query.IsFavorite = true; - query.IncludeItemTypes = new[] { typeof(Audio).Name }; + query.IncludeItemTypes = new[] { nameof(Audio) }; var result = _libraryManager.GetItemsResult(query); return ToResult(result); } + /// <summary> + /// Returns the series tagged as favourite that meet the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetFavoriteSeries(BaseItem parent, User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; query.SetUser(user); query.IsFavorite = true; - query.IncludeItemTypes = new[] { typeof(Series).Name }; + query.IncludeItemTypes = new[] { nameof(Series) }; var result = _libraryManager.GetItemsResult(query); return ToResult(result); } + /// <summary> + /// Returns the episodes tagged as favourite that meet the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetFavoriteEpisodes(BaseItem parent, User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; query.SetUser(user); query.IsFavorite = true; - query.IncludeItemTypes = new[] { typeof(Episode).Name }; + query.IncludeItemTypes = new[] { nameof(Episode) }; var result = _libraryManager.GetItemsResult(query); return ToResult(result); } + /// <summary> + /// Returns the movies tagged as favourite that meet the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetMovieFavorites(BaseItem parent, User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; query.SetUser(user); query.IsFavorite = true; - query.IncludeItemTypes = new[] { typeof(Movie).Name }; + query.IncludeItemTypes = new[] { nameof(Movie) }; var result = _libraryManager.GetItemsResult(query); return ToResult(result); } + /// <summary> + /// /// Returns the albums tagged as favourite that meet the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetFavoriteAlbums(BaseItem parent, User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; query.SetUser(user); query.IsFavorite = true; - query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name }; + query.IncludeItemTypes = new[] { nameof(MusicAlbum) }; var result = _libraryManager.GetItemsResult(query); return ToResult(result); } + /// <summary> + /// Returns the genres meeting the criteria. + /// The GetGenres. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetGenres(BaseItem parent, User user, InternalItemsQuery query) { var genresResult = _libraryManager.GetGenres(new InternalItemsQuery(user) @@ -1020,6 +1269,13 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } + /// <summary> + /// Returns the music genres meeting the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetMusicGenres(BaseItem parent, User user, InternalItemsQuery query) { var genresResult = _libraryManager.GetMusicGenres(new InternalItemsQuery(user) @@ -1038,6 +1294,13 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } + /// <summary> + /// Returns the music albums by artist that meet the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetMusicAlbumArtists(BaseItem parent, User user, InternalItemsQuery query) { var artists = _libraryManager.GetAlbumArtists(new InternalItemsQuery(user) @@ -1056,6 +1319,13 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } + /// <summary> + /// Returns the music artists meeting the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetMusicArtists(BaseItem parent, User user, InternalItemsQuery query) { var artists = _libraryManager.GetArtists(new InternalItemsQuery(user) @@ -1074,6 +1344,13 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } + /// <summary> + /// Returns the artists tagged as favourite that meet the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetFavoriteArtists(BaseItem parent, User user, InternalItemsQuery query) { var artists = _libraryManager.GetArtists(new InternalItemsQuery(user) @@ -1093,10 +1370,16 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult<ServerItem> GetMusicPlaylists(BaseItem parent, User user, InternalItemsQuery query) + /// <summary> + /// Returns the music playlists meeting the criteria. + /// </summary> + /// <param name="user">The user<see cref="User"/>.</param> + /// <param name="query">The query<see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> + private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query) { query.Parent = null; - query.IncludeItemTypes = new[] { typeof(Playlist).Name }; + query.IncludeItemTypes = new[] { nameof(Playlist) }; query.SetUser(user); query.Recursive = true; @@ -1105,72 +1388,114 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } + /// <summary> + /// Returns the latest music meeting the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetMusicLatest(BaseItem parent, User user, InternalItemsQuery query) { - query.OrderBy = new ValueTuple<string, SortOrder>[] { }; + query.OrderBy = Array.Empty<(string, SortOrder)>(); - var items = _userViewManager.GetLatestItems(new LatestItemsQuery - { - UserId = user.Id, - Limit = 50, - IncludeItemTypes = new[] { typeof(Audio).Name }, - ParentId = parent == null ? Guid.Empty : parent.Id, - GroupItems = true - - }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); + var items = _userViewManager.GetLatestItems( + new LatestItemsQuery + { + UserId = user.Id, + Limit = 50, + IncludeItemTypes = new[] { nameof(Audio) }, + ParentId = parent?.Id ?? Guid.Empty, + GroupItems = true + }, + query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); return ToResult(items); } - private QueryResult<ServerItem> GetNextUp(BaseItem parent, User user, InternalItemsQuery query) + /// <summary> + /// Returns the next up item meeting the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> + private QueryResult<ServerItem> GetNextUp(BaseItem parent, InternalItemsQuery query) { - query.OrderBy = new ValueTuple<string, SortOrder>[] { }; + query.OrderBy = Array.Empty<(string, SortOrder)>(); - var result = _tvSeriesManager.GetNextUp(new NextUpQuery - { - Limit = query.Limit, - StartIndex = query.StartIndex, - UserId = query.User.Id - - }, new[] { parent }, query.DtoOptions); + var result = _tvSeriesManager.GetNextUp( + new NextUpQuery + { + Limit = query.Limit, + StartIndex = query.StartIndex, + UserId = query.User.Id + }, + new[] { parent }, + query.DtoOptions); return ToResult(result); } + /// <summary> + /// Returns the latest tv meeting the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetTvLatest(BaseItem parent, User user, InternalItemsQuery query) { - query.OrderBy = new ValueTuple<string, SortOrder>[] { }; - - var items = _userViewManager.GetLatestItems(new LatestItemsQuery - { - UserId = user.Id, - Limit = 50, - IncludeItemTypes = new[] { typeof(Episode).Name }, - ParentId = parent == null ? Guid.Empty : parent.Id, - GroupItems = false + query.OrderBy = Array.Empty<(string, SortOrder)>(); - }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); + var items = _userViewManager.GetLatestItems( + new LatestItemsQuery + { + UserId = user.Id, + Limit = 50, + IncludeItemTypes = new[] { nameof(Episode) }, + ParentId = parent == null ? Guid.Empty : parent.Id, + GroupItems = false + }, + query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); return ToResult(items); } + /// <summary> + /// Returns the latest movies meeting the criteria. + /// </summary> + /// <param name="parent">The <see cref="BaseItem"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetMovieLatest(BaseItem parent, User user, InternalItemsQuery query) { - query.OrderBy = new ValueTuple<string, SortOrder>[] { }; - - var items = _userViewManager.GetLatestItems(new LatestItemsQuery - { - UserId = user.Id, - Limit = 50, - IncludeItemTypes = new[] { typeof(Movie).Name }, - ParentId = parent == null ? Guid.Empty : parent.Id, - GroupItems = true + query.OrderBy = Array.Empty<(string, SortOrder)>(); - }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); + var items = _userViewManager.GetLatestItems( + new LatestItemsQuery + { + UserId = user.Id, + Limit = 50, + IncludeItemTypes = new[] { nameof(Movie) }, + ParentId = parent?.Id ?? Guid.Empty, + GroupItems = true + }, + query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); return ToResult(items); } + /// <summary> + /// Returns music artist items that meet the criteria. + /// </summary> + /// <param name="item">The <see cref="BaseItem"/>.</param> + /// <param name="parentId">The <see cref="Guid"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="sort">The <see cref="SortCriteria"/>.</param> + /// <param name="startIndex">The start index.</param> + /// <param name="limit">The maximum number to return.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetMusicArtistItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) @@ -1178,7 +1503,7 @@ namespace Emby.Dlna.ContentDirectory Recursive = true, ParentId = parentId, ArtistIds = new[] { item.Id }, - IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, + IncludeItemTypes = new[] { nameof(MusicAlbum) }, Limit = limit, StartIndex = startIndex, DtoOptions = GetDtoOptions() @@ -1191,6 +1516,16 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } + /// <summary> + /// Returns the genre items meeting the criteria. + /// </summary> + /// <param name="item">The <see cref="BaseItem"/>.</param> + /// <param name="parentId">The <see cref="Guid"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="sort">The <see cref="SortCriteria"/>.</param> + /// <param name="startIndex">The start index.</param> + /// <param name="limit">The maximum number to return.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetGenreItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) @@ -1198,7 +1533,11 @@ namespace Emby.Dlna.ContentDirectory Recursive = true, ParentId = parentId, GenreIds = new[] { item.Id }, - IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Series).Name }, + IncludeItemTypes = new[] + { + nameof(Movie), + nameof(Series) + }, Limit = limit, StartIndex = startIndex, DtoOptions = GetDtoOptions() @@ -1211,6 +1550,16 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } + /// <summary> + /// Returns the music genre items meeting the criteria. + /// </summary> + /// <param name="item">The <see cref="BaseItem"/>.</param> + /// <param name="parentId">The <see cref="Guid"/>.</param> + /// <param name="user">The <see cref="User"/>.</param> + /// <param name="sort">The <see cref="SortCriteria"/>.</param> + /// <param name="startIndex">The start index.</param> + /// <param name="limit">The maximum number to return.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetMusicGenreItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) @@ -1218,7 +1567,7 @@ namespace Emby.Dlna.ContentDirectory Recursive = true, ParentId = parentId, GenreIds = new[] { item.Id }, - IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, + IncludeItemTypes = new[] { nameof(MusicAlbum) }, Limit = limit, StartIndex = startIndex, DtoOptions = GetDtoOptions() @@ -1231,7 +1580,12 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult<ServerItem> ToResult(BaseItem[] result) + /// <summary> + /// Converts a <see cref="BaseItem"/> array into a <see cref="QueryResult{ServerItem}"/>. + /// </summary> + /// <param name="result">An array of <see cref="BaseItem"/>.</param> + /// <returns>A <see cref="QueryResult{ServerItem}"/>.</returns> + private static QueryResult<ServerItem> ToResult(BaseItem[] result) { var serverItems = result .Select(i => new ServerItem(i)) @@ -1244,7 +1598,12 @@ namespace Emby.Dlna.ContentDirectory }; } - private QueryResult<ServerItem> ToResult(QueryResult<BaseItem> result) + /// <summary> + /// Converts a <see cref="QueryResult{BaseItem}"/> to a <see cref="QueryResult{ServerItem}"/>. + /// </summary> + /// <param name="result">A <see cref="QueryResult{BaseItem}"/>.</param> + /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> + private static QueryResult<ServerItem> ToResult(QueryResult<BaseItem> result) { var serverItems = result .Items @@ -1258,42 +1617,65 @@ namespace Emby.Dlna.ContentDirectory }; } - private void SetSorting(InternalItemsQuery query, SortCriteria sort, bool isPreSorted) + /// <summary> + /// Sets the sorting method on a query. + /// </summary> + /// <param name="query">The <see cref="InternalItemsQuery"/>.</param> + /// <param name="sort">The <see cref="SortCriteria"/>.</param> + /// <param name="isPreSorted">True if pre-sorted.</param> + private static void SetSorting(InternalItemsQuery query, SortCriteria sort, bool isPreSorted) { - var sortOrders = new List<string>(); - if (!isPreSorted) + if (isPreSorted) { - sortOrders.Add(ItemSortBy.SortName); + query.OrderBy = Array.Empty<(string, SortOrder)>(); + } + else + { + query.OrderBy = new[] { (ItemSortBy.SortName, sort.SortOrder) }; } - - query.OrderBy = sortOrders.Select(i => new ValueTuple<string, SortOrder>(i, sort.SortOrder)).ToArray(); } - private QueryResult<ServerItem> ApplyPaging(QueryResult<ServerItem> result, int? startIndex, int? limit) + /// <summary> + /// Apply paging to a query. + /// </summary> + /// <param name="result">The <see cref="QueryResult{ServerItem}"/>.</param> + /// <param name="startIndex">The start index.</param> + /// <param name="limit">The maximum number to return.</param> + /// <returns>A <see cref="QueryResult{ServerItem}"/>.</returns> + private static QueryResult<ServerItem> ApplyPaging(QueryResult<ServerItem> result, int? startIndex, int? limit) { result.Items = result.Items.Skip(startIndex ?? 0).Take(limit ?? int.MaxValue).ToArray(); return result; } - private ServerItem GetItemFromObjectId(string id, User user) + /// <summary> + /// Retrieves the ServerItem id. + /// </summary> + /// <param name="id">The id<see cref="string"/>.</param> + /// <returns>The <see cref="ServerItem"/>.</returns> + private ServerItem GetItemFromObjectId(string id) { return DidlBuilder.IsIdRoot(id) - ? new ServerItem(_libraryManager.GetUserRootFolder()) - : ParseItemId(id, user); + : ParseItemId(id); } - private ServerItem ParseItemId(string id, User user) + /// <summary> + /// Parses the item id into a <see cref="ServerItem"/>. + /// </summary> + /// <param name="id">The <see cref="string"/>.</param> + /// <returns>The corresponding <see cref="ServerItem"/>.</returns> + private ServerItem ParseItemId(string id) { StubType? stubType = null; // After using PlayTo, MediaMonkey sends a request to the server trying to get item info - const string paramsSrch = "Params="; - var paramsIndex = id.IndexOf(paramsSrch, StringComparison.OrdinalIgnoreCase); + const string ParamsSrch = "Params="; + var paramsIndex = id.IndexOf(ParamsSrch, StringComparison.OrdinalIgnoreCase); if (paramsIndex != -1) { - id = id.Substring(paramsIndex + paramsSrch.Length); + id = id.Substring(paramsIndex + ParamsSrch.Length); var parts = id.Split(';'); id = parts[23]; @@ -1304,8 +1686,8 @@ namespace Emby.Dlna.ContentDirectory { if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase)) { - stubType = (StubType)Enum.Parse(typeof(StubType), name, true); - id = id.Split(new[] { '_' }, 2)[1]; + stubType = Enum.Parse<StubType>(name, true); + id = id.Split('_', 2)[1]; break; } @@ -1321,48 +1703,9 @@ namespace Emby.Dlna.ContentDirectory }; } - _logger.LogError("Error parsing item Id: {id}. Returning user root folder.", id); + Logger.LogError("Error parsing item Id: {Id}. Returning user root folder.", id); return new ServerItem(_libraryManager.GetUserRootFolder()); } } - - internal class ServerItem - { - public BaseItem Item { get; set; } - public StubType? StubType { get; set; } - - public ServerItem(BaseItem item) - { - Item = item; - - if (item is IItemByName && !(item is Folder)) - { - StubType = Dlna.ContentDirectory.StubType.Folder; - } - } - } - - public enum StubType - { - Folder = 0, - Latest = 2, - Playlists = 3, - Albums = 4, - AlbumArtists = 5, - Artists = 6, - Songs = 7, - Genres = 8, - FavoriteSongs = 9, - FavoriteArtists = 10, - FavoriteAlbums = 11, - ContinueWatching = 12, - Movies = 13, - Collections = 14, - Favorites = 15, - NextUp = 16, - Series = 17, - FavoriteSeries = 18, - FavoriteEpisodes = 19 - } } diff --git a/Emby.Dlna/ContentDirectory/ServerItem.cs b/Emby.Dlna/ContentDirectory/ServerItem.cs new file mode 100644 index 000000000..ff30e6e4a --- /dev/null +++ b/Emby.Dlna/ContentDirectory/ServerItem.cs @@ -0,0 +1,36 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities; + +namespace Emby.Dlna.ContentDirectory +{ + /// <summary> + /// Defines the <see cref="ServerItem" />. + /// </summary> + internal class ServerItem + { + /// <summary> + /// Initializes a new instance of the <see cref="ServerItem"/> class. + /// </summary> + /// <param name="item">The <see cref="BaseItem"/>.</param> + public ServerItem(BaseItem item) + { + Item = item; + + if (item is IItemByName && item is not Folder) + { + StubType = Dlna.ContentDirectory.StubType.Folder; + } + } + + /// <summary> + /// Gets or sets the underlying base item. + /// </summary> + public BaseItem Item { get; set; } + + /// <summary> + /// Gets or sets the DLNA item type. + /// </summary> + public StubType? StubType { get; set; } + } +} diff --git a/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs b/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs index e999314fa..7e3db4651 100644 --- a/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs +++ b/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs @@ -3,9 +3,16 @@ using Emby.Dlna.Common; namespace Emby.Dlna.ContentDirectory { - public class ServiceActionListBuilder + /// <summary> + /// Defines the <see cref="ServiceActionListBuilder" />. + /// </summary> + public static class ServiceActionListBuilder { - public IEnumerable<ServiceAction> GetActions() + /// <summary> + /// Returns a list of services that this instance provides. + /// </summary> + /// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns> + public static IEnumerable<ServiceAction> GetActions() { return new[] { @@ -20,6 +27,10 @@ namespace Emby.Dlna.ContentDirectory }; } + /// <summary> + /// Returns the action details for "GetSystemUpdateID". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> private static ServiceAction GetGetSystemUpdateIDAction() { var action = new ServiceAction @@ -37,6 +48,10 @@ namespace Emby.Dlna.ContentDirectory return action; } + /// <summary> + /// Returns the action details for "GetSearchCapabilities". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> private static ServiceAction GetSearchCapabilitiesAction() { var action = new ServiceAction @@ -54,6 +69,10 @@ namespace Emby.Dlna.ContentDirectory return action; } + /// <summary> + /// Returns the action details for "GetSortCapabilities". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> private static ServiceAction GetSortCapabilitiesAction() { var action = new ServiceAction @@ -71,6 +90,10 @@ namespace Emby.Dlna.ContentDirectory return action; } + /// <summary> + /// Returns the action details for "X_GetFeatureList". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> private static ServiceAction GetX_GetFeatureListAction() { var action = new ServiceAction @@ -88,6 +111,10 @@ namespace Emby.Dlna.ContentDirectory return action; } + /// <summary> + /// Returns the action details for "Search". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> private static ServiceAction GetSearchAction() { var action = new ServiceAction @@ -168,7 +195,11 @@ namespace Emby.Dlna.ContentDirectory return action; } - private ServiceAction GetBrowseAction() + /// <summary> + /// Returns the action details for "Browse". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> + private static ServiceAction GetBrowseAction() { var action = new ServiceAction { @@ -248,7 +279,11 @@ namespace Emby.Dlna.ContentDirectory return action; } - private ServiceAction GetBrowseByLetterAction() + /// <summary> + /// Returns the action details for "X_BrowseByLetter". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> + private static ServiceAction GetBrowseByLetterAction() { var action = new ServiceAction { @@ -335,7 +370,11 @@ namespace Emby.Dlna.ContentDirectory return action; } - private ServiceAction GetXSetBookmarkAction() + /// <summary> + /// Returns the action details for "X_SetBookmark". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> + private static ServiceAction GetXSetBookmarkAction() { var action = new ServiceAction { diff --git a/Emby.Dlna/ContentDirectory/StubType.cs b/Emby.Dlna/ContentDirectory/StubType.cs new file mode 100644 index 000000000..187dc1d75 --- /dev/null +++ b/Emby.Dlna/ContentDirectory/StubType.cs @@ -0,0 +1,30 @@ +#pragma warning disable CS1591 + +namespace Emby.Dlna.ContentDirectory +{ + /// <summary> + /// Defines the DLNA item types. + /// </summary> + public enum StubType + { + Folder = 0, + Latest = 2, + Playlists = 3, + Albums = 4, + AlbumArtists = 5, + Artists = 6, + Songs = 7, + Genres = 8, + FavoriteSongs = 9, + FavoriteArtists = 10, + FavoriteAlbums = 11, + ContinueWatching = 12, + Movies = 13, + Collections = 14, + Favorites = 15, + NextUp = 16, + Series = 17, + FavoriteSeries = 18, + FavoriteEpisodes = 19 + } +} diff --git a/Emby.Dlna/ControlRequest.cs b/Emby.Dlna/ControlRequest.cs index afd9a0b87..8ee6325e9 100644 --- a/Emby.Dlna/ControlRequest.cs +++ b/Emby.Dlna/ControlRequest.cs @@ -1,21 +1,25 @@ -using System.Collections.Generic; +#nullable disable + +#pragma warning disable CS1591 + using System.IO; +using Microsoft.AspNetCore.Http; namespace Emby.Dlna { public class ControlRequest { - public IDictionary<string, string> Headers { get; set; } + public ControlRequest(IHeaderDictionary headers) + { + Headers = headers; + } + + public IHeaderDictionary Headers { get; } public Stream InputXml { get; set; } public string TargetServerUuId { get; set; } public string RequestedUrl { get; set; } - - public ControlRequest() - { - Headers = new Dictionary<string, string>(); - } } } diff --git a/Emby.Dlna/ControlResponse.cs b/Emby.Dlna/ControlResponse.cs index d2b79fc58..8b0958842 100644 --- a/Emby.Dlna/ControlResponse.cs +++ b/Emby.Dlna/ControlResponse.cs @@ -1,18 +1,28 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace Emby.Dlna { public class ControlResponse { - public IDictionary<string, string> Headers { get; set; } + public ControlResponse(string xml, bool isSuccessful) + { + Headers = new Dictionary<string, string>(); + Xml = xml; + IsSuccessful = isSuccessful; + } + + public IDictionary<string, string> Headers { get; } public string Xml { get; set; } public bool IsSuccessful { get; set; } - public ControlResponse() + /// <inheritdoc /> + public override string ToString() { - Headers = new Dictionary<string, string>(); + return Xml; } } } diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 605f4f37b..0a84f30c4 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -1,38 +1,45 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Xml; -using Emby.Dlna.Configuration; using Emby.Dlna.ContentDirectory; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; +using Genre = MediaBrowser.Controller.Entities.Genre; +using Movie = MediaBrowser.Controller.Entities.Movies.Movie; +using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; +using Season = MediaBrowser.Controller.Entities.TV.Season; +using Series = MediaBrowser.Controller.Entities.TV.Series; +using XmlAttribute = MediaBrowser.Model.Dlna.XmlAttribute; namespace Emby.Dlna.Didl { public class DidlBuilder { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; - private const string NS_DC = "http://purl.org/dc/elements/1.1/"; - private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/"; - private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/"; + private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; + private const string NsDc = "http://purl.org/dc/elements/1.1/"; + private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/"; + private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/"; private readonly DeviceProfile _profile; private readonly IImageProcessor _imageProcessor; @@ -44,6 +51,7 @@ namespace Emby.Dlna.Didl private readonly IMediaSourceManager _mediaSourceManager; private readonly ILogger _logger; private readonly IMediaEncoder _mediaEncoder; + private readonly ILibraryManager _libraryManager; public DidlBuilder( DeviceProfile profile, @@ -55,7 +63,8 @@ namespace Emby.Dlna.Didl ILocalizationManager localization, IMediaSourceManager mediaSourceManager, ILogger logger, - IMediaEncoder mediaEncoder) + IMediaEncoder mediaEncoder, + ILibraryManager libraryManager) { _profile = profile; _user = user; @@ -67,6 +76,7 @@ namespace Emby.Dlna.Didl _mediaSourceManager = mediaSourceManager; _logger = logger; _mediaEncoder = mediaEncoder; + _libraryManager = libraryManager; } public static string NormalizeDlnaMediaUrl(string url) @@ -74,7 +84,7 @@ namespace Emby.Dlna.Didl return url + "&dlnaheaders=true"; } - public string GetItemDidl(DlnaOptions options, BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo) + public string GetItemDidl(BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo) { var settings = new XmlWriterSettings { @@ -86,23 +96,24 @@ namespace Emby.Dlna.Didl using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8)) { + // If this using are changed to single lines, then write.Flush needs to be appended before the return. using (var writer = XmlWriter.Create(builder, settings)) { - //writer.WriteStartDocument(); + // writer.WriteStartDocument(); - writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); + writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl); - writer.WriteAttributeString("xmlns", "dc", null, NS_DC); - writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); - writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); - //didl.SetAttribute("xmlns:sec", NS_SEC); + writer.WriteAttributeString("xmlns", "dc", null, NsDc); + writer.WriteAttributeString("xmlns", "dlna", null, NsDlna); + writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp); + // didl.SetAttribute("xmlns:sec", NS_SEC); WriteXmlRootAttributes(_profile, writer); - WriteItemElement(options, writer, item, user, context, null, deviceId, filter, streamInfo); + WriteItemElement(writer, item, user, context, null, deviceId, filter, streamInfo); writer.WriteFullEndElement(); - //writer.WriteEndDocument(); + // writer.WriteEndDocument(); } return builder.ToString(); @@ -113,7 +124,7 @@ namespace Emby.Dlna.Didl { foreach (var att in profile.XmlRootAttributes) { - var parts = att.Name.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries); + var parts = att.Name.Split(':', StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 2) { writer.WriteAttributeString(parts[0], parts[1], null, att.Value); @@ -126,7 +137,6 @@ namespace Emby.Dlna.Didl } public void WriteItemElement( - DlnaOptions options, XmlWriter writer, BaseItem item, User user, @@ -138,7 +148,7 @@ namespace Emby.Dlna.Didl { var clientId = GetClientId(item, null); - writer.WriteStartElement(string.Empty, "item", NS_DIDL); + writer.WriteStartElement(string.Empty, "item", NsDidl); writer.WriteAttributeString("restricted", "1"); writer.WriteAttributeString("id", clientId); @@ -158,43 +168,28 @@ namespace Emby.Dlna.Didl AddGeneralProperties(item, null, context, writer, filter); - AddSamsungBookmarkInfo(item, user, writer); + AddSamsungBookmarkInfo(item, user, writer, streamInfo); // refID? // storeAttribute(itemNode, object, ClassProperties.REF_ID, false); - var hasMediaSources = item as IHasMediaSources; - - if (hasMediaSources != null) + if (item is IHasMediaSources) { if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) { - AddAudioResource(options, writer, item, deviceId, filter, streamInfo); + AddAudioResource(writer, item, deviceId, filter, streamInfo); } else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) { - AddVideoResource(options, writer, item, deviceId, filter, streamInfo); + AddVideoResource(writer, item, deviceId, filter, streamInfo); } } - AddCover(item, context, null, writer); + AddCover(item, null, writer); writer.WriteFullEndElement(); } - private string GetMimeType(string input) - { - var mime = MimeTypes.GetMimeType(input); - - // TODO: Instead of being hard-coded here, this should probably be moved into all of the existing profiles - if (string.Equals(mime, "video/mp2t", StringComparison.OrdinalIgnoreCase)) - { - mime = "video/mpeg"; - } - - return mime; - } - - private void AddVideoResource(DlnaOptions options, XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null) + private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null) { if (streamInfo == null) { @@ -213,7 +208,9 @@ namespace Emby.Dlna.Didl var targetWidth = streamInfo.TargetWidth; var targetHeight = streamInfo.TargetHeight; - var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(streamInfo.Container, + var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader( + _profile, + streamInfo.Container, streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(), targetWidth, @@ -238,7 +235,7 @@ namespace Emby.Dlna.Didl foreach (var contentFeature in contentFeatureList) { - AddVideoResource(writer, video, deviceId, filter, contentFeature, streamInfo); + AddVideoResource(writer, filter, contentFeature, streamInfo); } var subtitleProfiles = streamInfo.GetSubtitleProfiles(_mediaEncoder, false, _serverAddress, _accessToken); @@ -285,7 +282,7 @@ namespace Emby.Dlna.Didl } else if (string.Equals(subtitleMode, "smi", StringComparison.OrdinalIgnoreCase)) { - writer.WriteStartElement(string.Empty, "res", NS_DIDL); + writer.WriteStartElement(string.Empty, "res", NsDidl); writer.WriteAttributeString("protocolInfo", "http-get:*:smi/caption:*"); @@ -294,8 +291,11 @@ namespace Emby.Dlna.Didl } else { - writer.WriteStartElement(string.Empty, "res", NS_DIDL); - var protocolInfo = string.Format("http-get:*:text/{0}:*", info.Format.ToLowerInvariant()); + writer.WriteStartElement(string.Empty, "res", NsDidl); + var protocolInfo = string.Format( + CultureInfo.InvariantCulture, + "http-get:*:text/{0}:*", + info.Format.ToLowerInvariant()); writer.WriteAttributeString("protocolInfo", protocolInfo); writer.WriteString(info.Url); @@ -305,9 +305,9 @@ namespace Emby.Dlna.Didl return true; } - private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, string contentFeatures, StreamInfo streamInfo) + private void AddVideoResource(XmlWriter writer, Filter filter, string contentFeatures, StreamInfo streamInfo) { - writer.WriteStartElement(string.Empty, "res", NS_DIDL); + writer.WriteStartElement(string.Empty, "res", NsDidl); var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken)); @@ -315,7 +315,7 @@ namespace Emby.Dlna.Didl if (mediaSource.RunTimeTicks.HasValue) { - writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture)); + writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture)); } if (filter.Contains("res@size")) @@ -326,7 +326,7 @@ namespace Emby.Dlna.Didl if (size.HasValue) { - writer.WriteAttributeString("size", size.Value.ToString(_usCulture)); + writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture)); } } } @@ -340,28 +340,35 @@ namespace Emby.Dlna.Didl if (targetChannels.HasValue) { - writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture)); + writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture)); } if (filter.Contains("res@resolution")) { if (targetWidth.HasValue && targetHeight.HasValue) { - writer.WriteAttributeString("resolution", string.Format("{0}x{1}", targetWidth.Value, targetHeight.Value)); + writer.WriteAttributeString( + "resolution", + string.Format( + CultureInfo.InvariantCulture, + "{0}x{1}", + targetWidth.Value, + targetHeight.Value)); } } if (targetSampleRate.HasValue) { - writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture)); + writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture)); } if (totalBitrate.HasValue) { - writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture)); + writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(CultureInfo.InvariantCulture)); } - var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container, + var mediaProfile = _profile.GetVideoMediaProfile( + streamInfo.Container, streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetAudioBitrate, @@ -381,17 +388,19 @@ namespace Emby.Dlna.Didl streamInfo.TargetVideoCodecTag, streamInfo.IsTargetAVC); - var filename = url.Substring(0, url.IndexOf('?')); + var filename = url.Substring(0, url.IndexOf('?', StringComparison.Ordinal)); var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType) - ? GetMimeType(filename) + ? MimeTypes.GetMimeType(filename) : mediaProfile.MimeType; - writer.WriteAttributeString("protocolInfo", string.Format( - "http-get:*:{0}:{1}", - mimeType, - contentFeatures - )); + writer.WriteAttributeString( + "protocolInfo", + string.Format( + CultureInfo.InvariantCulture, + "http-get:*:{0}:{1}", + mimeType, + contentFeatures)); writer.WriteString(url); @@ -404,56 +413,123 @@ namespace Emby.Dlna.Didl { switch (itemStubType.Value) { - case StubType.Latest: return _localization.GetLocalizedString("Latest"); - case StubType.Playlists: return _localization.GetLocalizedString("Playlists"); - case StubType.AlbumArtists: return _localization.GetLocalizedString("HeaderAlbumArtists"); - case StubType.Albums: return _localization.GetLocalizedString("Albums"); - case StubType.Artists: return _localization.GetLocalizedString("Artists"); - case StubType.Songs: return _localization.GetLocalizedString("Songs"); - case StubType.Genres: return _localization.GetLocalizedString("Genres"); - case StubType.FavoriteAlbums: return _localization.GetLocalizedString("HeaderFavoriteAlbums"); - case StubType.FavoriteArtists: return _localization.GetLocalizedString("HeaderFavoriteArtists"); - case StubType.FavoriteSongs: return _localization.GetLocalizedString("HeaderFavoriteSongs"); + case StubType.Latest: return _localization.GetLocalizedString("Latest"); + case StubType.Playlists: return _localization.GetLocalizedString("Playlists"); + case StubType.AlbumArtists: return _localization.GetLocalizedString("HeaderAlbumArtists"); + case StubType.Albums: return _localization.GetLocalizedString("Albums"); + case StubType.Artists: return _localization.GetLocalizedString("Artists"); + case StubType.Songs: return _localization.GetLocalizedString("Songs"); + case StubType.Genres: return _localization.GetLocalizedString("Genres"); + case StubType.FavoriteAlbums: return _localization.GetLocalizedString("HeaderFavoriteAlbums"); + case StubType.FavoriteArtists: return _localization.GetLocalizedString("HeaderFavoriteArtists"); + case StubType.FavoriteSongs: return _localization.GetLocalizedString("HeaderFavoriteSongs"); case StubType.ContinueWatching: return _localization.GetLocalizedString("HeaderContinueWatching"); - case StubType.Movies: return _localization.GetLocalizedString("Movies"); - case StubType.Collections: return _localization.GetLocalizedString("Collections"); - case StubType.Favorites: return _localization.GetLocalizedString("Favorites"); - case StubType.NextUp: return _localization.GetLocalizedString("HeaderNextUp"); - case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows"); + case StubType.Movies: return _localization.GetLocalizedString("Movies"); + case StubType.Collections: return _localization.GetLocalizedString("Collections"); + case StubType.Favorites: return _localization.GetLocalizedString("Favorites"); + case StubType.NextUp: return _localization.GetLocalizedString("HeaderNextUp"); + case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows"); case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes"); - case StubType.Series: return _localization.GetLocalizedString("Shows"); - default: break; + case StubType.Series: return _localization.GetLocalizedString("Shows"); } } - if (item is Episode episode && context is Season season) + return item is Episode episode + ? GetEpisodeDisplayName(episode, context) + : item.Name; + } + + /// <summary> + /// Gets episode display name appropriate for the given context. + /// </summary> + /// <remarks> + /// If context is a season, this will return a string containing just episode number and name. + /// Otherwise the result will include series nams and season number. + /// </remarks> + /// <param name="episode">The episode.</param> + /// <param name="context">Current context.</param> + /// <returns>Formatted name of the episode.</returns> + private string GetEpisodeDisplayName(Episode episode, BaseItem context) + { + string[] components; + + if (context is Season season) { // This is a special embedded within a season - if (item.ParentIndexNumber.HasValue && item.ParentIndexNumber.Value == 0 + if (episode.ParentIndexNumber.HasValue && episode.ParentIndexNumber.Value == 0 && season.IndexNumber.HasValue && season.IndexNumber.Value != 0) { - return string.Format(_localization.GetLocalizedString("ValueSpecialEpisodeName"), item.Name); + return string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("ValueSpecialEpisodeName"), + episode.Name); } - if (item.IndexNumber.HasValue) - { - var number = item.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); + // inside a season use simple format (ex. '12 - Episode Name') + var epNumberName = GetEpisodeIndexFullName(episode); + components = new[] { epNumberName, episode.Name }; + } + else + { + // outside a season include series and season details (ex. 'TV Show - S05E11 - Episode Name') + var epNumberName = GetEpisodeNumberDisplayName(episode); + components = new[] { episode.SeriesName, epNumberName, episode.Name }; + } - if (episode.IndexNumberEnd.HasValue) - { - number += "-" + episode.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture); - } + return string.Join(" - ", components.Where(NotNullOrWhiteSpace)); + } - return number + " - " + item.Name; + /// <summary> + /// Gets complete episode number. + /// </summary> + /// <param name="episode">The episode.</param> + /// <returns>For single episodes returns just the number. For double episodes - current and ending numbers.</returns> + private string GetEpisodeIndexFullName(Episode episode) + { + var name = string.Empty; + if (episode.IndexNumber.HasValue) + { + name += episode.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); + + if (episode.IndexNumberEnd.HasValue) + { + name += "-" + episode.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture); } } - return item.Name; + return name; } - private void AddAudioResource(DlnaOptions options, XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null) + /// <summary> + /// Gets episode number formatted as 'S##E##'. + /// </summary> + /// <param name="episode">The episode.</param> + /// <returns>Formatted episode number.</returns> + private string GetEpisodeNumberDisplayName(Episode episode) { - writer.WriteStartElement(string.Empty, "res", NS_DIDL); + var name = string.Empty; + var seasonNumber = episode.Season?.IndexNumber; + + if (seasonNumber.HasValue) + { + name = "S" + seasonNumber.Value.ToString("00", CultureInfo.InvariantCulture); + } + + var indexName = GetEpisodeIndexFullName(episode); + + if (!string.IsNullOrWhiteSpace(indexName)) + { + name += "E" + indexName; + } + + return name; + } + + private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s); + + private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null) + { + writer.WriteStartElement(string.Empty, "res", NsDidl); if (streamInfo == null) { @@ -474,7 +550,7 @@ namespace Emby.Dlna.Didl if (mediaSource.RunTimeTicks.HasValue) { - writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture)); + writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture)); } if (filter.Contains("res@size")) @@ -485,7 +561,7 @@ namespace Emby.Dlna.Didl if (size.HasValue) { - writer.WriteAttributeString("size", size.Value.ToString(_usCulture)); + writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture)); } } } @@ -497,33 +573,36 @@ namespace Emby.Dlna.Didl if (targetChannels.HasValue) { - writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture)); + writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture)); } if (targetSampleRate.HasValue) { - writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture)); + writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture)); } if (targetAudioBitrate.HasValue) { - writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture)); + writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(CultureInfo.InvariantCulture)); } - var mediaProfile = _profile.GetAudioMediaProfile(streamInfo.Container, + var mediaProfile = _profile.GetAudioMediaProfile( + streamInfo.Container, streamInfo.TargetAudioCodec.FirstOrDefault(), targetChannels, targetAudioBitrate, targetSampleRate, targetAudioBitDepth); - var filename = url.Substring(0, url.IndexOf('?')); + var filename = url.Substring(0, url.IndexOf('?', StringComparison.Ordinal)); var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType) - ? GetMimeType(filename) + ? MimeTypes.GetMimeType(filename) : mediaProfile.MimeType; - var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container, + var contentFeatures = ContentFeatureBuilder.BuildAudioHeader( + _profile, + streamInfo.Container, streamInfo.TargetAudioCodec.FirstOrDefault(), targetAudioBitrate, targetSampleRate, @@ -533,11 +612,13 @@ namespace Emby.Dlna.Didl streamInfo.RunTimeTicks ?? 0, streamInfo.TranscodeSeekInfo); - writer.WriteAttributeString("protocolInfo", string.Format( - "http-get:*:{0}:{1}", - mimeType, - contentFeatures - )); + writer.WriteAttributeString( + "protocolInfo", + string.Format( + CultureInfo.InvariantCulture, + "http-get:*:{0}:{1}", + mimeType, + contentFeatures)); writer.WriteString(url); @@ -545,29 +626,22 @@ namespace Emby.Dlna.Didl } public static bool IsIdRoot(string id) - { - if (string.IsNullOrWhiteSpace(id) + => string.IsNullOrWhiteSpace(id) || string.Equals(id, "0", StringComparison.OrdinalIgnoreCase) // Samsung sometimes uses 1 as root - || string.Equals(id, "1", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - return false; - } + || string.Equals(id, "1", StringComparison.OrdinalIgnoreCase); public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null) { - writer.WriteStartElement(string.Empty, "container", NS_DIDL); + writer.WriteStartElement(string.Empty, "container", NsDidl); writer.WriteAttributeString("restricted", "1"); writer.WriteAttributeString("searchable", "1"); - writer.WriteAttributeString("childCount", childCount.ToString(_usCulture)); + writer.WriteAttributeString("childCount", childCount.ToString(CultureInfo.InvariantCulture)); var clientId = GetClientId(folder, stubType); - if (string.Equals(requestedId, "0")) + if (string.Equals(requestedId, "0", StringComparison.Ordinal)) { writer.WriteAttributeString("id", "0"); writer.WriteAttributeString("parentID", "-1"); @@ -596,19 +670,19 @@ namespace Emby.Dlna.Didl AddGeneralProperties(folder, stubType, context, writer, filter); - AddCover(folder, context, stubType, writer); + AddCover(folder, stubType, writer); writer.WriteFullEndElement(); } - private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer) + private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer, StreamInfo streamInfo) { if (!item.SupportsPositionTicksResume || item is Folder) { return; } - MediaBrowser.Model.Dlna.XmlAttribute secAttribute = null; + XmlAttribute secAttribute = null; foreach (var attribute in _profile.XmlRootAttributes) { if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase)) @@ -625,24 +699,28 @@ namespace Emby.Dlna.Didl } var userdata = _userDataManager.GetUserData(user, item); + var playbackPositionTicks = (streamInfo != null && streamInfo.StartPositionTicks > 0) ? streamInfo.StartPositionTicks : userdata.PlaybackPositionTicks; - if (userdata.PlaybackPositionTicks > 0) + if (playbackPositionTicks > 0) { - var elementValue = string.Format("BM={0}", Convert.ToInt32(TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds).ToString(_usCulture)); + var elementValue = string.Format( + CultureInfo.InvariantCulture, + "BM={0}", + Convert.ToInt32(TimeSpan.FromTicks(playbackPositionTicks).TotalSeconds)); AddValue(writer, "sec", "dcmInfo", elementValue, secAttribute.Value); } } /// <summary> - /// Adds fields used by both items and folders + /// Adds fields used by both items and folders. /// </summary> private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter) { // Don't filter on dc:title because not all devices will include it in the filter // MediaMonkey for example won't display content without a title - //if (filter.Contains("dc:title")) + // if (filter.Contains("dc:title")) { - AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC); + AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NsDc); } WriteObjectClass(writer, item, itemStubType); @@ -651,7 +729,7 @@ namespace Emby.Dlna.Didl { if (item.PremiereDate.HasValue) { - AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o"), NS_DC); + AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc); } } @@ -659,16 +737,16 @@ namespace Emby.Dlna.Didl { foreach (var genre in item.Genres) { - AddValue(writer, "upnp", "genre", genre, NS_UPNP); + AddValue(writer, "upnp", "genre", genre, NsUpnp); } } foreach (var studio in item.Studios) { - AddValue(writer, "upnp", "publisher", studio, NS_UPNP); + AddValue(writer, "upnp", "publisher", studio, NsUpnp); } - if (!(item is Folder)) + if (item is not Folder) { if (filter.Contains("dc:description")) { @@ -676,27 +754,29 @@ namespace Emby.Dlna.Didl if (!string.IsNullOrWhiteSpace(desc)) { - AddValue(writer, "dc", "description", desc, NS_DC); + AddValue(writer, "dc", "description", desc, NsDc); } } - //if (filter.Contains("upnp:longDescription")) - //{ + + // if (filter.Contains("upnp:longDescription")) + // { // if (!string.IsNullOrWhiteSpace(item.Overview)) // { - // AddValue(writer, "upnp", "longDescription", item.Overview, NS_UPNP); + // AddValue(writer, "upnp", "longDescription", item.Overview, NsUpnp); // } - //} + // } } if (!string.IsNullOrEmpty(item.OfficialRating)) { if (filter.Contains("dc:rating")) { - AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC); + AddValue(writer, "dc", "rating", item.OfficialRating, NsDc); } + if (filter.Contains("upnp:rating")) { - AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP); + AddValue(writer, "upnp", "rating", item.OfficialRating, NsUpnp); } } @@ -708,7 +788,7 @@ namespace Emby.Dlna.Didl // More types here // http://oss.linn.co.uk/repos/Public/LibUpnpCil/DidlLite/UpnpAv/Test/TestDidlLite.cs - writer.WriteStartElement("upnp", "class", NS_UPNP); + writer.WriteStartElement("upnp", "class", NsUpnp); if (item.IsDisplayedAsFolder || stubType.HasValue) { @@ -781,52 +861,50 @@ namespace Emby.Dlna.Didl private void AddPeople(BaseItem item, XmlWriter writer) { - //var types = new[] - //{ - // PersonType.Director, - // PersonType.Writer, - // PersonType.Producer, - // PersonType.Composer, - // "Creator" - //}; - - //var people = _libraryManager.GetPeople(item); - - //var index = 0; - - //// Seeing some LG models locking up due content with large lists of people - //// The actual issue might just be due to processing a more metadata than it can handle - //var limit = 6; + if (!item.SupportsPeople) + { + return; + } - //foreach (var actor in people) - //{ - // var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase)) - // ?? PersonType.Actor; + var types = new[] + { + PersonType.Director, + PersonType.Writer, + PersonType.Producer, + PersonType.Composer, + "creator" + }; - // AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NS_UPNP); + // Seeing some LG models locking up due content with large lists of people + // The actual issue might just be due to processing a more metadata than it can handle + var people = _libraryManager.GetPeople( + new InternalPeopleQuery + { + ItemId = item.Id, + Limit = 6 + }); - // index++; + foreach (var actor in people) + { + var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase)) + ?? PersonType.Actor; - // if (index >= limit) - // { - // break; - // } - //} + AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp); + } } private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter) { AddCommonFields(item, itemStubType, context, writer, filter); - var hasArtists = item as IHasArtist; var hasAlbumArtists = item as IHasAlbumArtist; - if (hasArtists != null) + if (item is IHasArtist hasArtists) { foreach (var artist in hasArtists.Artists) { - AddValue(writer, "upnp", "artist", artist, NS_UPNP); - AddValue(writer, "dc", "creator", artist, NS_DC); + AddValue(writer, "upnp", "artist", artist, NsUpnp); + AddValue(writer, "dc", "creator", artist, NsDc); // If it doesn't support album artists (musicvideo), then tag as both if (hasAlbumArtists == null) @@ -846,16 +924,16 @@ namespace Emby.Dlna.Didl if (!string.IsNullOrWhiteSpace(item.Album)) { - AddValue(writer, "upnp", "album", item.Album, NS_UPNP); + AddValue(writer, "upnp", "album", item.Album, NsUpnp); } if (item.IndexNumber.HasValue) { - AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP); + AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp); if (item is Episode) { - AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP); + AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp); } } } @@ -864,7 +942,7 @@ namespace Emby.Dlna.Didl { try { - writer.WriteStartElement("upnp", "artist", NS_UPNP); + writer.WriteStartElement("upnp", "artist", NsUpnp); writer.WriteAttributeString("role", "AlbumArtist"); writer.WriteString(name); @@ -873,7 +951,7 @@ namespace Emby.Dlna.Didl } catch (XmlException ex) { - _logger.LogError(ex, "Error adding xml value: {value}", name); + _logger.LogError(ex, "Error adding xml value: {Value}", name); } } @@ -885,11 +963,11 @@ namespace Emby.Dlna.Didl } catch (XmlException ex) { - _logger.LogError(ex, "Error adding xml value: {value}", value); + _logger.LogError(ex, "Error adding xml value: {Value}", value); } } - private void AddCover(BaseItem item, BaseItem context, StubType? stubType, XmlWriter writer) + private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer) { ImageDownloadInfo imageInfo = GetImageInfo(item); @@ -898,16 +976,29 @@ namespace Emby.Dlna.Didl return; } - var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg"); + // TODO: Remove these default values + var albumArtUrlInfo = GetImageUrl( + imageInfo, + _profile.MaxAlbumArtWidth ?? 10000, + _profile.MaxAlbumArtHeight ?? 10000, + "jpg"); + + writer.WriteStartElement("upnp", "albumArtURI", NsUpnp); + if (!string.IsNullOrEmpty(_profile.AlbumArtPn)) + { + writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn); + } - writer.WriteStartElement("upnp", "albumArtURI", NS_UPNP); - writer.WriteAttributeString("dlna", "profileID", NS_DLNA, _profile.AlbumArtPn); - writer.WriteString(albumartUrlInfo.Url); + writer.WriteString(albumArtUrlInfo.url); writer.WriteFullEndElement(); - // TOOD: Remove these default values - var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg"); - writer.WriteElementString("upnp", "icon", NS_UPNP, iconUrlInfo.Url); + // TODO: Remove these default values + var iconUrlInfo = GetImageUrl( + imageInfo, + _profile.MaxIconWidth ?? 48, + _profile.MaxIconHeight ?? 48, + "jpg"); + writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url); if (!_profile.EnableAlbumArtInDidl) { @@ -921,8 +1012,6 @@ namespace Emby.Dlna.Didl } } - AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN"); - if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) { AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG"); @@ -931,19 +1020,12 @@ namespace Emby.Dlna.Didl AddImageResElement(item, writer, 4096, 4096, "png", "PNG_LRG"); AddImageResElement(item, writer, 160, 160, "png", "PNG_TN"); } - } - private void AddEmbeddedImageAsCover(string name, XmlWriter writer) - { - writer.WriteStartElement("upnp", "albumArtURI", NS_UPNP); - writer.WriteAttributeString("dlna", "profileID", NS_DLNA, _profile.AlbumArtPn); - writer.WriteString(_serverAddress + "/Dlna/icons/people480.jpg"); - writer.WriteFullEndElement(); - - writer.WriteElementString("upnp", "icon", NS_UPNP, _serverAddress + "/Dlna/icons/people48.jpg"); + AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN"); } - private void AddImageResElement(BaseItem item, + private void AddImageResElement( + BaseItem item, XmlWriter writer, int maxWidth, int maxHeight, @@ -959,25 +1041,28 @@ namespace Emby.Dlna.Didl var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, format); - writer.WriteStartElement(string.Empty, "res", NS_DIDL); + writer.WriteStartElement(string.Empty, "res", NsDidl); // Images must have a reported size or many clients (Bubble upnp), will only use the first thumbnail // rather than using a larger one when available - var width = albumartUrlInfo.Width ?? maxWidth; - var height = albumartUrlInfo.Height ?? maxHeight; + var width = albumartUrlInfo.width ?? maxWidth; + var height = albumartUrlInfo.height ?? maxHeight; - var contentFeatures = new ContentFeatureBuilder(_profile) - .BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn); + var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn); - writer.WriteAttributeString("protocolInfo", string.Format( - "http-get:*:{0}:{1}", - GetMimeType("file." + format), - contentFeatures - )); + writer.WriteAttributeString( + "protocolInfo", + string.Format( + CultureInfo.InvariantCulture, + "http-get:*:{0}:{1}", + MimeTypes.GetMimeType("file." + format), + contentFeatures)); - writer.WriteAttributeString("resolution", string.Format("{0}x{1}", width, height)); + writer.WriteAttributeString( + "resolution", + string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height)); - writer.WriteString(albumartUrlInfo.Url); + writer.WriteString(albumartUrlInfo.url); writer.WriteFullEndElement(); } @@ -988,10 +1073,12 @@ namespace Emby.Dlna.Didl { return GetImageInfo(item, ImageType.Primary); } + if (item.HasImage(ImageType.Thumb)) { return GetImageInfo(item, ImageType.Thumb); } + if (item.HasImage(ImageType.Backdrop)) { if (item is Channel) @@ -1000,19 +1087,58 @@ namespace Emby.Dlna.Didl } } - item = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary)); + // For audio tracks without art use album art if available. + if (item is Audio audioItem) + { + var album = audioItem.AlbumEntity; + return album != null && album.HasImage(ImageType.Primary) + ? GetImageInfo(album, ImageType.Primary) + : null; + } - if (item != null) + // Don't look beyond album/playlist level. Metadata service may assign an image from a different album/show to the parent folder. + if (item is MusicAlbum || item is Playlist) { - if (item.HasImage(ImageType.Primary)) - { - return GetImageInfo(item, ImageType.Primary); - } + return null; + } + + // For other item types check parents, but be aware that image retrieved from a parent may be not suitable for this media item. + var parentWithImage = GetFirstParentWithImageBelowUserRoot(item); + if (parentWithImage != null) + { + return GetImageInfo(parentWithImage, ImageType.Primary); } return null; } + private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item) + { + if (item == null) + { + return null; + } + + if (item.HasImage(ImageType.Primary)) + { + return item; + } + + var parent = item.GetParent(); + if (parent is UserRootFolder) + { + return null; + } + + // terminate in case we went past user root folder (unlikely?) + if (parent is Folder folder && folder.IsRoot) + { + return null; + } + + return GetFirstParentWithImageBelowUserRoot(parent); + } + private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type) { var imageInfo = item.GetImageInfo(type, 0); @@ -1032,29 +1158,15 @@ namespace Emby.Dlna.Didl if (width == 0 || height == 0) { - //_imageProcessor.GetImageSize(item, imageInfo); width = null; height = null; } - else if (width == -1 || height == -1) { width = null; height = null; } - //try - //{ - // var size = _imageProcessor.GetImageSize(imageInfo); - - // width = size.Width; - // height = size.Height; - //} - //catch - //{ - - //} - var inputFormat = (Path.GetExtension(imageInfo.Path) ?? string.Empty) .TrimStart('.') .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase); @@ -1071,30 +1183,6 @@ namespace Emby.Dlna.Didl }; } - private class ImageDownloadInfo - { - internal Guid ItemId; - internal string ImageTag; - internal ImageType Type; - - internal int? Width; - internal int? Height; - - internal bool IsDirectStream; - - internal string Format; - - internal ItemImageInfo ItemImageInfo; - } - - private class ImageUrlInfo - { - internal string Url; - - internal int? Width; - internal int? Height; - } - public static string GetClientId(BaseItem item, StubType? stubType) { return GetClientId(item.Id, stubType); @@ -1102,7 +1190,7 @@ namespace Emby.Dlna.Didl public static string GetClientId(Guid idValue, StubType? stubType) { - var id = idValue.ToString("N"); + var id = idValue.ToString("N", CultureInfo.InvariantCulture); if (stubType.HasValue) { @@ -1112,11 +1200,13 @@ namespace Emby.Dlna.Didl return id; } - private ImageUrlInfo GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format) + private (string url, int? width, int? height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format) { - var url = string.Format("{0}/Items/{1}/Images/{2}/0/{3}/{4}/{5}/{6}/0/0", + var url = string.Format( + CultureInfo.InvariantCulture, + "{0}/Items/{1}/Images/{2}/0/{3}/{4}/{5}/{6}/0/0", _serverAddress, - info.ItemId.ToString("N"), + info.ItemId.ToString("N", CultureInfo.InvariantCulture), info.Type, info.ImageTag, format, @@ -1130,8 +1220,7 @@ namespace Emby.Dlna.Didl if (width.HasValue && height.HasValue) { - var newSize = DrawingUtils.Resize( - new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight); + var newSize = DrawingUtils.Resize(new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight); width = newSize.Width; height = newSize.Height; @@ -1148,12 +1237,26 @@ namespace Emby.Dlna.Didl // just lie info.IsDirectStream = true; - return new ImageUrlInfo - { - Url = url, - Width = width, - Height = height - }; + return (url, width, height); + } + + private class ImageDownloadInfo + { + internal Guid ItemId { get; set; } + + internal string ImageTag { get; set; } + + internal ImageType Type { get; set; } + + internal int? Width { get; set; } + + internal int? Height { get; set; } + + internal bool IsDirectStream { get; set; } + + internal string Format { get; set; } + + internal ItemImageInfo ItemImageInfo { get; set; } } } } diff --git a/Emby.Dlna/Didl/Filter.cs b/Emby.Dlna/Didl/Filter.cs index a0e67870e..d703f043e 100644 --- a/Emby.Dlna/Didl/Filter.cs +++ b/Emby.Dlna/Didl/Filter.cs @@ -1,5 +1,6 @@ +#pragma warning disable CS1591 + using System; -using MediaBrowser.Model.Extensions; namespace Emby.Dlna.Didl { @@ -11,21 +12,18 @@ namespace Emby.Dlna.Didl public Filter() : this("*") { - } public Filter(string filter) { - _all = StringHelper.EqualsIgnoreCase(filter, "*"); + _all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase); - _fields = (filter ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + _fields = (filter ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries); } public bool Contains(string field) { - // Don't bother with this. Some clients (media monkey) use the filter and then don't display very well when very little data comes back. - return true; - //return _all || ListHelper.ContainsIgnoreCase(_fields, field); + return _all || Array.Exists(_fields, x => x.Equals(field, StringComparison.OrdinalIgnoreCase)); } } } diff --git a/Emby.Dlna/Didl/StringWriterWithEncoding.cs b/Emby.Dlna/Didl/StringWriterWithEncoding.cs index c3c4bd393..b66f53ece 100644 --- a/Emby.Dlna/Didl/StringWriterWithEncoding.cs +++ b/Emby.Dlna/Didl/StringWriterWithEncoding.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable CA1305 + using System; using System.IO; using System.Text; @@ -6,7 +9,7 @@ namespace Emby.Dlna.Didl { public class StringWriterWithEncoding : StringWriter { - private readonly Encoding _encoding; + private readonly Encoding? _encoding; public StringWriterWithEncoding() { @@ -27,7 +30,6 @@ namespace Emby.Dlna.Didl { } - public StringWriterWithEncoding(Encoding encoding) { _encoding = encoding; @@ -51,6 +53,6 @@ namespace Emby.Dlna.Didl _encoding = encoding; } - public override Encoding Encoding => (null == _encoding) ? base.Encoding : _encoding; + public override Encoding Encoding => _encoding ?? base.Encoding; } } diff --git a/Emby.Dlna/DlnaConfigurationFactory.cs b/Emby.Dlna/DlnaConfigurationFactory.cs new file mode 100644 index 000000000..6cc6b73a0 --- /dev/null +++ b/Emby.Dlna/DlnaConfigurationFactory.cs @@ -0,0 +1,23 @@ +#pragma warning disable CS1591 + +using System.Collections.Generic; +using Emby.Dlna.Configuration; +using MediaBrowser.Common.Configuration; + +namespace Emby.Dlna +{ + public class DlnaConfigurationFactory : IConfigurationFactory + { + public IEnumerable<ConfigurationStore> GetConfigurations() + { + return new[] + { + new ConfigurationStore + { + Key = "dlna", + ConfigurationType = typeof(DlnaOptions) + } + }; + } + } +} diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index f53d27451..f37d2d7d7 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -1,12 +1,17 @@ +#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; +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; @@ -15,9 +20,10 @@ using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Reflection; using MediaBrowser.Model.Serialization; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; namespace Emby.Dlna { @@ -26,10 +32,10 @@ namespace Emby.Dlna private readonly IApplicationPaths _appPaths; private readonly IXmlSerializer _xmlSerializer; private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; - private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger<DlnaManager> _logger; private readonly IServerApplicationHost _appHost; - private readonly IAssemblyInfo _assemblyInfo; + 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); @@ -38,24 +44,24 @@ namespace Emby.Dlna IFileSystem fileSystem, IApplicationPaths appPaths, ILoggerFactory loggerFactory, - IJsonSerializer jsonSerializer, - IServerApplicationHost appHost, - IAssemblyInfo assemblyInfo) + IServerApplicationHost appHost) { _xmlSerializer = xmlSerializer; _fileSystem = fileSystem; _appPaths = appPaths; - _logger = loggerFactory.CreateLogger("Dlna"); - _jsonSerializer = jsonSerializer; + _logger = loggerFactory.CreateLogger<DlnaManager>(); _appHost = appHost; - _assemblyInfo = assemblyInfo; } + 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(); + await ExtractSystemProfilesAsync().ConfigureAwait(false); LoadProfiles(); } catch (Exception ex) @@ -85,15 +91,16 @@ namespace Emby.Dlna .Select(i => i.Item2) .ToList(); } - } + /// <inheritdoc /> public DeviceProfile GetDefaultProfile() { return new DefaultProfile(); } - public DeviceProfile GetProfile(DeviceIdentification deviceInfo) + /// <inheritdoc /> + public DeviceProfile? GetProfile(DeviceIdentification deviceInfo) { if (deviceInfo == null) { @@ -103,13 +110,13 @@ namespace Emby.Dlna var profile = GetProfiles() .FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification)); - if (profile != null) + if (profile == null) { - _logger.LogDebug("Found matching device profile: {0}", profile.Name); + LogUnmatchedProfile(deviceInfo); } else { - LogUnmatchedProfile(deviceInfo); + _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name); } return profile; @@ -120,83 +127,57 @@ namespace Emby.Dlna var builder = new StringBuilder(); builder.AppendLine("No matching device profile found. The default will need to be used."); - builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty)); - builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty)); - builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty)); - builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty)); - builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty)); - builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty)); - builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty)); - builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty)); - builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty)); + builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName); + builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer); + builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl); + builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription); + builder.Append("ModelName: ").AppendLine(profile.ModelName); + builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber); + builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl); + builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber); _logger.LogInformation(builder.ToString()); } - private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo) + /// <summary> + /// Attempts to match a device with a profile. + /// Rules: + /// - If the profile field has no value, the field matches irregardless 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) { - if (!string.IsNullOrEmpty(profileInfo.DeviceDescription)) - { - if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription)) - return false; - } - - if (!string.IsNullOrEmpty(profileInfo.FriendlyName)) - { - if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)) - return false; - } - - if (!string.IsNullOrEmpty(profileInfo.Manufacturer)) - { - if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)) - return false; - } - - if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl)) - { - if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)) - return false; - } - - if (!string.IsNullOrEmpty(profileInfo.ModelDescription)) - { - if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)) - return false; - } - - if (!string.IsNullOrEmpty(profileInfo.ModelName)) - { - if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName)) - return false; - } - - if (!string.IsNullOrEmpty(profileInfo.ModelNumber)) - { - if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)) - return false; - } + 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); + } - if (!string.IsNullOrEmpty(profileInfo.ModelUrl)) + private bool IsRegexOrSubstringMatch(string input, string pattern) + { + if (string.IsNullOrEmpty(pattern)) { - if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)) - return false; + // In profile identification: An empty pattern matches anything. + return true; } - if (!string.IsNullOrEmpty(profileInfo.SerialNumber)) + if (string.IsNullOrEmpty(input)) { - if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber)) - return false; + // The profile contains a value, and the device doesn't. + return false; } - return true; - } - - private bool IsRegexMatch(string input, string pattern) - { try { - return Regex.IsMatch(input, pattern); + return input.Equals(pattern, StringComparison.OrdinalIgnoreCase) + || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } catch (ArgumentException ex) { @@ -205,37 +186,33 @@ namespace Emby.Dlna } } - public DeviceProfile GetProfile(IDictionary<string, string> headers) + /// <inheritdoc /> + public DeviceProfile? GetProfile(IHeaderDictionary headers) { if (headers == null) { throw new ArgumentNullException(nameof(headers)); } - // Convert to case insensitive - headers = new Dictionary<string, string>(headers, StringComparer.OrdinalIgnoreCase); - var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification)); - - if (profile != null) + if (profile == null) { - _logger.LogDebug("Found matching device profile: {0}", profile.Name); + _logger.LogDebug("No matching device profile found. {@Headers}", headers); } else { - var headerString = string.Join(", ", headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray()); - _logger.LogDebug("No matching device profile found. {0}", headerString); + _logger.LogDebug("Found matching device profile: {0}", profile.Name); } return profile; } - private bool IsMatch(IDictionary<string, string> headers, DeviceIdentification profileInfo) + private bool IsMatch(IHeaderDictionary headers, DeviceIdentification profileInfo) { return profileInfo.Headers.Any(i => IsMatch(headers, i)); } - private bool IsMatch(IDictionary<string, string> headers, HttpHeaderInfo header) + private bool IsMatch(IHeaderDictionary headers, HttpHeaderInfo header) { // Handle invalid user setup if (string.IsNullOrEmpty(header.Name)) @@ -243,15 +220,15 @@ namespace Emby.Dlna return false; } - if (headers.TryGetValue(header.Name, out string value)) + 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.IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1; - //_logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch); + 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); @@ -263,10 +240,6 @@ namespace Emby.Dlna return false; } - private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user"); - - private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system"); - private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type) { try @@ -278,19 +251,19 @@ namespace Emby.Dlna return xmlFies .Select(i => ParseProfileFile(i, type)) .Where(i => i != null) - .ToList(); + .ToList()!; // We just filtered out all the nulls } catch (IOException) { - return new List<DeviceProfile>(); + return Array.Empty<DeviceProfile>(); } } - private DeviceProfile ParseProfileFile(string path, DeviceProfileType type) + private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type) { lock (_profiles) { - if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile> profileTuple)) + if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple)) { return profileTuple.Item2; } @@ -303,7 +276,7 @@ namespace Emby.Dlna profile = ReserializeProfile(tempProfile); - profile.Id = path.ToLowerInvariant().GetMD5().ToString("N"); + profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile); @@ -318,14 +291,20 @@ namespace Emby.Dlna } } - public DeviceProfile GetProfile(string id) + /// <inheritdoc /> + public DeviceProfile? GetProfile(string id) { if (string.IsNullOrEmpty(id)) { throw new ArgumentNullException(nameof(id)); } - var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase)); + var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase)); + + if (info == null) + { + return null; + } return ParseProfileFile(info.Path, info.Info.Type); } @@ -342,6 +321,7 @@ namespace Emby.Dlna } } + /// <inheritdoc /> public IEnumerable<DeviceProfileInfo> GetProfileInfos() { return GetProfileInfosInternal().Select(i => i.Info); @@ -349,17 +329,14 @@ namespace Emby.Dlna private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type) { - return new InternalProfileInfo - { - Path = file.FullName, - - Info = new DeviceProfileInfo + return new InternalProfileInfo( + new DeviceProfileInfo { - Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N"), + Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture), Name = _fileSystem.GetFileNameWithoutExtension(file), Type = type - } - }; + }, + file.FullName); } private async Task ExtractSystemProfilesAsync() @@ -368,25 +345,33 @@ namespace Emby.Dlna var systemProfilesPath = SystemProfilesPath; - foreach (var name in _assemblyInfo.GetManifestResourceNames(GetType()) - .Where(i => i.StartsWith(namespaceName)) - .ToList()) + foreach (var name in _assembly.GetManifestResourceNames()) { - var filename = Path.GetFileName(name).Substring(namespaceName.Length); + if (!name.StartsWith(namespaceName, StringComparison.Ordinal)) + { + continue; + } - var path = Path.Combine(systemProfilesPath, filename); + var path = Path.Join( + systemProfilesPath, + Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length)); - using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), name)) + // The stream should exist as we just got its name from GetManifestResourceNames + using (var stream = _assembly.GetManifestResourceStream(name)!) { + var length = stream.Length; var fileInfo = _fileSystem.GetFileInfo(path); - if (!fileInfo.Exists || fileInfo.Length != stream.Length) + if (!fileInfo.Exists || fileInfo.Length != length) { Directory.CreateDirectory(systemProfilesPath); - using (var fileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + var fileOptions = AsyncFile.WriteOptions; + fileOptions.Mode = FileMode.CreateNew; + fileOptions.PreallocationSize = length; + using (var fileStream = new FileStream(path, fileOptions)) { - await stream.CopyToAsync(fileStream); + await stream.CopyToAsync(fileStream).ConfigureAwait(false); } } } @@ -396,6 +381,7 @@ namespace Emby.Dlna Directory.CreateDirectory(UserProfilesPath); } + /// <inheritdoc /> public void DeleteProfile(string id) { var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase)); @@ -413,6 +399,7 @@ namespace Emby.Dlna } } + /// <inheritdoc /> public void CreateProfile(DeviceProfile profile) { profile = ReserializeProfile(profile); @@ -428,7 +415,8 @@ namespace Emby.Dlna SaveProfile(profile, path, DeviceProfileType.User); } - public void UpdateProfile(DeviceProfile profile) + /// <inheritdoc /> + public void UpdateProfile(string profileId, DeviceProfile profile) { profile = ReserializeProfile(profile); @@ -436,12 +424,13 @@ namespace Emby.Dlna { 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, profile.Id, StringComparison.OrdinalIgnoreCase)); + var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase)); var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml"; var path = Path.Combine(UserProfilesPath, newFilename); @@ -461,6 +450,7 @@ namespace Emby.Dlna { _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile); } + SerializeToXml(profile, path); } @@ -471,10 +461,10 @@ namespace Emby.Dlna /// <summary> /// Recreates the object using serialization, to ensure it's not a subclass. - /// If it's a subclass it may not serlialize properly to xml (different root element tag name) + /// If it's a subclass it may not serialize properly to xml (different root element tag name). /// </summary> - /// <param name="profile"></param> - /// <returns></returns> + /// <param name="profile">The device profile.</param> + /// <returns>The re-serialized device profile.</returns> private DeviceProfile ReserializeProfile(DeviceProfile profile) { if (profile.GetType() == typeof(DeviceProfile)) @@ -482,42 +472,56 @@ namespace Emby.Dlna return profile; } - var json = _jsonSerializer.SerializeToString(profile); + var json = JsonSerializer.Serialize(profile, _jsonOptions); - return _jsonSerializer.DeserializeFromString<DeviceProfile>(json); + // Output can't be null if the input isn't null + return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!; } - class InternalProfileInfo + /// <inheritdoc /> + public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress) { - internal DeviceProfileInfo Info { get; set; } - internal string Path { get; set; } - } - - public string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId, string serverAddress) - { - var profile = GetProfile(headers) ?? - GetDefaultProfile(); + var profile = GetDefaultProfile(); var serverId = _appHost.SystemId; return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml(); } - public ImageStream GetIcon(string filename) + /// <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 == null) + { + return null; + } - return new ImageStream + return new ImageStream(stream) { - Format = format, - Stream = _assemblyInfo.GetManifestResourceStream(GetType(), resource) + Format = format }; } + + private class InternalProfileInfo + { + internal InternalProfileInfo(DeviceProfileInfo info, string path) + { + Info = info; + Path = path; + } + + internal DeviceProfileInfo Info { get; } + + internal string Path { get; } + } } + /* class DlnaProfileEntryPoint : IServerEntryPoint { @@ -539,7 +543,7 @@ namespace Emby.Dlna private void DumpProfiles() { - DeviceProfile[] list = new [] + DeviceProfile[] list = new[] { new SamsungSmartTvProfile(), new XboxOneProfile(), @@ -563,9 +567,9 @@ namespace Emby.Dlna new Foobar2000Profile(), new SharpSmartTvProfile(), new MediaMonkeyProfile(), - //new Windows81Profile(), - //new WindowsMediaCenterProfile(), - //new WindowsPhoneProfile(), + // new Windows81Profile(), + // new WindowsMediaCenterProfile(), + // new WindowsPhoneProfile(), new DirectTvProfile(), new DishHopperJoeyProfile(), new DefaultProfile(), diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index 71ded2337..c8332e44e 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{805844AB-E92F-45E6-9D99-4F6D48D129A5}</ProjectGuid> + </PropertyGroup> + <ItemGroup> <Compile Include="..\SharedVersion.cs" /> </ItemGroup> @@ -12,10 +17,18 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>net6.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> + <!-- Code Analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + <ItemGroup> <EmbeddedResource Include="Images\logo120.jpg" /> <EmbeddedResource Include="Images\logo120.png" /> @@ -58,4 +71,8 @@ <EmbeddedResource Include="Profiles\Xml\Xbox One.xml" /> </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" /> + </ItemGroup> + </Project> diff --git a/Emby.Dlna/EventSubscriptionResponse.cs b/Emby.Dlna/EventSubscriptionResponse.cs index 6dc1aacf4..635d2c47a 100644 --- a/Emby.Dlna/EventSubscriptionResponse.cs +++ b/Emby.Dlna/EventSubscriptionResponse.cs @@ -1,17 +1,22 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace Emby.Dlna { public class EventSubscriptionResponse { - public string Content { get; set; } - public string ContentType { get; set; } - - public Dictionary<string, string> Headers { get; set; } - - public EventSubscriptionResponse() + public EventSubscriptionResponse(string content, string contentType) { + Content = content; + ContentType = contentType; Headers = new Dictionary<string, string>(); } + + public string Content { get; set; } + + public string ContentType { get; set; } + + public Dictionary<string, string> Headers { get; } } } diff --git a/Emby.Dlna/Eventing/EventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs index b4ff3ec1d..d17e23871 100644 --- a/Emby.Dlna/Eventing/EventManager.cs +++ b/Emby.Dlna/Eventing/DlnaEventManager.cs @@ -1,75 +1,76 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Net.Http; +using System.Net.Mime; using System.Text; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using Microsoft.Extensions.Logging; namespace Emby.Dlna.Eventing { - public class EventManager : IEventManager + public class DlnaEventManager : IDlnaEventManager { private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions = new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase); private readonly ILogger _logger; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; - public EventManager(ILogger logger, IHttpClient httpClient) + public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _logger = logger; } public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl) { var subscription = GetSubscription(subscriptionId, false); - - int timeoutSeconds; - - // Remove logging for now because some devices are sending this very frequently - // TODO re-enable with dlna debug logging setting - //_logger.LogDebug("Renewing event subscription for {0} with timeout of {1} to {2}", - // subscription.NotificationType, - // timeout, - // subscription.CallbackUrl); - if (subscription != null) { subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300; - timeoutSeconds = subscription.TimeoutSeconds; + int timeoutSeconds = subscription.TimeoutSeconds; subscription.SubscriptionTime = DateTime.UtcNow; - } - else - { - timeoutSeconds = 300; + + _logger.LogDebug( + "Renewing event subscription for {0} with timeout of {1} to {2}", + subscription.NotificationType, + timeoutSeconds, + subscription.CallbackUrl); + + return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds); } - return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds); + return new EventSubscriptionResponse(string.Empty, "text/plain"); } public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl) { var timeout = ParseTimeout(requestedTimeoutString) ?? 300; - var id = "uuid:" + Guid.NewGuid().ToString("N"); + var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - // Remove logging for now because some devices are sending this very frequently - // TODO re-enable with dlna debug logging setting - //_logger.LogDebug("Creating event subscription for {0} with timeout of {1} to {2}", - // notificationType, - // timeout, - // callbackUrl); + _logger.LogDebug( + "Creating event subscription for {0} with timeout of {1} to {2}", + notificationType, + timeout, + callbackUrl); _subscriptions.TryAdd(id, new EventSubscription { Id = id, CallbackUrl = callbackUrl, SubscriptionTime = DateTime.UtcNow, - TimeoutSeconds = timeout + TimeoutSeconds = timeout, + NotificationType = notificationType }); return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout); @@ -80,9 +81,7 @@ namespace Emby.Dlna.Eventing if (!string.IsNullOrEmpty(header)) { // Starts with SECOND- - header = header.Split('-').Last(); - - if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val)) + if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) { return val; } @@ -95,26 +94,17 @@ namespace Emby.Dlna.Eventing { _logger.LogDebug("Cancelling event subscription {0}", subscriptionId); - _subscriptions.TryRemove(subscriptionId, out EventSubscription sub); + _subscriptions.TryRemove(subscriptionId, out _); - return new EventSubscriptionResponse - { - Content = string.Empty, - ContentType = "text/plain" - }; + return new EventSubscriptionResponse(string.Empty, "text/plain"); } - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds) { - var response = new EventSubscriptionResponse - { - Content = string.Empty, - ContentType = "text/plain" - }; + var response = new EventSubscriptionResponse(string.Empty, "text/plain"); response.Headers["SID"] = subscriptionId; - response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString; + response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString; return response; } @@ -153,33 +143,30 @@ namespace Emby.Dlna.Eventing builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">"); foreach (var key in stateVariables.Keys) { - builder.Append("<e:property>"); - builder.Append("<" + key + ">"); - builder.Append(stateVariables[key]); - builder.Append("</" + key + ">"); - builder.Append("</e:property>"); + builder.Append("<e:property>") + .Append('<') + .Append(key) + .Append('>') + .Append(stateVariables[key]) + .Append("</") + .Append(key) + .Append('>') + .Append("</e:property>"); } - builder.Append("</e:propertyset>"); - var options = new HttpRequestOptions - { - RequestContent = builder.ToString(), - RequestContentType = "text/xml", - Url = subscription.CallbackUrl, - BufferContent = false - }; + builder.Append("</e:propertyset>"); - options.RequestHeaders.Add("NT", subscription.NotificationType); - options.RequestHeaders.Add("NTS", "upnp:propchange"); - options.RequestHeaders.Add("SID", subscription.Id); - options.RequestHeaders.Add("SEQ", subscription.TriggerCount.ToString(_usCulture)); + using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl); + options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml); + options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType); + options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange"); + options.Headers.TryAddWithoutValidation("SID", subscription.Id); + options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture)); try { - using (await _httpClient.SendAsync(options, "NOTIFY").ConfigureAwait(false)) - { - - } + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); } catch (OperationCanceledException) { diff --git a/Emby.Dlna/Eventing/EventSubscription.cs b/Emby.Dlna/Eventing/EventSubscription.cs index eb8781e0c..4fd7f8169 100644 --- a/Emby.Dlna/Eventing/EventSubscription.cs +++ b/Emby.Dlna/Eventing/EventSubscription.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; namespace Emby.Dlna.Eventing @@ -5,14 +9,19 @@ namespace Emby.Dlna.Eventing public class EventSubscription { public string Id { get; set; } + public string CallbackUrl { get; set; } + public string NotificationType { get; set; } public DateTime SubscriptionTime { get; set; } + public int TimeoutSeconds { get; set; } public long TriggerCount { get; set; } + public bool IsExpired => SubscriptionTime.AddSeconds(TimeoutSeconds) >= DateTime.UtcNow; + public void IncrementTriggerCount() { if (TriggerCount == long.MaxValue) @@ -22,7 +31,5 @@ namespace Emby.Dlna.Eventing TriggerCount++; } - - public bool IsExpired => SubscriptionTime.AddSeconds(TimeoutSeconds) >= DateTime.UtcNow; } } diff --git a/Emby.Dlna/IConnectionManager.cs b/Emby.Dlna/IConnectionManager.cs index 855c4454d..9f643a9e6 100644 --- a/Emby.Dlna/IConnectionManager.cs +++ b/Emby.Dlna/IConnectionManager.cs @@ -1,7 +1,8 @@ +#pragma warning disable CS1591 namespace Emby.Dlna { - public interface IConnectionManager : IEventManager, IUpnpService + public interface IConnectionManager : IDlnaEventManager, IUpnpService { } } diff --git a/Emby.Dlna/IContentDirectory.cs b/Emby.Dlna/IContentDirectory.cs index b54a17c00..10f4d6386 100644 --- a/Emby.Dlna/IContentDirectory.cs +++ b/Emby.Dlna/IContentDirectory.cs @@ -1,7 +1,8 @@ +#pragma warning disable CS1591 namespace Emby.Dlna { - public interface IContentDirectory : IEventManager, IUpnpService + public interface IContentDirectory : IDlnaEventManager, IUpnpService { } } diff --git a/Emby.Dlna/IEventManager.cs b/Emby.Dlna/IDlnaEventManager.cs index 4f67a1b9b..33cf0896b 100644 --- a/Emby.Dlna/IEventManager.cs +++ b/Emby.Dlna/IDlnaEventManager.cs @@ -1,22 +1,33 @@ +#pragma warning disable CS1591 namespace Emby.Dlna { - public interface IEventManager + public interface IDlnaEventManager { /// <summary> /// Cancels the event subscription. /// </summary> /// <param name="subscriptionId">The subscription identifier.</param> + /// <returns>The response.</returns> EventSubscriptionResponse CancelEventSubscription(string subscriptionId); /// <summary> /// Renews the event subscription. /// </summary> + /// <param name="subscriptionId">The subscription identifier.</param> + /// <param name="notificationType">The notification type.</param> + /// <param name="requestedTimeoutString">The requested timeout as a sting.</param> + /// <param name="callbackUrl">The callback url.</param> + /// <returns>The response.</returns> EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl); /// <summary> /// Creates the event subscription. /// </summary> + /// <param name="notificationType">The notification type.</param> + /// <param name="requestedTimeoutString">The requested timeout as a sting.</param> + /// <param name="callbackUrl">The callback url.</param> + /// <returns>The response.</returns> EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl); } } diff --git a/Emby.Dlna/IMediaReceiverRegistrar.cs b/Emby.Dlna/IMediaReceiverRegistrar.cs index 5dde01f58..43e934b53 100644 --- a/Emby.Dlna/IMediaReceiverRegistrar.cs +++ b/Emby.Dlna/IMediaReceiverRegistrar.cs @@ -1,7 +1,8 @@ +#pragma warning disable CS1591 namespace Emby.Dlna { - public interface IMediaReceiverRegistrar : IEventManager, IUpnpService + public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService { } } diff --git a/Emby.Dlna/IUpnpService.cs b/Emby.Dlna/IUpnpService.cs index ab8aa4619..9e7859567 100644 --- a/Emby.Dlna/IUpnpService.cs +++ b/Emby.Dlna/IUpnpService.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +#pragma warning disable CS1591 + +using System.Threading.Tasks; namespace Emby.Dlna { @@ -7,15 +9,14 @@ namespace Emby.Dlna /// <summary> /// Gets the content directory XML. /// </summary> - /// <param name="headers">The headers.</param> /// <returns>System.String.</returns> - string GetServiceXml(IDictionary<string, string> headers); + string GetServiceXml(); /// <summary> /// Processes the control request. /// </summary> /// <param name="request">The request.</param> /// <returns>ControlResponse.</returns> - ControlResponse ProcessControlRequest(ControlRequest request); + Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request); } } diff --git a/Emby.Dlna/Images/logo120.png b/Emby.Dlna/Images/logo120.png Binary files differindex 321c47729..14f6c8d5f 100644 --- a/Emby.Dlna/Images/logo120.png +++ b/Emby.Dlna/Images/logo120.png diff --git a/Emby.Dlna/Images/logo240.jpg b/Emby.Dlna/Images/logo240.jpg Binary files differindex da1cb5e07..78a27f1b5 100644 --- a/Emby.Dlna/Images/logo240.jpg +++ b/Emby.Dlna/Images/logo240.jpg diff --git a/Emby.Dlna/Images/logo240.png b/Emby.Dlna/Images/logo240.png Binary files differindex 64c828129..ff50314d4 100644 --- a/Emby.Dlna/Images/logo240.png +++ b/Emby.Dlna/Images/logo240.png diff --git a/Emby.Dlna/Images/logo48.png b/Emby.Dlna/Images/logo48.png Binary files differindex b8fc14564..d6b5fd1df 100644 --- a/Emby.Dlna/Images/logo48.png +++ b/Emby.Dlna/Images/logo48.png diff --git a/Emby.Dlna/Images/people48.png b/Emby.Dlna/Images/people48.png Binary files differindex 7fb25e6b3..dae5f6057 100644 --- a/Emby.Dlna/Images/people48.png +++ b/Emby.Dlna/Images/people48.png diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index a20006578..5d252d8dc 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -1,9 +1,17 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Globalization; using System.Linq; -using System.Threading; +using System.Net.Http; +using System.Net.Sockets; using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; +using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -19,23 +27,19 @@ using MediaBrowser.Controller.TV; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Net; -using MediaBrowser.Model.System; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; using Rssdp; using Rssdp.Infrastructure; namespace Emby.Dlna.Main { - public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup + public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup { private readonly IServerConfigurationManager _config; - private readonly ILogger _logger; + private readonly ILogger<DlnaEntryPoint> _logger; private readonly IServerApplicationHost _appHost; - - private PlayToManager _manager; private readonly ISessionManager _sessionManager; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IDlnaManager _dlnaManager; @@ -44,28 +48,25 @@ namespace Emby.Dlna.Main private readonly ILocalizationManager _localization; private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IDeviceDiscovery _deviceDiscovery; - - private SsdpDevicePublisher _Publisher; - private readonly ISocketFactory _socketFactory; - private readonly IEnvironmentInfo _environmentInfo; private readonly INetworkManager _networkManager; + private readonly object _syncLock = new object(); + private readonly NetworkConfiguration _netConfig; + private readonly bool _disabled; + private PlayToManager _manager; + private SsdpDevicePublisher _publisher; private ISsdpCommunicationsServer _communicationsServer; - internal IContentDirectory ContentDirectory { get; private set; } - internal IConnectionManager ConnectionManager { get; private set; } - internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; } + private bool _disposed; - public static DlnaEntryPoint Current; - - public DlnaEntryPoint(IServerConfigurationManager config, + public DlnaEntryPoint( + IServerConfigurationManager config, ILoggerFactory loggerFactory, IServerApplicationHost appHost, ISessionManager sessionManager, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, @@ -76,16 +77,14 @@ namespace Emby.Dlna.Main IDeviceDiscovery deviceDiscovery, IMediaEncoder mediaEncoder, ISocketFactory socketFactory, - IEnvironmentInfo environmentInfo, INetworkManager networkManager, IUserViewManager userViewManager, - IXmlReaderSettingsFactory xmlReaderSettingsFactory, ITVSeriesManager tvSeriesManager) { _config = config; _appHost = appHost; _sessionManager = sessionManager; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _libraryManager = libraryManager; _userManager = userManager; _dlnaManager = dlnaManager; @@ -96,41 +95,74 @@ namespace Emby.Dlna.Main _deviceDiscovery = deviceDiscovery; _mediaEncoder = mediaEncoder; _socketFactory = socketFactory; - _environmentInfo = environmentInfo; _networkManager = networkManager; - _logger = loggerFactory.CreateLogger("Dlna"); + _logger = loggerFactory.CreateLogger<DlnaEntryPoint>(); - ContentDirectory = new ContentDirectory.ContentDirectory(dlnaManager, + ContentDirectory = new ContentDirectory.ContentDirectoryService( + dlnaManager, userDataManager, imageProcessor, libraryManager, config, userManager, - _logger, - httpClient, + loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(), + httpClientFactory, localizationManager, mediaSourceManager, userViewManager, mediaEncoder, - xmlReaderSettingsFactory, tvSeriesManager); - ConnectionManager = new ConnectionManager.ConnectionManager(dlnaManager, config, _logger, httpClient, xmlReaderSettingsFactory); + ConnectionManager = new ConnectionManager.ConnectionManagerService( + dlnaManager, + config, + loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(), + httpClientFactory); - MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(_logger, httpClient, config, xmlReaderSettingsFactory); + MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService( + loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(), + httpClientFactory, + config); Current = this; + + _netConfig = config.GetConfiguration<NetworkConfiguration>("network"); + _disabled = appHost.ListenWithHttps && _netConfig.RequireHttps; + + if (_disabled && _config.GetDlnaConfiguration().EnableServer) + { + _logger.LogError("The DLNA specification does not support HTTPS."); + } } + public static DlnaEntryPoint Current { get; private set; } + + /// <summary> + /// Gets a value indicating whether the dlna server is enabled. + /// </summary> + public static bool Enabled { get; private set; } + + public IContentDirectory ContentDirectory { get; private set; } + + public IConnectionManager ConnectionManager { get; private set; } + + public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; } + public async Task RunAsync() { await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); + if (_disabled) + { + // No use starting as dlna won't work, as we're running purely on HTTPS. + return; + } + ReloadComponents(); - _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; + _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; } - void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) + private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) { if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) { @@ -138,15 +170,16 @@ namespace Emby.Dlna.Main } } - private async void ReloadComponents() + private void ReloadComponents() { var options = _config.GetDlnaConfiguration(); + Enabled = options.EnableServer; StartSsdpHandler(); if (options.EnableServer) { - await StartDevicePublisher(options).ConfigureAwait(false); + StartDevicePublisher(options); } else { @@ -169,7 +202,8 @@ namespace Emby.Dlna.Main { if (_communicationsServer == null) { - var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows; + var enableMultiSocketBinding = OperatingSystem.IsWindows() || + OperatingSystem.IsLinux(); _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding) { @@ -194,7 +228,10 @@ namespace Emby.Dlna.Main { try { - ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer); + if (communicationsServer != null) + { + ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer); + } } catch (Exception ex) { @@ -215,27 +252,34 @@ namespace Emby.Dlna.Main } } - public async Task StartDevicePublisher(Configuration.DlnaOptions options) + public void StartDevicePublisher(Configuration.DlnaOptions options) { if (!options.BlastAliveMessages) { return; } - if (_Publisher != null) + if (_publisher != null) { return; } try { - _Publisher = new SsdpDevicePublisher(_communicationsServer, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion); - _Publisher.LogFunction = LogMessage; - _Publisher.SupportPnpRootDevice = false; + _publisher = new SsdpDevicePublisher( + _communicationsServer, + _networkManager, + MediaBrowser.Common.System.OperatingSystem.Name, + Environment.OSVersion.VersionString, + _config.GetDlnaConfiguration().SendOnlyMatchedHost) + { + LogFunction = LogMessage, + SupportPnpRootDevice = false + }; - await RegisterServerEndpoints().ConfigureAwait(false); + RegisterServerEndpoints(); - _Publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); + _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); } catch (Exception ex) { @@ -243,31 +287,53 @@ namespace Emby.Dlna.Main } } - private async Task RegisterServerEndpoints() + private void RegisterServerEndpoints() { - var addresses = (await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false)).ToList(); - var udn = CreateUuid(_appHost.SystemId); + var descriptorUri = "/dlna/" + udn + "/description.xml"; + + var bindAddresses = NetworkManager.CreateCollection( + _networkManager.GetInternalBindAddresses() + .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0))); - foreach (var address in addresses) + if (bindAddresses.Count == 0) { - // TODO: Remove this condition on platforms that support it - //if (address.AddressFamily == IpAddressFamily.InterNetworkV6) - //{ - // continue; - //} + // No interfaces returned, so use loopback. + bindAddresses = _networkManager.GetLoopbacks(); + } + + foreach (IPNetAddress address in bindAddresses) + { + if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + // Not supporting IPv6 right now + continue; + } + + // Limit to LAN addresses only + if (!_networkManager.IsInLocalNetwork(address)) + { + continue; + } var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); - var descriptorUri = "/dlna/" + udn + "/description.xml"; - var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri); + var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri); + if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl)) + { + // DLNA will only work over http, so we must reset to http:// : {port}. + uri.Scheme = "http"; + uri.Port = _netConfig.HttpServerPortNumber; + } var device = new SsdpRootDevice { - CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info. - Location = uri, // Must point to the URL that serves your devices UPnP description document. + CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. + Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document. + Address = address.Address, + PrefixLength = address.PrefixLength, FriendlyName = "Jellyfin", Manufacturer = "Jellyfin", ModelName = "Jellyfin Server", @@ -276,13 +342,13 @@ namespace Emby.Dlna.Main }; SetProperies(device, fullService); - _Publisher.AddDevice(device); + _publisher.AddDevice(device); var embeddedDevices = new[] { "urn:schemas-upnp-org:service:ContentDirectory:1", "urn:schemas-upnp-org:service:ConnectionManager:1", - //"urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" + // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" }; foreach (var subDevice in embeddedDevices) @@ -308,12 +374,13 @@ namespace Emby.Dlna.Main { guid = text.GetMD5(); } - return guid.ToString("N"); + + return guid.ToString("N", CultureInfo.InvariantCulture); } private void SetProperies(SsdpDevice device, string fullDeviceType) { - var service = fullDeviceType.Replace("urn:", string.Empty).Replace(":1", string.Empty); + var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase); var serviceParts = service.Split(':'); @@ -324,7 +391,6 @@ namespace Emby.Dlna.Main device.DeviceType = serviceParts[2]; } - private readonly object _syncLock = new object(); private void StartPlayToManager() { lock (_syncLock) @@ -336,7 +402,8 @@ namespace Emby.Dlna.Main try { - _manager = new PlayToManager(_logger, + _manager = new PlayToManager( + _logger, _sessionManager, _libraryManager, _userManager, @@ -344,7 +411,7 @@ namespace Emby.Dlna.Main _appHost, _imageProcessor, _deviceDiscovery, - _httpClient, + _httpClientFactory, _config, _userDataManager, _localization, @@ -375,13 +442,30 @@ namespace Emby.Dlna.Main { _logger.LogError(ex, "Error disposing PlayTo manager"); } + _manager = null; } } } + public void DisposeDevicePublisher() + { + if (_publisher != null) + { + _logger.LogInformation("Disposing SsdpDevicePublisher"); + _publisher.Dispose(); + _publisher = null; + } + } + + /// <inheritdoc /> public void Dispose() { + if (_disposed) + { + return; + } + DisposeDevicePublisher(); DisposePlayToManager(); DisposeDeviceDiscovery(); @@ -397,16 +481,8 @@ namespace Emby.Dlna.Main ConnectionManager = null; MediaReceiverRegistrar = null; Current = null; - } - public void DisposeDevicePublisher() - { - if (_Publisher != null) - { - _logger.LogInformation("Disposing SsdpDevicePublisher"); - _Publisher.Dispose(); - _Publisher = null; - } + _disposed = true; } } } diff --git a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs index ae8175f4a..d8fb12742 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs @@ -1,44 +1,58 @@ using System; using System.Collections.Generic; +using System.Xml; using Emby.Dlna.Service; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace Emby.Dlna.MediaReceiverRegistrar { + /// <summary> + /// Defines the <see cref="ControlHandler" />. + /// </summary> public class ControlHandler : BaseControlHandler { - protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams) + /// <summary> + /// Initializes a new instance of the <see cref="ControlHandler"/> class. + /// </summary> + /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param> + /// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param> + public ControlHandler(IServerConfigurationManager config, ILogger logger) + : base(config, logger) { - if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase)) - return HandleIsAuthorized(); - if (string.Equals(methodName, "IsValidated", StringComparison.OrdinalIgnoreCase)) - return HandleIsValidated(); - - throw new ResourceNotFoundException("Unexpected control request name: " + methodName); } - private static IEnumerable<KeyValuePair<string, string>> HandleIsAuthorized() + /// <inheritdoc /> + protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter) { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) + if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase)) { - { "Result", "1" } - }; - } + HandleIsAuthorized(xmlWriter); + return; + } - private static IEnumerable<KeyValuePair<string, string>> HandleIsValidated() - { - return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) + if (string.Equals(methodName, "IsValidated", StringComparison.OrdinalIgnoreCase)) { - { "Result", "1" } - }; - } + HandleIsValidated(xmlWriter); + return; + } - public ControlHandler(IServerConfigurationManager config, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory) - : base(config, logger, xmlReaderSettingsFactory) - { + throw new ResourceNotFoundException("Unexpected control request name: " + methodName); } + + /// <summary> + /// Records that the handle is authorized in the xml stream. + /// </summary> + /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> + private static void HandleIsAuthorized(XmlWriter xmlWriter) + => xmlWriter.WriteElementString("Result", "1"); + + /// <summary> + /// Records that the handle is validated in the xml stream. + /// </summary> + /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> + private static void HandleIsValidated(XmlWriter xmlWriter) + => xmlWriter.WriteElementString("Result", "1"); } } diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs deleted file mode 100644 index 2b84528ea..000000000 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using Emby.Dlna.Service; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.Xml; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.MediaReceiverRegistrar -{ - public class MediaReceiverRegistrar : BaseService, IMediaReceiverRegistrar - { - private readonly IServerConfigurationManager _config; - protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory; - - public MediaReceiverRegistrar(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IXmlReaderSettingsFactory xmlReaderSettingsFactory) - : base(logger, httpClient) - { - _config = config; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; - } - - public string GetServiceXml(IDictionary<string, string> headers) - { - return new MediaReceiverRegistrarXmlBuilder().GetXml(); - } - - public ControlResponse ProcessControlRequest(ControlRequest request) - { - return new ControlHandler( - _config, - Logger, XmlReaderSettingsFactory) - .ProcessControlRequest(request); - } - } -} diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs new file mode 100644 index 000000000..a5aae515c --- /dev/null +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs @@ -0,0 +1,46 @@ +using System.Net.Http; +using System.Threading.Tasks; +using Emby.Dlna.Service; +using MediaBrowser.Controller.Configuration; +using Microsoft.Extensions.Logging; + +namespace Emby.Dlna.MediaReceiverRegistrar +{ + /// <summary> + /// Defines the <see cref="MediaReceiverRegistrarService" />. + /// </summary> + public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar + { + private readonly IServerConfigurationManager _config; + + /// <summary> + /// Initializes a new instance of the <see cref="MediaReceiverRegistrarService"/> class. + /// </summary> + /// <param name="logger">The <see cref="ILogger{MediaReceiverRegistrarService}"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param> + /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param> + public MediaReceiverRegistrarService( + ILogger<MediaReceiverRegistrarService> logger, + IHttpClientFactory httpClientFactory, + IServerConfigurationManager config) + : base(logger, httpClientFactory) + { + _config = config; + } + + /// <inheritdoc /> + public string GetServiceXml() + { + return MediaReceiverRegistrarXmlBuilder.GetXml(); + } + + /// <inheritdoc /> + public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request) + { + return new ControlHandler( + _config, + Logger) + .ProcessControlRequestAsync(request); + } + } +} diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs index 641341185..f3789a791 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs @@ -4,73 +4,85 @@ using Emby.Dlna.Service; namespace Emby.Dlna.MediaReceiverRegistrar { - public class MediaReceiverRegistrarXmlBuilder + /// <summary> + /// Defines the <see cref="MediaReceiverRegistrarXmlBuilder" />. + /// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-drmnd/5d37515e-7a63-4709-8258-8fd4e0ed4482. + /// </summary> + public static class MediaReceiverRegistrarXmlBuilder { - public string GetXml() + /// <summary> + /// Retrieves an XML description of the X_MS_MediaReceiverRegistrar. + /// </summary> + /// <returns>An XML representation of this service.</returns> + public static string GetXml() { - return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(), - GetStateVariables()); + return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables()); } + /// <summary> + /// The a list of all the state variables for this invocation. + /// </summary> + /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns> private static IEnumerable<StateVariable> GetStateVariables() { - var list = new List<StateVariable>(); - - list.Add(new StateVariable + var list = new List<StateVariable> { - Name = "AuthorizationGrantedUpdateID", - DataType = "ui4", - SendsEvents = true - }); + new StateVariable + { + Name = "AuthorizationGrantedUpdateID", + DataType = "ui4", + SendsEvents = true + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_DeviceID", - DataType = "string", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_DeviceID", + DataType = "string", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "AuthorizationDeniedUpdateID", - DataType = "ui4", - SendsEvents = true - }); + new StateVariable + { + Name = "AuthorizationDeniedUpdateID", + DataType = "ui4", + SendsEvents = true + }, - list.Add(new StateVariable - { - Name = "ValidationSucceededUpdateID", - DataType = "ui4", - SendsEvents = true - }); + new StateVariable + { + Name = "ValidationSucceededUpdateID", + DataType = "ui4", + SendsEvents = true + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_RegistrationRespMsg", - DataType = "bin.base64", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_RegistrationRespMsg", + DataType = "bin.base64", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_RegistrationReqMsg", - DataType = "bin.base64", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_RegistrationReqMsg", + DataType = "bin.base64", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "ValidationRevokedUpdateID", - DataType = "ui4", - SendsEvents = true - }); + new StateVariable + { + Name = "ValidationRevokedUpdateID", + DataType = "ui4", + SendsEvents = true + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_Result", - DataType = "int", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_Result", + DataType = "int", + SendsEvents = false + } + }; return list; } diff --git a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs index 86429f605..56788ae22 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs @@ -3,9 +3,16 @@ using Emby.Dlna.Common; namespace Emby.Dlna.MediaReceiverRegistrar { - public class ServiceActionListBuilder + /// <summary> + /// Defines the <see cref="ServiceActionListBuilder" />. + /// </summary> + public static class ServiceActionListBuilder { - public IEnumerable<ServiceAction> GetActions() + /// <summary> + /// Returns a list of services that this instance provides. + /// </summary> + /// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns> + public static IEnumerable<ServiceAction> GetActions() { return new[] { @@ -19,6 +26,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar }; } + /// <summary> + /// Returns the action details for "IsValidated". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> private static ServiceAction GetIsValidated() { var action = new ServiceAction @@ -41,6 +52,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar return action; } + /// <summary> + /// Returns the action details for "IsAuthorized". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> private static ServiceAction GetIsAuthorized() { var action = new ServiceAction @@ -63,6 +78,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar return action; } + /// <summary> + /// Returns the action details for "RegisterDevice". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> private static ServiceAction GetRegisterDevice() { var action = new ServiceAction @@ -85,6 +104,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar return action; } + /// <summary> + /// Returns the action details for "GetValidationSucceededUpdateID". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> private static ServiceAction GetGetValidationSucceededUpdateID() { var action = new ServiceAction @@ -101,7 +124,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar return action; } - private ServiceAction GetGetAuthorizationDeniedUpdateID() + /// <summary> + /// Returns the action details for "GetGetAuthorizationDeniedUpdateID". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> + private static ServiceAction GetGetAuthorizationDeniedUpdateID() { var action = new ServiceAction { @@ -117,7 +144,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar return action; } - private ServiceAction GetGetValidationRevokedUpdateID() + /// <summary> + /// Returns the action details for "GetValidationRevokedUpdateID". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> + private static ServiceAction GetGetValidationRevokedUpdateID() { var action = new ServiceAction { @@ -133,7 +164,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar return action; } - private ServiceAction GetGetAuthorizationGrantedUpdateID() + /// <summary> + /// Returns the action details for "GetAuthorizationGrantedUpdateID". + /// </summary> + /// <returns>The <see cref="ServiceAction"/>.</returns> + private static ServiceAction GetGetAuthorizationGrantedUpdateID() { var action = new ServiceAction { diff --git a/Emby.Dlna/PlayTo/CurrentIdEventArgs.cs b/Emby.Dlna/PlayTo/CurrentIdEventArgs.cs deleted file mode 100644 index fdf435bcf..000000000 --- a/Emby.Dlna/PlayTo/CurrentIdEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Emby.Dlna.PlayTo -{ - public class CurrentIdEventArgs : EventArgs - { - public string Id { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index b62c5e1d4..0b2288000 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -1,40 +1,65 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Net.Http; +using System.Security; using System.Threading; using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; using Emby.Dlna.Common; -using Emby.Dlna.Server; using Emby.Dlna.Ssdp; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using Microsoft.Extensions.Logging; namespace Emby.Dlna.PlayTo { public class Device : IDisposable { - #region Fields & Properties + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + + private readonly object _timerLock = new object(); private Timer _timer; + private int _muteVol; + private int _volume; + private DateTime _lastVolumeRefresh; + private bool _volumeRefreshActive; + private int _connectFailureCount; + private bool _disposed; + + public Device(DeviceInfo deviceProperties, IHttpClientFactory httpClientFactory, ILogger logger) + { + Properties = deviceProperties; + _httpClientFactory = httpClientFactory; + _logger = logger; + } + + public event EventHandler<PlaybackStartEventArgs> PlaybackStart; + + public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress; + + public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped; + + public event EventHandler<MediaChangedEventArgs> MediaChanged; public DeviceInfo Properties { get; set; } - private int _muteVol; public bool IsMuted { get; set; } - private int _volume; - public int Volume { get { - RefreshVolumeIfNeeded(); + RefreshVolumeIfNeeded().GetAwaiter().GetResult(); return _volume; } + set => _volume = value; } @@ -42,29 +67,21 @@ namespace Emby.Dlna.PlayTo public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0); - public TRANSPORTSTATE TransportState { get; private set; } + public TransportState TransportState { get; private set; } - public bool IsPlaying => TransportState == TRANSPORTSTATE.PLAYING; + public bool IsPlaying => TransportState == TransportState.Playing; - public bool IsPaused => TransportState == TRANSPORTSTATE.PAUSED || TransportState == TRANSPORTSTATE.PAUSED_PLAYBACK; + public bool IsPaused => TransportState == TransportState.Paused || TransportState == TransportState.PausedPlayback; - public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED; + public bool IsStopped => TransportState == TransportState.Stopped; - #endregion + public Action OnDeviceUnavailable { get; set; } - private readonly IHttpClient _httpClient; - private readonly ILogger _logger; - private readonly IServerConfigurationManager _config; + private TransportCommands AvCommands { get; set; } - public Action OnDeviceUnavailable { get; set; } + private TransportCommands RendererCommands { get; set; } - public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config) - { - Properties = deviceProperties; - _httpClient = httpClient; - _logger = logger; - _config = config; - } + public UBaseObject CurrentMediaInfo { get; private set; } public void Start() { @@ -72,26 +89,24 @@ namespace Emby.Dlna.PlayTo _timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite); } - private DateTime _lastVolumeRefresh; - private bool _volumeRefreshActive; - private void RefreshVolumeIfNeeded() + private Task RefreshVolumeIfNeeded() { - if (!_volumeRefreshActive) - { - return; - } - - if (DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5)) + if (_volumeRefreshActive + && DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5)) { _lastVolumeRefresh = DateTime.UtcNow; - RefreshVolume(CancellationToken.None); + return RefreshVolume(); } + + return Task.CompletedTask; } - private async void RefreshVolume(CancellationToken cancellationToken) + private async Task RefreshVolume(CancellationToken cancellationToken = default) { if (_disposed) + { return; + } try { @@ -104,7 +119,6 @@ namespace Emby.Dlna.PlayTo } } - private readonly object _timerLock = new object(); private void RestartTimer(bool immediate = false) { lock (_timerLock) @@ -139,8 +153,6 @@ namespace Emby.Dlna.PlayTo } } - #region Commanding - public Task VolumeDown(CancellationToken cancellationToken) { var sendVolume = Math.Max(Volume - 5, 0); @@ -207,9 +219,11 @@ namespace Emby.Dlna.PlayTo { var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); + var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); if (command == null) + { return false; + } var service = GetServiceRenderingControl(); @@ -221,7 +235,13 @@ namespace Emby.Dlna.PlayTo _logger.LogDebug("Setting mute"); var value = mute ? 1 : 0; - await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value)) + await new SsdpHttpClient(_httpClientFactory) + .SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + rendererCommands.BuildPost(command, service.ServiceType, value), + cancellationToken: cancellationToken) .ConfigureAwait(false); IsMuted = mute; @@ -230,15 +250,20 @@ namespace Emby.Dlna.PlayTo } /// <summary> - /// Sets volume on a scale of 0-100 + /// Sets volume on a scale of 0-100. /// </summary> + /// <param name="value">The volume on a scale of 0-100.</param> + /// <param name="cancellationToken">The cancellation token to cancel operation.</param> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> public async Task SetVolume(int value, CancellationToken cancellationToken) { var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume"); + var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume"); if (command == null) + { return; + } var service = GetServiceRenderingControl(); @@ -251,7 +276,13 @@ namespace Emby.Dlna.PlayTo // Remote control will perform better Volume = value; - await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value)) + await new SsdpHttpClient(_httpClientFactory) + .SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + rendererCommands.BuildPost(command, service.ServiceType, value), + cancellationToken: cancellationToken) .ConfigureAwait(false); } @@ -259,9 +290,11 @@ namespace Emby.Dlna.PlayTo { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); + var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); if (command == null) + { return; + } var service = GetAvTransportService(); @@ -270,7 +303,13 @@ namespace Emby.Dlna.PlayTo throw new InvalidOperationException("Unable to find service"); } - await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME")) + await new SsdpHttpClient(_httpClientFactory) + .SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), + cancellationToken: cancellationToken) .ConfigureAwait(false); RestartTimer(true); @@ -280,18 +319,20 @@ namespace Emby.Dlna.PlayTo { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - url = url.Replace("&", "&"); + url = url.Replace("&", "&", StringComparison.Ordinal); _logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header); - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI"); + var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI"); if (command == null) + { return; + } var dictionary = new Dictionary<string, string> { - {"CurrentURI", url}, - {"CurrentURIMetaData", CreateDidlMeta(metaData)} + { "CurrentURI", url }, + { "CurrentURIMetaData", CreateDidlMeta(metaData) } }; var service = GetAvTransportService(); @@ -302,14 +343,21 @@ namespace Emby.Dlna.PlayTo } var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); - await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header) + await new SsdpHttpClient(_httpClientFactory) + .SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + post, + header: header, + cancellationToken: cancellationToken) .ConfigureAwait(false); - await Task.Delay(50).ConfigureAwait(false); + await Task.Delay(50, cancellationToken).ConfigureAwait(false); try { - await SetPlay(avCommands, CancellationToken.None).ConfigureAwait(false); + await SetPlay(avCommands, cancellationToken).ConfigureAwait(false); } catch { @@ -320,14 +368,50 @@ namespace Emby.Dlna.PlayTo RestartTimer(true); } - private string CreateDidlMeta(string value) + /* + * SetNextAvTransport is used to specify to the DLNA device what is the next track to play. + * Without that information, the next track command on the device does not work. + */ + public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default) + { + var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); + + url = url.Replace("&", "&", StringComparison.Ordinal); + + _logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header); + + var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase)); + if (command == null) + { + return; + } + + var dictionary = new Dictionary<string, string> + { + { "NextURI", url }, + { "NextURIMetaData", CreateDidlMeta(metaData) } + }; + + var service = GetAvTransportService(); + + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); + await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken) + .ConfigureAwait(false); + } + + private static string CreateDidlMeta(string value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } - return DescriptionXmlBuilder.Escape(value); + return SecurityElement.Escape(value); } private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken) @@ -344,12 +428,21 @@ namespace Emby.Dlna.PlayTo throw new InvalidOperationException("Unable to find service"); } - return new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)); + return new SsdpHttpClient(_httpClientFactory).SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + avCommands.BuildPost(command, service.ServiceType, 1), + cancellationToken: cancellationToken); } public async Task SetPlay(CancellationToken cancellationToken) { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); + if (avCommands == null) + { + return; + } await SetPlay(avCommands, cancellationToken).ConfigureAwait(false); @@ -360,7 +453,7 @@ namespace Emby.Dlna.PlayTo { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop"); + var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop"); if (command == null) { return; @@ -368,7 +461,13 @@ namespace Emby.Dlna.PlayTo var service = GetAvTransportService(); - await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) + await new SsdpHttpClient(_httpClientFactory) + .SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + avCommands.BuildPost(command, service.ServiceType, 1), + cancellationToken: cancellationToken) .ConfigureAwait(false); RestartTimer(true); @@ -378,7 +477,7 @@ namespace Emby.Dlna.PlayTo { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause"); + var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause"); if (command == null) { return; @@ -386,19 +485,20 @@ namespace Emby.Dlna.PlayTo var service = GetAvTransportService(); - await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) + await new SsdpHttpClient(_httpClientFactory) + .SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + avCommands.BuildPost(command, service.ServiceType, 1), + cancellationToken: cancellationToken) .ConfigureAwait(false); - TransportState = TRANSPORTSTATE.PAUSED; + TransportState = TransportState.Paused; RestartTimer(true); } - #endregion - - #region Get data - - private int _connectFailureCount; private async void TimerCallback(object sender) { if (_disposed) @@ -427,7 +527,7 @@ namespace Emby.Dlna.PlayTo if (transportState.HasValue) { // If we're not playing anything no need to get additional data - if (transportState.Value == TRANSPORTSTATE.STOPPED) + if (transportState.Value == TransportState.Stopped) { UpdateMediaInfo(null, transportState.Value); } @@ -451,10 +551,12 @@ namespace Emby.Dlna.PlayTo _connectFailureCount = 0; if (_disposed) + { return; + } - // If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive - if (transportState.Value == TRANSPORTSTATE.STOPPED) + // If we're not playing anything make sure we don't get data more often than necessary to keep the Session alive + if (transportState.Value == TransportState.Stopped) { RestartTimerInactive(); } @@ -471,7 +573,9 @@ namespace Emby.Dlna.PlayTo catch (Exception ex) { if (_disposed) + { return; + } _logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name); @@ -487,6 +591,7 @@ namespace Emby.Dlna.PlayTo return; } } + RestartTimerInactive(); } } @@ -500,7 +605,7 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume"); + var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume"); if (command == null) { return; @@ -513,15 +618,19 @@ namespace Emby.Dlna.PlayTo return; } - var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true) - .ConfigureAwait(false); + var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + rendererCommands.BuildPost(command, service.ServiceType), + cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) { return; } - var volume = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null); + var volume = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null); var volumeValue = volume?.Value; if (string.IsNullOrWhiteSpace(volumeValue)) @@ -529,7 +638,7 @@ namespace Emby.Dlna.PlayTo return; } - Volume = int.Parse(volumeValue, UsCulture); + Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture); if (Volume > 0) { @@ -546,7 +655,7 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute"); + var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute"); if (command == null) { return; @@ -559,20 +668,26 @@ namespace Emby.Dlna.PlayTo return; } - var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true) - .ConfigureAwait(false); + var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + rendererCommands.BuildPost(command, service.ServiceType), + cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) + { return; + } - var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse") + var valueNode = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetMuteResponse") .Select(i => i.Element("CurrentMute")) .FirstOrDefault(i => i != null); IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase); } - private async Task<TRANSPORTSTATE?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken) + private async Task<TransportState?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken) { var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo"); if (command == null) @@ -586,8 +701,12 @@ namespace Emby.Dlna.PlayTo return null; } - var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType), false) - .ConfigureAwait(false); + var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + avCommands.BuildPost(command, service.ServiceType), + cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) { @@ -595,12 +714,12 @@ namespace Emby.Dlna.PlayTo } var transportState = - result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null); + result.Document.Descendants(UPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null); - var transportStateValue = transportState == null ? null : transportState.Value; + var transportStateValue = transportState?.Value; if (transportStateValue != null - && Enum.TryParse(transportStateValue, true, out TRANSPORTSTATE state)) + && Enum.TryParse(transportStateValue, true, out TransportState state)) { return state; } @@ -608,7 +727,7 @@ namespace Emby.Dlna.PlayTo return null; } - private async Task<uBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken) + private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken) { var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo"); if (command == null) @@ -623,9 +742,17 @@ namespace Emby.Dlna.PlayTo } var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); + if (rendererCommands == null) + { + return null; + } - var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false) - .ConfigureAwait(false); + var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + rendererCommands.BuildPost(command, service.ServiceType), + cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) { @@ -639,7 +766,7 @@ namespace Emby.Dlna.PlayTo return null; } - var e = track.Element(uPnpNamespaces.items) ?? track; + var e = track.Element(UPnpNamespaces.Items) ?? track; var elementString = (string)e; @@ -655,13 +782,13 @@ namespace Emby.Dlna.PlayTo return null; } - e = track.Element(uPnpNamespaces.items) ?? track; + e = track.Element(UPnpNamespaces.Items) ?? track; elementString = (string)e; if (!string.IsNullOrWhiteSpace(elementString)) { - return new uBaseObject + return new UBaseObject { Url = elementString }; @@ -670,7 +797,7 @@ namespace Emby.Dlna.PlayTo return null; } - private async Task<(bool, uBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken) + private async Task<(bool, UBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken) { var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo"); if (command == null) @@ -687,43 +814,52 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false) - .ConfigureAwait(false); + if (rendererCommands == null) + { + return (false, null); + } + + var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + rendererCommands.BuildPost(command, service.ServiceType), + cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) { return (false, null); } - var trackUriElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null); - var trackUri = trackUriElem == null ? null : trackUriElem.Value; + var trackUriElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null); + var trackUri = trackUriElem?.Value; - var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null); - var duration = durationElem == null ? null : durationElem.Value; + var durationElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null); + var duration = durationElem?.Value; if (!string.IsNullOrWhiteSpace(duration) && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) { - Duration = TimeSpan.Parse(duration, UsCulture); + Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture); } else { Duration = null; } - var positionElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null); - var position = positionElem == null ? null : positionElem.Value; + var positionElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null); + var position = positionElem?.Value; if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) { - Position = TimeSpan.Parse(position, UsCulture); + Position = TimeSpan.Parse(position, CultureInfo.InvariantCulture); } var track = result.Document.Descendants("TrackMetaData").FirstOrDefault(); if (track == null) { - //If track is null, some vendors do this, use GetMediaInfo instead + // If track is null, some vendors do this, use GetMediaInfo instead. return (true, null); } @@ -751,7 +887,7 @@ namespace Emby.Dlna.PlayTo return (true, null); } - var e = uPnpResponse.Element(uPnpNamespaces.items); + var e = uPnpResponse.Element(UPnpNamespaces.Items); var uTrack = CreateUBaseObject(e, trackUri); @@ -760,17 +896,16 @@ namespace Emby.Dlna.PlayTo private XElement ParseResponse(string xml) { - // Handle different variations sent back by devices + // Handle different variations sent back by devices. try { return XElement.Parse(xml); } catch (XmlException) { - } - // first try to add a root node with a dlna namesapce + // first try to add a root node with a dlna namespace. try { return XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + xml + "</data>") @@ -779,43 +914,41 @@ namespace Emby.Dlna.PlayTo } catch (XmlException) { - } // some devices send back invalid xml try { - return XElement.Parse(xml.Replace("&", "&")); + return XElement.Parse(xml.Replace("&", "&", StringComparison.Ordinal)); } catch (XmlException) { - } return null; } - private static uBaseObject CreateUBaseObject(XElement container, string trackUri) + private static UBaseObject CreateUBaseObject(XElement container, string trackUri) { if (container == null) { throw new ArgumentNullException(nameof(container)); } - var url = container.GetValue(uPnpNamespaces.Res); + var url = container.GetValue(UPnpNamespaces.Res); if (string.IsNullOrWhiteSpace(url)) { url = trackUri; } - return new uBaseObject + return new UBaseObject { - Id = container.GetAttributeValue(uPnpNamespaces.Id), - ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId), - Title = container.GetValue(uPnpNamespaces.title), - IconUrl = container.GetValue(uPnpNamespaces.Artwork), - SecondText = "", + Id = container.GetAttributeValue(UPnpNamespaces.Id), + ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId), + Title = container.GetValue(UPnpNamespaces.Title), + IconUrl = container.GetValue(UPnpNamespaces.Artwork), + SecondText = string.Empty, Url = url, ProtocolInfo = GetProtocolInfo(container), MetaData = container.ToString() @@ -829,11 +962,11 @@ namespace Emby.Dlna.PlayTo throw new ArgumentNullException(nameof(container)); } - var resElement = container.Element(uPnpNamespaces.Res); + var resElement = container.Element(UPnpNamespaces.Res); if (resElement != null) { - var info = resElement.Attribute(uPnpNamespaces.ProtocolInfo); + var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo); if (info != null && !string.IsNullOrWhiteSpace(info.Value)) { @@ -844,10 +977,6 @@ namespace Emby.Dlna.PlayTo return new string[4]; } - #endregion - - #region From XML - private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken) { if (AvCommands != null) @@ -868,9 +997,13 @@ namespace Emby.Dlna.PlayTo string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); - var httpClient = new SsdpHttpClient(_httpClient, _config); + var httpClient = new SsdpHttpClient(_httpClientFactory); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); + if (document == null) + { + return null; + } AvCommands = TransportCommands.Create(document); return AvCommands; @@ -896,9 +1029,13 @@ namespace Emby.Dlna.PlayTo string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); - var httpClient = new SsdpHttpClient(_httpClient, _config); + var httpClient = new SsdpHttpClient(_httpClientFactory); _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync"); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); + if (document == null) + { + return null; + } RendererCommands = TransportCommands.Create(document); return RendererCommands; @@ -912,12 +1049,12 @@ namespace Emby.Dlna.PlayTo return url; } - if (!url.Contains("/")) + if (!url.Contains('/', StringComparison.Ordinal)) { url = "/dmr/" + url; } - if (!url.StartsWith("/")) + if (!url.StartsWith('/')) { url = "/" + url; } @@ -925,25 +1062,25 @@ namespace Emby.Dlna.PlayTo return baseUrl + url; } - private TransportCommands AvCommands { get; set; } - - private TransportCommands RendererCommands { get; set; } - - public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, CancellationToken cancellationToken) + public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken) { - var ssdpHttpClient = new SsdpHttpClient(httpClient, config); + var ssdpHttpClient = new SsdpHttpClient(httpClientFactory); var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false); + if (document == null) + { + return null; + } var friendlyNames = new List<string>(); - var name = document.Descendants(uPnpNamespaces.ud.GetName("friendlyName")).FirstOrDefault(); + var name = document.Descendants(UPnpNamespaces.Ud.GetName("friendlyName")).FirstOrDefault(); if (name != null && !string.IsNullOrWhiteSpace(name.Value)) { friendlyNames.Add(name.Value); } - var room = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault(); + var room = document.Descendants(UPnpNamespaces.Ud.GetName("roomName")).FirstOrDefault(); if (room != null && !string.IsNullOrWhiteSpace(room.Value)) { friendlyNames.Add(room.Value); @@ -951,78 +1088,78 @@ namespace Emby.Dlna.PlayTo var deviceProperties = new DeviceInfo() { - Name = string.Join(" ", friendlyNames), - BaseUrl = string.Format("http://{0}:{1}", url.Host, url.Port) + Name = string.Join(' ', friendlyNames), + BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port) }; - var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault(); + var model = document.Descendants(UPnpNamespaces.Ud.GetName("modelName")).FirstOrDefault(); if (model != null) { deviceProperties.ModelName = model.Value; } - var modelNumber = document.Descendants(uPnpNamespaces.ud.GetName("modelNumber")).FirstOrDefault(); + var modelNumber = document.Descendants(UPnpNamespaces.Ud.GetName("modelNumber")).FirstOrDefault(); if (modelNumber != null) { deviceProperties.ModelNumber = modelNumber.Value; } - var uuid = document.Descendants(uPnpNamespaces.ud.GetName("UDN")).FirstOrDefault(); + var uuid = document.Descendants(UPnpNamespaces.Ud.GetName("UDN")).FirstOrDefault(); if (uuid != null) { deviceProperties.UUID = uuid.Value; } - var manufacturer = document.Descendants(uPnpNamespaces.ud.GetName("manufacturer")).FirstOrDefault(); + var manufacturer = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturer")).FirstOrDefault(); if (manufacturer != null) { deviceProperties.Manufacturer = manufacturer.Value; } - var manufacturerUrl = document.Descendants(uPnpNamespaces.ud.GetName("manufacturerURL")).FirstOrDefault(); + var manufacturerUrl = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturerURL")).FirstOrDefault(); if (manufacturerUrl != null) { deviceProperties.ManufacturerUrl = manufacturerUrl.Value; } - var presentationUrl = document.Descendants(uPnpNamespaces.ud.GetName("presentationURL")).FirstOrDefault(); + var presentationUrl = document.Descendants(UPnpNamespaces.Ud.GetName("presentationURL")).FirstOrDefault(); if (presentationUrl != null) { deviceProperties.PresentationUrl = presentationUrl.Value; } - var modelUrl = document.Descendants(uPnpNamespaces.ud.GetName("modelURL")).FirstOrDefault(); + var modelUrl = document.Descendants(UPnpNamespaces.Ud.GetName("modelURL")).FirstOrDefault(); if (modelUrl != null) { deviceProperties.ModelUrl = modelUrl.Value; } - var serialNumber = document.Descendants(uPnpNamespaces.ud.GetName("serialNumber")).FirstOrDefault(); + var serialNumber = document.Descendants(UPnpNamespaces.Ud.GetName("serialNumber")).FirstOrDefault(); if (serialNumber != null) { deviceProperties.SerialNumber = serialNumber.Value; } - var modelDescription = document.Descendants(uPnpNamespaces.ud.GetName("modelDescription")).FirstOrDefault(); + var modelDescription = document.Descendants(UPnpNamespaces.Ud.GetName("modelDescription")).FirstOrDefault(); if (modelDescription != null) { deviceProperties.ModelDescription = modelDescription.Value; } - var icon = document.Descendants(uPnpNamespaces.ud.GetName("icon")).FirstOrDefault(); + var icon = document.Descendants(UPnpNamespaces.Ud.GetName("icon")).FirstOrDefault(); if (icon != null) { deviceProperties.Icon = CreateIcon(icon); } - foreach (var services in document.Descendants(uPnpNamespaces.ud.GetName("serviceList"))) + foreach (var services in document.Descendants(UPnpNamespaces.Ud.GetName("serviceList"))) { if (services == null) { continue; } - var servicesList = services.Descendants(uPnpNamespaces.ud.GetName("service")); + var servicesList = services.Descendants(UPnpNamespaces.Ud.GetName("service")); if (servicesList == null) { continue; @@ -1039,12 +1176,9 @@ namespace Emby.Dlna.PlayTo } } - return new Device(deviceProperties, httpClient, logger, config); + return new Device(deviceProperties, httpClientFactory, logger); } - #endregion - - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); private static DeviceIcon CreateIcon(XElement element) { if (element == null) @@ -1052,14 +1186,14 @@ namespace Emby.Dlna.PlayTo throw new ArgumentNullException(nameof(element)); } - var mimeType = element.GetDescendantValue(uPnpNamespaces.ud.GetName("mimetype")); - var width = element.GetDescendantValue(uPnpNamespaces.ud.GetName("width")); - var height = element.GetDescendantValue(uPnpNamespaces.ud.GetName("height")); - var depth = element.GetDescendantValue(uPnpNamespaces.ud.GetName("depth")); - var url = element.GetDescendantValue(uPnpNamespaces.ud.GetName("url")); + var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype")); + var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width")); + var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height")); + var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth")); + var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url")); - var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture); - var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture); + var widthValue = int.Parse(width, NumberStyles.Integer, CultureInfo.InvariantCulture); + var heightValue = int.Parse(height, NumberStyles.Integer, CultureInfo.InvariantCulture); return new DeviceIcon { @@ -1073,11 +1207,11 @@ namespace Emby.Dlna.PlayTo private static DeviceService Create(XElement element) { - var type = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceType")); - var id = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceId")); - var scpdUrl = element.GetDescendantValue(uPnpNamespaces.ud.GetName("SCPDURL")); - var controlURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("controlURL")); - var eventSubURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("eventSubURL")); + var type = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType")); + var id = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId")); + var scpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL")); + var controlURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL")); + var eventSubURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL")); return new DeviceService { @@ -1089,14 +1223,7 @@ namespace Emby.Dlna.PlayTo }; } - public event EventHandler<PlaybackStartEventArgs> PlaybackStart; - public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress; - public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped; - public event EventHandler<MediaChangedEventArgs> MediaChanged; - - public uBaseObject CurrentMediaInfo { get; private set; } - - private void UpdateMediaInfo(uBaseObject mediaInfo, TRANSPORTSTATE state) + private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state) { TransportState = state; @@ -1105,7 +1232,7 @@ namespace Emby.Dlna.PlayTo if (previousMediaInfo == null && mediaInfo != null) { - if (state != TRANSPORTSTATE.STOPPED) + if (state != TransportState.Stopped) { OnPlaybackStart(mediaInfo); } @@ -1124,56 +1251,47 @@ namespace Emby.Dlna.PlayTo } } - private void OnPlaybackStart(uBaseObject mediaInfo) + private void OnPlaybackStart(UBaseObject mediaInfo) { - PlaybackStart?.Invoke(this, new PlaybackStartEventArgs + if (string.IsNullOrWhiteSpace(mediaInfo.Url)) { - MediaInfo = mediaInfo - }); + return; + } + + PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo)); } - private void OnPlaybackProgress(uBaseObject mediaInfo) + private void OnPlaybackProgress(UBaseObject mediaInfo) { - var mediaUrl = mediaInfo.Url; - if (string.IsNullOrWhiteSpace(mediaUrl)) + if (string.IsNullOrWhiteSpace(mediaInfo.Url)) { return; } - PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs - { - MediaInfo = mediaInfo - }); + PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs(mediaInfo)); } - private void OnPlaybackStop(uBaseObject mediaInfo) + private void OnPlaybackStop(UBaseObject mediaInfo) { - - PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs - { - MediaInfo = mediaInfo - }); + PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs(mediaInfo)); } - private void OnMediaChanged(uBaseObject old, uBaseObject newMedia) + private void OnMediaChanged(UBaseObject old, UBaseObject newMedia) { - MediaChanged?.Invoke(this, new MediaChangedEventArgs - { - OldMediaInfo = old, - NewMediaInfo = newMedia - }); + MediaChanged?.Invoke(this, new MediaChangedEventArgs(old, newMedia)); } - #region IDisposable - - bool _disposed; - + /// <inheritdoc /> public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } + /// <summary> + /// Releases unmanaged and optionally managed resources. + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> protected virtual void Dispose(bool disposing) { if (_disposed) @@ -1192,11 +1310,10 @@ namespace Emby.Dlna.PlayTo _disposed = true; } - #endregion - + /// <inheritdoc /> public override string ToString() { - return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl); + return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl); } } } diff --git a/Emby.Dlna/PlayTo/DeviceInfo.cs b/Emby.Dlna/PlayTo/DeviceInfo.cs index 9e7c04bdb..2acfff4eb 100644 --- a/Emby.Dlna/PlayTo/DeviceInfo.cs +++ b/Emby.Dlna/PlayTo/DeviceInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System.Collections.Generic; using Emby.Dlna.Common; using MediaBrowser.Model.Dlna; @@ -6,6 +10,9 @@ namespace Emby.Dlna.PlayTo { public class DeviceInfo { + private readonly List<DeviceService> _services = new List<DeviceService>(); + private string _baseUrl = string.Empty; + public DeviceInfo() { Name = "Generic Device"; @@ -31,7 +38,6 @@ namespace Emby.Dlna.PlayTo public string PresentationUrl { get; set; } - private string _baseUrl = string.Empty; public string BaseUrl { get => _baseUrl; @@ -40,7 +46,6 @@ namespace Emby.Dlna.PlayTo public DeviceIcon Icon { get; set; } - private readonly List<DeviceService> _services = new List<DeviceService>(); public List<DeviceService> Services => _services; public DeviceIdentification ToDeviceIdentification() diff --git a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs new file mode 100644 index 000000000..0f7a524d6 --- /dev/null +++ b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs @@ -0,0 +1,19 @@ +#pragma warning disable CS1591 + +using System; + +namespace Emby.Dlna.PlayTo +{ + public class MediaChangedEventArgs : EventArgs + { + public MediaChangedEventArgs(UBaseObject oldMediaInfo, UBaseObject newMediaInfo) + { + OldMediaInfo = oldMediaInfo; + NewMediaInfo = newMediaInfo; + } + + public UBaseObject OldMediaInfo { get; set; } + + public UBaseObject NewMediaInfo { get; set; } + } +} diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index be86dde16..f25d8017e 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -5,7 +9,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Emby.Dlna.Didl; -using MediaBrowser.Common.Configuration; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; @@ -15,17 +20,16 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Services; using MediaBrowser.Model.Session; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; +using Photo = MediaBrowser.Controller.Entities.Photo; namespace Emby.Dlna.PlayTo { public class PlayToController : ISessionController, IDisposable { - private Device _device; private readonly SessionInfo _session; private readonly ISessionManager _sessionManager; private readonly ILibraryManager _libraryManager; @@ -36,16 +40,17 @@ namespace Emby.Dlna.PlayTo private readonly IUserDataManager _userDataManager; private readonly ILocalizationManager _localization; private readonly IMediaSourceManager _mediaSourceManager; - private readonly IConfigurationManager _config; private readonly IMediaEncoder _mediaEncoder; private readonly IDeviceDiscovery _deviceDiscovery; private readonly string _serverAddress; private readonly string _accessToken; - public bool IsSessionActive => !_disposed && _device != null; + private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>(); + private Device _device; + private int _currentPlaylistIndex; - public bool SupportsMediaControl => IsSessionActive; + private bool _disposed; public PlayToController( SessionInfo session, @@ -61,7 +66,6 @@ namespace Emby.Dlna.PlayTo IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, - IConfigurationManager config, IMediaEncoder mediaEncoder) { _session = session; @@ -77,22 +81,41 @@ namespace Emby.Dlna.PlayTo _userDataManager = userDataManager; _localization = localization; _mediaSourceManager = mediaSourceManager; - _config = config; _mediaEncoder = mediaEncoder; } + public bool IsSessionActive => !_disposed && _device != null; + + public bool SupportsMediaControl => IsSessionActive; + public void Init(Device device) { _device = device; _device.OnDeviceUnavailable = OnDeviceUnavailable; - _device.PlaybackStart += _device_PlaybackStart; - _device.PlaybackProgress += _device_PlaybackProgress; - _device.PlaybackStopped += _device_PlaybackStopped; - _device.MediaChanged += _device_MediaChanged; + _device.PlaybackStart += OnDevicePlaybackStart; + _device.PlaybackProgress += OnDevicePlaybackProgress; + _device.PlaybackStopped += OnDevicePlaybackStopped; + _device.MediaChanged += OnDeviceMediaChanged; _device.Start(); - _deviceDiscovery.DeviceLeft += _deviceDiscovery_DeviceLeft; + _deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft; + } + + /* + * Send a message to the DLNA device to notify what is the next track in the playlist. + */ + private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken) + { + if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1) + { + // The current playing item is indeed in the play list and we are not yet at the end of the playlist. + var nextItemIndex = currentPlayListItemIndex + 1; + var nextItem = _playlist[nextItemIndex]; + + // Send the SetNextAvTransport message. + await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false); + } } private void OnDeviceUnavailable() @@ -101,36 +124,31 @@ namespace Emby.Dlna.PlayTo { _sessionManager.ReportSessionEnded(_session.Id); } - catch + catch (Exception ex) { // Could throw if the session is already gone + _logger.LogError(ex, "Error reporting the end of session {Id}", _session.Id); } } - void _deviceDiscovery_DeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e) + private void OnDeviceDiscoveryDeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e) { var info = e.Argument; - info.Headers.TryGetValue("NTS", out string nts); - - if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty; - - if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty; - - if (usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 && - !_disposed) + if (!_disposed + && info.Headers.TryGetValue("USN", out string usn) + && usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 + && (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 + || (info.Headers.TryGetValue("NT", out string nt) + && nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1))) { - if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 || - nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1) - { - OnDeviceUnavailable(); - } + OnDeviceUnavailable(); } } - async void _device_MediaChanged(object sender, MediaChangedEventArgs e) + private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e) { - if (_disposed) + if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url)) { return; } @@ -140,17 +158,29 @@ namespace Emby.Dlna.PlayTo var streamInfo = StreamParams.ParseFromUrl(e.OldMediaInfo.Url, _libraryManager, _mediaSourceManager); if (streamInfo.Item != null) { - var positionTicks = GetProgressPositionTicks(e.OldMediaInfo, streamInfo); + var positionTicks = GetProgressPositionTicks(streamInfo); - ReportPlaybackStopped(e.OldMediaInfo, streamInfo, positionTicks); + await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false); } streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager); - if (streamInfo.Item == null) return; + if (streamInfo.Item == null) + { + return; + } - var newItemProgress = GetProgressInfo(e.NewMediaInfo, streamInfo); + var newItemProgress = GetProgressInfo(streamInfo); await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false); + + // Send a message to the DLNA device to notify what is the next track in the playlist. + var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId); + if (currentItemIndex >= 0) + { + _currentPlaylistIndex = currentItemIndex; + } + + await SendNextTrackMessage(currentItemIndex, CancellationToken.None); } catch (Exception ex) { @@ -158,7 +188,7 @@ namespace Emby.Dlna.PlayTo } } - async void _device_PlaybackStopped(object sender, PlaybackStoppedEventArgs e) + private async void OnDevicePlaybackStopped(object sender, PlaybackStoppedEventArgs e) { if (_disposed) { @@ -169,11 +199,14 @@ namespace Emby.Dlna.PlayTo { var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager); - if (streamInfo.Item == null) return; + if (streamInfo.Item == null) + { + return; + } - var positionTicks = GetProgressPositionTicks(e.MediaInfo, streamInfo); + var positionTicks = GetProgressPositionTicks(streamInfo); - ReportPlaybackStopped(e.MediaInfo, streamInfo, positionTicks); + await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false); var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false); @@ -181,7 +214,7 @@ namespace Emby.Dlna.PlayTo (_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) : mediaSource.RunTimeTicks; - var playedToCompletion = (positionTicks.HasValue && positionTicks.Value == 0); + var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0; if (!playedToCompletion && duration.HasValue && positionTicks.HasValue) { @@ -197,7 +230,7 @@ namespace Emby.Dlna.PlayTo } else { - Playlist.Clear(); + _playlist.Clear(); } } catch (Exception ex) @@ -206,7 +239,7 @@ namespace Emby.Dlna.PlayTo } } - private async void ReportPlaybackStopped(uBaseObject mediaInfo, StreamParams streamInfo, long? positionTicks) + private async Task ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks) { try { @@ -216,7 +249,6 @@ namespace Emby.Dlna.PlayTo SessionId = _session.Id, PositionTicks = positionTicks, MediaSourceId = streamInfo.MediaSourceId - }).ConfigureAwait(false); } catch (Exception ex) @@ -225,7 +257,7 @@ namespace Emby.Dlna.PlayTo } } - async void _device_PlaybackStart(object sender, PlaybackStartEventArgs e) + private async void OnDevicePlaybackStart(object sender, PlaybackStartEventArgs e) { if (_disposed) { @@ -238,7 +270,7 @@ namespace Emby.Dlna.PlayTo if (info.Item != null) { - var progress = GetProgressInfo(e.MediaInfo, info); + var progress = GetProgressInfo(info); await _sessionManager.OnPlaybackStart(progress).ConfigureAwait(false); } @@ -249,7 +281,7 @@ namespace Emby.Dlna.PlayTo } } - async void _device_PlaybackProgress(object sender, PlaybackProgressEventArgs e) + private async void OnDevicePlaybackProgress(object sender, PlaybackProgressEventArgs e) { if (_disposed) { @@ -269,7 +301,7 @@ namespace Emby.Dlna.PlayTo if (info.Item != null) { - var progress = GetProgressInfo(e.MediaInfo, info); + var progress = GetProgressInfo(info); await _sessionManager.OnPlaybackProgress(progress).ConfigureAwait(false); } @@ -280,7 +312,7 @@ namespace Emby.Dlna.PlayTo } } - private long? GetProgressPositionTicks(uBaseObject mediaInfo, StreamParams info) + private long? GetProgressPositionTicks(StreamParams info) { var ticks = _device.Position.Ticks; @@ -292,13 +324,13 @@ namespace Emby.Dlna.PlayTo return ticks; } - private PlaybackStartInfo GetProgressInfo(uBaseObject mediaInfo, StreamParams info) + private PlaybackStartInfo GetProgressInfo(StreamParams info) { return new PlaybackStartInfo { ItemId = info.ItemId, SessionId = _session.Id, - PositionTicks = GetProgressPositionTicks(mediaInfo, info), + PositionTicks = GetProgressPositionTicks(info), IsMuted = _device.IsMuted, IsPaused = _device.IsPaused, MediaSourceId = info.MediaSourceId, @@ -313,11 +345,9 @@ namespace Emby.Dlna.PlayTo }; } - #region SendCommands - - public async Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) + public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) { - _logger.LogDebug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand); + _logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand); var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId); @@ -328,45 +358,52 @@ namespace Emby.Dlna.PlayTo } var startIndex = command.StartIndex ?? 0; + int len = items.Count - startIndex; if (startIndex > 0) { - items = items.Skip(startIndex).ToList(); + items = items.GetRange(startIndex, len); } - var playlist = new List<PlaylistItem>(); - var isFirst = true; + var playlist = new PlaylistItem[len]; + + // Not nullable enabled - so this is required. + playlist[0] = CreatePlaylistItem( + items[0], + user, + command.StartPositionTicks ?? 0, + command.MediaSourceId ?? string.Empty, + command.AudioStreamIndex, + command.SubtitleStreamIndex); - foreach (var item in items) + for (int i = 1; i < len; i++) { - if (isFirst && command.StartPositionTicks.HasValue) - { - playlist.Add(CreatePlaylistItem(item, user, command.StartPositionTicks.Value, command.MediaSourceId, command.AudioStreamIndex, command.SubtitleStreamIndex)); - isFirst = false; - } - else - { - playlist.Add(CreatePlaylistItem(item, user, 0, null, null, null)); - } + playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null); } _logger.LogDebug("{0} - Playlist created", _session.DeviceName); if (command.PlayCommand == PlayCommand.PlayLast) { - Playlist.AddRange(playlist); + _playlist.AddRange(playlist); } + if (command.PlayCommand == PlayCommand.PlayNext) { - Playlist.AddRange(playlist); + _playlist.AddRange(playlist); } if (!command.ControllingUserId.Equals(Guid.Empty)) { - _sessionManager.LogSessionActivity(_session.Client, _session.ApplicationVersion, _session.DeviceId, - _session.DeviceName, _session.RemoteEndPoint, user); + _sessionManager.LogSessionActivity( + _session.Client, + _session.ApplicationVersion, + _session.DeviceId, + _session.DeviceName, + _session.RemoteEndPoint, + user); } - await PlayItems(playlist).ConfigureAwait(false); + return PlayItems(playlist, cancellationToken); } private Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken) @@ -374,7 +411,7 @@ namespace Emby.Dlna.PlayTo switch (command.Command) { case PlaystateCommand.Stop: - Playlist.Clear(); + _playlist.Clear(); return _device.SetStop(CancellationToken.None); case PlaystateCommand.Pause: @@ -390,10 +427,10 @@ namespace Emby.Dlna.PlayTo return Seek(command.SeekPositionTicks ?? 0); case PlaystateCommand.NextTrack: - return SetPlaylistIndex(_currentPlaylistIndex + 1); + return SetPlaylistIndex(_currentPlaylistIndex + 1, cancellationToken); case PlaystateCommand.PreviousTrack: - return SetPlaylistIndex(_currentPlaylistIndex - 1); + return SetPlaylistIndex(_currentPlaylistIndex - 1, cancellationToken); } return Task.CompletedTask; @@ -413,8 +450,14 @@ namespace Emby.Dlna.PlayTo var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex); await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); + + // Send a message to the DLNA device to notify what is the next track in the play list. + var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl); + await SendNextTrackMessage(newItemIndex, CancellationToken.None); + return; } + await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); } } @@ -429,14 +472,6 @@ namespace Emby.Dlna.PlayTo return info.IsDirectStream; } - #endregion - - #region Playlist - - private int _currentPlaylistIndex; - private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>(); - private List<PlaylistItem> Playlist => _playlist; - private void AddItemFromId(Guid id, List<BaseItem> list) { var item = _libraryManager.GetItemById(id); @@ -446,7 +481,13 @@ namespace Emby.Dlna.PlayTo } } - private PlaylistItem CreatePlaylistItem(BaseItem item, User user, long startPostionTicks, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex) + private PlaylistItem CreatePlaylistItem( + BaseItem item, + User user, + long startPostionTicks, + string mediaSourceId, + int? audioStreamIndex, + int? subtitleStreamIndex) { var deviceInfo = _device.Properties; @@ -454,16 +495,27 @@ namespace Emby.Dlna.PlayTo _dlnaManager.GetDefaultProfile(); var mediaSources = item is IHasMediaSources - ? (_mediaSourceManager.GetStaticMediaSources(item, true, user)) - : new List<MediaSourceInfo>(); + ? _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray() + : Array.Empty<MediaSourceInfo>(); var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex); playlistItem.StreamInfo.StartPositionTicks = startPostionTicks; playlistItem.StreamUrl = DidlBuilder.NormalizeDlnaMediaUrl(playlistItem.StreamInfo.ToUrl(_serverAddress, _accessToken)); - var itemXml = new DidlBuilder(profile, user, _imageProcessor, _serverAddress, _accessToken, _userDataManager, _localization, _mediaSourceManager, _logger, _mediaEncoder) - .GetItemDidl(_config.GetDlnaConfiguration(), item, user, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo); + var itemXml = new DidlBuilder( + profile, + user, + _imageProcessor, + _serverAddress, + _accessToken, + _userDataManager, + _localization, + _mediaSourceManager, + _logger, + _mediaEncoder, + _libraryManager) + .GetItemDidl(item, user, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo); playlistItem.Didl = itemXml; @@ -477,43 +529,45 @@ namespace Emby.Dlna.PlayTo if (streamInfo.MediaType == DlnaProfileType.Audio) { - return new ContentFeatureBuilder(profile) - .BuildAudioHeader(streamInfo.Container, - streamInfo.TargetAudioCodec.FirstOrDefault(), - streamInfo.TargetAudioBitrate, - streamInfo.TargetAudioSampleRate, - streamInfo.TargetAudioChannels, - streamInfo.TargetAudioBitDepth, - streamInfo.IsDirectStream, - streamInfo.RunTimeTicks ?? 0, - streamInfo.TranscodeSeekInfo); + return ContentFeatureBuilder.BuildAudioHeader( + profile, + streamInfo.Container, + streamInfo.TargetAudioCodec.FirstOrDefault(), + streamInfo.TargetAudioBitrate, + streamInfo.TargetAudioSampleRate, + streamInfo.TargetAudioChannels, + streamInfo.TargetAudioBitDepth, + streamInfo.IsDirectStream, + streamInfo.RunTimeTicks ?? 0, + streamInfo.TranscodeSeekInfo); } if (streamInfo.MediaType == DlnaProfileType.Video) { - var list = new ContentFeatureBuilder(profile) - .BuildVideoHeader(streamInfo.Container, - streamInfo.TargetVideoCodec.FirstOrDefault(), - streamInfo.TargetAudioCodec.FirstOrDefault(), - streamInfo.TargetWidth, - streamInfo.TargetHeight, - streamInfo.TargetVideoBitDepth, - streamInfo.TargetVideoBitrate, - streamInfo.TargetTimestamp, - streamInfo.IsDirectStream, - streamInfo.RunTimeTicks ?? 0, - streamInfo.TargetVideoProfile, - streamInfo.TargetVideoLevel, - streamInfo.TargetFramerate ?? 0, - streamInfo.TargetPacketLength, - streamInfo.TranscodeSeekInfo, - streamInfo.IsTargetAnamorphic, - streamInfo.IsTargetInterlaced, - streamInfo.TargetRefFrames, - streamInfo.TargetVideoStreamCount, - streamInfo.TargetAudioStreamCount, - streamInfo.TargetVideoCodecTag, - streamInfo.IsTargetAVC); + var list = ContentFeatureBuilder.BuildVideoHeader( + profile, + streamInfo.Container, + streamInfo.TargetVideoCodec.FirstOrDefault(), + streamInfo.TargetAudioCodec.FirstOrDefault(), + streamInfo.TargetWidth, + streamInfo.TargetHeight, + streamInfo.TargetVideoBitDepth, + streamInfo.TargetVideoBitrate, + streamInfo.TargetTimestamp, + streamInfo.IsDirectStream, + streamInfo.RunTimeTicks ?? 0, + streamInfo.TargetVideoProfile, + streamInfo.TargetVideoLevel, + streamInfo.TargetFramerate ?? 0, + streamInfo.TargetPacketLength, + streamInfo.TranscodeSeekInfo, + streamInfo.IsTargetAnamorphic, + streamInfo.IsTargetInterlaced, + streamInfo.TargetRefFrames, + streamInfo.TargetVideoStreamCount, + streamInfo.TargetAudioStreamCount, + streamInfo.TargetVideoCodecTag, + streamInfo.IsTargetAVC); return list.Count == 0 ? null : list[0]; } @@ -521,7 +575,7 @@ namespace Emby.Dlna.PlayTo return null; } - private PlaylistItem GetPlaylistItem(BaseItem item, List<MediaSourceInfo> mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex) + private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex) { if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) { @@ -530,7 +584,7 @@ namespace Emby.Dlna.PlayTo StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions { ItemId = item.Id, - MediaSources = mediaSources.ToArray(), + MediaSources = mediaSources, Profile = profile, DeviceId = deviceId, MaxBitrate = profile.MaxStreamingBitrate, @@ -550,7 +604,7 @@ namespace Emby.Dlna.PlayTo StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions { ItemId = item.Id, - MediaSources = mediaSources.ToArray(), + MediaSources = mediaSources, Profile = profile, DeviceId = deviceId, MaxBitrate = profile.MaxStreamingBitrate, @@ -563,7 +617,7 @@ namespace Emby.Dlna.PlayTo if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) { - return new PlaylistItemFactory().Create((Photo)item, profile); + return PlaylistItemFactory.Create((Photo)item, profile); } throw new ArgumentException("Unrecognized item type."); @@ -573,30 +627,34 @@ namespace Emby.Dlna.PlayTo /// Plays the items. /// </summary> /// <param name="items">The items.</param> - /// <returns></returns> - private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items) + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns><c>true</c> on success.</returns> + private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items, CancellationToken cancellationToken = default) { - Playlist.Clear(); - Playlist.AddRange(items); - _logger.LogDebug("{0} - Playing {1} items", _session.DeviceName, Playlist.Count); + _playlist.Clear(); + _playlist.AddRange(items); + _logger.LogDebug("{0} - Playing {1} items", _session.DeviceName, _playlist.Count); - await SetPlaylistIndex(0).ConfigureAwait(false); + await SetPlaylistIndex(0, cancellationToken).ConfigureAwait(false); return true; } - private async Task SetPlaylistIndex(int index) + private async Task SetPlaylistIndex(int index, CancellationToken cancellationToken = default) { - if (index < 0 || index >= Playlist.Count) + if (index < 0 || index >= _playlist.Count) { - Playlist.Clear(); - await _device.SetStop(CancellationToken.None); + _playlist.Clear(); + await _device.SetStop(cancellationToken).ConfigureAwait(false); return; } _currentPlaylistIndex = index; - var currentitem = Playlist[index]; + var currentitem = _playlist[index]; + + await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false); - await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, CancellationToken.None); + // Send a message to the DLNA device to notify what is the next track in the play list. + await SendNextTrackMessage(index, cancellationToken); var streamInfo = currentitem.StreamInfo; if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo)) @@ -605,93 +663,93 @@ namespace Emby.Dlna.PlayTo } } - #endregion - - private bool _disposed; - + /// <inheritdoc /> public void Dispose() { - if (!_disposed) - { - _disposed = true; + Dispose(true); + GC.SuppressFinalize(this); + } - _device.PlaybackStart -= _device_PlaybackStart; - _device.PlaybackProgress -= _device_PlaybackProgress; - _device.PlaybackStopped -= _device_PlaybackStopped; - _device.MediaChanged -= _device_MediaChanged; - //_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft; - _device.OnDeviceUnavailable = null; + /// <summary> + /// Releases unmanaged and optionally managed resources. + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + if (disposing) + { _device.Dispose(); } - } - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + _device.PlaybackStart -= OnDevicePlaybackStart; + _device.PlaybackProgress -= OnDevicePlaybackProgress; + _device.PlaybackStopped -= OnDevicePlaybackStopped; + _device.MediaChanged -= OnDeviceMediaChanged; + _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft; + _device.OnDeviceUnavailable = null; + _device = null; + + _disposed = true; + } private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken) { - if (Enum.TryParse(command.Name, true, out GeneralCommandType commandType)) - { - switch (commandType) - { - case GeneralCommandType.VolumeDown: - return _device.VolumeDown(cancellationToken); - case GeneralCommandType.VolumeUp: - return _device.VolumeUp(cancellationToken); - case GeneralCommandType.Mute: - return _device.Mute(cancellationToken); - case GeneralCommandType.Unmute: - return _device.Unmute(cancellationToken); - case GeneralCommandType.ToggleMute: - return _device.ToggleMute(cancellationToken); - case GeneralCommandType.SetAudioStreamIndex: + switch (command.Name) + { + case GeneralCommandType.VolumeDown: + return _device.VolumeDown(cancellationToken); + case GeneralCommandType.VolumeUp: + return _device.VolumeUp(cancellationToken); + case GeneralCommandType.Mute: + return _device.Mute(cancellationToken); + case GeneralCommandType.Unmute: + return _device.Unmute(cancellationToken); + case GeneralCommandType.ToggleMute: + return _device.ToggleMute(cancellationToken); + case GeneralCommandType.SetAudioStreamIndex: + if (command.Arguments.TryGetValue("Index", out string index)) + { + if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) { - if (command.Arguments.TryGetValue("Index", out string arg)) - { - if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val)) - { - return SetAudioStreamIndex(val); - } + return SetAudioStreamIndex(val); + } - throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied."); - } + throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied."); + } - throw new ArgumentException("SetAudioStreamIndex argument cannot be null"); - } - case GeneralCommandType.SetSubtitleStreamIndex: + throw new ArgumentException("SetAudioStreamIndex argument cannot be null"); + case GeneralCommandType.SetSubtitleStreamIndex: + if (command.Arguments.TryGetValue("Index", out index)) + { + if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) { - if (command.Arguments.TryGetValue("Index", out string arg)) - { - if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val)) - { - return SetSubtitleStreamIndex(val); - } + return SetSubtitleStreamIndex(val); + } - throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied."); - } + throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied."); + } - throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null"); - } - case GeneralCommandType.SetVolume: + throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null"); + case GeneralCommandType.SetVolume: + if (command.Arguments.TryGetValue("Volume", out string vol)) + { + if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume)) { - if (command.Arguments.TryGetValue("Volume", out string arg)) - { - if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var volume)) - { - return _device.SetVolume(volume, cancellationToken); - } + return _device.SetVolume(volume, cancellationToken); + } - throw new ArgumentException("Unsupported volume value supplied."); - } + throw new ArgumentException("Unsupported volume value supplied."); + } - throw new ArgumentException("Volume argument cannot be null"); - } - default: - return Task.CompletedTask; - } + throw new ArgumentException("Volume argument cannot be null"); + default: + return Task.CompletedTask; } - - return Task.CompletedTask; } private async Task SetAudioStreamIndex(int? newIndex) @@ -704,13 +762,17 @@ namespace Emby.Dlna.PlayTo if (info.Item != null) { - var newPosition = GetProgressPositionTicks(media, info) ?? 0; + var newPosition = GetProgressPositionTicks(info) ?? 0; var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null; var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, info.SubtitleStreamIndex); await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); + // Send a message to the DLNA device to notify what is the next track in the play list. + var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl); + await SendNextTrackMessage(newItemIndex, CancellationToken.None); + if (EnableClientSideSeek(newItem.StreamInfo)) { await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); @@ -729,13 +791,17 @@ namespace Emby.Dlna.PlayTo if (info.Item != null) { - var newPosition = GetProgressPositionTicks(media, info) ?? 0; + var newPosition = GetProgressPositionTicks(info) ?? 0; var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null; var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex); await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); + // Send a message to the DLNA device to notify what is the next track in the play list. + var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl); + await SendNextTrackMessage(newItemIndex, CancellationToken.None); + if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0) { await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); @@ -746,20 +812,80 @@ namespace Emby.Dlna.PlayTo private async Task SeekAfterTransportChange(long positionTicks, CancellationToken cancellationToken) { - const int maxWait = 15000000; - const int interval = 500; + const int MaxWait = 15000000; + const int Interval = 500; + var currentWait = 0; - while (_device.TransportState != TRANSPORTSTATE.PLAYING && currentWait < maxWait) + while (_device.TransportState != TransportState.Playing && currentWait < MaxWait) { - await Task.Delay(interval).ConfigureAwait(false); - currentWait += interval; + await Task.Delay(Interval, cancellationToken).ConfigureAwait(false); + currentWait += Interval; } await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false); } + private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name) + { + var value = values.GetValueOrDefault(name); + + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + { + return result; + } + + return null; + } + + private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name) + { + var value = values.GetValueOrDefault(name); + + if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + { + return result; + } + + return 0; + } + + /// <inheritdoc /> + public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken) + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + + if (_device == null) + { + return Task.CompletedTask; + } + + if (name == SessionMessageType.Play) + { + return SendPlayCommand(data as PlayRequest, cancellationToken); + } + + if (name == SessionMessageType.Playstate) + { + return SendPlaystateCommand(data as PlaystateRequest, cancellationToken); + } + + if (name == SessionMessageType.GeneralCommand) + { + return SendGeneralCommand(data as GeneralCommand, cancellationToken); + } + + // Not supported or needed right now + return Task.CompletedTask; + } + private class StreamParams { + private MediaSourceInfo mediaSource; + private IMediaSourceManager _mediaSourceManager; + public Guid ItemId { get; set; } public bool IsDirectStream { get; set; } @@ -771,21 +897,20 @@ namespace Emby.Dlna.PlayTo public int? SubtitleStreamIndex { get; set; } public string DeviceProfileId { get; set; } + public string DeviceId { get; set; } public string MediaSourceId { get; set; } + public string LiveStreamId { get; set; } public BaseItem Item { get; set; } - private MediaSourceInfo MediaSource; - - private IMediaSourceManager _mediaSourceManager; public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken) { - if (MediaSource != null) + if (mediaSource != null) { - return MediaSource; + return mediaSource; } var hasMediaSources = Item as IHasMediaSources; @@ -795,9 +920,12 @@ namespace Emby.Dlna.PlayTo return null; } - MediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false); + if (_mediaSourceManager != null) + { + mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false); + } - return MediaSource; + return mediaSource; } private static Guid GetItemId(string url) @@ -809,16 +937,16 @@ namespace Emby.Dlna.PlayTo var parts = url.Split('/'); - for (var i = 0; i < parts.Length; i++) + for (var i = 0; i < parts.Length - 1; i++) { var part = parts[i]; if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) || string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase)) { - if (parts.Length > i + 1) + if (Guid.TryParse(parts[i + 1], out var result)) { - return Guid.Parse(parts[i + 1]); + return result; } } } @@ -843,18 +971,20 @@ namespace Emby.Dlna.PlayTo return request; } - var index = url.IndexOf('?'); - if (index == -1) return request; + var index = url.IndexOf('?', StringComparison.Ordinal); + if (index == -1) + { + return request; + } var query = url.Substring(index + 1); - QueryParamCollection values = MyHttpUtility.ParseQueryString(query); - - request.DeviceProfileId = values.Get("DeviceProfileId"); - request.DeviceId = values.Get("DeviceId"); - request.MediaSourceId = values.Get("MediaSourceId"); - request.LiveStreamId = values.Get("LiveStreamId"); - request.IsDirectStream = string.Equals("true", values.Get("Static"), StringComparison.OrdinalIgnoreCase); + Dictionary<string, string> values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString()); + request.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId"); + request.DeviceId = values.GetValueOrDefault("DeviceId"); + request.MediaSourceId = values.GetValueOrDefault("MediaSourceId"); + request.LiveStreamId = values.GetValueOrDefault("LiveStreamId"); + request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase); request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex"); request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex"); request.StartPositionTicks = GetLongValue(values, "StartPositionTicks"); @@ -866,58 +996,5 @@ namespace Emby.Dlna.PlayTo return request; } } - - private static int? GetIntValue(QueryParamCollection values, string name) - { - var value = values.Get(name); - - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - - return null; - } - - private static long GetLongValue(QueryParamCollection values, string name) - { - var value = values.Get(name); - - if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - - return 0; - } - - public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - - if (_device == null) - { - return Task.CompletedTask; - } - - if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase)) - { - return SendPlayCommand(data as PlayRequest, cancellationToken); - } - if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase)) - { - return SendPlaystateCommand(data as PlaystateRequest, cancellationToken); - } - if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase)) - { - return SendGeneralCommand(data as GeneralCommand, cancellationToken); - } - - // Not supported or needed right now - return Task.CompletedTask; - } } } diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index 28e70d046..7927f5f8f 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -1,9 +1,15 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Globalization; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; @@ -12,15 +18,13 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; namespace Emby.Dlna.PlayTo { - class PlayToManager : IDisposable + public sealed class PlayToManager : IDisposable { private readonly ILogger _logger; private readonly ISessionManager _sessionManager; @@ -30,7 +34,7 @@ namespace Emby.Dlna.PlayTo private readonly IDlnaManager _dlnaManager; private readonly IServerApplicationHost _appHost; private readonly IImageProcessor _imageProcessor; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerConfigurationManager _config; private readonly IUserDataManager _userDataManager; private readonly ILocalizationManager _localization; @@ -43,7 +47,7 @@ namespace Emby.Dlna.PlayTo private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1); private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); - public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder) + public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder) { _logger = logger; _sessionManager = sessionManager; @@ -53,7 +57,7 @@ namespace Emby.Dlna.PlayTo _appHost = appHost; _imageProcessor = imageProcessor; _deviceDiscovery = deviceDiscovery; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _config = config; _userDataManager = userDataManager; _localization = localization; @@ -63,10 +67,10 @@ namespace Emby.Dlna.PlayTo public void Start() { - _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; + _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered; } - async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) + private async void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) { if (_disposed) { @@ -75,17 +79,20 @@ namespace Emby.Dlna.PlayTo var info = e.Argument; - if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty; - - if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty; + if (!info.Headers.TryGetValue("USN", out string usn)) + { + usn = string.Empty; + } - string location = info.Location.ToString(); + if (!info.Headers.TryGetValue("NT", out string nt)) + { + nt = string.Empty; + } // It has to report that it's a media renderer - if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 && - nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1) + if (!usn.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase) + && !nt.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase)) { - //_logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location); return; } @@ -105,11 +112,10 @@ namespace Emby.Dlna.PlayTo return; } - await AddDevice(info, location, cancellationToken).ConfigureAwait(false); + await AddDevice(info, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { - } catch (Exception ex) { @@ -121,81 +127,88 @@ namespace Emby.Dlna.PlayTo } } - private string GetUuid(string usn) + internal static string GetUuid(string usn) { - var found = false; - var index = usn.IndexOf("uuid:", StringComparison.OrdinalIgnoreCase); - if (index != -1) + const string UuidStr = "uuid:"; + const string UuidColonStr = "::"; + + var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase); + if (index == -1) { - usn = usn.Substring(index); - found = true; + return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture); } - index = usn.IndexOf("::", StringComparison.OrdinalIgnoreCase); + + ReadOnlySpan<char> tmp = usn.AsSpan()[(index + UuidStr.Length)..]; + + index = tmp.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase); if (index != -1) { - usn = usn.Substring(0, index); + tmp = tmp[..index]; } - if (found) + index = tmp.IndexOf('{'); + if (index != -1) { - return usn; + int endIndex = tmp.IndexOf('}'); + if (endIndex != -1) + { + tmp = tmp[(index + 1)..endIndex]; + } } - return usn.GetMD5().ToString("N"); + return tmp.ToString(); } - private async Task AddDevice(UpnpDeviceInfo info, string location, CancellationToken cancellationToken) + private async Task AddDevice(UpnpDeviceInfo info, CancellationToken cancellationToken) { var uri = info.Location; - _logger.LogDebug("Attempting to create PlayToController from location {0}", location); + _logger.LogDebug("Attempting to create PlayToController from location {0}", uri); - _logger.LogDebug("Logging session activity from location {0}", location); if (info.Headers.TryGetValue("USN", out string uuid)) { uuid = GetUuid(uuid); } else { - uuid = location.GetMD5().ToString("N"); + uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture); } - var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersion, uuid, null, uri.OriginalString, null); + var sessionInfo = await _sessionManager + .LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null) + .ConfigureAwait(false); var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault(); if (controller == null) { - var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger, cancellationToken).ConfigureAwait(false); + var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false); + if (device == null) + { + _logger.LogError("Ignoring device as xml response is invalid."); + return; + } string deviceName = device.Properties.Name; _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); - string serverAddress; - if (info.LocalIpAddress == null || info.LocalIpAddress.Equals(IpAddressInfo.Any) || info.LocalIpAddress.Equals(IpAddressInfo.IPv6Any)) - { - serverAddress = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - } - else - { - serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress); - } - - controller = new PlayToController(sessionInfo, - _sessionManager, - _libraryManager, - _logger, - _dlnaManager, - _userManager, - _imageProcessor, - serverAddress, - null, - _deviceDiscovery, - _userDataManager, - _localization, - _mediaSourceManager, - _config, - _mediaEncoder); + string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress); + + controller = new PlayToController( + sessionInfo, + _sessionManager, + _libraryManager, + _logger, + _dlnaManager, + _userManager, + _imageProcessor, + serverAddress, + null, + _deviceDiscovery, + _userDataManager, + _localization, + _mediaSourceManager, + _mediaEncoder); sessionInfo.AddController(controller); @@ -208,17 +221,17 @@ namespace Emby.Dlna.PlayTo { PlayableMediaTypes = profile.GetSupportedMediaTypes(), - SupportedCommands = new string[] + SupportedCommands = new[] { - GeneralCommandType.VolumeDown.ToString(), - GeneralCommandType.VolumeUp.ToString(), - GeneralCommandType.Mute.ToString(), - GeneralCommandType.Unmute.ToString(), - GeneralCommandType.ToggleMute.ToString(), - GeneralCommandType.SetVolume.ToString(), - GeneralCommandType.SetAudioStreamIndex.ToString(), - GeneralCommandType.SetSubtitleStreamIndex.ToString(), - GeneralCommandType.PlayMediaSource.ToString() + GeneralCommandType.VolumeDown, + GeneralCommandType.VolumeUp, + GeneralCommandType.Mute, + GeneralCommandType.Unmute, + GeneralCommandType.ToggleMute, + GeneralCommandType.SetVolume, + GeneralCommandType.SetAudioStreamIndex, + GeneralCommandType.SetSubtitleStreamIndex, + GeneralCommandType.PlayMediaSource }, SupportsMediaControl = true @@ -228,19 +241,23 @@ namespace Emby.Dlna.PlayTo } } + /// <inheritdoc /> public void Dispose() { - _deviceDiscovery.DeviceDiscovered -= _deviceDiscovery_DeviceDiscovered; + _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered; try { _disposeCancellationTokenSource.Cancel(); } - catch + catch (Exception ex) { - + _logger.LogDebug(ex, "Error while disposing PlayToManager"); } + _sessionLock.Dispose(); + _disposeCancellationTokenSource.Dispose(); + _disposed = true; } } diff --git a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs index ffa56419b..c95d8b1e8 100644 --- a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs @@ -1,9 +1,16 @@ +#pragma warning disable CS1591 + using System; namespace Emby.Dlna.PlayTo { public class PlaybackProgressEventArgs : EventArgs { - public uBaseObject MediaInfo { get; set; } + public PlaybackProgressEventArgs(UBaseObject mediaInfo) + { + MediaInfo = mediaInfo; + } + + public UBaseObject MediaInfo { get; set; } } } diff --git a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs index 8cd8b47ac..619c861ed 100644 --- a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs @@ -1,9 +1,16 @@ +#pragma warning disable CS1591 + using System; namespace Emby.Dlna.PlayTo { public class PlaybackStartEventArgs : EventArgs { - public uBaseObject MediaInfo { get; set; } + public PlaybackStartEventArgs(UBaseObject mediaInfo) + { + MediaInfo = mediaInfo; + } + + public UBaseObject MediaInfo { get; set; } } } diff --git a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs index 2afdc324d..d0ec25059 100644 --- a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs @@ -1,15 +1,16 @@ +#pragma warning disable CS1591 + using System; namespace Emby.Dlna.PlayTo { public class PlaybackStoppedEventArgs : EventArgs { - public uBaseObject MediaInfo { get; set; } - } + public PlaybackStoppedEventArgs(UBaseObject mediaInfo) + { + MediaInfo = mediaInfo; + } - public class MediaChangedEventArgs : EventArgs - { - public uBaseObject OldMediaInfo { get; set; } - public uBaseObject NewMediaInfo { get; set; } + public UBaseObject MediaInfo { get; set; } } } diff --git a/Emby.Dlna/PlayTo/PlaylistItem.cs b/Emby.Dlna/PlayTo/PlaylistItem.cs index 1e62b61e9..5056e69ae 100644 --- a/Emby.Dlna/PlayTo/PlaylistItem.cs +++ b/Emby.Dlna/PlayTo/PlaylistItem.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.PlayTo diff --git a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs index aceb634e3..657491303 100644 --- a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs +++ b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs @@ -1,4 +1,7 @@ -using System.Globalization; +#nullable disable + +#pragma warning disable CS1591 + using System.IO; using System.Linq; using MediaBrowser.Controller.Entities; @@ -7,11 +10,9 @@ using MediaBrowser.Model.Session; namespace Emby.Dlna.PlayTo { - public class PlaylistItemFactory + public static class PlaylistItemFactory { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - public PlaylistItem Create(Photo item, DeviceProfile profile) + public static PlaylistItem Create(Photo item, DeviceProfile profile) { var playlistItem = new PlaylistItem { diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs index 1ad99fac5..cade7b4c2 100644 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -1,13 +1,17 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Globalization; -using System.IO; +using System.Net.Http; +using System.Net.Mime; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using Emby.Dlna.Common; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; namespace Emby.Dlna.PlayTo { @@ -16,35 +20,36 @@ namespace Emby.Dlna.PlayTo private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50"; private const string FriendlyName = "Jellyfin"; - private readonly IHttpClient _httpClient; - private readonly IServerConfigurationManager _config; + private readonly IHttpClientFactory _httpClientFactory; - public SsdpHttpClient(IHttpClient httpClient, IServerConfigurationManager config) + public SsdpHttpClient(IHttpClientFactory httpClientFactory) { - _httpClient = httpClient; - _config = config; + _httpClientFactory = httpClientFactory; } - public async Task<XDocument> SendCommandAsync(string baseUrl, + public async Task<XDocument> SendCommandAsync( + string baseUrl, DeviceService service, string command, string postData, - bool logRequest = true, - string header = null) + string header = null, + CancellationToken cancellationToken = default) { - var cancellationToken = CancellationToken.None; - - using (var response = await PostSoapDataAsync(NormalizeServiceUrl(baseUrl, service.ControlUrl), "\"" + service.ServiceType + "#" + command + "\"", postData, header, logRequest, cancellationToken) - .ConfigureAwait(false)) - { - using (var stream = response.Content) - { - using (var reader = new StreamReader(stream, Encoding.UTF8)) - { - return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace); - } - } - } + var url = NormalizeServiceUrl(baseUrl, service.ControlUrl); + using var response = await PostSoapDataAsync( + url, + $"\"{service.ServiceType}#{command}\"", + postData, + header, + cancellationToken) + .ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + return await XDocument.LoadAsync( + stream, + LoadOptions.None, + cancellationToken).ConfigureAwait(false); } private static string NormalizeServiceUrl(string baseUrl, string serviceUrl) @@ -55,110 +60,82 @@ namespace Emby.Dlna.PlayTo return serviceUrl; } - if (!serviceUrl.StartsWith("/")) + if (!serviceUrl.StartsWith('/')) + { serviceUrl = "/" + serviceUrl; + } return baseUrl + serviceUrl; } - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - public async Task SubscribeAsync(string url, + public async Task SubscribeAsync( + string url, string ip, int port, string localIp, int eventport, int timeOut = 3600) { - var options = new HttpRequestOptions - { - Url = url, - UserAgent = USERAGENT, - LogErrorResponseBody = true, - BufferContent = false, - - // The periodic requests may keep some devices awake - LogRequestAsDebug = true - }; - - options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture); - options.RequestHeaders["CALLBACK"] = "<" + localIp + ":" + eventport.ToString(_usCulture) + ">"; - options.RequestHeaders["NT"] = "upnp:event"; - options.RequestHeaders["TIMEOUT"] = "Second-" + timeOut.ToString(_usCulture); - - using (await _httpClient.SendAsync(options, "SUBSCRIBE").ConfigureAwait(false)) - { - - } + using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url); + options.Headers.UserAgent.ParseAdd(USERAGENT); + options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture)); + options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">"); + options.Headers.TryAddWithoutValidation("NT", "upnp:event"); + options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture)); + + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(options, HttpCompletionOption.ResponseHeadersRead) + .ConfigureAwait(false); + response.EnsureSuccessStatusCode(); } public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken) { - var options = new HttpRequestOptions + using var options = new HttpRequestMessage(HttpMethod.Get, url); + options.Headers.UserAgent.ParseAdd(USERAGENT); + options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + try { - Url = url, - UserAgent = USERAGENT, - LogErrorResponseBody = true, - BufferContent = false, - - // The periodic requests may keep some devices awake - LogRequestAsDebug = true, - - CancellationToken = cancellationToken - }; - - options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName; - - using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false)) + return await XDocument.LoadAsync( + stream, + LoadOptions.None, + cancellationToken).ConfigureAwait(false); + } + catch { - using (var stream = response.Content) - { - using (var reader = new StreamReader(stream, Encoding.UTF8)) - { - return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace); - } - } + return null; } } - private Task<HttpResponseInfo> PostSoapDataAsync(string url, + private async Task<HttpResponseMessage> PostSoapDataAsync( + string url, string soapAction, string postData, string header, - bool logRequest, CancellationToken cancellationToken) { - if (!soapAction.StartsWith("\"")) - soapAction = "\"" + soapAction + "\""; - - var options = new HttpRequestOptions + if (soapAction[0] != '\"') { - Url = url, - UserAgent = USERAGENT, - LogRequest = logRequest || _config.GetDlnaConfiguration().EnableDebugLog, - LogErrorResponseBody = true, - BufferContent = false, - - // The periodic requests may keep some devices awake - LogRequestAsDebug = true, - - CancellationToken = cancellationToken - }; + soapAction = $"\"{soapAction}\""; + } - options.RequestHeaders["SOAPAction"] = soapAction; - options.RequestHeaders["Pragma"] = "no-cache"; - options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName; + using var options = new HttpRequestMessage(HttpMethod.Post, url); + options.Headers.UserAgent.ParseAdd(USERAGENT); + options.Headers.TryAddWithoutValidation("SOAPACTION", soapAction); + options.Headers.TryAddWithoutValidation("Pragma", "no-cache"); + options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName); if (!string.IsNullOrEmpty(header)) { - options.RequestHeaders["contentFeatures.dlna.org"] = header; + options.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header); } - options.RequestContentType = "text/xml"; - options.AppendCharsetToMimeType = true; - options.RequestContent = postData; + options.Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml); - return _httpClient.Post(options); + return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); } } } diff --git a/Emby.Dlna/PlayTo/TRANSPORTSTATE.cs b/Emby.Dlna/PlayTo/TRANSPORTSTATE.cs deleted file mode 100644 index 9f1690b04..000000000 --- a/Emby.Dlna/PlayTo/TRANSPORTSTATE.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Emby.Dlna.PlayTo -{ - public enum TRANSPORTSTATE - { - STOPPED, - PLAYING, - TRANSITIONING, - PAUSED_PLAYBACK, - PAUSED - } -} diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs index b96fa43e5..b58669355 100644 --- a/Emby.Dlna/PlayTo/TransportCommands.cs +++ b/Emby.Dlna/PlayTo/TransportCommands.cs @@ -1,5 +1,8 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Xml.Linq; using Emby.Dlna.Common; @@ -9,36 +12,28 @@ namespace Emby.Dlna.PlayTo { public class TransportCommands { - private List<StateVariable> _stateVariables = new List<StateVariable>(); - public List<StateVariable> StateVariables - { - get => _stateVariables; - set => _stateVariables = value; - } + private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>"; - private List<ServiceAction> _serviceActions = new List<ServiceAction>(); - public List<ServiceAction> ServiceActions - { - get => _serviceActions; - set => _serviceActions = value; - } + public List<StateVariable> StateVariables { get; } = new List<StateVariable>(); + + public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>(); public static TransportCommands Create(XDocument document) { var command = new TransportCommands(); - var actionList = document.Descendants(uPnpNamespaces.svc + "actionList"); + var actionList = document.Descendants(UPnpNamespaces.Svc + "actionList"); - foreach (var container in actionList.Descendants(uPnpNamespaces.svc + "action")) + foreach (var container in actionList.Descendants(UPnpNamespaces.Svc + "action")) { command.ServiceActions.Add(ServiceActionFromXml(container)); } - var stateValues = document.Descendants(uPnpNamespaces.ServiceStateTable).FirstOrDefault(); + var stateValues = document.Descendants(UPnpNamespaces.ServiceStateTable).FirstOrDefault(); if (stateValues != null) { - foreach (var container in stateValues.Elements(uPnpNamespaces.svc + "stateVariable")) + foreach (var container in stateValues.Elements(UPnpNamespaces.Svc + "stateVariable")) { command.StateVariables.Add(FromXml(container)); } @@ -49,19 +44,19 @@ namespace Emby.Dlna.PlayTo private static ServiceAction ServiceActionFromXml(XElement container) { - var argumentList = new List<Argument>(); + var serviceAction = new ServiceAction + { + Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, + }; - foreach (var arg in container.Descendants(uPnpNamespaces.svc + "argument")) + var argumentList = serviceAction.ArgumentList; + + foreach (var arg in container.Descendants(UPnpNamespaces.Svc + "argument")) { argumentList.Add(ArgumentFromXml(arg)); } - return new ServiceAction - { - Name = container.GetValue(uPnpNamespaces.svc + "name"), - - ArgumentList = argumentList - }; + return serviceAction; } private static Argument ArgumentFromXml(XElement container) @@ -73,30 +68,30 @@ namespace Emby.Dlna.PlayTo return new Argument { - Name = container.GetValue(uPnpNamespaces.svc + "name"), - Direction = container.GetValue(uPnpNamespaces.svc + "direction"), - RelatedStateVariable = container.GetValue(uPnpNamespaces.svc + "relatedStateVariable") + Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, + Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty, + RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty }; } private static StateVariable FromXml(XElement container) { - var allowedValues = new List<string>(); - var element = container.Descendants(uPnpNamespaces.svc + "allowedValueList") + var allowedValues = Array.Empty<string>(); + var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList") .FirstOrDefault(); if (element != null) { - var values = element.Descendants(uPnpNamespaces.svc + "allowedValue"); + var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue"); - allowedValues.AddRange(values.Select(child => child.Value)); + allowedValues = values.Select(child => child.Value).ToArray(); } return new StateVariable { - Name = container.GetValue(uPnpNamespaces.svc + "name"), - DataType = container.GetValue(uPnpNamespaces.svc + "dataType"), - AllowedValues = allowedValues.ToArray() + Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, + DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty, + AllowedValues = allowedValues }; } @@ -106,16 +101,22 @@ namespace Emby.Dlna.PlayTo foreach (var arg in action.ArgumentList) { - if (arg.Direction == "out") + if (string.Equals(arg.Direction, "out", StringComparison.Ordinal)) + { continue; + } - if (arg.Name == "InstanceID") + if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal)) + { stateString += BuildArgumentXml(arg, "0"); + } else + { stateString += BuildArgumentXml(arg, null); + } } - return string.Format(CommandBase, action.Name, xmlNamespace, stateString); + return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString); } public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "") @@ -124,15 +125,22 @@ namespace Emby.Dlna.PlayTo foreach (var arg in action.ArgumentList) { - if (arg.Direction == "out") + if (string.Equals(arg.Direction, "out", StringComparison.Ordinal)) + { continue; - if (arg.Name == "InstanceID") + } + + if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal)) + { stateString += BuildArgumentXml(arg, "0"); + } else + { stateString += BuildArgumentXml(arg, value.ToString(), commandParameter); + } } - return string.Format(CommandBase, action.Name, xmlNamesapce, stateString); + return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString); } public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary) @@ -141,33 +149,36 @@ namespace Emby.Dlna.PlayTo foreach (var arg in action.ArgumentList) { - if (arg.Name == "InstanceID") + if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal)) + { stateString += BuildArgumentXml(arg, "0"); + } else if (dictionary.ContainsKey(arg.Name)) + { stateString += BuildArgumentXml(arg, dictionary[arg.Name]); + } else + { stateString += BuildArgumentXml(arg, value.ToString()); + } } - return string.Format(CommandBase, action.Name, xmlNamesapce, stateString); + return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString); } - private string BuildArgumentXml(Argument argument, string value, string commandParameter = "") + private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "") { var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase)); if (state != null) { var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ?? - state.AllowedValues.FirstOrDefault() ?? - value; + (state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value); - return string.Format("<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue); + return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue); } - return string.Format("<{0}>{1}</{0}>", argument.Name, value); + return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value); } - - private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>"; } } diff --git a/Emby.Dlna/PlayTo/TransportState.cs b/Emby.Dlna/PlayTo/TransportState.cs new file mode 100644 index 000000000..2058e9dc7 --- /dev/null +++ b/Emby.Dlna/PlayTo/TransportState.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 + +namespace Emby.Dlna.PlayTo +{ + public enum TransportState + { + Stopped, + Playing, + Transitioning, + PausedPlayback, + Paused + } +} diff --git a/Emby.Dlna/PlayTo/TransportStateEventArgs.cs b/Emby.Dlna/PlayTo/TransportStateEventArgs.cs deleted file mode 100644 index 7dcd39e10..000000000 --- a/Emby.Dlna/PlayTo/TransportStateEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Emby.Dlna.PlayTo -{ - public class TransportStateEventArgs : EventArgs - { - public TRANSPORTSTATE State { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/UpnpContainer.cs b/Emby.Dlna/PlayTo/UpnpContainer.cs index 943e0347b..05f27603f 100644 --- a/Emby.Dlna/PlayTo/UpnpContainer.cs +++ b/Emby.Dlna/PlayTo/UpnpContainer.cs @@ -1,25 +1,27 @@ +#pragma warning disable CS1591 + using System; using System.Xml.Linq; using Emby.Dlna.Ssdp; namespace Emby.Dlna.PlayTo { - public class UpnpContainer : uBaseObject + public class UpnpContainer : UBaseObject { - public static uBaseObject Create(XElement container) + public static UBaseObject Create(XElement container) { if (container == null) { throw new ArgumentNullException(nameof(container)); } - return new uBaseObject + return new UBaseObject { - Id = container.GetAttributeValue(uPnpNamespaces.Id), - ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId), - Title = container.GetValue(uPnpNamespaces.title), - IconUrl = container.GetValue(uPnpNamespaces.Artwork), - UpnpClass = container.GetValue(uPnpNamespaces.uClass) + Id = container.GetAttributeValue(UPnpNamespaces.Id), + ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId), + Title = container.GetValue(UPnpNamespaces.Title), + IconUrl = container.GetValue(UPnpNamespaces.Artwork), + UpnpClass = container.GetValue(UPnpNamespaces.Class) }; } } diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs index f29a126df..02d2da58d 100644 --- a/Emby.Dlna/PlayTo/uBaseObject.cs +++ b/Emby.Dlna/PlayTo/uBaseObject.cs @@ -1,8 +1,13 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Collections.Generic; namespace Emby.Dlna.PlayTo { - public class uBaseObject + public class UBaseObject { public string Id { get; set; } @@ -18,20 +23,10 @@ namespace Emby.Dlna.PlayTo public string Url { get; set; } - public string[] ProtocolInfo { get; set; } + public IReadOnlyList<string> ProtocolInfo { get; set; } public string UpnpClass { get; set; } - public bool Equals(uBaseObject obj) - { - if (obj == null) - { - throw new ArgumentNullException(nameof(obj)); - } - - return string.Equals(Id, obj.Id); - } - public string MediaType { get @@ -42,10 +37,12 @@ namespace Emby.Dlna.PlayTo { return MediaBrowser.Model.Entities.MediaType.Audio; } + if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1) { return MediaBrowser.Model.Entities.MediaType.Video; } + if (classType.IndexOf("image", StringComparison.Ordinal) != -1) { return MediaBrowser.Model.Entities.MediaType.Photo; @@ -54,5 +51,15 @@ namespace Emby.Dlna.PlayTo return null; } } + + public bool Equals(UBaseObject obj) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + + return string.Equals(Id, obj.Id, StringComparison.Ordinal); + } } } diff --git a/Emby.Dlna/PlayTo/uParser.cs b/Emby.Dlna/PlayTo/uParser.cs deleted file mode 100644 index 3a0ffffd4..000000000 --- a/Emby.Dlna/PlayTo/uParser.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Emby.Dlna.PlayTo -{ - public class uParser - { - public static IList<uBaseObject> ParseBrowseXml(XDocument doc) - { - if (doc == null) - { - throw new ArgumentException("doc"); - } - - var list = new List<uBaseObject>(); - - var document = doc.Document; - - if (document == null) - return list; - - var item = (from result in document.Descendants("Result") select result).FirstOrDefault(); - - if (item == null) - return list; - - var uPnpResponse = XElement.Parse((string)item); - - var uObjects = from container in uPnpResponse.Elements(uPnpNamespaces.containers) - select new uParserObject { Element = container }; - - var uObjects2 = from container in uPnpResponse.Elements(uPnpNamespaces.items) - select new uParserObject { Element = container }; - - list.AddRange(uObjects.Concat(uObjects2).Select(CreateObjectFromXML).Where(uObject => uObject != null)); - - return list; - } - - public static uBaseObject CreateObjectFromXML(uParserObject uItem) - { - return UpnpContainer.Create(uItem.Element); - } - } -} diff --git a/Emby.Dlna/PlayTo/uParserObject.cs b/Emby.Dlna/PlayTo/uParserObject.cs deleted file mode 100644 index 87a7f69c6..000000000 --- a/Emby.Dlna/PlayTo/uParserObject.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Xml.Linq; - -namespace Emby.Dlna.PlayTo -{ - public class uParserObject - { - public XElement Element { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/uPnpNamespaces.cs b/Emby.Dlna/PlayTo/uPnpNamespaces.cs index 7132ecd15..5042d4493 100644 --- a/Emby.Dlna/PlayTo/uPnpNamespaces.cs +++ b/Emby.Dlna/PlayTo/uPnpNamespaces.cs @@ -1,39 +1,67 @@ +#pragma warning disable CS1591 + using System.Xml.Linq; namespace Emby.Dlna.PlayTo { - public class uPnpNamespaces + public static class UPnpNamespaces { - public static XNamespace dc = "http://purl.org/dc/elements/1.1/"; - public static XNamespace ns = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; - public static XNamespace svc = "urn:schemas-upnp-org:service-1-0"; - public static XNamespace ud = "urn:schemas-upnp-org:device-1-0"; - public static XNamespace upnp = "urn:schemas-upnp-org:metadata-1-0/upnp/"; - public static XNamespace RenderingControl = "urn:schemas-upnp-org:service:RenderingControl:1"; - public static XNamespace AvTransport = "urn:schemas-upnp-org:service:AVTransport:1"; - public static XNamespace ContentDirectory = "urn:schemas-upnp-org:service:ContentDirectory:1"; - - public static XName containers = ns + "container"; - public static XName items = ns + "item"; - public static XName title = dc + "title"; - public static XName creator = dc + "creator"; - public static XName artist = upnp + "artist"; - public static XName Id = "id"; - public static XName ParentId = "parentID"; - public static XName uClass = upnp + "class"; - public static XName Artwork = upnp + "albumArtURI"; - public static XName Description = dc + "description"; - public static XName LongDescription = upnp + "longDescription"; - public static XName Album = upnp + "album"; - public static XName Author = upnp + "author"; - public static XName Director = upnp + "director"; - public static XName PlayCount = upnp + "playbackCount"; - public static XName Tracknumber = upnp + "originalTrackNumber"; - public static XName Res = ns + "res"; - public static XName Duration = "duration"; - public static XName ProtocolInfo = "protocolInfo"; - - public static XName ServiceStateTable = svc + "serviceStateTable"; - public static XName StateVariable = svc + "stateVariable"; + public static XNamespace Dc { get; } = "http://purl.org/dc/elements/1.1/"; + + public static XNamespace Ns { get; } = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; + + public static XNamespace Svc { get; } = "urn:schemas-upnp-org:service-1-0"; + + public static XNamespace Ud { get; } = "urn:schemas-upnp-org:device-1-0"; + + public static XNamespace UPnp { get; } = "urn:schemas-upnp-org:metadata-1-0/upnp/"; + + public static XNamespace RenderingControl { get; } = "urn:schemas-upnp-org:service:RenderingControl:1"; + + public static XNamespace AvTransport { get; } = "urn:schemas-upnp-org:service:AVTransport:1"; + + public static XNamespace ContentDirectory { get; } = "urn:schemas-upnp-org:service:ContentDirectory:1"; + + public static XName Containers { get; } = Ns + "container"; + + public static XName Items { get; } = Ns + "item"; + + public static XName Title { get; } = Dc + "title"; + + public static XName Creator { get; } = Dc + "creator"; + + public static XName Artist { get; } = UPnp + "artist"; + + public static XName Id { get; } = "id"; + + public static XName ParentId { get; } = "parentID"; + + public static XName Class { get; } = UPnp + "class"; + + public static XName Artwork { get; } = UPnp + "albumArtURI"; + + public static XName Description { get; } = Dc + "description"; + + public static XName LongDescription { get; } = UPnp + "longDescription"; + + public static XName Album { get; } = UPnp + "album"; + + public static XName Author { get; } = UPnp + "author"; + + public static XName Director { get; } = UPnp + "director"; + + public static XName PlayCount { get; } = UPnp + "playbackCount"; + + public static XName Tracknumber { get; } = UPnp + "originalTrackNumber"; + + public static XName Res { get; } = Ns + "res"; + + public static XName Duration { get; } = "duration"; + + public static XName ProtocolInfo { get; } = "protocolInfo"; + + public static XName ServiceStateTable { get; } = Svc + "serviceStateTable"; + + public static XName StateVariable { get; } = Svc + "stateVariable"; } } diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs index ea50bd4a7..8eaf12ba9 100644 --- a/Emby.Dlna/Profiles/DefaultProfile.cs +++ b/Emby.Dlna/Profiles/DefaultProfile.cs @@ -1,3 +1,7 @@ +#pragma warning disable CS1591 + +using System; +using System.Globalization; using System.Linq; using MediaBrowser.Model.Dlna; @@ -8,9 +12,10 @@ namespace Emby.Dlna.Profiles { public DefaultProfile() { + Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); Name = "Generic Device"; - ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*"; + ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*"; Manufacturer = "Jellyfin"; ModelDescription = "UPnP/AV 1.0 Compliant Media Server"; @@ -62,14 +67,14 @@ namespace Emby.Dlna.Profiles new DirectPlayProfile { // play all - Container = "", + Container = string.Empty, Type = DlnaProfileType.Video }, new DirectPlayProfile { // play all - Container = "", + Container = string.Empty, Type = DlnaProfileType.Audio } }; @@ -162,7 +167,7 @@ namespace Emby.Dlna.Profiles public void AddXmlRootAttribute(string name, string value) { - var atts = XmlRootAttributes ?? new XmlAttribute[] { }; + var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>(); var list = atts.ToList(); list.Add(new XmlAttribute diff --git a/Emby.Dlna/Profiles/DenonAvrProfile.cs b/Emby.Dlna/Profiles/DenonAvrProfile.cs index a73885191..a5ba0f36c 100644 --- a/Emby.Dlna/Profiles/DenonAvrProfile.cs +++ b/Emby.Dlna/Profiles/DenonAvrProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -26,7 +28,7 @@ namespace Emby.Dlna.Profiles }, }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = System.Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/DirectTvProfile.cs b/Emby.Dlna/Profiles/DirectTvProfile.cs index 317c0976a..f6f98b07d 100644 --- a/Emby.Dlna/Profiles/DirectTvProfile.cs +++ b/Emby.Dlna/Profiles/DirectTvProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -121,7 +123,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = System.Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/DishHopperJoeyProfile.cs b/Emby.Dlna/Profiles/DishHopperJoeyProfile.cs index d501cce0d..2a7524a6a 100644 --- a/Emby.Dlna/Profiles/DishHopperJoeyProfile.cs +++ b/Emby.Dlna/Profiles/DishHopperJoeyProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -9,7 +11,7 @@ namespace Emby.Dlna.Profiles { Name = "Dish Hopper-Joey"; - ProtocolInfo = "http-get:*:video/mp2t:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*"; + ProtocolInfo = "http-get:*:video/mp2t:*,http-get:*:video/mpeg:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*"; Identification = new DeviceIdentification { @@ -22,7 +24,7 @@ namespace Emby.Dlna.Profiles { Match = HeaderMatchType.Substring, Name = "User-Agent", - Value ="Zip_" + Value = "Zip_" } } }; @@ -79,7 +81,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -122,7 +124,7 @@ namespace Emby.Dlna.Profiles new CodecProfile { Type = CodecType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -159,7 +161,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "ac3,he-aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -175,7 +177,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -190,7 +192,7 @@ namespace Emby.Dlna.Profiles new CodecProfile { Type = CodecType.VideoAudio, - Conditions = new [] + Conditions = new[] { // The device does not have any audio switching capabilities new ProfileCondition diff --git a/Emby.Dlna/Profiles/Foobar2000Profile.cs b/Emby.Dlna/Profiles/Foobar2000Profile.cs index 947194bce..68e959770 100644 --- a/Emby.Dlna/Profiles/Foobar2000Profile.cs +++ b/Emby.Dlna/Profiles/Foobar2000Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -70,7 +72,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = System.Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/LgTvProfile.cs b/Emby.Dlna/Profiles/LgTvProfile.cs index 145685ab1..fbb368d3e 100644 --- a/Emby.Dlna/Profiles/LgTvProfile.cs +++ b/Emby.Dlna/Profiles/LgTvProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -82,7 +84,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -189,7 +191,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] + ResponseProfiles = new[] { new ResponseProfile { diff --git a/Emby.Dlna/Profiles/LinksysDMA2100Profile.cs b/Emby.Dlna/Profiles/LinksysDMA2100Profile.cs index 3f0bb4263..1a510dfec 100644 --- a/Emby.Dlna/Profiles/LinksysDMA2100Profile.cs +++ b/Emby.Dlna/Profiles/LinksysDMA2100Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -30,7 +32,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] + ResponseProfiles = new[] { new ResponseProfile { diff --git a/Emby.Dlna/Profiles/MarantzProfile.cs b/Emby.Dlna/Profiles/MarantzProfile.cs index 162e284be..24078ab29 100644 --- a/Emby.Dlna/Profiles/MarantzProfile.cs +++ b/Emby.Dlna/Profiles/MarantzProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -35,7 +37,7 @@ namespace Emby.Dlna.Profiles }, }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = System.Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/MediaMonkeyProfile.cs b/Emby.Dlna/Profiles/MediaMonkeyProfile.cs index 53cae4e2f..28d639b6d 100644 --- a/Emby.Dlna/Profiles/MediaMonkeyProfile.cs +++ b/Emby.Dlna/Profiles/MediaMonkeyProfile.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 + +using System; using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -35,7 +38,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/PanasonicVieraProfile.cs b/Emby.Dlna/Profiles/PanasonicVieraProfile.cs index 5f31ec484..0d536acf3 100644 --- a/Emby.Dlna/Profiles/PanasonicVieraProfile.cs +++ b/Emby.Dlna/Profiles/PanasonicVieraProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -136,7 +138,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/PopcornHourProfile.cs b/Emby.Dlna/Profiles/PopcornHourProfile.cs index aefe8c44f..7fbf8c164 100644 --- a/Emby.Dlna/Profiles/PopcornHourProfile.cs +++ b/Emby.Dlna/Profiles/PopcornHourProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -91,8 +93,8 @@ namespace Emby.Dlna.Profiles new CodecProfile { Type = CodecType.Video, - Codec="h264", - Conditions = new [] + Codec = "h264", + Conditions = new[] { new ProfileCondition(ProfileConditionType.EqualsAny, ProfileConditionValue.VideoProfile, "baseline|constrained baseline"), new ProfileCondition @@ -120,7 +122,7 @@ namespace Emby.Dlna.Profiles new CodecProfile { Type = CodecType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -148,7 +150,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -164,7 +166,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Audio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -180,7 +182,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Audio, Codec = "mp3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -200,7 +202,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] + ResponseProfiles = new[] { new ResponseProfile { diff --git a/Emby.Dlna/Profiles/SamsungSmartTvProfile.cs b/Emby.Dlna/Profiles/SamsungSmartTvProfile.cs index 51a1c8173..ddbebba2a 100644 --- a/Emby.Dlna/Profiles/SamsungSmartTvProfile.cs +++ b/Emby.Dlna/Profiles/SamsungSmartTvProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -137,7 +139,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SharpSmartTvProfile.cs b/Emby.Dlna/Profiles/SharpSmartTvProfile.cs index f840cfb34..aa8d434e3 100644 --- a/Emby.Dlna/Profiles/SharpSmartTvProfile.cs +++ b/Emby.Dlna/Profiles/SharpSmartTvProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs index 2af1d3b50..765c12504 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 + +using System; using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -147,7 +150,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -175,7 +178,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -194,7 +197,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -221,7 +224,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs index 3de0b5192..390c8a3e4 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 + +using System; using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -147,7 +150,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -175,7 +178,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -194,7 +197,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -221,7 +224,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs index 889484bea..25adc4d02 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 + +using System; using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -135,7 +138,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -163,7 +166,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -182,7 +185,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -209,7 +212,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs index acb90bd01..0a39a5f40 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 + +using System; using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -135,7 +138,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -163,7 +166,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -182,7 +185,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -209,7 +212,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs b/Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs index e1808b205..05c8ab1c1 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -112,7 +114,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -154,7 +156,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -170,7 +172,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -189,7 +191,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -215,7 +217,7 @@ namespace Emby.Dlna.Profiles VideoCodec = "h264,mpeg4,vc1", AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", + OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", Type = DlnaProfileType.Video }, diff --git a/Emby.Dlna/Profiles/SonyBravia2010Profile.cs b/Emby.Dlna/Profiles/SonyBravia2010Profile.cs index f8e8faa76..9f0d82b8f 100644 --- a/Emby.Dlna/Profiles/SonyBravia2010Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2010Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -11,7 +13,7 @@ namespace Emby.Dlna.Profiles Identification = new DeviceIdentification { - FriendlyName = @"KDL-\d{2}[EHLNPB]X\d[01]\d.*", + FriendlyName = @"KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*", Manufacturer = "Sony", Headers = new[] @@ -19,7 +21,7 @@ namespace Emby.Dlna.Profiles new HttpHeaderInfo { Name = "X-AV-Client-Info", - Value = @".*KDL-\d{2}[EHLNPB]X\d[01]\d.*", + Value = @".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*", Match = HeaderMatchType.Regex } } @@ -100,13 +102,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", + OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -126,13 +128,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/mpeg", - OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", + OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -146,28 +148,28 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", + OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "ts,mpegts", - VideoCodec="mpeg2video", + VideoCodec = "mpeg2video", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", + OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "mpeg", - VideoCodec="mpeg1video,mpeg2video", + VideoCodec = "mpeg1video,mpeg2video", MimeType = "video/mpeg", - OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", + OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL", Type = DlnaProfileType.Video } }; @@ -178,7 +180,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -202,7 +204,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -241,7 +243,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "mpeg2video", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -273,7 +275,7 @@ namespace Emby.Dlna.Profiles new CodecProfile { Type = CodecType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -301,7 +303,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -317,7 +319,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -339,7 +341,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "mp3,mp2", - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyBravia2011Profile.cs b/Emby.Dlna/Profiles/SonyBravia2011Profile.cs index 111f36e9b..dfb91817a 100644 --- a/Emby.Dlna/Profiles/SonyBravia2011Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2011Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -11,7 +13,7 @@ namespace Emby.Dlna.Profiles Identification = new DeviceIdentification { - FriendlyName = @"KDL-\d{2}([A-Z]X\d2\d|CX400).*", + FriendlyName = @"KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*", Manufacturer = "Sony", Headers = new[] @@ -19,7 +21,7 @@ namespace Emby.Dlna.Profiles new HttpHeaderInfo { Name = "X-AV-Client-Info", - Value = @".*KDL-\d{2}([A-Z]X\d2\d|CX400).*", + Value = @".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*", Match = HeaderMatchType.Regex } } @@ -118,7 +120,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -141,13 +143,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", + OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -167,13 +169,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/mpeg", - OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", + OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -187,28 +189,28 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", + OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "ts,mpegts", - VideoCodec="mpeg2video", + VideoCodec = "mpeg2video", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", + OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "mpeg", - VideoCodec="mpeg1video,mpeg2video", + VideoCodec = "mpeg1video,mpeg2video", MimeType = "video/mpeg", - OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", + OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL", Type = DlnaProfileType.Video }, new ResponseProfile @@ -225,7 +227,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -264,7 +266,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "mpeg2video", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -296,7 +298,7 @@ namespace Emby.Dlna.Profiles new CodecProfile { Type = CodecType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -324,7 +326,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -362,7 +364,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "mp3,mp2", - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyBravia2012Profile.cs b/Emby.Dlna/Profiles/SonyBravia2012Profile.cs index d5efe4270..d59ee38d7 100644 --- a/Emby.Dlna/Profiles/SonyBravia2012Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2012Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -11,7 +13,7 @@ namespace Emby.Dlna.Profiles Identification = new DeviceIdentification { - FriendlyName = @"KDL-\d{2}[A-Z]X\d5(\d|G).*", + FriendlyName = @"KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*", Manufacturer = "Sony", Headers = new[] @@ -19,7 +21,7 @@ namespace Emby.Dlna.Profiles new HttpHeaderInfo { Name = "X-AV-Client-Info", - Value = @".*KDL-\d{2}[A-Z]X\d5(\d|G).*", + Value = @".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*", Match = HeaderMatchType.Regex } } @@ -129,13 +131,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", + OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -155,13 +157,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/mpeg", - OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", + OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -175,28 +177,28 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", + OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "ts,mpegts", - VideoCodec="mpeg2video", + VideoCodec = "mpeg2video", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", + OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "mpeg", - VideoCodec="mpeg1video,mpeg2video", + VideoCodec = "mpeg1video,mpeg2video", MimeType = "video/mpeg", - OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", + OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL", Type = DlnaProfileType.Video }, new ResponseProfile @@ -213,7 +215,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -280,7 +282,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "mp3,mp2", - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyBravia2013Profile.cs b/Emby.Dlna/Profiles/SonyBravia2013Profile.cs index 3b0228694..73b0fd67e 100644 --- a/Emby.Dlna/Profiles/SonyBravia2013Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2013Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -11,7 +13,7 @@ namespace Emby.Dlna.Profiles Identification = new DeviceIdentification { - FriendlyName = @"KDL-\d{2}[WR][5689]\d{2}A.*", + FriendlyName = @"KDL-[0-9]{2}[WR][5689][0-9]{2}A.*", Manufacturer = "Sony", Headers = new[] @@ -19,7 +21,7 @@ namespace Emby.Dlna.Profiles new HttpHeaderInfo { Name = "X-AV-Client-Info", - Value = @".*KDL-\d{2}[WR][5689]\d{2}A.*", + Value = @".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*", Match = HeaderMatchType.Regex } } @@ -162,7 +164,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -185,13 +187,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", + OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -211,13 +213,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/mpeg", - OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", + OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -231,28 +233,28 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", + OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "ts,mpegts", - VideoCodec="mpeg2video", + VideoCodec = "mpeg2video", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", + OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "mpeg", - VideoCodec="mpeg1video,mpeg2video", + VideoCodec = "mpeg1video,mpeg2video", MimeType = "video/mpeg", - OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", + OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL", Type = DlnaProfileType.Video }, new ResponseProfile @@ -263,14 +265,13 @@ namespace Emby.Dlna.Profiles } }; - CodecProfiles = new[] { new CodecProfile { Type = CodecType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -298,7 +299,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "mp3,mp2", - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyBravia2014Profile.cs b/Emby.Dlna/Profiles/SonyBravia2014Profile.cs index e860eae34..db8ee5750 100644 --- a/Emby.Dlna/Profiles/SonyBravia2014Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2014Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -11,7 +13,7 @@ namespace Emby.Dlna.Profiles Identification = new DeviceIdentification { - FriendlyName = @"(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*", + FriendlyName = @"(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*", Manufacturer = "Sony", Headers = new[] @@ -19,7 +21,7 @@ namespace Emby.Dlna.Profiles new HttpHeaderInfo { Name = "X-AV-Client-Info", - Value = @".*(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*", + Value = @".*(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*", Match = HeaderMatchType.Regex } } @@ -162,7 +164,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -185,13 +187,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", + OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -211,13 +213,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/mpeg", - OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", + OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -231,28 +233,28 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", + OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "ts,mpegts", - VideoCodec="mpeg2video", + VideoCodec = "mpeg2video", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", + OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "mpeg", - VideoCodec="mpeg1video,mpeg2video", + VideoCodec = "mpeg1video,mpeg2video", MimeType = "video/mpeg", - OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", + OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL", Type = DlnaProfileType.Video }, new ResponseProfile @@ -263,14 +265,13 @@ namespace Emby.Dlna.Profiles } }; - CodecProfiles = new[] { new CodecProfile { Type = CodecType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -298,7 +299,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "mp3,mp2", - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyPs3Profile.cs b/Emby.Dlna/Profiles/SonyPs3Profile.cs index 88d064695..e4a7a3a59 100644 --- a/Emby.Dlna/Profiles/SonyPs3Profile.cs +++ b/Emby.Dlna/Profiles/SonyPs3Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -50,7 +52,7 @@ namespace Emby.Dlna.Profiles Container = "ts,mpegts", Type = DlnaProfileType.Video, VideoCodec = "mpeg1video,mpeg2video,h264", - AudioCodec = "ac3,mp2,mp3,aac" + AudioCodec = "aac,ac3,mp2" }, new DirectPlayProfile { @@ -90,7 +92,7 @@ namespace Emby.Dlna.Profiles { Container = "ts", VideoCodec = "h264", - AudioCodec = "ac3,aac,mp3", + AudioCodec = "aac,ac3,mp2", Type = DlnaProfileType.Video }, new TranscodingProfile @@ -106,7 +108,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -131,7 +133,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -174,7 +176,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -199,7 +201,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "wmapro", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -215,7 +217,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -233,7 +235,7 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "mp4,mov", - AudioCodec="aac", + AudioCodec = "aac", MimeType = "video/mp4", Type = DlnaProfileType.Video }, @@ -242,7 +244,7 @@ namespace Emby.Dlna.Profiles { Container = "avi", MimeType = "video/divx", - OrgPn="AVI", + OrgPn = "AVI", Type = DlnaProfileType.Video }, diff --git a/Emby.Dlna/Profiles/SonyPs4Profile.cs b/Emby.Dlna/Profiles/SonyPs4Profile.cs index 499cf8803..985df0c9a 100644 --- a/Emby.Dlna/Profiles/SonyPs4Profile.cs +++ b/Emby.Dlna/Profiles/SonyPs4Profile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -50,7 +52,7 @@ namespace Emby.Dlna.Profiles Container = "ts,mpegts", Type = DlnaProfileType.Video, VideoCodec = "mpeg1video,mpeg2video,h264", - AudioCodec = "ac3,mp2,mp3,aac" + AudioCodec = "aac,ac3,mp2" }, new DirectPlayProfile { @@ -92,7 +94,7 @@ namespace Emby.Dlna.Profiles { Container = "ts", VideoCodec = "h264", - AudioCodec = "mp3", + AudioCodec = "aac,ac3,mp2", Type = DlnaProfileType.Video }, new TranscodingProfile @@ -108,7 +110,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -133,7 +135,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -176,7 +178,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -201,7 +203,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "wmapro", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -217,7 +219,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -235,7 +237,7 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "mp4,mov", - AudioCodec="aac", + AudioCodec = "aac", MimeType = "video/mp4", Type = DlnaProfileType.Video }, @@ -244,7 +246,7 @@ namespace Emby.Dlna.Profiles { Container = "avi", MimeType = "video/divx", - OrgPn="AVI", + OrgPn = "AVI", Type = DlnaProfileType.Video }, diff --git a/Emby.Dlna/Profiles/WdtvLiveProfile.cs b/Emby.Dlna/Profiles/WdtvLiveProfile.cs index bf7b1ab47..937ca0f42 100644 --- a/Emby.Dlna/Profiles/WdtvLiveProfile.cs +++ b/Emby.Dlna/Profiles/WdtvLiveProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -18,7 +20,7 @@ namespace Emby.Dlna.Profiles Headers = new[] { - new HttpHeaderInfo {Name = "User-Agent", Value = "alphanetworks", Match = HeaderMatchType.Substring}, + new HttpHeaderInfo { Name = "User-Agent", Value = "alphanetworks", Match = HeaderMatchType.Substring }, new HttpHeaderInfo { Name = "User-Agent", @@ -166,7 +168,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -191,7 +193,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -219,7 +221,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/XboxOneProfile.cs b/Emby.Dlna/Profiles/XboxOneProfile.cs index 710b891e3..84d8184a2 100644 --- a/Emby.Dlna/Profiles/XboxOneProfile.cs +++ b/Emby.Dlna/Profiles/XboxOneProfile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -117,7 +119,7 @@ namespace Emby.Dlna.Profiles Type = DlnaProfileType.Video, Container = "mp4,mov", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -136,7 +138,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "mpeg4", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -185,7 +187,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -234,7 +236,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "wmv2,wmv3,vc1", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -282,7 +284,7 @@ namespace Emby.Dlna.Profiles new CodecProfile { Type = CodecType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -305,7 +307,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "ac3,wmav2,wmapro", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -321,7 +323,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/Xml/Default.xml b/Emby.Dlna/Profiles/Xml/Default.xml index daac4135a..9460f9d5a 100644 --- a/Emby.Dlna/Profiles/Xml/Default.xml +++ b/Emby.Dlna/Profiles/Xml/Default.xml @@ -21,7 +21,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Denon AVR.xml b/Emby.Dlna/Profiles/Xml/Denon AVR.xml index c76cd9a89..571786906 100644 --- a/Emby.Dlna/Profiles/Xml/Denon AVR.xml +++ b/Emby.Dlna/Profiles/Xml/Denon AVR.xml @@ -26,7 +26,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml b/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml index f2ce68ab5..eea0febfd 100644 --- a/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml +++ b/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml @@ -27,7 +27,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>10</TimelineOffsetSeconds> <RequiresPlainVideoItems>true</RequiresPlainVideoItems> <RequiresPlainFolders>true</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Dish Hopper-Joey.xml b/Emby.Dlna/Profiles/Xml/Dish Hopper-Joey.xml index 5ff497560..5b299577e 100644 --- a/Emby.Dlna/Profiles/Xml/Dish Hopper-Joey.xml +++ b/Emby.Dlna/Profiles/Xml/Dish Hopper-Joey.xml @@ -28,7 +28,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mp2t:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mp2t:http-get:*:video/mpeg:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/LG Smart TV.xml b/Emby.Dlna/Profiles/Xml/LG Smart TV.xml index a0f0e0ee8..20f5ba79b 100644 --- a/Emby.Dlna/Profiles/Xml/LG Smart TV.xml +++ b/Emby.Dlna/Profiles/Xml/LG Smart TV.xml @@ -27,7 +27,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>10</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml b/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml index 55910c77f..d01e3a145 100644 --- a/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml +++ b/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml @@ -25,7 +25,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Marantz.xml b/Emby.Dlna/Profiles/Xml/Marantz.xml index a6345ab3f..0cc9c86e8 100644 --- a/Emby.Dlna/Profiles/Xml/Marantz.xml +++ b/Emby.Dlna/Profiles/Xml/Marantz.xml @@ -27,7 +27,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/MediaMonkey.xml b/Emby.Dlna/Profiles/Xml/MediaMonkey.xml index 2c2c3a1de..9d5ddc3d1 100644 --- a/Emby.Dlna/Profiles/Xml/MediaMonkey.xml +++ b/Emby.Dlna/Profiles/Xml/MediaMonkey.xml @@ -27,7 +27,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml b/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml index 934f0550d..8f766853b 100644 --- a/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml +++ b/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml @@ -28,7 +28,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>10</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml b/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml index eab220fae..aa881d014 100644 --- a/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml +++ b/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml @@ -21,7 +21,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml b/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml index 3e6f56e5b..7160a9c2e 100644 --- a/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml +++ b/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml @@ -27,7 +27,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml b/Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml index 74240b843..c9b907e58 100644 --- a/Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml +++ b/Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml @@ -27,7 +27,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>true</RequiresPlainVideoItems> <RequiresPlainFolders>true</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml index f20e9fcb6..1461db311 100644 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml @@ -3,10 +3,10 @@ xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Name>Sony Bravia (2010)</Name> <Identification> - <FriendlyName>KDL-\d{2}[EHLNPB]X\d[01]\d.*</FriendlyName> + <FriendlyName>KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*</FriendlyName> <Manufacturer>Sony</Manufacturer> <Headers> - <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[EHLNPB]X\d[01]\d.*" match="Regex" /> + <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*" match="Regex" /> </Headers> </Identification> <Manufacturer>Microsoft Corporation</Manufacturer> diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml index 49819ccfd..7c5f2b181 100644 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml @@ -3,10 +3,10 @@ xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Name>Sony Bravia (2011)</Name> <Identification> - <FriendlyName>KDL-\d{2}([A-Z]X\d2\d|CX400).*</FriendlyName> + <FriendlyName>KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*</FriendlyName> <Manufacturer>Sony</Manufacturer> <Headers> - <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}([A-Z]X\d2\d|CX400).*" match="Regex" /> + <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*" match="Regex" /> </Headers> </Identification> <Manufacturer>Microsoft Corporation</Manufacturer> @@ -29,7 +29,7 @@ <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml index aaad7b342..842a8fba3 100644 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml @@ -3,10 +3,10 @@ xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Name>Sony Bravia (2012)</Name> <Identification> - <FriendlyName>KDL-\d{2}[A-Z]X\d5(\d|G).*</FriendlyName> + <FriendlyName>KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*</FriendlyName> <Manufacturer>Sony</Manufacturer> <Headers> - <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[A-Z]X\d5(\d|G).*" match="Regex" /> + <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*" match="Regex" /> </Headers> </Identification> <Manufacturer>Microsoft Corporation</Manufacturer> @@ -29,7 +29,7 @@ <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml index 8e2e8dbaa..f1135c3fe 100644 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml @@ -3,10 +3,10 @@ xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Name>Sony Bravia (2013)</Name> <Identification> - <FriendlyName>KDL-\d{2}[WR][5689]\d{2}A.*</FriendlyName> + <FriendlyName>KDL-[0-9]{2}[WR][5689][0-9]{2}A.*</FriendlyName> <Manufacturer>Sony</Manufacturer> <Headers> - <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[WR][5689]\d{2}A.*" match="Regex" /> + <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*" match="Regex" /> </Headers> </Identification> <Manufacturer>Microsoft Corporation</Manufacturer> @@ -29,7 +29,7 @@ <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml index 17a6135e1..85c7868c6 100644 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml @@ -3,10 +3,10 @@ xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Name>Sony Bravia (2014)</Name> <Identification> - <FriendlyName>(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*</FriendlyName> + <FriendlyName>(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*</FriendlyName> <Manufacturer>Sony</Manufacturer> <Headers> - <HttpHeaderInfo name="X-AV-Client-Info" value=".*(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*" match="Regex" /> + <HttpHeaderInfo name="X-AV-Client-Info" value=".*(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*" match="Regex" /> </Headers> </Identification> <Manufacturer>Microsoft Corporation</Manufacturer> @@ -29,7 +29,7 @@ <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml index df385135c..129b188e2 100644 --- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml +++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml @@ -29,7 +29,7 @@ <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> @@ -38,7 +38,7 @@ <XmlRootAttributes /> <DirectPlayProfiles> <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" /> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> + <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" /> <DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" /> <DirectPlayProfile container="aac,mp3,wav" type="Audio" /> @@ -46,7 +46,7 @@ </DirectPlayProfiles> <TranscodingProfiles> <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml index 20f50f6b6..592119305 100644 --- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml +++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml @@ -29,7 +29,7 @@ <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> @@ -38,7 +38,7 @@ <XmlRootAttributes /> <DirectPlayProfiles> <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" /> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> + <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" /> <DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" /> <DirectPlayProfile container="aac,mp3,wav" type="Audio" /> @@ -46,7 +46,7 @@ </DirectPlayProfiles> <TranscodingProfiles> <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> diff --git a/Emby.Dlna/Profiles/Xml/WDTV Live.xml b/Emby.Dlna/Profiles/Xml/WDTV Live.xml index 05380e33a..ccb74ee64 100644 --- a/Emby.Dlna/Profiles/Xml/WDTV Live.xml +++ b/Emby.Dlna/Profiles/Xml/WDTV Live.xml @@ -28,7 +28,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>5</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Xbox One.xml b/Emby.Dlna/Profiles/Xml/Xbox One.xml index 5f5cf1af3..26a65bbd4 100644 --- a/Emby.Dlna/Profiles/Xml/Xbox One.xml +++ b/Emby.Dlna/Profiles/Xml/Xbox One.xml @@ -28,7 +28,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>40</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/foobar2000.xml b/Emby.Dlna/Profiles/Xml/foobar2000.xml index f3eedb35c..5ce75ace5 100644 --- a/Emby.Dlna/Profiles/Xml/foobar2000.xml +++ b/Emby.Dlna/Profiles/Xml/foobar2000.xml @@ -27,7 +27,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Properties/AssemblyInfo.cs b/Emby.Dlna/Properties/AssemblyInfo.cs index 9d3a22c97..606ffcf4f 100644 --- a/Emby.Dlna/Properties/AssemblyInfo.cs +++ b/Emby.Dlna/Properties/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Resources; +using System.Runtime.CompilerServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information @@ -8,11 +9,12 @@ using System.Resources; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyProduct("Jellyfin Server")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] +[assembly: InternalsVisibleTo("Jellyfin.Dlna.Tests")] // Version information for an assembly consists of the following four values: // diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index 03d8f80ab..80a45f2b2 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -1,11 +1,13 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Security; using System.Text; using Emby.Dlna.Common; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Extensions; namespace Emby.Dlna.Server { @@ -13,7 +15,6 @@ namespace Emby.Dlna.Server { private readonly DeviceProfile _profile; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly string _serverUdn; private readonly string _serverAddress; private readonly string _serverName; @@ -38,8 +39,6 @@ namespace Emby.Dlna.Server _serverId = serverId; } - private static bool EnableAbsoluteUrls => false; - public string GetXml() { var builder = new StringBuilder(); @@ -63,21 +62,16 @@ namespace Emby.Dlna.Server foreach (var att in attributes) { - builder.AppendFormat(" {0}=\"{1}\"", att.Name, att.Value); + builder.AppendFormat(CultureInfo.InvariantCulture, " {0}=\"{1}\"", att.Name, att.Value); } - builder.Append(">"); + builder.Append('>'); builder.Append("<specVersion>"); builder.Append("<major>1</major>"); builder.Append("<minor>0</minor>"); builder.Append("</specVersion>"); - if (!EnableAbsoluteUrls) - { - builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>"); - } - AppendDeviceInfo(builder); builder.Append("</root>"); @@ -92,86 +86,14 @@ namespace Emby.Dlna.Server AppendIconList(builder); - builder.Append("<presentationURL>" + Escape(_serverAddress) + "/web/index.html</presentationURL>"); + builder.Append("<presentationURL>") + .Append(SecurityElement.Escape(_serverAddress)) + .Append("/web/index.html</presentationURL>"); AppendServiceList(builder); builder.Append("</device>"); } - private static readonly char[] s_escapeChars = new char[] - { - '<', - '>', - '"', - '\'', - '&' - }; - - private static readonly string[] s_escapeStringPairs = new[] - { - "<", - "<", - ">", - ">", - "\"", - """, - "'", - "'", - "&", - "&" - }; - - private static string GetEscapeSequence(char c) - { - int num = s_escapeStringPairs.Length; - for (int i = 0; i < num; i += 2) - { - string text = s_escapeStringPairs[i]; - string result = s_escapeStringPairs[i + 1]; - if (text[0] == c) - { - return result; - } - } - return c.ToString(CultureInfo.InvariantCulture); - } - - /// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary> - /// <returns>The input string with invalid characters replaced.</returns> - /// <param name="str">The string within which to escape invalid characters. </param> - public static string Escape(string str) - { - if (str == null) - { - return null; - } - - StringBuilder stringBuilder = null; - int length = str.Length; - int num = 0; - while (true) - { - int num2 = str.IndexOfAny(s_escapeChars, num); - if (num2 == -1) - { - break; - } - if (stringBuilder == null) - { - stringBuilder = new StringBuilder(); - } - stringBuilder.Append(str, num, num2 - num); - stringBuilder.Append(GetEscapeSequence(str[num2])); - num = num2 + 1; - } - if (stringBuilder == null) - { - return str; - } - stringBuilder.Append(str, num, length - num); - return stringBuilder.ToString(); - } - private void AppendDeviceProperties(StringBuilder builder) { builder.Append("<dlna:X_DLNACAP/>"); @@ -181,32 +103,54 @@ namespace Emby.Dlna.Server builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>"); - builder.Append("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>"); - builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>"); - builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>"); - - builder.Append("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>"); - builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>"); - - builder.Append("<modelNumber>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>"); - builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>"); + builder.Append("<friendlyName>") + .Append(SecurityElement.Escape(GetFriendlyName())) + .Append("</friendlyName>"); + builder.Append("<manufacturer>") + .Append(SecurityElement.Escape(_profile.Manufacturer ?? string.Empty)) + .Append("</manufacturer>"); + builder.Append("<manufacturerURL>") + .Append(SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty)) + .Append("</manufacturerURL>"); + + builder.Append("<modelDescription>") + .Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty)) + .Append("</modelDescription>"); + builder.Append("<modelName>") + .Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty)) + .Append("</modelName>"); + + builder.Append("<modelNumber>") + .Append(SecurityElement.Escape(_profile.ModelNumber ?? string.Empty)) + .Append("</modelNumber>"); + builder.Append("<modelURL>") + .Append(SecurityElement.Escape(_profile.ModelUrl ?? string.Empty)) + .Append("</modelURL>"); if (string.IsNullOrEmpty(_profile.SerialNumber)) { - builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>"); + builder.Append("<serialNumber>") + .Append(SecurityElement.Escape(_serverId)) + .Append("</serialNumber>"); } else { - builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>"); + builder.Append("<serialNumber>") + .Append(SecurityElement.Escape(_profile.SerialNumber)) + .Append("</serialNumber>"); } builder.Append("<UPC/>"); - builder.Append("<UDN>uuid:" + Escape(_serverUdn) + "</UDN>"); + builder.Append("<UDN>uuid:") + .Append(SecurityElement.Escape(_serverUdn)) + .Append("</UDN>"); if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags)) { - builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">" + Escape(_profile.SonyAggregationFlags) + "</av:aggregationFlags>"); + builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">") + .Append(SecurityElement.Escape(_profile.SonyAggregationFlags)) + .Append("</av:aggregationFlags>"); } } @@ -244,11 +188,21 @@ namespace Emby.Dlna.Server { builder.Append("<icon>"); - builder.Append("<mimetype>" + Escape(icon.MimeType ?? string.Empty) + "</mimetype>"); - builder.Append("<width>" + Escape(icon.Width.ToString(_usCulture)) + "</width>"); - builder.Append("<height>" + Escape(icon.Height.ToString(_usCulture)) + "</height>"); - builder.Append("<depth>" + Escape(icon.Depth ?? string.Empty) + "</depth>"); - builder.Append("<url>" + BuildUrl(icon.Url) + "</url>"); + builder.Append("<mimetype>") + .Append(SecurityElement.Escape(icon.MimeType ?? string.Empty)) + .Append("</mimetype>"); + builder.Append("<width>") + .Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture))) + .Append("</width>"); + builder.Append("<height>") + .Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture))) + .Append("</height>"); + builder.Append("<depth>") + .Append(SecurityElement.Escape(icon.Depth ?? string.Empty)) + .Append("</depth>"); + builder.Append("<url>") + .Append(BuildUrl(icon.Url)) + .Append("</url>"); builder.Append("</icon>"); } @@ -264,11 +218,21 @@ namespace Emby.Dlna.Server { builder.Append("<service>"); - builder.Append("<serviceType>" + Escape(service.ServiceType ?? string.Empty) + "</serviceType>"); - builder.Append("<serviceId>" + Escape(service.ServiceId ?? string.Empty) + "</serviceId>"); - builder.Append("<SCPDURL>" + BuildUrl(service.ScpdUrl) + "</SCPDURL>"); - builder.Append("<controlURL>" + BuildUrl(service.ControlUrl) + "</controlURL>"); - builder.Append("<eventSubURL>" + BuildUrl(service.EventSubUrl) + "</eventSubURL>"); + builder.Append("<serviceType>") + .Append(SecurityElement.Escape(service.ServiceType ?? string.Empty)) + .Append("</serviceType>"); + builder.Append("<serviceId>") + .Append(SecurityElement.Escape(service.ServiceId ?? string.Empty)) + .Append("</serviceId>"); + builder.Append("<SCPDURL>") + .Append(BuildUrl(service.ScpdUrl)) + .Append("</SCPDURL>"); + builder.Append("<controlURL>") + .Append(BuildUrl(service.ControlUrl)) + .Append("</controlURL>"); + builder.Append("<eventSubURL>") + .Append(BuildUrl(service.EventSubUrl)) + .Append("</eventSubURL>"); builder.Append("</service>"); } @@ -283,16 +247,9 @@ namespace Emby.Dlna.Server return string.Empty; } - url = url.TrimStart('/'); - - url = "/dlna/" + _serverUdn + "/" + url; - - if (EnableAbsoluteUrls) - { - url = _serverAddress.TrimEnd('/') + url; - } + url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/'); - return Escape(url); + return SecurityElement.Escape(url); } private IEnumerable<DeviceIcon> GetIcons() diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs index 5f78674b8..581e4a286 100644 --- a/Emby.Dlna/Service/BaseControlHandler.cs +++ b/Emby.Dlna/Service/BaseControlHandler.cs @@ -1,81 +1,70 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; +using System.Threading.Tasks; using System.Xml; +using Diacritics.Extensions; using Emby.Dlna.Didl; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace Emby.Dlna.Service { public abstract class BaseControlHandler { - private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"; - - protected readonly IServerConfigurationManager Config; - protected readonly ILogger _logger; - protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory; + private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/"; - protected BaseControlHandler(IServerConfigurationManager config, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + protected BaseControlHandler(IServerConfigurationManager config, ILogger logger) { Config = config; - _logger = logger; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; + Logger = logger; } - public ControlResponse ProcessControlRequest(ControlRequest request) + protected IServerConfigurationManager Config { get; } + + protected ILogger Logger { get; } + + public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request) { try { - var enableDebugLogging = Config.GetDlnaConfiguration().EnableDebugLog; - - if (enableDebugLogging) - { - LogRequest(request); - } - - var response = ProcessControlRequestInternal(request); - - if (enableDebugLogging) - { - LogResponse(response); - } + LogRequest(request); + var response = await ProcessControlRequestInternalAsync(request).ConfigureAwait(false); + LogResponse(response); return response; } catch (Exception ex) { - _logger.LogError(ex, "Error processing control request"); + Logger.LogError(ex, "Error processing control request"); - return new ControlErrorHandler().GetResponse(ex); + return ControlErrorHandler.GetResponse(ex); } } - private ControlResponse ProcessControlRequestInternal(ControlRequest request) + private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request) { - ControlRequestInfo requestInfo = null; + ControlRequestInfo? requestInfo = null; - using (var streamReader = new StreamReader(request.InputXml)) + using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8)) { - var readerSettings = XmlReaderSettingsFactory.Create(false); - - readerSettings.CheckCharacters = false; - readerSettings.IgnoreProcessingInstructions = true; - readerSettings.IgnoreComments = true; - - using (var reader = XmlReader.Create(streamReader, readerSettings)) + var readerSettings = new XmlReaderSettings() { - requestInfo = ParseRequest(reader); - } + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + Async = true + }; + + using var reader = XmlReader.Create(streamReader, readerSettings); + requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false); } - _logger.LogDebug("Received control request {0}", requestInfo.LocalName); - - var result = GetResult(requestInfo.LocalName, requestInfo.Headers); + Logger.LogDebug("Received control request {0}", requestInfo.LocalName); var settings = new XmlWriterSettings { @@ -89,17 +78,14 @@ namespace Emby.Dlna.Service { writer.WriteStartDocument(true); - writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV); - writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/"); + writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv); + writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/"); - writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV); + writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv); writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI); - foreach (var i in result) - { - writer.WriteStartElement(i.Key); - writer.WriteString(i.Value); - writer.WriteFullEndElement(); - } + + WriteResult(requestInfo.LocalName, requestInfo.Headers, writer); + writer.WriteFullEndElement(); writer.WriteFullEndElement(); @@ -107,25 +93,19 @@ namespace Emby.Dlna.Service writer.WriteEndDocument(); } - var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u="); - - var controlResponse = new ControlResponse - { - Xml = xml, - IsSuccessful = true - }; + var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal); - //logger.LogDebug(xml); + var controlResponse = new ControlResponse(xml, true); controlResponse.Headers.Add("EXT", string.Empty); return controlResponse; } - private ControlRequestInfo ParseRequest(XmlReader reader) + private async Task<ControlRequestInfo> ParseRequestAsync(XmlReader reader) { - reader.MoveToContent(); - reader.Read(); + await reader.MoveToContentAsync().ConfigureAwait(false); + await reader.ReadAsync().ConfigureAwait(false); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) @@ -138,74 +118,78 @@ namespace Emby.Dlna.Service { if (!reader.IsEmptyElement) { - using (var subReader = reader.ReadSubtree()) - { - return ParseBodyTag(subReader); - } + using var subReader = reader.ReadSubtree(); + return await ParseBodyTagAsync(subReader).ConfigureAwait(false); } else { - reader.Read(); + await reader.ReadAsync().ConfigureAwait(false); } + break; } + default: { - reader.Skip(); + await reader.SkipAsync().ConfigureAwait(false); break; } } } else { - reader.Read(); + await reader.ReadAsync().ConfigureAwait(false); } } - return new ControlRequestInfo(); + throw new EndOfStreamException("Stream ended but no body tag found."); } - private ControlRequestInfo ParseBodyTag(XmlReader reader) + private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader) { - var result = new ControlRequestInfo(); + string? namespaceURI = null, localName = null; - reader.MoveToContent(); - reader.Read(); + await reader.MoveToContentAsync().ConfigureAwait(false); + await reader.ReadAsync().ConfigureAwait(false); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) { if (reader.NodeType == XmlNodeType.Element) { - result.LocalName = reader.LocalName; - result.NamespaceURI = reader.NamespaceURI; + localName = reader.LocalName; + namespaceURI = reader.NamespaceURI; if (!reader.IsEmptyElement) { - using (var subReader = reader.ReadSubtree()) - { - ParseFirstBodyChild(subReader, result.Headers); - return result; - } + var result = new ControlRequestInfo(localName, namespaceURI); + using var subReader = reader.ReadSubtree(); + await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false); + return result; } else { - reader.Read(); + await reader.ReadAsync().ConfigureAwait(false); } } else { - reader.Read(); + await reader.ReadAsync().ConfigureAwait(false); } } - return result; + if (localName != null && namespaceURI != null) + { + return new ControlRequestInfo(localName, namespaceURI); + } + + throw new EndOfStreamException("Stream ended but no control found."); } - private void ParseFirstBodyChild(XmlReader reader, IDictionary<string, string> headers) + private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary<string, string> headers) { - reader.MoveToContent(); - reader.Read(); + await reader.MoveToContentAsync().ConfigureAwait(false); + await reader.ReadAsync().ConfigureAwait(false); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) @@ -213,23 +197,16 @@ namespace Emby.Dlna.Service if (reader.NodeType == XmlNodeType.Element) { // TODO: Should we be doing this here, or should it be handled earlier when decoding the request? - headers[reader.LocalName.RemoveDiacritics()] = reader.ReadElementContentAsString(); + headers[reader.LocalName.RemoveDiacritics()] = await reader.ReadElementContentAsStringAsync().ConfigureAwait(false); } else { - reader.Read(); + await reader.ReadAsync().ConfigureAwait(false); } } } - private class ControlRequestInfo - { - public string LocalName; - public string NamespaceURI; - public IDictionary<string, string> Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } - - protected abstract IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams); + protected abstract void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter); private void LogRequest(ControlRequest request) { @@ -238,10 +215,7 @@ namespace Emby.Dlna.Service return; } - var originalHeaders = request.Headers; - var headers = string.Join(", ", originalHeaders.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray()); - - _logger.LogDebug("Control request. Headers: {0}", headers); + Logger.LogDebug("Control request. Headers: {@Headers}", request.Headers); } private void LogResponse(ControlResponse response) @@ -251,11 +225,23 @@ namespace Emby.Dlna.Service return; } - var originalHeaders = response.Headers; - var headers = string.Join(", ", originalHeaders.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray()); - //builder.Append(response.Xml); + Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml); + } + + private class ControlRequestInfo + { + public ControlRequestInfo(string localName, string namespaceUri) + { + LocalName = localName; + NamespaceURI = namespaceUri; + Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + + public string LocalName { get; set; } + + public string NamespaceURI { get; set; } - _logger.LogDebug("Control response. Headers: {0}", headers); + public Dictionary<string, string> Headers { get; } } } } diff --git a/Emby.Dlna/Service/BaseService.cs b/Emby.Dlna/Service/BaseService.cs index 5359e94c4..68fd98758 100644 --- a/Emby.Dlna/Service/BaseService.cs +++ b/Emby.Dlna/Service/BaseService.cs @@ -1,36 +1,36 @@ +#pragma warning disable CS1591 + +using System.Net.Http; using Emby.Dlna.Eventing; -using MediaBrowser.Common.Net; using Microsoft.Extensions.Logging; namespace Emby.Dlna.Service { - public class BaseService : IEventManager + public class BaseService : IDlnaEventManager { - protected IEventManager EventManager; - protected IHttpClient HttpClient; - protected ILogger Logger; - - protected BaseService(ILogger logger, IHttpClient httpClient) + protected BaseService(ILogger<BaseService> logger, IHttpClientFactory httpClientFactory) { Logger = logger; - HttpClient = httpClient; - - EventManager = new EventManager(Logger, HttpClient); + EventManager = new DlnaEventManager(logger, httpClientFactory); } + protected IDlnaEventManager EventManager { get; } + + protected ILogger Logger { get; } + public EventSubscriptionResponse CancelEventSubscription(string subscriptionId) { return EventManager.CancelEventSubscription(subscriptionId); } - public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string timeoutString, string callbackUrl) + public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl) { - return EventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callbackUrl); + return EventManager.RenewEventSubscription(subscriptionId, notificationType, requestedTimeoutString, callbackUrl); } - public EventSubscriptionResponse CreateEventSubscription(string notificationType, string timeoutString, string callbackUrl) + public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl) { - return EventManager.CreateEventSubscription(notificationType, timeoutString, callbackUrl); + return EventManager.CreateEventSubscription(notificationType, requestedTimeoutString, callbackUrl); } } } diff --git a/Emby.Dlna/Service/ControlErrorHandler.cs b/Emby.Dlna/Service/ControlErrorHandler.cs index d5eb4a887..3e2cd6d2e 100644 --- a/Emby.Dlna/Service/ControlErrorHandler.cs +++ b/Emby.Dlna/Service/ControlErrorHandler.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using System.Text; @@ -6,11 +8,11 @@ using Emby.Dlna.Didl; namespace Emby.Dlna.Service { - public class ControlErrorHandler + public static class ControlErrorHandler { - private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"; + private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/"; - public ControlResponse GetResponse(Exception ex) + public static ControlResponse GetResponse(Exception ex) { var settings = new XmlWriterSettings { @@ -24,11 +26,11 @@ namespace Emby.Dlna.Service { writer.WriteStartDocument(true); - writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV); - writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/"); + writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv); + writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/"); - writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV); - writer.WriteStartElement("SOAP-ENV", "Fault", NS_SOAPENV); + writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv); + writer.WriteStartElement("SOAP-ENV", "Fault", NsSoapEnv); writer.WriteElementString("faultcode", "500"); writer.WriteElementString("faultstring", ex.Message); @@ -44,11 +46,7 @@ namespace Emby.Dlna.Service writer.WriteEndDocument(); } - return new ControlResponse - { - Xml = builder.ToString(), - IsSuccessful = false - }; + return new ControlResponse(builder.ToString(), false); } } } diff --git a/Emby.Dlna/Service/ServiceXmlBuilder.cs b/Emby.Dlna/Service/ServiceXmlBuilder.cs index bd1f0bf05..1e56d09b2 100644 --- a/Emby.Dlna/Service/ServiceXmlBuilder.cs +++ b/Emby.Dlna/Service/ServiceXmlBuilder.cs @@ -1,7 +1,9 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; +using System.Security; using System.Text; using Emby.Dlna.Common; -using Emby.Dlna.Server; namespace Emby.Dlna.Service { @@ -35,7 +37,9 @@ namespace Emby.Dlna.Service { builder.Append("<action>"); - builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>"); + builder.Append("<name>") + .Append(SecurityElement.Escape(item.Name ?? string.Empty)) + .Append("</name>"); builder.Append("<argumentList>"); @@ -43,9 +47,15 @@ namespace Emby.Dlna.Service { builder.Append("<argument>"); - builder.Append("<name>" + DescriptionXmlBuilder.Escape(argument.Name ?? string.Empty) + "</name>"); - builder.Append("<direction>" + DescriptionXmlBuilder.Escape(argument.Direction ?? string.Empty) + "</direction>"); - builder.Append("<relatedStateVariable>" + DescriptionXmlBuilder.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>"); + builder.Append("<name>") + .Append(SecurityElement.Escape(argument.Name ?? string.Empty)) + .Append("</name>"); + builder.Append("<direction>") + .Append(SecurityElement.Escape(argument.Direction ?? string.Empty)) + .Append("</direction>"); + builder.Append("<relatedStateVariable>") + .Append(SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty)) + .Append("</relatedStateVariable>"); builder.Append("</argument>"); } @@ -66,18 +76,27 @@ namespace Emby.Dlna.Service { var sendEvents = item.SendsEvents ? "yes" : "no"; - builder.Append("<stateVariable sendEvents=\"" + sendEvents + "\">"); + builder.Append("<stateVariable sendEvents=\"") + .Append(sendEvents) + .Append("\">"); - builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>"); - builder.Append("<dataType>" + DescriptionXmlBuilder.Escape(item.DataType ?? string.Empty) + "</dataType>"); + builder.Append("<name>") + .Append(SecurityElement.Escape(item.Name ?? string.Empty)) + .Append("</name>"); + builder.Append("<dataType>") + .Append(SecurityElement.Escape(item.DataType ?? string.Empty)) + .Append("</dataType>"); - if (item.AllowedValues.Length > 0) + if (item.AllowedValues.Count > 0) { builder.Append("<allowedValueList>"); foreach (var allowedValue in item.AllowedValues) { - builder.Append("<allowedValue>" + DescriptionXmlBuilder.Escape(allowedValue) + "</allowedValue>"); + builder.Append("<allowedValue>") + .Append(SecurityElement.Escape(allowedValue)) + .Append("</allowedValue>"); } + builder.Append("</allowedValueList>"); } diff --git a/Emby.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs index 298f68a28..391dda147 100644 --- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs +++ b/Emby.Dlna/Ssdp/DeviceDiscovery.cs @@ -1,27 +1,38 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Net; -using Microsoft.Extensions.Logging; using Rssdp; using Rssdp.Infrastructure; namespace Emby.Dlna.Ssdp { - public class DeviceDiscovery : IDeviceDiscovery + public sealed class DeviceDiscovery : IDeviceDiscovery, IDisposable { - private bool _disposed; + private readonly object _syncLock = new object(); - private readonly ILogger _logger; private readonly IServerConfigurationManager _config; - private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal; + private SsdpDeviceLocator _deviceLocator; + private ISsdpCommunicationsServer _commsServer; private int _listenerCount; - private object _syncLock = new object(); + private bool _disposed; + + public DeviceDiscovery(IServerConfigurationManager config) + { + _config = config; + } + + private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal; + + /// <inheritdoc /> public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered { add @@ -31,8 +42,10 @@ namespace Emby.Dlna.Ssdp _listenerCount++; DeviceDiscoveredInternal += value; } + StartInternal(); } + remove { lock (_syncLock) @@ -43,23 +56,9 @@ namespace Emby.Dlna.Ssdp } } + /// <inheritdoc /> public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft; - private SsdpDeviceLocator _deviceLocator; - - private readonly ISocketFactory _socketFactory; - private ISsdpCommunicationsServer _commsServer; - - public DeviceDiscovery( - ILoggerFactory loggerFactory, - IServerConfigurationManager config, - ISocketFactory socketFactory) - { - _logger = loggerFactory.CreateLogger(nameof(DeviceDiscovery)); - _config = config; - _socketFactory = socketFactory; - } - // Call this method from somewhere in your code to start the search. public void Start(ISsdpCommunicationsServer communicationsServer) { @@ -72,18 +71,18 @@ namespace Emby.Dlna.Ssdp { lock (_syncLock) { - if (_listenerCount > 0 && _deviceLocator == null) + if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null) { _deviceLocator = new SsdpDeviceLocator(_commsServer); // (Optional) Set the filter so we only see notifications for devices we care about // (can be any search target value i.e device type, uuid value etc - any value that appears in the // DiscoverdSsdpDevice.NotificationType property or that is used with the searchTarget parameter of the Search method). - //_DeviceLocator.NotificationFilter = "upnp:rootdevice"; + // _DeviceLocator.NotificationFilter = "upnp:rootdevice"; // Connect our event handler so we process devices as they are found - _deviceLocator.DeviceAvailable += deviceLocator_DeviceAvailable; - _deviceLocator.DeviceUnavailable += _DeviceLocator_DeviceUnavailable; + _deviceLocator.DeviceAvailable += OnDeviceLocatorDeviceAvailable; + _deviceLocator.DeviceUnavailable += OnDeviceLocatorDeviceUnavailable; var dueTime = TimeSpan.FromSeconds(5); var interval = TimeSpan.FromSeconds(_config.GetDlnaConfiguration().ClientDiscoveryIntervalSeconds); @@ -94,7 +93,7 @@ namespace Emby.Dlna.Ssdp } // Process each found device in the event handler - void deviceLocator_DeviceAvailable(object sender, DeviceAvailableEventArgs e) + private void OnDeviceLocatorDeviceAvailable(object sender, DeviceAvailableEventArgs e) { var originalHeaders = e.DiscoveredDevice.ResponseHeaders; @@ -102,20 +101,18 @@ namespace Emby.Dlna.Ssdp var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); - var args = new GenericEventArgs<UpnpDeviceInfo> - { - Argument = new UpnpDeviceInfo + var args = new GenericEventArgs<UpnpDeviceInfo>( + new UpnpDeviceInfo { Location = e.DiscoveredDevice.DescriptionLocation, Headers = headers, - LocalIpAddress = e.LocalIpAddress - } - }; + RemoteIpAddress = e.RemoteIpAddress + }); DeviceDiscoveredInternal?.Invoke(this, args); } - private void _DeviceLocator_DeviceUnavailable(object sender, DeviceUnavailableEventArgs e) + private void OnDeviceLocatorDeviceUnavailable(object sender, DeviceUnavailableEventArgs e) { var originalHeaders = e.DiscoveredDevice.ResponseHeaders; @@ -123,18 +120,17 @@ namespace Emby.Dlna.Ssdp var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); - var args = new GenericEventArgs<UpnpDeviceInfo> - { - Argument = new UpnpDeviceInfo + var args = new GenericEventArgs<UpnpDeviceInfo>( + new UpnpDeviceInfo { Location = e.DiscoveredDevice.DescriptionLocation, Headers = headers - } - }; + }); DeviceLeft?.Invoke(this, args); } + /// <inheritdoc /> public void Dispose() { if (!_disposed) diff --git a/Emby.Dlna/Ssdp/Extensions.cs b/Emby.Dlna/Ssdp/Extensions.cs deleted file mode 100644 index c680c123e..000000000 --- a/Emby.Dlna/Ssdp/Extensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Xml.Linq; - -namespace Emby.Dlna.Ssdp -{ - public static class Extensions - { - public static string GetValue(this XElement container, XName name) - { - var node = container.Element(name); - - return node == null ? null : node.Value; - } - - public static string GetAttributeValue(this XElement container, XName name) - { - var node = container.Attribute(name); - - return node == null ? null : node.Value; - } - - public static string GetDescendantValue(this XElement container, XName name) - { - foreach (var node in container.Descendants(name)) - { - return node.Value; - } - - return null; - } - } -} diff --git a/Emby.Dlna/Ssdp/SsdpExtensions.cs b/Emby.Dlna/Ssdp/SsdpExtensions.cs new file mode 100644 index 000000000..d00eb02b4 --- /dev/null +++ b/Emby.Dlna/Ssdp/SsdpExtensions.cs @@ -0,0 +1,27 @@ +#pragma warning disable CS1591 + +using System.Linq; +using System.Xml.Linq; + +namespace Emby.Dlna.Ssdp +{ + public static class SsdpExtensions + { + public static string? GetValue(this XElement container, XName name) + { + var node = container.Element(name); + + return node?.Value; + } + + public static string? GetAttributeValue(this XElement container, XName name) + { + var node = container.Attribute(name); + + return node?.Value; + } + + public static string? GetDescendantValue(this XElement container, XName name) + => container.Descendants(name).FirstOrDefault()?.Value; + } +} |
