|
@@ -0,0 +1,171 @@
|
|
|
+using MediaBrowser.Common.Extensions;
|
|
|
+using MediaBrowser.Common.Net;
|
|
|
+using MediaBrowser.Controller.Dlna;
|
|
|
+using MediaBrowser.Model.Dlna;
|
|
|
+using MediaBrowser.Model.Logging;
|
|
|
+using System;
|
|
|
+using System.Collections.Concurrent;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Globalization;
|
|
|
+using System.Linq;
|
|
|
+using System.Text;
|
|
|
+using System.Threading.Tasks;
|
|
|
+
|
|
|
+namespace MediaBrowser.Dlna.Eventing
|
|
|
+{
|
|
|
+ public class EventManager : IEventManager
|
|
|
+ {
|
|
|
+ private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions =
|
|
|
+ new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
|
|
|
+
|
|
|
+ private readonly ILogger _logger;
|
|
|
+ private readonly IHttpClient _httpClient;
|
|
|
+
|
|
|
+ public EventManager(ILogManager logManager, IHttpClient httpClient)
|
|
|
+ {
|
|
|
+ _httpClient = httpClient;
|
|
|
+ _logger = logManager.GetLogger("DlnaEventManager");
|
|
|
+ }
|
|
|
+
|
|
|
+ public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, int? timeoutSeconds)
|
|
|
+ {
|
|
|
+ var timeout = timeoutSeconds ?? 300;
|
|
|
+
|
|
|
+ var subscription = GetSubscription(subscriptionId, true);
|
|
|
+
|
|
|
+ _logger.Debug("Renewing event subscription for {0} with timeout of {1} to {2}",
|
|
|
+ subscription.NotificationType,
|
|
|
+ timeout,
|
|
|
+ subscription.CallbackUrl);
|
|
|
+
|
|
|
+ subscription.TimeoutSeconds = timeout;
|
|
|
+ subscription.SubscriptionTime = DateTime.UtcNow;
|
|
|
+
|
|
|
+ return GetEventSubscriptionResponse(subscriptionId, timeout);
|
|
|
+ }
|
|
|
+
|
|
|
+ public EventSubscriptionResponse CreateEventSubscription(string notificationType, int? timeoutSeconds, string callbackUrl)
|
|
|
+ {
|
|
|
+ var timeout = timeoutSeconds ?? 300;
|
|
|
+ var id = Guid.NewGuid().ToString("N");
|
|
|
+
|
|
|
+ _logger.Debug("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
|
|
|
+ });
|
|
|
+
|
|
|
+ return GetEventSubscriptionResponse(id, timeout);
|
|
|
+ }
|
|
|
+
|
|
|
+ public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)
|
|
|
+ {
|
|
|
+ _logger.Debug("Cancelling event subscription {0}", subscriptionId);
|
|
|
+
|
|
|
+ EventSubscription sub;
|
|
|
+ _subscriptions.TryRemove(subscriptionId, out sub);
|
|
|
+
|
|
|
+ return new EventSubscriptionResponse
|
|
|
+ {
|
|
|
+ Content = "\r\n",
|
|
|
+ ContentType = "text/plain"
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
|
|
+ private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, int timeoutSeconds)
|
|
|
+ {
|
|
|
+ var response = new EventSubscriptionResponse
|
|
|
+ {
|
|
|
+ Content = "\r\n",
|
|
|
+ ContentType = "text/plain"
|
|
|
+ };
|
|
|
+
|
|
|
+ response.Headers["SID"] = "uuid:" + subscriptionId;
|
|
|
+ response.Headers["TIMEOUT"] = "SECOND-" + timeoutSeconds.ToString(_usCulture);
|
|
|
+
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ public EventSubscription GetSubscription(string id)
|
|
|
+ {
|
|
|
+ return GetSubscription(id, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ private EventSubscription GetSubscription(string id, bool throwOnMissing)
|
|
|
+ {
|
|
|
+ EventSubscription e;
|
|
|
+
|
|
|
+ if (!_subscriptions.TryGetValue(id, out e) && throwOnMissing)
|
|
|
+ {
|
|
|
+ throw new ResourceNotFoundException("Event with Id " + id + " not found.");
|
|
|
+ }
|
|
|
+
|
|
|
+ return e;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Task TriggerEvent(string notificationType, IDictionary<string, string> stateVariables)
|
|
|
+ {
|
|
|
+ var subs = _subscriptions.Values
|
|
|
+ .Where(i => !i.IsExpired && string.Equals(notificationType, i.NotificationType, StringComparison.OrdinalIgnoreCase))
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ var tasks = subs.Select(i => TriggerEvent(i, stateVariables));
|
|
|
+
|
|
|
+ return Task.WhenAll(tasks);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task TriggerEvent(EventSubscription subscription, IDictionary<string, string> stateVariables)
|
|
|
+ {
|
|
|
+ var builder = new StringBuilder();
|
|
|
+
|
|
|
+ builder.Append("<?xml version=\"1.0\"?>");
|
|
|
+ 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:propertyset>");
|
|
|
+
|
|
|
+ var options = new HttpRequestOptions
|
|
|
+ {
|
|
|
+ RequestContent = builder.ToString(),
|
|
|
+ RequestContentType = "text/xml",
|
|
|
+ Url = subscription.CallbackUrl
|
|
|
+ };
|
|
|
+
|
|
|
+ options.RequestHeaders.Add("NT", subscription.NotificationType);
|
|
|
+ options.RequestHeaders.Add("NTS", "upnp:propchange");
|
|
|
+ options.RequestHeaders.Add("SID", "uuid:" + subscription.Id);
|
|
|
+ options.RequestHeaders.Add("SEQ", subscription.TriggerCount.ToString(_usCulture));
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ await _httpClient.SendAsync(options, "NOTIFY").ConfigureAwait(false);
|
|
|
+ }
|
|
|
+ catch (OperationCanceledException)
|
|
|
+ {
|
|
|
+ throw;
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ // Already logged at lower levels
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ subscription.IncrementTriggerCount();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|