Bläddra i källkod

Merge branch 'dev' of https://github.com/MediaBrowser/Emby into dev

Tavares André 9 år sedan
förälder
incheckning
15a98c5eaa
53 ändrade filer med 940 tillägg och 565 borttagningar
  1. 1 54
      MediaBrowser.Api/ConnectService.cs
  2. 63 22
      MediaBrowser.Api/PinLoginService.cs
  3. 14 2
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  4. 2 2
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  5. 3 0
      MediaBrowser.Api/Playback/StreamRequest.cs
  6. 0 17
      MediaBrowser.Api/UserService.cs
  7. 0 20
      MediaBrowser.Controller/Connect/IConnectManager.cs
  8. 1 1
      MediaBrowser.Controller/Entities/IHasMediaSources.cs
  9. 11 1
      MediaBrowser.Controller/Entities/UserItemData.cs
  10. 3 0
      MediaBrowser.Controller/LiveTv/ITunerHost.cs
  11. 6 0
      MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs
  12. 1 0
      MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
  13. 7 0
      MediaBrowser.Controller/Session/ISessionManager.cs
  14. 2 8
      MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs
  15. 48 21
      MediaBrowser.Dlna/Ssdp/SsdpHandler.cs
  16. 2 0
      MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs
  17. 9 3
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  18. 97 48
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  19. 0 7
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  20. 8 0
      MediaBrowser.Model/Configuration/UserConfiguration.cs
  21. 1 0
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  22. 4 1
      MediaBrowser.Model/Dlna/StreamInfo.cs
  23. 3 0
      MediaBrowser.Model/Dlna/TranscodingProfile.cs
  24. 4 0
      MediaBrowser.Model/Entities/PersonType.cs
  25. 6 0
      MediaBrowser.Model/LiveTv/LiveTvTunerInfoDto.cs
  26. 5 5
      MediaBrowser.Model/MediaInfo/MediaInfo.cs
  27. 2 2
      MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
  28. 12 0
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  29. 0 145
      MediaBrowser.Server.Implementations/Connect/ConnectManager.cs
  30. 2 0
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  31. 1 1
      MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs
  32. 6 0
      MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs
  33. 3 6
      MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs
  34. 0 5
      MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  35. 49 11
      MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs
  36. 4 0
      MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  37. 2 2
      MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  38. 2 1
      MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
  39. 17 3
      MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
  40. 1 1
      MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
  41. 1 1
      MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  42. 4 77
      MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  43. 115 0
      MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  44. 158 41
      MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs
  45. 156 30
      MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs
  46. 3 2
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  47. 1 1
      MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs
  48. 21 4
      MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs
  49. 50 11
      MediaBrowser.Server.Implementations/Session/SessionManager.cs
  50. 19 2
      MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs
  51. 1 1
      MediaBrowser.Server.Implementations/packages.config
  52. 1 1
      MediaBrowser.Server.Startup.Common/ApplicationHost.cs
  53. 8 5
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

+ 1 - 54
MediaBrowser.Api/ConnectService.cs

@@ -1,10 +1,8 @@
-using System;
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Connect;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Connect;
-using MediaBrowser.Model.Dto;
 using ServiceStack;
 using System.Collections.Generic;
 using System.Linq;
@@ -75,28 +73,6 @@ namespace MediaBrowser.Api
         public string ConnectUserId { get; set; }
     }
 
-    [Route("/Connect/Supporters", "GET")]
-    [Authenticated(Roles = "Admin")]
-    public class GetConnectSupporterSummary : IReturn<ConnectSupporterSummary>
-    {
-    }
-
-    [Route("/Connect/Supporters", "DELETE")]
-    [Authenticated(Roles = "Admin")]
-    public class RemoveConnectSupporter : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Connect/Supporters", "POST")]
-    [Authenticated(Roles = "Admin")]
-    public class AddConnectSupporter : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
     public class ConnectService : BaseApiService
     {
         private readonly IConnectManager _connectManager;
@@ -108,35 +84,6 @@ namespace MediaBrowser.Api
             _userManager = userManager;
         }
 
-        public async Task<object> Get(GetConnectSupporterSummary request)
-        {
-            var result = await _connectManager.GetConnectSupporterSummary().ConfigureAwait(false);
-            var existingConnectUserIds = result.Users.Select(i => i.Id).ToList();
-
-            result.EligibleUsers = _userManager.Users
-                .Where(i => !string.IsNullOrWhiteSpace(i.ConnectUserId))
-                .Where(i => !existingConnectUserIds.Contains(i.ConnectUserId, StringComparer.OrdinalIgnoreCase))
-                .OrderBy(i => i.Name)
-                .Select(i => _userManager.GetUserDto(i))
-                .ToList();
-
-            return ToOptimizedResult(result);
-        }
-
-        public void Delete(RemoveConnectSupporter request)
-        {
-            var task = _connectManager.RemoveConnectSupporter(request.Id);
-
-            Task.WaitAll(task);
-        }
-
-        public void Post(AddConnectSupporter request)
-        {
-            var task = _connectManager.AddConnectSupporter(request.Id);
-
-            Task.WaitAll(task);
-        }
-
         public object Post(CreateConnectLink request)
         {
             return _connectManager.LinkUser(request.Id, request.ConnectUsername);

+ 63 - 22
MediaBrowser.Api/PinLoginService.cs

@@ -1,9 +1,14 @@
 using System;
 using System.Collections.Concurrent;
 using System.Globalization;
+using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Connect;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Session;
 using ServiceStack;
 
 namespace MediaBrowser.Api
@@ -13,6 +18,8 @@ namespace MediaBrowser.Api
     {
         [ApiMember(Name = "DeviceId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
         public string DeviceId { get; set; }
+        [ApiMember(Name = "AppName", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string AppName { get; set; }
     }
 
     [Route("/Auth/Pin", "GET", Summary = "Gets pin status")]
@@ -35,7 +42,7 @@ namespace MediaBrowser.Api
 
     [Route("/Auth/Pin/Validate", "POST", Summary = "Validates a pin")]
     [Authenticated]
-    public class ValidatePinRequest : IReturnVoid
+    public class ValidatePinRequest : IReturn<SessionInfoDto>
     {
         [ApiMember(Name = "Pin", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
         public string Pin { get; set; }
@@ -43,10 +50,27 @@ namespace MediaBrowser.Api
 
     public class PinLoginService : BaseApiService
     {
-        private readonly ConcurrentDictionary<string, MyPinStatus> _activeRequests = new ConcurrentDictionary<string, MyPinStatus>(StringComparer.OrdinalIgnoreCase);
+        private static readonly ConcurrentDictionary<string, MyPinStatus> _activeRequests = new ConcurrentDictionary<string, MyPinStatus>(StringComparer.OrdinalIgnoreCase);
+        private readonly ISessionManager _sessionManager;
+        private readonly IUserManager _userManager;
+
+        public PinLoginService(ISessionManager sessionManager, IUserManager userManager)
+        {
+            _sessionManager = sessionManager;
+            _userManager = userManager;
+        }
 
         public object Post(CreatePinRequest request)
         {
+            if (string.IsNullOrWhiteSpace(request.DeviceId))
+            {
+                throw new ArgumentNullException("DeviceId");
+            }
+            if (string.IsNullOrWhiteSpace(request.AppName))
+            {
+                throw new ArgumentNullException("AppName");
+            }
+
             var pin = GetNewPin();
 
             var value = new MyPinStatus
@@ -55,7 +79,8 @@ namespace MediaBrowser.Api
                 IsConfirmed = false,
                 IsExpired = false,
                 Pin = pin,
-                DeviceId = request.DeviceId
+                DeviceId = request.DeviceId,
+                AppName = request.AppName
             };
 
             _activeRequests.AddOrUpdate(pin, value, (k, v) => value);
@@ -75,6 +100,7 @@ namespace MediaBrowser.Api
 
             if (!_activeRequests.TryGetValue(request.Pin, out status))
             {
+                Logger.Debug("Pin {0} not found.", request.Pin);
                 throw new ResourceNotFoundException();
             }
 
@@ -88,12 +114,13 @@ namespace MediaBrowser.Api
             });
         }
 
-        public object Post(ExchangePinRequest request)
+        public async Task<object> Post(ExchangePinRequest request)
         {
             MyPinStatus status;
 
             if (!_activeRequests.TryGetValue(request.Pin, out status))
             {
+                Logger.Debug("Pin {0} not found.", request.Pin);
                 throw new ResourceNotFoundException();
             }
 
@@ -104,14 +131,24 @@ namespace MediaBrowser.Api
                 throw new ResourceNotFoundException();
             }
 
-            return ToOptimizedResult(new PinExchangeResult
+            var auth = AuthorizationContext.GetAuthorizationInfo(Request);
+            var user = _userManager.GetUserById(status.UserId);
+
+            var result = await _sessionManager.CreateNewSession(new AuthenticationRequest
             {
-                // TODO: Add access token
-                UserId = status.UserId
-            });
+                App = auth.Client,
+                AppVersion = auth.Version,
+                DeviceId = auth.DeviceId,
+                DeviceName = auth.Device,
+                RemoteEndPoint = Request.RemoteIp,
+                Username = user.Name
+
+            }).ConfigureAwait(false);
+
+            return ToOptimizedResult(result);
         }
 
-        public void Post(ValidatePinRequest request)
+        public object Post(ValidatePinRequest request)
         {
             MyPinStatus status;
 
@@ -124,12 +161,18 @@ namespace MediaBrowser.Api
 
             status.IsConfirmed = true;
             status.UserId = AuthorizationContext.GetAuthorizationInfo(Request).UserId;
+
+            return ToOptimizedResult(new ValidatePinResult
+            {
+                AppName = status.AppName
+            });
         }
 
         private void EnsureValid(string requestedDeviceId, MyPinStatus status)
         {
             if (!string.Equals(requestedDeviceId, status.DeviceId, StringComparison.OrdinalIgnoreCase))
             {
+                Logger.Debug("Pin device Id's do not match. requestedDeviceId: {0}, status.DeviceId: {1}", requestedDeviceId, status.DeviceId);
                 throw new ResourceNotFoundException();
             }
 
@@ -145,6 +188,7 @@ namespace MediaBrowser.Api
 
             if (status.IsExpired)
             {
+                Logger.Debug("Pin {0} is expired", status.Pin);
                 throw new ResourceNotFoundException();
             }
         }
@@ -163,16 +207,7 @@ namespace MediaBrowser.Api
 
         private string GetNewPinInternal()
         {
-            var length = 5;
-            var pin = string.Empty;
-
-            while (pin.Length < length)
-            {
-                var digit = new Random().Next(0, 9);
-                pin += digit.ToString(CultureInfo.InvariantCulture);
-            }
-
-            return pin;
+            return new Random().Next(10000, 99999).ToString(CultureInfo.InvariantCulture);
         }
 
         private bool IsPinActive(string pin)
@@ -181,15 +216,15 @@ namespace MediaBrowser.Api
 
             if (!_activeRequests.TryGetValue(pin, out status))
             {
-                return true;
+                return false;
             }
 
             if (status.IsExpired)
             {
-                return true;
+                return false;
             }
 
-            return false;
+            return true;
         }
 
         public class MyPinStatus : PinStatusResult
@@ -197,6 +232,12 @@ namespace MediaBrowser.Api
             public DateTime CreationTimeUtc { get; set; }
             public string DeviceId { get; set; }
             public string UserId { get; set; }
+            public string AppName { get; set; }
         }
     }
+
+    public class ValidatePinResult
+    {
+        public string AppName { get; set; }
+    }
 }

+ 14 - 2
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -1462,6 +1462,13 @@ namespace MediaBrowser.Api.Playback
                 {
                     // Duplicating ItemId because of MediaMonkey
                 }
+                else if (i == 24)
+                {
+                    if (videoRequest != null)
+                    {
+                        videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+                    }
+                }
             }
         }
 
@@ -2021,6 +2028,11 @@ namespace MediaBrowser.Api.Playback
                     state.EstimateContentLength = transcodingProfile.EstimateContentLength;
                     state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
                     state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
+
+                    if (state.VideoRequest != null)
+                    {
+                        state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
+                    }
                 }
             }
         }
@@ -2184,9 +2196,9 @@ namespace MediaBrowser.Api.Playback
 
             if (state.VideoRequest != null)
             {
-                if (string.Equals(state.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase))
+                if (string.Equals(state.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase) && state.VideoRequest.CopyTimestamps)
                 {
-                    //inputModifier += " -noaccurate_seek";
+                    inputModifier += " -noaccurate_seek";
                 }
             }
             

+ 2 - 2
MediaBrowser.Api/Playback/Progressive/VideoService.cs

@@ -137,9 +137,9 @@ namespace MediaBrowser.Api.Playback.Progressive
 
             var isOutputMkv = string.Equals(state.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase);
 
-            if (state.RunTimeTicks.HasValue)
+            if (state.RunTimeTicks.HasValue && state.VideoRequest.CopyTimestamps)
             {
-                //args += " -copyts -avoid_negative_ts disabled -start_at_zero";
+                args += " -copyts -avoid_negative_ts disabled -start_at_zero";
             }
             
             if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase))

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

@@ -187,6 +187,9 @@ namespace MediaBrowser.Api.Playback
         [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; }
 
+        [ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool CopyTimestamps { get; set; }
+        
         [ApiMember(Name = "Cabac", Description = "Enable if cabac encoding is required", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
         public bool? Cabac { get; set; }
         

+ 0 - 17
MediaBrowser.Api/UserService.cs

@@ -415,23 +415,6 @@ namespace MediaBrowser.Api
         {
             var auth = AuthorizationContext.GetAuthorizationInfo(Request);
 
-            if (string.IsNullOrWhiteSpace(auth.Client))
-            {
-                auth.Client = "Unknown app";
-            }
-            if (string.IsNullOrWhiteSpace(auth.Device))
-            {
-                auth.Device = "Unknown device";
-            }
-            if (string.IsNullOrWhiteSpace(auth.Version))
-            {
-                auth.Version = "Unknown version";
-            }
-            if (string.IsNullOrWhiteSpace(auth.DeviceId))
-            {
-                auth.DeviceId = "Unknown device id";
-            }
-
             var result = await _sessionMananger.AuthenticateNewSession(new AuthenticationRequest
             {
                 App = auth.Client,

+ 0 - 20
MediaBrowser.Controller/Connect/IConnectManager.cs

@@ -76,25 +76,5 @@ namespace MediaBrowser.Controller.Connect
         /// <param name="token">The token.</param>
         /// <returns><c>true</c> if [is authorization token valid] [the specified token]; otherwise, <c>false</c>.</returns>
         bool IsAuthorizationTokenValid(string token);
-
-        /// <summary>
-        /// Gets the connect supporter summary.
-        /// </summary>
-        /// <returns>Task&lt;ConnectSupporterSummary&gt;.</returns>
-        Task<ConnectSupporterSummary> GetConnectSupporterSummary();
-
-        /// <summary>
-        /// Removes the connect supporter.
-        /// </summary>
-        /// <param name="id">The identifier.</param>
-        /// <returns>Task.</returns>
-        Task RemoveConnectSupporter(string id);
-
-        /// <summary>
-        /// Adds the connect supporter.
-        /// </summary>
-        /// <param name="id">The identifier.</param>
-        /// <returns>Task.</returns>
-        Task AddConnectSupporter(string id);
     }
 }

+ 1 - 1
MediaBrowser.Controller/Entities/IHasMediaSources.cs

@@ -3,7 +3,7 @@ using System.Collections.Generic;
 
 namespace MediaBrowser.Controller.Entities
 {
-    public interface IHasMediaSources : IHasId
+    public interface IHasMediaSources : IHasUserData
     {
         /// <summary>
         /// Gets the media sources.

+ 11 - 1
MediaBrowser.Controller/Entities/UserItemData.cs

@@ -78,7 +78,17 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         /// <value><c>true</c> if played; otherwise, <c>false</c>.</value>
         public bool Played { get; set; }
-
+        /// <summary>
+        /// Gets or sets the index of the audio stream.
+        /// </summary>
+        /// <value>The index of the audio stream.</value>
+        public int? AudioStreamIndex { get; set; }
+        /// <summary>
+        /// Gets or sets the index of the subtitle stream.
+        /// </summary>
+        /// <value>The index of the subtitle stream.</value>
+        public int? SubtitleStreamIndex { get; set; }
+    
         /// <summary>
         /// This is an interpreted property to indicate likes or dislikes
         /// This should never be serialized.

+ 3 - 0
MediaBrowser.Controller/LiveTv/ITunerHost.cs

@@ -46,6 +46,9 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task&lt;List&lt;MediaSourceInfo&gt;&gt;.</returns>
         Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken);
+    }
+    public interface IConfigurableTunerHost
+    {
         /// <summary>
         /// Validates the specified information.
         /// </summary>

+ 6 - 0
MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs

@@ -59,6 +59,12 @@ namespace MediaBrowser.Controller.LiveTv
         /// <value>The clients.</value>
         public List<string> Clients { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance can reset.
+        /// </summary>
+        /// <value><c>true</c> if this instance can reset; otherwise, <c>false</c>.</value>
+        public bool CanReset { get; set; }
+
         public LiveTvTunerInfo()
         {
             Clients = new List<string>();

+ 1 - 0
MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs

@@ -44,6 +44,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         public int? CpuCoreLimit { get; set; }
         public bool ReadInputAtNativeFramerate { get; set; }
         public SubtitleDeliveryMethod SubtitleMethod { get; set; }
+        public bool CopyTimestamps { get; set; }
 
         /// <summary>
         /// Gets a value indicating whether this instance has fixed resolution.

+ 7 - 0
MediaBrowser.Controller/Session/ISessionManager.cs

@@ -250,6 +250,13 @@ namespace MediaBrowser.Controller.Session
         /// <returns>Task{SessionInfo}.</returns>
         Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request);
 
+        /// <summary>
+        /// Creates the new session.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>Task&lt;AuthenticationResult&gt;.</returns>
+        Task<AuthenticationResult> CreateNewSession(AuthenticationRequest request);
+
         /// <summary>
         /// Reports the capabilities.
         /// </summary>

+ 2 - 8
MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs

@@ -132,6 +132,8 @@ namespace MediaBrowser.Dlna.Ssdp
                                 return;
                             }
 
+                            _ssdpHandler.LogMessageReceived(args, true);
+
                             TryCreateDevice(args);
                         }
                     }
@@ -219,14 +221,6 @@ namespace MediaBrowser.Dlna.Ssdp
                 return;
             }
 
-            if (_config.GetDlnaConfiguration().EnableDebugLog)
-            {
-                var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value));
-                var headerText = string.Join(",", headerTexts.ToArray());
-
-                _logger.Debug("{0} Device message received from {1}. Headers: {2}", args.Method, args.EndPoint, headerText);
-            }
-
             EventHelper.FireEventIfNotNull(DeviceDiscovered, this, args, _logger);
         }
 

+ 48 - 21
MediaBrowser.Dlna/Ssdp/SsdpHandler.cs

@@ -93,17 +93,7 @@ namespace MediaBrowser.Dlna.Ssdp
                 return;
             }
 
-            var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog;
-            
-            if (enableDebugLogging)
-            {
-                var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value));
-                var headerText = string.Join(",", headerTexts.ToArray());
-
-                var protocol = isMulticast ? "Multicast" : "Unicast";
-                var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString();
-                _logger.Debug("{0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol);
-            }
+            LogMessageReceived(args, isMulticast);
 
             var headers = args.Headers;
             string st;
@@ -125,6 +115,21 @@ namespace MediaBrowser.Dlna.Ssdp
             EventHelper.FireEventIfNotNull(MessageReceived, this, args, _logger);
         }
 
+        internal void LogMessageReceived(SsdpMessageEventArgs args, bool isMulticast)
+        {
+            var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog;
+
+            if (enableDebugLogging)
+            {
+                var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value));
+                var headerText = string.Join(",", headerTexts.ToArray());
+
+                var protocol = isMulticast ? "Multicast" : "Unicast";
+                var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString();
+                _logger.Debug("{0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol);
+            }
+        }
+
         internal bool IgnoreMessage(SsdpMessageEventArgs args, bool isMulticast)
         {
             string usn;
@@ -139,7 +144,7 @@ namespace MediaBrowser.Dlna.Ssdp
                     //var protocol = isMulticast ? "Multicast" : "Unicast";
                     //var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString();
                     //_logger.Debug("IGNORING {0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol);
-                    
+
                     return true;
                 }
             }
@@ -159,7 +164,7 @@ namespace MediaBrowser.Dlna.Ssdp
                     return true;
                 }
             }
-            
+
             return false;
         }
 
@@ -298,9 +303,17 @@ namespace MediaBrowser.Dlna.Ssdp
 
                     var msg = new SsdpMessageBuilder().BuildMessage(header, values);
 
-                    SendDatagram(msg, endpoint, null, false, 1);
-                    SendDatagram(msg, endpoint, new IPEndPoint(d.Address, 0), false, 1);
-                    //SendDatagram(header, values, endpoint, null, true);
+                    var ipEndPoint = endpoint as IPEndPoint;
+                    if (ipEndPoint != null)
+                    {
+                        SendUnicastRequest(msg, ipEndPoint);
+                    }
+                    else
+                    {
+                        SendDatagram(msg, endpoint, null, false, 2);
+                        SendDatagram(msg, endpoint, new IPEndPoint(d.Address, 0), false, 2);
+                        //SendDatagram(header, values, endpoint, null, true);
+                    }
 
                     if (enableDebugLogging)
                     {
@@ -473,6 +486,7 @@ namespace MediaBrowser.Dlna.Ssdp
             var msg = new SsdpMessageBuilder().BuildMessage(header, values);
 
             SendDatagram(msg, _ssdpEndp, new IPEndPoint(dev.Address, 0), true);
+            //SendUnicastRequest(msg, 1);
         }
 
         public void RegisterNotification(string uuid, Uri descriptionUri, IPAddress address, IEnumerable<string> services)
@@ -577,12 +591,12 @@ namespace MediaBrowser.Dlna.Ssdp
                 }
                 catch (ObjectDisposedException)
                 {
-                    
+
                 }
             }
         }
 
-        private async void SendUnicastRequest(string request)
+        private void SendUnicastRequest(string request, int sendCount = 3)
         {
             if (_unicastClient == null)
             {
@@ -591,19 +605,32 @@ namespace MediaBrowser.Dlna.Ssdp
 
             _logger.Debug("Sending unicast search request");
 
-            byte[] req = Encoding.ASCII.GetBytes(request);
             var ipSsdp = IPAddress.Parse(SSDPAddr);
             var ipTxEnd = new IPEndPoint(ipSsdp, SSDPPort);
 
+            SendUnicastRequest(request, ipTxEnd, sendCount);
+        }
+
+        private async void SendUnicastRequest(string request, IPEndPoint toEndPoint, int sendCount = 3)
+        {
+            if (_unicastClient == null)
+            {
+                return;
+            }
+
+            _logger.Debug("Sending unicast search request");
+
+            byte[] req = Encoding.ASCII.GetBytes(request);
+
             try
             {
-                for (var i = 0; i < 3; i++)
+                for (var i = 0; i < sendCount; i++)
                 {
                     if (i > 0)
                     {
                         await Task.Delay(50).ConfigureAwait(false);
                     }
-                    _unicastClient.Send(req, req.Length, ipTxEnd);
+                    _unicastClient.Send(req, req.Length, toEndPoint);
                 }
             }
             catch (Exception ex)

+ 2 - 0
MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs

@@ -794,6 +794,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 state.EstimateContentLength = transcodingProfile.EstimateContentLength;
                 state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
                 state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
+
+                state.Options.CopyTimestamps = transcodingProfile.CopyTimestamps;
             }
         }
     }

+ 9 - 3
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -129,7 +129,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// <param name="request">The request.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        public Task<Model.MediaInfo.MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
+        public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
         {
             var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
 
@@ -175,7 +175,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{MediaInfoResult}.</returns>
         /// <exception cref="System.ApplicationException">ffprobe failed - streams and format are both null.</exception>
-        private async Task<Model.MediaInfo.MediaInfo> GetMediaInfoInternal(string inputPath,
+        private async Task<MediaInfo> GetMediaInfoInternal(string inputPath,
             string primaryPath,
             MediaProtocol protocol,
             bool extractChapters,
@@ -934,7 +934,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
                     _mediaEncoder._runningProcesses.Remove(this);
                 }
 
-                process.Dispose();
+                try
+                {
+                    process.Dispose();
+                }
+                catch (Exception ex)
+                {
+                }
             }
 
             private bool _disposed;

+ 97 - 48
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -27,7 +27,7 @@ namespace MediaBrowser.MediaEncoding.Probing
 
         public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType videoType, bool isAudio, string path, MediaProtocol protocol)
         {
-            var info = new Model.MediaInfo.MediaInfo
+            var info = new MediaInfo
             {
                 Path = path,
                 Protocol = protocol
@@ -56,40 +56,81 @@ namespace MediaBrowser.MediaEncoding.Probing
                 }
             }
 
-            if (isAudio)
-            {
-                SetAudioRuntimeTicks(data, info);
+            var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            var tagStreamType = isAudio ? "audio" : "video";
 
-                var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
-                // tags are normally located under data.format, but we've seen some cases with ogg where they're part of the audio stream
-                // so let's create a combined list of both
+            if (data.streams != null)
+            {
+                var tagStream = data.streams.FirstOrDefault(i => string.Equals(i.codec_type, tagStreamType, StringComparison.OrdinalIgnoreCase));
 
-                if (data.streams != null)
+                if (tagStream != null && tagStream.tags != null)
                 {
-                    var audioStream = data.streams.FirstOrDefault(i => string.Equals(i.codec_type, "audio", StringComparison.OrdinalIgnoreCase));
-
-                    if (audioStream != null && audioStream.tags != null)
+                    foreach (var pair in tagStream.tags)
                     {
-                        foreach (var pair in audioStream.tags)
-                        {
-                            tags[pair.Key] = pair.Value;
-                        }
+                        tags[pair.Key] = pair.Value;
                     }
                 }
+            }
 
-                if (data.format != null && data.format.tags != null)
+            if (data.format != null && data.format.tags != null)
+            {
+                foreach (var pair in data.format.tags)
                 {
-                    foreach (var pair in data.format.tags)
-                    {
-                        tags[pair.Key] = pair.Value;
-                    }
+                    tags[pair.Key] = pair.Value;
                 }
+            }
+
+            FetchGenres(info, tags);
+            var overview = FFProbeHelpers.GetDictionaryValue(tags, "description");
+            if (!string.IsNullOrWhiteSpace(overview))
+            {
+                info.Overview = overview;
+            }
+
+            var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
+            if (!string.IsNullOrWhiteSpace(title))
+            {
+                info.Name = title;
+            }
+
+            info.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
+
+            // Several different forms of retaildate
+            info.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
+                FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
+                FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
+                FFProbeHelpers.GetDictionaryDateTime(tags, "date");
+
+            if (isAudio)
+            {
+                SetAudioRuntimeTicks(data, info);
+
+                // tags are normally located under data.format, but we've seen some cases with ogg where they're part of the audio stream
+                // so let's create a combined list of both
 
                 SetAudioInfoFromTags(info, tags);
             }
             else
             {
+                var iTunEXTC = FFProbeHelpers.GetDictionaryValue(tags, "iTunEXTC");
+                if (!string.IsNullOrWhiteSpace(iTunEXTC))
+                {
+                    var parts = iTunEXTC.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+                    // Example 
+                    // mpaa|G|100|For crude humor
+                    if (parts.Length == 4)
+                    {
+                        info.OfficialRating = parts[1];
+                        info.OfficialRatingDescription = parts[3];
+                    }
+                }
+                
+                var itunesXml = FFProbeHelpers.GetDictionaryValue(tags, "iTunMOVI");
+                if (!string.IsNullOrWhiteSpace(itunesXml))
+                {
+                    FetchFromItunesInfo(itunesXml, info);
+                }
+                
                 if (data.format != null && !string.IsNullOrEmpty(data.format.duration))
                 {
                     info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks;
@@ -108,6 +149,11 @@ namespace MediaBrowser.MediaEncoding.Probing
             return info;
         }
 
+        private void FetchFromItunesInfo(string xml, MediaInfo info)
+        {
+            // <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>cast</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Blender Foundation</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Janus Bager Kristensen</string>\n\t\t</dict>\n\t</array>\n\t<key>directors</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Sacha Goedegebure</string>\n\t\t</dict>\n\t</array>\n\t<key>studio</key>\n\t<string>Blender Foundation</string>\n</dict>\n</plist>\n
+        }
+
         /// <summary>
         /// Converts ffprobe stream info to our MediaStream class
         /// </summary>
@@ -430,16 +476,8 @@ namespace MediaBrowser.MediaEncoding.Probing
             }
         }
 
-        private void SetAudioInfoFromTags(Model.MediaInfo.MediaInfo audio, Dictionary<string, string> tags)
+        private void SetAudioInfoFromTags(MediaInfo audio, Dictionary<string, string> tags)
         {
-            var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
-
-            // Only set Name if title was found in the dictionary
-            if (!string.IsNullOrEmpty(title))
-            {
-                audio.Title = title;
-            }
-
             var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
             if (!string.IsNullOrWhiteSpace(composer))
             {
@@ -458,6 +496,26 @@ namespace MediaBrowser.MediaEncoding.Probing
                 }
             }
 
+            var lyricist = FFProbeHelpers.GetDictionaryValue(tags, "lyricist");
+
+            if (!string.IsNullOrWhiteSpace(lyricist))
+            {
+                foreach (var person in Split(lyricist, false))
+                {
+                    audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist });
+                }
+            }
+            // Check for writer some music is tagged that way as alternative to composer/lyricist
+            var writer = FFProbeHelpers.GetDictionaryValue(tags, "writer");
+
+            if (!string.IsNullOrWhiteSpace(writer))
+            {
+                foreach (var person in Split(writer, false))
+                {
+                    audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer });
+                }
+            }
+
             audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
 
             var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
@@ -511,22 +569,12 @@ namespace MediaBrowser.MediaEncoding.Probing
             // Disc number
             audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
 
-            audio.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
-
-            // Several different forms of retaildate
-            audio.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
-                FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
-                FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
-                FFProbeHelpers.GetDictionaryDateTime(tags, "date");
-
             // If we don't have a ProductionYear try and get it from PremiereDate
             if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
             {
                 audio.ProductionYear = audio.PremiereDate.Value.ToLocalTime().Year;
             }
 
-            FetchGenres(audio, tags);
-
             // There's several values in tags may or may not be present
             FetchStudios(audio, tags, "organization");
             FetchStudios(audio, tags, "ensemble");
@@ -693,7 +741,7 @@ namespace MediaBrowser.MediaEncoding.Probing
         /// </summary>
         /// <param name="info">The information.</param>
         /// <param name="tags">The tags.</param>
-        private void FetchGenres(Model.MediaInfo.MediaInfo info, Dictionary<string, string> tags)
+        private void FetchGenres(MediaInfo info, Dictionary<string, string> tags)
         {
             var val = FFProbeHelpers.GetDictionaryValue(tags, "genre");
 
@@ -764,7 +812,7 @@ namespace MediaBrowser.MediaEncoding.Probing
 
         private const int MaxSubtitleDescriptionExtractionLength = 100; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
 
-        private void FetchWtvInfo(Model.MediaInfo.MediaInfo video, InternalMediaInfoResult data)
+        private void FetchWtvInfo(MediaInfo video, InternalMediaInfoResult data)
         {
             if (data.format == null || data.format.tags == null)
             {
@@ -775,15 +823,16 @@ namespace MediaBrowser.MediaEncoding.Probing
 
             if (!string.IsNullOrWhiteSpace(genres))
             {
-                //genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "genre");
-            }
-
-            if (!string.IsNullOrWhiteSpace(genres))
-            {
-                video.Genres = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries)
+                var genreList = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries)
                     .Where(i => !string.IsNullOrWhiteSpace(i))
                     .Select(i => i.Trim())
                     .ToList();
+
+                // If this is empty then don't overwrite genres that might have been fetched earlier
+                if (genreList.Count > 0)
+                {
+                    video.Genres = genreList;
+                }
             }
 
             var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating");

+ 0 - 7
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -182,8 +182,6 @@ namespace MediaBrowser.Model.Configuration
         public PeopleMetadataOptions PeopleMetadataOptions { get; set; }
         public bool FindInternetTrailers { get; set; }
 
-        public string[] InsecureApps9 { get; set; }
-
         public bool SaveMetadataHidden { get; set; }
 
         public NameValuePair[] ContentTypes { get; set; }
@@ -256,11 +254,6 @@ namespace MediaBrowser.Model.Configuration
 
             PeopleMetadataOptions = new PeopleMetadataOptions();
 
-            InsecureApps9 = new[]
-            {
-                "Windows Phone"
-            };
-
             MetadataOptions = new[]
             {
                 new MetadataOptions(1, 1280) {ItemType = "Book"},

+ 8 - 0
MediaBrowser.Model/Configuration/UserConfiguration.cs

@@ -48,11 +48,19 @@ namespace MediaBrowser.Model.Configuration
         public bool HidePlayedInLatest { get; set; }
         public bool DisplayChannelsInline { get; set; }
 
+        public bool RememberAudioSelections { get; set; }
+        public bool RememberSubtitleSelections { get; set; }
+        public bool EnableEpisodeAutoQueue { get; set; }
+    
         /// <summary>
         /// Initializes a new instance of the <see cref="UserConfiguration" /> class.
         /// </summary>
         public UserConfiguration()
         {
+            EnableEpisodeAutoQueue = true;
+            RememberAudioSelections = true;
+            RememberSubtitleSelections = true;
+            
             HidePlayedInLatest = true;
             PlayDefaultAudioTrack = true;
 

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

@@ -425,6 +425,7 @@ namespace MediaBrowser.Model.Dlna
                 playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
                 playlistItem.AudioCodec = transcodingProfile.AudioCodec.Split(',')[0];
                 playlistItem.VideoCodec = transcodingProfile.VideoCodec;
+                playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps;
                 playlistItem.SubProtocol = transcodingProfile.Protocol;
                 playlistItem.AudioStreamIndex = audioStreamIndex;
 

+ 4 - 1
MediaBrowser.Model/Dlna/StreamInfo.cs

@@ -32,6 +32,7 @@ namespace MediaBrowser.Model.Dlna
         public string VideoProfile { get; set; }
 
         public bool? Cabac { get; set; }
+        public bool CopyTimestamps { get; set; }
         public string AudioCodec { get; set; }
 
         public int? AudioStreamIndex { get; set; }
@@ -231,6 +232,8 @@ namespace MediaBrowser.Model.Dlna
             {
                 list.Add(new NameValuePair("ItemId", item.ItemId));
             }
+
+            list.Add(new NameValuePair("CopyTimestamps", (item.CopyTimestamps).ToString().ToLower()));
             
             return list;
         }
@@ -269,7 +272,7 @@ namespace MediaBrowser.Model.Dlna
             // HLS will preserve timestamps so we can just grab the full subtitle stream
             long startPositionTicks = StringHelper.EqualsIgnoreCase(SubProtocol, "hls")
                 ? 0
-				: (this.PlayMethod == PlayMethod.Transcode ? StartPositionTicks : 0);
+				: (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
 
             // First add the selected track
             if (SubtitleStreamIndex.HasValue)

+ 3 - 0
MediaBrowser.Model/Dlna/TranscodingProfile.cs

@@ -29,6 +29,9 @@ namespace MediaBrowser.Model.Dlna
         [XmlAttribute("transcodeSeekInfo")]
         public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
 
+        [XmlAttribute("copyTimestamps")]
+        public bool CopyTimestamps { get; set; }
+
         [XmlAttribute("context")]
         public EncodingContext Context { get; set; }
 

+ 4 - 0
MediaBrowser.Model/Entities/PersonType.cs

@@ -34,5 +34,9 @@ namespace MediaBrowser.Model.Entities
         /// The conductor
         /// </summary>
         public const string Conductor = "Conductor";
+        /// <summary>
+        /// The lyricist
+        /// </summary>
+        public const string Lyricist = "Lyricist";
     }
 }

+ 6 - 0
MediaBrowser.Model/LiveTv/LiveTvTunerInfoDto.cs

@@ -64,6 +64,12 @@ namespace MediaBrowser.Model.LiveTv
         /// <value>The clients.</value>
         public List<string> Clients { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance can reset.
+        /// </summary>
+        /// <value><c>true</c> if this instance can reset; otherwise, <c>false</c>.</value>
+        public bool CanReset { get; set; }
+        
         public LiveTvTunerInfoDto()
         {
             Clients = new List<string>();

+ 5 - 5
MediaBrowser.Model/MediaInfo/MediaInfo.cs

@@ -9,11 +9,6 @@ namespace MediaBrowser.Model.MediaInfo
     {
         public List<ChapterInfo> Chapters { get; set; }
 
-        /// <summary>
-        /// Gets or sets the title.
-        /// </summary>
-        /// <value>The title.</value>
-        public string Title { get; set; }
         /// <summary>
         /// Gets or sets the album.
         /// </summary>
@@ -47,6 +42,11 @@ namespace MediaBrowser.Model.MediaInfo
         /// <value>The official rating.</value>
         public string OfficialRating { get; set; }
         /// <summary>
+        /// Gets or sets the official rating description.
+        /// </summary>
+        /// <value>The official rating description.</value>
+        public string OfficialRatingDescription { get; set; }
+        /// <summary>
         /// Gets or sets the overview.
         /// </summary>
         /// <value>The overview.</value>

+ 2 - 2
MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs

@@ -125,9 +125,9 @@ namespace MediaBrowser.Providers.MediaInfo
         private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo data)
         {
             // Only set Name if title was found in the dictionary
-            if (!string.IsNullOrEmpty(data.Title))
+            if (!string.IsNullOrEmpty(data.Name))
             {
-                audio.Name = data.Title;
+                audio.Name = data.Name;
             }
 
             if (!audio.LockedFields.Contains(MetadataFields.Cast))

+ 12 - 0
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -383,6 +383,11 @@ namespace MediaBrowser.Providers.MediaInfo
                 }
             }
 
+            if (!string.IsNullOrWhiteSpace(data.OfficialRatingDescription) || isFullRefresh)
+            {
+                video.OfficialRatingDescription = data.OfficialRatingDescription;
+            }
+
             if (!video.LockedFields.Contains(MetadataFields.Genres))
             {
                 if (video.Genres.Count == 0 || isFullRefresh)
@@ -437,6 +442,13 @@ namespace MediaBrowser.Providers.MediaInfo
                     video.ParentIndexNumber = data.ParentIndexNumber;
                 }
             }
+            if (!string.IsNullOrWhiteSpace(data.Name))
+            {
+                if (string.IsNullOrWhiteSpace(video.Name) || string.Equals(video.Name, Path.GetFileNameWithoutExtension(video.Path), StringComparison.OrdinalIgnoreCase))
+                {
+                    video.Name = data.Name;
+                }
+            }
 
             // If we don't have a ProductionYear try and get it from PremiereDate
             if (video.PremiereDate.HasValue && !video.ProductionYear.HasValue)

+ 0 - 145
MediaBrowser.Server.Implementations/Connect/ConnectManager.cs

@@ -10,7 +10,6 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Model.Connect;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Serialization;
@@ -24,7 +23,6 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using CommonIO;
-using MediaBrowser.Common.IO;
 
 namespace MediaBrowser.Server.Implementations.Connect
 {
@@ -121,7 +119,6 @@ namespace MediaBrowser.Server.Implementations.Connect
             _securityManager = securityManager;
             _fileSystem = fileSystem;
 
-            _userManager.UserConfigurationUpdated += _userManager_UserConfigurationUpdated;
             _config.ConfigurationUpdated += _config_ConfigurationUpdated;
 
             LoadCachedData();
@@ -1071,90 +1068,6 @@ namespace MediaBrowser.Server.Implementations.Connect
             }
         }
 
-        public async Task<ConnectSupporterSummary> GetConnectSupporterSummary()
-        {
-            var url = GetConnectUrl("keyAssociation");
-
-            var options = new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = CancellationToken.None
-            };
-
-            var postData = new Dictionary<string, string>
-                {
-                    {"serverId", ConnectServerId},
-                    {"supporterKey", _securityManager.SupporterKey}
-                };
-
-            options.SetPostData(postData);
-
-            SetServerAccessToken(options);
-            SetApplicationHeader(options);
-
-            // No need to examine the response
-            using (var stream = (await _httpClient.SendAsync(options, "POST").ConfigureAwait(false)).Content)
-            {
-                return _json.DeserializeFromStream<ConnectSupporterSummary>(stream);
-            }
-        }
-
-        public async Task AddConnectSupporter(string id)
-        {
-            var url = GetConnectUrl("keyAssociation");
-
-            var options = new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = CancellationToken.None
-            };
-
-            var postData = new Dictionary<string, string>
-                {
-                    {"serverId", ConnectServerId},
-                    {"supporterKey", _securityManager.SupporterKey},
-                    {"userId", id}
-                };
-
-            options.SetPostData(postData);
-
-            SetServerAccessToken(options);
-            SetApplicationHeader(options);
-
-            // No need to examine the response
-            using (var stream = (await _httpClient.SendAsync(options, "POST").ConfigureAwait(false)).Content)
-            {
-            }
-        }
-
-        public async Task RemoveConnectSupporter(string id)
-        {
-            var url = GetConnectUrl("keyAssociation");
-
-            var options = new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = CancellationToken.None
-            };
-
-            var postData = new Dictionary<string, string>
-                {
-                    {"serverId", ConnectServerId},
-                    {"supporterKey", _securityManager.SupporterKey},
-                    {"userId", id}
-                };
-
-            options.SetPostData(postData);
-
-            SetServerAccessToken(options);
-            SetApplicationHeader(options);
-
-            // No need to examine the response
-            using (var stream = (await _httpClient.SendAsync(options, "DELETE").ConfigureAwait(false)).Content)
-            {
-            }
-        }
-
         public async Task Authenticate(string username, string passwordMd5)
         {
             if (string.IsNullOrWhiteSpace(username))
@@ -1186,64 +1099,6 @@ namespace MediaBrowser.Server.Implementations.Connect
             }
         }
 
-        async void _userManager_UserConfigurationUpdated(object sender, GenericEventArgs<User> e)
-        {
-            var user = e.Argument;
-
-            await TryUploadUserPreferences(user, CancellationToken.None).ConfigureAwait(false);
-        }
-
-        private async Task TryUploadUserPreferences(User user, CancellationToken cancellationToken)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException("user");
-            }
-
-            if (string.IsNullOrEmpty(user.ConnectUserId))
-            {
-                return;
-            }
-            if (string.IsNullOrEmpty(ConnectAccessKey))
-            {
-                return;
-            }
-
-            var url = GetConnectUrl("user/preferences");
-            url += "?userId=" + user.ConnectUserId;
-            url += "&key=userpreferences";
-
-            var options = new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken
-            };
-
-            var postData = new Dictionary<string, string>();
-            postData["data"] = _json.SerializeToString(ConnectUserPreferences.FromUserConfiguration(user.Configuration));
-            options.SetPostData(postData);
-
-            SetServerAccessToken(options);
-            SetApplicationHeader(options);
-
-            try
-            {
-                // No need to examine the response
-                using (var stream = (await _httpClient.SendAsync(options, "POST").ConfigureAwait(false)).Content)
-                {
-                }
-            }
-            catch (Exception ex)
-            {
-                _logger.ErrorException("Error uploading user preferences", ex);
-            }
-        }
-
-        private async Task DownloadUserPreferences(User user, CancellationToken cancellationToken)
-        {
-
-        }
-
         public async Task<User> GetLocalUser(string connectUserId)
         {
             var user = _userManager.Users

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

@@ -639,6 +639,8 @@ namespace MediaBrowser.Server.Implementations.Dto
         private IEnumerable<string> GetCacheTags(BaseItem item, ImageType type, int limit)
         {
             return item.GetImages(type)
+                // Convert to a list now in case GetImageCacheTag is slow
+                .ToList()
                 .Select(p => GetImageCacheTag(item, p))
                 .Where(i => i != null)
                 .Take(limit)

+ 1 - 1
MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs

@@ -502,7 +502,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
                 }
             }
 
-            return series ?? new Series();
+            return series;
         }
 
         /// <summary>

+ 6 - 0
MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -348,6 +348,12 @@ namespace MediaBrowser.Server.Implementations.HttpServer
                 return Task.FromResult(true);
             }
 
+            if (string.Equals(localPath, "/emby/pin", StringComparison.OrdinalIgnoreCase))
+            {
+                httpRes.RedirectToUrl("web/pin.html");
+                return Task.FromResult(true);
+            }
+            
             if (!string.IsNullOrWhiteSpace(GlobalResponse))
             {
                 httpRes.StatusCode = 503;

+ 3 - 6
MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -134,20 +134,17 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security
 
         private bool IsExemptFromAuthenticationToken(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues)
         {
-            if (!_config.Configuration.IsStartupWizardCompleted &&
-                authAttribtues.AllowBeforeStartupWizard)
+            if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
             {
                 return true;
             }
 
-            return _config.Configuration.InsecureApps9.Contains(auth.Client ?? string.Empty,
-                StringComparer.OrdinalIgnoreCase);
+            return false;
         }
 
         private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, AuthenticationInfo tokenInfo)
         {
-            if (!_config.Configuration.IsStartupWizardCompleted &&
-                authAttribtues.AllowBeforeStartupWizard)
+            if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
             {
                 return true;
             }

+ 0 - 5
MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

@@ -45,7 +45,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security
         {
             var auth = GetAuthorizationDictionary(httpReq);
 
-            string userId = null;
             string deviceId = null;
             string device = null;
             string client = null;
@@ -53,9 +52,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security
 
             if (auth != null)
             {
-                // TODO: Remove this 
-                auth.TryGetValue("UserId", out userId);
-
                 auth.TryGetValue("DeviceId", out deviceId);
                 auth.TryGetValue("Device", out device);
                 auth.TryGetValue("Client", out client);
@@ -78,7 +74,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security
                 Client = client,
                 Device = device,
                 DeviceId = deviceId,
-                UserId = userId,
                 Version = version,
                 Token = token
             };

+ 49 - 11
MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs

@@ -30,8 +30,9 @@ namespace MediaBrowser.Server.Implementations.Library
 
         private IMediaSourceProvider[] _providers;
         private readonly ILogger _logger;
+        private readonly IUserDataManager _userDataManager;
 
-        public MediaSourceManager(IItemRepository itemRepo, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem)
+        public MediaSourceManager(IItemRepository itemRepo, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IUserDataManager userDataManager)
         {
             _itemRepo = itemRepo;
             _userManager = userManager;
@@ -39,6 +40,7 @@ namespace MediaBrowser.Server.Implementations.Library
             _logger = logger;
             _jsonSerializer = jsonSerializer;
             _fileSystem = fileSystem;
+            _userDataManager = userDataManager;
         }
 
         public void AddParts(IEnumerable<IMediaSourceProvider> providers)
@@ -140,7 +142,7 @@ namespace MediaBrowser.Server.Implementations.Library
             {
                 if (user != null)
                 {
-                    SetUserProperties(source, user);
+                    SetUserProperties(hasMediaSources, source, user);
                 }
                 if (source.Protocol == MediaProtocol.File)
                 {
@@ -257,25 +259,38 @@ namespace MediaBrowser.Server.Implementations.Library
             {
                 foreach (var source in sources)
                 {
-                    SetUserProperties(source, user);
+                    SetUserProperties(item, source, user);
                 }
             }
 
             return sources;
         }
 
-        private void SetUserProperties(MediaSourceInfo source, User user)
+        private void SetUserProperties(IHasUserData item, MediaSourceInfo source, User user)
         {
-            var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference)
-            ? new string[] { }
-            : new[] { user.Configuration.AudioLanguagePreference };
+            var userData = item == null ? new UserItemData() : _userDataManager.GetUserData(user.Id, item.GetUserDataKey());
+
+            SetDefaultAudioStreamIndex(source, userData, user);
+            SetDefaultSubtitleStreamIndex(source, userData, user);
+        }
 
+        private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user)
+        {
+            if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections)
+            {
+                var index = userData.SubtitleStreamIndex.Value;
+                // Make sure the saved index is still valid
+                if (index == -1 || source.MediaStreams.Any(i => i.Type == MediaStreamType.Subtitle && i.Index == index))
+                {
+                    source.DefaultSubtitleStreamIndex = index;
+                    return;
+                }
+            }
+            
             var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference)
                 ? new List<string> { }
                 : new List<string> { user.Configuration.SubtitleLanguagePreference };
 
-            source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack);
-
             var defaultAudioIndex = source.DefaultAudioStreamIndex;
             var audioLangage = defaultAudioIndex == null
                 ? null
@@ -290,6 +305,26 @@ namespace MediaBrowser.Server.Implementations.Library
                 user.Configuration.SubtitleMode, audioLangage);
         }
 
+        private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user)
+        {
+            if (userData.AudioStreamIndex.HasValue && user.Configuration.RememberAudioSelections)
+            {
+                var index = userData.AudioStreamIndex.Value;
+                // Make sure the saved index is still valid
+                if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
+                {
+                    source.DefaultAudioStreamIndex = index;
+                    return;
+                }
+            }
+
+            var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference)
+                ? new string[] { }
+                : new[] { user.Configuration.AudioLanguagePreference };
+
+            source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack);
+        }
+
         private IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
         {
             return sources.OrderBy(i =>
@@ -349,11 +384,14 @@ namespace MediaBrowser.Server.Implementations.Library
                 var json = _jsonSerializer.SerializeToString(mediaSource);
                 _logger.Debug("Live stream opened: " + json);
                 var clone = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
-
+               
                 if (!string.IsNullOrWhiteSpace(request.UserId))
                 {
                     var user = _userManager.GetUserById(request.UserId);
-                    SetUserProperties(clone, user);
+                    var item = string.IsNullOrWhiteSpace(request.ItemId)
+                        ? null
+                        : _libraryManager.GetItemById(request.ItemId);
+                    SetUserProperties(item, clone, user);
                 }
 
                 return new LiveStreamResponse

+ 4 - 0
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -771,6 +771,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
                         recordPath = Path.ChangeExtension(recordPath, ".mp4");
                     }
 
+                    _libraryMonitor.ReportFileSystemChangeBeginning(recordPath);
+
                     recording.Path = recordPath;
                     recording.Status = RecordingStatus.InProgress;
                     recording.DateLastUpdated = DateTime.UtcNow;
@@ -801,6 +803,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
                     {
                         result.Item2.Release();
                     }
+
+                    _libraryMonitor.ReportFileSystemChangeComplete(recordPath, false);
                 }
             }
             catch (OperationCanceledException)

+ 2 - 2
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -116,7 +116,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
                 videoArgs = "-codec:v:0 copy";
             }
 
-            var commandLineArgs = "-fflags +genpts -i \"{0}\" -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
+            var commandLineArgs = "-fflags +genpts -async 1 -vsync -1 -i \"{0}\" -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
 
             if (mediaSource.ReadAtNativeFramerate)
             {
@@ -143,7 +143,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             {
                 audioChannels = audioStream.Channels ?? audioChannels;
             }
-            return "-codec:a:0 aac -strict experimental -ab 320000 -ac " + audioChannels.ToString(CultureInfo.InvariantCulture);
+            return "-codec:a:0 aac -strict experimental -ab 320000";
         }
 
         private bool EncodeVideo(MediaSourceInfo mediaSource)

+ 2 - 1
MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs

@@ -178,7 +178,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 SourceType = info.SourceType,
                 Status = info.Status,
                 ChannelName = channelName,
-                Url = info.Url
+                Url = info.Url,
+                CanReset = info.CanReset
             };
 
             if (!string.IsNullOrEmpty(info.ChannelId))

+ 17 - 3
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -801,11 +801,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             {
                 if (!string.IsNullOrWhiteSpace(info.ImagePath))
                 {
-                    item.SetImagePath(ImageType.Primary, info.ImagePath);
+                    item.SetImage(new ItemImageInfo
+                    {
+                        Path = info.ImagePath,
+                        Type = ImageType.Primary,
+                        IsPlaceholder = true
+                    }, 0);
                 }
                 else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
                 {
-                    item.SetImagePath(ImageType.Primary, info.ImageUrl);
+                    item.SetImage(new ItemImageInfo
+                    {
+                        Path = info.ImageUrl,
+                        Type = ImageType.Primary,
+                        IsPlaceholder = true
+                    }, 0);
                 }
             }
 
@@ -2343,7 +2353,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 throw new ResourceNotFoundException();
             }
 
-            await provider.Validate(info).ConfigureAwait(false);
+            var configurable = provider as IConfigurableTunerHost;
+            if (configurable != null)
+            {
+                await configurable.Validate(info).ConfigureAwait(false);
+            }
 
             var config = GetConfiguration();
 

+ 1 - 1
MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs

@@ -64,7 +64,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
             return list;
         }
 
-        private List<TunerHostInfo> GetTunerHosts()
+        protected virtual List<TunerHostInfo> GetTunerHosts()
         {
             return GetConfiguration().TunerHosts
                 .Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))

+ 1 - 1
MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs

@@ -20,7 +20,7 @@ using MediaBrowser.Model.Dlna;
 
 namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 {
-    public class HdHomerunHost : BaseTunerHost, ITunerHost
+    public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
     {
         private readonly IHttpClient _httpClient;
 

+ 4 - 77
MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -8,19 +8,17 @@ using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.MediaInfo;
 using System;
 using System.Collections.Generic;
-using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using CommonIO;
-using MediaBrowser.Common.IO;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Serialization;
 
 namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
 {
-    public class M3UTunerHost : BaseTunerHost, ITunerHost
+    public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
     {
         private readonly IFileSystem _fileSystem;
         private readonly IHttpClient _httpClient;
@@ -46,65 +44,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
 
         protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
         {
-            var urlHash = info.Url.GetMD5().ToString("N");
-
-            // Read the file and display it line by line.
-            using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
-            {
-                return GetChannels(reader, urlHash);
-            }
-        }
-
-        private List<M3UChannel> GetChannels(StreamReader reader, string urlHash)
-        {
-            var channels = new List<M3UChannel>();
-
-            string channnelName = null;
-            string channelNumber = null;
-            string line;
-
-            while ((line = reader.ReadLine()) != null)
-            {
-                line = line.Trim();
-                if (string.IsNullOrWhiteSpace(line))
-                {
-                    continue;
-                }
-
-                if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
-                {
-                    continue;
-                }
-
-                if (line.StartsWith("#EXTINF:", StringComparison.OrdinalIgnoreCase))
-                {
-                    line = line.Substring(8);
-                    Logger.Info("Found m3u channel: {0}", line);
-                    var parts = line.Split(new[] { ',' }, 2);
-                    channelNumber = parts[0];
-                    channnelName = parts[1];
-                }
-                else if (!string.IsNullOrWhiteSpace(channelNumber))
-                {
-                    channels.Add(new M3UChannel
-                    {
-                        Name = channnelName,
-                        Number = channelNumber,
-                        Id = ChannelIdPrefix + urlHash + line.GetMD5().ToString("N"),
-                        Path = line
-                    });
-
-                    channelNumber = null;
-                    channnelName = null;
-                }
-            }
-            return channels;
+            return await new M3uParser(Logger, _fileSystem, _httpClient).Parse(info.Url, ChannelIdPrefix, cancellationToken).ConfigureAwait(false);
         }
 
         public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
         {
-            var list = GetConfiguration().TunerHosts
-            .Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
+            var list = GetTunerHosts()
             .Select(i => new LiveTvTunerInfo()
             {
                 Name = Name,
@@ -125,18 +70,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
             return sources.First();
         }
 
-        class M3UChannel : ChannelInfo
-        {
-            public string Path { get; set; }
-
-            public M3UChannel()
-            {
-            }
-        }
-
         public async Task Validate(TunerHostInfo info)
         {
-            using (var stream = await GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
+            using (var stream = await new M3uParser(Logger, _fileSystem, _httpClient).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
             {
 
             }
@@ -147,15 +83,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
             return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
         }
 
-        private Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
-        {
-            if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
-            {
-                return _httpClient.Get(info.Url, cancellationToken);
-            }
-            return Task.FromResult(_fileSystem.OpenRead(info.Url));
-        }
-
         protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken)
         {
             var urlHash = info.Url.GetMD5().ToString("N");

+ 115 - 0
MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -0,0 +1,115 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using CommonIO;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Logging;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
+{
+    public class M3uParser
+    {
+        private readonly ILogger _logger;
+        private readonly IFileSystem _fileSystem;
+        private readonly IHttpClient _httpClient;
+
+        public M3uParser(ILogger logger, IFileSystem fileSystem, IHttpClient httpClient)
+        {
+            _logger = logger;
+            _fileSystem = fileSystem;
+            _httpClient = httpClient;
+        }
+
+        public async Task<List<M3UChannel>> Parse(string url, string channelIdPrefix, CancellationToken cancellationToken)
+        {
+            var urlHash = url.GetMD5().ToString("N");
+
+            // Read the file and display it line by line.
+            using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false)))
+            {
+                return GetChannels(reader, urlHash, channelIdPrefix);
+            }
+        }
+
+        public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken)
+        {
+            if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+            {
+                return _httpClient.Get(url, cancellationToken);
+            }
+            return Task.FromResult(_fileSystem.OpenRead(url));
+        }
+
+        private List<M3UChannel> GetChannels(StreamReader reader, string urlHash, string channelIdPrefix)
+        {
+            var channels = new List<M3UChannel>();
+
+            string channnelName = null;
+            string channelNumber = null;
+            string line;
+            string imageUrl = null;
+            while ((line = reader.ReadLine()) != null)
+            {
+                line = line.Trim();
+                if (string.IsNullOrWhiteSpace(line))
+                {
+                    continue;
+                }
+
+                if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
+                {
+                    continue;
+                }
+
+                if (line.StartsWith("#EXTINF:", StringComparison.OrdinalIgnoreCase))
+                {
+                    line = line.Substring(8);
+                    _logger.Info("Found m3u channel: {0}", line);
+                    var parts = line.Split(new[] { ',' }, 2);
+                    channelNumber = parts[0].Trim().Split(' ')[0] ?? "0";
+                    channnelName = FindProperty("tvg-name", line, parts[1]);
+                    imageUrl = FindProperty("tvg-logo", line, null);
+                }
+                else if (!string.IsNullOrWhiteSpace(channelNumber))
+                {
+                    channels.Add(new M3UChannel
+                    {
+                        Name = channnelName,
+                        Number = channelNumber,
+                        Id = channelIdPrefix + urlHash + line.GetMD5().ToString("N"),
+                        ImageUrl = imageUrl
+                    });
+
+                    imageUrl = null;
+                    channelNumber = null;
+                    channnelName = null;
+                }
+            }
+            return channels;
+        }
+        public string FindProperty(string property, string properties, string defaultResult = "")
+        {
+            var reg = new Regex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
+            var matches = reg.Matches(properties);
+            foreach (Match match in matches)
+            {
+                if (match.Groups[1].Value == property)
+                {
+                    return match.Groups[2].Value;
+                }
+            }
+            return defaultResult;
+        }
+    }
+
+
+    public class M3UChannel : ChannelInfo
+    {
+        public string Path { get; set; }
+    }
+}

+ 158 - 41
MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs

@@ -1,10 +1,14 @@
 using System;
 using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
 using System.Linq;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using System.Xml;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
@@ -13,6 +17,7 @@ using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
 
 namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
 {
@@ -24,14 +29,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
         private readonly ILiveTvManager _liveTvManager;
         private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
         private readonly IHttpClient _httpClient;
+        private readonly IJsonSerializer _json;
 
-        public SatIpDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient)
+        public static SatIpDiscovery Current;
+
+        private readonly List<TunerHostInfo> _discoveredHosts = new List<TunerHostInfo>();
+
+        public List<TunerHostInfo> DiscoveredHosts
+        {
+            get { return _discoveredHosts.ToList(); }
+        }
+
+        public SatIpDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient, IJsonSerializer json)
         {
             _deviceDiscovery = deviceDiscovery;
             _config = config;
             _logger = logger;
             _liveTvManager = liveTvManager;
             _httpClient = httpClient;
+            _json = json;
+            Current = this;
         }
 
         public void Run()
@@ -42,7 +59,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
         void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e)
         {
             string st = null;
-            if (e.Headers.TryGetValue("ST", out st) && string.Equals(st, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase))
+            string nt = null;
+            e.Headers.TryGetValue("ST", out st);
+            e.Headers.TryGetValue("NT", out nt);
+
+            if (string.Equals(st, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(nt, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase))
             {
                 string location;
                 if (e.Headers.TryGetValue("Location", out location) && !string.IsNullOrWhiteSpace(location))
@@ -61,26 +83,23 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
 
             try
             {
-                var options = GetConfiguration();
-
-                //if (options.TunerHosts.Any(i =>
-                //            string.Equals(i.Type, SatIpHost.DeviceType, StringComparison.OrdinalIgnoreCase) &&
-                //            UriEquals(i.Url, url)))
-                //{
-                //    return;
-                //}
+                if (_discoveredHosts.Any(i => string.Equals(i.Type, SatIpHost.DeviceType, StringComparison.OrdinalIgnoreCase) && string.Equals(location, i.Url, StringComparison.OrdinalIgnoreCase)))
+                {
+                    return;
+                }
 
-                //// Strip off the port
-                //url = new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped).TrimEnd('/');
+                _logger.Debug("Will attempt to add SAT device {0}", location);
+                var info = await GetInfo(location, CancellationToken.None).ConfigureAwait(false);
 
-                //await TestUrl(url).ConfigureAwait(false);
+                _discoveredHosts.Add(info);
+            }
+            catch (OperationCanceledException)
+            {
 
-                //await _liveTvManager.SaveTunerHost(new TunerHostInfo
-                //{
-                //    Type = SatIpHost.DeviceType,
-                //    Url = url
+            }
+            catch (NotImplementedException)
+            {
 
-                //}).ConfigureAwait(false);
             }
             catch (Exception ex)
             {
@@ -92,43 +111,141 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
             }
         }
 
-        private async Task TestUrl(string url)
+        public void Dispose()
         {
-            // Test it by pulling down the lineup
-            using (await _httpClient.Get(new HttpRequestOptions
+        }
+
+        public async Task<SatIpTunerHostInfo> GetInfo(string url, CancellationToken cancellationToken)
+        {
+            var result = new SatIpTunerHostInfo
             {
-                Url = string.Format("{0}/lineup.json", url),
-                CancellationToken = CancellationToken.None
-            }))
+                Url = url,
+                IsEnabled = true,
+                Type = SatIpHost.DeviceType,
+                Tuners = 1,
+                TunersAvailable = 1
+            };
+
+            using (var stream = await _httpClient.Get(url, cancellationToken).ConfigureAwait(false))
+            {
+                using (var streamReader = new StreamReader(stream))
+                {
+                    // Use XmlReader for best performance
+                    using (var reader = XmlReader.Create(streamReader))
+                    {
+                        reader.MoveToContent();
+
+                        // Loop through each element
+                        while (reader.Read())
+                        {
+                            if (reader.NodeType == XmlNodeType.Element)
+                            {
+                                switch (reader.Name)
+                                {
+                                    case "device":
+                                        using (var subtree = reader.ReadSubtree())
+                                        {
+                                            FillFromDeviceNode(result, subtree);
+                                        }
+                                        break;
+                                    default:
+                                        reader.Skip();
+                                        break;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (string.IsNullOrWhiteSpace(result.Id))
             {
+                throw new NotImplementedException();
             }
-        }
 
-        private bool UriEquals(string savedUri, string location)
-        {
-            return string.Equals(NormalizeUrl(location), NormalizeUrl(savedUri), StringComparison.OrdinalIgnoreCase);
-        }
+            // Device hasn't implemented an m3u list
+            if (string.IsNullOrWhiteSpace(result.M3UUrl))
+            {
+                result.IsEnabled = false;
+            }
 
-        private string NormalizeUrl(string url)
-        {
-            if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+            else if (!result.M3UUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
             {
-                url = "http://" + url;
+                var fullM3uUrl = url.Substring(0, url.LastIndexOf('/'));
+                result.M3UUrl = fullM3uUrl + "/" + result.M3UUrl.TrimStart('/');
             }
 
-            url = url.TrimEnd('/');
+            _logger.Debug("SAT device result: {0}", _json.SerializeToString(result));
 
-            // Strip off the port
-            return new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped);
+            return result;
         }
 
-        private LiveTvOptions GetConfiguration()
+        private void FillFromDeviceNode(SatIpTunerHostInfo info, XmlReader reader)
         {
-            return _config.GetConfiguration<LiveTvOptions>("livetv");
-        }
+            reader.MoveToContent();
 
-        public void Dispose()
-        {
+            while (reader.Read())
+            {
+                if (reader.NodeType == XmlNodeType.Element)
+                {
+                    switch (reader.LocalName)
+                    {
+                        case "UDN":
+                            {
+                                info.Id = reader.ReadElementContentAsString();
+                                break;
+                            }
+
+                        case "friendlyName":
+                            {
+                                info.FriendlyName = reader.ReadElementContentAsString();
+                                break;
+                            }
+
+                        case "satip:X_SATIPCAP":
+                        case "X_SATIPCAP":
+                            {
+                                // <satip:X_SATIPCAP xmlns:satip="urn:ses-com:satip">DVBS2-2</satip:X_SATIPCAP>
+                                var value = reader.ReadElementContentAsString() ?? string.Empty;
+                                var parts = value.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries);
+                                if (parts.Length == 2)
+                                {
+                                    int intValue;
+                                    if (int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out intValue))
+                                    {
+                                        info.TunersAvailable = intValue;
+                                    }
+
+                                    if (int.TryParse(parts[0].Substring(parts[0].Length - 1), NumberStyles.Any, CultureInfo.InvariantCulture, out intValue))
+                                    {
+                                        info.Tuners = intValue;
+                                    }
+                                }
+                                break;
+                            }
+
+                        case "satip:X_SATIPM3U":
+                        case "X_SATIPM3U":
+                            {
+                                // <satip:X_SATIPM3U xmlns:satip="urn:ses-com:satip">/channellist.lua?select=m3u</satip:X_SATIPM3U>
+                                info.M3UUrl = reader.ReadElementContentAsString();
+                                break;
+                            }
+
+                        default:
+                            reader.Skip();
+                            break;
+                    }
+                }
+            }
         }
     }
+
+    public class SatIpTunerHostInfo : TunerHostInfo
+    {
+        public int Tuners { get; set; }
+        public int TunersAvailable { get; set; }
+        public string M3UUrl { get; set; }
+        public string FriendlyName { get; set; }
+    }
 }

+ 156 - 30
MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs

@@ -1,45 +1,171 @@
-namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using CommonIO;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
 {
-    public class SatIpHost /*: BaseTunerHost*/
+    public class SatIpHost : BaseTunerHost, ITunerHost
     {
-        //public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder)
-        //    : base(config, logger, jsonSerializer, mediaEncoder)
-        //{
-        //}
+        private readonly IFileSystem _fileSystem;
+        private readonly IHttpClient _httpClient;
+        
+        public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
+            : base(config, logger, jsonSerializer, mediaEncoder)
+        {
+            _fileSystem = fileSystem;
+            _httpClient = httpClient;
+        }
+
+        private const string ChannelIdPrefix = "sat_";
+        
+        protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
+        {
+            var satInfo = (SatIpTunerHostInfo) tuner;
 
-        //protected override Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
-        //{
-        //    throw new NotImplementedException();
-        //}
+            return await new M3uParser(Logger, _fileSystem, _httpClient).Parse(satInfo.M3UUrl, ChannelIdPrefix, cancellationToken).ConfigureAwait(false);
+        }
 
         public static string DeviceType
         {
             get { return "satip"; }
         }
 
-        //public override string Type
-        //{
-        //    get { return DeviceType; }
-        //}
+        public override string Type
+        {
+            get { return DeviceType; }
+        }
+
+        protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
+        {
+            var urlHash = tuner.Url.GetMD5().ToString("N");
+            var prefix = ChannelIdPrefix + urlHash;
+            if (!channelId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+            {
+                return null;
+            }
+
+            var channels = await GetChannels(tuner, true, cancellationToken).ConfigureAwait(false);
+            var m3uchannels = channels.Cast<M3UChannel>();
+            var channel = m3uchannels.FirstOrDefault(c => string.Equals(c.Id, channelId, StringComparison.OrdinalIgnoreCase));
+            if (channel != null)
+            {
+                var path = channel.Path;
+                MediaProtocol protocol = MediaProtocol.File;
+                if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+                {
+                    protocol = MediaProtocol.Http;
+                }
+                else if (path.StartsWith("rtmp", StringComparison.OrdinalIgnoreCase))
+                {
+                    protocol = MediaProtocol.Rtmp;
+                }
+                else if (path.StartsWith("rtsp", StringComparison.OrdinalIgnoreCase))
+                {
+                    protocol = MediaProtocol.Rtsp;
+                }
+
+                var mediaSource = new MediaSourceInfo
+                {
+                    Path = channel.Path,
+                    Protocol = protocol,
+                    MediaStreams = new List<MediaStream>
+                    {
+                        new MediaStream
+                        {
+                            Type = MediaStreamType.Video,
+                            // Set the index to -1 because we don't know the exact index of the video stream within the container
+                            Index = -1,
+                            IsInterlaced = true
+                        },
+                        new MediaStream
+                        {
+                            Type = MediaStreamType.Audio,
+                            // Set the index to -1 because we don't know the exact index of the audio stream within the container
+                            Index = -1
+
+                        }
+                    },
+                    RequiresOpening = false,
+                    RequiresClosing = false
+                };
+
+                return new List<MediaSourceInfo> { mediaSource };
+            }
+            return new List<MediaSourceInfo> { };
+        }
+
+        protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken)
+        {
+            var sources = await GetChannelStreamMediaSources(tuner, channelId, cancellationToken).ConfigureAwait(false);
+
+            return sources.First();
+        }
+
+        protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
+        {
+            var updatedInfo = await SatIpDiscovery.Current.GetInfo(tuner.Url, cancellationToken).ConfigureAwait(false);
 
-        //protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
-        //{
-        //    throw new NotImplementedException();
-        //}
+            return updatedInfo.TunersAvailable > 0;
+        }
+
+        protected override bool IsValidChannelId(string channelId)
+        {
+            return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
+        }
 
-        //protected override Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken)
-        //{
-        //    throw new NotImplementedException();
-        //}
+        protected override List<TunerHostInfo> GetTunerHosts()
+        {
+            return SatIpDiscovery.Current.DiscoveredHosts;
+        }
 
-        //protected override Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
-        //{
-        //    throw new NotImplementedException();
-        //}
+        public string Name
+        {
+            get { return "Sat IP"; }
+        }
 
-        //protected override bool IsValidChannelId(string channelId)
-        //{
-        //    throw new NotImplementedException();
-        //}
+        public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
+        {
+            var list = GetTunerHosts()
+            .SelectMany(i => GetTunerInfos(i, cancellationToken))
+            .ToList();
+
+            return Task.FromResult(list);
+        }
+
+        public List<LiveTvTunerInfo> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken)
+        {
+            var satInfo = (SatIpTunerHostInfo) info;
+
+            var list = new List<LiveTvTunerInfo>();
+
+            for (var i = 0; i < satInfo.Tuners; i++)
+            {
+                list.Add(new LiveTvTunerInfo
+                {
+                    Name = satInfo.FriendlyName ?? Name,
+                    SourceType = Type,
+                    Status = LiveTvTunerStatus.Available,
+                    Id = info.Url.GetMD5().ToString("N") + i.ToString(CultureInfo.InvariantCulture),
+                    Url = info.Url
+                });
+            }
+
+            return list;
+        }
     }
 }

+ 3 - 2
MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

@@ -52,9 +52,9 @@
     <Reference Include="Interfaces.IO">
       <HintPath>..\packages\Interfaces.IO.1.0.0.5\lib\portable-net45+sl4+wp71+win8+wpa81\Interfaces.IO.dll</HintPath>
     </Reference>
-    <Reference Include="MediaBrowser.Naming, Version=1.0.5884.23751, Culture=neutral, processorArchitecture=MSIL">
+    <Reference Include="MediaBrowser.Naming, Version=1.0.5891.29179, Culture=neutral, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
-      <HintPath>..\packages\MediaBrowser.Naming.1.0.0.47\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath>
+      <HintPath>..\packages\MediaBrowser.Naming.1.0.0.48\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath>
     </Reference>
     <Reference Include="MoreLinq">
       <HintPath>..\packages\morelinq.1.4.0\lib\net35\MoreLinq.dll</HintPath>
@@ -234,6 +234,7 @@
     <Compile Include="LiveTv\TunerHosts\BaseTunerHost.cs" />
     <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHost.cs" />
     <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunDiscovery.cs" />
+    <Compile Include="LiveTv\TunerHosts\M3uParser.cs" />
     <Compile Include="LiveTv\TunerHosts\M3UTunerHost.cs" />
     <Compile Include="LiveTv\ProgramImageProvider.cs" />
     <Compile Include="LiveTv\RecordingImageProvider.cs" />

+ 1 - 1
MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs

@@ -1014,7 +1014,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
             if (!reader.IsDBNull(31))
             {
-                item.OfficialRating = reader.GetString(31);
+                item.OfficialRatingDescription = reader.GetString(31);
             }
 
             if (!reader.IsDBNull(32))

+ 21 - 4
MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs

@@ -56,6 +56,9 @@ namespace MediaBrowser.Server.Implementations.Persistence
                                };
 
             _connection.RunQueries(queries, Logger);
+
+            _connection.AddColumn(Logger, "userdata", "AudioStreamIndex", "int");
+            _connection.AddColumn(Logger, "userdata", "SubtitleStreamIndex", "int");
         }
 
         /// <summary>
@@ -127,7 +130,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
                 using (var cmd = _connection.CreateCommand())
                 {
-                    cmd.CommandText = "replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate)";
+                    cmd.CommandText = "replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)";
 
                     cmd.Parameters.Add(cmd, "@key", DbType.String).Value = key;
                     cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId;
@@ -137,6 +140,8 @@ namespace MediaBrowser.Server.Implementations.Persistence
                     cmd.Parameters.Add(cmd, "@isFavorite", DbType.Boolean).Value = userData.IsFavorite;
                     cmd.Parameters.Add(cmd, "@playbackPositionTicks", DbType.Int64).Value = userData.PlaybackPositionTicks;
                     cmd.Parameters.Add(cmd, "@lastPlayedDate", DbType.DateTime).Value = userData.LastPlayedDate;
+                    cmd.Parameters.Add(cmd, "@AudioStreamIndex", DbType.Int32).Value = userData.AudioStreamIndex;
+                    cmd.Parameters.Add(cmd, "@SubtitleStreamIndex", DbType.Int32).Value = userData.SubtitleStreamIndex;
 
                     cmd.Transaction = transaction;
 
@@ -199,7 +204,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 {
                     using (var cmd = _connection.CreateCommand())
                     {
-                        cmd.CommandText = "replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate)";
+                        cmd.CommandText = "replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)";
 
                         cmd.Parameters.Add(cmd, "@key", DbType.String).Value = userItemData.Key;
                         cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId;
@@ -209,6 +214,8 @@ namespace MediaBrowser.Server.Implementations.Persistence
                         cmd.Parameters.Add(cmd, "@isFavorite", DbType.Boolean).Value = userItemData.IsFavorite;
                         cmd.Parameters.Add(cmd, "@playbackPositionTicks", DbType.Int64).Value = userItemData.PlaybackPositionTicks;
                         cmd.Parameters.Add(cmd, "@lastPlayedDate", DbType.DateTime).Value = userItemData.LastPlayedDate;
+                        cmd.Parameters.Add(cmd, "@AudioStreamIndex", DbType.Int32).Value = userItemData.AudioStreamIndex;
+                        cmd.Parameters.Add(cmd, "@SubtitleStreamIndex", DbType.Int32).Value = userItemData.SubtitleStreamIndex;
 
                         cmd.Transaction = transaction;
 
@@ -275,7 +282,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
             using (var cmd = _connection.CreateCommand())
             {
-                cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate from userdata where key = @key and userId=@userId";
+                cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where key = @key and userId=@userId";
 
                 cmd.Parameters.Add(cmd, "@key", DbType.String).Value = key;
                 cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId;
@@ -310,7 +317,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
             using (var cmd = _connection.CreateCommand())
             {
-                cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate from userdata where userId=@userId";
+                cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where userId=@userId";
 
                 cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId;
 
@@ -350,6 +357,16 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 userData.LastPlayedDate = reader.GetDateTime(7).ToUniversalTime();
             }
 
+            if (!reader.IsDBNull(8))
+            {
+                userData.AudioStreamIndex = reader.GetInt32(8);
+            }
+
+            if (!reader.IsDBNull(9))
+            {
+                userData.SubtitleStreamIndex = reader.GetInt32(9);
+            }
+
             return userData;
         }
 

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

@@ -680,7 +680,7 @@ namespace MediaBrowser.Server.Implementations.Session
 
                 foreach (var user in users)
                 {
-                    await OnPlaybackProgress(user.Id, key, libraryItem, info.PositionTicks).ConfigureAwait(false);
+                    await OnPlaybackProgress(user, key, libraryItem, info).ConfigureAwait(false);
                 }
             }
 
@@ -712,15 +712,40 @@ namespace MediaBrowser.Server.Implementations.Session
             StartIdleCheckTimer();
         }
 
-        private async Task OnPlaybackProgress(Guid userId, string userDataKey, BaseItem item, long? positionTicks)
+        private async Task OnPlaybackProgress(User user, string userDataKey, BaseItem item, PlaybackProgressInfo info)
         {
-            var data = _userDataRepository.GetUserData(userId, userDataKey);
+            var data = _userDataRepository.GetUserData(user.Id, userDataKey);
+
+            var positionTicks = info.PositionTicks;
 
             if (positionTicks.HasValue)
             {
                 _userDataRepository.UpdatePlayState(item, data, positionTicks.Value);
 
-                await _userDataRepository.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None).ConfigureAwait(false);
+                UpdatePlaybackSettings(user, info, data);
+
+                await _userDataRepository.SaveUserData(user.Id, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None).ConfigureAwait(false);
+            }
+        }
+
+        private void UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
+        {
+            if (user.Configuration.RememberAudioSelections)
+            {
+                data.AudioStreamIndex = info.AudioStreamIndex;
+            }
+            else
+            {
+                data.AudioStreamIndex = null;
+            }
+
+            if (user.Configuration.RememberSubtitleSelections)
+            {
+                data.SubtitleStreamIndex = info.SubtitleStreamIndex;
+            }
+            else
+            {
+                data.SubtitleStreamIndex = null;
             }
         }
 
@@ -1268,7 +1293,17 @@ namespace MediaBrowser.Server.Implementations.Session
         /// </summary>
         /// <param name="request">The request.</param>
         /// <returns>Task{SessionInfo}.</returns>
-        public async Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
+        public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
+        {
+            return AuthenticateNewSessionInternal(request, true);
+        }
+
+        public Task<AuthenticationResult> CreateNewSession(AuthenticationRequest request)
+        {
+            return AuthenticateNewSessionInternal(request, false);
+        }
+
+        private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
         {
             var user = _userManager.Users
                 .FirstOrDefault(i => string.Equals(request.Username, i.Name, StringComparison.OrdinalIgnoreCase));
@@ -1281,13 +1316,16 @@ namespace MediaBrowser.Server.Implementations.Session
                 }
             }
 
-            var result = await _userManager.AuthenticateUser(request.Username, request.PasswordSha1, request.PasswordMd5, request.RemoteEndPoint).ConfigureAwait(false);
-
-            if (!result)
+            if (enforcePassword)
             {
-                EventHelper.FireEventIfNotNull(AuthenticationFailed, this, new GenericEventArgs<AuthenticationRequest>(request), _logger);
+                var result = await _userManager.AuthenticateUser(request.Username, request.PasswordSha1, request.PasswordMd5, request.RemoteEndPoint).ConfigureAwait(false);
 
-                throw new SecurityException("Invalid user or password entered.");
+                if (!result)
+                {
+                    EventHelper.FireEventIfNotNull(AuthenticationFailed, this, new GenericEventArgs<AuthenticationRequest>(request), _logger);
+
+                    throw new SecurityException("Invalid user or password entered.");
+                }
             }
 
             var token = await GetAuthorizationToken(user.Id.ToString("N"), request.DeviceId, request.App, request.AppVersion, request.DeviceName).ConfigureAwait(false);
@@ -1310,7 +1348,8 @@ namespace MediaBrowser.Server.Implementations.Session
                 ServerId = _appHost.SystemId
             };
         }
-
+        
+        
         private async Task<string> GetAuthorizationToken(string userId, string deviceId, string app, string appVersion, string deviceName)
         {
             var existing = _authRepo.Get(new AuthenticationInfoQuery

+ 19 - 2
MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs

@@ -19,6 +19,7 @@ using MediaBrowser.Model.Sync;
 using MoreLinq;
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Threading;
@@ -125,7 +126,23 @@ namespace MediaBrowser.Server.Implementations.Sync
 
         private string GetSyncJobItemName(BaseItem item)
         {
-            return item.Name;
+            var name = item.Name;
+            var episode = item as Episode;
+
+            if (episode != null)
+            {
+                if (episode.IndexNumber.HasValue)
+                {
+                    name = "E" + episode.IndexNumber.Value.ToString(CultureInfo.InvariantCulture) + " - " + name;
+                }
+
+                if (episode.ParentIndexNumber.HasValue)
+                {
+                    name = "S" + episode.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture) + ", " + name;
+                }
+            }
+
+            return name;
         }
 
         public Task UpdateJobStatus(string id)
@@ -699,7 +716,7 @@ namespace MediaBrowser.Server.Implementations.Sync
 
             var path = Path.Combine(temporaryPath, filename);
 
-			_fileSystem.CreateDirectory(Path.GetDirectoryName(path));
+            _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
 
             using (var stream = await _subtitleEncoder.GetSubtitles(streamInfo.ItemId, streamInfo.MediaSourceId, subtitleStreamIndex, subtitleStreamInfo.Format, 0, null, cancellationToken).ConfigureAwait(false))
             {

+ 1 - 1
MediaBrowser.Server.Implementations/packages.config

@@ -3,7 +3,7 @@
   <package id="CommonIO" version="1.0.0.7" targetFramework="net45" />
   <package id="Emby.XmlTv" version="1.0.0.48" targetFramework="net45" />
   <package id="Interfaces.IO" version="1.0.0.5" targetFramework="net45" />
-  <package id="MediaBrowser.Naming" version="1.0.0.47" targetFramework="net45" />
+  <package id="MediaBrowser.Naming" version="1.0.0.48" targetFramework="net45" />
   <package id="Mono.Nat" version="1.2.24.0" targetFramework="net45" />
   <package id="morelinq" version="1.4.0" targetFramework="net45" />
   <package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" />

+ 1 - 1
MediaBrowser.Server.Startup.Common/ApplicationHost.cs

@@ -490,7 +490,7 @@ namespace MediaBrowser.Server.Startup.Common
             ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LogManager.GetLogger("ChannelManager"), ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, LocalizationManager, HttpClient, ProviderManager);
             RegisterSingleInstance(ChannelManager);
 
-            MediaSourceManager = new MediaSourceManager(ItemRepository, UserManager, LibraryManager, LogManager.GetLogger("MediaSourceManager"), JsonSerializer, FileSystemManager);
+            MediaSourceManager = new MediaSourceManager(ItemRepository, UserManager, LibraryManager, LogManager.GetLogger("MediaSourceManager"), JsonSerializer, FileSystemManager, UserDataManager);
             RegisterSingleInstance(MediaSourceManager);
 
             SessionManager = new SessionManager(UserDataManager, LogManager.GetLogger("SessionManager"), UserRepository, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, JsonSerializer, this, HttpClient, AuthenticationRepository, DeviceManager, MediaSourceManager);

+ 8 - 5
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -140,6 +140,9 @@
     <Content Include="dashboard-ui\components\remotecontrolautoplay.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\devices\windowsphone\wp.css">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\legacy\buttonenabled.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -254,9 +257,6 @@
     <Content Include="dashboard-ui\favorites.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
-    <None Include="dashboard-ui\legacy\deferred.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
     <Content Include="dashboard-ui\livetvguideprovider.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -275,10 +275,10 @@
     <Content Include="dashboard-ui\mysyncsettings.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
-    <Content Include="dashboard-ui\robots.txt">
+    <Content Include="dashboard-ui\pin.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
-    <Content Include="dashboard-ui\scripts\actionsheet.js">
+    <Content Include="dashboard-ui\robots.txt">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     <Content Include="dashboard-ui\scripts\globalize.js">
@@ -317,6 +317,9 @@
     <Content Include="dashboard-ui\scripts\autoorganizesmart.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\scripts\pin.js">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\scripts\searchmenu.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>