Browse Source

Merge branch 'master' of https://github.com/MediaBrowser/MediaBrowser

tikuf 11 years ago
parent
commit
13196544c2
100 changed files with 2022 additions and 395 deletions
  1. 89 0
      MediaBrowser.Api/Dlna/DlnaServerService.cs
  2. 1 1
      MediaBrowser.Api/Dlna/DlnaService.cs
  3. 1 1
      MediaBrowser.Api/Library/LibraryService.cs
  4. 2 1
      MediaBrowser.Api/MediaBrowser.Api.csproj
  5. 18 26
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  6. 31 20
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  7. 0 8
      MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
  8. 8 0
      MediaBrowser.Api/Playback/StreamRequest.cs
  9. 3 0
      MediaBrowser.Api/Playback/StreamState.cs
  10. 11 7
      MediaBrowser.Api/SessionsService.cs
  11. 40 1
      MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs
  12. 28 0
      MediaBrowser.Controller/Dlna/ControlRequest.cs
  13. 22 0
      MediaBrowser.Controller/Dlna/IDlnaManager.cs
  14. 1 10
      MediaBrowser.Controller/Entities/Audio/Audio.cs
  15. 1 10
      MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
  16. 20 1
      MediaBrowser.Controller/LiveTv/LiveStreamInfo.cs
  17. 1 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  18. 3 2
      MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs
  19. 16 8
      MediaBrowser.Controller/Session/ISessionController.cs
  20. 12 0
      MediaBrowser.Dlna/Common/Argument.cs
  21. 1 1
      MediaBrowser.Dlna/Common/DeviceIcon.cs
  22. 1 10
      MediaBrowser.Dlna/Common/DeviceService.cs
  23. 21 0
      MediaBrowser.Dlna/Common/ServiceAction.cs
  24. 25 0
      MediaBrowser.Dlna/Common/StateVariable.cs
  25. 24 2
      MediaBrowser.Dlna/DlnaManager.cs
  26. BIN
      MediaBrowser.Dlna/Images/logo-120.jpg
  27. BIN
      MediaBrowser.Dlna/Images/logo-120.png
  28. BIN
      MediaBrowser.Dlna/Images/logo-48.jpg
  29. BIN
      MediaBrowser.Dlna/Images/logo-48.png
  30. 16 5
      MediaBrowser.Dlna/MediaBrowser.Dlna.csproj
  31. 0 29
      MediaBrowser.Dlna/PlayTo/Argument.cs
  32. 19 8
      MediaBrowser.Dlna/PlayTo/Device.cs
  33. 2 2
      MediaBrowser.Dlna/PlayTo/DeviceInfo.cs
  34. 32 5
      MediaBrowser.Dlna/PlayTo/DlnaController.cs
  35. 2 1
      MediaBrowser.Dlna/PlayTo/PlayToManager.cs
  36. 0 34
      MediaBrowser.Dlna/PlayTo/ServiceAction.cs
  37. 1 0
      MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs
  38. 0 52
      MediaBrowser.Dlna/PlayTo/StateVariable.cs
  39. 58 3
      MediaBrowser.Dlna/PlayTo/TransportCommands.cs
  40. 235 0
      MediaBrowser.Dlna/Server/ContentDirectoryXmlBuilder.cs
  41. 190 0
      MediaBrowser.Dlna/Server/ControlHandler.cs
  42. 65 0
      MediaBrowser.Dlna/Server/Datagram.cs
  43. 190 0
      MediaBrowser.Dlna/Server/DescriptionXmlBuilder.cs
  44. 24 3
      MediaBrowser.Dlna/Server/DlnaServerEntryPoint.cs
  45. 209 0
      MediaBrowser.Dlna/Server/ServiceActionListBuilder.cs
  46. 145 32
      MediaBrowser.Dlna/Server/SsdpHandler.cs
  47. 5 1
      MediaBrowser.Dlna/Server/UpnpDevice.cs
  48. 5 1
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  49. 8 2
      MediaBrowser.Model/ApiClient/IApiClient.cs
  50. 1 0
      MediaBrowser.Model/Dlna/DeviceProfile.cs
  51. 15 0
      MediaBrowser.Model/Dto/StreamOptions.cs
  52. 3 3
      MediaBrowser.Model/Dto/UserDto.cs
  53. 60 0
      MediaBrowser.Model/Entities/BaseItemInfo.cs
  54. 3 1
      MediaBrowser.Model/Session/GeneralCommand.cs
  55. 0 4
      MediaBrowser.Model/Session/PlaystateCommand.cs
  56. 8 6
      MediaBrowser.Model/Session/SessionInfoDto.cs
  57. 18 8
      MediaBrowser.Mono.sln
  58. 1 1
      MediaBrowser.Mono.userprefs
  59. 9 1
      MediaBrowser.Providers/Manager/ImageSaver.cs
  60. 8 25
      MediaBrowser.Providers/Movies/MovieDbProvider.cs
  61. 1 4
      MediaBrowser.Providers/Music/LastfmArtistProvider.cs
  62. 1 1
      MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs
  63. 2 2
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  64. 4 2
      MediaBrowser.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  65. 14 2
      MediaBrowser.Server.Implementations/Library/SearchEngine.cs
  66. 1 0
      MediaBrowser.Server.Implementations/Localization/JavaScript/ca.json
  67. 1 1
      MediaBrowser.Server.Implementations/Localization/JavaScript/de.json
  68. 1 0
      MediaBrowser.Server.Implementations/Localization/JavaScript/ms.json
  69. 1 1
      MediaBrowser.Server.Implementations/Localization/JavaScript/pt_BR.json
  70. 0 0
      MediaBrowser.Server.Implementations/Localization/JavaScript/ru.json
  71. 13 2
      MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs
  72. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/ar.json
  73. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/ca.json
  74. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/cs.json
  75. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/de.json
  76. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/el.json
  77. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/en_GB.json
  78. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/en_US.json
  79. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/es.json
  80. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/es_MX.json
  81. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/fr.json
  82. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/he.json
  83. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/it.json
  84. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/ms.json
  85. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/nb.json
  86. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/nl.json
  87. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/pt_BR.json
  88. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/pt_PT.json
  89. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/ru.json
  90. 66 1
      MediaBrowser.Server.Implementations/Localization/Server/server.json
  91. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/sv.json
  92. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/zh_TW.json
  93. 4 0
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  94. 10 10
      MediaBrowser.Server.Implementations/Roku/RokuSessionController.cs
  95. 137 11
      MediaBrowser.Server.Implementations/Session/SessionManager.cs
  96. 24 12
      MediaBrowser.Server.Implementations/Session/WebSocketController.cs
  97. 1 2
      MediaBrowser.Server.Implementations/Udp/UdpServer.cs
  98. 5 1
      MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj
  99. 14 3
      MediaBrowser.Server.Mono/Program.cs
  100. 10 4
      MediaBrowser.WebDashboard/Api/DashboardService.cs

+ 89 - 0
MediaBrowser.Api/Dlna/DlnaServerService.cs

@@ -0,0 +1,89 @@
+using MediaBrowser.Controller.Dlna;
+using ServiceStack;
+using ServiceStack.Web;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Dlna
+{
+    [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.xml", "GET", Summary = "Gets dlna content directory xml")]
+    [Route("/Dlna/{UuId}/contentdirectory", "GET", Summary = "Gets the 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}/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; }
+    }
+
+    public class DlnaServerService : BaseApiService
+    {
+        private readonly IDlnaManager _dlnaManager;
+
+        public DlnaServerService(IDlnaManager dlnaManager)
+        {
+            _dlnaManager = dlnaManager;
+        }
+
+        public object Get(GetDescriptionXml request)
+        {
+            var xml = _dlnaManager.GetServerDescriptionXml(GetRequestHeaders(), request.UuId);
+
+            return ResultFactory.GetResult(xml, "text/xml");
+        }
+
+        public object Get(GetContentDirectory request)
+        {
+            var xml = _dlnaManager.GetContentDirectoryXml(GetRequestHeaders());
+
+            return ResultFactory.GetResult(xml, "text/xml");
+        }
+
+        public object Post(ProcessControlRequest request)
+        {
+            var response = PostAsync(request).Result;
+
+            return ResultFactory.GetResult(response.Xml, "text/xml");
+        }
+
+        private async Task<ControlResponse> PostAsync(ProcessControlRequest request)
+        {
+            using (var reader = new StreamReader(request.RequestStream))
+            {
+                return _dlnaManager.ProcessControlRequest(new ControlRequest
+                {
+                    Headers = GetRequestHeaders(),
+                    InputXml = await reader.ReadToEndAsync().ConfigureAwait(false)
+                });
+            }
+        } 
+
+        private IDictionary<string, string> GetRequestHeaders()
+        {
+            var headers = new Dictionary<string, string>();
+
+            foreach (var key in Request.Headers.AllKeys)
+            {
+                headers[key] = Request.Headers[key];
+            }
+
+            return headers;
+        }
+    }
+}

+ 1 - 1
MediaBrowser.Api/DlnaService.cs → MediaBrowser.Api/Dlna/DlnaService.cs

@@ -4,7 +4,7 @@ using ServiceStack;
 using System.Collections.Generic;
 using System.Linq;
 
-namespace MediaBrowser.Api
+namespace MediaBrowser.Api.Dlna
 {
     [Route("/Dlna/ProfileInfos", "GET", Summary = "Gets a list of profiles")]
     public class GetProfileInfos : IReturn<List<DeviceProfileInfo>>

+ 1 - 1
MediaBrowser.Api/Library/LibraryService.cs

@@ -36,7 +36,7 @@ namespace MediaBrowser.Api.Library
         public string Id { get; set; }
     }
 
-    [Route("/Videos/{Id}/Subtitle/{Index}", "GET")]
+    [Route("/Videos/{Id}/Subtitles/{Index}", "GET")]
     [Api(Description = "Gets an external subtitle file")]
     public class GetSubtitle
     {

+ 2 - 1
MediaBrowser.Api/MediaBrowser.Api.csproj

@@ -66,7 +66,8 @@
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
     <Compile Include="ChannelService.cs" />
-    <Compile Include="DlnaService.cs" />
+    <Compile Include="Dlna\DlnaServerService.cs" />
+    <Compile Include="Dlna\DlnaService.cs" />
     <Compile Include="Movies\CollectionService.cs" />
     <Compile Include="Music\AlbumsService.cs" />
     <Compile Include="AppThemeService.cs" />

+ 18 - 26
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -185,9 +185,9 @@ namespace MediaBrowser.Api.Playback
         {
             var args = string.Empty;
 
-            if (state.IsRemote || !state.HasMediaStreams)
+            if (!state.HasMediaStreams)
             {
-                return string.Empty;
+                return state.IsInputVideo ? "-sn" : string.Empty;
             }
 
             if (state.VideoStream != null)
@@ -1341,6 +1341,12 @@ namespace MediaBrowser.Api.Playback
                 RequestedUrl = url
             };
 
+            if (!string.IsNullOrWhiteSpace(request.AudioCodec))
+            {
+                state.SupportedAudioCodecs = request.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
+                state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault();
+            }
+
             var item = string.IsNullOrEmpty(request.MediaSourceId) ?
                 DtoService.GetItemByDtoId(request.Id) :
                 DtoService.GetItemByDtoId(request.MediaSourceId);
@@ -1487,33 +1493,27 @@ namespace MediaBrowser.Api.Playback
 
             if (videoRequest != null)
             {
-                if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream, state.VideoType))
+                if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream))
                 {
                     videoRequest.VideoCodec = "copy";
                 }
 
-                //if (state.AudioStream != null && CanStreamCopyAudio(request, state.AudioStream))
-                //{
-                //    request.AudioCodec = "copy";
-                //}
+                if (state.AudioStream != null && CanStreamCopyAudio(request, state.AudioStream, state.SupportedAudioCodecs))
+                {
+                    request.AudioCodec = "copy";
+                }
             }
 
             return state;
         }
 
-        private bool CanStreamCopyVideo(VideoStreamRequest request, MediaStream videoStream, VideoType videoType)
+        private bool CanStreamCopyVideo(VideoStreamRequest request, MediaStream videoStream)
         {
             if (videoStream.IsInterlaced)
             {
                 return false;
             }
 
-            // Not going to attempt this with folder rips
-            if (videoType != VideoType.VideoFile)
-            {
-                return false;
-            }
-
             // Source and target codecs must match
             if (!string.Equals(request.VideoCodec, videoStream.Codec, StringComparison.OrdinalIgnoreCase))
             {
@@ -1584,13 +1584,13 @@ namespace MediaBrowser.Api.Playback
                 }
             }
 
-            return SupportsAutomaticVideoStreamCopy;
+            return request.EnableAutoStreamCopy;
         }
 
-        private bool CanStreamCopyAudio(StreamRequest request, MediaStream audioStream)
+        private bool CanStreamCopyAudio(StreamRequest request, MediaStream audioStream, List<string> supportedAudioCodecs)
         {
             // Source and target codecs must match
-            if (string.IsNullOrEmpty(request.AudioCodec) || !string.Equals(request.AudioCodec, audioStream.Codec, StringComparison.OrdinalIgnoreCase))
+            if (string.IsNullOrEmpty(audioStream.Codec) || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase))
             {
                 return false;
             }
@@ -1623,15 +1623,7 @@ namespace MediaBrowser.Api.Playback
                 }
             }
 
-            return SupportsAutomaticVideoStreamCopy;
-        }
-
-        protected virtual bool SupportsAutomaticVideoStreamCopy
-        {
-            get
-            {
-                return false;
-            }
+            return true;
         }
 
         private void ApplyDeviceProfileSettings(StreamState state)

+ 31 - 20
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -24,7 +24,8 @@ namespace MediaBrowser.Api.Playback.Hls
     /// </summary>
     public abstract class BaseHlsService : BaseStreamingService
     {
-        protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager, IEncodingManager encodingManager, IDlnaManager dlnaManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager, encodingManager, dlnaManager)
+        protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager, IEncodingManager encodingManager, IDlnaManager dlnaManager)
+            : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager, encodingManager, dlnaManager)
         {
         }
 
@@ -77,6 +78,7 @@ namespace MediaBrowser.Api.Playback.Hls
             return ProcessRequestAsync(request).Result;
         }
 
+        private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1);
         /// <summary>
         /// Processes the request async.
         /// </summary>
@@ -103,32 +105,41 @@ namespace MediaBrowser.Api.Playback.Hls
             }
 
             var playlist = GetOutputFilePath(state);
-            var isPlaylistNewlyCreated = false;
 
-            // If the playlist doesn't already exist, startup ffmpeg
-            if (!File.Exists(playlist))
+            if (File.Exists(playlist))
             {
-                isPlaylistNewlyCreated = true;
-
+                ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls);
+            }
+            else
+            {
+                await FfmpegStartLock.WaitAsync().ConfigureAwait(false);
                 try
                 {
-                    await StartFfMpeg(state, playlist).ConfigureAwait(false);
+                    if (File.Exists(playlist))
+                    {
+                        ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls);
+                    }
+                    else
+                    {
+                        // If the playlist doesn't already exist, startup ffmpeg
+                        try
+                        {
+                            await StartFfMpeg(state, playlist).ConfigureAwait(false);
+                        }
+                        catch
+                        {
+                            state.Dispose();
+                            throw;
+                        }
+                    }
+
+                    await WaitForMinimumSegmentCount(playlist, GetSegmentWait()).ConfigureAwait(false);
                 }
-                catch
+                finally
                 {
-                    state.Dispose();
-                    throw;
+                    FfmpegStartLock.Release();
                 }
             }
-            else
-            {
-                ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls);
-            }
-
-            if (isPlaylistNewlyCreated)
-            {
-                await WaitForMinimumSegmentCount(playlist, GetSegmentWait()).ConfigureAwait(false);
-            }
 
             int audioBitrate;
             int videoBitrate;
@@ -295,7 +306,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             // If performSubtitleConversions is true we're actually starting ffmpeg
             var startNumberParam = performSubtitleConversions ? GetStartNumber(state).ToString(UsCulture) : "0";
-            
+
             var args = string.Format("{0} {1} -i {2}{3} -map_metadata -1 -threads {4} {5} {6} -sc_threshold 0 {7} -hls_time {8} -start_number {9} -hls_list_size {10} \"{11}\"",
                 itsOffset,
                 inputModifier,

+ 0 - 8
MediaBrowser.Api/Playback/Hls/VideoHlsService.cs

@@ -98,14 +98,6 @@ namespace MediaBrowser.Api.Playback.Hls
             ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls);
         }
 
-        protected override bool SupportsAutomaticVideoStreamCopy
-        {
-            get
-            {
-                return true;
-            }
-        }
-
         /// <summary>
         /// Gets the specified request.
         /// </summary>

+ 8 - 0
MediaBrowser.Api/Playback/StreamRequest.cs

@@ -171,5 +171,13 @@ namespace MediaBrowser.Api.Playback
                 return Width.HasValue || Height.HasValue;
             }
         }
+
+        [ApiMember(Name = "EnableAutoStreamCopy", Description = "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool EnableAutoStreamCopy { get; set; }
+
+        public VideoStreamRequest()
+        {
+            EnableAutoStreamCopy = true;
+        }
     }
 }

+ 3 - 0
MediaBrowser.Api/Playback/StreamState.cs

@@ -67,10 +67,13 @@ namespace MediaBrowser.Api.Playback
         public string AudioSync = "1";
         public string VideoSync = "vfr";
 
+        public List<string> SupportedAudioCodecs { get; set; }
+
         public StreamState(ILiveTvManager liveTvManager, ILogger logger)
         {
             _liveTvManager = liveTvManager;
             _logger = logger;
+            SupportedAudioCodecs = new List<string>();
         }
 
         public string InputAudioSync { get; set; }

+ 11 - 7
MediaBrowser.Api/SessionsService.cs

@@ -1,5 +1,4 @@
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Session;
 using ServiceStack;
@@ -32,10 +31,10 @@ namespace MediaBrowser.Api
     }
 
     /// <summary>
-    /// Class BrowseTo
+    /// Class DisplayContent
     /// </summary>
     [Route("/Sessions/{Id}/Viewing", "POST", Summary = "Instructs a session to browse to an item or view")]
-    public class BrowseTo : IReturnVoid
+    public class DisplayContent : IReturnVoid
     {
         /// <summary>
         /// Gets or sets the id.
@@ -218,6 +217,7 @@ namespace MediaBrowser.Api
         public Guid UserId { get; set; }
     }
 
+    [Route("/Sessions/Capabilities", "POST", Summary = "Updates capabilities for a device")]
     [Route("/Sessions/{Id}/Capabilities", "POST", Summary = "Updates capabilities for a device")]
     public class PostCapabilities : IReturnVoid
     {
@@ -226,7 +226,7 @@ namespace MediaBrowser.Api
         /// </summary>
         /// <value>The id.</value>
         [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid Id { get; set; }
+        public string Id { get; set; }
 
         [ApiMember(Name = "PlayableMediaTypes", Description = "A list of playable media types, comma delimited. Audio, Video, Book, Game, Photo.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
         public string PlayableMediaTypes { get; set; }
@@ -307,7 +307,7 @@ namespace MediaBrowser.Api
         /// Posts the specified request.
         /// </summary>
         /// <param name="request">The request.</param>
-        public void Post(BrowseTo request)
+        public void Post(DisplayContent request)
         {
             var command = new BrowseRequest
             {
@@ -421,7 +421,11 @@ namespace MediaBrowser.Api
 
         public void Post(PostCapabilities request)
         {
-            _sessionManager.ReportCapabilities(request.Id, new SessionCapabilities
+            if (string.IsNullOrWhiteSpace(request.Id))
+            {
+                request.Id = GetSession().Id.ToString("N");
+            }
+            _sessionManager.ReportCapabilities(new Guid(request.Id), new SessionCapabilities
             {
                 PlayableMediaTypes = request.PlayableMediaTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(),
 

+ 40 - 1
MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs

@@ -15,6 +15,44 @@ namespace MediaBrowser.Common.Implementations.Networking
         /// </summary>
         /// <returns>IPAddress.</returns>
         public IEnumerable<string> GetLocalIpAddresses()
+        {
+            var list = GetIPsDefault().Where(i => !IPAddress.IsLoopback(i)).Select(i => i.ToString()).ToList();
+
+            if (list.Count > 0)
+            {
+                return list;
+            }
+
+            return GetLocalIpAddressesFallback();
+        }
+
+        private IEnumerable<IPAddress> GetIPsDefault()
+        {
+            foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces())
+            {
+                var props = adapter.GetIPProperties();
+                var gateways = from ga in props.GatewayAddresses
+                               where !ga.Address.Equals(IPAddress.Any)
+                               select true;
+
+                if (!gateways.Any())
+                {
+                    continue;
+                }
+
+                foreach (var uni in props.UnicastAddresses)
+                {
+                    var address = uni.Address;
+                    if (address.AddressFamily != AddressFamily.InterNetwork)
+                    {
+                        continue;
+                    }
+                    yield return address;
+                }
+            }
+        }
+
+        private IEnumerable<string> GetLocalIpAddressesFallback()
         {
             var host = Dns.GetHostEntry(Dns.GetHostName());
 
@@ -25,7 +63,7 @@ namespace MediaBrowser.Common.Implementations.Networking
                 .Select(i => i.ToString())
                 .Reverse();
         }
-
+        
         /// <summary>
         /// Gets a random port number that is currently available
         /// </summary>
@@ -50,6 +88,7 @@ namespace MediaBrowser.Common.Implementations.Networking
                 .Select(i => BitConverter.ToString(i.GetPhysicalAddress().GetAddressBytes()))
                 .FirstOrDefault();
         }
+
         /// <summary>
         /// Parses the specified endpointstring.
         /// </summary>

+ 28 - 0
MediaBrowser.Controller/Dlna/ControlRequest.cs

@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Dlna
+{
+    public class ControlRequest
+    {
+        public IDictionary<string, string> Headers { get; set; }
+
+        public string InputXml { get; set; }
+
+        public ControlRequest()
+        {
+            Headers = new Dictionary<string, string>();
+        }
+    }
+
+    public class ControlResponse
+    {
+        public IDictionary<string, string> Headers { get; set; }
+
+        public string Xml { get; set; }
+
+        public ControlResponse()
+        {
+            Headers = new Dictionary<string, string>();
+        }
+    }
+}

+ 22 - 0
MediaBrowser.Controller/Dlna/IDlnaManager.cs

@@ -55,5 +55,27 @@ namespace MediaBrowser.Controller.Dlna
         /// <param name="deviceInfo">The device information.</param>
         /// <returns>DeviceProfile.</returns>
         DeviceProfile GetProfile(DeviceIdentification deviceInfo);
+
+        /// <summary>
+        /// Gets the server description XML.
+        /// </summary>
+        /// <param name="headers">The headers.</param>
+        /// <param name="serverUuId">The server uu identifier.</param>
+        /// <returns>System.String.</returns>
+        string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId);
+
+        /// <summary>
+        /// Gets the content directory XML.
+        /// </summary>
+        /// <param name="headers">The headers.</param>
+        /// <returns>System.String.</returns>
+        string GetContentDirectoryXml(IDictionary<string, string> headers);
+
+        /// <summary>
+        /// Processes the control request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>ControlResponse.</returns>
+        ControlResponse ProcessControlRequest(ControlRequest request);
     }
 }

+ 1 - 10
MediaBrowser.Controller/Entities/Audio/Audio.cs

@@ -10,7 +10,7 @@ namespace MediaBrowser.Controller.Entities.Audio
     /// <summary>
     /// Class Audio
     /// </summary>
-    public class Audio : BaseItem, IHasMediaStreams, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<SongInfo>, IHasSeries
+    public class Audio : BaseItem, IHasMediaStreams, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<SongInfo>
     {
         public Audio()
         {
@@ -51,15 +51,6 @@ namespace MediaBrowser.Controller.Entities.Audio
             }
         }
 
-        [IgnoreDataMember]
-        public string SeriesName
-        {
-            get
-            {
-                return Album;
-            }
-        }
-
         /// <summary>
         /// Gets or sets the artist.
         /// </summary>

+ 1 - 10
MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs

@@ -11,7 +11,7 @@ namespace MediaBrowser.Controller.Entities.Audio
     /// <summary>
     /// Class MusicAlbum
     /// </summary>
-    public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasTags, IHasLookupInfo<AlbumInfo>, IHasSeries
+    public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasTags, IHasLookupInfo<AlbumInfo>
     {
         public List<Guid> SoundtrackIds { get; set; }
 
@@ -67,15 +67,6 @@ namespace MediaBrowser.Controller.Entities.Audio
             }
         }
 
-        [IgnoreDataMember]
-        public string SeriesName
-        {
-            get
-            {
-                return AlbumArtist;
-            }
-        }
-
         /// <summary>
         /// Override this to true if class should be grouped under a container in indicies
         /// The container class should be defined via IndexContainer

+ 20 - 1
MediaBrowser.Controller/LiveTv/LiveStreamInfo.cs

@@ -1,4 +1,6 @@
-
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+
 namespace MediaBrowser.Controller.LiveTv
 {
     public class LiveStreamInfo
@@ -20,5 +22,22 @@ namespace MediaBrowser.Controller.LiveTv
         /// </summary>
         /// <value>The identifier.</value>
         public string Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the media container.
+        /// </summary>
+        /// <value>The media container.</value>
+        public string MediaContainer { get; set; }
+
+        /// <summary>
+        /// Gets or sets the media streams.
+        /// </summary>
+        /// <value>The media streams.</value>
+        public List<MediaStream> MediaStreams { get; set; }
+
+        public LiveStreamInfo()
+        {
+            MediaStreams = new List<MediaStream>();
+        }
     }
 }

+ 1 - 0
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -78,6 +78,7 @@
     <Compile Include="Channels\Channel.cs" />
     <Compile Include="Collections\CollectionCreationOptions.cs" />
     <Compile Include="Collections\ICollectionManager.cs" />
+    <Compile Include="Dlna\ControlRequest.cs" />
     <Compile Include="Dlna\IDlnaManager.cs" />
     <Compile Include="Drawing\IImageProcessor.cs" />
     <Compile Include="Drawing\ImageFormat.cs" />

+ 3 - 2
MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs

@@ -103,8 +103,9 @@ namespace MediaBrowser.Controller.Resolvers
             ".wav",
             ".ape",
             ".ogg",
-            ".oga"
-
+            ".oga",
+            ".asf",
+            ".mp4"
         };
 
         private static readonly Dictionary<string, string> AudioFileExtensionsDictionary = AudioFileExtensions.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);

+ 16 - 8
MediaBrowser.Controller/Session/ISessionController.cs

@@ -35,14 +35,6 @@ namespace MediaBrowser.Controller.Session
         /// <returns>Task.</returns>
         Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken);
 
-        /// <summary>
-        /// Sends the browse command.
-        /// </summary>
-        /// <param name="command">The command.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        Task SendBrowseCommand(BrowseRequest command, CancellationToken cancellationToken);
-
         /// <summary>
         /// Sends the playstate command.
         /// </summary>
@@ -96,6 +88,22 @@ namespace MediaBrowser.Controller.Session
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Sends the playback start notification.
+        /// </summary>
+        /// <param name="sessionInfo">The session information.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Sends the playback start notification.
+        /// </summary>
+        /// <param name="sessionInfo">The session information.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken);
         
         /// <summary>
         /// Sends the server restart notification.

+ 12 - 0
MediaBrowser.Dlna/Common/Argument.cs

@@ -0,0 +1,12 @@
+
+namespace MediaBrowser.Dlna.Common
+{  
+    public class Argument
+    {
+        public string Name { get;  set; }
+
+        public string Direction { get;  set; }
+
+        public string RelatedStateVariable { get;  set; }
+    }
+}

+ 1 - 1
MediaBrowser.Dlna/PlayTo/DeviceIcon.cs → MediaBrowser.Dlna/Common/DeviceIcon.cs

@@ -1,5 +1,5 @@
 
-namespace MediaBrowser.Dlna.PlayTo
+namespace MediaBrowser.Dlna.Common
 {
     public class DeviceIcon
     {

+ 1 - 10
MediaBrowser.Dlna/PlayTo/DeviceService.cs → MediaBrowser.Dlna/Common/DeviceService.cs

@@ -1,5 +1,5 @@
 
-namespace MediaBrowser.Dlna.PlayTo
+namespace MediaBrowser.Dlna.Common
 {
     public class DeviceService
     {
@@ -13,15 +13,6 @@ namespace MediaBrowser.Dlna.PlayTo
 
         public string EventSubUrl { get; set; }
 
-        public DeviceService(string serviceType, string serviceId, string scpdUrl, string controlUrl, string eventSubUrl)
-        {
-            ServiceType = serviceType;
-            ServiceId = serviceId;
-            ScpdUrl = scpdUrl;
-            ControlUrl = controlUrl;
-            EventSubUrl = eventSubUrl;
-        }
-
         public override string ToString()
         {
             return string.Format("{0}", ServiceId);

+ 21 - 0
MediaBrowser.Dlna/Common/ServiceAction.cs

@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Dlna.Common
+{
+    public class ServiceAction
+    {
+        public string Name { get; set; }
+
+        public List<Argument> ArgumentList { get; set; }
+
+        public override string ToString()
+        {
+            return Name;
+        }
+
+        public ServiceAction()
+        {
+            ArgumentList = new List<Argument>();
+        }
+    }
+}

+ 25 - 0
MediaBrowser.Dlna/Common/StateVariable.cs

@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Dlna.Common
+{
+    public class StateVariable
+    {
+        public string Name { get; set; }
+
+        public string DataType { get; set; }
+
+        public bool SendsEvents { get; set; }
+
+        public List<string> AllowedValues { get; set; }
+
+        public override string ToString()
+        {
+            return Name;
+        }
+
+        public StateVariable()
+        {
+            AllowedValues = new List<string>();
+        }
+    }
+}

+ 24 - 2
MediaBrowser.Dlna/DlnaManager.cs

@@ -1,9 +1,9 @@
-using System.Text;
-using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Dlna.Profiles;
+using MediaBrowser.Dlna.Server;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Serialization;
@@ -11,6 +11,7 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Text;
 using System.Text.RegularExpressions;
 
 namespace MediaBrowser.Dlna
@@ -476,5 +477,26 @@ namespace MediaBrowser.Dlna
             internal DeviceProfileInfo Info { get; set; }
             internal string Path { get; set; }
         }
+
+        public string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId)
+        {
+            var profile = GetProfile(headers) ??
+                          GetDefaultProfile();
+
+            return new DescriptionXmlBuilder(profile, serverUuId).GetXml();
+        }
+
+        public string GetContentDirectoryXml(IDictionary<string, string> headers)
+        {
+            var profile = GetProfile(headers) ??
+                          GetDefaultProfile();
+
+            return new ContentDirectoryXmlBuilder(profile).GetXml();
+        }
+
+        public ControlResponse ProcessControlRequest(ControlRequest request)
+        {
+            return new ControlHandler(_logger).ProcessControlRequest(request);
+        }
     }
 }

BIN
MediaBrowser.Dlna/Images/logo-120.jpg


BIN
MediaBrowser.Dlna/Images/logo-120.png


BIN
MediaBrowser.Dlna/Images/logo-48.jpg


BIN
MediaBrowser.Dlna/Images/logo-48.png


+ 16 - 5
MediaBrowser.Dlna/MediaBrowser.Dlna.csproj

@@ -52,13 +52,13 @@
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
     <Compile Include="DlnaManager.cs" />
-    <Compile Include="PlayTo\Argument.cs" />
+    <Compile Include="Common\Argument.cs" />
     <Compile Include="PlayTo\CurrentIdEventArgs.cs" />
     <Compile Include="PlayTo\Device.cs">
       <SubType>Code</SubType>
     </Compile>
     <Compile Include="PlayTo\DeviceInfo.cs" />
-    <Compile Include="PlayTo\DeviceService.cs" />
+    <Compile Include="Common\DeviceService.cs" />
     <Compile Include="PlayTo\DidlBuilder.cs" />
     <Compile Include="PlayTo\DlnaController.cs" />
     <Compile Include="PlayTo\Extensions.cs" />
@@ -68,20 +68,25 @@
     <Compile Include="PlayTo\PlaylistItemFactory.cs" />
     <Compile Include="PlayTo\PlayToManager.cs" />
     <Compile Include="PlayTo\PlayToServerEntryPoint.cs" />
-    <Compile Include="PlayTo\ServiceAction.cs" />
+    <Compile Include="Common\ServiceAction.cs" />
     <Compile Include="Profiles\Foobar2000Profile.cs" />
     <Compile Include="Profiles\Windows81Profile.cs" />
     <Compile Include="Profiles\WindowsMediaCenterProfile.cs" />
     <Compile Include="Profiles\WindowsPhoneProfile.cs" />
+    <Compile Include="Server\ControlHandler.cs" />
+    <Compile Include="Server\ServiceActionListBuilder.cs" />
+    <Compile Include="Server\ContentDirectoryXmlBuilder.cs" />
+    <Compile Include="Server\Datagram.cs" />
+    <Compile Include="Server\DescriptionXmlBuilder.cs" />
     <Compile Include="Ssdp\SsdpHelper.cs" />
     <Compile Include="PlayTo\SsdpHttpClient.cs" />
-    <Compile Include="PlayTo\StateVariable.cs" />
+    <Compile Include="Common\StateVariable.cs" />
     <Compile Include="PlayTo\StreamHelper.cs" />
     <Compile Include="PlayTo\TransportCommands.cs" />
     <Compile Include="PlayTo\TransportStateEventArgs.cs" />
     <Compile Include="PlayTo\uBaseObject.cs" />
     <Compile Include="PlayTo\uContainer.cs" />
-    <Compile Include="PlayTo\DeviceIcon.cs" />
+    <Compile Include="Common\DeviceIcon.cs" />
     <Compile Include="PlayTo\uParser.cs" />
     <Compile Include="PlayTo\uPnpNamespaces.cs" />
     <Compile Include="Profiles\DefaultProfile.cs" />
@@ -141,6 +146,12 @@
   <ItemGroup>
     <EmbeddedResource Include="Profiles\Xml\Default.xml" />
   </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="Images\logo-120.jpg" />
+    <EmbeddedResource Include="Images\logo-120.png" />
+    <EmbeddedResource Include="Images\logo-48.jpg" />
+    <EmbeddedResource Include="Images\logo-48.png" />
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.

+ 0 - 29
MediaBrowser.Dlna/PlayTo/Argument.cs

@@ -1,29 +0,0 @@
-using System;
-using System.Xml.Linq;
-
-namespace MediaBrowser.Dlna.PlayTo
-{  
-    public class Argument
-    {
-        public string Name { get;  set; }
-
-        public string Direction { get;  set; }
-
-        public string RelatedStateVariable { get;  set; }
-
-        public static Argument FromXml(XElement container)
-        {
-            if (container == null)
-            {
-                throw new ArgumentNullException("container");
-            }
-
-            return new Argument
-            {
-                Name = container.GetValue(uPnpNamespaces.svc + "name"),
-                Direction = container.GetValue(uPnpNamespaces.svc + "direction"),
-                RelatedStateVariable = container.GetValue(uPnpNamespaces.svc + "relatedStateVariable")                
-            };
-        }
-    }
-}

+ 19 - 8
MediaBrowser.Dlna/PlayTo/Device.cs

@@ -1,9 +1,10 @@
-using System.Globalization;
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Dlna.Common;
 using MediaBrowser.Model.Logging;
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
@@ -209,6 +210,9 @@ namespace MediaBrowser.Dlna.PlayTo
             return SetVolume(tmp);
         }
 
+        /// <summary>
+        /// Sets volume on a scale of 0-100
+        /// </summary>
         public async Task<bool> SetVolume(int value)
         {
             var command = RendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
@@ -337,7 +341,7 @@ namespace MediaBrowser.Dlna.PlayTo
                 throw new InvalidOperationException("Unable to find service");
             }
 
-            var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, 1))
+            var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, 1))
                 .ConfigureAwait(false);
 
             _lapsCount = GetLapsCount();
@@ -352,7 +356,7 @@ namespace MediaBrowser.Dlna.PlayTo
 
             var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType);
 
-            var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, 1))
+            var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, 1))
                 .ConfigureAwait(false);
             await Task.Delay(50).ConfigureAwait(false);
             return true;
@@ -366,7 +370,7 @@ namespace MediaBrowser.Dlna.PlayTo
 
             var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType);
 
-            var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, 1))
+            var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, 1))
                 .ConfigureAwait(false);
 
             await Task.Delay(50).ConfigureAwait(false);
@@ -575,7 +579,7 @@ namespace MediaBrowser.Dlna.PlayTo
                 return false;
             }
 
-            var trackString = (string) track;
+            var trackString = (string)track;
 
             if (string.IsNullOrWhiteSpace(trackString) || string.Equals(trackString, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
             {
@@ -583,7 +587,7 @@ namespace MediaBrowser.Dlna.PlayTo
             }
 
             XElement uPnpResponse;
-            
+
             try
             {
                 uPnpResponse = XElement.Parse(trackString);
@@ -838,7 +842,14 @@ namespace MediaBrowser.Dlna.PlayTo
             var controlURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("controlURL"));
             var eventSubURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("eventSubURL"));
 
-            return new DeviceService(type, id, scpdUrl, controlURL, eventSubURL);
+            return new DeviceService
+            {
+                ControlUrl = controlURL,
+                EventSubUrl = eventSubURL,
+                ScpdUrl = scpdUrl,
+                ServiceId = id,
+                ServiceType = type
+            };
         }
 
         #region Events

+ 2 - 2
MediaBrowser.Dlna/PlayTo/DeviceInfo.cs

@@ -1,6 +1,6 @@
-using MediaBrowser.Controller.Dlna;
-using System.Collections.Generic;
+using MediaBrowser.Dlna.Common;
 using MediaBrowser.Model.Dlna;
+using System.Collections.Generic;
 
 namespace MediaBrowser.Dlna.PlayTo
 {

+ 32 - 5
MediaBrowser.Dlna/PlayTo/DlnaController.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Net;
+using System.Globalization;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Entities;
@@ -206,7 +207,8 @@ namespace MediaBrowser.Dlna.PlayTo
                         IsPaused = _device.IsPaused,
                         MediaSourceId = playlistItem.MediaSourceId,
                         AudioStreamIndex = playlistItem.AudioStreamIndex,
-                        SubtitleStreamIndex = playlistItem.SubtitleStreamIndex
+                        SubtitleStreamIndex = playlistItem.SubtitleStreamIndex,
+                        VolumeLevel = _device.Volume
 
                     }).ConfigureAwait(false);
                 }
@@ -331,12 +333,17 @@ namespace MediaBrowser.Dlna.PlayTo
             return Task.FromResult(true);
         }
 
-        public Task SendServerShutdownNotification(CancellationToken cancellationToken)
+        public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
         {
             return Task.FromResult(true);
         }
 
-        public Task SendBrowseCommand(BrowseRequest command, CancellationToken cancellationToken)
+        public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
+        {
+            return Task.FromResult(true);
+        }
+
+        public Task SendServerShutdownNotification(CancellationToken cancellationToken)
         {
             return Task.FromResult(true);
         }
@@ -609,11 +616,13 @@ namespace MediaBrowser.Dlna.PlayTo
             }
         }
 
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        
         public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
         {
             GeneralCommandType commandType;
 
-            if (!Enum.TryParse(command.Name, true, out commandType))
+            if (Enum.TryParse(command.Name, true, out commandType))
             {
                 switch (commandType)
                 {
@@ -627,6 +636,24 @@ namespace MediaBrowser.Dlna.PlayTo
                         return _device.VolumeUp(true);
                     case GeneralCommandType.ToggleMute:
                         return _device.ToggleMute();
+                    case GeneralCommandType.SetVolume:
+                    {
+                        string volumeArg;
+
+                        if (command.Arguments.TryGetValue("Volume", out volumeArg))
+                        {
+                            int volume;
+
+                            if (int.TryParse(volumeArg, NumberStyles.Any, _usCulture, out volume))
+                            {
+                                return _device.SetVolume(volume);
+                            }
+
+                            throw new ArgumentException("Unsupported volume value supplied.");
+                        }
+
+                        throw new ArgumentException("Volume argument cannot be null");
+                    }
                     default:
                         return Task.FromResult(true);
                 }

+ 2 - 1
MediaBrowser.Dlna/PlayTo/PlayToManager.cs

@@ -265,7 +265,8 @@ namespace MediaBrowser.Dlna.PlayTo
                             GeneralCommandType.VolumeUp.ToString(),
                             GeneralCommandType.Mute.ToString(),
                             GeneralCommandType.Unmute.ToString(),
-                            GeneralCommandType.ToggleMute.ToString()
+                            GeneralCommandType.ToggleMute.ToString(),
+                            GeneralCommandType.SetVolume.ToString()
                         }
                     });
 

+ 0 - 34
MediaBrowser.Dlna/PlayTo/ServiceAction.cs

@@ -1,34 +0,0 @@
-using System.Collections.Generic;
-using System.Xml.Linq;
-
-namespace MediaBrowser.Dlna.PlayTo
-{
-    public class ServiceAction
-    {
-        public string Name { get; set; }
-
-        public List<Argument> ArgumentList { get; set; }
-
-        public override string ToString()
-        {
-            return Name;
-        }
-
-        public static ServiceAction FromXml(XElement container)
-        {
-            var argumentList = new List<Argument>();
-
-            foreach (var arg in container.Descendants(uPnpNamespaces.svc + "argument"))
-            {
-                argumentList.Add(Argument.FromXml(arg));
-            }
-            
-            return new ServiceAction
-            {
-                Name = container.GetValue(uPnpNamespaces.svc + "name"),
-
-                ArgumentList = argumentList
-            };
-        }
-    }
-}

+ 1 - 0
MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Dlna.Common;
 using System;
 using System.Globalization;
 using System.IO;

+ 0 - 52
MediaBrowser.Dlna/PlayTo/StateVariable.cs

@@ -1,52 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Xml.Linq;
-
-namespace MediaBrowser.Dlna.PlayTo
-{
-    public class StateVariable
-    {
-        public string Name { get; set; }
-
-        public string DataType { get; set; }
-
-        private List<string> _allowedValues = new List<string>();
-        public List<string> AllowedValues
-        {
-            get
-            {
-                return _allowedValues;
-            }
-            set
-            {
-                _allowedValues = value;
-            }
-        }
-
-        public override string ToString()
-        {
-            return Name;
-        }
-
-        public static StateVariable FromXml(XElement container)
-        {
-            var allowedValues = new List<string>();
-            var element = container.Descendants(uPnpNamespaces.svc + "allowedValueList")
-                .FirstOrDefault();
-            
-            if (element != null)
-            {
-                var values = element.Descendants(uPnpNamespaces.svc + "allowedValue");
-
-                allowedValues.AddRange(values.Select(child => child.Value));
-            }
-
-            return new StateVariable
-            {
-                Name = container.GetValue(uPnpNamespaces.svc + "name"),
-                DataType = container.GetValue(uPnpNamespaces.svc + "dataType"),
-                AllowedValues = allowedValues
-            };
-        }
-    }
-}

+ 58 - 3
MediaBrowser.Dlna/PlayTo/TransportCommands.cs

@@ -1,4 +1,6 @@
-using System.Collections.Generic;
+using System;
+using MediaBrowser.Dlna.Common;
+using System.Collections.Generic;
 using System.Linq;
 using System.Xml.Linq;
 
@@ -40,7 +42,7 @@ namespace MediaBrowser.Dlna.PlayTo
 
             foreach (var container in actionList.Descendants(uPnpNamespaces.svc + "action"))
             {
-                command.ServiceActions.Add(ServiceAction.FromXml(container));
+                command.ServiceActions.Add(ServiceActionFromXml(container));
             }
 
             var stateValues = document.Descendants(uPnpNamespaces.ServiceStateTable).FirstOrDefault();
@@ -49,13 +51,66 @@ namespace MediaBrowser.Dlna.PlayTo
             {
                 foreach (var container in stateValues.Elements(uPnpNamespaces.svc + "stateVariable"))
                 {
-                    command.StateVariables.Add(StateVariable.FromXml(container));
+                    command.StateVariables.Add(FromXml(container));
                 }
             }
 
             return command;
         }
 
+        private static ServiceAction ServiceActionFromXml(XElement container)
+        {
+            var argumentList = new List<Argument>();
+
+            foreach (var arg in container.Descendants(uPnpNamespaces.svc + "argument"))
+            {
+                argumentList.Add(ArgumentFromXml(arg));
+            }
+
+            return new ServiceAction
+            {
+                Name = container.GetValue(uPnpNamespaces.svc + "name"),
+
+                ArgumentList = argumentList
+            };
+        }
+
+        private static Argument ArgumentFromXml(XElement container)
+        {
+            if (container == null)
+            {
+                throw new ArgumentNullException("container");
+            }
+
+            return new Argument
+            {
+                Name = container.GetValue(uPnpNamespaces.svc + "name"),
+                Direction = container.GetValue(uPnpNamespaces.svc + "direction"),
+                RelatedStateVariable = container.GetValue(uPnpNamespaces.svc + "relatedStateVariable")
+            };
+        }
+
+        public static StateVariable FromXml(XElement container)
+        {
+            var allowedValues = new List<string>();
+            var element = container.Descendants(uPnpNamespaces.svc + "allowedValueList")
+                .FirstOrDefault();
+
+            if (element != null)
+            {
+                var values = element.Descendants(uPnpNamespaces.svc + "allowedValue");
+
+                allowedValues.AddRange(values.Select(child => child.Value));
+            }
+
+            return new StateVariable
+            {
+                Name = container.GetValue(uPnpNamespaces.svc + "name"),
+                DataType = container.GetValue(uPnpNamespaces.svc + "dataType"),
+                AllowedValues = allowedValues
+            };
+        }
+
         public string BuildPost(ServiceAction action, string xmlNamespace)
         {
             var stateString = string.Empty;

+ 235 - 0
MediaBrowser.Dlna/Server/ContentDirectoryXmlBuilder.cs

@@ -0,0 +1,235 @@
+using MediaBrowser.Dlna.Common;
+using MediaBrowser.Model.Dlna;
+using System.Collections.Generic;
+using System.Security;
+using System.Text;
+
+namespace MediaBrowser.Dlna.Server
+{
+    public class ContentDirectoryXmlBuilder
+    {
+        private readonly DeviceProfile _profile;
+
+        public ContentDirectoryXmlBuilder(DeviceProfile profile)
+        {
+            _profile = profile;
+        }
+
+        public string GetXml()
+        {
+            var builder = new StringBuilder();
+
+            builder.Append("<?xml version=\"1.0\"?>");
+            builder.Append("scpd xmlns=\"urn:schemas-upnp-org:service-1-0\"");
+
+            builder.Append("<specVersion>");
+            builder.Append("<major>1</major>");
+            builder.Append("<minor>0</minor>");
+            builder.Append("</specVersion>");
+
+            AppendActionList(builder);
+            AppendServiceStateTable(builder);
+
+            builder.Append("</scpd>");
+
+            return builder.ToString();
+        }
+
+        private void AppendActionList(StringBuilder builder)
+        {
+            builder.Append("<actionList>");
+
+            foreach (var item in new ServiceActionListBuilder().GetActions())
+            {
+                builder.Append("<action>");
+
+                builder.Append("<name>" + SecurityElement.Escape(item.Name ?? string.Empty) + "</name>");
+
+                builder.Append("<argumentList>");
+
+                foreach (var argument in item.ArgumentList)
+                {
+                    builder.Append("<argument>");
+
+                    builder.Append("<name>" + SecurityElement.Escape(argument.Name ?? string.Empty) + "</name>");
+                    builder.Append("<direction>" + SecurityElement.Escape(argument.Direction ?? string.Empty) + "</direction>");
+                    builder.Append("<relatedStateVariable>" + SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>");
+                    
+                    builder.Append("</argument>");
+                }
+
+                builder.Append("</argumentList>");
+                
+                builder.Append("</action>");
+            }
+            
+            builder.Append("</actionList>");
+        }
+
+        private void AppendServiceStateTable(StringBuilder builder)
+        {
+            builder.Append("<serviceStateTable>");
+
+            foreach (var item in GetStateVariables())
+            {
+                var sendEvents = item.SendsEvents ? "yes" : "no";
+
+                builder.Append("<stateVariable sendEvents=\"" + sendEvents + "\">");
+
+                builder.Append("<name>" + SecurityElement.Escape(item.Name ?? string.Empty) + "</name>");
+                builder.Append("<dataType>" + SecurityElement.Escape(item.DataType ?? string.Empty) + "</dataType>");
+
+                if (item.AllowedValues.Count > 0)
+                {
+                    builder.Append("<allowedValueList>");
+                    foreach (var allowedValue in item.AllowedValues)
+                    {
+                        builder.Append("<allowedValue>" + SecurityElement.Escape(allowedValue) + "</allowedValue>");
+                    }
+                    builder.Append("</allowedValueList>");
+                }
+
+                builder.Append("</stateVariable>");
+            }
+
+            builder.Append("</serviceStateTable>");
+        }
+
+        private IEnumerable<StateVariable> GetStateVariables()
+        {
+            var list = new List<StateVariable>();
+
+            list.Add(new StateVariable
+            {
+                Name = "A_ARG_TYPE_SortCriteria",
+                DataType = "string",
+                SendsEvents = false
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "A_ARG_TYPE_UpdateID",
+                DataType = "ui4",
+                SendsEvents = false
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "A_ARG_TYPE_SearchCriteria",
+                DataType = "string",
+                SendsEvents = false
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "A_ARG_TYPE_Filter",
+                DataType = "string",
+                SendsEvents = false
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "A_ARG_TYPE_Result",
+                DataType = "string",
+                SendsEvents = false
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "A_ARG_TYPE_Index",
+                DataType = "ui4",
+                SendsEvents = false
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "A_ARG_TYPE_ObjectID",
+                DataType = "string",
+                SendsEvents = false
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "SortCapabilities",
+                DataType = "string",
+                SendsEvents = false
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "SearchCapabilities",
+                DataType = "string",
+                SendsEvents = false
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "A_ARG_TYPE_Count",
+                DataType = "ui4",
+                SendsEvents = false
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "A_ARG_TYPE_BrowseFlag",
+                DataType = "string",
+                SendsEvents = false,
+
+                AllowedValues = new List<string>
+                {
+                    "BrowseMetadata",
+                    "BrowseDirectChildren"
+                }
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "SystemUpdateID",
+                DataType = "ui4",
+                SendsEvents = true
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "A_ARG_TYPE_BrowseLetter",
+                DataType = "string",
+                SendsEvents = false
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "A_ARG_TYPE_CategoryType",
+                DataType = "ui4",
+                SendsEvents = false
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "A_ARG_TYPE_RID",
+                DataType = "ui4",
+                SendsEvents = false
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "A_ARG_TYPE_PosSec",
+                DataType = "ui4",
+                SendsEvents = false
+            });
+
+            list.Add(new StateVariable
+            {
+                Name = "A_ARG_TYPE_Featurelist",
+                DataType = "string",
+                SendsEvents = false
+            });
+            
+            return list;
+        }
+
+        public override string ToString()
+        {
+            return GetXml();
+        }
+    }
+}

+ 190 - 0
MediaBrowser.Dlna/Server/ControlHandler.cs

@@ -0,0 +1,190 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Xml;
+
+namespace MediaBrowser.Dlna.Server
+{
+    public class ControlHandler
+    {
+        private readonly ILogger _logger;
+
+        private const string NS_DC = "http://purl.org/dc/elements/1.1/";
+        private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
+        private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
+        private const string NS_SEC = "http://www.sec.co.kr/";
+        private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
+        private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
+
+        private const int systemID = 0;
+
+        public ControlHandler(ILogger logger)
+        {
+            _logger = logger;
+        }
+
+        public ControlResponse ProcessControlRequest(ControlRequest request)
+        {
+            var soap = new XmlDocument();
+            soap.LoadXml(request.InputXml);
+            var sparams = new Headers();
+            var body = soap.GetElementsByTagName("Body", NS_SOAPENV).Item(0);
+
+            var method = body.FirstChild;
+
+            foreach (var p in method.ChildNodes)
+            {
+                var e = p as XmlElement;
+                if (e == null)
+                {
+                    continue;
+                }
+                sparams.Add(e.LocalName, e.InnerText.Trim());
+            }
+
+            var env = new XmlDocument();
+            env.AppendChild(env.CreateXmlDeclaration("1.0", "utf-8", "yes"));
+            var envelope = env.CreateElement("SOAP-ENV", "Envelope", NS_SOAPENV);
+            env.AppendChild(envelope);
+            envelope.SetAttribute("encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/");
+
+            var rbody = env.CreateElement("SOAP-ENV:Body", NS_SOAPENV);
+            env.DocumentElement.AppendChild(rbody);
+
+            IEnumerable<KeyValuePair<string, string>> result;
+            switch (method.LocalName)
+            {
+                case "GetSearchCapabilities":
+                    result = HandleGetSearchCapabilities();
+                    break;
+                case "GetSortCapabilities":
+                    result = HandleGetSortCapabilities();
+                    break;
+                case "GetSystemUpdateID":
+                    result = HandleGetSystemUpdateID();
+                    break;
+                case "Browse":
+                    result = HandleBrowse(sparams);
+                    break;
+                case "X_GetFeatureList":
+                    result = HandleXGetFeatureList();
+                    break;
+                case "X_SetBookmark":
+                    result = HandleXSetBookmark(sparams);
+                    break;
+                default:
+                    throw new ResourceNotFoundException();
+            }
+
+            var response = env.CreateElement(String.Format("u:{0}Response", method.LocalName), method.NamespaceURI);
+            rbody.AppendChild(response);
+
+            foreach (var i in result)
+            {
+                var ri = env.CreateElement(i.Key);
+                ri.InnerText = i.Value;
+                response.AppendChild(ri);
+            }
+
+            var controlResponse = new ControlResponse
+            {
+                Xml = env.OuterXml
+            };
+
+            controlResponse.Headers.Add("EXT", string.Empty);
+
+            return controlResponse;
+        }
+
+        private Headers HandleXSetBookmark(Headers sparams)
+        {
+            var id = sparams["ObjectID"];
+            //var item = GetItem(id) as IBookmarkable;
+            //if (item != null)
+            //{
+            //    var newbookmark = long.Parse(sparams["PosSecond"]);
+            //    if (newbookmark > 30)
+            //    {
+            //        newbookmark -= 5;
+            //    }
+            //    if (newbookmark > 30 || !item.Bookmark.HasValue || item.Bookmark.Value < 60)
+            //    {
+            //        item.Bookmark = newbookmark;
+            //    }
+            //}
+            return new Headers();
+        }
+
+        private Headers HandleGetSearchCapabilities()
+        {
+            return new Headers { { "SearchCaps", string.Empty } };
+        }
+
+        private Headers HandleGetSortCapabilities()
+        {
+            return new Headers { { "SortCaps", string.Empty } };
+        }
+
+        private Headers HandleGetSystemUpdateID()
+        {
+            return new Headers { { "Id", systemID.ToString() } };
+        }
+
+        private Headers HandleXGetFeatureList()
+        {
+            return new Headers { { "FeatureList", GetFeatureListXml() } };
+        }
+
+        private string GetFeatureListXml()
+        {
+            var builder = new StringBuilder();
+
+            builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+            builder.Append("<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">");
+
+            builder.Append("<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">");
+            builder.Append("<container id=\"I\" type=\"object.item.imageItem\"/>");
+            builder.Append("<container id=\"A\" type=\"object.item.audioItem\"/>");
+            builder.Append("<container id=\"V\" type=\"object.item.videoItem\"/>");
+            builder.Append("</Feature>");
+
+            builder.Append("</Features>");
+
+            return builder.ToString();
+        }
+
+        private IEnumerable<KeyValuePair<string, string>> HandleBrowse(Headers sparams)
+        {
+            var id = sparams["ObjectID"];
+            var flag = sparams["BrowseFlag"];
+
+            int requested;
+            var provided = 0;
+            int start;
+
+            if (sparams.ContainsKey("RequestedCount") && int.TryParse(sparams["RequestedCount"], out requested) && requested <= 0)
+            {
+                requested = 20;
+            }
+            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);
+
+            return null;
+        }
+    }
+}

+ 65 - 0
MediaBrowser.Dlna/Server/Datagram.cs

@@ -0,0 +1,65 @@
+using MediaBrowser.Model.Logging;
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+
+namespace MediaBrowser.Dlna.Server
+{
+    public class Datagram
+    {
+        public IPEndPoint EndPoint { get; private set; }
+        public IPAddress LocalAddress { get; private set; }
+        public string Message { get; private set; }
+        public bool Sticky { get; private set; }
+
+        public int SendCount { get; private set; }
+
+        private readonly ILogger _logger;
+
+        public Datagram(IPEndPoint endPoint, IPAddress localAddress, ILogger logger, string message, bool sticky)
+        {
+            Message = message;
+            _logger = logger;
+            Sticky = sticky;
+            LocalAddress = localAddress;
+            EndPoint = endPoint;
+        }
+
+        public void Send()
+        {
+            var msg = Encoding.ASCII.GetBytes(Message);
+            try
+            {
+                var client = new UdpClient();
+                client.Client.Bind(new IPEndPoint(LocalAddress, 0));
+                client.BeginSend(msg, msg.Length, EndPoint, result =>
+                {
+                    try
+                    {
+                        client.EndSend(result);
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.ErrorException("Error sending Datagram", ex);
+                    }
+                    finally
+                    {
+                        try
+                        {
+                            client.Close();
+                        }
+                        catch (Exception)
+                        {
+                        }
+                    }
+                }, null);
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error sending Datagram", ex);
+            }
+            ++SendCount;
+        }
+    }
+}

+ 190 - 0
MediaBrowser.Dlna/Server/DescriptionXmlBuilder.cs

@@ -0,0 +1,190 @@
+using MediaBrowser.Dlna.Common;
+using MediaBrowser.Model.Dlna;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Security;
+using System.Text;
+
+namespace MediaBrowser.Dlna.Server
+{
+    public class DescriptionXmlBuilder
+    {
+        private readonly DeviceProfile _profile;
+
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        private readonly string _serverUdn;
+
+        public DescriptionXmlBuilder(DeviceProfile profile, string serverUdn)
+        {
+            if (string.IsNullOrWhiteSpace(serverUdn))
+            {
+                throw new ArgumentNullException("serverUdn");
+            }
+
+            _profile = profile;
+            _serverUdn = serverUdn;
+        }
+
+        public string GetXml()
+        {
+            var builder = new StringBuilder();
+
+            builder.Append("<?xml version=\"1.0\"?>");
+            builder.Append("<root xmlns=\"urn:schemas-upnp-org:device-1-0\" xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\" xmlns:sec=\"http://www.sec.co.kr/dlna\">");
+
+            builder.Append("<specVersion>");
+            builder.Append("<major>1</major>");
+            builder.Append("<minor>0</minor>");
+            builder.Append("</specVersion>");
+
+            AppendDeviceInfo(builder);
+
+            builder.Append("</root>");
+
+            return builder.ToString();
+        }
+
+        private void AppendDeviceInfo(StringBuilder builder)
+        {
+            builder.Append("<device>");
+            AppendDeviceProperties(builder);
+
+            AppendIconList(builder);
+            AppendServiceList(builder);
+            builder.Append("</device>");
+        }
+
+        private void AppendDeviceProperties(StringBuilder builder)
+        {
+            builder.Append("<UDN>" + SecurityElement.Escape(_serverUdn) + "</UDN>");
+            builder.Append("<dlna:X_DLNACAP>" + SecurityElement.Escape(_profile.XDlnaCap ?? string.Empty) + "</dlna:X_DLNACAP>");
+
+            if (!string.IsNullOrWhiteSpace(_profile.XDlnaDoc))
+            {
+                builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" +
+                               SecurityElement.Escape(_profile.XDlnaDoc) + "</dlna:X_DLNADOC>");
+            }
+            else
+            {
+                builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">DMS-1.50</dlna:X_DLNADOC>");
+            }
+
+            builder.Append("<friendlyName>" + SecurityElement.Escape(_profile.FriendlyName ?? string.Empty) + "</friendlyName>");
+            builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>");
+            builder.Append("<manufacturer>" + SecurityElement.Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>");
+            builder.Append("<manufacturerURL>" + SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>");
+            builder.Append("<modelName>" + SecurityElement.Escape(_profile.ModelName ?? string.Empty) + "</modelName>");
+            builder.Append("<modelDescription>" + SecurityElement.Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>");
+            builder.Append("<modelNumber>" + SecurityElement.Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>");
+            builder.Append("<modelURL>" + SecurityElement.Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>");
+            builder.Append("<serialNumber>" + SecurityElement.Escape(_profile.SerialNumber ?? string.Empty) + "</serialNumber>");
+
+            builder.Append("<sec:ProductCap>DCM10,getMediaInfo.sec</sec:ProductCap>");
+            builder.Append("<sec:X_ProductCap>DCM10,getMediaInfo.sec</sec:X_ProductCap>");
+        }
+
+        private void AppendIconList(StringBuilder builder)
+        {
+            builder.Append("<iconList>");
+
+            foreach (var icon in GetIcons())
+            {
+                builder.Append("<icon>");
+
+                builder.Append("<mimetype>" + SecurityElement.Escape(icon.MimeType ?? string.Empty) + "</mimetype>");
+                builder.Append("<width>" + SecurityElement.Escape(icon.Width.ToString(_usCulture)) + "</width>");
+                builder.Append("<height>" + SecurityElement.Escape(icon.Height.ToString(_usCulture)) + "</height>");
+                builder.Append("<depth>" + SecurityElement.Escape(icon.Depth ?? string.Empty) + "</depth>");
+                builder.Append("<url>" + SecurityElement.Escape(icon.Url ?? string.Empty) + "</url>");
+
+                builder.Append("</icon>");
+            }
+
+            builder.Append("</iconList>");
+        }
+
+        private void AppendServiceList(StringBuilder builder)
+        {
+            builder.Append("<serviceList>");
+
+            foreach (var service in GetServices())
+            {
+                builder.Append("<service>");
+
+                builder.Append("<serviceType>" + SecurityElement.Escape(service.ServiceType ?? string.Empty) + "</serviceType>");
+                builder.Append("<serviceId>" + SecurityElement.Escape(service.ServiceId ?? string.Empty) + "</serviceId>");
+                builder.Append("<SCPDURL>" + SecurityElement.Escape(service.ScpdUrl ?? string.Empty) + "</SCPDURL>");
+                builder.Append("<controlURL>" + SecurityElement.Escape(service.ControlUrl ?? string.Empty) + "</controlURL>");
+                builder.Append("<eventSubURL>" + SecurityElement.Escape(service.EventSubUrl ?? string.Empty) + "</eventSubURL>");
+
+                builder.Append("</service>");
+            }
+
+            builder.Append("</serviceList>");
+        }
+
+        private IEnumerable<DeviceIcon> GetIcons()
+        {
+            var list = new List<DeviceIcon>();
+
+            list.Add(new DeviceIcon
+            {
+                MimeType = "image/jpeg",
+                Depth = "24",
+                Width = 48,
+                Height = 48,
+                Url = "/mediabrowser/dlna/icons/small.jpg"
+            });
+
+            list.Add(new DeviceIcon
+            {
+                MimeType = "image/jpeg",
+                Depth = "24",
+                Width = 120,
+                Height = 120,
+                Url = "/mediabrowser/dlna/icons/large.jpg"
+            });
+
+            list.Add(new DeviceIcon
+            {
+                MimeType = "image/png",
+                Depth = "24",
+                Width = 48,
+                Height = 48,
+                Url = "/mediabrowser/dlna/icons/small.png"
+            });
+
+            list.Add(new DeviceIcon
+            {
+                MimeType = "image/png",
+                Depth = "24",
+                Width = 120,
+                Height = 120,
+                Url = "/mediabrowser/dlna/icons/large.png"
+            });
+            
+            return list;
+        }
+
+        private IEnumerable<DeviceService> GetServices()
+        {
+            var list = new List<DeviceService>();
+
+            list.Add(new DeviceService
+            {
+                ServiceType = "urn:schemas-upnp-org:service:ContentDirectory:1",
+                ServiceId = "urn:upnp-org:serviceId:ContentDirectory",
+                ScpdUrl = "/mediabrowser/dlna/contentdirectory.xml",
+                ControlUrl = "/servicecontrol"
+            });
+
+            return list;
+        }
+
+        public override string ToString()
+        {
+            return GetXml();
+        }
+    }
+}

+ 24 - 3
MediaBrowser.Dlna/Server/DlnaServerEntryPoint.cs

@@ -1,8 +1,11 @@
 using MediaBrowser.Common;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.Logging;
 using System;
+using System.Net;
 
 namespace MediaBrowser.Dlna.Server
 {
@@ -13,11 +16,13 @@ namespace MediaBrowser.Dlna.Server
 
         private SsdpHandler _ssdpHandler;
         private readonly IApplicationHost _appHost;
+        private readonly INetworkManager _network;
 
-        public DlnaServerEntryPoint(IServerConfigurationManager config, ILogManager logManager, IApplicationHost appHost)
+        public DlnaServerEntryPoint(IServerConfigurationManager config, ILogManager logManager, IApplicationHost appHost, INetworkManager network)
         {
             _config = config;
             _appHost = appHost;
+            _network = network;
             _logger = logManager.GetLogger("DlnaServer");
         }
 
@@ -25,12 +30,12 @@ namespace MediaBrowser.Dlna.Server
         {
             _config.ConfigurationUpdated += ConfigurationUpdated;
 
-            //ReloadServer();
+            ReloadServer();
         }
 
         void ConfigurationUpdated(object sender, EventArgs e)
         {
-            //ReloadServer();
+            ReloadServer();
         }
 
         private void ReloadServer()
@@ -57,6 +62,8 @@ namespace MediaBrowser.Dlna.Server
                 try
                 {
                     _ssdpHandler = new SsdpHandler(_logger, _config, signature);
+
+                    RegisterEndpoints();
                 }
                 catch (Exception ex)
                 {
@@ -65,6 +72,20 @@ namespace MediaBrowser.Dlna.Server
             }
         }
 
+        private void RegisterEndpoints()
+        {
+            foreach (var address in _network.GetLocalIpAddresses())
+            {
+                var guid = address.GetMD5();
+
+                var descriptorURI = "/mediabrowser/dlna/" + guid.ToString("N") + "/description.xml";
+
+                var uri = new Uri(string.Format("http://{0}:{1}{2}", address, _config.Configuration.HttpServerPortNumber, descriptorURI));
+
+                _ssdpHandler.RegisterNotification(guid, uri, IPAddress.Parse(address));
+            }
+        }
+
         private void DisposeServer()
         {
             lock (_syncLock)

+ 209 - 0
MediaBrowser.Dlna/Server/ServiceActionListBuilder.cs

@@ -0,0 +1,209 @@
+using MediaBrowser.Dlna.Common;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Dlna.Server
+{
+    public class ServiceActionListBuilder
+    {
+        public IEnumerable<ServiceAction> GetActions()
+        {
+            var list = new List<ServiceAction>
+            {
+                GetGetSystemUpdateIDAction(),
+                GetSearchCapabilitiesAction(),
+                GetSortCapabilitiesAction(),
+                GetBrowseAction(),
+                GetX_GetFeatureListAction(),
+                GetXSetBookmarkAction()
+            };
+
+            return list;
+        }
+
+        private ServiceAction GetGetSystemUpdateIDAction()
+        {
+            var action = new ServiceAction
+            {
+                Name = "GetSystemUpdateID"
+            };
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "Id",
+                Direction = "out",
+                RelatedStateVariable = "SystemUpdateID"
+            });
+
+            return action;
+        }
+
+        private ServiceAction GetSearchCapabilitiesAction()
+        {
+            var action = new ServiceAction
+            {
+                Name = "GetSearchCapabilities"
+            };
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "SearchCaps",
+                Direction = "out",
+                RelatedStateVariable = "SearchCapabilities"
+            });
+
+            return action;
+        }
+
+        private ServiceAction GetSortCapabilitiesAction()
+        {
+            var action = new ServiceAction
+            {
+                Name = "GetSortCapabilities"
+            };
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "SortCaps",
+                Direction = "out",
+                RelatedStateVariable = "SortCapabilities"
+            });
+
+            return action;
+        }
+
+        private ServiceAction GetX_GetFeatureListAction()
+        {
+            var action = new ServiceAction
+            {
+                Name = "X_GetFeatureList"
+            };
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "FeatureList",
+                Direction = "out",
+                RelatedStateVariable = "A_ARG_TYPE_Featurelist"
+            });
+
+            return action;
+        }
+
+        private ServiceAction GetBrowseAction()
+        {
+            var action = new ServiceAction
+            {
+                Name = "Browse"
+            };
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "ObjectID",
+                Direction = "in",
+                RelatedStateVariable = "A_ARG_TYPE_ObjectID"
+            });
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "BrowseFlag",
+                Direction = "in",
+                RelatedStateVariable = "A_ARG_TYPE_BrowseFlag"
+            });
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "Filter",
+                Direction = "in",
+                RelatedStateVariable = "A_ARG_TYPE_Filter"
+            });
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "StartingIndex",
+                Direction = "in",
+                RelatedStateVariable = "A_ARG_TYPE_Index"
+            });
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "RequestedCount",
+                Direction = "in",
+                RelatedStateVariable = "A_ARG_TYPE_Count"
+            });
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "SortCriteria",
+                Direction = "in",
+                RelatedStateVariable = "A_ARG_TYPE_SortCriteria"
+            });
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "Result",
+                Direction = "out",
+                RelatedStateVariable = "A_ARG_TYPE_Result"
+            });
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "NumberReturned",
+                Direction = "out",
+                RelatedStateVariable = "A_ARG_TYPE_Count"
+            });
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "TotalMatches",
+                Direction = "out",
+                RelatedStateVariable = "A_ARG_TYPE_Count"
+            });
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "UpdateID",
+                Direction = "out",
+                RelatedStateVariable = "A_ARG_TYPE_UpdateID"
+            });
+
+            return action;
+        }
+
+        private ServiceAction GetXSetBookmarkAction()
+        {
+            var action = new ServiceAction
+            {
+                Name = "X_SetBookmark"
+            };
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "CategoryType",
+                Direction = "in",
+                RelatedStateVariable = "A_ARG_TYPE_CategoryType"
+            });
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "RID",
+                Direction = "in",
+                RelatedStateVariable = "A_ARG_TYPE_RID"
+            });
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "ObjectID",
+                Direction = "in",
+                RelatedStateVariable = "A_ARG_TYPE_ObjectID"
+            });
+
+            action.ArgumentList.Add(new Argument
+            {
+                Name = "PosSecond",
+                Direction = "in",
+                RelatedStateVariable = "A_ARG_TYPE_PosSec"
+            });
+
+            return action;
+        }
+    }
+}

+ 145 - 32
MediaBrowser.Dlna/Server/SsdpHandler.cs

@@ -1,21 +1,26 @@
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.Logging;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Net;
 using System.Net.Sockets;
 using System.Text;
+using System.Threading;
 
 namespace MediaBrowser.Dlna.Server
 {
     public class SsdpHandler : IDisposable
     {
+        private readonly AutoResetEvent _datagramPosted = new AutoResetEvent(false);
+        private readonly ConcurrentQueue<Datagram> _messageQueue = new ConcurrentQueue<Datagram>();
+
         private readonly ILogger _logger;
         private readonly IServerConfigurationManager _config;
         private readonly string _serverSignature;
-        private bool _isDisposed = false;
+        private bool _isDisposed;
 
         const string SSDPAddr = "239.255.255.250";
         const int SSDPPort = 1900;
@@ -27,6 +32,9 @@ namespace MediaBrowser.Dlna.Server
 
         private readonly Dictionary<Guid, List<UpnpDevice>> _devices = new Dictionary<Guid, List<UpnpDevice>>();
 
+        private Timer _queueTimer;
+        private Timer _notificationTimer;
+
         public SsdpHandler(ILogger logger, IServerConfigurationManager config, string serverSignature)
         {
             _logger = logger;
@@ -59,6 +67,8 @@ namespace MediaBrowser.Dlna.Server
             _udpClient.JoinMulticastGroup(_ssdpIp, 2);
             _logger.Info("SSDP service started");
             Receive();
+
+            StartNotificationTimer();
         }
 
         private void Receive()
@@ -137,7 +147,7 @@ namespace MediaBrowser.Dlna.Server
 
             foreach (var d in Devices)
             {
-                if (!string.IsNullOrEmpty(req) && req != d.Type)
+                if (!string.IsNullOrEmpty(req) && !string.Equals(req, d.Type, StringComparison.OrdinalIgnoreCase))
                 {
                     continue;
                 }
@@ -157,28 +167,67 @@ namespace MediaBrowser.Dlna.Server
             headers.Add("ST", dev.Type);
             headers.Add("USN", dev.USN);
 
-            SendDatagram(endpoint, String.Format("HTTP/1.1 200 OK\r\n{0}\r\n", headers.HeaderBlock), false);
+            var msg = String.Format("HTTP/1.1 200 OK\r\n{0}\r\n", headers.HeaderBlock);
+
+            SendDatagram(endpoint, dev.Address, msg, false);
+
             _logger.Info("{1} - Responded to a {0} request", dev.Type, endpoint);
         }
 
-        private void SendDatagram(IPEndPoint endpoint, string msg, bool sticky)
+        private void SendDatagram(IPEndPoint endpoint, IPAddress localAddress, string msg, bool sticky)
         {
             if (_isDisposed)
             {
                 return;
             }
-            //var dgram = new Datagram(endpoint, msg, sticky);
-            //if (messageQueue.Count == 0)
-            //{
-            //    dgram.Send();
-            //}
-            //messageQueue.Enqueue(dgram);
-            //queueTimer.Enabled = true;
+
+            var dgram = new Datagram(endpoint, localAddress, _logger, msg, sticky);
+            if (_messageQueue.Count == 0)
+            {
+                dgram.Send();
+            }
+            _messageQueue.Enqueue(dgram);
+            StartQueueTimer();
+        }
+
+        private void QueueTimerCallback(object state)
+        {
+            while (_messageQueue.Count != 0)
+            {
+                Datagram msg;
+                if (!_messageQueue.TryPeek(out msg))
+                {
+                    continue;
+                }
+
+                if (msg != null && (!_isDisposed || msg.Sticky))
+                {
+                    msg.Send();
+                    if (msg.SendCount > 2)
+                    {
+                        _messageQueue.TryDequeue(out msg);
+                    }
+                    break;
+                }
+
+                _messageQueue.TryDequeue(out msg);
+            }
+
+            _datagramPosted.Set();
+
+            if (_messageQueue.Count > 0)
+            {
+                StartQueueTimer();
+            }
+            else
+            {
+                DisposeQueueTimer();
+            }
         }
 
         private void NotifyAll()
         {
-            _logger.Debug("NotifyAll");
+            _logger.Debug("Sending alive notifications");
             foreach (var d in Devices)
             {
                 NotifyDevice(d, "alive", false);
@@ -197,64 +246,128 @@ namespace MediaBrowser.Dlna.Server
             headers.Add("NT", dev.Type);
             headers.Add("USN", dev.USN);
 
-            SendDatagram(_ssdpEndp, String.Format("NOTIFY * HTTP/1.1\r\n{0}\r\n", headers.HeaderBlock), sticky);
+            var msg = String.Format("NOTIFY * HTTP/1.1\r\n{0}\r\n", headers.HeaderBlock);
+
             _logger.Debug("{0} said {1}", dev.USN, type);
+            SendDatagram(_ssdpEndp, dev.Address, msg, sticky);
         }
 
-        private void RegisterNotification(Guid UUID, Uri Descriptor)
+        public void RegisterNotification(Guid uuid, Uri descriptor, IPAddress address)
         {
             List<UpnpDevice> list;
             lock (_devices)
             {
-                if (!_devices.TryGetValue(UUID, out list))
+                if (!_devices.TryGetValue(uuid, out list))
                 {
-                    _devices.Add(UUID, list = new List<UpnpDevice>());
+                    _devices.Add(uuid, list = new List<UpnpDevice>());
                 }
             }
 
-            foreach (var t in new[] { "upnp:rootdevice", "urn:schemas-upnp-org:device:MediaServer:1", "urn:schemas-upnp-org:service:ContentDirectory:1", "uuid:" + UUID })
+            foreach (var t in new[]
             {
-                list.Add(new UpnpDevice(UUID, t, Descriptor));
+                "upnp:rootdevice", 
+                "urn:schemas-upnp-org:device:MediaServer:1", 
+                "urn:schemas-upnp-org:service:ContentDirectory:1", 
+                "uuid:" + uuid
+            })
+            {
+                list.Add(new UpnpDevice(uuid, t, descriptor, address));
             }
 
             NotifyAll();
-            _logger.Debug("Registered mount {0}", UUID);
+            _logger.Debug("Registered mount {0} at {1}", uuid, descriptor);
         }
 
-        internal void UnregisterNotification(Guid UUID)
+        private void UnregisterNotification(Guid uuid)
         {
             List<UpnpDevice> dl;
             lock (_devices)
             {
-                if (!_devices.TryGetValue(UUID, out dl))
+                if (!_devices.TryGetValue(uuid, out dl))
                 {
                     return;
                 }
-                _devices.Remove(UUID);
+                _devices.Remove(uuid);
             }
             foreach (var d in dl)
             {
                 NotifyDevice(d, "byebye", true);
             }
-            _logger.Debug("Unregistered mount {0}", UUID);
+            _logger.Debug("Unregistered mount {0}", uuid);
         }
 
         public void Dispose()
         {
             _isDisposed = true;
-            //while (messageQueue.Count != 0)
-            //{
-            //    datagramPosted.WaitOne();
-            //}
+            while (_messageQueue.Count != 0)
+            {
+                _datagramPosted.WaitOne();
+            }
 
             _udpClient.DropMulticastGroup(_ssdpIp);
             _udpClient.Close();
 
-            //notificationTimer.Enabled = false;
-            //queueTimer.Enabled = false;
-            //notificationTimer.Dispose();
-            //queueTimer.Dispose();
-            //datagramPosted.Dispose();
+            DisposeNotificationTimer();
+            DisposeQueueTimer();
+            _datagramPosted.Dispose();
+        }
+
+        private readonly object _queueTimerSyncLock = new object();
+        private void StartQueueTimer()
+        {
+            lock (_queueTimerSyncLock)
+            {
+                if (_queueTimer == null)
+                {
+                    _queueTimer = new Timer(QueueTimerCallback, null, 1000, Timeout.Infinite);
+                }
+                else
+                {
+                    _queueTimer.Change(1000, Timeout.Infinite);
+                }
+            }
+        }
+
+        private void DisposeQueueTimer()
+        {
+            lock (_queueTimerSyncLock)
+            {
+                if (_queueTimer != null)
+                {
+                    _queueTimer.Dispose();
+                    _queueTimer = null;
+                }
+            }
+        }
+
+        private readonly object _notificationTimerSyncLock = new object();
+        private void StartNotificationTimer()
+        {
+            const int intervalMs = 60000;
+
+            lock (_notificationTimerSyncLock)
+            {
+                if (_notificationTimer == null)
+                {
+                    _notificationTimer = new Timer(state => NotifyAll(), null, intervalMs, intervalMs);
+                }
+                else
+                {
+                    _notificationTimer.Change(intervalMs, intervalMs);
+                }
+            }
+        }
+
+        private void DisposeNotificationTimer()
+        {
+            lock (_notificationTimerSyncLock)
+            {
+                if (_notificationTimer != null)
+                {
+                    _notificationTimer.Dispose();
+                    _notificationTimer = null;
+                }
+            }
         }
     }
 }

+ 5 - 1
MediaBrowser.Dlna/Server/UpnpDevice.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Net;
 
 namespace MediaBrowser.Dlna.Server
 {
@@ -8,13 +9,16 @@ namespace MediaBrowser.Dlna.Server
         public readonly string Type;
         public readonly string USN;
         public readonly Guid Uuid;
+        public readonly IPAddress Address;
 
-        public UpnpDevice(Guid aUuid, string aType, Uri aDescriptor)
+        public UpnpDevice(Guid aUuid, string aType, Uri aDescriptor, IPAddress address)
         {
             Uuid = aUuid;
             Type = aType;
             Descriptor = aDescriptor;
 
+            Address = address;
+
             if (Type.StartsWith("uuid:"))
             {
                 USN = Type;

+ 5 - 1
MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj

@@ -31,6 +31,11 @@
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
   </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release Mono|AnyCPU' ">
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Release Mono</OutputPath>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
   <ItemGroup>
     <Reference Include="BDInfo">
       <HintPath>..\packages\MediaBrowser.BdInfo.1.0.0.10\lib\net35\BDInfo.dll</HintPath>
@@ -75,7 +80,6 @@
     <None Include="packages.config" />
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
-  <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.
   <Target Name="BeforeBuild">

+ 8 - 2
MediaBrowser.Model/ApiClient/IApiClient.cs

@@ -62,11 +62,10 @@ namespace MediaBrowser.Model.ApiClient
         /// <summary>
         /// Reports the capabilities.
         /// </summary>
-        /// <param name="sessionId">The session identifier.</param>
         /// <param name="capabilities">The capabilities.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        Task ReportCapabilities(string sessionId, ClientCapabilities capabilities, CancellationToken cancellationToken);
+        Task ReportCapabilities(ClientCapabilities capabilities, CancellationToken cancellationToken);
 
         /// <summary>
         /// Gets the index of the game players.
@@ -771,6 +770,13 @@ namespace MediaBrowser.Model.ApiClient
         /// <returns>System.String.</returns>
         string GetImageUrl(ProgramInfoDto item, ImageOptions options);
 
+        /// <summary>
+        /// Gets the subtitle URL.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <returns>System.String.</returns>
+        string GetSubtitleUrl(SubtitleOptions options);
+        
         /// <summary>
         /// Gets an image url that can be used to download an image from the api
         /// </summary>

+ 1 - 0
MediaBrowser.Model/Dlna/DeviceProfile.cs

@@ -34,6 +34,7 @@ namespace MediaBrowser.Model.Dlna
         public string ModelDescription { get; set; }
         public string ModelNumber { get; set; }
         public string ModelUrl { get; set; }
+        public string SerialNumber { get; set; }
         public bool IgnoreTranscodeByteRangeRequests { get; set; }
         public bool EnableAlbumArtInDidl { get; set; }
 

+ 15 - 0
MediaBrowser.Model/Dto/StreamOptions.cs

@@ -158,4 +158,19 @@
         /// <value>The device id.</value>
         public string DeviceId { get; set; }
     }
+
+    public class SubtitleOptions
+    {
+        /// <summary>
+        /// Gets or sets the item identifier.
+        /// </summary>
+        /// <value>The item identifier.</value>
+        public string ItemId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the index of the stream.
+        /// </summary>
+        /// <value>The index of the stream.</value>
+        public int StreamIndex { get; set; }
+    }
 }

+ 3 - 3
MediaBrowser.Model/Dto/UserDto.cs

@@ -1,7 +1,7 @@
-using System.ComponentModel;
-using System.Diagnostics;
-using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Configuration;
 using System;
+using System.ComponentModel;
+using System.Diagnostics;
 using System.Runtime.Serialization;
 
 namespace MediaBrowser.Model.Dto

+ 60 - 0
MediaBrowser.Model/Entities/BaseItemInfo.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Diagnostics;
 using System.Runtime.Serialization;
 
@@ -46,6 +47,12 @@ namespace MediaBrowser.Model.Entities
         /// <value>The primary image tag.</value>
         public Guid? PrimaryImageTag { get; set; }
 
+        /// <summary>
+        /// Gets or sets the primary image item identifier.
+        /// </summary>
+        /// <value>The primary image item identifier.</value>
+        public string PrimaryImageItemId { get; set; }
+        
         /// <summary>
         /// Gets or sets the thumb image tag.
         /// </summary>
@@ -75,6 +82,54 @@ namespace MediaBrowser.Model.Entities
         /// </summary>
         /// <value>The media version identifier.</value>
         public string MediaSourceId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the premiere date.
+        /// </summary>
+        /// <value>The premiere date.</value>
+        public DateTime? PremiereDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the production year.
+        /// </summary>
+        /// <value>The production year.</value>
+        public int? ProductionYear { get; set; }
+
+        /// <summary>
+        /// Gets or sets the index number.
+        /// </summary>
+        /// <value>The index number.</value>
+        public int? IndexNumber { get; set; }
+
+        /// <summary>
+        /// Gets or sets the index number end.
+        /// </summary>
+        /// <value>The index number end.</value>
+        public int? IndexNumberEnd { get; set; }
+
+        /// <summary>
+        /// Gets or sets the parent index number.
+        /// </summary>
+        /// <value>The parent index number.</value>
+        public int? ParentIndexNumber { get; set; }
+
+        /// <summary>
+        /// Gets or sets the name of the series.
+        /// </summary>
+        /// <value>The name of the series.</value>
+        public string SeriesName { get; set; }
+
+        /// <summary>
+        /// Gets or sets the album.
+        /// </summary>
+        /// <value>The album.</value>
+        public string Album { get; set; }
+
+        /// <summary>
+        /// Gets or sets the artists.
+        /// </summary>
+        /// <value>The artists.</value>
+        public List<string> Artists { get; set; }
         
         /// <summary>
         /// Gets a value indicating whether this instance has primary image.
@@ -85,5 +140,10 @@ namespace MediaBrowser.Model.Entities
         {
             get { return PrimaryImageTag.HasValue; }
         }
+
+        public BaseItemInfo()
+        {
+            Artists = new List<string>();
+        }
     }
 }

+ 3 - 1
MediaBrowser.Model/Session/GeneralCommand.cs

@@ -46,6 +46,8 @@ namespace MediaBrowser.Model.Session
         ToggleMute = 21,
         SetVolume = 22,
         SetAudioStreamIndex = 23,
-        SetSubtitleStreamIndex = 24
+        SetSubtitleStreamIndex = 24,
+        ToggleFullscreen = 25,
+        DisplayContent = 26
     }
 }

+ 0 - 4
MediaBrowser.Model/Session/PlaystateCommand.cs

@@ -31,10 +31,6 @@ namespace MediaBrowser.Model.Session
         /// </summary>
         Seek,
         /// <summary>
-        /// The fullscreen
-        /// </summary>
-        Fullscreen,
-        /// <summary>
         /// The rewind
         /// </summary>
         Rewind,

+ 8 - 6
MediaBrowser.Model/Session/SessionInfoDto.cs

@@ -51,6 +51,12 @@ namespace MediaBrowser.Model.Session
         /// <value>The user id.</value>
         public string UserId { get; set; }
 
+        /// <summary>
+        /// Gets or sets the user primary image tag.
+        /// </summary>
+        /// <value>The user primary image tag.</value>
+        public Guid? UserPrimaryImageTag { get; set; }
+        
         /// <summary>
         /// Gets or sets the name of the user.
         /// </summary>
@@ -164,12 +170,6 @@ namespace MediaBrowser.Model.Session
         /// </summary>
         /// <value><c>true</c> if [supports remote control]; otherwise, <c>false</c>.</value>
         public bool SupportsRemoteControl { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [supports navigation commands].
-        /// </summary>
-        /// <value><c>true</c> if [supports navigation commands]; otherwise, <c>false</c>.</value>
-        public bool SupportsNavigationControl { get; set; }
         
         public event PropertyChangedEventHandler PropertyChanged;
 
@@ -203,10 +203,12 @@ namespace MediaBrowser.Model.Session
     public class ClientCapabilities
     {
         public List<string> PlayableMediaTypes { get; set; }
+        public List<string> SupportedCommands { get; set; }
 
         public ClientCapabilities()
         {
             PlayableMediaTypes = new List<string>();
+            SupportedCommands = new List<string>();
         }
     }
 }

+ 18 - 8
MediaBrowser.Mono.sln

@@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Server.Mono",
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Dlna", "MediaBrowser.Dlna\MediaBrowser.Dlna.csproj", "{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.MediaEncoding", "MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj", "{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|x86 = Debug|x86
@@ -29,6 +31,14 @@ Global
 		Release Mono|Any CPU = Release Mono|Any CPU
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Debug|x86.Build.0 = Debug|Any CPU
+		{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU
+		{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release Mono|Any CPU.Build.0 = Release|Any CPU
+		{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release|Any CPU.Build.0 = Release|Any CPU
+		{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release|x86.ActiveCfg = Release|Any CPU
+		{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release|x86.Build.0 = Release|Any CPU
 		{175A9388-F352-4586-A6B4-070DED62B644}.Debug|x86.ActiveCfg = Debug|x86
 		{175A9388-F352-4586-A6B4-070DED62B644}.Debug|x86.Build.0 = Debug|x86
 		{175A9388-F352-4586-A6B4-070DED62B644}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU
@@ -77,6 +87,14 @@ Global
 		{5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.Build.0 = Release|Any CPU
 		{5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|x86.ActiveCfg = Release|Any CPU
 		{5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|x86.Build.0 = Release|Any CPU
+		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Debug|x86.Build.0 = Debug|Any CPU
+		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU
+		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release Mono|Any CPU.Build.0 = Release Mono|Any CPU
+		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|Any CPU.Build.0 = Release|Any CPU
+		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|x86.ActiveCfg = Release|Any CPU
+		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|x86.Build.0 = Release|Any CPU
 		{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|x86.ActiveCfg = Debug|Any CPU
 		{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|x86.Build.0 = Debug|Any CPU
 		{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU
@@ -101,14 +119,6 @@ Global
 		{C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Release|Any CPU.Build.0 = Release|Any CPU
 		{C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Release|x86.ActiveCfg = Release|Any CPU
 		{C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Release|x86.Build.0 = Release|Any CPU
-		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Debug|x86.ActiveCfg = Debug|Any CPU
-		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Debug|x86.Build.0 = Debug|Any CPU
-		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU
-		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release Mono|Any CPU.Build.0 = Release Mono|Any CPU
-		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|Any CPU.Build.0 = Release|Any CPU
-		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|x86.ActiveCfg = Release|Any CPU
-		{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(MonoDevelopProperties) = preSolution
 		StartupItem = MediaBrowser.Server.Mono\MediaBrowser.Server.Mono.csproj

+ 1 - 1
MediaBrowser.Mono.userprefs

@@ -5,7 +5,7 @@
       <File FileName="MediaBrowser.Server.Mono\app.config" Line="1" Column="1" />
       <File FileName="MediaBrowser.ServerApplication\ApplicationHost.cs" Line="1" Column="1" />
       <File FileName="MediaBrowser.Server.Mono\Native\NativeApp.cs" Line="1" Column="1" />
-      <File FileName="MediaBrowser.Server.Mono\Program.cs" Line="36" Column="34" />
+      <File FileName="MediaBrowser.Server.Mono\Program.cs" Line="36" Column="37" />
     </Files>
   </MonoDevelop.Ide.Workbench>
   <MonoDevelop.Ide.DebuggingService.Breakpoints>

+ 9 - 1
MediaBrowser.Providers/Manager/ImageSaver.cs

@@ -71,10 +71,18 @@ namespace MediaBrowser.Providers.Manager
 
             var saveLocally = item.SupportsLocalMetadata && item.IsSaveLocalMetadataEnabled() && !item.IsOwnedItem && !(item is Audio);
 
-            if (item is IItemByName || item is User)
+            if (item is User)
             {
                 saveLocally = true;
             }
+            if (item is IItemByName)
+            {
+                var hasDualAccess = item as IHasDualAccess;
+                if (hasDualAccess == null || hasDualAccess.IsAccessedByName)
+                {
+                    saveLocally = true;
+                }
+            }
 
             if (type != ImageType.Primary && item is Episode)
             {

+ 8 - 25
MediaBrowser.Providers/Movies/MovieDbProvider.cs

@@ -139,8 +139,6 @@ namespace MediaBrowser.Providers.Movies
         /// </summary>
         private TmdbSettingsResult _tmdbSettings;
 
-        private readonly SemaphoreSlim _tmdbSettingsSemaphore = new SemaphoreSlim(1, 1);
-
         /// <summary>
         /// Gets the TMDB settings.
         /// </summary>
@@ -152,32 +150,17 @@ namespace MediaBrowser.Providers.Movies
                 return _tmdbSettings;
             }
 
-            await _tmdbSettingsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
-            try
+            using (var json = await GetMovieDbResponse(new HttpRequestOptions
             {
-                // Check again in case it got populated while we were waiting.
-                if (_tmdbSettings != null)
-                {
-                    return _tmdbSettings;
-                }
-
-                using (var json = await GetMovieDbResponse(new HttpRequestOptions
-                {
-                    Url = string.Format(TmdbConfigUrl, ApiKey),
-                    CancellationToken = cancellationToken,
-                    AcceptHeader = AcceptHeader
-
-                }).ConfigureAwait(false))
-                {
-                    _tmdbSettings = _jsonSerializer.DeserializeFromStream<TmdbSettingsResult>(json);
+                Url = string.Format(TmdbConfigUrl, ApiKey),
+                CancellationToken = cancellationToken,
+                AcceptHeader = AcceptHeader
 
-                    return _tmdbSettings;
-                }
-            }
-            finally
+            }).ConfigureAwait(false))
             {
-                _tmdbSettingsSemaphore.Release();
+                _tmdbSettings = _jsonSerializer.DeserializeFromStream<TmdbSettingsResult>(json);
+
+                return _tmdbSettings;
             }
         }
 

+ 1 - 4
MediaBrowser.Providers/Music/LastfmArtistProvider.cs

@@ -9,11 +9,8 @@ using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Serialization;
 using System;
 using System.Collections.Generic;
-using System.Globalization;
 using System.IO;
-using System.Linq;
 using System.Net;
-using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 
@@ -64,7 +61,7 @@ namespace MediaBrowser.Providers.Music
             return result;
         }
 
-        protected virtual async Task FetchLastfmData(MusicArtist item, string musicBrainzId, CancellationToken cancellationToken)
+        protected async Task FetchLastfmData(MusicArtist item, string musicBrainzId, CancellationToken cancellationToken)
         {
             // Get artist info with provided id
             var url = RootUrl + String.Format("method=artist.getInfo&mbid={0}&api_key={1}&format=json", UrlEncode(musicBrainzId), ApiKey);

+ 1 - 1
MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs

@@ -78,7 +78,7 @@ namespace MediaBrowser.Providers.TV
                 try
                 {
                     var seriesOffset = TvdbSeriesProvider.GetSeriesOffset(series.ProviderIds);
-                    if (seriesOffset != null)
+                    if (seriesOffset != null && seriesOffset.Value != 0)
                         return TvdbSeasonImageProvider.GetImages(path, language, seriesOffset.Value + 1, cancellationToken);
                     
                     return GetImages(path, language, cancellationToken);

+ 2 - 2
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -360,7 +360,7 @@ namespace MediaBrowser.Server.Implementations.Dto
             {
                 return _imageProcessor.GetImageCacheTag(item, type);
             }
-            catch (IOException ex)
+            catch (Exception ex)
             {
                 _logger.ErrorException("Error getting {0} image info", ex, type);
                 return null;
@@ -373,7 +373,7 @@ namespace MediaBrowser.Server.Implementations.Dto
             {
                 return _imageProcessor.GetImageCacheTag(item, image);
             }
-            catch (IOException ex)
+            catch (Exception ex)
             {
                 _logger.ErrorException("Error getting {0} image info for {1}", ex, image.Type, image.Path);
                 return null;

+ 4 - 2
MediaBrowser.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs

@@ -34,8 +34,10 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio
                 {
                     var collectionType = args.GetCollectionType();
 
-                    if (string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase) ||
-                        string.IsNullOrWhiteSpace(collectionType))
+                    var isStandalone = args.Parent == null;
+
+                    if (isStandalone ||
+                        string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase))
                     {
                         return new Controller.Entities.Audio.Audio();
                     }

+ 14 - 2
MediaBrowser.Server.Implementations/Library/SearchEngine.cs

@@ -31,9 +31,21 @@ namespace MediaBrowser.Server.Implementations.Library
 
         public async Task<QueryResult<SearchHintInfo>> GetSearchHints(SearchQuery query)
         {
-            var user = _userManager.GetUserById(new Guid(query.UserId));
+            IEnumerable<BaseItem> inputItems;
 
-            var inputItems = user.RootFolder.GetRecursiveChildren(user, null).Where(i => !(i is ICollectionFolder));
+            if (string.IsNullOrEmpty(query.UserId))
+            {
+                inputItems = _libraryManager.RootFolder.RecursiveChildren;
+            }
+            else
+            {
+                var user = _userManager.GetUserById(new Guid(query.UserId));
+
+                inputItems = user.RootFolder.GetRecursiveChildren(user, null);
+            }
+
+
+            inputItems = inputItems.Where(i => !(i is ICollectionFolder));
 
             inputItems = _libraryManager.ReplaceVideosWithPrimaryVersions(inputItems);
 

+ 1 - 0
MediaBrowser.Server.Implementations/Localization/JavaScript/ca.json

@@ -0,0 +1 @@
+{"SettingsSaved":"Configuraci\u00f3 guardada.","AddUser":"Afegir Usuari","Users":"Usuaris","Delete":"Esborrar","Administrator":"Administrador","Password":"Contrasenya","DeleteImage":"Esborrar Imatge","DeleteImageConfirmation":"Esteu segur que voleu suprimir aquesta imatge?","FileReadCancelled":"La lectura de l'arxiu ha estat cancel\u00b7lada.","FileNotFound":"Arxiu no trobat.","FileReadError":"S'ha produ\u00eft un error en llegir el fitxer.","DeleteUser":"Esborrar Usuari","DeleteUserConfirmation":"Esteu segur que voleu suprimir {0}?","PasswordResetHeader":"Restablir contrasenya","PasswordResetComplete":"La contrasenya s'ha restablert.","PasswordResetConfirmation":"Esteu segur que voleu restablir la contrasenya?","PasswordSaved":"S'ha guardat la contrasenya.","PasswordMatchError":"Confirmaci\u00f3 de la contrasenya i la contrasenya han de coincidir.","OptionOff":"Apagat","OptionOn":"Enc\u00e8s","OptionRelease":"Versi\u00f3 Oficial","OptionBeta":"Beta","OptionDev":"Dev (Inestable)","UninstallPluginHeader":"Desinstal\u00b7lar Plugin.","UninstallPluginConfirmation":"Esteu segur que voleu desinstal\u00b7lar {0}?","NoPluginConfigurationMessage":"Aquest plugin no necessita configuraci\u00f3.","NoPluginsInstalledMessage":"No t\u00e9 cap plugin instal\u00b7lat.","BrowsePluginCatalogMessage":"Consulti el nostre cat\u00e0leg per veure els plugins disponibles."}

+ 1 - 1
MediaBrowser.Server.Implementations/Localization/JavaScript/de.json

@@ -1 +1 @@
-{"SettingsSaved":"Einstellungen gespeichert","AddUser":"Benutzer hinzuf\u00fcgen","Users":"Benutzer","Delete":"L\u00f6schen","Administrator":"Administrator","Password":"Passwort","DeleteImage":"Bild l\u00f6schen","DeleteImageConfirmation":"M\u00f6chten Sie das Bild wirklich l\u00f6schen?","FileReadCancelled":"Das Einlesen der Datei wurde abgebrochen.","FileNotFound":"Datei nicht gefunden","FileReadError":"Beim Lesen der Datei ist ein Fehler aufgetreten.","DeleteUser":"Benutzer l\u00f6schen","DeleteUserConfirmation":"M\u00f6chten Sie {0} wirklich l\u00f6schen?","PasswordResetHeader":"Passwort zur\u00fccksetzen","PasswordResetComplete":"Das Passwort wurde zur\u00fcckgesetzt.","PasswordResetConfirmation":"M\u00f6chten Sie das Passwort wirklich zur\u00fccksetzen?","PasswordSaved":"Passwort gespeichert","PasswordMatchError":"Passwort und Passwortbest\u00e4tigung stimmen nicht \u00fcberein.","OptionOff":"Aus","OptionOn":"Ein","OptionRelease":"Release","OptionBeta":"Beta","OptionDev":"Dev","UninstallPluginHeader":"Deinstalliere Plugin","UninstallPluginConfirmation":"M\u00f6chten Sie {0} wirklich deinstallieren?","NoPluginConfigurationMessage":"Bei diesem Plugin kann nichts eingestellt werden.","NoPluginsInstalledMessage":"Sie haben keine Plugins installiert.","BrowsePluginCatalogMessage":"Durchsuchen Sie unsere Bibliothek um alle verf\u00fcgbaren Plugins anzuzeigen."}
+{"SettingsSaved":"Einstellungen gespeichert","AddUser":"Benutzer hinzuf\u00fcgen","Users":"Benutzer","Delete":"L\u00f6schen","Administrator":"Administrator","Password":"Passwort","DeleteImage":"Bild l\u00f6schen","DeleteImageConfirmation":"M\u00f6chten Sie das Bild wirklich l\u00f6schen?","FileReadCancelled":"Das Einlesen der Datei wurde abgebrochen.","FileNotFound":"Datei nicht gefunden","FileReadError":"Beim Lesen der Datei ist ein Fehler aufgetreten.","DeleteUser":"Benutzer l\u00f6schen","DeleteUserConfirmation":"M\u00f6chten Sie {0} wirklich l\u00f6schen?","PasswordResetHeader":"Passwort zur\u00fccksetzen","PasswordResetComplete":"Das Passwort wurde zur\u00fcckgesetzt.","PasswordResetConfirmation":"M\u00f6chten Sie das Passwort wirklich zur\u00fccksetzen?","PasswordSaved":"Passwort gespeichert","PasswordMatchError":"Passwort und Passwortbest\u00e4tigung stimmen nicht \u00fcberein.","OptionOff":"Aus","OptionOn":"Ein","OptionRelease":"Offizielles Release","OptionBeta":"Beta","OptionDev":"Entwickler (instabil)","UninstallPluginHeader":"Deinstalliere Plugin","UninstallPluginConfirmation":"M\u00f6chten Sie {0} wirklich deinstallieren?","NoPluginConfigurationMessage":"Bei diesem Plugin kann nichts eingestellt werden.","NoPluginsInstalledMessage":"Sie haben keine Plugins installiert.","BrowsePluginCatalogMessage":"Durchsuchen Sie unsere Bibliothek um alle verf\u00fcgbaren Plugins anzuzeigen."}

+ 1 - 0
MediaBrowser.Server.Implementations/Localization/JavaScript/ms.json

@@ -0,0 +1 @@
+{"SettingsSaved":"Seting Disimpan","AddUser":"Tambah Pengguna","Users":"Para Pengguna","Delete":"Padam","Administrator":"Administrator","Password":"Password","DeleteImage":"Delete Image","DeleteImageConfirmation":"Are you sure you wish to delete this image?","FileReadCancelled":"The file read has been canceled.","FileNotFound":"File not found.","FileReadError":"An error occurred while reading the file.","DeleteUser":"Delete User","DeleteUserConfirmation":"Are you sure you wish to delete {0}?","PasswordResetHeader":"Password Reset","PasswordResetComplete":"The password has been reset.","PasswordResetConfirmation":"Are you sure you wish to reset the password?","PasswordSaved":"Password saved.","PasswordMatchError":"Password and password confirmation must match.","OptionOff":"Off","OptionOn":"On","OptionRelease":"Official Release","OptionBeta":"Beta","OptionDev":"Dev (Unstable)","UninstallPluginHeader":"Uninstall Plugin","UninstallPluginConfirmation":"Are you sure you wish to uninstall {0}?","NoPluginConfigurationMessage":"This plugin has nothing to configure.","NoPluginsInstalledMessage":"You have no plugins installed.","BrowsePluginCatalogMessage":"Browse our plugin catalog to view available plugins."}

+ 1 - 1
MediaBrowser.Server.Implementations/Localization/JavaScript/pt_BR.json

@@ -1 +1 @@
-{"SettingsSaved":"Prefer\u00eancias salvas.","AddUser":"Adicionar Usu\u00e1rio","Users":"Usu\u00e1rios","Delete":"Apagar","Administrator":"Administrador","Password":"Senha","DeleteImage":"Apagar Imagem","DeleteImageConfirmation":"Tem certeza que deseja apagar esta imagem?","FileReadCancelled":"A leitura do arquivo foi cancelada.","FileNotFound":"Arquivo n\u00e3o encontrado.","FileReadError":"Ocorreu um erro ao ler o arquivo.","DeleteUser":"Apagar Usu\u00e1rio","DeleteUserConfirmation":"Tem certeza que deseja apagar {0}?","PasswordResetHeader":"Redefinir Senha","PasswordResetComplete":"A senha foi redefinida.","PasswordResetConfirmation":"Deseja realmente redefinir a senha?","PasswordSaved":"Senha salva.","PasswordMatchError":"A senha e confirma\u00e7\u00e3o da senha devem conferir.","OptionOff":"Off","OptionOn":"On","OptionRelease":"Lan\u00e7amento Oficial","OptionBeta":"Beta","OptionDev":"Dev (Inst\u00e1vel)","UninstallPluginHeader":"Desintalar Plugin","UninstallPluginConfirmation":"Deseja realmente desinstalar {0}?","NoPluginConfigurationMessage":"Este plugin n\u00e3o necessita configurar.","NoPluginsInstalledMessage":"N\u00e3o existem plugins instalados.","BrowsePluginCatalogMessage":"Navegue pelo cat\u00e1logo de plugins para ver os dispon\u00edveis."}
+{"SettingsSaved":"Ajustes salvos.","AddUser":"Adicionar Usu\u00e1rio","Users":"Usu\u00e1rios","Delete":"Apagar","Administrator":"Administrador","Password":"Senha","DeleteImage":"Apagar Imagem","DeleteImageConfirmation":"Tem certeza que deseja apagar esta imagem?","FileReadCancelled":"A leitura do arquivo foi cancelada.","FileNotFound":"Arquivo n\u00e3o encontrado.","FileReadError":"Ocorreu um erro ao ler o arquivo.","DeleteUser":"Apagar Usu\u00e1rio","DeleteUserConfirmation":"Deseja realmente apagar {0}?","PasswordResetHeader":"Redefinir Senha","PasswordResetComplete":"A senha foi redefinida.","PasswordResetConfirmation":"Deseja realmente redefinir a senha?","PasswordSaved":"Senha salva.","PasswordMatchError":"A senha e a confirma\u00e7\u00e3o da senha devem ser iguais.","OptionOff":"Off","OptionOn":"On","OptionRelease":"Lan\u00e7amento Oficial","OptionBeta":"Beta","OptionDev":"Dev (Inst\u00e1vel)","UninstallPluginHeader":"Desinstalar Plugin","UninstallPluginConfirmation":"Deseja realmente desinstalar {0}?","NoPluginConfigurationMessage":"Este plugin n\u00e3o necessita configurar.","NoPluginsInstalledMessage":"N\u00e3o existem plugins instalados.","BrowsePluginCatalogMessage":"Navegue pelo cat\u00e1logo de plugins para ver os dispon\u00edveis."}

File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/JavaScript/ru.json


+ 13 - 2
MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs

@@ -125,7 +125,18 @@ namespace MediaBrowser.Server.Implementations.Localization
         public IEnumerable<CountryInfo> GetCountries()
         {
             return CultureInfo.GetCultures(CultureTypes.SpecificCultures)
-                .Select(c => new RegionInfo(c.LCID))
+                .Select(c =>
+                {
+                    try
+                    {
+                        return new RegionInfo(c.LCID);
+                    }
+                    catch (CultureNotFoundException)
+                    {
+                        return null;
+                    }
+                })
+                .Where(i => i != null)
                 .OrderBy(c => c.DisplayName)
                 .DistinctBy(c => c.TwoLetterISORegionName)
                 .Select(c => new CountryInfo
@@ -356,7 +367,7 @@ namespace MediaBrowser.Server.Implementations.Localization
             }.OrderBy(i => i.Name);
         }
 
-        public string LocalizeDocument(string document, string culture, Func<string,string> tokenBuilder)
+        public string LocalizeDocument(string document, string culture, Func<string, string> tokenBuilder)
         {
             foreach (var pair in GetLocalizationDictionary(culture).ToList())
             {

File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/ar.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/ca.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/cs.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/de.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/el.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/en_GB.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/en_US.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/es.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/es_MX.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/fr.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/he.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/it.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/ms.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/nb.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/nl.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/pt_BR.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/pt_PT.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/ru.json


+ 66 - 1
MediaBrowser.Server.Implementations/Localization/Server/server.json

@@ -450,5 +450,70 @@
 	"LabelMinResumeDuration": "Min resume duration (seconds):",
 	"LabelMinResumePercentageHelp": "Titles are assumed unplayed if stopped before this time",
 	"LabelMaxResumePercentageHelp": "Titles are assumed fully played if stopped after this time",
-	"LabelMinResumeDurationHelp": "Titles shorter than this will not be resumable"
+	"LabelMinResumeDurationHelp": "Titles shorter than this will not be resumable",
+	"TitleAutoOrganize": "Auto-Organize",
+	"TabActivityLog": "Activity Log",
+	"HeaderName": "Name",
+	"HeaderDate": "Date",
+	"HeaderSource": "Source",
+	"HeaderStatus": "Status",
+	"HeaderDestination": "Destination",
+	"HeaderProgram": "Program",
+	"HeaderClients": "Clients",
+	"LabelCompleted": "Completed",
+	"LabelFailed": "Failed",
+	"LabelSkipped": "Skipped",
+	"HeaderEpisodeOrganization": "Episode Organization",
+	"LabelSeries": "Series:",
+	"LabelSeasonNumber": "Season number:",
+	"LabelEpisodeNumber": "Episode number:",
+	"LabelEndingEpisodeNumber": "Ending episode number:",
+	"LabelEndingEpisodeNumberHelp": "Only required for multi-episode files",
+	"HeaderSupportTheTeam": "Support the Media Browser Team",
+	"LabelSupportAmount": "Amount (USD)",
+	"HeaderSupportTheTeamHelp": "Help ensure the continued development of this project by donating. A portion of all donations will be contributed to other free tools we depend on.",
+	"ButtonEnterSupporterKey": "Enter supporter key",
+	"DonationNextStep": "Once complete, please return and enter your supporter key, which you will receive by email.",
+	"AutoOrganizeHelp": "Auto-organize monitors your download folders for new files and moves them to your media directories.",
+	"AutoOrganizeTvHelp": "TV file organizing will only add episodes to existing series. It will not create new series folders.",
+	"OptionEnableEpisodeOrganization": "Enable new episode organization",
+	"LabelWatchFolder": "Watch folder:",
+	"LabelWatchFolderHelp": "The server will poll this folder during the 'Organize new media files' scheduled task.",
+	"ButtonViewScheduledTasks": "View scheduled tasks",
+	"LabelMinFileSizeForOrganize": "Minimum file size (MB):",
+	"LabelMinFileSizeForOrganizeHelp": "Files under this size will be ignored.",
+	"LabelSeasonFolderPattern": "Season folder pattern:",
+	"LabelSeasonZeroFolderName": "Season zero folder name:",
+	"HeaderEpisodeFilePattern": "Episode file pattern",
+	"LabelEpisodePattern": "Episode pattern:",
+	"LabelMultiEpisodePattern": "Multi-Episode pattern:",
+	"HeaderSupportedPatterns": "Supported Patterns",
+	"HeaderTerm": "Term",
+	"HeaderPattern": "Pattern",
+	"HeaderResult": "Result",
+	"LabelDeleteEmptyFolders": "Delete empty folders after organizing",
+	"LabelDeleteEmptyFoldersHelp": "Enable this to keep the download directory clean.",
+	"LabelDeleteLeftOverFiles": "Delete left over files with the following extensions:",
+	"LabelDeleteLeftOverFilesHelp": "Separate with ;. For example: .nfo;.txt",
+	"OptionOverwriteExistingEpisodes": "Overwrite existing episodes",
+	"LabelTransferMethod": "Transfer method",
+	"OptionCopy": "Copy",
+	"OptionMove": "Move",
+	"LabelTransferMethodHelp": "Copy or move files from the watch folder",
+	"HeaderLatestNews": "Latest News",
+	"HeaderHelpImproveMediaBrowser": "Help Improve Media Browser",
+	"HeaderRunningTasks": "Running Tasks",
+	"HeaderActiveDevices": "Active Devices",
+	"HeaderPendingInstallations": "Pending Installations",
+	"HeaerServerInformation": "Server Information",
+	"ButtonRestartNow": "Restart Now",
+	"ButtonRestart": "Restart",
+	"ButtonShutdown": "Shutdown",
+	"ButtonUpdateNow": "Update Now",
+	"PleaseUpdateManually": "Please shutdown the server and update manually.",
+	"NewServerVersionAvailable": "A new version of Media Browser Server is available!",
+	"ServerUpToDate": "Media Browser Server is up to date",
+	"ErrorConnectingToMediaBrowserRepository": "There was an error connecting to the remote Media Browser repository.",
+	"LabelComponentsUpdated": "The following components have been installed or updated:",
+	"MessagePleaseRestartServerToFinishUpdating": "Please restart the server to finish applying updates."
 }

File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/sv.json


File diff suppressed because it is too large
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/zh_TW.json


+ 4 - 0
MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

@@ -319,6 +319,10 @@
     <EmbeddedResource Include="Localization\Server\sv.json" />
     <EmbeddedResource Include="Localization\JavaScript\cs.json" />
     <EmbeddedResource Include="Localization\Server\cs.json" />
+    <EmbeddedResource Include="Localization\JavaScript\ca.json" />
+    <EmbeddedResource Include="Localization\JavaScript\ms.json" />
+    <EmbeddedResource Include="Localization\Server\ca.json" />
+    <EmbeddedResource Include="Localization\Server\ms.json" />
     <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup>

+ 10 - 10
MediaBrowser.Server.Implementations/Roku/RokuSessionController.cs

@@ -46,6 +46,16 @@ namespace MediaBrowser.Server.Implementations.Roku
             return Task.FromResult(true);
         }
 
+        public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
+        {
+            return Task.FromResult(true);
+        }
+
+        public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
+        {
+            return Task.FromResult(true);
+        }
+
         public Task SendMessageCommand(MessageCommand command, CancellationToken cancellationToken)
         {
             return SendCommand(new WebSocketMessage<MessageCommand>
@@ -66,16 +76,6 @@ namespace MediaBrowser.Server.Implementations.Roku
             }, cancellationToken);
         }
 
-        public Task SendBrowseCommand(BrowseRequest command, CancellationToken cancellationToken)
-        {
-            return SendCommand(new WebSocketMessage<BrowseRequest>
-            {
-                MessageType = "Browse",
-                Data = command
-
-            }, cancellationToken);
-        }
-
         public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
         {
             return SendCommand(new WebSocketMessage<PlaystateRequest>

+ 137 - 11
MediaBrowser.Server.Implementations/Session/SessionManager.cs

@@ -8,6 +8,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
@@ -444,6 +445,8 @@ namespace MediaBrowser.Server.Implementations.Session
                 MediaSourceId = info.MediaSourceId
 
             }, _logger);
+
+            await SendPlaybackStartNotification(session, CancellationToken.None).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -583,6 +586,8 @@ namespace MediaBrowser.Server.Implementations.Session
                 MediaSourceId = mediaSourceId
 
             }, _logger);
+
+            await SendPlaybackStoppedNotification(session, CancellationToken.None).ConfigureAwait(false);
         }
 
         private string GetMediaSourceId(BaseItem item, string reportedMediaSourceId)
@@ -858,12 +863,17 @@ namespace MediaBrowser.Server.Implementations.Session
 
         public Task SendBrowseCommand(Guid controllingSessionId, Guid sessionId, BrowseRequest command, CancellationToken cancellationToken)
         {
-            var session = GetSessionForRemoteControl(sessionId);
+            var generalCommand = new GeneralCommand
+            {
+                Name = GeneralCommandType.DisplayContent.ToString()
+            };
 
-            var controllingSession = GetSession(controllingSessionId);
-            AssertCanControl(session, controllingSession);
+            generalCommand.Arguments["Context"] = command.Context;
+            generalCommand.Arguments["ItemId"] = command.ItemId;
+            generalCommand.Arguments["ItemName"] = command.ItemName;
+            generalCommand.Arguments["ItemType"] = command.ItemType;
 
-            return session.SessionController.SendBrowseCommand(command, cancellationToken);
+            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
         }
 
         public Task SendPlaystateCommand(Guid controllingSessionId, Guid sessionId, PlaystateRequest command, CancellationToken cancellationToken)
@@ -972,7 +982,6 @@ namespace MediaBrowser.Server.Implementations.Session
             return Task.WhenAll(tasks);
         }
 
-
         public Task SendSessionEndedNotification(SessionInfo sessionInfo, CancellationToken cancellationToken)
         {
             var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList();
@@ -994,6 +1003,48 @@ namespace MediaBrowser.Server.Implementations.Session
             return Task.WhenAll(tasks);
         }
 
+        public Task SendPlaybackStartNotification(SessionInfo sessionInfo, CancellationToken cancellationToken)
+        {
+            var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList();
+            var dto = GetSessionInfoDto(sessionInfo);
+
+            var tasks = sessions.Select(session => Task.Run(async () =>
+            {
+                try
+                {
+                    await session.SessionController.SendPlaybackStartNotification(dto, cancellationToken).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error in SendPlaybackStartNotification.", ex);
+                }
+
+            }, cancellationToken));
+
+            return Task.WhenAll(tasks);
+        }
+
+        public Task SendPlaybackStoppedNotification(SessionInfo sessionInfo, CancellationToken cancellationToken)
+        {
+            var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList();
+            var dto = GetSessionInfoDto(sessionInfo);
+
+            var tasks = sessions.Select(session => Task.Run(async () =>
+            {
+                try
+                {
+                    await session.SessionController.SendPlaybackStoppedNotification(dto, cancellationToken).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error in SendPlaybackStoppedNotification.", ex);
+                }
+
+            }, cancellationToken));
+
+            return Task.WhenAll(tasks);
+        }
+
         /// <summary>
         /// Adds the additional user.
         /// </summary>
@@ -1131,6 +1182,13 @@ namespace MediaBrowser.Server.Implementations.Session
             if (session.UserId.HasValue)
             {
                 dto.UserId = session.UserId.Value.ToString("N");
+
+                var user = _userManager.GetUserById(session.UserId.Value);
+
+                if (user != null)
+                {
+                    dto.UserPrimaryImageTag = GetImageCacheTag(user, ImageType.Primary);
+                }
             }
 
             return dto;
@@ -1158,19 +1216,84 @@ namespace MediaBrowser.Server.Implementations.Session
                 MediaType = item.MediaType,
                 Type = item.GetClientTypeName(),
                 RunTimeTicks = nowPlayingRuntimeTicks,
-                MediaSourceId = mediaSourceId
+                MediaSourceId = mediaSourceId,
+                IndexNumber = item.IndexNumber,
+                ParentIndexNumber = item.ParentIndexNumber,
+                PremiereDate = item.PremiereDate,
+                ProductionYear = item.ProductionYear
             };
 
             info.PrimaryImageTag = GetImageCacheTag(item, ImageType.Primary);
+            if (info.PrimaryImageTag.HasValue)
+            {
+                info.PrimaryImageItemId = GetDtoId(item);
+            }
+
+            var episode = item as Episode;
+            if (episode != null)
+            {
+                info.IndexNumberEnd = episode.IndexNumberEnd;
+            }
+
+            var hasSeries = item as IHasSeries;
+            if (hasSeries != null)
+            {
+                info.SeriesName = hasSeries.SeriesName;
+            }
+
+            var recording = item as ILiveTvRecording;
+            if (recording != null && recording.RecordingInfo != null)
+            {
+                if (recording.RecordingInfo.IsSeries)
+                {
+                    info.Name = recording.RecordingInfo.EpisodeTitle;
+                    info.SeriesName = recording.RecordingInfo.Name;
+
+                    if (string.IsNullOrWhiteSpace(info.Name))
+                    {
+                        info.Name = recording.RecordingInfo.Name;
+                    }
+                }
+            }
+
+            var audio = item as Audio;
+            if (audio != null)
+            {
+                info.Album = audio.Album;
+                info.Artists = audio.Artists;
+
+                if (!info.PrimaryImageTag.HasValue)
+                {
+                    var album = audio.Parents.OfType<MusicAlbum>().FirstOrDefault();
+
+                    if (album != null && album.HasImage(ImageType.Primary))
+                    {
+                        info.PrimaryImageTag = GetImageCacheTag(album, ImageType.Primary);
+                        if (info.PrimaryImageTag.HasValue)
+                        {
+                            info.PrimaryImageItemId = GetDtoId(album);
+                        }
+                    }
+                }
+            }
+
+            var musicVideo = item as MusicVideo;
+            if (musicVideo != null)
+            {
+                info.Album = musicVideo.Album;
 
+                if (!string.IsNullOrWhiteSpace(musicVideo.Artist))
+                {
+                    info.Artists.Add(musicVideo.Artist);
+                }
+            }
+            
             var backropItem = item.HasImage(ImageType.Backdrop) ? item : null;
 
             var thumbItem = item.HasImage(ImageType.Thumb) ? item : null;
 
             if (thumbItem == null)
             {
-                var episode = item as Episode;
-
                 if (episode != null)
                 {
                     var series = episode.Series;
@@ -1184,8 +1307,6 @@ namespace MediaBrowser.Server.Implementations.Session
 
             if (backropItem == null)
             {
-                var episode = item as Episode;
-
                 if (episode != null)
                 {
                     var series = episode.Series;
@@ -1197,6 +1318,11 @@ namespace MediaBrowser.Server.Implementations.Session
                 }
             }
 
+            if (backropItem == null)
+            {
+                backropItem = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Backdrop));
+            }
+
             if (thumbItem == null)
             {
                 thumbItem = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Thumb));
@@ -1208,7 +1334,7 @@ namespace MediaBrowser.Server.Implementations.Session
                 info.ThumbItemId = GetDtoId(thumbItem);
             }
 
-            if (thumbItem != null)
+            if (backropItem != null)
             {
                 info.BackdropImageTag = GetImageCacheTag(backropItem, ImageType.Backdrop);
                 info.BackdropItemId = GetDtoId(backropItem);

+ 24 - 12
MediaBrowser.Server.Implementations/Session/WebSocketController.cs

@@ -81,18 +81,6 @@ namespace MediaBrowser.Server.Implementations.Session
             }, cancellationToken);
         }
 
-        public Task SendBrowseCommand(BrowseRequest command, CancellationToken cancellationToken)
-        {
-            var socket = GetActiveSocket();
-
-            return socket.SendAsync(new WebSocketMessage<BrowseRequest>
-            {
-                MessageType = "Browse",
-                Data = command
-
-            }, cancellationToken);
-        }
-
         public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
         {
             var socket = GetActiveSocket();
@@ -210,5 +198,29 @@ namespace MediaBrowser.Server.Implementations.Session
 
             }, cancellationToken);
         }
+
+        public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
+        {
+            var socket = GetActiveSocket();
+
+            return socket.SendAsync(new WebSocketMessage<SessionInfoDto>
+            {
+                MessageType = "PlaybackStart",
+                Data = sessionInfo
+
+            }, cancellationToken);
+        }
+
+        public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
+        {
+            var socket = GetActiveSocket();
+
+            return socket.SendAsync(new WebSocketMessage<SessionInfoDto>
+            {
+                MessageType = "PlaybackStopped",
+                Data = sessionInfo
+
+            }, cancellationToken);
+        }
     }
 }

+ 1 - 2
MediaBrowser.Server.Implementations/Udp/UdpServer.cs

@@ -1,5 +1,4 @@
-using System.Threading;
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Logging;

+ 5 - 1
MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj

@@ -101,7 +101,7 @@
       <Name>MediaBrowser.Providers</Name>
     </ProjectReference>
     <ProjectReference Include="..\MediaBrowser.Dlna\MediaBrowser.Dlna.csproj">
-      <Project>{734098eb-6dc1-4dd0-a1ca-3140dcd2737c}</Project>
+      <Project>{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}</Project>
       <Name>MediaBrowser.Dlna</Name>
     </ProjectReference>
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
@@ -124,6 +124,10 @@
       <Project>{4FD51AC5-2C16-4308-A993-C3A84F3B4582}</Project>
       <Name>MediaBrowser.Api</Name>
     </ProjectReference>
+    <ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj">
+      <Project>{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}</Project>
+      <Name>MediaBrowser.MediaEncoding</Name>
+    </ProjectReference>
   </ItemGroup>
   <ItemGroup>
     <Folder Include="EntryPoints\" />

+ 14 - 3
MediaBrowser.Server.Mono/Program.cs

@@ -16,6 +16,7 @@ using System.Net.Security;
 using System.Security.Cryptography.X509Certificates;
 using System.Threading.Tasks;
 using System.Reflection;
+using System.Linq;
 // MONOMKBUNDLE: For the embedded version, mkbundle tool
 #if MONOMKBUNDLE
 using Mono.Unix;
@@ -39,8 +40,13 @@ namespace MediaBrowser.Server.Mono
 			#else
 			var applicationPath = Assembly.GetEntryAssembly ().Location;
 			#endif
+			
+			var commandArgs = Environment.GetCommandLineArgs();
+			
+			// Allow this to be specified on the command line.
+			var customProgramDataPath = commandArgs.ElementAtOrDefault(1);
 
-			var appPaths = CreateApplicationPaths(applicationPath);
+			var appPaths = CreateApplicationPaths(applicationPath, customProgramDataPath);
 
 			var logManager = new NlogManager(appPaths.LogDirectoryPath, "server");
 			logManager.ReloadLogger(LogSeverity.Info);
@@ -70,9 +76,14 @@ namespace MediaBrowser.Server.Mono
 			}
 		}
 
-		private static ServerApplicationPaths CreateApplicationPaths(string applicationPath)
+		private static ServerApplicationPaths CreateApplicationPaths(string applicationPath, string programDataPath)
 		{
-			return new ServerApplicationPaths(applicationPath);
+			if (string.IsNullOrEmpty(programDataPath))
+			{
+				return new ServerApplicationPaths(applicationPath);
+			}
+			
+			return new ServerApplicationPaths(programDataPath, applicationPath);
 		}
 
 		/// <summary>

+ 10 - 4
MediaBrowser.WebDashboard/Api/DashboardService.cs

@@ -372,8 +372,11 @@ namespace MediaBrowser.WebDashboard.Api
             sb.Append("<meta http-equiv=\"X-UA-Compatibility\" content=\"IE=Edge\">");
             sb.Append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">");
             sb.Append("<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">");
+            sb.Append("<meta name=\"mobile-web-app-capable\" content=\"yes\">");
             //sb.Append("<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">");
 
+            sb.Append("<link rel=\"icon\" sizes=\"114x114\" href=\"css/images/touchicon114.png\" />");
+            
             // http://developer.apple.com/library/ios/#DOCUMENTATION/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html
             sb.Append("<link rel=\"apple-touch-icon\" href=\"css/images/touchicon.png\" />");
             sb.Append("<link rel=\"apple-touch-icon\" sizes=\"72x72\" href=\"css/images/touchicon72.png\" />");
@@ -419,6 +422,7 @@ namespace MediaBrowser.WebDashboard.Api
                             {
                                 "scripts/all.js" + versionString,
                                 "thirdparty/jstree1.0/jquery.jstree.min.js",
+                                "thirdparty/jquery.unveil-custom.js",
                                 "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js"
             };
 
@@ -453,17 +457,17 @@ namespace MediaBrowser.WebDashboard.Api
             await memoryStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false);
 
             var builder = new StringBuilder();
-            var assembly = GetType().Assembly;
-            using (var stream = assembly.GetManifestResourceStream("MediaBrowser.WebDashboard.ApiClient.js"))
+
+            using (var fs = _fileSystem.GetFileStream(GetDashboardResourcePath("thirdparty/mediabrowser.apiclient.js"), FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
             {
-                using (var streamReader = new StreamReader(stream))
+                using (var streamReader = new StreamReader(fs))
                 {
                     var text = await streamReader.ReadToEndAsync().ConfigureAwait(false);
                     builder.Append(text);
                     builder.Append(Environment.NewLine);
                 }
             }
-
+            
             foreach (var file in GetScriptFiles())
             {
                 var path = GetDashboardResourcePath("scripts/" + file);
@@ -515,6 +519,7 @@ namespace MediaBrowser.WebDashboard.Api
 
                                 "mediaplayer.js",
                                 "mediaplayer-video.js",
+                                "nowplayingbar.js",
 
                                 "ratingdialog.js",
                                 "aboutpage.js",
@@ -603,6 +608,7 @@ namespace MediaBrowser.WebDashboard.Api
                                 "supporterkeypage.js",
                                 "supporterpage.js",
                                 "episodes.js",
+                                "thememediaplayer.js",
                                 "tvgenres.js",
                                 "tvlatest.js",
                                 "tvpeople.js",

Some files were not shown because too many files changed in this diff