浏览代码

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

tikuf 11 年之前
父节点
当前提交
8ae71b75fb
共有 88 个文件被更改,包括 1307 次插入440 次删除
  1. 24 23
      CONTRIBUTORS.md
  2. 88 36
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  3. 12 1
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  4. 4 1
      MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
  5. 39 4
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  6. 69 3
      MediaBrowser.Api/Playback/StreamState.cs
  7. 3 5
      MediaBrowser.Api/SessionsService.cs
  8. 3 5
      MediaBrowser.Api/UserLibrary/ArtistsService.cs
  9. 24 4
      MediaBrowser.Api/UserLibrary/UserLibraryService.cs
  10. 1 1
      MediaBrowser.Api/UserService.cs
  11. 2 6
      MediaBrowser.Api/WebSocket/SessionInfoWebSocketListener.cs
  12. 0 9
      MediaBrowser.Controller/Dto/IDtoService.cs
  13. 4 1
      MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
  14. 19 0
      MediaBrowser.Controller/Entities/MusicVideo.cs
  15. 1 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  16. 8 0
      MediaBrowser.Controller/Session/ISessionController.cs
  17. 17 0
      MediaBrowser.Controller/Session/ISessionManager.cs
  18. 12 0
      MediaBrowser.Controller/Session/PlaybackInfo.cs
  19. 18 0
      MediaBrowser.Controller/Session/PlaybackProgressInfo.cs
  20. 9 0
      MediaBrowser.Controller/Session/SessionEventArgs.cs
  21. 10 1
      MediaBrowser.Controller/Session/SessionInfo.cs
  22. 20 1
      MediaBrowser.Dlna/DlnaManager.cs
  23. 1 0
      MediaBrowser.Dlna/MediaBrowser.Dlna.csproj
  24. 51 14
      MediaBrowser.Dlna/PlayTo/Device.cs
  25. 44 37
      MediaBrowser.Dlna/PlayTo/DlnaController.cs
  26. 7 0
      MediaBrowser.Dlna/PlayTo/Extensions.cs
  27. 14 8
      MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs
  28. 1 45
      MediaBrowser.Dlna/PlayTo/uBaseObject.cs
  29. 2 3
      MediaBrowser.Dlna/Profiles/DefaultProfile.cs
  30. 2 2
      MediaBrowser.Dlna/Profiles/Windows81Profile.cs
  31. 200 0
      MediaBrowser.Dlna/Profiles/WindowsPhoneProfile.cs
  32. 1 1
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  33. 77 20
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  34. 24 3
      MediaBrowser.Model/Dlna/StreamInfo.cs
  35. 2 0
      MediaBrowser.Model/Dto/MediaVersionInfo.cs
  36. 5 1
      MediaBrowser.Model/Entities/MediaStream.cs
  37. 4 1
      MediaBrowser.Model/Session/GeneralCommand.cs
  38. 10 0
      MediaBrowser.Model/Session/PlaybackReports.cs
  39. 18 12
      MediaBrowser.Model/Session/SessionInfoDto.cs
  40. 1 0
      MediaBrowser.Providers/Manager/MetadataService.cs
  41. 14 121
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  42. 2 2
      MediaBrowser.Server.Implementations/Library/LibraryManager.cs
  43. 5 3
      MediaBrowser.Server.Implementations/Library/Validators/PeopleValidator.cs
  44. 0 0
      MediaBrowser.Server.Implementations/Localization/JavaScript/ar.json
  45. 0 0
      MediaBrowser.Server.Implementations/Localization/JavaScript/el.json
  46. 1 1
      MediaBrowser.Server.Implementations/Localization/JavaScript/en_US.json
  47. 1 1
      MediaBrowser.Server.Implementations/Localization/JavaScript/es.json
  48. 1 1
      MediaBrowser.Server.Implementations/Localization/JavaScript/es_MX.json
  49. 1 1
      MediaBrowser.Server.Implementations/Localization/JavaScript/fr.json
  50. 0 0
      MediaBrowser.Server.Implementations/Localization/JavaScript/he.json
  51. 1 1
      MediaBrowser.Server.Implementations/Localization/JavaScript/it.json
  52. 3 3
      MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json
  53. 1 0
      MediaBrowser.Server.Implementations/Localization/JavaScript/nb.json
  54. 1 1
      MediaBrowser.Server.Implementations/Localization/JavaScript/nl.json
  55. 1 1
      MediaBrowser.Server.Implementations/Localization/JavaScript/pt_BR.json
  56. 1 1
      MediaBrowser.Server.Implementations/Localization/JavaScript/pt_PT.json
  57. 0 0
      MediaBrowser.Server.Implementations/Localization/JavaScript/ru.json
  58. 1 1
      MediaBrowser.Server.Implementations/Localization/JavaScript/zh_TW.json
  59. 3 0
      MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs
  60. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/ar.json
  61. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/de.json
  62. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/el.json
  63. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/en_GB.json
  64. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/en_US.json
  65. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/es.json
  66. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/es_MX.json
  67. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/fr.json
  68. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/he.json
  69. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/it.json
  70. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/nb.json
  71. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/nl.json
  72. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/pt_BR.json
  73. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/pt_PT.json
  74. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/ru.json
  75. 97 2
      MediaBrowser.Server.Implementations/Localization/Server/server.json
  76. 0 0
      MediaBrowser.Server.Implementations/Localization/Server/zh_TW.json
  77. 8 0
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  78. 7 4
      MediaBrowser.Server.Implementations/Roku/RokuSessionController.cs
  79. 2 3
      MediaBrowser.Server.Implementations/ServerManager/WebSocketConnection.cs
  80. 242 25
      MediaBrowser.Server.Implementations/Session/SessionManager.cs
  81. 29 1
      MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs
  82. 12 0
      MediaBrowser.Server.Implementations/Session/WebSocketController.cs
  83. 3 3
      MediaBrowser.ServerApplication/ApplicationHost.cs
  84. 10 11
      MediaBrowser.WebDashboard/Api/DashboardService.cs
  85. 4 0
      MediaBrowser.WebDashboard/ApiClient.js
  86. 2 2
      Nuget/MediaBrowser.Common.Internal.nuspec
  87. 1 1
      Nuget/MediaBrowser.Common.nuspec
  88. 2 2
      Nuget/MediaBrowser.Server.Core.nuspec

+ 24 - 23
CONTRIBUTORS.md

@@ -13,48 +13,49 @@
  - [SivaramAdhiappan](https://github.com/shivaram1190)
  - [CWatkinsNash](https://github.com/CWatkinsNash)
  - [sfnetwork](https://github.com/sfnetwork)
- - [Logos302] (https://github.com/Logos302)
+ - [Logos302](https://github.com/Logos302)
  - [TheWorkz](https://github.com/TheWorkz)
  - [mboehler](https://github.com/mboehler)
  - [KaHooli](https://github.com/KaHooli)
  - [xzener](https://github.com/xzener)
- - [CBers] (https://github.com/CBers)
- - [Sagaia] (https://github.com/Sagaia)
+ - [CBers](https://github.com/CBers)
+ - [Sagaia](https://github.com/Sagaia)
  - [JHawk111](https://github.com/JHawk111)
  - [David3663](https://github.com/david3663)
  - [Smyken](https://github.com/Smyken)
- - [doron1] (https://github.com/doron1)
- - [brainfryd] (https://github.com/brainfryd)
- - [DGMayor] (http://github.com/DGMayor)
- - [Jon-theHTPC] (https://github.com/Jon-theHTPC)
- - [aspdend] (https://github.com/aspdend)
+ - [doron1](https://github.com/doron1)
+ - [brainfryd](https://github.com/brainfryd)
+ - [DGMayor](http://github.com/DGMayor)
+ - [Jon-theHTPC](https://github.com/Jon-theHTPC)
+ - [aspdend](https://github.com/aspdend)
  - [RedshirtMB](https://github.com/RedshirtMB)
  - [thealienamongus](https://github.com/thealienamongus)
  - [brocass](https://github.com/brocass)
  - [pjrollo2000](https://github.com/pjrollo2000)
  - [abobader](https://github.com/abobader)
  - [milli260876](https://github.com/milli260876)
- - [vileboy] (https://github.com/vileboy)
- - [starkadius] (https://github.com/starkadius)
+ - [vileboy](https://github.com/vileboy)
+ - [starkadius](https://github.com/starkadius)
  - [wraslor](https://github.com/wraslor)
  - [mrwebsmith](https://github.com/mrwebsmith)
  - [rickster53](https://github.com/rickster53)
- - [Tharnax] (https://github.com/Tharnax)
- - [0sm0] (https://github.com/0sm0)
+ - [Tharnax](https://github.com/Tharnax)
+ - [0sm0](https://github.com/0sm0)
  - [swhitmore](https://github.com/swhitmore)
  - [DigiTM](https://github.com/DigiTM)
  - [crisliv / xliv](https://github.com/crisliv)
  - [Yogi](https://github.com/yogi12)
- - [madFloyd] (https://github.com/madFloyd)
- - [yardameus] (https://github.com/yardameus)
- - [rrb008] (https://github.com/rrb008)
- - [Toonguy] (https://github.com/Toonguy)
- - [Alwin Hummels] (https://github.com/AlwinHummels)
- - [trooper11] (https://github.com/trooper11)
- - [danlotfy] (https://github.com/danlotfy)
- - [jordy1955] (https://github.com/jordy1955)
- - [JoshFink] (https://github.com/JoshFink)
+ - [madFloyd](https://github.com/madFloyd)
+ - [yardameus](https://github.com/yardameus)
+ - [rrb008](https://github.com/rrb008)
+ - [Toonguy](https://github.com/Toonguy)
+ - [Alwin Hummels](https://github.com/AlwinHummels)
+ - [trooper11](https://github.com/trooper11)
+ - [danlotfy](https://github.com/danlotfy)
+ - [jordy1955](https://github.com/jordy1955)
+ - [JoshFink](https://github.com/JoshFink)
  - [Detector1](https://github.com/Detector1)
  - [BlackIce013](https://github.com/blackice013)
- - [mporcas] (https://github.com/mporcas)
- - [tikuf] (https://github.com/tikuf/)
+ - [mporcas](https://github.com/mporcas)
+ - [tikuf](https://github.com/tikuf/)
+ - [Tim Hobbs](https://github.com/timhobbs)

+ 88 - 36
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -1,5 +1,4 @@
-using System.Text;
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
@@ -22,6 +21,7 @@ using System.Diagnostics;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 
@@ -937,8 +937,6 @@ namespace MediaBrowser.Api.Playback
 
                 ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType);
 
-                state.LogFileStream.Dispose();
-
                 throw;
             }
 
@@ -1096,23 +1094,12 @@ namespace MediaBrowser.Api.Playback
         /// </summary>
         /// <param name="process">The process.</param>
         /// <param name="state">The state.</param>
-        protected async void OnFfMpegProcessExited(Process process, StreamState state)
+        protected void OnFfMpegProcessExited(Process process, StreamState state)
         {
-            if (state.IsoMount != null)
-            {
-                state.IsoMount.Dispose();
-                state.IsoMount = null;
-            }
-
-            if (state.StandardInputCancellationTokenSource != null)
-            {
-                state.StandardInputCancellationTokenSource.Cancel();
-            }
+            state.Dispose();
 
             var outputFilePath = GetOutputFilePath(state);
 
-            state.LogFileStream.Dispose();
-
             try
             {
                 Logger.Info("FFMpeg exited with code {0} for {1}", process.ExitCode, outputFilePath);
@@ -1121,18 +1108,6 @@ namespace MediaBrowser.Api.Playback
             {
                 Logger.Info("FFMpeg exited with an error for {0}", outputFilePath);
             }
-
-            if (!string.IsNullOrEmpty(state.LiveTvStreamId))
-            {
-                try
-                {
-                    await LiveTvManager.CloseLiveStream(state.LiveTvStreamId, CancellationToken.None).ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    Logger.ErrorException("Error closing live tv stream", ex);
-                }
-            }
         }
 
         protected double? GetFramerateParam(StreamState state)
@@ -1357,7 +1332,7 @@ namespace MediaBrowser.Api.Playback
                 request.AudioCodec = InferAudioCodec(url);
             }
 
-            var state = new StreamState
+            var state = new StreamState(LiveTvManager, Logger)
             {
                 Request = request,
                 RequestedUrl = url
@@ -1393,7 +1368,7 @@ namespace MediaBrowser.Api.Playback
                     mediaUrl = streamInfo.Url;
                 }
 
-                if (!string.IsNullOrEmpty(path) && File.Exists(path))
+                if (!string.IsNullOrEmpty(path))
                 {
                     state.MediaPath = path;
                     state.IsRemote = false;
@@ -1406,7 +1381,7 @@ namespace MediaBrowser.Api.Playback
 
                 state.RunTimeTicks = recording.RunTimeTicks;
 
-                if (recording.RecordingInfo.Status == RecordingStatus.InProgress && !state.IsRemote)
+                if (recording.RecordingInfo.Status == RecordingStatus.InProgress)
                 {
                     await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
                 }
@@ -1429,7 +1404,7 @@ namespace MediaBrowser.Api.Playback
 
                 state.LiveTvStreamId = streamInfo.Id;
 
-                if (!string.IsNullOrEmpty(streamInfo.Path) && File.Exists(streamInfo.Path))
+                if (!string.IsNullOrEmpty(streamInfo.Path))
                 {
                     state.MediaPath = streamInfo.Path;
                     state.IsRemote = false;
@@ -1515,6 +1490,14 @@ namespace MediaBrowser.Api.Playback
                 }
             }
 
+            if (state.AudioStream != null)
+            {
+                //if (CanStreamCopyAudio(request, state.AudioStream))
+                //{
+                //    request.AudioCodec = "copy";
+                //}
+            }
+
             return state;
         }
 
@@ -1538,7 +1521,7 @@ namespace MediaBrowser.Api.Playback
             }
 
             // If client is requesting a specific video profile, it must match the source
-            if (!string.IsNullOrEmpty(request.Profile) && !string.Equals(request.Profile, videoStream.Profile))
+            if (!string.IsNullOrEmpty(request.Profile) && !string.Equals(request.Profile, videoStream.Profile, StringComparison.OrdinalIgnoreCase))
             {
                 return false;
             }
@@ -1599,12 +1582,50 @@ namespace MediaBrowser.Api.Playback
                         return false;
                     }
                 }
-                return false;
             }
 
             return SupportsAutomaticVideoStreamCopy;
         }
 
+        private bool CanStreamCopyAudio(StreamRequest request, MediaStream audioStream)
+        {
+            // Source and target codecs must match
+            if (string.IsNullOrEmpty(request.AudioCodec) || !string.Equals(request.AudioCodec, audioStream.Codec, StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            // Video bitrate must fall within requested value
+            if (request.AudioBitRate.HasValue)
+            {
+                if (!audioStream.BitRate.HasValue || audioStream.BitRate.Value > request.AudioBitRate.Value)
+                {
+                    return false;
+                }
+            }
+
+            // Channels must fall within requested value
+            var channels = request.AudioChannels ?? request.MaxAudioChannels;
+            if (channels.HasValue)
+            {
+                if (!audioStream.Channels.HasValue || audioStream.Channels.Value > channels.Value)
+                {
+                    return false;
+                }
+            }
+
+            // Sample rate must fall within requested value
+            if (request.AudioSampleRate.HasValue)
+            {
+                if (!audioStream.SampleRate.HasValue || audioStream.SampleRate.Value > request.AudioSampleRate.Value)
+                {
+                    return false;
+                }
+            }
+
+            return SupportsAutomaticVideoStreamCopy;
+        }
+        
         protected virtual bool SupportsAutomaticVideoStreamCopy
         {
             get
@@ -1722,7 +1743,21 @@ namespace MediaBrowser.Api.Playback
             // 0 = native, 1 = transcoded
             var orgCi = isStaticallyStreamed ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1";
 
-            const string dlnaflags = ";DLNA.ORG_FLAGS=01500000000000000000000000000000";
+            var flagValue = DlnaFlags.DLNA_ORG_FLAG_STREAMING_TRANSFER_MODE |
+                            DlnaFlags.DLNA_ORG_FLAG_BACKGROUND_TRANSFERT_MODE |
+                            DlnaFlags.DLNA_ORG_FLAG_DLNA_V15;
+
+            if (isStaticallyStreamed)
+            {
+                //flagValue = flagValue | DlnaFlags.DLNA_ORG_FLAG_BYTE_BASED_SEEK;
+            }
+            else if (state.RunTimeTicks.HasValue)
+            {
+                //flagValue = flagValue | DlnaFlags.DLNA_ORG_FLAG_TIME_BASED_SEEK;
+            }
+
+            var dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}000000000000000000000000",
+                Enum.Format(typeof(DlnaFlags), flagValue, "x"));
 
             if (!string.IsNullOrWhiteSpace(state.OrgPn))
             {
@@ -1772,6 +1807,23 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
+        [Flags]
+        private enum DlnaFlags
+        {
+            DLNA_ORG_FLAG_SENDER_PACED = (1 << 31),
+            DLNA_ORG_FLAG_TIME_BASED_SEEK = (1 << 30),
+            DLNA_ORG_FLAG_BYTE_BASED_SEEK = (1 << 29),
+            DLNA_ORG_FLAG_PLAY_CONTAINER = (1 << 28),
+            DLNA_ORG_FLAG_S0_INCREASE = (1 << 27),
+            DLNA_ORG_FLAG_SN_INCREASE = (1 << 26),
+            DLNA_ORG_FLAG_RTSP_PAUSE = (1 << 25),
+            DLNA_ORG_FLAG_STREAMING_TRANSFER_MODE = (1 << 24),
+            DLNA_ORG_FLAG_INTERACTIVE_TRANSFERT_MODE = (1 << 23),
+            DLNA_ORG_FLAG_BACKGROUND_TRANSFERT_MODE = (1 << 22),
+            DLNA_ORG_FLAG_CONNECTION_STALL = (1 << 21),
+            DLNA_ORG_FLAG_DLNA_V15 = (1 << 20),
+        };
+
         private void AddTimeSeekResponseHeaders(StreamState state, IDictionary<string, string> responseHeaders)
         {
             var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(UsCulture);

+ 12 - 1
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -93,10 +93,12 @@ namespace MediaBrowser.Api.Playback.Hls
 
             if (!state.VideoRequest.VideoBitRate.HasValue && (string.IsNullOrEmpty(state.VideoRequest.VideoCodec) || !string.Equals(state.VideoRequest.VideoCodec, "copy", StringComparison.OrdinalIgnoreCase)))
             {
+                state.Dispose();
                 throw new ArgumentException("A video bitrate is required");
             }
             if (!state.Request.AudioBitRate.HasValue && (string.IsNullOrEmpty(state.Request.AudioCodec) || !string.Equals(state.Request.AudioCodec, "copy", StringComparison.OrdinalIgnoreCase)))
             {
+                state.Dispose();
                 throw new ArgumentException("An audio bitrate is required");
             }
 
@@ -107,7 +109,16 @@ namespace MediaBrowser.Api.Playback.Hls
             if (!File.Exists(playlist))
             {
                 isPlaylistNewlyCreated = true;
-                await StartFfMpeg(state, playlist).ConfigureAwait(false);
+
+                try
+                {
+                    await StartFfMpeg(state, playlist).ConfigureAwait(false);
+                }
+                catch
+                {
+                    state.Dispose();
+                    throw;
+                }
             }
             else
             {

+ 4 - 1
MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs

@@ -103,7 +103,10 @@ namespace MediaBrowser.Api.Playback.Hls
                 .Where(i => i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
                 .ToList())
             {
-                ExtendPlaylistTimer(playlist);
+                if (!string.IsNullOrEmpty(playlist))
+                {
+                    ExtendPlaylistTimer(playlist);
+                }
             }
         }
 

+ 39 - 4
MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs

@@ -121,11 +121,19 @@ namespace MediaBrowser.Api.Playback.Progressive
 
             var responseHeaders = new Dictionary<string, string>();
 
+            // Static remote stream
             if (request.Static && state.IsRemote)
             {
                 AddDlnaHeaders(state, responseHeaders, true);
 
-                return GetStaticRemoteStreamResult(state.MediaPath, responseHeaders, isHeadRequest).Result;
+                try
+                {
+                    return GetStaticRemoteStreamResult(state.MediaPath, responseHeaders, isHeadRequest).Result;
+                }
+                finally
+                {
+                    state.Dispose();
+                }
             }
 
             var outputPath = GetOutputFilePath(state);
@@ -136,21 +144,47 @@ namespace MediaBrowser.Api.Playback.Progressive
 
             AddDlnaHeaders(state, responseHeaders, isStatic);
 
+            // Static stream
             if (request.Static)
             {
                 var contentType = state.GetMimeType(state.MediaPath);
 
-                return ResultFactory.GetStaticFileResult(Request, state.MediaPath, contentType, FileShare.Read, responseHeaders, isHeadRequest);
+                try
+                {
+                    return ResultFactory.GetStaticFileResult(Request, state.MediaPath, contentType, FileShare.Read, responseHeaders, isHeadRequest);
+                }
+                finally
+                {
+                    state.Dispose();
+                }
             }
 
+            // Not static but transcode cache file exists
             if (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive))
             {
                 var contentType = state.GetMimeType(outputPath);
 
-                return ResultFactory.GetStaticFileResult(Request, outputPath, contentType, FileShare.Read, responseHeaders, isHeadRequest);
+                try
+                {
+                    return ResultFactory.GetStaticFileResult(Request, outputPath, contentType, FileShare.Read, responseHeaders, isHeadRequest);
+                }
+                finally
+                {
+                    state.Dispose();
+                }
+            }
+
+            // Need to start ffmpeg
+            try
+            {
+                return GetStreamResult(state, responseHeaders, isHeadRequest).Result;
             }
+            catch
+            {
+                state.Dispose();
 
-            return GetStreamResult(state, responseHeaders, isHeadRequest).Result;
+                throw;
+            }
         }
 
         /// <summary>
@@ -251,6 +285,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             else
             {
                 ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
+                state.Dispose();
             }
 
             var result = new ProgressiveStreamWriter(outputPath, Logger, FileSystem);

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

@@ -1,15 +1,21 @@
 using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Threading;
 
 namespace MediaBrowser.Api.Playback
 {
-    public class StreamState
+    public class StreamState : IDisposable
     {
+        private readonly ILogger _logger;
+        private readonly ILiveTvManager _liveTvManager;
+
         public string RequestedUrl { get; set; }
 
         public StreamRequest Request { get; set; }
@@ -51,8 +57,6 @@ namespace MediaBrowser.Api.Playback
 
         public bool HasMediaStreams { get; set; }
 
-        public CancellationTokenSource StandardInputCancellationTokenSource { get; set; }
-
         public string LiveTvStreamId { get; set; }
 
         public int SegmentLength = 10;
@@ -63,6 +67,12 @@ namespace MediaBrowser.Api.Playback
         public string AudioSync = "1";
         public string VideoSync = "vfr";
 
+        public StreamState(ILiveTvManager liveTvManager, ILogger logger)
+        {
+            _liveTvManager = liveTvManager;
+            _logger = logger;
+        }
+
         public string InputAudioSync { get; set; }
         public string InputVideoSync { get; set; }
  
@@ -93,5 +103,61 @@ namespace MediaBrowser.Api.Playback
 
             return MimeTypes.GetMimeType(outputPath);
         }
+
+        public void Dispose()
+        {
+            DisposeLiveStream();
+            DisposeLogStream();
+            DisposeIsoMount();
+        }
+
+        private void DisposeLogStream()
+        {
+            if (LogFileStream != null)
+            {
+                try
+                {
+                    LogFileStream.Dispose();
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error disposing log stream", ex);
+                }
+
+                LogFileStream = null;
+            }
+        }
+
+        private void DisposeIsoMount()
+        {
+            if (IsoMount != null)
+            {
+                try
+                {
+                    IsoMount.Dispose();
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error disposing iso mount", ex);
+                }
+
+                IsoMount = null;
+            }
+        }
+
+        private async void DisposeLiveStream()
+        {
+            if (!string.IsNullOrEmpty(LiveTvStreamId))
+            {
+                try
+                {
+                    await _liveTvManager.CloseLiveStream(LiveTvStreamId, CancellationToken.None).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error closing live tv stream", ex);
+                }
+            }
+        }
     }
 }

+ 3 - 5
MediaBrowser.Api/SessionsService.cs

@@ -245,18 +245,16 @@ namespace MediaBrowser.Api
         /// </summary>
         private readonly ISessionManager _sessionManager;
 
-        private readonly IDtoService _dtoService;
         private readonly IUserManager _userManager;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="SessionsService" /> class.
         /// </summary>
         /// <param name="sessionManager">The session manager.</param>
-        /// <param name="dtoService">The dto service.</param>
-        public SessionsService(ISessionManager sessionManager, IDtoService dtoService, IUserManager userManager)
+        /// <param name="userManager">The user manager.</param>
+        public SessionsService(ISessionManager sessionManager, IUserManager userManager)
         {
             _sessionManager = sessionManager;
-            _dtoService = dtoService;
             _userManager = userManager;
         }
 
@@ -289,7 +287,7 @@ namespace MediaBrowser.Api
                 }
             }
 
-            return ToOptimizedResult(result.Select(_dtoService.GetSessionInfoDto).ToList());
+            return ToOptimizedResult(result.Select(_sessionManager.GetSessionInfoDto).ToList());
         }
 
         public void Post(SendPlaystateCommand request)

+ 3 - 5
MediaBrowser.Api/UserLibrary/ArtistsService.cs

@@ -15,14 +15,12 @@ namespace MediaBrowser.Api.UserLibrary
     /// <summary>
     /// Class GetArtists
     /// </summary>
-    [Route("/Artists", "GET")]
-    [Api(Description = "Gets all artists from a given item, folder, or the entire library")]
+    [Route("/Artists", "GET", Summary = "Gets all artists from a given item, folder, or the entire library")]
     public class GetArtists : GetItemsByName
     {
     }
 
-    [Route("/Artists/{Name}", "GET")]
-    [Api(Description = "Gets an artist, by name")]
+    [Route("/Artists/{Name}", "GET", Summary = "Gets an artist, by name")]
     public class GetArtist : IReturn<BaseItemDto>
     {
         /// <summary>
@@ -112,7 +110,7 @@ namespace MediaBrowser.Api.UserLibrary
         protected override IEnumerable<MusicArtist> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items)
         {
             return items
-                .OfType<Audio>()
+                .OfType<IHasArtist>()
                 .SelectMany(i => i.AllArtists)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Select(name =>

+ 24 - 4
MediaBrowser.Api/UserLibrary/UserLibraryService.cs

@@ -257,6 +257,12 @@ namespace MediaBrowser.Api.UserLibrary
         /// <value>The id.</value>
         [ApiMember(Name = "QueueableMediaTypes", Description = "A list of media types that can be queued from this item, comma delimited. Audio,Video,Book,Game", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
         public string QueueableMediaTypes { get; set; }
+
+        [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
+        public int? AudioStreamIndex { get; set; }
+
+        [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
+        public int? SubtitleStreamIndex { get; set; }
     }
 
     /// <summary>
@@ -282,7 +288,7 @@ namespace MediaBrowser.Api.UserLibrary
 
         [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
         public string MediaSourceId { get; set; }
-        
+
         /// <summary>
         /// Gets or sets the position ticks.
         /// </summary>
@@ -295,6 +301,15 @@ namespace MediaBrowser.Api.UserLibrary
 
         [ApiMember(Name = "IsMuted", Description = "Indicates if the player is muted.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
         public bool IsMuted { get; set; }
+
+        [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
+        public int? AudioStreamIndex { get; set; }
+
+        [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
+        public int? SubtitleStreamIndex { get; set; }
+
+        [ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
+        public int? VolumeLevel { get; set; }
     }
 
     /// <summary>
@@ -320,7 +335,7 @@ namespace MediaBrowser.Api.UserLibrary
 
         [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
         public string MediaSourceId { get; set; }
-        
+
         /// <summary>
         /// Gets or sets the position ticks.
         /// </summary>
@@ -737,7 +752,9 @@ namespace MediaBrowser.Api.UserLibrary
                 Item = item,
                 SessionId = GetSession(_sessionManager).Id,
                 QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(),
-                MediaSourceId = request.MediaSourceId
+                MediaSourceId = request.MediaSourceId,
+                AudioStreamIndex = request.AudioStreamIndex,
+                SubtitleStreamIndex = request.SubtitleStreamIndex
             };
 
             _sessionManager.OnPlaybackStart(info);
@@ -760,7 +777,10 @@ namespace MediaBrowser.Api.UserLibrary
                 IsMuted = request.IsMuted,
                 IsPaused = request.IsPaused,
                 SessionId = GetSession(_sessionManager).Id,
-                MediaSourceId = request.MediaSourceId
+                MediaSourceId = request.MediaSourceId,
+                AudioStreamIndex = request.AudioStreamIndex,
+                SubtitleStreamIndex = request.SubtitleStreamIndex,
+                VolumeLevel = request.VolumeLevel
             };
 
             var task = _sessionManager.OnPlaybackProgress(info);

+ 1 - 1
MediaBrowser.Api/UserService.cs

@@ -323,7 +323,7 @@ namespace MediaBrowser.Api
             var result = new AuthenticationResult
             {
                 User = _dtoService.GetUserDto(user),
-                SessionInfo = _dtoService.GetSessionInfoDto(session)
+                SessionInfo = _sessionMananger.GetSessionInfoDto(session)
             };
 
             return result;

+ 2 - 6
MediaBrowser.Api/WebSocket/SessionInfoWebSocketListener.cs

@@ -1,5 +1,4 @@
 using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Session;
@@ -14,8 +13,6 @@ namespace MediaBrowser.Api.WebSocket
     /// </summary>
     class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfoDto>, object>
     {
-        private readonly IDtoService _dtoService;
-
         /// <summary>
         /// Gets the name.
         /// </summary>
@@ -35,11 +32,10 @@ namespace MediaBrowser.Api.WebSocket
         /// </summary>
         /// <param name="logger">The logger.</param>
         /// <param name="sessionManager">The session manager.</param>
-        public SessionInfoWebSocketListener(ILogger logger, ISessionManager sessionManager, IDtoService dtoService)
+        public SessionInfoWebSocketListener(ILogger logger, ISessionManager sessionManager)
             : base(logger)
         {
             _sessionManager = sessionManager;
-            _dtoService = dtoService;
         }
 
         /// <summary>
@@ -49,7 +45,7 @@ namespace MediaBrowser.Api.WebSocket
         /// <returns>Task{SystemInfo}.</returns>
         protected override Task<IEnumerable<SessionInfoDto>> GetDataToSend(object state)
         {
-            return Task.FromResult(_sessionManager.Sessions.Where(i => i.IsActive).Select(_dtoService.GetSessionInfoDto));
+            return Task.FromResult(_sessionManager.Sessions.Where(i => i.IsActive).Select(_sessionManager.GetSessionInfoDto));
         }
     }
 }

+ 0 - 9
MediaBrowser.Controller/Dto/IDtoService.cs

@@ -1,9 +1,7 @@
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Session;
 using System;
 using System.Collections.Generic;
 
@@ -21,13 +19,6 @@ namespace MediaBrowser.Controller.Dto
         /// <returns>UserDto.</returns>
         UserDto GetUserDto(User user);
 
-        /// <summary>
-        /// Gets the session info dto.
-        /// </summary>
-        /// <param name="session">The session.</param>
-        /// <returns>SessionInfoDto.</returns>
-        SessionInfoDto GetSessionInfoDto(SessionInfo session);
-
         /// <summary>
         /// Gets the dto id.
         /// </summary>

+ 4 - 1
MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs

@@ -1,4 +1,5 @@
-
+using System.Collections.Generic;
+
 namespace MediaBrowser.Controller.Entities.Audio
 {
     public interface IHasAlbumArtist
@@ -9,5 +10,7 @@ namespace MediaBrowser.Controller.Entities.Audio
     public interface IHasArtist
     {
         bool HasArtist(string name);
+
+        List<string> AllArtists { get; }
     }
 }

+ 19 - 0
MediaBrowser.Controller/Entities/MusicVideo.cs

@@ -3,7 +3,9 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using System;
+using System.Collections.Generic;
 using System.Linq;
+using System.Runtime.Serialization;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -33,6 +35,23 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The revenue.</value>
         public double? Revenue { get; set; }
 
+        [IgnoreDataMember]
+        public List<string> AllArtists
+        {
+            get
+            {
+                var list = new List<string>();
+
+                if (!string.IsNullOrEmpty(Artist))
+                {
+                    list.Add(Artist);
+                }
+
+                return list;
+
+            }
+        }
+
         /// <summary>
         /// Determines whether the specified name has artist.
         /// </summary>

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

@@ -253,6 +253,7 @@
     <Compile Include="Session\PlaybackInfo.cs" />
     <Compile Include="Session\PlaybackProgressInfo.cs" />
     <Compile Include="Session\PlaybackStopInfo.cs" />
+    <Compile Include="Session\SessionEventArgs.cs" />
     <Compile Include="Session\SessionInfo.cs" />
     <Compile Include="Sorting\IBaseItemComparer.cs" />
     <Compile Include="Sorting\IUserBaseItemComparer.cs" />

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

@@ -89,6 +89,14 @@ namespace MediaBrowser.Controller.Session
         /// <returns>Task.</returns>
         Task SendServerShutdownNotification(CancellationToken cancellationToken);
 
+        /// <summary>
+        /// Sends the session ended notification.
+        /// </summary>
+        /// <param name="sessionInfo">The session information.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken);
+        
         /// <summary>
         /// Sends the server restart notification.
         /// </summary>

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

@@ -28,6 +28,16 @@ namespace MediaBrowser.Controller.Session
         /// </summary>
         event EventHandler<PlaybackStopEventArgs> PlaybackStopped;
 
+        /// <summary>
+        /// Occurs when [session started].
+        /// </summary>
+        event EventHandler<SessionEventArgs> SessionStarted;
+
+        /// <summary>
+        /// Occurs when [session ended].
+        /// </summary>
+        event EventHandler<SessionEventArgs> SessionEnded;
+        
         /// <summary>
         /// Gets the sessions.
         /// </summary>
@@ -83,6 +93,13 @@ namespace MediaBrowser.Controller.Session
         /// <returns>Task.</returns>
         Task ReportSessionEnded(Guid sessionId);
 
+        /// <summary>
+        /// Gets the session info dto.
+        /// </summary>
+        /// <param name="session">The session.</param>
+        /// <returns>SessionInfoDto.</returns>
+        SessionInfoDto GetSessionInfoDto(SessionInfo session);
+
         /// <summary>
         /// Sends the general command.
         /// </summary>

+ 12 - 0
MediaBrowser.Controller/Session/PlaybackInfo.cs

@@ -40,5 +40,17 @@ namespace MediaBrowser.Controller.Session
         /// </summary>
         /// <value>The media version identifier.</value>
         public string MediaSourceId { 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; }
     }
 }

+ 18 - 0
MediaBrowser.Controller/Session/PlaybackProgressInfo.cs

@@ -40,5 +40,23 @@ namespace MediaBrowser.Controller.Session
         /// </summary>
         /// <value>The media version identifier.</value>
         public string MediaSourceId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the volume level.
+        /// </summary>
+        /// <value>The volume level.</value>
+        public int? VolumeLevel { 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; }
     }
 }

+ 9 - 0
MediaBrowser.Controller/Session/SessionEventArgs.cs

@@ -0,0 +1,9 @@
+using System;
+
+namespace MediaBrowser.Controller.Session
+{
+    public class SessionEventArgs : EventArgs
+    {
+        public SessionInfo SessionInfo { get; set; }
+    }
+}

+ 10 - 1
MediaBrowser.Controller/Session/SessionInfo.cs

@@ -126,7 +126,6 @@ namespace MediaBrowser.Controller.Session
         /// <value>The now playing media version identifier.</value>
         public string NowPlayingMediaSourceId { get; set; }
         
-
         /// <summary>
         /// Gets or sets the now playing run time ticks.
         /// </summary>
@@ -150,6 +149,16 @@ namespace MediaBrowser.Controller.Session
         /// <value><c>true</c> if this instance is muted; otherwise, <c>false</c>.</value>
         public bool IsMuted { get; set; }
 
+        /// <summary>
+        /// Gets or sets the volume level, on a scale of 0-100
+        /// </summary>
+        /// <value>The volume level.</value>
+        public int? VolumeLevel { get; set; }
+
+        public int? NowPlayingAudioStreamIndex { get; set; }
+
+        public int? NowPlayingSubtitleStreamIndex { get; set; }
+        
         /// <summary>
         /// Gets or sets the device id.
         /// </summary>

+ 20 - 1
MediaBrowser.Dlna/DlnaManager.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Configuration;
+using System.Text;
+using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Dlna;
@@ -128,11 +129,29 @@ namespace MediaBrowser.Dlna
             else
             {
                 _logger.Debug("No matching device profile found. The default will need to be used.");
+                LogUnmatchedProfile(deviceInfo);
             }
 
             return profile;
         }
 
+        private void LogUnmatchedProfile(DeviceIdentification profile)
+        {
+            var builder = new StringBuilder();
+
+            builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty));
+            builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
+            builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
+            builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
+            builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
+            builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
+            builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
+            builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
+            builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
+
+            _logger.LogMultiline("No matching device profile found. The default will need to be used.", LogSeverity.Info, builder);
+        }
+
         private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
         {
             if (!string.IsNullOrWhiteSpace(profileInfo.DeviceDescription))

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

@@ -72,6 +72,7 @@
     <Compile Include="Profiles\Foobar2000Profile.cs" />
     <Compile Include="Profiles\Windows81Profile.cs" />
     <Compile Include="Profiles\WindowsMediaCenterProfile.cs" />
+    <Compile Include="Profiles\WindowsPhoneProfile.cs" />
     <Compile Include="Ssdp\SsdpHelper.cs" />
     <Compile Include="PlayTo\SsdpHttpClient.cs" />
     <Compile Include="PlayTo\StateVariable.cs" />

+ 51 - 14
MediaBrowser.Dlna/PlayTo/Device.cs

@@ -512,17 +512,15 @@ namespace MediaBrowser.Dlna.PlayTo
             if (result == null || result.Document == null)
                 return;
 
-            var track = result.Document.Descendants("CurrentURIMetaData").Select(i => i.Value).FirstOrDefault();
+            var track = result.Document.Descendants("CurrentURIMetaData").FirstOrDefault();
 
-            if (String.IsNullOrEmpty(track))
+            if (track == null)
             {
                 CurrentId = null;
                 return;
             }
 
-            var uPnpResponse = XElement.Parse(track);
-
-            var e = uPnpResponse.Element(uPnpNamespaces.items) ?? uPnpResponse;
+            var e = track.Element(uPnpNamespaces.items) ?? track;
 
             var uTrack = uParser.CreateObjectFromXML(new uParserObject
             {
@@ -556,7 +554,7 @@ namespace MediaBrowser.Dlna.PlayTo
             var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
             var duration = durationElem == null ? null : durationElem.Value;
 
-            if (!string.IsNullOrWhiteSpace(duration))
+            if (!string.IsNullOrWhiteSpace(duration) && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
             {
                 Duration = TimeSpan.Parse(duration, UsCulture);
             }
@@ -564,25 +562,22 @@ namespace MediaBrowser.Dlna.PlayTo
             var positionElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
             var position = positionElem == null ? null : positionElem.Value;
 
-            if (!string.IsNullOrWhiteSpace(position))
+            if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
             {
                 Position = TimeSpan.Parse(position, UsCulture);
             }
 
-            var track = result.Document.Descendants("TrackMetaData").Select(i => i.Value)
-                .FirstOrDefault();
+            var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
 
-            if (String.IsNullOrEmpty(track))
+            if (track == null)
             {
                 //If track is null, some vendors do this, use GetMediaInfo instead                    
                 return false;
             }
 
-            var uPnpResponse = XElement.Parse(track);
-
-            var e = uPnpResponse.Element(uPnpNamespaces.items) ?? uPnpResponse;
+            var e = track.Element(uPnpNamespaces.items) ?? track;
 
-            var uTrack = uBaseObject.Create(e);
+            var uTrack = CreateUBaseObject(e);
 
             if (uTrack == null)
                 return true;
@@ -592,6 +587,48 @@ namespace MediaBrowser.Dlna.PlayTo
             return true;
         }
 
+        private static uBaseObject CreateUBaseObject(XElement container)
+        {
+            if (container == null)
+            {
+                throw new ArgumentNullException("container");
+            }
+
+            return new uBaseObject
+            {
+                Id = container.GetAttributeValue(uPnpNamespaces.Id),
+                ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
+                Title = container.GetValue(uPnpNamespaces.title),
+                IconUrl = container.GetValue(uPnpNamespaces.Artwork),
+                SecondText = "",
+                Url = container.GetValue(uPnpNamespaces.Res),
+                ProtocolInfo = GetProtocolInfo(container),
+                MetaData = container.ToString()
+            };
+        }
+
+        private static string[] GetProtocolInfo(XElement container)
+        {
+            if (container == null)
+            {
+                throw new ArgumentNullException("container");
+            }
+
+            var resElement = container.Element(uPnpNamespaces.Res);
+
+            if (resElement != null)
+            {
+                var info = resElement.Attribute(uPnpNamespaces.ProtocolInfo);
+
+                if (info != null && !string.IsNullOrWhiteSpace(info.Value))
+                {
+                    return info.Value.Split(':');
+                }
+            }
+
+            return new string[4];
+        }
+
         #endregion
 
         #region From XML

+ 44 - 37
MediaBrowser.Dlna/PlayTo/DlnaController.cs

@@ -167,44 +167,46 @@ namespace MediaBrowser.Dlna.PlayTo
             if (_currentItem == null || _device.IsStopped)
                 return;
 
-            if (!_playbackStarted)
+            var playlistItem = Playlist.FirstOrDefault(p => p.PlayState == 1);
+            
+            if (playlistItem != null)
             {
-                await _sessionManager.OnPlaybackStart(new PlaybackInfo
+                if (!_playbackStarted)
                 {
-                    Item = _currentItem, 
-                    SessionId = _session.Id, 
-                    CanSeek = true,
-                    QueueableMediaTypes = new List<string> { _currentItem.MediaType }
-
-                }).ConfigureAwait(false);
-
-                _playbackStarted = true;
-            }
-
-            if ((_device.IsPlaying || _device.IsPaused))
-            {
-                var playlistItem = Playlist.FirstOrDefault(p => p.PlayState == 1);
-                if (playlistItem != null && playlistItem.Transcode)
-                {
-                    await _sessionManager.OnPlaybackProgress(new Controller.Session.PlaybackProgressInfo
+                    await _sessionManager.OnPlaybackStart(new PlaybackInfo
                     {
                         Item = _currentItem,
                         SessionId = _session.Id,
-                        PositionTicks = _device.Position.Ticks + playlistItem.StartPositionTicks,
-                        IsMuted = _device.IsMuted,
-                        IsPaused = _device.IsPaused
+                        CanSeek = true,
+                        QueueableMediaTypes = new List<string> { _currentItem.MediaType },
+                        MediaSourceId = playlistItem.MediaSourceId,
+                        AudioStreamIndex = playlistItem.AudioStreamIndex,
+                        SubtitleStreamIndex = playlistItem.SubtitleStreamIndex
 
                     }).ConfigureAwait(false);
+
+                    _playbackStarted = true;
                 }
-                else if (_currentItem != null)
+
+                if ((_device.IsPlaying || _device.IsPaused))
                 {
+                    var ticks = _device.Position.Ticks;
+
+                    if (playlistItem.Transcode)
+                    {
+                        ticks += playlistItem.StartPositionTicks;
+                    }
+
                     await _sessionManager.OnPlaybackProgress(new Controller.Session.PlaybackProgressInfo
                     {
                         Item = _currentItem,
                         SessionId = _session.Id,
-                        PositionTicks = _device.Position.Ticks,
+                        PositionTicks = ticks,
                         IsMuted = _device.IsMuted,
-                        IsPaused = _device.IsPaused
+                        IsPaused = _device.IsPaused,
+                        MediaSourceId = playlistItem.MediaSourceId,
+                        AudioStreamIndex = playlistItem.AudioStreamIndex,
+                        SubtitleStreamIndex = playlistItem.SubtitleStreamIndex
 
                     }).ConfigureAwait(false);
                 }
@@ -284,16 +286,16 @@ namespace MediaBrowser.Dlna.PlayTo
                     return _device.SetPlay();
 
                 case PlaystateCommand.Seek:
-                    var playlistItem = Playlist.FirstOrDefault(p => p.PlayState == 1);
-                    if (playlistItem != null && playlistItem.Transcode && playlistItem.MediaType == DlnaProfileType.Video && _currentItem != null)
-                    {
-                        var newItem = CreatePlaylistItem(_currentItem, command.SeekPositionTicks ?? 0, GetServerAddress());
-                        playlistItem.StartPositionTicks = newItem.StartPositionTicks;
-                        playlistItem.StreamUrl = newItem.StreamUrl;
-                        playlistItem.Didl = newItem.Didl;
-                        return _device.SetAvTransport(playlistItem.StreamUrl, GetDlnaHeaders(playlistItem), playlistItem.Didl);
-
-                    }
+                    //var playlistItem = Playlist.FirstOrDefault(p => p.PlayState == 1);
+                    //if (playlistItem != null && playlistItem.Transcode && _currentItem != null)
+                    //{
+                    //    var newItem = CreatePlaylistItem(_currentItem, command.SeekPositionTicks ?? 0, GetServerAddress());
+                    //    playlistItem.StartPositionTicks = newItem.StartPositionTicks;
+                    //    playlistItem.StreamUrl = newItem.StreamUrl;
+                    //    playlistItem.Didl = newItem.Didl;
+                    //    return _device.SetAvTransport(playlistItem.StreamUrl, GetDlnaHeaders(playlistItem), playlistItem.Didl);
+
+                    //}
                     return _device.Seek(TimeSpan.FromTicks(command.SeekPositionTicks ?? 0));
 
 
@@ -324,6 +326,11 @@ namespace MediaBrowser.Dlna.PlayTo
             return Task.FromResult(true);
         }
 
+        public Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
+        {
+            return Task.FromResult(true);
+        }
+
         public Task SendServerShutdownNotification(CancellationToken cancellationToken)
         {
             return Task.FromResult(true);
@@ -483,7 +490,7 @@ namespace MediaBrowser.Dlna.PlayTo
 
             return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';');
         }
-        
+
         private PlaylistItem GetPlaylistItem(BaseItem item, List<MediaStream> mediaStreams, DeviceProfile profile)
         {
             var video = item as Video;
@@ -559,10 +566,10 @@ namespace MediaBrowser.Dlna.PlayTo
             _logger.Debug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", _device.Properties.Name, nextTrack.StreamUrl, dlnaheaders);
 
             await _device.SetAvTransport(nextTrack.StreamUrl, dlnaheaders, nextTrack.Didl);
-         
+
             if (nextTrack.StartPositionTicks > 0 && !nextTrack.Transcode)
                 await _device.Seek(TimeSpan.FromTicks(nextTrack.StartPositionTicks));
-           
+
             return true;
         }
 

+ 7 - 0
MediaBrowser.Dlna/PlayTo/Extensions.cs

@@ -40,6 +40,13 @@ namespace MediaBrowser.Dlna.PlayTo
             return node == null ? null : node.Value;
         }
 
+        public static string GetAttributeValue(this XElement container, XName name)
+        {
+            var node = container.Attribute(name);
+
+            return node == null ? null : node.Value;
+        }
+
         public static string GetDescendantValue(this XElement container, XName name)
         {
             var node = container.Descendants(name)

+ 14 - 8
MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using System;
+using System.Globalization;
 using System.IO;
 using System.Text;
 using System.Threading.Tasks;
@@ -44,6 +45,8 @@ namespace MediaBrowser.Dlna.PlayTo
             }
         }
 
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        
         public async Task SubscribeAsync(string url, 
             string ip, 
             int port, 
@@ -58,11 +61,13 @@ namespace MediaBrowser.Dlna.PlayTo
                 LogRequest = _config.Configuration.DlnaOptions.EnableDebugLogging
             };
 
-            options.RequestHeaders["HOST"] = ip + ":" + port;
-            options.RequestHeaders["CALLBACK"] = "<" + localIp + ":" + eventport + ">";
+            options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture);
+            options.RequestHeaders["CALLBACK"] = "<" + localIp + ":" + eventport.ToString(_usCulture) + ">";
             options.RequestHeaders["NT"] = "upnp:event";
-            options.RequestHeaders["TIMEOUT"] = "Second - " + timeOut;
+            options.RequestHeaders["TIMEOUT"] = "Second-" + timeOut.ToString(_usCulture);
 
+            // TODO: Method should be SUBSCRIBE
+            // https://github.com/stormboy/node-upnp-controlpoint/blob/master/lib/upnp-service.js#L106
             using (await _httpClient.Get(options).ConfigureAwait(false))
             {
             }
@@ -71,8 +76,9 @@ namespace MediaBrowser.Dlna.PlayTo
         public async Task RespondAsync(Uri url, 
             string ip, 
             int port, 
-            string localIp, 
-            int eventport)
+            string localIp,
+            int eventport,
+            int timeOut = 3600)
         {
             var options = new HttpRequestOptions
             {
@@ -80,10 +86,10 @@ namespace MediaBrowser.Dlna.PlayTo
                 UserAgent = USERAGENT
             };
 
-            options.RequestHeaders["HOST"] = ip + ":" + port;
-            options.RequestHeaders["CALLBACK"] = "<" + localIp + ":" + eventport + ">";
+            options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture);
+            options.RequestHeaders["CALLBACK"] = "<" + localIp + ":" + eventport.ToString(_usCulture) + ">";
             options.RequestHeaders["NT"] = "upnp:event";
-            options.RequestHeaders["TIMEOUT"] = "Second - 3600";
+            options.RequestHeaders["TIMEOUT"] = "Second-" + timeOut.ToString(_usCulture);
 
             using (await _httpClient.Get(options).ConfigureAwait(false))
             {

+ 1 - 45
MediaBrowser.Dlna/PlayTo/uBaseObject.cs

@@ -1,6 +1,4 @@
-using System;
-using System.Xml.Linq;
-
+
 namespace MediaBrowser.Dlna.PlayTo
 {
     public class uBaseObject 
@@ -20,47 +18,5 @@ namespace MediaBrowser.Dlna.PlayTo
         public string Url { get; set; }
 
         public string[] ProtocolInfo { get; set; }
-
-        public static uBaseObject Create(XElement container)
-        {
-            if (container == null)
-            {
-                throw new ArgumentNullException("container");
-            }
-
-            return new uBaseObject
-            {
-                Id = container.Attribute(uPnpNamespaces.Id).Value,
-                ParentId = container.Attribute(uPnpNamespaces.ParentId).Value,
-                Title = container.GetValue(uPnpNamespaces.title),
-                IconUrl = container.GetValue(uPnpNamespaces.Artwork),
-                SecondText = "",
-                Url = container.GetValue(uPnpNamespaces.Res),
-                ProtocolInfo = GetProtocolInfo(container),
-                MetaData = container.ToString()
-            };
-        }
-
-        private static string[] GetProtocolInfo(XElement container)
-        {
-            if (container == null)
-            {
-                throw new ArgumentNullException("container");
-            }
-
-            var resElement = container.Element(uPnpNamespaces.Res);
-
-            if (resElement != null)
-            {
-                var info = resElement.Attribute(uPnpNamespaces.ProtocolInfo);
-
-                if (info != null && !string.IsNullOrWhiteSpace(info.Value))
-                {
-                    return info.Value.Split(':');
-                }
-            }
-
-            return new string[4];
-        }
     }
 }

+ 2 - 3
MediaBrowser.Dlna/Profiles/DefaultProfile.cs

@@ -1,6 +1,5 @@
-using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Model.Dlna;
 using System.Xml.Serialization;
-using MediaBrowser.Model.Dlna;
 
 namespace MediaBrowser.Dlna.Profiles
 {
@@ -32,7 +31,7 @@ namespace MediaBrowser.Dlna.Profiles
 
                 new TranscodingProfile
                 {
-                    Container = "ts",
+                    Container = "mp4",
                     Type = DlnaProfileType.Video,
                     AudioCodec = "aac",
                     VideoCodec = "h264",

+ 2 - 2
MediaBrowser.Dlna/Profiles/Windows81Profile.cs

@@ -1,5 +1,5 @@
-using System.Xml.Serialization;
-using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dlna;
+using System.Xml.Serialization;
 
 namespace MediaBrowser.Dlna.Profiles
 {

+ 200 - 0
MediaBrowser.Dlna/Profiles/WindowsPhoneProfile.cs

@@ -0,0 +1,200 @@
+using MediaBrowser.Model.Dlna;
+using System.Xml.Serialization;
+
+namespace MediaBrowser.Dlna.Profiles
+{
+    [XmlRoot("Profile")]
+    public class WindowsPhoneProfile : DefaultProfile
+    {
+        public WindowsPhoneProfile()
+        {
+            Name = "Windows Phone";
+
+            TranscodingProfiles = new[]
+            {
+                new TranscodingProfile
+                {
+                    Container = "mp3",
+                    AudioCodec = "mp3",
+                    Type = DlnaProfileType.Audio
+                },
+                new TranscodingProfile
+                {
+                    Container = "mp4",
+                    VideoCodec = "h264",
+                    AudioCodec = "aac",
+                    Type = DlnaProfileType.Video,
+                    VideoProfile = "Baseline"
+                }
+            };
+
+            DirectPlayProfiles = new[]
+            {
+                new DirectPlayProfile
+                {
+                    Container = "mp4,mov",
+                    VideoCodec = "h264",
+                    AudioCodec = "aac,mp3",
+                    Type = DlnaProfileType.Video
+                },
+
+                new DirectPlayProfile
+                {
+                    Container = "mp4,avi",
+                    VideoCodec = "mpeg4,msmpeg4",
+                    AudioCodec = "aac,mp3",
+                    Type = DlnaProfileType.Video
+                },
+
+                new DirectPlayProfile
+                {
+                    Container = "asf",
+                    VideoCodec = "wmv2,wmv3,vc1",
+                    AudioCodec = "wmav2,wmapro,wmavoice",
+                    Type = DlnaProfileType.Video
+                },
+
+                new DirectPlayProfile
+                {
+                    Container = "asf",
+                    AudioCodec = "wmav2,wmapro,wmavoice",
+                    Type = DlnaProfileType.Audio
+                },
+
+                new DirectPlayProfile
+                {
+                    Container = "mp4,aac",
+                    AudioCodec = "aac",
+                    Type = DlnaProfileType.Audio
+                },
+
+                new DirectPlayProfile
+                {
+                    Container = "mp3",
+                    AudioCodec = "mp3",
+                    Type = DlnaProfileType.Audio
+                },
+
+                new DirectPlayProfile
+                {
+                    Container = "jpeg,png,gif,bmp",
+                    Type = DlnaProfileType.Photo
+                }
+            };
+
+            CodecProfiles = new[]
+            {
+                new CodecProfile
+                {
+                    Type = CodecType.Video,
+                    Codec="h264",
+                    Conditions = new []
+                    {
+                        new ProfileCondition
+                        {
+                            Condition = ProfileConditionType.LessThanEqual,
+                            Property = ProfileConditionValue.Width,
+                            Value = "800"
+                        },
+                        new ProfileCondition
+                        {
+                            Condition = ProfileConditionType.LessThanEqual,
+                            Property = ProfileConditionValue.Height,
+                            Value = "480"
+                        },
+                        new ProfileCondition
+                        {
+                            Condition = ProfileConditionType.LessThanEqual,
+                            Property = ProfileConditionValue.VideoBitrate,
+                            Value = "1000000",
+                            IsRequired = false
+                        },
+                        new ProfileCondition
+                        {
+                            Condition = ProfileConditionType.LessThanEqual,
+                            Property = ProfileConditionValue.VideoFramerate,
+                            Value = "24",
+                            IsRequired = false
+                        },
+                        new ProfileCondition
+                        {
+                            Condition = ProfileConditionType.LessThanEqual,
+                            Property = ProfileConditionValue.VideoLevel,
+                            Value = "3"
+                        }
+                    }
+                },
+
+                new CodecProfile
+                {
+                    Type = CodecType.Video,
+                    Conditions = new []
+                    {
+                        new ProfileCondition
+                        {
+                            Condition = ProfileConditionType.LessThanEqual,
+                            Property = ProfileConditionValue.Width,
+                            Value = "800"
+                        },
+                        new ProfileCondition
+                        {
+                            Condition = ProfileConditionType.LessThanEqual,
+                            Property = ProfileConditionValue.Height,
+                            Value = "480"
+                        },
+                        new ProfileCondition
+                        {
+                            Condition = ProfileConditionType.LessThanEqual,
+                            Property = ProfileConditionValue.VideoBitrate,
+                            Value = "1000000",
+                            IsRequired = false
+                        },
+                        new ProfileCondition
+                        {
+                            Condition = ProfileConditionType.LessThanEqual,
+                            Property = ProfileConditionValue.VideoFramerate,
+                            Value = "24",
+                            IsRequired = false
+                        }
+                    }
+                },
+
+                new CodecProfile
+                {
+                    Type = CodecType.VideoAudio,
+                    Conditions = new []
+                    {
+                        new ProfileCondition
+                        {
+                            Condition = ProfileConditionType.LessThanEqual,
+                            Property = ProfileConditionValue.AudioBitrate,
+                            Value = "128000"
+                        },
+
+                        new ProfileCondition
+                        {
+                            Condition = ProfileConditionType.LessThanEqual,
+                            Property = ProfileConditionValue.AudioChannels,
+                            Value = "2"
+                        }
+                    }
+                },
+
+                new CodecProfile
+                {
+                    Type = CodecType.Audio,
+                    Conditions = new []
+                    {
+                        new ProfileCondition
+                        {
+                            Condition = ProfileConditionType.LessThanEqual,
+                            Property = ProfileConditionValue.AudioBitrate,
+                            Value = "128000"
+                        }
+                    }
+                }
+            };
+
+        }
+    }
+}

+ 1 - 1
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -768,7 +768,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             }
 
             // Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case.
-            var args = useIFrame ? string.Format("-i {0} -threads 0 -v quiet -vframes 1 -vf \"{2},thumbnail=50\" -f image2 \"{1}\"", inputPath, "-", vf) :
+            var args = useIFrame ? string.Format("-i {0} -threads 0 -v quiet -vframes 1 -vf \"{2},thumbnail=40\" -f image2 \"{1}\"", inputPath, "-", vf) :
                 string.Format("-i {0} -threads 0 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, "-", vf);
 
             var probeSize = GetProbeSizeArgument(type);

+ 77 - 20
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -28,7 +28,7 @@ namespace MediaBrowser.Model.Dlna
                     .ToList();
             }
 
-            var streams = mediaSources.Select(i => BuildAudioItem(options.ItemId, i, options.Profile)).ToList();
+            var streams = mediaSources.Select(i => BuildAudioItem(i, options)).ToList();
 
             foreach (var stream in streams)
             {
@@ -75,36 +75,40 @@ namespace MediaBrowser.Model.Dlna
                 streams.FirstOrDefault();
         }
 
-        private StreamInfo BuildAudioItem(string itemId, MediaSourceInfo item, DeviceProfile profile)
+        private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options)
         {
             var playlistItem = new StreamInfo
             {
-                ItemId = itemId,
+                ItemId = options.ItemId,
                 MediaType = DlnaProfileType.Audio,
                 MediaSourceId = item.Id
             };
 
             var audioStream = item.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
 
-            var directPlay = profile.DirectPlayProfiles
-                .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsAudioProfileSupported(i, item, audioStream));
-
-            if (directPlay != null)
+            // Honor the max bitrate setting
+            if (IsAudioEligibleForDirectPlay(item, options))
             {
-                var audioCodec = audioStream == null ? null : audioStream.Codec;
+                var directPlay = options.Profile.DirectPlayProfiles
+                    .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsAudioDirectPlaySupported(i, item, audioStream));
 
-                // Make sure audio codec profiles are satisfied
-                if (!string.IsNullOrEmpty(audioCodec) && profile.CodecProfiles.Where(i => i.Type == CodecType.Audio && i.ContainsCodec(audioCodec))
-                    .All(i => AreConditionsSatisfied(i.Conditions, item.Path, null, audioStream)))
+                if (directPlay != null)
                 {
-                    playlistItem.IsDirectStream = true;
-                    playlistItem.Container = item.Container;
+                    var audioCodec = audioStream == null ? null : audioStream.Codec;
 
-                    return playlistItem;
+                    // Make sure audio codec profiles are satisfied
+                    if (!string.IsNullOrEmpty(audioCodec) && options.Profile.CodecProfiles.Where(i => i.Type == CodecType.Audio && i.ContainsCodec(audioCodec))
+                        .All(i => AreConditionsSatisfied(i.Conditions, item.Path, null, audioStream)))
+                    {
+                        playlistItem.IsDirectStream = true;
+                        playlistItem.Container = item.Container;
+
+                        return playlistItem;
+                    }
                 }
             }
 
-            var transcodingProfile = profile.TranscodingProfiles
+            var transcodingProfile = options.Profile.TranscodingProfiles
                 .FirstOrDefault(i => i.Type == playlistItem.MediaType);
 
             if (transcodingProfile != null)
@@ -113,12 +117,28 @@ namespace MediaBrowser.Model.Dlna
                 playlistItem.Container = transcodingProfile.Container;
                 playlistItem.AudioCodec = transcodingProfile.AudioCodec;
 
-                var audioTranscodingConditions = profile.CodecProfiles
+                var audioTranscodingConditions = options.Profile.CodecProfiles
                     .Where(i => i.Type == CodecType.Audio && i.ContainsCodec(transcodingProfile.AudioCodec))
                     .Take(1)
                     .SelectMany(i => i.Conditions);
 
                 ApplyTranscodingConditions(playlistItem, audioTranscodingConditions);
+
+                // Honor requested max channels
+                if (options.MaxAudioChannels.HasValue)
+                {
+                    var currentValue = playlistItem.MaxAudioChannels ?? options.MaxAudioChannels.Value;
+
+                    playlistItem.MaxAudioChannels = Math.Min(options.MaxAudioChannels.Value, currentValue);
+                }
+
+                // Honor requested max bitrate
+                if (options.MaxBitrate.HasValue)
+                {
+                    var currentValue = playlistItem.AudioBitrate ?? options.MaxBitrate.Value;
+
+                    playlistItem.AudioBitrate = Math.Min(options.MaxBitrate.Value, currentValue);
+                }
             }
 
             return playlistItem;
@@ -140,7 +160,7 @@ namespace MediaBrowser.Model.Dlna
             {
                 // See if it can be direct played
                 var directPlay = options.Profile.DirectPlayProfiles
-                    .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsVideoProfileSupported(i, item, videoStream, audioStream));
+                    .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsVideoDirectPlaySupported(i, item, videoStream, audioStream));
 
                 if (directPlay != null)
                 {
@@ -189,6 +209,37 @@ namespace MediaBrowser.Model.Dlna
                     .SelectMany(i => i.Conditions);
 
                 ApplyTranscodingConditions(playlistItem, audioTranscodingConditions);
+
+                // Honor requested max channels
+                if (options.MaxAudioChannels.HasValue)
+                {
+                    var currentValue = playlistItem.MaxAudioChannels ?? options.MaxAudioChannels.Value;
+
+                    playlistItem.MaxAudioChannels = Math.Min(options.MaxAudioChannels.Value, currentValue);
+                }
+
+                // Honor requested max bitrate
+                if (options.MaxAudioTranscodingBitrate.HasValue)
+                {
+                    var currentValue = playlistItem.AudioBitrate ?? options.MaxAudioTranscodingBitrate.Value;
+
+                    playlistItem.AudioBitrate = Math.Min(options.MaxAudioTranscodingBitrate.Value, currentValue);
+                }
+
+                // Honor max rate
+                if (options.MaxBitrate.HasValue)
+                {
+                    var videoBitrate = options.MaxBitrate.Value;
+
+                    if (playlistItem.AudioBitrate.HasValue)
+                    {
+                        videoBitrate -= playlistItem.AudioBitrate.Value;
+                    }
+
+                    var currentValue = playlistItem.VideoBitrate ?? videoBitrate;
+
+                    playlistItem.VideoBitrate = Math.Min(videoBitrate, currentValue);
+                }
             }
 
             return playlistItem;
@@ -207,7 +258,13 @@ namespace MediaBrowser.Model.Dlna
                 return false;
             }
 
-            return true;
+            return IsAudioEligibleForDirectPlay(item, options);
+        }
+
+        private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, AudioOptions options)
+        {
+            // Honor the max bitrate setting
+            return !options.MaxBitrate.HasValue || (item.Bitrate.HasValue && item.Bitrate.Value <= options.MaxBitrate.Value);
         }
 
         private void ValidateInput(VideoOptions options)
@@ -331,7 +388,7 @@ namespace MediaBrowser.Model.Dlna
             }
         }
 
-        private bool IsAudioProfileSupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
+        private bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
         {
             if (profile.Container.Length > 0)
             {
@@ -346,7 +403,7 @@ namespace MediaBrowser.Model.Dlna
             return true;
         }
 
-        private bool IsVideoProfileSupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream)
+        private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream)
         {
             // Only plain video files can be direct played
             if (item.VideoType != VideoType.VideoFile)

+ 24 - 3
MediaBrowser.Model/Dlna/StreamInfo.cs

@@ -1,7 +1,7 @@
-using System;
+using MediaBrowser.Model.Dto;
+using System;
 using System.Collections.Generic;
 using System.Globalization;
-using MediaBrowser.Model.Dto;
 
 namespace MediaBrowser.Model.Dlna
 {
@@ -107,10 +107,25 @@ namespace MediaBrowser.Model.Dlna
     {
         public string ItemId { get; set; }
         public List<MediaSourceInfo> MediaSources { get; set; }
-        public int? MaxBitrateSetting { get; set; }
         public DeviceProfile Profile { get; set; }
+
+        /// <summary>
+        /// Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested.
+        /// </summary>
         public string MediaSourceId { get; set; }
+
         public string DeviceId { get; set; }
+
+        /// <summary>
+        /// Allows an override of supported number of audio channels
+        /// Example: DeviceProfile supports five channel, but user only has stereo speakers
+        /// </summary>
+        public int? MaxAudioChannels { get; set; }
+
+        /// <summary>
+        /// The application's configured quality setting
+        /// </summary>
+        public int? MaxBitrate { get; set; }
     }
 
     /// <summary>
@@ -120,5 +135,11 @@ namespace MediaBrowser.Model.Dlna
     {
         public int? AudioStreamIndex { get; set; }
         public int? SubtitleStreamIndex { get; set; }
+        public int? MaxAudioTranscodingBitrate { get; set; }
+
+        public VideoOptions()
+        {
+            MaxAudioTranscodingBitrate = 128000;
+        }
     }
 }

+ 2 - 0
MediaBrowser.Model/Dto/MediaVersionInfo.cs

@@ -24,5 +24,7 @@ namespace MediaBrowser.Model.Dto
         public Video3DFormat? Video3DFormat { get; set; }
         
         public List<MediaStream> MediaStreams { get; set; }
+
+        public int? Bitrate { get; set; }
     }
 }

+ 5 - 1
MediaBrowser.Model/Entities/MediaStream.cs

@@ -152,7 +152,11 @@ namespace MediaBrowser.Model.Entities
         /// <summary>
         /// The subtitle
         /// </summary>
-        Subtitle
+        Subtitle,
+        /// <summary>
+        /// The embedded image
+        /// </summary>
+        EmbeddedImage
     }
 
     public class MediaInfo

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

@@ -43,6 +43,9 @@ namespace MediaBrowser.Model.Session
         VolumeDown = 18,
         Mute = 19,
         Unmute = 20,
-        ToggleMute = 21
+        ToggleMute = 21,
+        SetVolume = 22,
+        SetAudioStreamIndex = 23,
+        SetSubtitleStreamIndex = 24
     }
 }

+ 10 - 0
MediaBrowser.Model/Session/PlaybackReports.cs

@@ -16,6 +16,10 @@ namespace MediaBrowser.Model.Session
 
         public string[] QueueableMediaTypes { get; set; }
 
+        public int? AudioStreamIndex { get; set; }
+
+        public int? SubtitleStreamIndex { get; set; }
+
         public PlaybackStartInfo()
         {
             QueueableMediaTypes = new string[] { };
@@ -38,6 +42,12 @@ namespace MediaBrowser.Model.Session
         public bool IsPaused { get; set; }
 
         public bool IsMuted { get; set; }
+
+        public int? AudioStreamIndex { get; set; }
+
+        public int? SubtitleStreamIndex { get; set; }
+
+        public int? VolumeLevel { get; set; }
     }
 
     /// <summary>

+ 18 - 12
MediaBrowser.Model/Session/SessionInfoDto.cs

@@ -111,6 +111,24 @@ namespace MediaBrowser.Model.Session
         /// <value>The name of the device.</value>
         public string DeviceName { get; set; }
 
+        /// <summary>
+        /// Gets or sets the volume level.
+        /// </summary>
+        /// <value>The volume level.</value>
+        public int? VolumeLevel { get; set; }
+
+        /// <summary>
+        /// Gets or sets the index of the now playing audio stream.
+        /// </summary>
+        /// <value>The index of the now playing audio stream.</value>
+        public int? NowPlayingAudioStreamIndex { get; set; }
+
+        /// <summary>
+        /// Gets or sets the index of the now playing subtitle stream.
+        /// </summary>
+        /// <value>The index of the now playing subtitle stream.</value>
+        public int? NowPlayingSubtitleStreamIndex { get; set; }
+
         /// <summary>
         /// Gets or sets a value indicating whether this instance is paused.
         /// </summary>
@@ -140,12 +158,6 @@ namespace MediaBrowser.Model.Session
         /// </summary>
         /// <value>The device id.</value>
         public string DeviceId { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [supports fullscreen toggle].
-        /// </summary>
-        /// <value><c>true</c> if [supports fullscreen toggle]; otherwise, <c>false</c>.</value>
-        public bool SupportsFullscreenToggle { get; set; }
         
         /// <summary>
         /// Gets or sets a value indicating whether [supports remote control].
@@ -153,12 +165,6 @@ namespace MediaBrowser.Model.Session
         /// <value><c>true</c> if [supports remote control]; otherwise, <c>false</c>.</value>
         public bool SupportsRemoteControl { get; set; }
 
-        /// <summary>
-        /// Gets or sets a value indicating whether [supports osd toggle].
-        /// </summary>
-        /// <value><c>true</c> if [supports osd toggle]; otherwise, <c>false</c>.</value>
-        public bool SupportsOsdToggle { get; set; }
-
         /// <summary>
         /// Gets or sets a value indicating whether [supports navigation commands].
         /// </summary>

+ 1 - 0
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -321,6 +321,7 @@ namespace MediaBrowser.Providers.Manager
 
                         // Only one local provider allowed per item
                         hasLocalMetadata = true;
+                        item.IsUnidentified = false;
                         break;
                     }
 

+ 14 - 121
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -245,127 +245,6 @@ namespace MediaBrowser.Server.Implementations.Dto
             return dto;
         }
 
-        public SessionInfoDto GetSessionInfoDto(SessionInfo session)
-        {
-            var dto = new SessionInfoDto
-            {
-                Client = session.Client,
-                DeviceId = session.DeviceId,
-                DeviceName = session.DeviceName,
-                Id = session.Id.ToString("N"),
-                LastActivityDate = session.LastActivityDate,
-                NowPlayingPositionTicks = session.NowPlayingPositionTicks,
-                SupportsRemoteControl = session.SupportsRemoteControl,
-                IsPaused = session.IsPaused,
-                IsMuted = session.IsMuted,
-                NowViewingContext = session.NowViewingContext,
-                NowViewingItemId = session.NowViewingItemId,
-                NowViewingItemName = session.NowViewingItemName,
-                NowViewingItemType = session.NowViewingItemType,
-                ApplicationVersion = session.ApplicationVersion,
-                CanSeek = session.CanSeek,
-                QueueableMediaTypes = session.QueueableMediaTypes,
-                PlayableMediaTypes = session.PlayableMediaTypes,
-                RemoteEndPoint = session.RemoteEndPoint,
-                AdditionalUsers = session.AdditionalUsers,
-                SupportedCommands = session.SupportedCommands
-            };
-
-            if (session.NowPlayingItem != null)
-            {
-                dto.NowPlayingItem = GetNowPlayingInfo(session.NowPlayingItem, session.NowPlayingMediaSourceId, session.NowPlayingRunTimeTicks);
-            }
-
-            if (session.UserId.HasValue)
-            {
-                dto.UserId = session.UserId.Value.ToString("N");
-            }
-            dto.UserName = session.UserName;
-
-            return dto;
-        }
-
-        /// <summary>
-        /// Converts a BaseItem to a BaseItemInfo
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="mediaSourceId">The media version identifier.</param>
-        /// <param name="nowPlayingRuntimeTicks">The now playing runtime ticks.</param>
-        /// <returns>BaseItemInfo.</returns>
-        /// <exception cref="System.ArgumentNullException">item</exception>
-        private BaseItemInfo GetNowPlayingInfo(BaseItem item, string mediaSourceId, long? nowPlayingRuntimeTicks)
-        {
-            if (item == null)
-            {
-                throw new ArgumentNullException("item");
-            }
-
-            var info = new BaseItemInfo
-            {
-                Id = GetDtoId(item),
-                Name = item.Name,
-                MediaType = item.MediaType,
-                Type = item.GetClientTypeName(),
-                RunTimeTicks = nowPlayingRuntimeTicks,
-                MediaSourceId = mediaSourceId
-            };
-
-            info.PrimaryImageTag = GetImageCacheTag(item, ImageType.Primary);
-
-            var backropItem = item.HasImage(ImageType.Backdrop) ? item : null;
-
-            var thumbItem = item.HasImage(ImageType.Thumb) ? item : null;
-
-            if (thumbItem == null)
-            {
-                var episode = item as Episode;
-
-                if (episode != null)
-                {
-                    var series = episode.Series;
-
-                    if (series != null && series.HasImage(ImageType.Thumb))
-                    {
-                        thumbItem = series;
-                    }
-                }
-            }
-
-            if (backropItem == null)
-            {
-                var episode = item as Episode;
-
-                if (episode != null)
-                {
-                    var series = episode.Series;
-
-                    if (series != null && series.HasImage(ImageType.Backdrop))
-                    {
-                        backropItem = series;
-                    }
-                }
-            }
-
-            if (thumbItem == null)
-            {
-                thumbItem = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Thumb));
-            }
-
-            if (thumbItem != null)
-            {
-                info.ThumbImageTag = GetImageCacheTag(thumbItem, ImageType.Thumb);
-                info.ThumbItemId = GetDtoId(thumbItem);
-            }
-
-            if (thumbItem != null)
-            {
-                info.BackdropImageTag = GetImageCacheTag(backropItem, ImageType.Backdrop);
-                info.BackdropItemId = GetDtoId(backropItem);
-            }
-
-            return info;
-        }
-
         /// <summary>
         /// Gets client-side Id of a server-side BaseItem
         /// </summary>
@@ -1367,6 +1246,13 @@ namespace MediaBrowser.Server.Implementations.Dto
                 }
             }
 
+            var bitrate = info.MediaStreams.Where(m => m.Type == MediaStreamType.Audio || m.Type == MediaStreamType.Video).Select(m => m.BitRate ?? 0).Sum();
+
+            if (bitrate > 0)
+            {
+                info.Bitrate = bitrate;
+            }
+
             return info;
         }
 
@@ -1388,6 +1274,13 @@ namespace MediaBrowser.Server.Implementations.Dto
                 info.Container = Path.GetExtension(i.Path).TrimStart('.');
             }
 
+            var bitrate = info.MediaStreams.Where(m => m.Type == MediaStreamType.Audio).Select(m => m.BitRate ?? 0).Sum();
+
+            if (bitrate > 0)
+            {
+                info.Bitrate = bitrate;
+            }
+
             return info;
         }
 

+ 2 - 2
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -785,12 +785,12 @@ namespace MediaBrowser.Server.Implementations.Library
         private T GetItemByName<T>(string path, string name)
             where T : BaseItem, new()
         {
-            if (string.IsNullOrEmpty(path))
+            if (string.IsNullOrWhiteSpace(path))
             {
                 throw new ArgumentNullException("path");
             }
 
-            if (string.IsNullOrEmpty(name))
+            if (string.IsNullOrWhiteSpace(name))
             {
                 throw new ArgumentNullException("name");
             }

+ 5 - 3
MediaBrowser.Server.Implementations/Library/Validators/PeopleValidator.cs

@@ -50,7 +50,9 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
 
             var people = _libraryManager.RootFolder.GetRecursiveChildren()
                 .SelectMany(c => c.People)
-                .DistinctBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
+                .Where(i => !string.IsNullOrWhiteSpace(i.Name))
+                .Select(i => i.Name)
+                .Distinct(StringComparer.OrdinalIgnoreCase)
                 .ToList();
 
             var numComplete = 0;
@@ -61,13 +63,13 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
 
                 try
                 {
-                    var item = _libraryManager.GetPerson(person.Name);
+                    var item = _libraryManager.GetPerson(person);
 
                     await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
                 }
                 catch (Exception ex)
                 {
-                    _logger.ErrorException("Error validating IBN entry {0}", ex, person.Name);
+                    _logger.ErrorException("Error validating IBN entry {0}", ex, person);
                 }
 
                 // Update progress

文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/JavaScript/ar.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/JavaScript/el.json


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

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

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

@@ -1 +1 @@
-{"SettingsSaved":"Configuracion guardada","AddUser":"Agregar usuario","Users":"Usuarios","Delete":"Borrar","Administrator":"Administrador","Password":"Contrase\u00f1a","DeleteImage":"Borrar Imagen","DeleteImageConfirmation":"Esta seguro que desea borrar esta imagen?","FileReadCancelled":"La lectura del archivo se ha cancelado.","FileNotFound":"Archivo no encontrado.","FileReadError":"Se encontr\u00f3 un error al leer el archivo.","DeleteUser":"Borrar Usuario","DeleteUserConfirmation":"Esta seguro que desea eliminar a {0}?","PasswordResetHeader":"Restablecer contrase\u00f1a","PasswordResetComplete":"La contrase\u00f1a se ha restablecido.","PasswordResetConfirmation":"Esta seguro que desea restablecer la contrase\u00f1a?","PasswordSaved":"Contrase\u00f1a guardada.","PasswordMatchError":"La contrase\u00f1a y la confirmaci\u00f3n de la contrase\u00f1a deben de ser iguales.","OptionOff":"Apagado","OptionOn":"Prendido","OptionRelease":"Liberar","OptionBeta":"Beta","OptionDev":"Desarrollo","UninstallPluginHeader":"Desinstalar Plugin","UninstallPluginConfirmation":"Esta seguro que desea desinstalar {0}?","NoPluginConfigurationMessage":"El plugin no requiere configuraci\u00f3n","NoPluginsInstalledMessage":"No tiene plugins instalados.","BrowsePluginCatalogMessage":"Navegar el catalogo de plugins para ver los plugins disponibles."}
+{"SettingsSaved":"Configuraci\u00f3n guardada","AddUser":"Agregar usuario","Users":"Usuarios","Delete":"Borrar","Administrator":"Administrador","Password":"Contrase\u00f1a","DeleteImage":"Borrar Imagen","DeleteImageConfirmation":"Est\u00e1 seguro que desea borrar esta imagen?","FileReadCancelled":"La lectura del archivo se ha cancelado.","FileNotFound":"Archivo no encontrado.","FileReadError":"Se encontr\u00f3 un error al leer el archivo.","DeleteUser":"Borrar Usuario","DeleteUserConfirmation":"Esta seguro que desea eliminar a {0}?","PasswordResetHeader":"Restablecer contrase\u00f1a","PasswordResetComplete":"La contrase\u00f1a se ha restablecido.","PasswordResetConfirmation":"Esta seguro que desea restablecer la contrase\u00f1a?","PasswordSaved":"Contrase\u00f1a guardada.","PasswordMatchError":"La contrase\u00f1a y la confirmaci\u00f3n de la contrase\u00f1a deben de ser iguales.","OptionOff":"Apagado","OptionOn":"Encendido","OptionRelease":"Liberar","OptionBeta":"Beta","OptionDev":"Desarrollo","UninstallPluginHeader":"Desinstalar Plugin","UninstallPluginConfirmation":"Esta seguro que desea desinstalar {0}?","NoPluginConfigurationMessage":"El plugin no requiere configuraci\u00f3n","NoPluginsInstalledMessage":"No tiene plugins instalados.","BrowsePluginCatalogMessage":"Navegar el catalogo de plugins para ver los plugins disponibles."}

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

@@ -1 +1 @@
-{"SettingsSaved":"Configuraci\u00f3n guardada.","AddUser":"Agregar usuario","Users":"Usuarios","Delete":"Eliminar","Administrator":"Administrador","Password":"Contrase\u00f1a","DeleteImage":"Eliminar imagen","DeleteImageConfirmation":"\u00bfEst\u00e1 seguro que desea eliminar esta imagen?","FileReadCancelled":"La lectura del archivo ha sido cancelada.","FileNotFound":"Archivo no encontrado.","FileReadError":"Ha ocurrido un error al leer el archivo.","DeleteUser":"Eliminar Usuario","DeleteUserConfirmation":"\u00bfEsta seguro que desea eliminar a {0}?","PasswordResetHeader":"Restablecer Contrase\u00f1a","PasswordResetComplete":"La contrase\u00f1a ha sido restablecida.","PasswordResetConfirmation":"\u00bfEst\u00e1 seguro que desea restablecer la contrase\u00f1a?","PasswordSaved":"Contrase\u00f1a guardada.","PasswordMatchError":"La Contrase\u00f1a y la confirmaci\u00f3n de la contrase\u00f1a deben coincidir.","OptionOff":"Apagado","OptionOn":"Encendido","OptionRelease":"Liberar","OptionBeta":"Beta","OptionDev":"Desarrollo","UninstallPluginHeader":"Desinstalar Complemento","UninstallPluginConfirmation":"\u00bfEst\u00e1 seguro que desea desinstalar {0}?","NoPluginConfigurationMessage":"El complemento no requiere configuraci\u00f3n","NoPluginsInstalledMessage":"No tiene complementos instalados.","BrowsePluginCatalogMessage":"Navege en el catalogo de complementos para ver los complementos disponibles."}
+{"SettingsSaved":"Configuraci\u00f3n guardada.","AddUser":"Agregar usuario","Users":"Usuarios","Delete":"Eliminar","Administrator":"Administrador","Password":"Contrase\u00f1a","DeleteImage":"Eliminar imagen","DeleteImageConfirmation":"\u00bfEst\u00e1 seguro que desea eliminar esta imagen?","FileReadCancelled":"La lectura del archivo ha sido cancelada.","FileNotFound":"Archivo no encontrado.","FileReadError":"Ha ocurrido un error al leer el archivo.","DeleteUser":"Eliminar Usuario","DeleteUserConfirmation":"\u00bfEsta seguro que desea eliminar a {0}?","PasswordResetHeader":"Restablecer Contrase\u00f1a","PasswordResetComplete":"La contrase\u00f1a ha sido restablecida.","PasswordResetConfirmation":"\u00bfEst\u00e1 seguro que desea restablecer la contrase\u00f1a?","PasswordSaved":"Contrase\u00f1a guardada.","PasswordMatchError":"La Contrase\u00f1a y la confirmaci\u00f3n de la contrase\u00f1a deben coincidir.","OptionOff":"Apagado","OptionOn":"Encendido","OptionRelease":"Versi\u00f3n Oficial","OptionBeta":"Beta","OptionDev":"Desarrollo (Inestable)","UninstallPluginHeader":"Desinstalar Complemento","UninstallPluginConfirmation":"\u00bfEst\u00e1 seguro que desea desinstalar {0}?","NoPluginConfigurationMessage":"El complemento no requiere configuraci\u00f3n","NoPluginsInstalledMessage":"No tiene complementos instalados.","BrowsePluginCatalogMessage":"Navege en el catalogo de complementos para ver los complementos disponibles."}

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

@@ -1 +1 @@
-{"SettingsSaved":"Param\u00e8tres sauvegard\u00e9s.","AddUser":"Ajouter utilisateur","Users":"Utilisateurs","Delete":"Supprimer","Administrator":"Administrateur","Password":"Mot de passe","DeleteImage":"Supprimer Image","DeleteImageConfirmation":"\u00cates-vous s\u00fbr de vouloir supprimer l'image?","FileReadCancelled":"La lecture du fichier a \u00e9t\u00e9 annul\u00e9e.","FileNotFound":"Fichier non trouv\u00e9","FileReadError":"Un erreur est survenue pendant la lecture du fichier.","DeleteUser":"Supprimer utilisateur","DeleteUserConfirmation":"\u00cates-vous s\u00fbr de vouloir supprimer {0}?","PasswordResetHeader":"R\u00e9initialisation du mot de passe","PasswordResetComplete":"Le mot de passe a \u00e9t\u00e9 r\u00e9initialis\u00e9.","PasswordResetConfirmation":"\u00cates-vous s\u00fbr de vouloir r\u00e9initialiser le mot de passe?","PasswordSaved":"Mot de passe sauvegard\u00e9.","PasswordMatchError":"Le mot de passe et sa confirmation doivent correspondre.","OptionOff":"Off","OptionOn":"On","OptionRelease":"Lancement","OptionBeta":"Beta","OptionDev":"Dev","UninstallPluginHeader":"D\u00e9sinstaller Plug-in","UninstallPluginConfirmation":"\u00cates-vous s\u00fbr de vouloir d\u00e9sinstaller {0}?","NoPluginConfigurationMessage":"Ce module d'extension n'a rien \u00e0 configurer.","NoPluginsInstalledMessage":"Vous n'avez aucun module d'extension install\u00e9.","BrowsePluginCatalogMessage":"Explorer notre catalogue de Plug-ins disponibles."}
+{"SettingsSaved":"Param\u00e8tres sauvegard\u00e9s.","AddUser":"Ajouter utilisateur","Users":"Utilisateurs","Delete":"Supprimer","Administrator":"Administrateur","Password":"Mot de passe","DeleteImage":"Supprimer Image","DeleteImageConfirmation":"\u00cates-vous s\u00fbr de vouloir supprimer l'image?","FileReadCancelled":"La lecture du fichier a \u00e9t\u00e9 annul\u00e9e.","FileNotFound":"Fichier non trouv\u00e9","FileReadError":"Un erreur est survenue pendant la lecture du fichier.","DeleteUser":"Supprimer utilisateur","DeleteUserConfirmation":"\u00cates-vous s\u00fbr de vouloir supprimer {0}?","PasswordResetHeader":"R\u00e9initialisation du mot de passe","PasswordResetComplete":"Le mot de passe a \u00e9t\u00e9 r\u00e9initialis\u00e9.","PasswordResetConfirmation":"\u00cates-vous s\u00fbr de vouloir r\u00e9initialiser le mot de passe?","PasswordSaved":"Mot de passe sauvegard\u00e9.","PasswordMatchError":"Le mot de passe et sa confirmation doivent correspondre.","OptionOff":"Off","OptionOn":"On","OptionRelease":"Lancement","OptionBeta":"Beta","OptionDev":"Dev (Instable)","UninstallPluginHeader":"D\u00e9sinstaller Plug-in","UninstallPluginConfirmation":"\u00cates-vous s\u00fbr de vouloir d\u00e9sinstaller {0}?","NoPluginConfigurationMessage":"Ce module d'extension n'a rien \u00e0 configurer.","NoPluginsInstalledMessage":"Vous n'avez aucun module d'extension install\u00e9.","BrowsePluginCatalogMessage":"Explorer notre catalogue de Plug-ins disponibles."}

文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/JavaScript/he.json


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

@@ -1 +1 @@
-{"SettingsSaved":"Settaggi salvati.","AddUser":"Aggiungi utente","Users":"Utenti","Delete":"Elimina","Administrator":"Amministratore","Password":"Password","DeleteImage":"Elimina immagine","DeleteImageConfirmation":"Sei sicuro di voler eliminare questa immagine?","FileReadCancelled":"Il file letto \u00e8 stato cancellato.","FileNotFound":"File non trovato","FileReadError":"Errore durante la lettura del file.","DeleteUser":"Elimina utente","DeleteUserConfirmation":"Sei sicuro di voler eliminare {0}?","PasswordResetHeader":"Ripristina Password","PasswordResetComplete":"la password \u00e8 stata ripristinata.","PasswordResetConfirmation":"Sei sicuro di voler ripristinare la password?","PasswordSaved":"Password salvata.","PasswordMatchError":"Le password non coincidono.","OptionOff":"Spegni","OptionOn":"Accendi","OptionRelease":"Versione","OptionBeta":"Beta","OptionDev":"Dev","UninstallPluginHeader":"Disinstalla Plugin","UninstallPluginConfirmation":"Sei sicuro di voler Disinstallare {0}?","NoPluginConfigurationMessage":"Questo Plugin non \u00e8 stato configurato.","NoPluginsInstalledMessage":"non ci sono Plugins installati.","BrowsePluginCatalogMessage":"Sfoglia il catalogo dei Plugins."}
+{"SettingsSaved":"Settaggi salvati.","AddUser":"Aggiungi utente","Users":"Utenti","Delete":"Elimina","Administrator":"Amministratore","Password":"Password","DeleteImage":"Elimina immagine","DeleteImageConfirmation":"Sei sicuro di voler eliminare questa immagine?","FileReadCancelled":"Il file letto \u00e8 stato cancellato.","FileNotFound":"File non trovato","FileReadError":"Errore durante la lettura del file.","DeleteUser":"Elimina utente","DeleteUserConfirmation":"Sei sicuro di voler eliminare {0}?","PasswordResetHeader":"Ripristina Password","PasswordResetComplete":"la password \u00e8 stata ripristinata.","PasswordResetConfirmation":"Sei sicuro di voler ripristinare la password?","PasswordSaved":"Password salvata.","PasswordMatchError":"Le password non coincidono.","OptionOff":"Off","OptionOn":"On","OptionRelease":"Versione Ufficiale","OptionBeta":"Beta","OptionDev":"Dev (instabile)","UninstallPluginHeader":"Disinstalla Plugin","UninstallPluginConfirmation":"Sei sicuro di voler Disinstallare {0}?","NoPluginConfigurationMessage":"Questo Plugin non \u00e8 stato configurato.","NoPluginsInstalledMessage":"non ci sono Plugins installati.","BrowsePluginCatalogMessage":"Sfoglia il catalogo dei Plugins."}

+ 3 - 3
MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json

@@ -7,7 +7,7 @@
 	"Password": "Password",
 	"DeleteImage": "Delete Image",
 	"DeleteImageConfirmation": "Are you sure you wish to delete this image?",
-	"FileReadCancelled": "The file read has been cancelled.",
+	"FileReadCancelled": "The file read has been canceled.",
 	"FileNotFound": "File not found.",
 	"FileReadError": "An error occurred while reading the file.",
 	"DeleteUser": "Delete User",
@@ -19,9 +19,9 @@
 	"PasswordMatchError": "Password and password confirmation must match.",
 	"OptionOff": "Off",
 	"OptionOn": "On",
-	"OptionRelease": "Release",
+	"OptionRelease": "Official Release",
 	"OptionBeta": "Beta",
-	"OptionDev": "Dev",
+	"OptionDev": "Dev (Unstable)",
 	"UninstallPluginHeader": "Uninstall Plugin",
 	"UninstallPluginConfirmation": "Are you sure you wish to uninstall {0}?",
 	"NoPluginConfigurationMessage": "This plugin has nothing to configure.",

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

@@ -0,0 +1 @@
+{"SettingsSaved":"Innstillinger lagret","AddUser":"Legg til bruker","Users":"Brukere","Delete":"Slett","Administrator":"Administrator","Password":"PAssord","DeleteImage":"Slett bilde","DeleteImageConfirmation":"Er du sikker p\u00e5 at du vil slette bildet?","FileReadCancelled":"Lesing av filen avbrutt","FileNotFound":"Fil ikke funnet","FileReadError":"Feil oppstod i det filen ble lest","DeleteUser":"Slett bruker","DeleteUserConfirmation":"Er du sikker p\u00e5 at du vil slette{0}?","PasswordResetHeader":"Resett passord","PasswordResetComplete":"Passordet har blitt resatt","PasswordResetConfirmation":"Er du sikker p\u00e5 at du vil resette passordet?","PasswordSaved":"Passord lagret","PasswordMatchError":"Passord og passord-verifiseringen m\u00e5 matche","OptionOff":"Av","OptionOn":"P\u00e5","OptionRelease":"Sluppet","OptionBeta":"Beta","OptionDev":"Dev","UninstallPluginHeader":"Avinstaller plugin","UninstallPluginConfirmation":"Are you sure you wish to uninstall {0}?","NoPluginConfigurationMessage":"Denne pluginn-en har intet \u00e5 konfigurere","NoPluginsInstalledMessage":"Du har ikke installert noen plugins enn\u00e5","BrowsePluginCatalogMessage":"Browse v\u00e5r plugin-katalog for \u00e5 se tilgjengelige plugins"}

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

@@ -1 +1 @@
-{"SettingsSaved":"Instellingen opgeslagen.","AddUser":"Gebruiker toevoegen","Users":"Gebruikers","Delete":"Verwijderen","Administrator":"Beheerder","Password":"Wachtwoord","DeleteImage":"Verwijder afbeelding","DeleteImageConfirmation":"Weet je zeker dat je deze afbeelding wilt verwijderen?","FileReadCancelled":"Het lezen van het bestand is geannuleerd","FileNotFound":"Bestand niet gevonden.","FileReadError":"Er is een fout opgetreden bij het lezen van het bestand.","DeleteUser":"Verwijder gebruiker","DeleteUserConfirmation":"Weet je zeker dat je {0} wilt verwijderen?","PasswordResetHeader":"Wachtwoord opnieuw instellen","PasswordResetComplete":"Het wachtwoord is opnieuw ingesteld.","PasswordResetConfirmation":"Weet je zeker dat je het wachtwoord opnieuw in wilt stellen?","PasswordSaved":"Wachtwoord opgeslagen.","PasswordMatchError":"Wachtwoord en wachtwoord bevestiging moeten hetzelfde zijn.","OptionOff":"Uit","OptionOn":"Aan","OptionRelease":"Release","OptionBeta":"Beta","OptionDev":"Ontwikkeling","UninstallPluginHeader":"Deinstalleer Plugin","UninstallPluginConfirmation":"Weet u zeker dat u {0} wilt deinstalleren?","NoPluginConfigurationMessage":"Deze plugin heeft niets in te stellen","NoPluginsInstalledMessage":"U heeft geen plugins geinstalleerd","BrowsePluginCatalogMessage":"Blader door de Plugincatalogus voor beschikbare plugins."}
+{"SettingsSaved":"Instellingen opgeslagen.","AddUser":"Gebruiker toevoegen","Users":"Gebruikers","Delete":"Verwijderen","Administrator":"Beheerder","Password":"Wachtwoord","DeleteImage":"Verwijder afbeelding","DeleteImageConfirmation":"Weet je zeker dat je deze afbeelding wilt verwijderen?","FileReadCancelled":"Bestand lezen is geannuleerd.","FileNotFound":"Bestand niet gevonden.","FileReadError":"Er is een fout opgetreden bij het lezen van het bestand.","DeleteUser":"Verwijder gebruiker","DeleteUserConfirmation":"Weet je zeker dat je {0} wilt verwijderen?","PasswordResetHeader":"Wachtwoord opnieuw instellen","PasswordResetComplete":"Het wachtwoord is opnieuw ingesteld.","PasswordResetConfirmation":"Weet je zeker dat je het wachtwoord opnieuw in wilt stellen?","PasswordSaved":"Wachtwoord opgeslagen.","PasswordMatchError":"Wachtwoord en wachtwoord bevestiging moeten hetzelfde zijn.","OptionOff":"Uit","OptionOn":"Aan","OptionRelease":"Offici\u00eble Release","OptionBeta":"Beta","OptionDev":"Alpha (Onstabiel)","UninstallPluginHeader":"Plug-in de\u00efnstalleren","UninstallPluginConfirmation":"Weet u zeker dat u {0} wilt de\u00efnstalleren?","NoPluginConfigurationMessage":"Deze plug-in heeft niets in te stellen","NoPluginsInstalledMessage":"U heeft geen plug-ins ge\u00efnstalleerd","BrowsePluginCatalogMessage":"Bekijk de Plug-in catalogus voor beschikbare plug-ins."}

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

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

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

@@ -1 +1 @@
-{"SettingsSaved":"Configura\u00e7\u00f5es guardadas.","AddUser":"Adicionar Utilizador","Users":"Utilizadores","Delete":"Apagar","Administrator":"Administrador","Password":"Senha","DeleteImage":"Apagar Imagem","DeleteImageConfirmation":"Tem a certeza que deseja apagar a imagem?","FileReadCancelled":"A leitura do ficheiro foi cancelada.","FileNotFound":"Ficheiro n\u00e3o encontrado.","FileReadError":"Ocorreu um erro ao ler o ficheiro.","DeleteUser":"Apagar Utilizador","DeleteUserConfirmation":"Tem a certeza que deseja apagar {0}?","PasswordResetHeader":"Redefinir Senha","PasswordResetComplete":"A senha foi redefinida.","PasswordResetConfirmation":"Tem a certeza que deseja redefinir a senha?","PasswordSaved":"Senha guardada.","PasswordMatchError":"A senha e a confirma\u00e7\u00e3o da senha devem coincidir.","OptionOff":"Desligado","OptionOn":"Ligado","OptionRelease":"Final","OptionBeta":"Beta","OptionDev":"Dev","UninstallPluginHeader":"Desinstalar extens\u00e3o","UninstallPluginConfirmation":"Tem a certeza que deseja desinstalar {0}?","NoPluginConfigurationMessage":"Esta extens\u00e3o n\u00e3o \u00e9 configur\u00e1vel.","NoPluginsInstalledMessage":"N\u00e3o tem extens\u00f5es instaladas.","BrowsePluginCatalogMessage":"Navegue o nosso cat\u00e1logo de extens\u00f5es, para ver as extens\u00f5es dispon\u00edveis."}
+{"SettingsSaved":"Configura\u00e7\u00f5es guardadas.","AddUser":"Adicionar Utilizador","Users":"Utilizadores","Delete":"Apagar","Administrator":"Administrador","Password":"Senha","DeleteImage":"Apagar Imagem","DeleteImageConfirmation":"Tem a certeza que deseja apagar a imagem?","FileReadCancelled":"A leitura do ficheiro foi cancelada.","FileNotFound":"Ficheiro n\u00e3o encontrado.","FileReadError":"Ocorreu um erro ao ler o ficheiro.","DeleteUser":"Apagar Utilizador","DeleteUserConfirmation":"Tem a certeza que deseja apagar {0}?","PasswordResetHeader":"Redefinir Senha","PasswordResetComplete":"A senha foi redefinida.","PasswordResetConfirmation":"Tem a certeza que deseja redefinir a senha?","PasswordSaved":"Senha guardada.","PasswordMatchError":"A senha e a confirma\u00e7\u00e3o da senha devem coincidir.","OptionOff":"Desligado","OptionOn":"Ligado","OptionRelease":"Lan\u00e7amento Oficial","OptionBeta":"Beta","OptionDev":"Dev (Inst\u00e1vel)","UninstallPluginHeader":"Desinstalar extens\u00e3o","UninstallPluginConfirmation":"Tem a certeza que deseja desinstalar {0}?","NoPluginConfigurationMessage":"Esta extens\u00e3o n\u00e3o \u00e9 configur\u00e1vel.","NoPluginsInstalledMessage":"N\u00e3o tem extens\u00f5es instaladas.","BrowsePluginCatalogMessage":"Navegue o nosso cat\u00e1logo de extens\u00f5es, para ver as extens\u00f5es dispon\u00edveis."}

文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/JavaScript/ru.json


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

@@ -1 +1 @@
-{"SettingsSaved":"\u8a2d\u7f6e\u5df2\u4fdd\u5b58","AddUser":"Add User","Users":"\u7528\u6236","Delete":"\u522a\u9664","Administrator":"\u7ba1\u7406\u54e1","Password":"\u5bc6\u78bc","DeleteImage":"\u522a\u9664\u5716\u50cf","DeleteImageConfirmation":"\u4f60\u78ba\u5b9a\u8981\u522a\u9664\u9019\u5f35\u5716\u7247\uff1f","FileReadCancelled":"The file read has been cancelled.","FileNotFound":"File not found.","FileReadError":"An error occurred while reading the file.","DeleteUser":"\u522a\u9664\u7528\u6236","DeleteUserConfirmation":"Are you sure you wish to delete {0}?","PasswordResetHeader":"\u91cd\u8a2d\u5bc6\u78bc","PasswordResetComplete":"\u5bc6\u78bc\u5df2\u91cd\u8a2d","PasswordResetConfirmation":"\u4f60\u78ba\u5b9a\u8981\u91cd\u8a2d\u5bc6\u78bc\uff1f","PasswordSaved":"\u5bc6\u78bc\u5df2\u4fdd\u5b58\u3002","PasswordMatchError":"\u5bc6\u78bc\u548c\u78ba\u8a8d\u5bc6\u78bc\u5fc5\u9808\u4e00\u81f4\u3002","OptionOff":"Off","OptionOn":"On","OptionRelease":"Release","OptionBeta":"Beta","OptionDev":"Dev","UninstallPluginHeader":"Uninstall Plugin","UninstallPluginConfirmation":"Are you sure you wish to uninstall {0}?","NoPluginConfigurationMessage":"This plugin has nothing to configure.","NoPluginsInstalledMessage":"You have no plugins installed.","BrowsePluginCatalogMessage":"Browse our plugin catalog to view available plugins."}
+{"SettingsSaved":"\u8a2d\u7f6e\u5df2\u4fdd\u5b58\u3002","AddUser":"\u6dfb\u52a0\u7528\u6236","Users":"\u7528\u6236","Delete":"\u522a\u9664","Administrator":"\u7ba1\u7406\u54e1","Password":"\u5bc6\u78bc","DeleteImage":"\u522a\u9664\u5716\u50cf","DeleteImageConfirmation":"\u4f60\u78ba\u5b9a\u8981\u522a\u9664\u9019\u5f35\u5716\u50cf\uff1f","FileReadCancelled":"\u6a94\u6848\u8b80\u53d6\u5df2\u88ab\u53d6\u6d88\u3002","FileNotFound":"\u672a\u627e\u5230\u6a94\u6848\u3002","FileReadError":"\u5728\u8b80\u53d6\u6a94\u6848\u6642\u767c\u751f\u932f\u8aa4\u3002","DeleteUser":"\u522a\u9664\u7528\u6236","DeleteUserConfirmation":"\u4f60\u78ba\u5b9a\u8981\u522a\u9664{0}\uff1f","PasswordResetHeader":"\u91cd\u8a2d\u5bc6\u78bc","PasswordResetComplete":"\u5bc6\u78bc\u5df2\u91cd\u8a2d","PasswordResetConfirmation":"\u4f60\u78ba\u5b9a\u8981\u91cd\u8a2d\u5bc6\u78bc\uff1f","PasswordSaved":"\u5bc6\u78bc\u5df2\u4fdd\u5b58\u3002","PasswordMatchError":"\u5bc6\u78bc\u548c\u5bc6\u78bc\u78ba\u8a8d\u5fc5\u9808\u4e00\u81f4\u3002","OptionOff":"\u95dc\u9589","OptionOn":"\u958b\u555f","OptionRelease":"\u6b63\u5f0f\u7248\u672c","OptionBeta":"\u516c\u6e2c\u7248\u672c","OptionDev":"\u958b\u767c\u7248\u672c","UninstallPluginHeader":"\u5378\u8f09\u63d2\u4ef6","UninstallPluginConfirmation":"\u4f60\u78ba\u5b9a\u8981\u5378\u8f09{0}\uff1f","NoPluginConfigurationMessage":"\u9019\u500b\u63d2\u4ef6\u6c92\u6709\u8a2d\u5b9a\u9078\u9805\u3002","NoPluginsInstalledMessage":"\u4f60\u6c92\u6709\u5b89\u88dd\u63d2\u4ef6\u3002","BrowsePluginCatalogMessage":"\u700f\u89bd\u6211\u5011\u7684\u63d2\u4ef6\u76ee\u9304\u4f86\u67e5\u770b\u53ef\u7528\u7684\u63d2\u4ef6\u3002"}

+ 3 - 0
MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs

@@ -333,11 +333,14 @@ namespace MediaBrowser.Server.Implementations.Localization
         {
             return new List<LocalizatonOption>
             {
+                new LocalizatonOption{ Name="Arabic", Value="ar"},
+                new LocalizatonOption{ Name="English (United Kingdom)", Value="en-GB"},
                 new LocalizatonOption{ Name="English (United States)", Value="en-us"},
                 new LocalizatonOption{ Name="Chinese Traditional", Value="zh-TW"},
                 new LocalizatonOption{ Name="Dutch", Value="nl"},
                 new LocalizatonOption{ Name="French", Value="fr"},
                 new LocalizatonOption{ Name="German", Value="de"},
+                new LocalizatonOption{ Name="Greek", Value="el"},
                 new LocalizatonOption{ Name="Hebrew", Value="he"},
                 new LocalizatonOption{ Name="Italian", Value="it"},
                 new LocalizatonOption{ Name="Norwegian Bokmål", Value="nb"},

文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/ar.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/de.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/el.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/en_GB.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/en_US.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/es.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/es_MX.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/fr.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/he.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/it.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/nb.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/nl.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/pt_BR.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/pt_PT.json


文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/ru.json


+ 97 - 2
MediaBrowser.Server.Implementations/Localization/Server/server.json

@@ -134,7 +134,7 @@
 	"OptionVideoBitrate": "Video Bitrate",
 	"OptionResumable": "Resumable",
 	"ScheduledTasksHelp": "Click a task to adjust its schedule.",
-	"ScheduledTasksTitle": "ScheduledTasks",
+	"ScheduledTasksTitle": "Scheduled Tasks",
 	"TabMyPlugins": "My Plugins",
 	"TabCatalog": "Catalog",
 	"TabUpdates": "Updates",
@@ -207,5 +207,100 @@
 	"OptionAllowBrowsingLiveTv": "Allow browsing of live tv",
 	"OptionAllowDeleteLibraryContent": "Allow this user to delete library content",
 	"OptionAllowManageLiveTv": "Allow management of live tv recordings",
-	"OptionAllowRemoteControlOthers": "Allow this user to remote control other users"
+	"OptionAllowRemoteControlOthers": "Allow this user to remote control other users",
+	"OptionMissingTmdbId": "Missing Tmdb Id",
+	"OptionIsHD": "HD",
+	"OptionIsSD": "SD",
+	"OptionMetascore": "Metascore",
+	"OptionImdbRating": "IMDb rating",
+	"ButtonSelect": "Select",
+	"ButtonGroupVersions": "Group Versions",
+	"PismoMessage": "Utilizing Pismo File Mount through a donated license.",
+	"PleaseSupportOtherProduces": "Please support other free products we utilize:",
+	"VersionNumber": "Version {0}",
+	"TabPaths": "Paths",
+	"TabServer": "Server",
+	"TabTranscoding": "Transcoding",
+	"TitleAdvanced": "Advanced",
+	"LabelAutomaticUpdateLevel": "Automatic update level",
+	"OptionRelease": "Official Release",
+	"OptionBeta": "Beta",
+	"OptionDev": "Dev (Unstable)",
+	"LabelAllowServerAutoRestart": "Allow the server to restart automatically to apply updates",
+	"LabelAllowServerAutoRestartHelp": "The server will only restart during idle periods, when no users are active.",
+	"LabelEnableDebugLogging": "Enable debug logging",
+	"LabelRunServerAtStartup": "Run server at startup",
+	"LabelRunServerAtStartupHelp": "This will start the tray icon on windows startup. To start the windows service, uncheck this and run the service from the windows control panel. Please note that you cannot run both at the same time, so you will need to exit the tray icon before starting the service.",
+	"ButtonSelectDirectory": "Select Directory",
+	"LabelCustomPaths": "Specify custom paths where desired. Leave fields empty to use the defaults.",
+	"LabelCachePath": "Cache path:",
+	"LabelCachePathHelp": "This folder contains server cache files, such as images.",
+	"LabelImagesByNamePath": "Images by name path:",
+	"LabelImagesByNamePathHelp": "This folder contains actor, artist, genre and studio images.",
+	"LabelMetadataPath": "Metadata path:",
+	"LabelMetadataPathHelp": "This location contains downloaded artwork and metadata that is not configured to be stored in media folders.",
+	"LabelTranscodingTempPath": "Transcoding temporary path:",
+	"LabelTranscodingTempPathHelp": "This folder contains working files used by the transcoder.",
+	"TabBasics": "Basics",
+	"TabTV": "TV",
+	"TabGames": "Games",
+	"TabMusic": "Music",
+	"TabOthers": "Others",
+	"HeaderExtractChapterImagesFor": "Extract chapter images for:",
+	"OptionMovies": "Movies",
+	"OptionEpisodes": "Episodes",
+	"OptionOtherVideos": "Other Videos",
+	"TitleMetadata": "Metadata",
+	"LabelAutomaticUpdatesFanart": "Enable automatic updates from FanArt.tv",
+	"LabelAutomaticUpdatesTmdb": "Enable automatic updates from TheMovieDB.org",
+	"LabelAutomaticUpdatesTvdb": "Enable automatic updates from TheTVDB.com",
+	"LabelAutomaticUpdatesFanartHelp": "If enabled, new images will be downloaded automatically as they're added to fanart.tv. Existing images will not be replaced.",
+	"LabelAutomaticUpdatesTmdbHelp": "If enabled, new images will be downloaded automatically as they're added to TheMovieDB.org. Existing images will not be replaced.",
+	"LabelAutomaticUpdatesTvdbHelp": "If enabled, new images will be downloaded automatically as they're added to TheTVDB.com. Existing images will not be replaced.",
+	"ExtractChapterImagesHelp": "Extracting chapter images will allow clients to display graphical scene selection menus. The process can be slow, cpu-intensive and may require several gigabytes of space. It runs as a nightly scheduled task at 4am, although this is configurable in the scheduled tasks area. It is not recommended to run this task during peak usage hours.",
+	"LabelMetadataDownloadLanguage": "Preferred language:",
+	"ButtonAutoScroll": "Auto-scroll",
+	"LabelImageSavingConvention": "Image saving convention:",
+	"LabelImageSavingConventionHelp": "Media Browser recognizes images from most major media applications. Choosing your downloading convention is useful if you also use other products.",
+	"OptionImageSavingCompatible": "Compatible - MB3/Plex/Xbmc",
+	"OptionImageSavingStandard": "Standard - MB3/MB2",
+	"ButtonSignIn": "Sign In",
+	"TitleSignIn": "Sign In",
+	"HeaderPleaseSignIn": "Please sign in",
+	"LabelUser": "User:",
+	"LabelPassword": "Password:",
+	"ButtonManualLogin": "Manual Login:",
+	"PasswordLocalhostMessage": "Passwords are not required when logging in from localhost.",
+	"TabGuide": "Guide",
+	"TabChannels": "Channels",
+	"HeaderChannels": "Channels",
+	"TabRecordings": "Recordings",
+	"TabScheduled": "Scheduled",
+	"TabSeries": "Series",
+	"ButtonCancelRecording": "Cancel Recording",
+	"HeaderPrePostPadding": "Pre/Post Padding",
+	"LabelPrePaddingMinutes": "Pre-padding minutes:",
+	"OptionPrePaddingRequired": "Pre-padding is required in order to record.",
+	"LabelPostPaddingMinutes": "Post-padding minutes:",
+	"OptionPostPaddingRequired": "Post-padding is required in order to record.",
+	"HeaderWhatsOnTV": "What's On",
+	"HeaderUpcomingTV": "Upcoming TV",
+	"TabStatus": "Status",
+	"TabSettings": "Settings",
+	"ButtonRefreshGuideData": "Refresh Guide Data",
+	"OptionPriority": "Priority",
+	"OptionRecordOnAllChannels": "Record program on all channels",
+	"OptionRecordAnytime": "Record program at any time",
+	"OptionRecordOnlyNewEpisodes": "Record only new episodes",
+	"HeaderDays": "Days",
+	"HeaderActiveRecordings": "Active Recordings",
+	"HeaderLatestRecordings": "Latest Recordings",
+	"HeaderAllRecordings": "All Recordings",
+	"ButtonPlay": "Play",
+	"ButtonEdit": "Edit",
+	"ButtonRecord": "Record",
+	"ButtonDelete": "Delete",
+	"OptionRecordSeries": "Record Series",
+	"HeaderDetails": "Details",
+	"ButtonCancelRecording": "Cancel Recording"
 }

文件差异内容过多而无法显示
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/Server/zh_TW.json


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

@@ -307,6 +307,14 @@
     <EmbeddedResource Include="Localization\Server\it.json" />
     <EmbeddedResource Include="Localization\Server\es_MX.json" />
     <EmbeddedResource Include="Localization\JavaScript\es_MX.json" />
+    <EmbeddedResource Include="Localization\JavaScript\ar.json" />
+    <EmbeddedResource Include="Localization\JavaScript\he.json" />
+    <EmbeddedResource Include="Localization\JavaScript\nb.json" />
+    <EmbeddedResource Include="Localization\Server\ar.json" />
+    <EmbeddedResource Include="Localization\Server\el.json" />
+    <EmbeddedResource Include="Localization\Server\nb.json" />
+    <EmbeddedResource Include="Localization\JavaScript\el.json" />
+    <EmbeddedResource Include="Localization\Server\en_GB.json" />
     <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup>

+ 7 - 4
MediaBrowser.Server.Implementations/Roku/RokuSessionController.cs

@@ -41,6 +41,11 @@ namespace MediaBrowser.Server.Implementations.Roku
             }
         }
 
+        public Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
+        {
+            return Task.FromResult(true);
+        }
+
         public Task SendMessageCommand(MessageCommand command, CancellationToken cancellationToken)
         {
             return SendCommand(new WebSocketMessage<MessageCommand>
@@ -81,11 +86,10 @@ namespace MediaBrowser.Server.Implementations.Roku
             }, cancellationToken);
         }
 
-        private readonly Task _cachedTask = Task.FromResult(true);
         public Task SendLibraryUpdateInfo(LibraryUpdateInfo info, CancellationToken cancellationToken)
         {
             // Roku probably won't care about this
-            return _cachedTask;
+            return Task.FromResult(true);
         }
 
         public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
@@ -101,7 +105,7 @@ namespace MediaBrowser.Server.Implementations.Roku
         public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken)
         {
             // Roku probably won't care about this
-            return _cachedTask;
+            return Task.FromResult(true);
         }
 
         public Task SendServerShutdownNotification(CancellationToken cancellationToken)
@@ -137,7 +141,6 @@ namespace MediaBrowser.Server.Implementations.Roku
             });
         }
 
-
         public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
         {
             return SendCommand(new WebSocketMessage<GeneralCommand>

+ 2 - 3
MediaBrowser.Server.Implementations/ServerManager/WebSocketConnection.cs

@@ -157,11 +157,10 @@ namespace MediaBrowser.Server.Implementations.ServerManager
                 var info = new WebSocketMessageInfo
                 {
                     MessageType = stub.MessageType,
-                    Data = stub.Data == null ? null : stub.Data.ToString()
+                    Data = stub.Data == null ? null : stub.Data.ToString(),
+                    Connection = this
                 };
 
-                info.Connection = this;
-
                 OnReceive(info);
             }
             catch (Exception ex)

+ 242 - 25
MediaBrowser.Server.Implementations/Session/SessionManager.cs

@@ -1,8 +1,12 @@
-using MediaBrowser.Common.Events;
+using System.IO;
+using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Session;
@@ -42,6 +46,8 @@ namespace MediaBrowser.Server.Implementations.Session
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
         private readonly IMusicManager _musicManager;
+        private readonly IDtoService _dtoService;
+        private readonly IImageProcessor _imageProcessor;
 
         /// <summary>
         /// Gets or sets the configuration manager.
@@ -68,6 +74,10 @@ namespace MediaBrowser.Server.Implementations.Session
         /// </summary>
         public event EventHandler<PlaybackStopEventArgs> PlaybackStopped;
 
+        public event EventHandler<SessionEventArgs> SessionStarted;
+
+        public event EventHandler<SessionEventArgs> SessionEnded;
+
         private IEnumerable<ISessionControllerFactory> _sessionFactories = new List<ISessionControllerFactory>();
 
         private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
@@ -80,7 +90,7 @@ namespace MediaBrowser.Server.Implementations.Session
         /// <param name="logger">The logger.</param>
         /// <param name="userRepository">The user repository.</param>
         /// <param name="libraryManager">The library manager.</param>
-        public SessionManager(IUserDataManager userDataRepository, IServerConfigurationManager configurationManager, ILogger logger, IUserRepository userRepository, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager)
+        public SessionManager(IUserDataManager userDataRepository, IServerConfigurationManager configurationManager, ILogger logger, IUserRepository userRepository, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, IDtoService dtoService, IImageProcessor imageProcessor)
         {
             _userDataRepository = userDataRepository;
             _configurationManager = configurationManager;
@@ -89,6 +99,8 @@ namespace MediaBrowser.Server.Implementations.Session
             _libraryManager = libraryManager;
             _userManager = userManager;
             _musicManager = musicManager;
+            _dtoService = dtoService;
+            _imageProcessor = imageProcessor;
         }
 
         /// <summary>
@@ -109,6 +121,47 @@ namespace MediaBrowser.Server.Implementations.Session
             get { return _activeConnections.Values.OrderByDescending(c => c.LastActivityDate).ToList(); }
         }
 
+        private void OnSessionStarted(SessionInfo info)
+        {
+            EventHelper.QueueEventIfNotNull(SessionStarted, this, new SessionEventArgs
+            {
+                SessionInfo = info
+
+            }, _logger);
+        }
+
+        private async void OnSessionEnded(SessionInfo info)
+        {
+            try
+            {
+                await SendSessionEndedNotification(info, CancellationToken.None).ConfigureAwait(false);
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error in SendSessionEndedNotification", ex);
+            }
+
+            EventHelper.QueueEventIfNotNull(SessionEnded, this, new SessionEventArgs
+            {
+                SessionInfo = info
+
+            }, _logger);
+
+            var disposable = info.SessionController as IDisposable;
+
+            if (disposable != null)
+            {
+                try
+                {
+                    disposable.Dispose();
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error disposing session controller", ex);
+                }
+            }
+        }
+
         /// <summary>
         /// Logs the user activity.
         /// </summary>
@@ -194,19 +247,7 @@ namespace MediaBrowser.Server.Implementations.Session
 
                 if (_activeConnections.TryRemove(key, out removed))
                 {
-                    var disposable = removed.SessionController as IDisposable;
-
-                    if (disposable != null)
-                    {
-                        try
-                        {
-                            disposable.Dispose();
-                        }
-                        catch (Exception ex)
-                        {
-                            _logger.ErrorException("Error disposing session controller", ex);
-                        }
-                    }
+                    OnSessionEnded(removed);
                 }
             }
             finally
@@ -222,11 +263,9 @@ namespace MediaBrowser.Server.Implementations.Session
         /// <param name="item">The item.</param>
         /// <param name="mediaSourceId">The media version identifier.</param>
         /// <param name="isPaused">if set to <c>true</c> [is paused].</param>
-        /// <param name="isMuted">if set to <c>true</c> [is muted].</param>
         /// <param name="currentPositionTicks">The current position ticks.</param>
-        private void UpdateNowPlayingItem(SessionInfo session, BaseItem item, string mediaSourceId, bool isPaused, bool isMuted, long? currentPositionTicks = null)
+        private void UpdateNowPlayingItem(SessionInfo session, BaseItem item, string mediaSourceId, bool isPaused, long? currentPositionTicks = null)
         {
-            session.IsMuted = isMuted;
             session.IsPaused = isPaused;
             session.NowPlayingPositionTicks = currentPositionTicks;
             session.NowPlayingItem = item;
@@ -291,12 +330,19 @@ namespace MediaBrowser.Server.Implementations.Session
 
             try
             {
-                var connection = _activeConnections.GetOrAdd(key, keyName => new SessionInfo
+                var connection = _activeConnections.GetOrAdd(key, keyName =>
                 {
-                    Client = clientType,
-                    DeviceId = deviceId,
-                    ApplicationVersion = appVersion,
-                    Id = Guid.NewGuid()
+                    var sessionInfo = new SessionInfo
+                    {
+                        Client = clientType,
+                        DeviceId = deviceId,
+                        ApplicationVersion = appVersion,
+                        Id = Guid.NewGuid()
+                    };
+
+                    OnSessionStarted(sessionInfo);
+
+                    return sessionInfo;
                 });
 
                 connection.DeviceName = deviceName;
@@ -372,11 +418,14 @@ namespace MediaBrowser.Server.Implementations.Session
 
             var mediaSourceId = GetMediaSourceId(item, info.MediaSourceId);
 
-            UpdateNowPlayingItem(session, item, mediaSourceId, false, false);
+            UpdateNowPlayingItem(session, item, mediaSourceId, false);
 
             session.CanSeek = info.CanSeek;
             session.QueueableMediaTypes = info.QueueableMediaTypes;
 
+            session.NowPlayingAudioStreamIndex = info.AudioStreamIndex;
+            session.NowPlayingSubtitleStreamIndex = info.SubtitleStreamIndex;
+
             var key = item.GetUserDataKey();
 
             var users = GetUsers(session);
@@ -442,7 +491,12 @@ namespace MediaBrowser.Server.Implementations.Session
 
             var mediaSourceId = GetMediaSourceId(info.Item, info.MediaSourceId);
 
-            UpdateNowPlayingItem(session, info.Item, mediaSourceId, info.IsPaused, info.IsMuted, info.PositionTicks);
+            UpdateNowPlayingItem(session, info.Item, mediaSourceId, info.IsPaused, info.PositionTicks);
+
+            session.IsMuted = info.IsMuted;
+            session.VolumeLevel = info.VolumeLevel;
+            session.NowPlayingAudioStreamIndex = info.AudioStreamIndex;
+            session.NowPlayingSubtitleStreamIndex = info.SubtitleStreamIndex;
 
             var key = info.Item.GetUserDataKey();
 
@@ -919,6 +973,27 @@ namespace MediaBrowser.Server.Implementations.Session
         }
 
 
+        public Task SendSessionEndedNotification(SessionInfo sessionInfo, CancellationToken cancellationToken)
+        {
+            var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList();
+            var dto = GetSessionInfoDto(sessionInfo);
+
+            var tasks = sessions.Select(session => Task.Run(async () =>
+            {
+                try
+                {
+                    await session.SessionController.SendSessionEndedNotification(dto, cancellationToken).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error in SendSessionEndedNotification.", ex);
+                }
+
+            }, cancellationToken));
+
+            return Task.WhenAll(tasks);
+        }
+
         /// <summary>
         /// Adds the additional user.
         /// </summary>
@@ -1017,5 +1092,147 @@ namespace MediaBrowser.Server.Implementations.Session
             session.PlayableMediaTypes = capabilities.PlayableMediaTypes;
             session.SupportedCommands = capabilities.SupportedCommands;
         }
+
+        public SessionInfoDto GetSessionInfoDto(SessionInfo session)
+        {
+            var dto = new SessionInfoDto
+            {
+                Client = session.Client,
+                DeviceId = session.DeviceId,
+                DeviceName = session.DeviceName,
+                Id = session.Id.ToString("N"),
+                LastActivityDate = session.LastActivityDate,
+                NowPlayingPositionTicks = session.NowPlayingPositionTicks,
+                SupportsRemoteControl = session.SupportsRemoteControl,
+                IsPaused = session.IsPaused,
+                IsMuted = session.IsMuted,
+                NowViewingContext = session.NowViewingContext,
+                NowViewingItemId = session.NowViewingItemId,
+                NowViewingItemName = session.NowViewingItemName,
+                NowViewingItemType = session.NowViewingItemType,
+                ApplicationVersion = session.ApplicationVersion,
+                CanSeek = session.CanSeek,
+                QueueableMediaTypes = session.QueueableMediaTypes,
+                PlayableMediaTypes = session.PlayableMediaTypes,
+                RemoteEndPoint = session.RemoteEndPoint,
+                AdditionalUsers = session.AdditionalUsers,
+                SupportedCommands = session.SupportedCommands,
+                NowPlayingAudioStreamIndex = session.NowPlayingAudioStreamIndex,
+                NowPlayingSubtitleStreamIndex = session.NowPlayingSubtitleStreamIndex,
+                UserName = session.UserName,
+                VolumeLevel = session.VolumeLevel
+            };
+
+            if (session.NowPlayingItem != null)
+            {
+                dto.NowPlayingItem = GetNowPlayingInfo(session.NowPlayingItem, session.NowPlayingMediaSourceId, session.NowPlayingRunTimeTicks);
+            }
+
+            if (session.UserId.HasValue)
+            {
+                dto.UserId = session.UserId.Value.ToString("N");
+            }
+
+            return dto;
+        }
+
+        /// <summary>
+        /// Converts a BaseItem to a BaseItemInfo
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="mediaSourceId">The media version identifier.</param>
+        /// <param name="nowPlayingRuntimeTicks">The now playing runtime ticks.</param>
+        /// <returns>BaseItemInfo.</returns>
+        /// <exception cref="System.ArgumentNullException">item</exception>
+        private BaseItemInfo GetNowPlayingInfo(BaseItem item, string mediaSourceId, long? nowPlayingRuntimeTicks)
+        {
+            if (item == null)
+            {
+                throw new ArgumentNullException("item");
+            }
+
+            var info = new BaseItemInfo
+            {
+                Id = GetDtoId(item),
+                Name = item.Name,
+                MediaType = item.MediaType,
+                Type = item.GetClientTypeName(),
+                RunTimeTicks = nowPlayingRuntimeTicks,
+                MediaSourceId = mediaSourceId
+            };
+
+            info.PrimaryImageTag = GetImageCacheTag(item, ImageType.Primary);
+
+            var backropItem = item.HasImage(ImageType.Backdrop) ? item : null;
+
+            var thumbItem = item.HasImage(ImageType.Thumb) ? item : null;
+
+            if (thumbItem == null)
+            {
+                var episode = item as Episode;
+
+                if (episode != null)
+                {
+                    var series = episode.Series;
+
+                    if (series != null && series.HasImage(ImageType.Thumb))
+                    {
+                        thumbItem = series;
+                    }
+                }
+            }
+
+            if (backropItem == null)
+            {
+                var episode = item as Episode;
+
+                if (episode != null)
+                {
+                    var series = episode.Series;
+
+                    if (series != null && series.HasImage(ImageType.Backdrop))
+                    {
+                        backropItem = series;
+                    }
+                }
+            }
+
+            if (thumbItem == null)
+            {
+                thumbItem = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Thumb));
+            }
+
+            if (thumbItem != null)
+            {
+                info.ThumbImageTag = GetImageCacheTag(thumbItem, ImageType.Thumb);
+                info.ThumbItemId = GetDtoId(thumbItem);
+            }
+
+            if (thumbItem != null)
+            {
+                info.BackdropImageTag = GetImageCacheTag(backropItem, ImageType.Backdrop);
+                info.BackdropItemId = GetDtoId(backropItem);
+            }
+
+            return info;
+        }
+
+        private Guid? GetImageCacheTag(BaseItem item, ImageType type)
+        {
+            try
+            {
+                return _imageProcessor.GetImageCacheTag(item, type);
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error getting {0} image info", ex, type);
+                return null;
+            }
+        }
+
+        private string GetDtoId(BaseItem item)
+        {
+            return _dtoService.GetDtoId(item);
+        }
     }
 }

+ 29 - 1
MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Net;
+using System.Globalization;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Session;
@@ -187,6 +188,8 @@ namespace MediaBrowser.Server.Implementations.Session
             return result;
         }
 
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        
         /// <summary>
         /// Reports the playback start.
         /// </summary>
@@ -228,6 +231,16 @@ namespace MediaBrowser.Server.Implementations.Session
                     info.MediaSourceId = vals[3];
                 }
 
+                if (vals.Length > 4 && !string.IsNullOrWhiteSpace(vals[4]))
+                {
+                    info.AudioStreamIndex = int.Parse(vals[4], _usCulture);
+                }
+
+                if (vals.Length > 5 && !string.IsNullOrWhiteSpace(vals[5]))
+                {
+                    info.SubtitleStreamIndex = int.Parse(vals[5], _usCulture);
+                }
+
                 _sessionManager.OnPlaybackStart(info);
             }
         }
@@ -275,6 +288,21 @@ namespace MediaBrowser.Server.Implementations.Session
                     info.MediaSourceId = vals[4];
                 }
 
+                if (vals.Length > 5 && !string.IsNullOrWhiteSpace(vals[5]))
+                {
+                    info.VolumeLevel = int.Parse(vals[5], _usCulture);
+                }
+
+                if (vals.Length > 5 && !string.IsNullOrWhiteSpace(vals[6]))
+                {
+                    info.AudioStreamIndex = int.Parse(vals[6], _usCulture);
+                }
+
+                if (vals.Length > 7 && !string.IsNullOrWhiteSpace(vals[7]))
+                {
+                    info.SubtitleStreamIndex = int.Parse(vals[7], _usCulture);
+                }
+
                 _sessionManager.OnPlaybackProgress(info);
             }
         }

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

@@ -198,5 +198,17 @@ namespace MediaBrowser.Server.Implementations.Session
 
             }, cancellationToken);
         }
+
+        public Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
+        {
+            var socket = GetActiveSocket();
+
+            return socket.SendAsync(new WebSocketMessage<SessionInfoDto>
+            {
+                MessageType = "SessionEnded",
+                Data = sessionInfo
+
+            }, cancellationToken);
+        }
     }
 }

+ 3 - 3
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -466,9 +466,6 @@ namespace MediaBrowser.ServerApplication
 
             RegisterSingleInstance<ISearchEngine>(() => new SearchEngine(LogManager, LibraryManager, UserManager));
 
-            SessionManager = new SessionManager(UserDataManager, ServerConfigurationManager, Logger, UserRepository, LibraryManager, UserManager, musicManager);
-            RegisterSingleInstance(SessionManager);
-
             HttpServer = ServerFactory.CreateServer(this, LogManager, "Media Browser", "mediabrowser", "dashboard/index.html");
             RegisterSingleInstance(HttpServer, false);
             progress.Report(10);
@@ -488,6 +485,9 @@ namespace MediaBrowser.ServerApplication
             DtoService = new DtoService(Logger, LibraryManager, UserManager, UserDataManager, ItemRepository, ImageProcessor, ServerConfigurationManager, FileSystemManager, ProviderManager);
             RegisterSingleInstance(DtoService);
 
+            SessionManager = new SessionManager(UserDataManager, ServerConfigurationManager, Logger, UserRepository, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor);
+            RegisterSingleInstance(SessionManager);
+
             var newsService = new Server.Implementations.News.NewsService(ApplicationPaths, JsonSerializer);
             RegisterSingleInstance<INewsService>(newsService);
 

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

@@ -14,7 +14,6 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
-using System.Reflection;
 using System.Text;
 using System.Threading.Tasks;
 using WebMarkupMin.Core.Minifiers;
@@ -335,16 +334,16 @@ namespace MediaBrowser.WebDashboard.Api
                         html = html.Replace("<html>", "<html lang=\"" + lang + "\">");
                     }
 
-                    try
-                    {
-                        var minifier = new HtmlMinifier(new HtmlMinificationSettings(true));
+                    //try
+                    //{
+                    //    var minifier = new HtmlMinifier(new HtmlMinificationSettings(true));
 
-                        html = minifier.Minify(html).MinifiedContent;
-                    }
-                    catch (Exception ex)
-                    {
-                        Logger.ErrorException("Error minifying html", ex);
-                    }
+                    //    html = minifier.Minify(html).MinifiedContent;
+                    //}
+                    //catch (Exception ex)
+                    //{
+                    //    Logger.ErrorException("Error minifying html", ex);
+                    //}
                 }
 
                 var version = GetType().Assembly.GetName().Version;
@@ -510,10 +509,10 @@ namespace MediaBrowser.WebDashboard.Api
                                 "librarylist.js",
                                 "editorsidebar.js",
                                 "librarymenu.js",
+                                "mediacontroller.js",
                                 "chromecast.js",
                                 "contextmenu.js",
 
-                                "mediacontroller.js",
                                 "mediaplayer.js",
                                 "mediaplayer-video.js",
 

+ 4 - 0
MediaBrowser.WebDashboard/ApiClient.js

@@ -78,6 +78,10 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi
             return name;
         }());
 
+        self.deviceName = function () {
+            return deviceName;
+        };
+
         self.deviceId = function () {
             return deviceId;
         };

+ 2 - 2
Nuget/MediaBrowser.Common.Internal.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Common.Internal</id>
-        <version>3.0.347</version>
+        <version>3.0.348</version>
         <title>MediaBrowser.Common.Internal</title>
         <authors>Luke</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.347" />
+            <dependency id="MediaBrowser.Common" version="3.0.348" />
             <dependency id="NLog" version="2.1.0" />
             <dependency id="SimpleInjector" version="2.4.1" />
             <dependency id="sharpcompress" version="0.10.2" />

+ 1 - 1
Nuget/MediaBrowser.Common.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Common</id>
-        <version>3.0.347</version>
+        <version>3.0.348</version>
         <title>MediaBrowser.Common</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>

+ 2 - 2
Nuget/MediaBrowser.Server.Core.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Server.Core</id>
-        <version>3.0.347</version>
+        <version>3.0.348</version>
         <title>Media Browser.Server.Core</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains core components required to build plugins for Media Browser Server.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.347" />
+            <dependency id="MediaBrowser.Common" version="3.0.348" />
         </dependencies>
     </metadata>
     <files>

部分文件因为文件数量过多而无法显示