From 247400717e0768238436a9fdeae268de7c26d8cc Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 20 Apr 2014 01:21:08 -0400 Subject: [PATCH] dlna server fixes --- MediaBrowser.Api/Dlna/DlnaServerService.cs | 96 +++++- MediaBrowser.Dlna/DlnaManager.cs | 48 ++- MediaBrowser.Dlna/Server/ControlHandler.cs | 326 +++++++++++++----- .../Server/DescriptionXmlBuilder.cs | 7 +- MediaBrowser.Dlna/Server/Headers.cs | 12 + MediaBrowser.Dlna/Server/SsdpHandler.cs | 59 ++-- MediaBrowser.Dlna/Server/UpnpDevice.cs | 4 +- .../Encoder/EncodingUtils.cs | 2 +- .../MediaBrowser.Model.Portable.csproj | 9 + .../MediaBrowser.Model.net35.csproj | 9 + .../Configuration/DlnaOptions.cs | 5 + MediaBrowser.Model/Dlna/DeviceProfile.cs | 2 + MediaBrowser.Model/Dlna/Filter.cs | 26 ++ MediaBrowser.Model/Dlna/SearchCriteria.cs | 49 +++ MediaBrowser.Model/Dlna/SortCriteria.cs | 11 + MediaBrowser.Model/MediaBrowser.Model.csproj | 3 + .../Movies/MovieXmlProvider.cs | 11 +- .../Localization/Server/ar.json | 2 +- .../Localization/Server/ca.json | 2 +- .../Localization/Server/cs.json | 2 +- .../Localization/Server/de.json | 2 +- .../Localization/Server/el.json | 2 +- .../Localization/Server/en_GB.json | 2 +- .../Localization/Server/en_US.json | 2 +- .../Localization/Server/es.json | 2 +- .../Localization/Server/es_MX.json | 2 +- .../Localization/Server/fr.json | 2 +- .../Localization/Server/he.json | 2 +- .../Localization/Server/it.json | 2 +- .../Localization/Server/kk.json | 2 +- .../Localization/Server/ms.json | 2 +- .../Localization/Server/nb.json | 2 +- .../Localization/Server/nl.json | 2 +- .../Localization/Server/pt_BR.json | 2 +- .../Localization/Server/pt_PT.json | 2 +- .../Localization/Server/ru.json | 2 +- .../Localization/Server/server.json | 14 +- .../Localization/Server/sv.json | 2 +- .../Localization/Server/zh_TW.json | 2 +- .../ApplicationHost.cs | 2 +- .../Api/DashboardService.cs | 1 + .../MediaBrowser.WebDashboard.csproj | 6 + 42 files changed, 596 insertions(+), 148 deletions(-) create mode 100644 MediaBrowser.Model/Dlna/Filter.cs create mode 100644 MediaBrowser.Model/Dlna/SearchCriteria.cs create mode 100644 MediaBrowser.Model/Dlna/SortCriteria.cs diff --git a/MediaBrowser.Api/Dlna/DlnaServerService.cs b/MediaBrowser.Api/Dlna/DlnaServerService.cs index 0dd7ee7d15..3ae9ddc63e 100644 --- a/MediaBrowser.Api/Dlna/DlnaServerService.cs +++ b/MediaBrowser.Api/Dlna/DlnaServerService.cs @@ -1,10 +1,12 @@ -using System; -using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Dlna; using ServiceStack; using ServiceStack.Text.Controller; using ServiceStack.Web; +using System; using System.Collections.Generic; +using System.Globalization; using System.IO; +using System.Linq; using System.Threading.Tasks; namespace MediaBrowser.Api.Dlna @@ -17,21 +19,28 @@ namespace MediaBrowser.Api.Dlna public string UuId { get; set; } } - [Route("/Dlna/contentdirectory.xml", "GET", Summary = "Gets dlna content directory xml")] - [Route("/Dlna/contentdirectory", "GET", Summary = "Gets dlna content directory xml")] + [Route("/Dlna/contentdirectory/contentdirectory.xml", "GET", Summary = "Gets dlna content directory xml")] + [Route("/Dlna/contentdirectory/contentdirectory", "GET", Summary = "Gets dlna content directory xml")] public class GetContentDirectory { } - [Route("/Dlna/{UuId}/control", "POST", Summary = "Processes a control request")] + [Route("/Dlna/contentdirectory/{UuId}/control", "POST", Summary = "Processes a control request")] public class ProcessControlRequest : 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/contentdirectory/{UuId}/events", Summary = "Processes an event subscription request")] + public class ProcessEventRequest + { + [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] + public string UuId { get; set; } + } + [Route("/Dlna/icons/{Filename}", "GET", Summary = "Gets a server icon")] public class GetIcon { @@ -72,8 +81,8 @@ namespace MediaBrowser.Api.Dlna private async Task PostAsync(ProcessControlRequest request) { var pathInfo = PathInfo.Parse(Request.PathInfo); - var id = pathInfo.GetArgumentValue(1); - + var id = pathInfo.GetArgumentValue(2); + using (var reader = new StreamReader(request.RequestStream)) { return _dlnaManager.ProcessControlRequest(new ControlRequest @@ -111,5 +120,76 @@ namespace MediaBrowser.Api.Dlna } } } + + public object Any(ProcessEventRequest request) + { + var subscriptionId = GetHeader("SID"); + var notificationType = GetHeader("NT"); + var callback = GetHeader("CALLBACK"); + var timeoutString = GetHeader("TIMEOUT"); + + var timeout = ParseTimeout(timeoutString) ?? 300; + + if (string.Equals(Request.Verb, "SUBSCRIBE", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrEmpty(notificationType)) + { + RenewEvent(subscriptionId, timeout); + } + else + { + SubscribeToEvent(notificationType, timeout, callback); + } + + return GetSubscriptionResponse(request.UuId, timeout); + } + + UnsubscribeFromEvent(subscriptionId); + return ResultFactory.GetResult("", "text/plain"); + } + + private void UnsubscribeFromEvent(string subscriptionId) + { + + } + + private void SubscribeToEvent(string notificationType, int? timeout, string callback) + { + + } + + private void RenewEvent(string subscriptionId, int? timeout) + { + + } + + private object GetSubscriptionResponse(string uuid, int timeout) + { + var headers = new Dictionary(); + + headers["SID"] = "uuid:" + uuid; + headers["TIMEOUT"] = "SECOND-" + timeout.ToString(_usCulture); + + return ResultFactory.GetResult("\r\n", "text/plain", headers); + } + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private int? ParseTimeout(string header) + { + if (!string.IsNullOrEmpty(header)) + { + // Starts with SECOND- + header = header.Split('-').Last(); + + int val; + + if (int.TryParse(header, NumberStyles.Any, _usCulture, out val)) + { + return val; + } + } + + return null; + } } } diff --git a/MediaBrowser.Dlna/DlnaManager.cs b/MediaBrowser.Dlna/DlnaManager.cs index c2a7ee35f9..592d8cfa8a 100644 --- a/MediaBrowser.Dlna/DlnaManager.cs +++ b/MediaBrowser.Dlna/DlnaManager.cs @@ -1,9 +1,11 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Dlna.Profiles; using MediaBrowser.Dlna.Server; @@ -31,8 +33,9 @@ namespace MediaBrowser.Dlna private readonly IDtoService _dtoService; private readonly IImageProcessor _imageProcessor; private readonly IUserDataManager _userDataManager; + private readonly IServerConfigurationManager _config; - public DlnaManager(IXmlSerializer xmlSerializer, IFileSystem fileSystem, IApplicationPaths appPaths, ILogger logger, IJsonSerializer jsonSerializer, IUserManager userManager, ILibraryManager libraryManager, IDtoService dtoService, IImageProcessor imageProcessor, IUserDataManager userDataManager) + public DlnaManager(IXmlSerializer xmlSerializer, IFileSystem fileSystem, IApplicationPaths appPaths, ILogger logger, IJsonSerializer jsonSerializer, IUserManager userManager, ILibraryManager libraryManager, IDtoService dtoService, IImageProcessor imageProcessor, IUserDataManager userDataManager, IServerConfigurationManager config) { _xmlSerializer = xmlSerializer; _fileSystem = fileSystem; @@ -44,6 +47,7 @@ namespace MediaBrowser.Dlna _dtoService = dtoService; _imageProcessor = imageProcessor; _userDataManager = userDataManager; + _config = config; //DumpProfiles(); } @@ -451,15 +455,11 @@ namespace MediaBrowser.Dlna var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase)); - if (current.Info.Type == DeviceProfileType.System) - { - throw new ArgumentException("System profiles are readonly"); - } - var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml"; var path = Path.Combine(UserProfilesPath, newFilename); - if (!string.Equals(path, current.Path, StringComparison.Ordinal)) + if (!string.Equals(path, current.Path, StringComparison.Ordinal) && + current.Info.Type != DeviceProfileType.System) { File.Delete(current.Path); } @@ -516,15 +516,17 @@ namespace MediaBrowser.Dlna var serverAddress = device.Descriptor.ToString().Substring(0, device.Descriptor.ToString().IndexOf("/dlna", StringComparison.OrdinalIgnoreCase)); + var user = GetUser(profile); + return new ControlHandler( _logger, - _userManager, _libraryManager, profile, serverAddress, _dtoService, _imageProcessor, - _userDataManager) + _userDataManager, + user) .ProcessControlRequest(request); } @@ -540,5 +542,33 @@ namespace MediaBrowser.Dlna Stream = GetType().Assembly.GetManifestResourceStream("MediaBrowser.Dlna.Images." + filename.ToLower()) }; } + + + + private User GetUser(DeviceProfile profile) + { + if (!string.IsNullOrEmpty(profile.UserId)) + { + var user = _userManager.GetUserById(new Guid(profile.UserId)); + + if (user != null) + { + return user; + } + } + + if (!string.IsNullOrEmpty(_config.Configuration.DlnaOptions.DefaultUserId)) + { + var user = _userManager.GetUserById(new Guid(_config.Configuration.DlnaOptions.DefaultUserId)); + + if (user != null) + { + return user; + } + } + + // No configuration so it's going to be pretty arbitrary + return _userManager.Users.First(); + } } } \ No newline at end of file diff --git a/MediaBrowser.Dlna/Server/ControlHandler.cs b/MediaBrowser.Dlna/Server/ControlHandler.cs index e49bf3b401..b7670f1d95 100644 --- a/MediaBrowser.Dlna/Server/ControlHandler.cs +++ b/MediaBrowser.Dlna/Server/ControlHandler.cs @@ -25,12 +25,12 @@ namespace MediaBrowser.Dlna.Server public class ControlHandler { private readonly ILogger _logger; - private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; private readonly DeviceProfile _profile; private readonly IDtoService _dtoService; private readonly IImageProcessor _imageProcessor; private readonly IUserDataManager _userDataManager; + private readonly User _user; private readonly string _serverAddress; @@ -44,16 +44,16 @@ namespace MediaBrowser.Dlna.Server private int systemID = 0; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public ControlHandler(ILogger logger, IUserManager userManager, ILibraryManager libraryManager, DeviceProfile profile, string serverAddress, IDtoService dtoService, IImageProcessor imageProcessor, IUserDataManager userDataManager) + public ControlHandler(ILogger logger, ILibraryManager libraryManager, DeviceProfile profile, string serverAddress, IDtoService dtoService, IImageProcessor imageProcessor, IUserDataManager userDataManager, User user) { _logger = logger; - _userManager = userManager; _libraryManager = libraryManager; _profile = profile; _serverAddress = serverAddress; _dtoService = dtoService; _imageProcessor = imageProcessor; _userDataManager = userDataManager; + _user = user; } public ControlResponse ProcessControlRequest(ControlRequest request) @@ -104,31 +104,24 @@ namespace MediaBrowser.Dlna.Server _logger.Debug("Received control request {0}", method.Name); - var user = _userManager.Users.First(); + var user = _user; - switch (method.LocalName) - { - case "GetSearchCapabilities": - result = HandleGetSearchCapabilities(); - break; - case "GetSortCapabilities": - result = HandleGetSortCapabilities(); - break; - case "GetSystemUpdateID": - result = HandleGetSystemUpdateID(); - break; - case "Browse": - result = HandleBrowse(sparams, user, deviceId); - break; - case "X_GetFeatureList": - result = HandleXGetFeatureList(); - break; - case "X_SetBookmark": - result = HandleXSetBookmark(sparams, user); - break; - default: - throw new ResourceNotFoundException(); - } + if (string.Equals(method.LocalName, "GetSearchCapabilities", StringComparison.OrdinalIgnoreCase)) + result = HandleGetSearchCapabilities(); + else if (string.Equals(method.LocalName, "GetSortCapabilities", StringComparison.OrdinalIgnoreCase)) + result = HandleGetSortCapabilities(); + else if (string.Equals(method.LocalName, "GetSystemUpdateID", StringComparison.OrdinalIgnoreCase)) + result = HandleGetSystemUpdateID(); + else if (string.Equals(method.LocalName, "Browse", StringComparison.OrdinalIgnoreCase)) + result = HandleBrowse(sparams, user, deviceId); + else if (string.Equals(method.LocalName, "X_GetFeatureList", StringComparison.OrdinalIgnoreCase)) + result = HandleXGetFeatureList(); + else if (string.Equals(method.LocalName, "X_SetBookmark", StringComparison.OrdinalIgnoreCase)) + result = HandleXSetBookmark(sparams, user); + else if (string.Equals(method.LocalName, "Search", StringComparison.OrdinalIgnoreCase)) + result = HandleSearch(sparams, user, deviceId); + else + throw new ResourceNotFoundException("Unexpected control request name: " + method.LocalName); var response = env.CreateElement(String.Format("u:{0}Response", method.LocalName), method.NamespaceURI); rbody.AppendChild(response); @@ -241,10 +234,12 @@ namespace MediaBrowser.Dlna.Server { var id = sparams["ObjectID"]; var flag = sparams["BrowseFlag"]; + var filter = new Filter(sparams.GetValueOrDefault("Filter", "*")); + var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", "")); var provided = 0; - int requested = 0; - int start = 0; + var requested = 0; + var start = 0; if (sparams.ContainsKey("RequestedCount") && int.TryParse(sparams["RequestedCount"], out requested) && requested <= 0) { @@ -267,11 +262,13 @@ namespace MediaBrowser.Dlna.Server var folder = (Folder)GetItemFromObjectId(id, user); - var children = GetChildrenSorted(folder, user).ToList(); + var children = GetChildrenSorted(folder, user, sortCriteria).ToList(); + var totalCount = children.Count; + if (string.Equals(flag, "BrowseMetadata")) { - Browse_AddFolder(result, folder, children.Count); + Browse_AddFolder(result, folder, children.Count, filter); provided++; } else @@ -292,13 +289,13 @@ namespace MediaBrowser.Dlna.Server if (i.IsFolder) { var f = (Folder)i; - var childCount = GetChildrenSorted(f, user).Count(); + var childCount = GetChildrenSorted(f, user, sortCriteria).Count(); - Browse_AddFolder(result, f, childCount); + Browse_AddFolder(result, f, childCount, filter); } else { - Browse_AddItem(result, i, user, deviceId); + Browse_AddItem(result, i, user, deviceId, filter); } } } @@ -309,35 +306,175 @@ namespace MediaBrowser.Dlna.Server { new KeyValuePair("Result", resXML), new KeyValuePair("NumberReturned", provided.ToString(_usCulture)), - new KeyValuePair("TotalMatches", children.Count.ToString(_usCulture)), + new KeyValuePair("TotalMatches", totalCount.ToString(_usCulture)), new KeyValuePair("UpdateID", systemID.ToString(_usCulture)) }; } - private IEnumerable GetChildrenSorted(Folder folder, User user) + private IEnumerable> HandleSearch(Headers sparams, User user, string deviceId) { - var children = folder.GetChildren(user, true).Where(i => i.LocationType != LocationType.Virtual); + var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", "")); + var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", "")); + var filter = new Filter(sparams.GetValueOrDefault("Filter", "*")); + + // sort example: dc:title, dc:date + + var provided = 0; + var requested = 0; + var start = 0; + + if (sparams.ContainsKey("RequestedCount") && int.TryParse(sparams["RequestedCount"], out requested) && requested <= 0) + { + requested = 0; + } + if (sparams.ContainsKey("StartingIndex") && int.TryParse(sparams["StartingIndex"], out start) && start <= 0) + { + start = 0; + } + + //var root = GetItem(id) as IMediaFolder; + var result = new XmlDocument(); + + var didl = result.CreateElement(string.Empty, "DIDL-Lite", NS_DIDL); + didl.SetAttribute("xmlns:dc", NS_DC); + didl.SetAttribute("xmlns:dlna", NS_DLNA); + didl.SetAttribute("xmlns:upnp", NS_UPNP); + didl.SetAttribute("xmlns:sec", NS_SEC); + result.AppendChild(didl); + + var folder = (Folder)GetItemFromObjectId(sparams["ContainerID"], user); + + var children = GetChildrenSorted(folder, user, searchCriteria, sortCriteria).ToList(); + + var totalCount = children.Count; + + if (start > 0) + { + children = children.Skip(start).ToList(); + } + if (requested > 0) + { + children = children.Take(requested).ToList(); + } + + provided = children.Count; + + foreach (var i in children) + { + if (i.IsFolder) + { + var f = (Folder)i; + var childCount = GetChildrenSorted(f, user, searchCriteria, sortCriteria).Count(); + + Browse_AddFolder(result, f, childCount, filter); + } + else + { + Browse_AddItem(result, i, user, deviceId, filter); + } + } + + var resXML = result.OuterXml; + + return new List> + { + new KeyValuePair("Result", resXML), + new KeyValuePair("NumberReturned", provided.ToString(_usCulture)), + new KeyValuePair("TotalMatches", totalCount.ToString(_usCulture)), + new KeyValuePair("UpdateID", systemID.ToString(_usCulture)) + }; + } + + private IEnumerable GetChildrenSorted(Folder folder, User user, SearchCriteria search, SortCriteria sort) + { + if (search.SearchType == SearchType.Unknown) + { + return GetChildrenSorted(folder, user, sort); + } + + var items = folder.GetRecursiveChildren(user); + items = FilterUnsupportedContent(items); + + if (search.SearchType == SearchType.Audio) + { + items = items.OfType