commit
b9c64d7e8d
|
@ -19,6 +19,7 @@ using System.Threading.Tasks;
|
|||
using CommonIO;
|
||||
using MediaBrowser.Api.Playback.Progressive;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
|
||||
|
||||
namespace MediaBrowser.Api.LiveTv
|
||||
|
@ -390,6 +391,7 @@ namespace MediaBrowser.Api.LiveTv
|
|||
public bool? EnableUserData { get; set; }
|
||||
|
||||
public string SeriesTimerId { get; set; }
|
||||
public string LibrarySeriesId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Fields to return within the items, in addition to basic information
|
||||
|
@ -990,6 +992,17 @@ namespace MediaBrowser.Api.LiveTv
|
|||
query.SeriesTimerId = request.SeriesTimerId;
|
||||
query.Genres = (request.Genres ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.LibrarySeriesId))
|
||||
{
|
||||
query.IsSeries = true;
|
||||
|
||||
var series = _libraryManager.GetItemById(request.LibrarySeriesId) as Series;
|
||||
if (series != null)
|
||||
{
|
||||
query.Name = series.Name;
|
||||
}
|
||||
}
|
||||
|
||||
var result = await _liveTvManager.GetPrograms(query, GetDtoOptions(request), CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace MediaBrowser.Controller.Dlna
|
||||
{
|
||||
public interface ISsdpHandler
|
||||
{
|
||||
}
|
||||
}
|
|
@ -111,7 +111,6 @@
|
|||
<Compile Include="Dlna\IDlnaManager.cs" />
|
||||
<Compile Include="Dlna\IEventManager.cs" />
|
||||
<Compile Include="Dlna\IMediaReceiverRegistrar.cs" />
|
||||
<Compile Include="Dlna\ISsdpHandler.cs" />
|
||||
<Compile Include="Dlna\IUpnpService.cs" />
|
||||
<Compile Include="Dlna\SsdpMessageEventArgs.cs" />
|
||||
<Compile Include="Drawing\IImageProcessor.cs" />
|
||||
|
|
|
@ -696,16 +696,36 @@ namespace MediaBrowser.Dlna.Didl
|
|||
|
||||
private void AddPeople(BaseItem item, XmlElement element)
|
||||
{
|
||||
var types = new[] { PersonType.Director, PersonType.Writer, PersonType.Producer, PersonType.Composer, "Creator" };
|
||||
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 = 10;
|
||||
|
||||
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;
|
||||
|
||||
AddValue(element, "upnp", type.ToLower(), actor.Name, NS_UPNP);
|
||||
|
||||
index++;
|
||||
|
||||
if (index >= limit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ using System.Net;
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using Rssdp;
|
||||
using Rssdp.Infrastructure;
|
||||
|
||||
namespace MediaBrowser.Dlna.Main
|
||||
{
|
||||
|
@ -154,8 +155,14 @@ namespace MediaBrowser.Dlna.Main
|
|||
}
|
||||
}
|
||||
|
||||
private void LogMessage(string msg)
|
||||
{
|
||||
//_logger.Debug(msg);
|
||||
}
|
||||
|
||||
private void StartPublishing()
|
||||
{
|
||||
SsdpDevicePublisherBase.LogFunction = LogMessage;
|
||||
_Publisher = new SsdpDevicePublisher();
|
||||
}
|
||||
|
||||
|
@ -237,46 +244,64 @@ namespace MediaBrowser.Dlna.Main
|
|||
|
||||
var udn = (addressString).GetMD5().ToString("N");
|
||||
|
||||
var services = new List<string>
|
||||
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
|
||||
|
||||
_logger.Info("Registering publisher for {0} on {1}", fullService, addressString);
|
||||
|
||||
var descriptorUri = "/dlna/" + udn + "/description.xml";
|
||||
var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
|
||||
|
||||
var device = new SsdpRootDevice
|
||||
{
|
||||
CacheLifetime = TimeSpan.FromSeconds(cacheLength), //How long SSDP clients can cache this info.
|
||||
Location = uri, // Must point to the URL that serves your devices UPnP description document.
|
||||
FriendlyName = "Emby Server",
|
||||
Manufacturer = "Emby",
|
||||
ModelName = "Emby Server",
|
||||
Uuid = udn
|
||||
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
|
||||
};
|
||||
|
||||
SetProperies(device, fullService);
|
||||
_Publisher.AddDevice(device);
|
||||
|
||||
var embeddedDevices = new List<string>
|
||||
{
|
||||
"urn:schemas-upnp-org:device:MediaServer:1",
|
||||
"urn:schemas-upnp-org:service:ContentDirectory:1",
|
||||
"urn:schemas-upnp-org:service:ConnectionManager:1",
|
||||
"urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
|
||||
};
|
||||
|
||||
foreach (var fullService in services)
|
||||
foreach (var subDevice in embeddedDevices)
|
||||
{
|
||||
_logger.Info("Registering publisher for {0} on {1}", fullService, addressString);
|
||||
|
||||
var descriptorURI = "/dlna/" + udn + "/description.xml";
|
||||
var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorURI);
|
||||
|
||||
var service = fullService.Replace("urn:", string.Empty).Replace(":1", string.Empty);
|
||||
|
||||
var serviceParts = service.Split(':');
|
||||
|
||||
var deviceTypeNamespace = serviceParts[0].Replace('.', '-');
|
||||
|
||||
var device = new SsdpRootDevice
|
||||
var embeddedDevice = new SsdpEmbeddedDevice
|
||||
{
|
||||
CacheLifetime = TimeSpan.FromSeconds(cacheLength), //How long SSDP clients can cache this info.
|
||||
Location = uri, // Must point to the URL that serves your devices UPnP description document.
|
||||
DeviceTypeNamespace = deviceTypeNamespace,
|
||||
DeviceClass = serviceParts[1],
|
||||
DeviceType = serviceParts[2],
|
||||
FriendlyName = "Emby Server",
|
||||
Manufacturer = "Emby",
|
||||
ModelName = "Emby Server",
|
||||
FriendlyName = device.FriendlyName,
|
||||
Manufacturer = device.Manufacturer,
|
||||
ModelName = device.ModelName,
|
||||
Uuid = udn
|
||||
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
|
||||
};
|
||||
|
||||
_Publisher.AddDevice(device);
|
||||
SetProperies(embeddedDevice, subDevice);
|
||||
device.AddDevice(embeddedDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetProperies(SsdpDevice device, string fullDeviceType)
|
||||
{
|
||||
var service = fullDeviceType.Replace("urn:", string.Empty).Replace(":1", string.Empty);
|
||||
|
||||
var serviceParts = service.Split(':');
|
||||
|
||||
var deviceTypeNamespace = serviceParts[0].Replace('.', '-');
|
||||
|
||||
device.DeviceTypeNamespace = deviceTypeNamespace;
|
||||
device.DeviceClass = serviceParts[1];
|
||||
device.DeviceType = serviceParts[2];
|
||||
}
|
||||
|
||||
private readonly object _syncLock = new object();
|
||||
private void StartPlayToManager()
|
||||
{
|
||||
|
|
|
@ -108,7 +108,6 @@
|
|||
<Compile Include="Profiles\SonyBravia2014Profile.cs" />
|
||||
<Compile Include="Profiles\SonyPs4Profile.cs" />
|
||||
<Compile Include="Profiles\VlcProfile.cs" />
|
||||
<Compile Include="Ssdp\DeviceDiscoveryInfo.cs" />
|
||||
<Compile Include="Ssdp\Extensions.cs" />
|
||||
<Compile Include="PlayTo\PlaybackProgressEventArgs.cs" />
|
||||
<Compile Include="PlayTo\PlaybackStoppedEventArgs.cs" />
|
||||
|
@ -130,7 +129,6 @@
|
|||
<Compile Include="Service\BaseService.cs" />
|
||||
<Compile Include="Service\ControlErrorHandler.cs" />
|
||||
<Compile Include="Service\ServiceXmlBuilder.cs" />
|
||||
<Compile Include="Ssdp\Datagram.cs" />
|
||||
<Compile Include="Server\DescriptionXmlBuilder.cs" />
|
||||
<Compile Include="Ssdp\DeviceDiscovery.cs" />
|
||||
<Compile Include="PlayTo\SsdpHttpClient.cs" />
|
||||
|
@ -160,8 +158,6 @@
|
|||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Server\Headers.cs" />
|
||||
<Compile Include="Server\UpnpDevice.cs" />
|
||||
<Compile Include="Ssdp\SsdpMessageBuilder.cs" />
|
||||
<Compile Include="Ssdp\SsdpHandler.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace MediaBrowser.Dlna.Ssdp
|
||||
{
|
||||
public class Datagram
|
||||
{
|
||||
public EndPoint ToEndPoint { get; private set; }
|
||||
public EndPoint FromEndPoint { get; private set; }
|
||||
public string Message { get; private set; }
|
||||
public bool IsBroadcast { get; private set; }
|
||||
public bool EnableDebugLogging { get; private set; }
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public Datagram(EndPoint toEndPoint, EndPoint fromEndPoint, ILogger logger, string message, bool isBroadcast, bool enableDebugLogging)
|
||||
{
|
||||
Message = message;
|
||||
_logger = logger;
|
||||
EnableDebugLogging = enableDebugLogging;
|
||||
IsBroadcast = isBroadcast;
|
||||
FromEndPoint = fromEndPoint;
|
||||
ToEndPoint = toEndPoint;
|
||||
}
|
||||
|
||||
public void Send()
|
||||
{
|
||||
var msg = Encoding.ASCII.GetBytes(Message);
|
||||
|
||||
var socket = CreateSocket();
|
||||
|
||||
if (socket == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (FromEndPoint != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
socket.Bind(FromEndPoint);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (EnableDebugLogging)
|
||||
{
|
||||
_logger.ErrorException("Error binding datagram socket", ex);
|
||||
}
|
||||
|
||||
if (IsBroadcast)
|
||||
{
|
||||
CloseSocket(socket, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
socket.BeginSendTo(msg, 0, msg.Length, SocketFlags.None, ToEndPoint, result =>
|
||||
{
|
||||
try
|
||||
{
|
||||
socket.EndSend(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (EnableDebugLogging)
|
||||
{
|
||||
_logger.ErrorException("Error sending Datagram to {0} from {1}: " + Message, ex, ToEndPoint, FromEndPoint == null ? "" : FromEndPoint.ToString());
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseSocket(socket, true);
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error sending Datagram to {0} from {1}: " + Message, ex, ToEndPoint, FromEndPoint == null ? "" : FromEndPoint.ToString());
|
||||
CloseSocket(socket, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseSocket(Socket socket, bool logError)
|
||||
{
|
||||
try
|
||||
{
|
||||
socket.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (logError && EnableDebugLogging)
|
||||
{
|
||||
_logger.ErrorException("Error closing datagram socket", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Socket CreateSocket()
|
||||
{
|
||||
try
|
||||
{
|
||||
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4);
|
||||
|
||||
if (IsBroadcast)
|
||||
{
|
||||
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true);
|
||||
}
|
||||
|
||||
return socket;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error creating socket", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
using MediaBrowser.Dlna.PlayTo;
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace MediaBrowser.Dlna.Ssdp
|
||||
{
|
||||
public class DeviceDiscoveryInfo
|
||||
{
|
||||
public Device Device { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The server's ip address that the device responded to
|
||||
/// </summary>
|
||||
public IPAddress LocalIpAddress { get; set; }
|
||||
|
||||
public Uri Uri { get; set; }
|
||||
|
||||
public string Usn { get; set; }
|
||||
public string Nt { get; set; }
|
||||
}
|
||||
}
|
|
@ -9,30 +9,6 @@ namespace MediaBrowser.Dlna.Ssdp
|
|||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static Task<int> ReceiveAsync(this Socket socket, byte[] buffer, int offset, int size)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<int>(socket);
|
||||
var remoteip = new IPEndPoint(IPAddress.Any, 0);
|
||||
var endpoint = (EndPoint)remoteip;
|
||||
|
||||
socket.BeginReceiveFrom(buffer, offset, size, SocketFlags.None, ref endpoint, iar =>
|
||||
{
|
||||
var result = (TaskCompletionSource<int>)iar.AsyncState;
|
||||
var iarSocket = (Socket)result.Task.AsyncState;
|
||||
|
||||
try
|
||||
{
|
||||
result.TrySetResult(iarSocket.EndReceive(iar));
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
result.TrySetException(exc);
|
||||
}
|
||||
}, tcs);
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public static string GetValue(this XElement container, XName name)
|
||||
{
|
||||
var node = container.Element(name);
|
||||
|
|
|
@ -1,328 +0,0 @@
|
|||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Dlna.Server;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace MediaBrowser.Dlna.Ssdp
|
||||
{
|
||||
public class SsdpHandler : IDisposable, ISsdpHandler
|
||||
{
|
||||
private Socket _multicastSocket;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
const string SSDPAddr = "239.255.255.250";
|
||||
const int SSDPPort = 1900;
|
||||
private readonly string _serverSignature;
|
||||
|
||||
private readonly IPAddress _ssdpIp = IPAddress.Parse(SSDPAddr);
|
||||
private readonly IPEndPoint _ssdpEndp = new IPEndPoint(IPAddress.Parse(SSDPAddr), SSDPPort);
|
||||
|
||||
private Timer _notificationTimer;
|
||||
|
||||
private bool _isDisposed;
|
||||
private readonly Dictionary<string, List<UpnpDevice>> _devices = new Dictionary<string, List<UpnpDevice>>();
|
||||
|
||||
private readonly IApplicationHost _appHost;
|
||||
|
||||
private readonly int _unicastPort = 1901;
|
||||
private UdpClient _unicastClient;
|
||||
|
||||
public SsdpHandler(ILogger logger, IServerConfigurationManager config, IApplicationHost appHost)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_appHost = appHost;
|
||||
|
||||
_config.NamedConfigurationUpdated += _config_ConfigurationUpdated;
|
||||
_serverSignature = GenerateServerSignature();
|
||||
}
|
||||
|
||||
private string GenerateServerSignature()
|
||||
{
|
||||
var os = Environment.OSVersion;
|
||||
var pstring = os.Platform.ToString();
|
||||
switch (os.Platform)
|
||||
{
|
||||
case PlatformID.Win32NT:
|
||||
case PlatformID.Win32S:
|
||||
case PlatformID.Win32Windows:
|
||||
pstring = "WIN";
|
||||
break;
|
||||
}
|
||||
|
||||
return String.Format(
|
||||
"{0}{1}/{2}.{3} UPnP/1.0 DLNADOC/1.5 Emby/{4}",
|
||||
pstring,
|
||||
IntPtr.Size * 8,
|
||||
os.Version.Major,
|
||||
os.Version.Minor,
|
||||
_appHost.ApplicationVersion
|
||||
);
|
||||
}
|
||||
|
||||
void _config_ConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
|
||||
{
|
||||
if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ReloadAliveNotifier();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<UpnpDevice> RegisteredDevices
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_devices)
|
||||
{
|
||||
var devices = _devices.ToList();
|
||||
|
||||
return devices.SelectMany(i => i.Value).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
DisposeSocket();
|
||||
StopAliveNotifier();
|
||||
|
||||
RestartSocketListener();
|
||||
ReloadAliveNotifier();
|
||||
|
||||
SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged;
|
||||
SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
|
||||
}
|
||||
|
||||
void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
|
||||
{
|
||||
if (e.Mode == PowerModes.Resume)
|
||||
{
|
||||
Start();
|
||||
}
|
||||
}
|
||||
|
||||
public async void SendDatagram(string msg,
|
||||
EndPoint endpoint,
|
||||
EndPoint localAddress,
|
||||
bool isBroadcast,
|
||||
int sendCount = 3)
|
||||
{
|
||||
var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog;
|
||||
|
||||
for (var i = 0; i < sendCount; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
await Task.Delay(500).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var dgram = new Datagram(endpoint, localAddress, _logger, msg, isBroadcast, enableDebugLogging);
|
||||
dgram.Send();
|
||||
}
|
||||
}
|
||||
|
||||
private void RestartSocketListener()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_multicastSocket = CreateMulticastSocket();
|
||||
|
||||
_logger.Info("MultiCast socket created");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error creating MultiCast socket", ex);
|
||||
//StartSocketRetryTimer();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_config.NamedConfigurationUpdated -= _config_ConfigurationUpdated;
|
||||
SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged;
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
DisposeSocket();
|
||||
StopAliveNotifier();
|
||||
}
|
||||
|
||||
private void DisposeSocket()
|
||||
{
|
||||
if (_multicastSocket != null)
|
||||
{
|
||||
_multicastSocket.Close();
|
||||
_multicastSocket.Dispose();
|
||||
_multicastSocket = null;
|
||||
}
|
||||
}
|
||||
|
||||
private Socket CreateMulticastSocket()
|
||||
{
|
||||
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true);
|
||||
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4);
|
||||
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(_ssdpIp, 0));
|
||||
|
||||
socket.Bind(new IPEndPoint(IPAddress.Any, SSDPPort));
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
private void NotifyAll()
|
||||
{
|
||||
var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog;
|
||||
|
||||
if (enableDebugLogging)
|
||||
{
|
||||
_logger.Debug("Sending alive notifications");
|
||||
}
|
||||
foreach (var d in RegisteredDevices)
|
||||
{
|
||||
NotifyDevice(d, "alive", enableDebugLogging);
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyDevice(UpnpDevice dev, string type, bool logMessage)
|
||||
{
|
||||
const string header = "NOTIFY * HTTP/1.1";
|
||||
|
||||
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// If needed later for non-server devices, these headers will need to be dynamic
|
||||
values["HOST"] = "239.255.255.250:1900";
|
||||
values["CACHE-CONTROL"] = "max-age = 600";
|
||||
values["LOCATION"] = dev.Descriptor.ToString();
|
||||
values["SERVER"] = _serverSignature;
|
||||
values["NTS"] = "ssdp:" + type;
|
||||
values["NT"] = dev.Type;
|
||||
values["USN"] = dev.USN;
|
||||
|
||||
if (logMessage)
|
||||
{
|
||||
_logger.Debug("{0} said {1}", dev.USN, type);
|
||||
}
|
||||
|
||||
var msg = new SsdpMessageBuilder().BuildMessage(header, values);
|
||||
|
||||
SendDatagram(msg, _ssdpEndp, new IPEndPoint(dev.Address, 0), true, 2);
|
||||
//SendUnicastRequest(msg, 1);
|
||||
}
|
||||
|
||||
public void RegisterNotification(string uuid, Uri descriptionUri, IPAddress address, IEnumerable<string> services)
|
||||
{
|
||||
lock (_devices)
|
||||
{
|
||||
List<UpnpDevice> list;
|
||||
List<UpnpDevice> dl;
|
||||
if (_devices.TryGetValue(uuid, out dl))
|
||||
{
|
||||
list = dl;
|
||||
}
|
||||
else
|
||||
{
|
||||
list = new List<UpnpDevice>();
|
||||
_devices[uuid] = list;
|
||||
}
|
||||
|
||||
list.AddRange(services.Select(i => new UpnpDevice(uuid, i, descriptionUri, address)));
|
||||
|
||||
NotifyAll();
|
||||
_logger.Debug("Registered mount {0} at {1}", uuid, descriptionUri);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterNotification(string uuid)
|
||||
{
|
||||
lock (_devices)
|
||||
{
|
||||
List<UpnpDevice> dl;
|
||||
if (_devices.TryGetValue(uuid, out dl))
|
||||
{
|
||||
_devices.Remove(uuid);
|
||||
foreach (var d in dl.ToList())
|
||||
{
|
||||
NotifyDevice(d, "byebye", true);
|
||||
}
|
||||
|
||||
_logger.Debug("Unregistered mount {0}", uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly object _notificationTimerSyncLock = new object();
|
||||
private int _aliveNotifierIntervalMs;
|
||||
private void ReloadAliveNotifier()
|
||||
{
|
||||
var config = _config.GetDlnaConfiguration();
|
||||
|
||||
if (!config.BlastAliveMessages)
|
||||
{
|
||||
StopAliveNotifier();
|
||||
return;
|
||||
}
|
||||
|
||||
var intervalMs = config.BlastAliveMessageIntervalSeconds * 1000;
|
||||
|
||||
if (_notificationTimer == null || _aliveNotifierIntervalMs != intervalMs)
|
||||
{
|
||||
lock (_notificationTimerSyncLock)
|
||||
{
|
||||
if (_notificationTimer == null)
|
||||
{
|
||||
_logger.Debug("Starting alive notifier");
|
||||
const int initialDelayMs = 3000;
|
||||
_notificationTimer = new Timer(state => NotifyAll(), null, initialDelayMs, intervalMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Updating alive notifier");
|
||||
_notificationTimer.Change(intervalMs, intervalMs);
|
||||
}
|
||||
|
||||
_aliveNotifierIntervalMs = intervalMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StopAliveNotifier()
|
||||
{
|
||||
lock (_notificationTimerSyncLock)
|
||||
{
|
||||
if (_notificationTimer != null)
|
||||
{
|
||||
_logger.Debug("Stopping alive notifier");
|
||||
_notificationTimer.Dispose();
|
||||
_notificationTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class UdpState
|
||||
{
|
||||
public UdpClient UdpClient;
|
||||
public IPEndPoint EndPoint;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace MediaBrowser.Dlna.Ssdp
|
||||
{
|
||||
public class SsdpMessageBuilder
|
||||
{
|
||||
public string BuildMessage(string header, Dictionary<string, string> values)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
const string argFormat = "{0}: {1}\r\n";
|
||||
|
||||
builder.AppendFormat("{0}\r\n", header);
|
||||
|
||||
foreach (var pair in values)
|
||||
{
|
||||
builder.AppendFormat(argFormat, pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
builder.Append("\r\n");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ namespace MediaBrowser.Model.LiveTv
|
|||
public string RecordingEncodingFormat { get; set; }
|
||||
public bool EnableRecordingSubfolders { get; set; }
|
||||
public bool EnableOriginalAudioWithEncodedRecordings { get; set; }
|
||||
public bool EnableOriginalVideoWithEncodedRecordings { get; set; }
|
||||
|
||||
public List<TunerHostInfo> TunerHosts { get; set; }
|
||||
public List<ListingsProviderInfo> ListingProviders { get; set; }
|
||||
|
|
|
@ -42,6 +42,7 @@ namespace MediaBrowser.Model.LiveTv
|
|||
/// <value>The user identifier.</value>
|
||||
public string UserId { get; set; }
|
||||
public string SeriesTimerId { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The earliest date for which a program starts to return
|
||||
|
|
|
@ -100,6 +100,7 @@ namespace MediaBrowser.Model.Net
|
|||
.ToDictionary(x => x.Key, x => x.First().Key, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
dict["image/jpg"] = ".jpg";
|
||||
dict["image/x-png"] = ".png";
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
|
|
@ -356,6 +356,11 @@ namespace MediaBrowser.Providers.Manager
|
|||
var season = item as Season;
|
||||
var extension = MimeTypes.ToExtension(mimeType);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(extension))
|
||||
{
|
||||
throw new ArgumentException(string.Format("Unable to determine image file extension from mime type {0}", mimeType));
|
||||
}
|
||||
|
||||
if (type == ImageType.Thumb && saveLocally)
|
||||
{
|
||||
if (season != null && season.IndexNumber.HasValue)
|
||||
|
|
|
@ -2840,7 +2840,11 @@ namespace MediaBrowser.Server.Implementations.Library
|
|||
{
|
||||
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
|
||||
{
|
||||
return Directory.Exists(path);
|
||||
// We can't validate protocol-based paths, so just allow them
|
||||
if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
return Directory.Exists(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Without native support for unc, we cannot validate this when running under mono
|
||||
|
|
|
@ -360,7 +360,6 @@ namespace MediaBrowser.Server.Implementations.Library
|
|||
|
||||
public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, bool enableAutoClose, CancellationToken cancellationToken)
|
||||
{
|
||||
enableAutoClose = false;
|
||||
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
|
|
|
@ -52,7 +52,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
|||
{
|
||||
var format = _liveTvOptions.RecordingEncodingFormat;
|
||||
|
||||
if (string.Equals(format, "mkv", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(format, "mkv", StringComparison.OrdinalIgnoreCase) || _liveTvOptions.EnableOriginalVideoWithEncodedRecordings)
|
||||
{
|
||||
return "mkv";
|
||||
}
|
||||
|
@ -204,6 +204,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
|||
|
||||
private bool EncodeVideo(MediaSourceInfo mediaSource)
|
||||
{
|
||||
if (_liveTvOptions.EnableOriginalAudioWithEncodedRecordings)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>();
|
||||
return !mediaStreams.Any(i => i.Type == MediaStreamType.Video && string.Equals(i.Codec, "h264", StringComparison.OrdinalIgnoreCase) && !i.IsInterlaced);
|
||||
}
|
||||
|
|
|
@ -877,6 +877,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
SortOrder = query.SortOrder ?? SortOrder.Ascending,
|
||||
EnableTotalRecordCount = query.EnableTotalRecordCount,
|
||||
TopParentIds = new[] { topFolder.Id.ToString("N") },
|
||||
Name = query.Name,
|
||||
DtoOptions = options
|
||||
};
|
||||
|
||||
|
@ -2304,7 +2305,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
|
||||
var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false);
|
||||
|
||||
info.RecordAnyChannel = true;
|
||||
info.RecordAnyTime = true;
|
||||
info.Days = new List<DayOfWeek>
|
||||
{
|
||||
|
|
|
@ -13,6 +13,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using CommonIO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
@ -24,12 +25,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
public M3UTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
|
||||
public M3UTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost)
|
||||
: base(config, logger, jsonSerializer, mediaEncoder)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_httpClient = httpClient;
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
public override string Type
|
||||
|
@ -46,7 +49,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
|||
|
||||
protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
return await new M3uParser(Logger, _fileSystem, _httpClient).Parse(info.Url, ChannelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
|
||||
return await new M3uParser(Logger, _fileSystem, _httpClient, _appHost).Parse(info.Url, ChannelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
|
||||
|
@ -75,7 +78,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
|||
|
||||
public async Task Validate(TunerHostInfo info)
|
||||
{
|
||||
using (var stream = await new M3uParser(Logger, _fileSystem, _httpClient).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
|
||||
using (var stream = await new M3uParser(Logger, _fileSystem, _httpClient, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
|
||||
{
|
||||
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||
using CommonIO;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
||||
|
@ -18,12 +19,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
|||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
public M3uParser(ILogger logger, IFileSystem fileSystem, IHttpClient httpClient)
|
||||
public M3uParser(ILogger logger, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_httpClient = httpClient;
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
public async Task<List<M3UChannel>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken)
|
||||
|
@ -41,7 +44,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _httpClient.Get(url, cancellationToken);
|
||||
return _httpClient.Get(new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = cancellationToken,
|
||||
// Some data providers will require a user agent
|
||||
UserAgent = _appHost.FriendlyName + "/" + _appHost.ApplicationVersion
|
||||
});
|
||||
}
|
||||
return Task.FromResult(_fileSystem.OpenRead(url));
|
||||
}
|
||||
|
@ -111,15 +120,31 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
|||
channel.Number = "0";
|
||||
}
|
||||
|
||||
channel.ImageUrl = FindProperty("tvg-logo", extInf, null);
|
||||
channel.Number = FindProperty("channel-id", extInf, channel.Number);
|
||||
channel.Number = FindProperty("tvg-id", extInf, channel.Number);
|
||||
channel.Name = FindProperty("tvg-id", extInf, channel.Name);
|
||||
channel.Name = FindProperty("tvg-name", extInf, channel.Name);
|
||||
channel.ImageUrl = FindProperty("tvg-logo", extInf);
|
||||
|
||||
var name = FindProperty("tvg-name", extInf);
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
name = FindProperty("tvg-id", extInf);
|
||||
}
|
||||
|
||||
channel.Name = name;
|
||||
|
||||
var numberString = FindProperty("tvg-id", extInf);
|
||||
if (string.IsNullOrWhiteSpace(numberString))
|
||||
{
|
||||
numberString = FindProperty("channel-id", extInf);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(numberString))
|
||||
{
|
||||
channel.Number = numberString;
|
||||
}
|
||||
|
||||
return channel;
|
||||
|
||||
}
|
||||
private string FindProperty(string property, string properties, string defaultResult = "")
|
||||
private string FindProperty(string property, string properties)
|
||||
{
|
||||
var reg = new Regex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
|
||||
var matches = reg.Matches(properties);
|
||||
|
@ -130,7 +155,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
|||
return match.Groups[2].Value;
|
||||
}
|
||||
}
|
||||
return defaultResult;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ using CommonIO;
|
|||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
|
@ -25,12 +26,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
|||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
public SatIpHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
|
||||
public SatIpHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost)
|
||||
: base(config, logger, jsonSerializer, mediaEncoder)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_httpClient = httpClient;
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
private const string ChannelIdPrefix = "sat_";
|
||||
|
@ -39,7 +42,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
|||
{
|
||||
if (!string.IsNullOrWhiteSpace(tuner.M3UUrl))
|
||||
{
|
||||
return await new M3uParser(Logger, _fileSystem, _httpClient).Parse(tuner.M3UUrl, ChannelIdPrefix, tuner.Id, cancellationToken).ConfigureAwait(false);
|
||||
return await new M3uParser(Logger, _fileSystem, _httpClient, _appHost).Parse(tuner.M3UUrl, ChannelIdPrefix, tuner.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var channels = await new ChannelScan(Logger).Scan(tuner, cancellationToken).ConfigureAwait(false);
|
||||
|
|
|
@ -71,7 +71,7 @@ namespace MediaBrowser.ServerApplication
|
|||
|
||||
if (_isRunningAsService)
|
||||
{
|
||||
_canRestartService = CanRestartWindowsService();
|
||||
//_canRestartService = CanRestartWindowsService();
|
||||
}
|
||||
|
||||
var currentProcess = Process.GetCurrentProcess();
|
||||
|
|
|
@ -986,9 +986,6 @@
|
|||
<Content Include="dashboard-ui\scripts\userparentalcontrol.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="dashboard-ui\scripts\wizardservice.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="dashboard-ui\scripts\wizardsettings.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
@ -1034,9 +1031,6 @@
|
|||
<Content Include="dashboard-ui\wizardlivetvtuner.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="dashboard-ui\wizardservice.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="dashboard-ui\wizardsettings.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>MediaBrowser.Common.Internal</id>
|
||||
<version>3.0.663</version>
|
||||
<version>3.0.664</version>
|
||||
<title>MediaBrowser.Common.Internal</title>
|
||||
<authors>Luke</authors>
|
||||
<owners>ebr,Luke,scottisafool</owners>
|
||||
|
@ -12,7 +12,7 @@
|
|||
<description>Contains common components shared by Emby Theater and Emby Server. Not intended for plugin developer consumption.</description>
|
||||
<copyright>Copyright © Emby 2013</copyright>
|
||||
<dependencies>
|
||||
<dependency id="MediaBrowser.Common" version="3.0.663" />
|
||||
<dependency id="MediaBrowser.Common" version="3.0.664" />
|
||||
<dependency id="NLog" version="4.3.8" />
|
||||
<dependency id="SimpleInjector" version="3.2.2" />
|
||||
</dependencies>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>MediaBrowser.Common</id>
|
||||
<version>3.0.663</version>
|
||||
<version>3.0.664</version>
|
||||
<title>MediaBrowser.Common</title>
|
||||
<authors>Emby Team</authors>
|
||||
<owners>ebr,Luke,scottisafool</owners>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>MediaBrowser.Server.Core</id>
|
||||
<version>3.0.663</version>
|
||||
<version>3.0.664</version>
|
||||
<title>Media Browser.Server.Core</title>
|
||||
<authors>Emby Team</authors>
|
||||
<owners>ebr,Luke,scottisafool</owners>
|
||||
|
@ -12,7 +12,7 @@
|
|||
<description>Contains core components required to build plugins for Emby Server.</description>
|
||||
<copyright>Copyright © Emby 2013</copyright>
|
||||
<dependencies>
|
||||
<dependency id="MediaBrowser.Common" version="3.0.663" />
|
||||
<dependency id="MediaBrowser.Common" version="3.0.664" />
|
||||
<dependency id="Interfaces.IO" version="1.0.0.5" />
|
||||
</dependencies>
|
||||
</metadata>
|
||||
|
|
Loading…
Reference in New Issue
Block a user