mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-09-06 11:34:57 +02:00
Merge pull request #2957 from crobibero/api-dlna-server
Migrate DlnaServerController to Jellyfin.Api
This commit is contained in:
commit
3a0bccb941
6 changed files with 331 additions and 386 deletions
|
@ -1,383 +0,0 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Emby.Dlna.Main;
|
|
||||||
using MediaBrowser.Common.Extensions;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
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 const string XMLContentType = "text/xml; charset=UTF-8";
|
|
||||||
|
|
||||||
private readonly IDlnaManager _dlnaManager;
|
|
||||||
private readonly IHttpResultFactory _resultFactory;
|
|
||||||
private readonly IServerConfigurationManager _configurationManager;
|
|
||||||
|
|
||||||
public IRequest Request { get; set; }
|
|
||||||
|
|
||||||
private IContentDirectory ContentDirectory => DlnaEntryPoint.Current.ContentDirectory;
|
|
||||||
|
|
||||||
private IConnectionManager ConnectionManager => DlnaEntryPoint.Current.ConnectionManager;
|
|
||||||
|
|
||||||
private IMediaReceiverRegistrar MediaReceiverRegistrar => DlnaEntryPoint.Current.MediaReceiverRegistrar;
|
|
||||||
|
|
||||||
public DlnaServerService(
|
|
||||||
IDlnaManager dlnaManager,
|
|
||||||
IHttpResultFactory httpResultFactory,
|
|
||||||
IServerConfigurationManager configurationManager)
|
|
||||||
{
|
|
||||||
_dlnaManager = dlnaManager;
|
|
||||||
_resultFactory = httpResultFactory;
|
|
||||||
_configurationManager = configurationManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 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)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
|
||||||
public object Get(GetContentDirectory request)
|
|
||||||
{
|
|
||||||
var xml = ContentDirectory.GetServiceXml();
|
|
||||||
|
|
||||||
return _resultFactory.GetResult(Request, xml, XMLContentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
|
||||||
public object Get(GetMediaReceiverRegistrar request)
|
|
||||||
{
|
|
||||||
var xml = MediaReceiverRegistrar.GetServiceXml();
|
|
||||||
|
|
||||||
return _resultFactory.GetResult(Request, xml, XMLContentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
|
||||||
public object Get(GetConnnectionManager request)
|
|
||||||
{
|
|
||||||
var xml = ConnectionManager.GetServiceXml();
|
|
||||||
|
|
||||||
return _resultFactory.GetResult(Request, xml, XMLContentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<object> Post(ProcessMediaReceiverRegistrarControlRequest request)
|
|
||||||
{
|
|
||||||
var response = await PostAsync(request.RequestStream, MediaReceiverRegistrar).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<object> Post(ProcessContentDirectoryControlRequest request)
|
|
||||||
{
|
|
||||||
var response = await PostAsync(request.RequestStream, ContentDirectory).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<object> Post(ProcessConnectionManagerControlRequest request)
|
|
||||||
{
|
|
||||||
var response = await PostAsync(request.RequestStream, ConnectionManager).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<ControlResponse> PostAsync(Stream requestStream, IUpnpService service)
|
|
||||||
{
|
|
||||||
var id = GetPathValue(2).ToString();
|
|
||||||
|
|
||||||
return service.ProcessControlRequestAsync(new ControlRequest
|
|
||||||
{
|
|
||||||
Headers = Request.Headers,
|
|
||||||
InputXml = requestStream,
|
|
||||||
TargetServerUuId = id,
|
|
||||||
RequestedUrl = Request.AbsoluteUri
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copied from MediaBrowser.Api/BaseApiService.cs
|
|
||||||
// TODO: Remove code duplication
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the path segment at the specified index.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The index of the path segment.</param>
|
|
||||||
/// <returns>The path segment at the specified index.</returns>
|
|
||||||
/// <exception cref="IndexOutOfRangeException" >Path doesn't contain enough segments.</exception>
|
|
||||||
/// <exception cref="InvalidDataException" >Path doesn't start with the base url.</exception>
|
|
||||||
protected internal ReadOnlySpan<char> GetPathValue(int index)
|
|
||||||
{
|
|
||||||
static void ThrowIndexOutOfRangeException()
|
|
||||||
=> throw new IndexOutOfRangeException("Path doesn't contain enough segments.");
|
|
||||||
|
|
||||||
static void ThrowInvalidDataException()
|
|
||||||
=> throw new InvalidDataException("Path doesn't start with the base url.");
|
|
||||||
|
|
||||||
ReadOnlySpan<char> path = Request.PathInfo;
|
|
||||||
|
|
||||||
// Remove the protocol part from the url
|
|
||||||
int pos = path.LastIndexOf("://");
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
|
||||||
path = path.Slice(pos + 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the query string
|
|
||||||
pos = path.LastIndexOf('?');
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
|
||||||
path = path.Slice(0, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the domain
|
|
||||||
pos = path.IndexOf('/');
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
|
||||||
path = path.Slice(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove base url
|
|
||||||
string baseUrl = _configurationManager.Configuration.BaseUrl;
|
|
||||||
int baseUrlLen = baseUrl.Length;
|
|
||||||
if (baseUrlLen != 0)
|
|
||||||
{
|
|
||||||
if (path.StartsWith(baseUrl, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
path = path.Slice(baseUrlLen);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// The path doesn't start with the base url,
|
|
||||||
// how did we get here?
|
|
||||||
ThrowInvalidDataException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove leading /
|
|
||||||
path = path.Slice(1);
|
|
||||||
|
|
||||||
// Backwards compatibility
|
|
||||||
const string Emby = "emby/";
|
|
||||||
if (path.StartsWith(Emby, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
path = path.Slice(Emby.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
const string MediaBrowser = "mediabrowser/";
|
|
||||||
if (path.StartsWith(MediaBrowser, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
path = path.Slice(MediaBrowser.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip segments until we are at the right index
|
|
||||||
for (int i = 0; i < index; i++)
|
|
||||||
{
|
|
||||||
pos = path.IndexOf('/');
|
|
||||||
if (pos == -1)
|
|
||||||
{
|
|
||||||
ThrowIndexOutOfRangeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
path = path.Slice(pos + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the rest
|
|
||||||
pos = path.IndexOf('/');
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
|
||||||
path = path.Slice(0, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
|
||||||
public object Subscribe(ProcessContentDirectoryEventRequest request)
|
|
||||||
{
|
|
||||||
return ProcessEventRequest(ContentDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
|
||||||
public object Subscribe(ProcessConnectionManagerEventRequest request)
|
|
||||||
{
|
|
||||||
return ProcessEventRequest(ConnectionManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
|
||||||
public object Subscribe(ProcessMediaReceiverRegistrarEventRequest request)
|
|
||||||
{
|
|
||||||
return ProcessEventRequest(MediaReceiverRegistrar);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
|
||||||
public object Unsubscribe(ProcessContentDirectoryEventRequest request)
|
|
||||||
{
|
|
||||||
return ProcessEventRequest(ContentDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
|
||||||
public object Unsubscribe(ProcessConnectionManagerEventRequest request)
|
|
||||||
{
|
|
||||||
return ProcessEventRequest(ConnectionManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -54,11 +54,11 @@ namespace Emby.Dlna.Main
|
||||||
private SsdpDevicePublisher _publisher;
|
private SsdpDevicePublisher _publisher;
|
||||||
private ISsdpCommunicationsServer _communicationsServer;
|
private ISsdpCommunicationsServer _communicationsServer;
|
||||||
|
|
||||||
internal IContentDirectory ContentDirectory { get; private set; }
|
public IContentDirectory ContentDirectory { get; private set; }
|
||||||
|
|
||||||
internal IConnectionManager ConnectionManager { get; private set; }
|
public IConnectionManager ConnectionManager { get; private set; }
|
||||||
|
|
||||||
internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
|
public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
|
||||||
|
|
||||||
public static DlnaEntryPoint Current;
|
public static DlnaEntryPoint Current;
|
||||||
|
|
||||||
|
|
35
Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
Normal file
35
Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies an action that supports the HTTP GET method.
|
||||||
|
/// </summary>
|
||||||
|
public class HttpSubscribeAttribute : HttpMethodAttribute
|
||||||
|
{
|
||||||
|
private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public HttpSubscribeAttribute()
|
||||||
|
: base(_supportedMethods)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="template">The route template. May not be null.</param>
|
||||||
|
public HttpSubscribeAttribute(string template)
|
||||||
|
: base(_supportedMethods, template)
|
||||||
|
{
|
||||||
|
if (template == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(template));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
Normal file
35
Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies an action that supports the HTTP GET method.
|
||||||
|
/// </summary>
|
||||||
|
public class HttpUnsubscribeAttribute : HttpMethodAttribute
|
||||||
|
{
|
||||||
|
private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public HttpUnsubscribeAttribute()
|
||||||
|
: base(_supportedMethods)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="template">The route template. May not be null.</param>
|
||||||
|
public HttpUnsubscribeAttribute(string template)
|
||||||
|
: base(_supportedMethods, template)
|
||||||
|
{
|
||||||
|
if (template == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(template));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
257
Jellyfin.Api/Controllers/DlnaServerController.cs
Normal file
257
Jellyfin.Api/Controllers/DlnaServerController.cs
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Emby.Dlna;
|
||||||
|
using Emby.Dlna.Main;
|
||||||
|
using Jellyfin.Api.Attributes;
|
||||||
|
using MediaBrowser.Controller.Dlna;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dlna Server Controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("Dlna")]
|
||||||
|
public class DlnaServerController : BaseJellyfinApiController
|
||||||
|
{
|
||||||
|
private const string XMLContentType = "text/xml; charset=UTF-8";
|
||||||
|
|
||||||
|
private readonly IDlnaManager _dlnaManager;
|
||||||
|
private readonly IContentDirectory _contentDirectory;
|
||||||
|
private readonly IConnectionManager _connectionManager;
|
||||||
|
private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DlnaServerController"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||||
|
public DlnaServerController(IDlnaManager dlnaManager)
|
||||||
|
{
|
||||||
|
_dlnaManager = dlnaManager;
|
||||||
|
_contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
|
||||||
|
_connectionManager = DlnaEntryPoint.Current.ConnectionManager;
|
||||||
|
_mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get Description Xml.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <response code="200">Description xml returned.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
|
||||||
|
[HttpGet("{serverId}/description.xml")]
|
||||||
|
[HttpGet("{serverId}/description")]
|
||||||
|
[Produces(XMLContentType)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult GetDescriptionXml([FromRoute] string serverId)
|
||||||
|
{
|
||||||
|
var url = GetAbsoluteUri();
|
||||||
|
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
|
||||||
|
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
|
||||||
|
return Ok(xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets Dlna content directory xml.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <response code="200">Dlna content directory returned.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
|
||||||
|
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml")]
|
||||||
|
[HttpGet("{serverId}/ContentDirectory/ContentDirectory")]
|
||||||
|
[Produces(XMLContentType)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
|
public ActionResult GetContentDirectory([FromRoute] string serverId)
|
||||||
|
{
|
||||||
|
return Ok(_contentDirectory.GetServiceXml());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets Dlna media receiver registrar xml.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <returns>Dlna media receiver registrar xml.</returns>
|
||||||
|
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml")]
|
||||||
|
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar")]
|
||||||
|
[Produces(XMLContentType)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
|
public ActionResult GetMediaReceiverRegistrar([FromRoute] string serverId)
|
||||||
|
{
|
||||||
|
return Ok(_mediaReceiverRegistrar.GetServiceXml());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets Dlna media receiver registrar xml.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <returns>Dlna media receiver registrar xml.</returns>
|
||||||
|
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml")]
|
||||||
|
[HttpGet("{serverId}/ConnectionManager/ConnectionManager")]
|
||||||
|
[Produces(XMLContentType)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
|
public ActionResult GetConnectionManager([FromRoute] string serverId)
|
||||||
|
{
|
||||||
|
return Ok(_connectionManager.GetServiceXml());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process a content directory control request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <returns>Control response.</returns>
|
||||||
|
[HttpPost("{serverId}/ContentDirectory/Control")]
|
||||||
|
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute] string serverId)
|
||||||
|
{
|
||||||
|
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process a connection manager control request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <returns>Control response.</returns>
|
||||||
|
[HttpPost("{serverId}/ConnectionManager/Control")]
|
||||||
|
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute] string serverId)
|
||||||
|
{
|
||||||
|
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process a media receiver registrar control request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <returns>Control response.</returns>
|
||||||
|
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
|
||||||
|
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute] string serverId)
|
||||||
|
{
|
||||||
|
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes an event subscription request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <returns>Event subscription response.</returns>
|
||||||
|
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
||||||
|
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
|
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
|
||||||
|
{
|
||||||
|
return ProcessEventRequest(_mediaReceiverRegistrar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes an event subscription request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <returns>Event subscription response.</returns>
|
||||||
|
[HttpSubscribe("{serverId}/ContentDirectory/Events")]
|
||||||
|
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
|
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
|
||||||
|
{
|
||||||
|
return ProcessEventRequest(_contentDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes an event subscription request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <returns>Event subscription response.</returns>
|
||||||
|
[HttpSubscribe("{serverId}/ConnectionManager/Events")]
|
||||||
|
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
|
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
|
||||||
|
{
|
||||||
|
return ProcessEventRequest(_connectionManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a server icon.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <param name="fileName">The icon filename.</param>
|
||||||
|
/// <returns>Icon stream.</returns>
|
||||||
|
[HttpGet("{serverId}/icons/{filename}")]
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
|
public ActionResult GetIconId([FromRoute] string serverId, [FromRoute] string fileName)
|
||||||
|
{
|
||||||
|
return GetIconInternal(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a server icon.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileName">The icon filename.</param>
|
||||||
|
/// <returns>Icon stream.</returns>
|
||||||
|
[HttpGet("icons/{filename}")]
|
||||||
|
public ActionResult GetIcon([FromRoute] string fileName)
|
||||||
|
{
|
||||||
|
return GetIconInternal(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActionResult GetIconInternal(string fileName)
|
||||||
|
{
|
||||||
|
var icon = _dlnaManager.GetIcon(fileName);
|
||||||
|
if (icon == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentType = "image/" + Path.GetExtension(fileName)
|
||||||
|
.TrimStart('.')
|
||||||
|
.ToLowerInvariant();
|
||||||
|
|
||||||
|
return File(icon.Stream, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetAbsoluteUri()
|
||||||
|
{
|
||||||
|
return $"{Request.Scheme}://{Request.Host}{Request.Path}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
|
||||||
|
{
|
||||||
|
return service.ProcessControlRequestAsync(new ControlRequest
|
||||||
|
{
|
||||||
|
Headers = Request.Headers,
|
||||||
|
InputXml = requestStream,
|
||||||
|
TargetServerUuId = id,
|
||||||
|
RequestedUrl = GetAbsoluteUri()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private EventSubscriptionResponse ProcessEventRequest(IEventManager eventManager)
|
||||||
|
{
|
||||||
|
var subscriptionId = Request.Headers["SID"];
|
||||||
|
if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var notificationType = Request.Headers["NT"];
|
||||||
|
var callback = Request.Headers["CALLBACK"];
|
||||||
|
var timeoutString = Request.Headers["TIMEOUT"];
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(notificationType))
|
||||||
|
{
|
||||||
|
return eventManager.RenewEventSubscription(
|
||||||
|
subscriptionId,
|
||||||
|
notificationType,
|
||||||
|
timeoutString,
|
||||||
|
callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventManager.CreateEventSubscription(notificationType, timeoutString, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventManager.CancelEventSubscription(subscriptionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue