Browse Source

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

Michalis Adamidis 11 years ago
parent
commit
b957e7c7b9
75 changed files with 689 additions and 201 deletions
  1. 6 1
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  2. 62 9
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  3. 5 1
      MediaBrowser.Api/Playback/StreamRequest.cs
  4. 3 0
      MediaBrowser.Api/Playback/StreamState.cs
  5. 5 2
      MediaBrowser.Api/PlaylistService.cs
  6. 78 4
      MediaBrowser.Api/Subtitles/SubtitleService.cs
  7. 2 0
      MediaBrowser.Api/SystemService.cs
  8. 5 0
      MediaBrowser.Common/Net/MimeTypes.cs
  9. 5 0
      MediaBrowser.Controller/Entities/Audio/Audio.cs
  10. 5 0
      MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
  11. 5 0
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  12. 5 0
      MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
  13. 8 0
      MediaBrowser.Controller/Entities/BaseItem.cs
  14. 8 0
      MediaBrowser.Controller/Entities/LinkedChild.cs
  15. 5 0
      MediaBrowser.Controller/Entities/TV/Season.cs
  16. 5 0
      MediaBrowser.Controller/Entities/TV/Series.cs
  17. 5 0
      MediaBrowser.Controller/Entities/Video.cs
  18. 6 0
      MediaBrowser.Controller/IServerApplicationHost.cs
  19. 1 1
      MediaBrowser.Controller/Library/TVUtils.cs
  20. 4 0
      MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
  21. 2 2
      MediaBrowser.Controller/Playlists/IPlaylistManager.cs
  22. 20 5
      MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs
  23. 52 4
      MediaBrowser.Dlna/Didl/DidlBuilder.cs
  24. 3 2
      MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs
  25. 3 2
      MediaBrowser.Dlna/Profiles/PanasonicVieraProfile.cs
  26. 3 2
      MediaBrowser.Dlna/Profiles/SamsungSmartTvProfile.cs
  27. 8 0
      MediaBrowser.Dlna/Profiles/Windows81Profile.cs
  28. 0 2
      MediaBrowser.Dlna/Profiles/Xml/Android.xml
  29. 0 2
      MediaBrowser.Dlna/Profiles/Xml/Default.xml
  30. 0 2
      MediaBrowser.Dlna/Profiles/Xml/Denon AVR.xml
  31. 0 2
      MediaBrowser.Dlna/Profiles/Xml/LG Smart TV.xml
  32. 0 2
      MediaBrowser.Dlna/Profiles/Xml/Linksys DMA2100.xml
  33. 0 2
      MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml
  34. 3 6
      MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml
  35. 3 6
      MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml
  36. 0 2
      MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml
  37. 0 2
      MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player.xml
  38. 0 2
      MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2010).xml
  39. 0 2
      MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2011).xml
  40. 0 2
      MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2012).xml
  41. 0 2
      MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2013).xml
  42. 0 2
      MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 3.xml
  43. 0 2
      MediaBrowser.Dlna/Profiles/Xml/WDTV Live.xml
  44. 3 2
      MediaBrowser.Dlna/Profiles/Xml/Windows 8 RT.xml
  45. 0 2
      MediaBrowser.Dlna/Profiles/Xml/Windows Phone.xml
  46. 0 2
      MediaBrowser.Dlna/Profiles/Xml/Xbox 360.xml
  47. 0 2
      MediaBrowser.Dlna/Profiles/Xml/Xbox One.xml
  48. 0 2
      MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml
  49. 14 0
      MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
  50. 21 3
      MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
  51. 9 3
      MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs
  52. 1 0
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  53. 28 10
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  54. 59 0
      MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs
  55. 3 2
      MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
  56. 1 5
      MediaBrowser.Model/Dlna/DeviceProfile.cs
  57. 1 1
      MediaBrowser.Model/Dlna/DlnaMaps.cs
  58. 13 2
      MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
  59. 15 41
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  60. 72 8
      MediaBrowser.Model/Dlna/StreamInfo.cs
  61. 10 2
      MediaBrowser.Model/Dlna/SubtitleProfile.cs
  62. 6 6
      MediaBrowser.Model/Dto/BaseItemDto.cs
  63. 1 0
      MediaBrowser.Model/MediaInfo/SubtitleFormat.cs
  64. 1 0
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  65. 47 0
      MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
  66. 3 11
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  67. 14 11
      MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
  68. 2 1
      MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json
  69. 22 2
      MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs
  70. 1 1
      MediaBrowser.Server.Implementations/Udp/UdpServer.cs
  71. 11 1
      MediaBrowser.ServerApplication/ApplicationHost.cs
  72. 2 2
      Nuget/MediaBrowser.Common.Internal.nuspec
  73. 1 1
      Nuget/MediaBrowser.Common.nuspec
  74. 1 1
      Nuget/MediaBrowser.Model.Signed.nuspec
  75. 2 2
      Nuget/MediaBrowser.Server.Core.nuspec

+ 6 - 1
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -1605,6 +1605,8 @@ namespace MediaBrowser.Api.Playback
             {
             {
                 state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
                 state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
             }
             }
+
+            state.AllMediaStreams = mediaStreams;
         }
         }
 
 
         private async Task<MediaSourceInfo> GetChannelMediaInfo(string id,
         private async Task<MediaSourceInfo> GetChannelMediaInfo(string id,
@@ -1640,7 +1642,10 @@ namespace MediaBrowser.Api.Playback
             // Can't stream copy if we're burning in subtitles
             // Can't stream copy if we're burning in subtitles
             if (request.SubtitleStreamIndex.HasValue)
             if (request.SubtitleStreamIndex.HasValue)
             {
             {
-                return false;
+                if (request.SubtitleMethod == SubtitleDeliveryMethod.Encode)
+                {
+                    return false;
+                }
             }
             }
 
 
             // Source and target codecs must match
             // Source and target codecs must match

+ 62 - 9
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -5,6 +5,8 @@ using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using ServiceStack;
 using ServiceStack;
 using System;
 using System;
@@ -18,8 +20,7 @@ using System.Threading.Tasks;
 
 
 namespace MediaBrowser.Api.Playback.Hls
 namespace MediaBrowser.Api.Playback.Hls
 {
 {
-    [Route("/Videos/{Id}/master.m3u8", "GET")]
-    [Api(Description = "Gets a video stream using HTTP live streaming.")]
+    [Route("/Videos/{Id}/master.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
     public class GetMasterHlsVideoStream : VideoStreamRequest
     public class GetMasterHlsVideoStream : VideoStreamRequest
     {
     {
         public bool EnableAdaptiveBitrateStreaming { get; set; }
         public bool EnableAdaptiveBitrateStreaming { get; set; }
@@ -30,8 +31,7 @@ namespace MediaBrowser.Api.Playback.Hls
         }
         }
     }
     }
 
 
-    [Route("/Videos/{Id}/main.m3u8", "GET")]
-    [Api(Description = "Gets a video stream using HTTP live streaming.")]
+    [Route("/Videos/{Id}/main.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
     public class GetMainHlsVideoStream : VideoStreamRequest
     public class GetMainHlsVideoStream : VideoStreamRequest
     {
     {
     }
     }
@@ -359,7 +359,17 @@ namespace MediaBrowser.Api.Playback.Hls
             var playlistUrl = (state.RunTimeTicks ?? 0) > 0 ? "main.m3u8" : "live.m3u8";
             var playlistUrl = (state.RunTimeTicks ?? 0) > 0 ? "main.m3u8" : "live.m3u8";
             playlistUrl += queryString;
             playlistUrl += queryString;
 
 
-            AppendPlaylist(builder, playlistUrl, totalBitrate);
+            var request = (GetMasterHlsVideoStream) state.Request;
+
+            var subtitleStreams = state.AllMediaStreams
+                .Where(i => i.IsTextSubtitleStream)
+                .ToList();
+
+            var subtitleGroup = subtitleStreams.Count > 0 && request.SubtitleMethod == SubtitleDeliveryMethod.Hls ? 
+                "subs" : 
+                null;
+
+            AppendPlaylist(builder, playlistUrl, totalBitrate, subtitleGroup);
 
 
             if (EnableAdaptiveBitrateStreaming(state))
             if (EnableAdaptiveBitrateStreaming(state))
             {
             {
@@ -369,16 +379,52 @@ namespace MediaBrowser.Api.Playback.Hls
                 var variation = GetBitrateVariation(totalBitrate);
                 var variation = GetBitrateVariation(totalBitrate);
 
 
                 var newBitrate = totalBitrate - variation;
                 var newBitrate = totalBitrate - variation;
-                AppendPlaylist(builder, playlistUrl.Replace(requestedVideoBitrate.ToString(UsCulture), (requestedVideoBitrate - variation).ToString(UsCulture)), newBitrate);
+                AppendPlaylist(builder, playlistUrl.Replace(requestedVideoBitrate.ToString(UsCulture), (requestedVideoBitrate - variation).ToString(UsCulture)), newBitrate, subtitleGroup);
 
 
                 variation *= 2;
                 variation *= 2;
                 newBitrate = totalBitrate - variation;
                 newBitrate = totalBitrate - variation;
-                AppendPlaylist(builder, playlistUrl.Replace(requestedVideoBitrate.ToString(UsCulture), (requestedVideoBitrate - variation).ToString(UsCulture)), newBitrate);
+                AppendPlaylist(builder, playlistUrl.Replace(requestedVideoBitrate.ToString(UsCulture), (requestedVideoBitrate - variation).ToString(UsCulture)), newBitrate, subtitleGroup);
+            }
+
+            if (!string.IsNullOrWhiteSpace(subtitleGroup))
+            {
+                AddSubtitles(state, subtitleStreams, builder);
             }
             }
 
 
             return builder.ToString();
             return builder.ToString();
         }
         }
 
 
+        private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
+        {
+            var selectedIndex = state.SubtitleStream == null ? (int?)null : state.SubtitleStream.Index;
+
+            foreach (var stream in subtitles)
+            {
+                const string format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},URI=\"{3}\",LANGUAGE=\"{4}\"";
+
+                var name = stream.Language;
+
+                var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
+                var isForced = stream.IsForced;
+
+                if (string.IsNullOrWhiteSpace(name)) name = stream.Codec ?? "Unknown";
+
+                var url = string.Format("{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}",
+                    state.Request.MediaSourceId,
+                    stream.Index.ToString(UsCulture),
+                    30.ToString(UsCulture));
+
+                var line = string.Format(format,
+                    name,
+                    isDefault ? "YES" : "NO",
+                    isForced ? "YES" : "NO",
+                    url,
+                    stream.Language ?? "Unknown");
+
+                builder.AppendLine(line);
+            }
+        }
+
         private bool EnableAdaptiveBitrateStreaming(StreamState state)
         private bool EnableAdaptiveBitrateStreaming(StreamState state)
         {
         {
             var request = state.Request as GetMasterHlsVideoStream;
             var request = state.Request as GetMasterHlsVideoStream;
@@ -397,9 +443,16 @@ namespace MediaBrowser.Api.Playback.Hls
             return state.VideoRequest.VideoBitRate.HasValue;
             return state.VideoRequest.VideoBitRate.HasValue;
         }
         }
 
 
-        private void AppendPlaylist(StringBuilder builder, string url, int bitrate)
+        private void AppendPlaylist(StringBuilder builder, string url, int bitrate, string subtitleGroup)
         {
         {
-            builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + bitrate.ToString(UsCulture));
+            var header = "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + bitrate.ToString(UsCulture);
+
+            if (!string.IsNullOrWhiteSpace(subtitleGroup))
+            {
+                header += string.Format(",SUBTITLES=\"{0}\"", subtitleGroup);
+            }
+
+            builder.AppendLine(header);
             builder.AppendLine(url);
             builder.AppendLine(url);
         }
         }
 
 

+ 5 - 1
MediaBrowser.Api/Playback/StreamRequest.cs

@@ -1,4 +1,5 @@
-using ServiceStack;
+using MediaBrowser.Model.Dlna;
+using ServiceStack;
 
 
 namespace MediaBrowser.Api.Playback
 namespace MediaBrowser.Api.Playback
 {
 {
@@ -160,6 +161,9 @@ namespace MediaBrowser.Api.Playback
         [ApiMember(Name = "Level", Description = "Optional. Specify a level for the h264 profile, e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         [ApiMember(Name = "Level", Description = "Optional. Specify a level for the h264 profile, e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string Level { get; set; }
         public string Level { get; set; }
 
 
+        [ApiMember(Name = "SubtitleDeliveryMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public SubtitleDeliveryMethod SubtitleMethod { get; set; }
+        
         /// <summary>
         /// <summary>
         /// Gets a value indicating whether this instance has fixed resolution.
         /// Gets a value indicating whether this instance has fixed resolution.
         /// </summary>
         /// </summary>

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

@@ -38,6 +38,8 @@ namespace MediaBrowser.Api.Playback
 
 
         public string InputContainer { get; set; }
         public string InputContainer { get; set; }
 
 
+        public List<MediaStream> AllMediaStreams { get; set; }
+        
         public MediaStream AudioStream { get; set; }
         public MediaStream AudioStream { get; set; }
         public MediaStream VideoStream { get; set; }
         public MediaStream VideoStream { get; set; }
         public MediaStream SubtitleStream { get; set; }
         public MediaStream SubtitleStream { get; set; }
@@ -78,6 +80,7 @@ namespace MediaBrowser.Api.Playback
             SupportedAudioCodecs = new List<string>();
             SupportedAudioCodecs = new List<string>();
             PlayableStreamFileNames = new List<string>();
             PlayableStreamFileNames = new List<string>();
             RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
             RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            AllMediaStreams = new List<MediaStream>();
         }
         }
 
 
         public string InputAudioSync { get; set; }
         public string InputAudioSync { get; set; }

+ 5 - 2
MediaBrowser.Api/PlaylistService.cs

@@ -41,6 +41,9 @@ namespace MediaBrowser.Api
     {
     {
         [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
         [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
         public string Id { get; set; }
         public string Id { get; set; }
+
+        [ApiMember(Name = "EntryIds", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
+        public string EntryIds { get; set; }
     }
     }
 
 
     [Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")]
     [Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")]
@@ -122,9 +125,9 @@ namespace MediaBrowser.Api
 
 
         public void Delete(RemoveFromPlaylist request)
         public void Delete(RemoveFromPlaylist request)
         {
         {
-            //var task = _playlistManager.RemoveFromPlaylist(request.Id, request.Ids.Split(',').Select(i => new Guid(i)));
+            var task = _playlistManager.RemoveFromPlaylist(request.Id, request.EntryIds.Split(','));
 
 
-            //Task.WaitAll(task);
+            Task.WaitAll(task);
         }
         }
 
 
         public object Get(GetPlaylistItems request)
         public object Get(GetPlaylistItems request)

+ 78 - 4
MediaBrowser.Api/Subtitles/SubtitleService.cs

@@ -1,6 +1,4 @@
-using System.IO;
-using System.Linq;
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
@@ -11,6 +9,10 @@ using MediaBrowser.Model.Providers;
 using ServiceStack;
 using ServiceStack;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
@@ -69,7 +71,8 @@ namespace MediaBrowser.Api.Subtitles
         public string Id { get; set; }
         public string Id { get; set; }
     }
     }
 
 
-    [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format (vtt).")]
+    [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")]
+    [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/{StartPositionTicks}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")]
     public class GetSubtitle
     public class GetSubtitle
     {
     {
         /// <summary>
         /// <summary>
@@ -90,6 +93,29 @@ namespace MediaBrowser.Api.Subtitles
 
 
         [ApiMember(Name = "StartPositionTicks", Description = "StartPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         [ApiMember(Name = "StartPositionTicks", Description = "StartPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public long StartPositionTicks { get; set; }
         public long StartPositionTicks { get; set; }
+
+        [ApiMember(Name = "EndPositionTicks", Description = "EndPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public long? EndPositionTicks { get; set; }
+    }
+
+    [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/subtitles.m3u8", "GET", Summary = "Gets an HLS subtitle playlist.")]
+    public class GetSubtitlePlaylist
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+
+        [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string MediaSourceId { get; set; }
+
+        [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
+        public int Index { get; set; }
+
+        [ApiMember(Name = "SegmentLength", Description = "The subtitle srgment length", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
+        public int SegmentLength { get; set; }
     }
     }
 
 
     public class SubtitleService : BaseApiService
     public class SubtitleService : BaseApiService
@@ -105,6 +131,53 @@ namespace MediaBrowser.Api.Subtitles
             _subtitleEncoder = subtitleEncoder;
             _subtitleEncoder = subtitleEncoder;
         }
         }
 
 
+        public object Get(GetSubtitlePlaylist request)
+        {
+            var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
+
+            var mediaSource = item.GetMediaSources(false)
+                .First(i => string.Equals(i.Id, request.MediaSourceId ?? request.Id));
+
+            var builder = new StringBuilder();
+
+            var runtime = mediaSource.RunTimeTicks ?? -1;
+
+            if (runtime <= 0)
+            {
+                throw new ArgumentException("HLS Subtitles are not supported for this media.");
+            }
+
+            builder.AppendLine("#EXTM3U");
+            builder.AppendLine("#EXT-X-TARGETDURATION:" + request.SegmentLength.ToString(CultureInfo.InvariantCulture));
+            builder.AppendLine("#EXT-X-VERSION:3");
+            builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
+
+            long positionTicks = 0;
+            var segmentLengthTicks = TimeSpan.FromSeconds(request.SegmentLength).Ticks;
+
+            while (positionTicks < runtime)
+            {
+                var remaining = runtime - positionTicks;
+                var lengthTicks = Math.Min(remaining, segmentLengthTicks);
+
+                builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
+
+                var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
+
+                var url = string.Format("stream.srt?StartPositionTicks={0}&EndPositionTicks={1}",
+                    positionTicks.ToString(CultureInfo.InvariantCulture),
+                    endPositionTicks.ToString(CultureInfo.InvariantCulture));
+
+                builder.AppendLine(url);
+
+                positionTicks += segmentLengthTicks;
+            }
+
+            builder.AppendLine("#EXT-X-ENDLIST");
+            
+            return ResultFactory.GetResult(builder.ToString(), Common.Net.MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
+        }
+
         public object Get(GetSubtitle request)
         public object Get(GetSubtitle request)
         {
         {
             if (string.IsNullOrEmpty(request.Format))
             if (string.IsNullOrEmpty(request.Format))
@@ -132,6 +205,7 @@ namespace MediaBrowser.Api.Subtitles
                 request.Index,
                 request.Index,
                 request.Format,
                 request.Format,
                 request.StartPositionTicks,
                 request.StartPositionTicks,
+                request.EndPositionTicks,
                 CancellationToken.None).ConfigureAwait(false);
                 CancellationToken.None).ConfigureAwait(false);
         }
         }
 
 

+ 2 - 0
MediaBrowser.Api/SystemService.cs

@@ -71,6 +71,8 @@ namespace MediaBrowser.Api
         /// Initializes a new instance of the <see cref="SystemService" /> class.
         /// Initializes a new instance of the <see cref="SystemService" /> class.
         /// </summary>
         /// </summary>
         /// <param name="appHost">The app host.</param>
         /// <param name="appHost">The app host.</param>
+        /// <param name="appPaths">The application paths.</param>
+        /// <param name="fileSystem">The file system.</param>
         /// <exception cref="System.ArgumentNullException">jsonSerializer</exception>
         /// <exception cref="System.ArgumentNullException">jsonSerializer</exception>
         public SystemService(IServerApplicationHost appHost, IApplicationPaths appPaths, IFileSystem fileSystem)
         public SystemService(IServerApplicationHost appHost, IApplicationPaths appPaths, IFileSystem fileSystem)
         {
         {

+ 5 - 0
MediaBrowser.Common/Net/MimeTypes.cs

@@ -236,6 +236,11 @@ namespace MediaBrowser.Common.Net
                 return "text/vtt";
                 return "text/vtt";
             }
             }
 
 
+            if (ext.Equals(".ttml", StringComparison.OrdinalIgnoreCase))
+            {
+                return "application/ttml+xml";
+            }
+
             if (ext.Equals(".bif", StringComparison.OrdinalIgnoreCase))
             if (ext.Equals(".bif", StringComparison.OrdinalIgnoreCase))
             {
             {
                 return "application/octet-stream";
                 return "application/octet-stream";

+ 5 - 0
MediaBrowser.Controller/Entities/Audio/Audio.cs

@@ -34,6 +34,11 @@ namespace MediaBrowser.Controller.Entities.Audio
             Tags = new List<string>();
             Tags = new List<string>();
         }
         }
 
 
+        public override bool SupportsAddingToPlaylist
+        {
+            get { return LocationType == LocationType.FileSystem && RunTimeTicks.HasValue; }
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether this instance has embedded image.
         /// Gets or sets a value indicating whether this instance has embedded image.
         /// </summary>
         /// </summary>

+ 5 - 0
MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs

@@ -21,6 +21,11 @@ namespace MediaBrowser.Controller.Entities.Audio
             SoundtrackIds = new List<Guid>();
             SoundtrackIds = new List<Guid>();
         }
         }
 
 
+        public override bool SupportsAddingToPlaylist
+        {
+            get { return true; }
+        }
+
         [IgnoreDataMember]
         [IgnoreDataMember]
         public MusicArtist MusicArtist
         public MusicArtist MusicArtist
         {
         {

+ 5 - 0
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -26,6 +26,11 @@ namespace MediaBrowser.Controller.Entities.Audio
             }
             }
         }
         }
 
 
+        public override bool SupportsAddingToPlaylist
+        {
+            get { return true; }
+        }
+
         protected override IEnumerable<BaseItem> ActualChildren
         protected override IEnumerable<BaseItem> ActualChildren
         {
         {
             get
             get

+ 5 - 0
MediaBrowser.Controller/Entities/Audio/MusicGenre.cs

@@ -18,6 +18,11 @@ namespace MediaBrowser.Controller.Entities.Audio
             return "MusicGenre-" + Name;
             return "MusicGenre-" + Name;
         }
         }
 
 
+        public override bool SupportsAddingToPlaylist
+        {
+            get { return true; }
+        }
+
         /// <summary>
         /// <summary>
         /// Returns the folder containing the item.
         /// Returns the folder containing the item.
         /// If the item is a folder, it returns the folder itself
         /// If the item is a folder, it returns the folder itself

+ 8 - 0
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -52,6 +52,14 @@ namespace MediaBrowser.Controller.Entities
 
 
         public List<ItemImageInfo> ImageInfos { get; set; }
         public List<ItemImageInfo> ImageInfos { get; set; }
 
 
+        public virtual bool SupportsAddingToPlaylist
+        {
+            get
+            {
+                return false;
+            }
+        }
+
         /// <summary>
         /// <summary>
         /// Gets a value indicating whether this instance is in mixed folder.
         /// Gets a value indicating whether this instance is in mixed folder.
         /// </summary>
         /// </summary>

+ 8 - 0
MediaBrowser.Controller/Entities/LinkedChild.cs

@@ -13,6 +13,9 @@ namespace MediaBrowser.Controller.Entities
         public string ItemType { get; set; }
         public string ItemType { get; set; }
         public int? ItemYear { get; set; }
         public int? ItemYear { get; set; }
 
 
+        [IgnoreDataMember]
+        public string Id { get; set; }
+
         /// <summary>
         /// <summary>
         /// Serves as a cache
         /// Serves as a cache
         /// </summary>
         /// </summary>
@@ -27,6 +30,11 @@ namespace MediaBrowser.Controller.Entities
                 Type = LinkedChildType.Manual
                 Type = LinkedChildType.Manual
             };
             };
         }
         }
+
+        public LinkedChild()
+        {
+            Id = Guid.NewGuid().ToString("N");
+        }
     }
     }
 
 
     public enum LinkedChildType
     public enum LinkedChildType

+ 5 - 0
MediaBrowser.Controller/Entities/TV/Season.cs

@@ -29,6 +29,11 @@ namespace MediaBrowser.Controller.Entities.TV
             }
             }
         }
         }
 
 
+        public override bool SupportsAddingToPlaylist
+        {
+            get { return true; }
+        }
+
         [IgnoreDataMember]
         [IgnoreDataMember]
         public override bool IsPreSorted
         public override bool IsPreSorted
         {
         {

+ 5 - 0
MediaBrowser.Controller/Entities/TV/Series.cs

@@ -39,6 +39,11 @@ namespace MediaBrowser.Controller.Entities.TV
             DisplaySpecialsWithSeasons = true;
             DisplaySpecialsWithSeasons = true;
         }
         }
 
 
+        public override bool SupportsAddingToPlaylist
+        {
+            get { return true; }
+        }
+
         [IgnoreDataMember]
         [IgnoreDataMember]
         public override bool IsPreSorted
         public override bool IsPreSorted
         {
         {

+ 5 - 0
MediaBrowser.Controller/Entities/Video.cs

@@ -55,6 +55,11 @@ namespace MediaBrowser.Controller.Entities
             LinkedAlternateVersions = new List<LinkedChild>();
             LinkedAlternateVersions = new List<LinkedChild>();
         }
         }
 
 
+        public override bool SupportsAddingToPlaylist
+        {
+            get { return LocationType == LocationType.FileSystem && RunTimeTicks.HasValue; }
+        }
+
         [IgnoreDataMember]
         [IgnoreDataMember]
         public int MediaSourceCount
         public int MediaSourceCount
         {
         {

+ 6 - 0
MediaBrowser.Controller/IServerApplicationHost.cs

@@ -46,5 +46,11 @@ namespace MediaBrowser.Controller
         /// </summary>
         /// </summary>
         /// <value>The server identifier.</value>
         /// <value>The server identifier.</value>
         string ServerId { get; }
         string ServerId { get; }
+
+        /// <summary>
+        /// Gets the name of the friendly.
+        /// </summary>
+        /// <value>The name of the friendly.</value>
+        string FriendlyName { get; }
     }
     }
 }
 }

+ 1 - 1
MediaBrowser.Controller/Library/TVUtils.cs

@@ -126,7 +126,7 @@ namespace MediaBrowser.Controller.Library
         {
         {
             var filename = Path.GetFileName(path);
             var filename = Path.GetFileName(path);
 
 
-            if (string.Equals(path, "specials", StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase))
             {
             {
                 return 0;
                 return 0;
             }
             }

+ 4 - 0
MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs

@@ -13,6 +13,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <param name="inputFormat">The input format.</param>
         /// <param name="inputFormat">The input format.</param>
         /// <param name="outputFormat">The output format.</param>
         /// <param name="outputFormat">The output format.</param>
         /// <param name="startTimeTicks">The start time ticks.</param>
         /// <param name="startTimeTicks">The start time ticks.</param>
+        /// <param name="endTimeTicks">The end time ticks.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{Stream}.</returns>
         /// <returns>Task{Stream}.</returns>
         Task<Stream> ConvertSubtitles(
         Task<Stream> ConvertSubtitles(
@@ -20,6 +21,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             string inputFormat,
             string inputFormat,
             string outputFormat,
             string outputFormat,
             long startTimeTicks,
             long startTimeTicks,
+            long? endTimeTicks,
             CancellationToken cancellationToken);
             CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
@@ -30,6 +32,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
         /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
         /// <param name="outputFormat">The output format.</param>
         /// <param name="outputFormat">The output format.</param>
         /// <param name="startTimeTicks">The start time ticks.</param>
         /// <param name="startTimeTicks">The start time ticks.</param>
+        /// <param name="endTimeTicks">The end time ticks.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{Stream}.</returns>
         /// <returns>Task{Stream}.</returns>
         Task<Stream> GetSubtitles(string itemId,
         Task<Stream> GetSubtitles(string itemId,
@@ -37,6 +40,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             int subtitleStreamIndex,
             int subtitleStreamIndex,
             string outputFormat,
             string outputFormat,
             long startTimeTicks,
             long startTimeTicks,
+            long? endTimeTicks,
             CancellationToken cancellationToken);
             CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>

+ 2 - 2
MediaBrowser.Controller/Playlists/IPlaylistManager.cs

@@ -32,9 +32,9 @@ namespace MediaBrowser.Controller.Playlists
         /// Removes from playlist.
         /// Removes from playlist.
         /// </summary>
         /// </summary>
         /// <param name="playlistId">The playlist identifier.</param>
         /// <param name="playlistId">The playlist identifier.</param>
-        /// <param name="indeces">The indeces.</param>
+        /// <param name="entryIds">The entry ids.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
-        Task RemoveFromPlaylist(string playlistId, IEnumerable<int> indeces);
+        Task RemoveFromPlaylist(string playlistId, IEnumerable<string> entryIds);
 
 
         /// <summary>
         /// <summary>
         /// Gets the playlists folder.
         /// Gets the playlists folder.

+ 20 - 5
MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs

@@ -183,19 +183,34 @@ namespace MediaBrowser.Dlna.ContentDirectory
             //didl.SetAttribute("xmlns:sec", NS_SEC);
             //didl.SetAttribute("xmlns:sec", NS_SEC);
             result.AppendChild(didl);
             result.AppendChild(didl);
 
 
-            var folder = (Folder)GetItemFromObjectId(id, user);
-
-            var childrenResult = (await GetChildrenSorted(folder, user, sortCriteria, start, requested).ConfigureAwait(false));
+            var item = GetItemFromObjectId(id, user);
 
 
-            var totalCount = childrenResult.TotalRecordCount;
+            var totalCount = 0;
 
 
             if (string.Equals(flag, "BrowseMetadata"))
             if (string.Equals(flag, "BrowseMetadata"))
             {
             {
-                result.DocumentElement.AppendChild(_didlBuilder.GetFolderElement(result, folder, totalCount, filter));
+                var folder = item as Folder;
+
+                if (folder == null)
+                {
+                    result.DocumentElement.AppendChild(_didlBuilder.GetItemElement(result, item, deviceId, filter));
+                }
+                else
+                {
+                    var childrenResult = (await GetChildrenSorted(folder, user, sortCriteria, start, requested).ConfigureAwait(false));
+                    totalCount = childrenResult.TotalRecordCount;
+
+                    result.DocumentElement.AppendChild(_didlBuilder.GetFolderElement(result, folder, totalCount, filter));
+                }
                 provided++;
                 provided++;
             }
             }
             else
             else
             {
             {
+                var folder = (Folder)item;
+
+                var childrenResult = (await GetChildrenSorted(folder, user, sortCriteria, start, requested).ConfigureAwait(false));
+                totalCount = childrenResult.TotalRecordCount;
+
                 provided = childrenResult.Items.Length;
                 provided = childrenResult.Items.Length;
 
 
                 foreach (var i in childrenResult.Items)
                 foreach (var i in childrenResult.Items)

+ 52 - 4
MediaBrowser.Dlna/Didl/DidlBuilder.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Net;
+using System.IO;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -13,6 +14,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Xml;
 using System.Xml;
+using MediaBrowser.Common.Extensions;
 
 
 namespace MediaBrowser.Dlna.Didl
 namespace MediaBrowser.Dlna.Didl
 {
 {
@@ -101,7 +103,7 @@ namespace MediaBrowser.Dlna.Didl
             {
             {
                 var sources = _user == null ? video.GetMediaSources(true).ToList() : video.GetMediaSources(true, _user).ToList();
                 var sources = _user == null ? video.GetMediaSources(true).ToList() : video.GetMediaSources(true, _user).ToList();
 
 
-                streamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
+               streamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
                {
                {
                    ItemId = video.Id.ToString("N"),
                    ItemId = video.Id.ToString("N"),
                    MediaSources = sources,
                    MediaSources = sources,
@@ -137,6 +139,23 @@ namespace MediaBrowser.Dlna.Didl
             {
             {
                 AddVideoResource(container, video, deviceId, filter, contentFeature, streamInfo);
                 AddVideoResource(container, video, deviceId, filter, contentFeature, streamInfo);
             }
             }
+
+            foreach (var subtitle in streamInfo.GetExternalSubtitles(_serverAddress))
+            {
+                AddSubtitleElement(container, subtitle);
+            }
+        }
+
+        private void AddSubtitleElement(XmlElement container, SubtitleStreamInfo info)
+        {
+            var res = container.OwnerDocument.CreateElement(string.Empty, "res", NS_DIDL);
+
+            res.InnerText = info.Url;
+
+            // TODO: Remove this hard-coding
+            res.SetAttribute("protocolInfo", "http-get:*:text/srt:*");
+
+            container.AppendChild(res);
         }
         }
 
 
         private void AddVideoResource(XmlElement container, Video video, string deviceId, Filter filter, string contentFeatures, StreamInfo streamInfo)
         private void AddVideoResource(XmlElement container, Video video, string deviceId, Filter filter, string contentFeatures, StreamInfo streamInfo)
@@ -598,9 +617,11 @@ namespace MediaBrowser.Dlna.Didl
             }
             }
 
 
             AddImageResElement(item, element, 4096, 4096, "jpg");
             AddImageResElement(item, element, 4096, 4096, "jpg");
+            AddImageResElement(item, element, 4096, 4096, "png");
             AddImageResElement(item, element, 1024, 768, "jpg");
             AddImageResElement(item, element, 1024, 768, "jpg");
             AddImageResElement(item, element, 640, 480, "jpg");
             AddImageResElement(item, element, 640, 480, "jpg");
             AddImageResElement(item, element, 160, 160, "jpg");
             AddImageResElement(item, element, 160, 160, "jpg");
+            AddImageResElement(item, element, 160, 160, "png");
         }
         }
 
 
         private void AddImageResElement(BaseItem item, XmlElement element, int maxWidth, int maxHeight, string format)
         private void AddImageResElement(BaseItem item, XmlElement element, int maxWidth, int maxHeight, string format)
@@ -623,7 +644,7 @@ namespace MediaBrowser.Dlna.Didl
             var width = albumartUrlInfo.Width;
             var width = albumartUrlInfo.Width;
             var height = albumartUrlInfo.Height;
             var height = albumartUrlInfo.Height;
 
 
-            var contentFeatures = new ContentFeatureBuilder(_profile).BuildImageHeader(format, width, height);
+            var contentFeatures = new ContentFeatureBuilder(_profile).BuildImageHeader(format, width, height, imageInfo.IsDirectStream);
 
 
             res.SetAttribute("protocolInfo", String.Format(
             res.SetAttribute("protocolInfo", String.Format(
                 "http-get:*:{0}:{1}",
                 "http-get:*:{0}:{1}",
@@ -631,6 +652,14 @@ namespace MediaBrowser.Dlna.Didl
                 contentFeatures
                 contentFeatures
                 ));
                 ));
 
 
+            res.SetAttribute("colorDepth", "24");
+
+            if (imageInfo.IsDirectStream)
+            {
+                // TODO: Add file size
+                //res.SetAttribute("size", imageInfo.Size.Value.ToString(_usCulture));
+            }
+
             if (width.HasValue && height.HasValue)
             if (width.HasValue && height.HasValue)
             {
             {
                 res.SetAttribute("resolution", string.Format("{0}x{1}", width.Value, height.Value));
                 res.SetAttribute("resolution", string.Format("{0}x{1}", width.Value, height.Value));
@@ -705,7 +734,8 @@ namespace MediaBrowser.Dlna.Didl
                 Type = type,
                 Type = type,
                 ImageTag = tag,
                 ImageTag = tag,
                 Width = width,
                 Width = width,
-                Height = height
+                Height = height,
+                File = imageInfo.Path
             };
             };
         }
         }
 
 
@@ -717,6 +747,10 @@ namespace MediaBrowser.Dlna.Didl
 
 
             internal int? Width;
             internal int? Width;
             internal int? Height;
             internal int? Height;
+
+            internal bool IsDirectStream;
+
+            internal string File;
         }
         }
 
 
         class ImageUrlInfo
         class ImageUrlInfo
@@ -741,6 +775,8 @@ namespace MediaBrowser.Dlna.Didl
             var width = info.Width;
             var width = info.Width;
             var height = info.Height;
             var height = info.Height;
 
 
+            info.IsDirectStream = false;
+
             if (width.HasValue && height.HasValue)
             if (width.HasValue && height.HasValue)
             {
             {
                 var newSize = DrawingUtils.Resize(new ImageSize
                 var newSize = DrawingUtils.Resize(new ImageSize
@@ -752,6 +788,18 @@ namespace MediaBrowser.Dlna.Didl
 
 
                 width = Convert.ToInt32(newSize.Width);
                 width = Convert.ToInt32(newSize.Width);
                 height = Convert.ToInt32(newSize.Height);
                 height = Convert.ToInt32(newSize.Height);
+
+                var inputFormat = (Path.GetExtension(info.File) ?? string.Empty)
+                    .TrimStart('.')
+                    .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
+
+                var normalizedFormat = format
+                    .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
+
+                if (string.Equals(inputFormat, normalizedFormat, StringComparison.OrdinalIgnoreCase))
+                {
+                    info.IsDirectStream = maxWidth >= width.Value && maxHeight >= height.Value;
+                }
             }
             }
 
 
             return new ImageUrlInfo
             return new ImageUrlInfo

+ 3 - 2
MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Session;
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
@@ -29,7 +30,7 @@ namespace MediaBrowser.Dlna.PlayTo
 
 
             if (directPlay != null)
             if (directPlay != null)
             {
             {
-                playlistItem.StreamInfo.IsDirectStream = true;
+                playlistItem.StreamInfo.PlayMethod = PlayMethod.DirectStream;
                 playlistItem.StreamInfo.Container = Path.GetExtension(item.Path);
                 playlistItem.StreamInfo.Container = Path.GetExtension(item.Path);
 
 
                 return playlistItem;
                 return playlistItem;
@@ -40,7 +41,7 @@ namespace MediaBrowser.Dlna.PlayTo
 
 
             if (transcodingProfile != null)
             if (transcodingProfile != null)
             {
             {
-                playlistItem.StreamInfo.IsDirectStream = true;
+                playlistItem.StreamInfo.PlayMethod = PlayMethod.Transcode;
                 playlistItem.StreamInfo.Container = "." + transcodingProfile.Container.TrimStart('.');
                 playlistItem.StreamInfo.Container = "." + transcodingProfile.Container.TrimStart('.');
             }
             }
 
 

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

@@ -193,11 +193,12 @@ namespace MediaBrowser.Dlna.Profiles
                }
                }
            };
            };
 
 
-            SoftSubtitleProfiles = new[]
+            SubtitleProfiles = new[]
             {
             {
                 new SubtitleProfile
                 new SubtitleProfile
                 {
                 {
-                    Format = "srt"
+                    Format = "srt",
+                    Method = SubtitleDeliveryMethod.External
                 }
                 }
             };
             };
         }
         }

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

@@ -339,11 +339,12 @@ namespace MediaBrowser.Dlna.Profiles
                 }
                 }
             };
             };
 
 
-            SoftSubtitleProfiles = new[]
+            SubtitleProfiles = new[]
             {
             {
                 new SubtitleProfile
                 new SubtitleProfile
                 {
                 {
-                    Format = "smi"
+                    Format = "smi",
+                    Method = SubtitleDeliveryMethod.External
                 }
                 }
             };
             };
         }
         }

+ 8 - 0
MediaBrowser.Dlna/Profiles/Windows81Profile.cs

@@ -155,6 +155,14 @@ namespace MediaBrowser.Dlna.Profiles
                 }
                 }
             };
             };
 
 
+            SubtitleProfiles = new[]
+            {
+                new SubtitleProfile
+                {
+                    Format = "ttml",
+                    Method = SubtitleDeliveryMethod.External
+                }
+            };
         }
         }
     }
     }
 }
 }

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/Android.xml

@@ -65,6 +65,4 @@
     </CodecProfile>
     </CodecProfile>
   </CodecProfiles>
   </CodecProfiles>
   <ResponseProfiles />
   <ResponseProfiles />
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/Default.xml

@@ -35,6 +35,4 @@
   <ContainerProfiles />
   <ContainerProfiles />
   <CodecProfiles />
   <CodecProfiles />
   <ResponseProfiles />
   <ResponseProfiles />
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/Denon AVR.xml

@@ -39,6 +39,4 @@
   <ContainerProfiles />
   <ContainerProfiles />
   <CodecProfiles />
   <CodecProfiles />
   <ResponseProfiles />
   <ResponseProfiles />
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/LG Smart TV.xml

@@ -73,6 +73,4 @@
     </CodecProfile>
     </CodecProfile>
   </CodecProfiles>
   </CodecProfiles>
   <ResponseProfiles />
   <ResponseProfiles />
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/Linksys DMA2100.xml

@@ -39,6 +39,4 @@
   <ContainerProfiles />
   <ContainerProfiles />
   <CodecProfiles />
   <CodecProfiles />
   <ResponseProfiles />
   <ResponseProfiles />
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml

@@ -45,6 +45,4 @@
   <ContainerProfiles />
   <ContainerProfiles />
   <CodecProfiles />
   <CodecProfiles />
   <ResponseProfiles />
   <ResponseProfiles />
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 3 - 6
MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml

@@ -68,10 +68,7 @@
     </CodecProfile>
     </CodecProfile>
   </CodecProfiles>
   </CodecProfiles>
   <ResponseProfiles />
   <ResponseProfiles />
-  <SoftSubtitleProfiles>
-    <SubtitleProfile>
-      <Format>srt</Format>
-    </SubtitleProfile>
-  </SoftSubtitleProfiles>
-  <ExternalSubtitleProfiles />
+  <SubtitleProfiles>
+    <SubtitleProfile format="srt" method="External" />
+  </SubtitleProfiles>
 </Profile>
 </Profile>

+ 3 - 6
MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml

@@ -106,10 +106,7 @@
       <Conditions />
       <Conditions />
     </ResponseProfile>
     </ResponseProfile>
   </ResponseProfiles>
   </ResponseProfiles>
-  <SoftSubtitleProfiles>
-    <SubtitleProfile>
-      <Format>smi</Format>
-    </SubtitleProfile>
-  </SoftSubtitleProfiles>
-  <ExternalSubtitleProfiles />
+  <SubtitleProfiles>
+    <SubtitleProfile format="smi" method="External" />
+  </SubtitleProfiles>
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml

@@ -71,6 +71,4 @@
     </CodecProfile>
     </CodecProfile>
   </CodecProfiles>
   </CodecProfiles>
   <ResponseProfiles />
   <ResponseProfiles />
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player.xml

@@ -99,6 +99,4 @@
       <Conditions />
       <Conditions />
     </ResponseProfile>
     </ResponseProfile>
   </ResponseProfiles>
   </ResponseProfiles>
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2010).xml

@@ -107,6 +107,4 @@
       <Conditions />
       <Conditions />
     </ResponseProfile>
     </ResponseProfile>
   </ResponseProfiles>
   </ResponseProfiles>
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2011).xml

@@ -110,6 +110,4 @@
       <Conditions />
       <Conditions />
     </ResponseProfile>
     </ResponseProfile>
   </ResponseProfiles>
   </ResponseProfiles>
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2012).xml

@@ -93,6 +93,4 @@
       <Conditions />
       <Conditions />
     </ResponseProfile>
     </ResponseProfile>
   </ResponseProfiles>
   </ResponseProfiles>
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2013).xml

@@ -93,6 +93,4 @@
       <Conditions />
       <Conditions />
     </ResponseProfile>
     </ResponseProfile>
   </ResponseProfiles>
   </ResponseProfiles>
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 3.xml

@@ -93,6 +93,4 @@
       <Conditions />
       <Conditions />
     </ResponseProfile>
     </ResponseProfile>
   </ResponseProfiles>
   </ResponseProfiles>
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/WDTV Live.xml

@@ -78,6 +78,4 @@
       <Conditions />
       <Conditions />
     </ResponseProfile>
     </ResponseProfile>
   </ResponseProfiles>
   </ResponseProfiles>
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 3 - 2
MediaBrowser.Dlna/Profiles/Xml/Windows 8 RT.xml

@@ -62,6 +62,7 @@
     </CodecProfile>
     </CodecProfile>
   </CodecProfiles>
   </CodecProfiles>
   <ResponseProfiles />
   <ResponseProfiles />
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
+  <SubtitleProfiles>
+    <SubtitleProfile format="ttml" method="External" />
+  </SubtitleProfiles>
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/Windows Phone.xml

@@ -76,6 +76,4 @@
     </CodecProfile>
     </CodecProfile>
   </CodecProfiles>
   </CodecProfiles>
   <ResponseProfiles />
   <ResponseProfiles />
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/Xbox 360.xml

@@ -100,6 +100,4 @@
       <Conditions />
       <Conditions />
     </ResponseProfile>
     </ResponseProfile>
   </ResponseProfiles>
   </ResponseProfiles>
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/Xbox One.xml

@@ -90,6 +90,4 @@
       <Conditions />
       <Conditions />
     </ResponseProfile>
     </ResponseProfile>
   </ResponseProfiles>
   </ResponseProfiles>
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 0 - 2
MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml

@@ -45,6 +45,4 @@
   <ContainerProfiles />
   <ContainerProfiles />
   <CodecProfiles />
   <CodecProfiles />
   <ResponseProfiles />
   <ResponseProfiles />
-  <SoftSubtitleProfiles />
-  <ExternalSubtitleProfiles />
 </Profile>
 </Profile>

+ 14 - 0
MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs

@@ -18,6 +18,20 @@ namespace MediaBrowser.LocalMetadata.Parsers
         {
         {
             switch (reader.Name)
             switch (reader.Name)
             {
             {
+                case "OwnerUserId":
+                    {
+                        item.OwnerUserId = reader.ReadElementContentAsString();
+
+                        break;
+                    }
+
+                case "PlaylistMediaType":
+                    {
+                        item.PlaylistMediaType = reader.ReadElementContentAsString();
+
+                        break;
+                    }
+
                 case "PlaylistItems":
                 case "PlaylistItems":
 
 
                     using (var subReader = reader.ReadSubtree())
                     using (var subReader = reader.ReadSubtree())

+ 21 - 3
MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Entities;
+using System.Security;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Playlists;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -42,17 +43,34 @@ namespace MediaBrowser.LocalMetadata.Savers
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         public void Save(IHasMetadata item, CancellationToken cancellationToken)
         public void Save(IHasMetadata item, CancellationToken cancellationToken)
         {
         {
+            var playlist = (Playlist)item;
+
             var builder = new StringBuilder();
             var builder = new StringBuilder();
 
 
             builder.Append("<Item>");
             builder.Append("<Item>");
 
 
-            XmlSaverHelpers.AddCommonNodes((Playlist)item, builder);
+            if (!string.IsNullOrEmpty(playlist.OwnerUserId))
+            {
+                builder.Append("<OwnerUserId>" + SecurityElement.Escape(playlist.OwnerUserId) + "</OwnerUserId>");
+            }
+
+            if (!string.IsNullOrEmpty(playlist.PlaylistMediaType))
+            {
+                builder.Append("<PlaylistMediaType>" + SecurityElement.Escape(playlist.PlaylistMediaType) + "</PlaylistMediaType>");
+            }
+            
+            XmlSaverHelpers.AddCommonNodes(playlist, builder);
 
 
             builder.Append("</Item>");
             builder.Append("</Item>");
 
 
             var xmlFilePath = GetSavePath(item);
             var xmlFilePath = GetSavePath(item);
 
 
-            XmlSaverHelpers.Save(builder, xmlFilePath, new List<string> { });
+            XmlSaverHelpers.Save(builder, xmlFilePath, new List<string>
+            {
+                "OwnerUserId",
+                "PlaylistMediaType"
+
+            });
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 9 - 3
MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs

@@ -704,7 +704,7 @@ namespace MediaBrowser.LocalMetadata.Savers
         public static void AddLinkedChildren(Folder item, StringBuilder builder, string pluralNodeName, string singularNodeName)
         public static void AddLinkedChildren(Folder item, StringBuilder builder, string pluralNodeName, string singularNodeName)
         {
         {
             var items = item.LinkedChildren
             var items = item.LinkedChildren
-                .Where(i => i.Type == LinkedChildType.Manual && !string.IsNullOrWhiteSpace(i.ItemName))
+                .Where(i => i.Type == LinkedChildType.Manual)
                 .ToList();
                 .ToList();
 
 
             if (items.Count == 0)
             if (items.Count == 0)
@@ -717,14 +717,20 @@ namespace MediaBrowser.LocalMetadata.Savers
             {
             {
                 builder.Append("<" + singularNodeName + ">");
                 builder.Append("<" + singularNodeName + ">");
 
 
-                builder.Append("<Type>" + SecurityElement.Escape(link.ItemType) + "</Type>");
+                if (!string.IsNullOrWhiteSpace(link.ItemType))
+                {
+                    builder.Append("<Type>" + SecurityElement.Escape(link.ItemType) + "</Type>");
+                }
 
 
                 if (link.ItemYear.HasValue)
                 if (link.ItemYear.HasValue)
                 {
                 {
                     builder.Append("<Year>" + SecurityElement.Escape(link.ItemYear.Value.ToString(UsCulture)) + "</Year>");
                     builder.Append("<Year>" + SecurityElement.Escape(link.ItemYear.Value.ToString(UsCulture)) + "</Year>");
                 }
                 }
 
 
-                builder.Append("<Path>" + SecurityElement.Escape((link.Path ?? string.Empty)) + "</Path>");
+                if (!string.IsNullOrWhiteSpace(link.Path))
+                {
+                    builder.Append("<Path>" + SecurityElement.Escape((link.Path)) + "</Path>");
+                }
 
 
                 builder.Append("</" + singularNodeName + ">");
                 builder.Append("</" + singularNodeName + ">");
             }
             }

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

@@ -66,6 +66,7 @@
     <Compile Include="Subtitles\SsaParser.cs" />
     <Compile Include="Subtitles\SsaParser.cs" />
     <Compile Include="Subtitles\SubtitleEncoder.cs" />
     <Compile Include="Subtitles\SubtitleEncoder.cs" />
     <Compile Include="Subtitles\SubtitleTrackInfo.cs" />
     <Compile Include="Subtitles\SubtitleTrackInfo.cs" />
+    <Compile Include="Subtitles\TtmlWriter.cs" />
     <Compile Include="Subtitles\VttWriter.cs" />
     <Compile Include="Subtitles\VttWriter.cs" />
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>

+ 28 - 10
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -48,6 +48,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             string inputFormat,
             string inputFormat,
             string outputFormat,
             string outputFormat,
             long startTimeTicks,
             long startTimeTicks,
+            long? endTimeTicks,
             CancellationToken cancellationToken)
             CancellationToken cancellationToken)
         {
         {
             var ms = new MemoryStream();
             var ms = new MemoryStream();
@@ -56,6 +57,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             {
             {
                 // Return the original without any conversions, if possible
                 // Return the original without any conversions, if possible
                 if (startTimeTicks == 0 && 
                 if (startTimeTicks == 0 && 
+                    !endTimeTicks.HasValue &&
                     string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase))
                     string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     await stream.CopyToAsync(ms, 81920, cancellationToken).ConfigureAwait(false);
                     await stream.CopyToAsync(ms, 81920, cancellationToken).ConfigureAwait(false);
@@ -64,7 +66,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 {
                 {
                     var trackInfo = await GetTrackInfo(stream, inputFormat, cancellationToken).ConfigureAwait(false);
                     var trackInfo = await GetTrackInfo(stream, inputFormat, cancellationToken).ConfigureAwait(false);
 
 
-                    UpdateStartingPosition(trackInfo, startTimeTicks);
+                    FilterEvents(trackInfo, startTimeTicks, endTimeTicks, false);
 
 
                     var writer = GetWriter(outputFormat);
                     var writer = GetWriter(outputFormat);
 
 
@@ -81,19 +83,30 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             return ms;
             return ms;
         }
         }
 
 
-        private void UpdateStartingPosition(SubtitleTrackInfo track, long startPositionTicks)
+        private void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long? endTimeTicks, bool preserveTimestamps)
         {
         {
-            if (startPositionTicks == 0) return;
+            // Drop subs that are earlier than what we're looking for
+            track.TrackEvents = track.TrackEvents
+                .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 || (i.EndPositionTicks - startPositionTicks) < 0)
+                .ToList();
 
 
-            foreach (var trackEvent in track.TrackEvents)
+            if (endTimeTicks.HasValue)
             {
             {
-                trackEvent.EndPositionTicks -= startPositionTicks;
-                trackEvent.StartPositionTicks -= startPositionTicks;
+                var endTime = endTimeTicks.Value;
+
+                track.TrackEvents = track.TrackEvents
+                    .TakeWhile(i => i.StartPositionTicks <= endTime)
+                    .ToList();
             }
             }
 
 
-            track.TrackEvents = track.TrackEvents
-                .SkipWhile(i => i.StartPositionTicks < 0 || i.EndPositionTicks < 0)
-                .ToList();
+            if (!preserveTimestamps)
+            {
+                foreach (var trackEvent in track.TrackEvents)
+                {
+                    trackEvent.EndPositionTicks -= startPositionTicks;
+                    trackEvent.StartPositionTicks -= startPositionTicks;
+                }
+            }
         }
         }
 
 
         public async Task<Stream> GetSubtitles(string itemId,
         public async Task<Stream> GetSubtitles(string itemId,
@@ -101,6 +114,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             int subtitleStreamIndex,
             int subtitleStreamIndex,
             string outputFormat,
             string outputFormat,
             long startTimeTicks,
             long startTimeTicks,
+            long? endTimeTicks,
             CancellationToken cancellationToken)
             CancellationToken cancellationToken)
         {
         {
             var subtitle = await GetSubtitleStream(itemId, mediaSourceId, subtitleStreamIndex, cancellationToken)
             var subtitle = await GetSubtitleStream(itemId, mediaSourceId, subtitleStreamIndex, cancellationToken)
@@ -110,7 +124,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             {
             {
                 var inputFormat = subtitle.Item2;
                 var inputFormat = subtitle.Item2;
 
 
-                return await ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, cancellationToken).ConfigureAwait(false);
+                return await ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, cancellationToken).ConfigureAwait(false);
             }
             }
         }
         }
 
 
@@ -254,6 +268,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             {
             {
                 return new VttWriter();
                 return new VttWriter();
             }
             }
+            if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase))
+            {
+                return new TtmlWriter();
+            }
 
 
             throw new ArgumentException("Unsupported format: " + format);
             throw new ArgumentException("Unsupported format: " + format);
         }
         }

+ 59 - 0
MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs

@@ -0,0 +1,59 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+
+namespace MediaBrowser.MediaEncoding.Subtitles
+{
+    public class TtmlWriter : ISubtitleWriter
+    {
+        public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
+        {
+            // Example: https://github.com/zmalltalker/ttml2vtt/blob/master/data/sample.xml
+            // Parser example: https://github.com/mozilla/popcorn-js/blob/master/parsers/parserTTML/popcorn.parserTTML.js
+
+            using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
+            {
+                writer.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
+                writer.WriteLine("<tt xmlns=\"http://www.w3.org/ns/ttml\" xmlns:tts=\"http://www.w3.org/2006/04/ttaf1#styling\" lang=\"no\">");
+
+                writer.WriteLine("<head>");
+                writer.WriteLine("<styling>");
+                writer.WriteLine("<style id=\"italic\" tts:fontStyle=\"italic\" />");
+                writer.WriteLine("<style id=\"left\" tts:textAlign=\"left\" />");
+                writer.WriteLine("<style id=\"center\" tts:textAlign=\"center\" />");
+                writer.WriteLine("<style id=\"right\" tts:textAlign=\"right\" />");
+                writer.WriteLine("</styling>");
+                writer.WriteLine("</head>");
+
+                writer.WriteLine("<body>");
+                writer.WriteLine("<div>");
+
+                foreach (var trackEvent in info.TrackEvents)
+                {
+                    var text = trackEvent.Text;
+
+                    text = Regex.Replace(text, @"\\N", "<br/>", RegexOptions.IgnoreCase);
+
+                    writer.WriteLine("<p begin=\"{0}\" dur=\"{1}\">{2}</p>",
+                        trackEvent.StartPositionTicks,
+                        (trackEvent.EndPositionTicks - trackEvent.StartPositionTicks),
+                        text);
+                }
+
+                writer.WriteLine("</div>");
+                writer.WriteLine("</body>");
+                
+                writer.WriteLine("</tt>");
+            }
+        }
+
+        private string FormatTime(long ticks)
+        {
+            var time = TimeSpan.FromTicks(ticks);
+
+            return string.Format(@"{0:hh\:mm\:ss\,fff}", time);
+        }
+    }
+}

+ 3 - 2
MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs

@@ -14,12 +14,13 @@ namespace MediaBrowser.Model.Dlna
 
 
         public string BuildImageHeader(string container,
         public string BuildImageHeader(string container,
             int? width,
             int? width,
-            int? height)
+            int? height,
+            bool isDirectStream)
         {
         {
             string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetImageOrgOpValue();
             string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetImageOrgOpValue();
 
 
             // 0 = native, 1 = transcoded
             // 0 = native, 1 = transcoded
-            const string orgCi = ";DLNA.ORG_CI=0";
+            var orgCi = isDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1";
 
 
             DlnaFlags flagValue = DlnaFlags.StreamingTransferMode |
             DlnaFlags flagValue = DlnaFlags.StreamingTransferMode |
                             DlnaFlags.BackgroundTransferMode |
                             DlnaFlags.BackgroundTransferMode |

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

@@ -90,8 +90,7 @@ namespace MediaBrowser.Model.Dlna
         public CodecProfile[] CodecProfiles { get; set; }
         public CodecProfile[] CodecProfiles { get; set; }
         public ResponseProfile[] ResponseProfiles { get; set; }
         public ResponseProfile[] ResponseProfiles { get; set; }
 
 
-        public SubtitleProfile[] SoftSubtitleProfiles { get; set; }
-        public SubtitleProfile[] ExternalSubtitleProfiles { get; set; }
+        public SubtitleProfile[] SubtitleProfiles { get; set; }
       
       
         public DeviceProfile()
         public DeviceProfile()
         {
         {
@@ -100,9 +99,6 @@ namespace MediaBrowser.Model.Dlna
             ResponseProfiles = new ResponseProfile[] { };
             ResponseProfiles = new ResponseProfile[] { };
             CodecProfiles = new CodecProfile[] { };
             CodecProfiles = new CodecProfile[] { };
             ContainerProfiles = new ContainerProfile[] { };
             ContainerProfiles = new ContainerProfile[] { };
-
-            SoftSubtitleProfiles = new SubtitleProfile[] { };
-            ExternalSubtitleProfiles = new SubtitleProfile[] { };
             
             
             XmlRootAttributes = new XmlAttribute[] { };
             XmlRootAttributes = new XmlAttribute[] { };
             
             

+ 1 - 1
MediaBrowser.Model/Dlna/DlnaMaps.cs

@@ -48,7 +48,7 @@
             orgOp += "0";
             orgOp += "0";
 
 
             // Byte-based seeking only possible when not transcoding
             // Byte-based seeking only possible when not transcoding
-            orgOp += "1";
+            orgOp += "0";
 
 
             return orgOp;
             return orgOp;
         }
         }

+ 13 - 2
MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs

@@ -385,7 +385,7 @@ namespace MediaBrowser.Model.Dlna
                 return ResolveImageJPGFormat(width, height);
                 return ResolveImageJPGFormat(width, height);
 
 
             if (StringHelper.EqualsIgnoreCase(container, "png"))
             if (StringHelper.EqualsIgnoreCase(container, "png"))
-                return MediaFormatProfile.PNG_LRG;
+                return ResolveImagePNGFormat(width, height);
 
 
             if (StringHelper.EqualsIgnoreCase(container, "gif"))
             if (StringHelper.EqualsIgnoreCase(container, "gif"))
                 return MediaFormatProfile.GIF_LRG;
                 return MediaFormatProfile.GIF_LRG;
@@ -401,7 +401,7 @@ namespace MediaBrowser.Model.Dlna
             if (width.HasValue && height.HasValue)
             if (width.HasValue && height.HasValue)
             {
             {
                 if ((width.Value <= 160) && (height.Value <= 160))
                 if ((width.Value <= 160) && (height.Value <= 160))
-                    return MediaFormatProfile.JPEG_SM;
+                    return MediaFormatProfile.JPEG_TN;
 
 
                 if ((width.Value <= 640) && (height.Value <= 480))
                 if ((width.Value <= 640) && (height.Value <= 480))
                     return MediaFormatProfile.JPEG_SM;
                     return MediaFormatProfile.JPEG_SM;
@@ -416,5 +416,16 @@ namespace MediaBrowser.Model.Dlna
 
 
             return MediaFormatProfile.JPEG_SM;
             return MediaFormatProfile.JPEG_SM;
         }
         }
+
+        private MediaFormatProfile ResolveImagePNGFormat(int? width, int? height)
+        {
+            if (width.HasValue && height.HasValue)
+            {
+                if ((width.Value <= 160) && (height.Value <= 160))
+                    return MediaFormatProfile.PNG_TN;
+            }
+
+            return MediaFormatProfile.PNG_LRG;
+        }
     }
     }
 }
 }

+ 15 - 41
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Session;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 
 
@@ -9,7 +10,7 @@ namespace MediaBrowser.Model.Dlna
 {
 {
     public class StreamBuilder
     public class StreamBuilder
     {
     {
-        private string[] _serverTextSubtitleOutputs = new string[] { "srt", "vtt" };
+        private readonly string[] _serverTextSubtitleOutputs = { "srt", "vtt", "ttml" };
 
 
         public StreamInfo BuildAudioItem(AudioOptions options)
         public StreamInfo BuildAudioItem(AudioOptions options)
         {
         {
@@ -158,7 +159,7 @@ namespace MediaBrowser.Model.Dlna
 
 
                         if (all)
                         if (all)
                         {
                         {
-                            playlistItem.IsDirectStream = true;
+                            playlistItem.PlayMethod = PlayMethod.DirectStream;
                             playlistItem.Container = item.Container;
                             playlistItem.Container = item.Container;
 
 
                             return playlistItem;
                             return playlistItem;
@@ -179,7 +180,7 @@ namespace MediaBrowser.Model.Dlna
 
 
             if (transcodingProfile != null)
             if (transcodingProfile != null)
             {
             {
-                playlistItem.IsDirectStream = false;
+                playlistItem.PlayMethod = PlayMethod.Transcode;
                 playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
                 playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
                 playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
                 playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
                 playlistItem.Container = transcodingProfile.Container;
                 playlistItem.Container = transcodingProfile.Container;
@@ -252,12 +253,12 @@ namespace MediaBrowser.Model.Dlna
 
 
                 if (directPlay != null)
                 if (directPlay != null)
                 {
                 {
-                    playlistItem.IsDirectStream = true;
+                    playlistItem.PlayMethod = PlayMethod.DirectStream;
                     playlistItem.Container = item.Container;
                     playlistItem.Container = item.Container;
 
 
                     if (subtitleStream != null)
                     if (subtitleStream != null)
                     {
                     {
-                        playlistItem.SubtitleDeliveryMethod = GetDirectStreamSubtitleDeliveryMethod(subtitleStream, options);
+                        playlistItem.SubtitleDeliveryMethod = GetSubtitleDeliveryMethod(subtitleStream, options);
                     }
                     }
 
 
                     return playlistItem;
                     return playlistItem;
@@ -279,10 +280,10 @@ namespace MediaBrowser.Model.Dlna
             {
             {
                 if (subtitleStream != null)
                 if (subtitleStream != null)
                 {
                 {
-                    playlistItem.SubtitleDeliveryMethod = GetTranscodedSubtitleDeliveryMethod(subtitleStream, options);
+                    playlistItem.SubtitleDeliveryMethod = GetSubtitleDeliveryMethod(subtitleStream, options);
                 }
                 }
 
 
-                playlistItem.IsDirectStream = false;
+                playlistItem.PlayMethod = PlayMethod.Transcode;
                 playlistItem.Container = transcodingProfile.Container;
                 playlistItem.Container = transcodingProfile.Container;
                 playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
                 playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
                 playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
                 playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
@@ -499,9 +500,9 @@ namespace MediaBrowser.Model.Dlna
                     return false;
                     return false;
                 }
                 }
 
 
-                SubtitleDeliveryMethod subtitleMethod = GetDirectStreamSubtitleDeliveryMethod(subtitleStream, options);
+                SubtitleDeliveryMethod subtitleMethod = GetSubtitleDeliveryMethod(subtitleStream, options);
 
 
-                if (subtitleMethod != SubtitleDeliveryMethod.External && subtitleMethod != SubtitleDeliveryMethod.Direct)
+                if (subtitleMethod != SubtitleDeliveryMethod.External && subtitleMethod != SubtitleDeliveryMethod.Embed)
                 {
                 {
                     return false;
                     return false;
                 }
                 }
@@ -510,41 +511,14 @@ namespace MediaBrowser.Model.Dlna
             return IsAudioEligibleForDirectPlay(item, maxBitrate);
             return IsAudioEligibleForDirectPlay(item, maxBitrate);
         }
         }
 
 
-        private SubtitleDeliveryMethod GetDirectStreamSubtitleDeliveryMethod(MediaStream subtitleStream,
-            VideoOptions options)
-        {
-            if (subtitleStream.IsTextSubtitleStream)
-            {
-                string subtitleFormat = NormalizeSubtitleFormat(subtitleStream.Codec);
-
-                bool supportsDirect = ContainsSubtitleFormat(options.Profile.SoftSubtitleProfiles, new[] { subtitleFormat });
-
-                if (supportsDirect)
-                {
-                    return SubtitleDeliveryMethod.Direct;
-                }
-                
-                // See if the device can retrieve the subtitles externally
-                bool supportsSubsExternally = options.Context == EncodingContext.Streaming &&
-                    ContainsSubtitleFormat(options.Profile.ExternalSubtitleProfiles, _serverTextSubtitleOutputs);
-
-                if (supportsSubsExternally)
-                {
-                    return SubtitleDeliveryMethod.External;
-                }
-            }
-
-            return SubtitleDeliveryMethod.Encode;
-        }
-
-        private SubtitleDeliveryMethod GetTranscodedSubtitleDeliveryMethod(MediaStream subtitleStream,
+        private SubtitleDeliveryMethod GetSubtitleDeliveryMethod(MediaStream subtitleStream,
             VideoOptions options)
             VideoOptions options)
         {
         {
             if (subtitleStream.IsTextSubtitleStream)
             if (subtitleStream.IsTextSubtitleStream)
             {
             {
                 // See if the device can retrieve the subtitles externally
                 // See if the device can retrieve the subtitles externally
                 bool supportsSubsExternally = options.Context == EncodingContext.Streaming &&
                 bool supportsSubsExternally = options.Context == EncodingContext.Streaming &&
-                    ContainsSubtitleFormat(options.Profile.ExternalSubtitleProfiles, _serverTextSubtitleOutputs);
+                    ContainsSubtitleFormat(options.Profile.SubtitleProfiles, SubtitleDeliveryMethod.External, _serverTextSubtitleOutputs);
 
 
                 if (supportsSubsExternally)
                 if (supportsSubsExternally)
                 {
                 {
@@ -552,7 +526,7 @@ namespace MediaBrowser.Model.Dlna
                 }
                 }
 
 
                 // See if the device can retrieve the subtitles externally
                 // See if the device can retrieve the subtitles externally
-                bool supportsEmbedded = ContainsSubtitleFormat(options.Profile.SoftSubtitleProfiles, _serverTextSubtitleOutputs);
+                bool supportsEmbedded = ContainsSubtitleFormat(options.Profile.SubtitleProfiles, SubtitleDeliveryMethod.Embed, _serverTextSubtitleOutputs);
 
 
                 if (supportsEmbedded)
                 if (supportsEmbedded)
                 {
                 {
@@ -573,11 +547,11 @@ namespace MediaBrowser.Model.Dlna
             return codec;
             return codec;
         }
         }
 
 
-        private bool ContainsSubtitleFormat(SubtitleProfile[] profiles, string[] formats)
+        private bool ContainsSubtitleFormat(SubtitleProfile[] profiles, SubtitleDeliveryMethod method, string[] formats)
         {
         {
             foreach (SubtitleProfile profile in profiles)
             foreach (SubtitleProfile profile in profiles)
             {
             {
-                if (ListHelper.ContainsIgnoreCase(formats, profile.Format))
+                if (method == profile.Method && ListHelper.ContainsIgnoreCase(formats, profile.Format))
                 {
                 {
                     return true;
                     return true;
                 }
                 }

+ 72 - 8
MediaBrowser.Model/Dlna/StreamInfo.cs

@@ -3,6 +3,7 @@ using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Session;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 
 
@@ -15,7 +16,7 @@ namespace MediaBrowser.Model.Dlna
     {
     {
         public string ItemId { get; set; }
         public string ItemId { get; set; }
 
 
-        public bool IsDirectStream { get; set; }
+        public PlayMethod PlayMethod { get; set; }
 
 
         public DlnaProfileType MediaType { get; set; }
         public DlnaProfileType MediaType { get; set; }
 
 
@@ -59,6 +60,7 @@ namespace MediaBrowser.Model.Dlna
         public MediaSourceInfo MediaSource { get; set; }
         public MediaSourceInfo MediaSource { get; set; }
 
 
         public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
         public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
+        public string SubtitleFormat { get; set; }
 
 
         public string MediaSourceId
         public string MediaSourceId
         {
         {
@@ -68,6 +70,11 @@ namespace MediaBrowser.Model.Dlna
             }
             }
         }
         }
 
 
+        public bool IsDirectStream
+        {
+            get { return PlayMethod == PlayMethod.DirectStream; }
+        }
+
         public string ToUrl(string baseUrl)
         public string ToUrl(string baseUrl)
         {
         {
             return ToDlnaUrl(baseUrl);
             return ToDlnaUrl(baseUrl);
@@ -124,6 +131,55 @@ namespace MediaBrowser.Model.Dlna
             return string.Format("Params={0}", string.Join(";", list.ToArray()));
             return string.Format("Params={0}", string.Join(";", list.ToArray()));
         }
         }
 
 
+        public List<SubtitleStreamInfo> GetExternalSubtitles(string baseUrl)
+        {
+            if (string.IsNullOrEmpty(baseUrl))
+            {
+                throw new ArgumentNullException(baseUrl);
+            }
+
+            List<SubtitleStreamInfo> list = new List<SubtitleStreamInfo>();
+
+            if (SubtitleDeliveryMethod != SubtitleDeliveryMethod.External)
+            {
+                return list;
+            }
+
+            if (!SubtitleStreamIndex.HasValue)
+            {
+                return list;
+            }
+
+            // HLS will preserve timestamps so we can just grab the full subtitle stream
+            long startPositionTicks = StringHelper.EqualsIgnoreCase(Protocol, "hls")
+                ? 0
+                : StartPositionTicks;
+
+            string url = string.Format("{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
+                baseUrl,
+                ItemId,
+                MediaSourceId,
+                StringHelper.ToStringCultureInvariant(SubtitleStreamIndex.Value),
+                StringHelper.ToStringCultureInvariant(startPositionTicks),
+                SubtitleFormat);
+
+            foreach (MediaStream stream in MediaSource.MediaStreams)
+            {
+                if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value)
+                {
+                    list.Add(new SubtitleStreamInfo
+                    {
+                        Url = url,
+                        IsForced = stream.IsForced,
+                        Language = stream.Language,
+                        Name = stream.Language ?? "Unknown"
+                    });
+                }
+            }
+
+            return list;
+        }
+
         /// <summary>
         /// <summary>
         /// Returns the audio stream that will be used
         /// Returns the audio stream that will be used
         /// </summary>
         /// </summary>
@@ -137,7 +193,7 @@ namespace MediaBrowser.Model.Dlna
                     {
                     {
                         foreach (MediaStream i in MediaSource.MediaStreams)
                         foreach (MediaStream i in MediaSource.MediaStreams)
                         {
                         {
-                            if (i.Index == AudioStreamIndex.Value && i.Type == MediaStreamType.Audio) 
+                            if (i.Index == AudioStreamIndex.Value && i.Type == MediaStreamType.Audio)
                                 return i;
                                 return i;
                         }
                         }
                         return null;
                         return null;
@@ -437,16 +493,24 @@ namespace MediaBrowser.Model.Dlna
         /// </summary>
         /// </summary>
         Encode = 0,
         Encode = 0,
         /// <summary>
         /// <summary>
-        /// Internal format is supported natively
-        /// </summary>
-        Direct = 1,
-        /// <summary>
         /// The embed
         /// The embed
         /// </summary>
         /// </summary>
-        Embed = 2,
+        Embed = 1,
         /// <summary>
         /// <summary>
         /// The external
         /// The external
         /// </summary>
         /// </summary>
-        External = 3
+        External = 2,
+        /// <summary>
+        /// The HLS
+        /// </summary>
+        Hls = 3
+    }
+
+    public class SubtitleStreamInfo
+    {
+        public string Url { get; set; }
+        public string Language { get; set; }
+        public string Name { get; set; }
+        public bool IsForced { get; set; }
     }
     }
 }
 }

+ 10 - 2
MediaBrowser.Model/Dlna/SubtitleProfile.cs

@@ -1,8 +1,16 @@
-
+using System.Xml.Serialization;
+
 namespace MediaBrowser.Model.Dlna
 namespace MediaBrowser.Model.Dlna
 {
 {
     public class SubtitleProfile
     public class SubtitleProfile
     {
     {
+        [XmlAttribute("format")]
         public string Format { get; set; }
         public string Format { get; set; }
+
+        [XmlAttribute("protocol")]
+        public string Protocol { get; set; }
+
+        [XmlAttribute("method")]
+        public SubtitleDeliveryMethod Method { get; set; }
     }
     }
-}
+}

+ 6 - 6
MediaBrowser.Model/Dto/BaseItemDto.cs

@@ -525,6 +525,12 @@ namespace MediaBrowser.Model.Dto
             return IsType(type.Name);
             return IsType(type.Name);
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets a value indicating whether [supports playlists].
+        /// </summary>
+        /// <value><c>true</c> if [supports playlists]; otherwise, <c>false</c>.</value>
+        public bool SupportsPlaylists { get; set; }
+
         /// <summary>
         /// <summary>
         /// Determines whether the specified type is type.
         /// Determines whether the specified type is type.
         /// </summary>
         /// </summary>
@@ -631,12 +637,6 @@ namespace MediaBrowser.Model.Dto
         /// <value>The type of the media.</value>
         /// <value>The type of the media.</value>
         public string MediaType { get; set; }
         public string MediaType { get; set; }
 
 
-        /// <summary>
-        /// Gets or sets the overview HTML.
-        /// </summary>
-        /// <value>The overview HTML.</value>
-        public string OverviewHtml { get; set; }
-
         /// <summary>
         /// <summary>
         /// Gets or sets the end date.
         /// Gets or sets the end date.
         /// </summary>
         /// </summary>

+ 1 - 0
MediaBrowser.Model/MediaInfo/SubtitleFormat.cs

@@ -8,5 +8,6 @@
         public const string VTT = "vtt";
         public const string VTT = "vtt";
         public const string SUB = "sub";
         public const string SUB = "sub";
         public const string SMI = "smi";
         public const string SMI = "smi";
+        public const string TTML = "ttml";
     }
     }
 }
 }

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

@@ -147,6 +147,7 @@
     <Compile Include="Photos\PhotoHelper.cs" />
     <Compile Include="Photos\PhotoHelper.cs" />
     <Compile Include="Photos\PhotoMetadataService.cs" />
     <Compile Include="Photos\PhotoMetadataService.cs" />
     <Compile Include="Photos\PhotoProvider.cs" />
     <Compile Include="Photos\PhotoProvider.cs" />
+    <Compile Include="Playlists\PlaylistMetadataService.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Manager\ProviderUtils.cs" />
     <Compile Include="Manager\ProviderUtils.cs" />
     <Compile Include="Studios\StudiosImageProvider.cs" />
     <Compile Include="Studios\StudiosImageProvider.cs" />

+ 47 - 0
MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs

@@ -0,0 +1,47 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Providers.Manager;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Playlists
+{
+    class PlaylistMetadataService : MetadataService<Playlist, ItemLookupInfo>
+    {
+        public PlaylistMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, IFileSystem fileSystem)
+            : base(serverConfigurationManager, logger, providerManager, providerRepo, fileSystem)
+        {
+        }
+
+        /// <summary>
+        /// Merges the specified source.
+        /// </summary>
+        /// <param name="source">The source.</param>
+        /// <param name="target">The target.</param>
+        /// <param name="lockedFields">The locked fields.</param>
+        /// <param name="replaceData">if set to <c>true</c> [replace data].</param>
+        /// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param>
+        protected override void MergeData(Playlist source, Playlist target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
+        {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+
+            if (replaceData || string.IsNullOrEmpty(target.PlaylistMediaType))
+            {
+                target.PlaylistMediaType = source.PlaylistMediaType;
+            }
+
+            if (replaceData || string.IsNullOrEmpty(target.OwnerUserId))
+            {
+                target.OwnerUserId = source.OwnerUserId;
+            }
+
+            if (mergeMetadataSettings)
+            {
+                target.LinkedChildren = source.LinkedChildren;
+            }
+        }
+    }
+}

+ 3 - 11
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -112,6 +112,8 @@ namespace MediaBrowser.Server.Implementations.Dto
 
 
             var dto = new BaseItemDto();
             var dto = new BaseItemDto();
 
 
+            dto.SupportsPlaylists = item.SupportsAddingToPlaylist;
+
             if (fields.Contains(ItemFields.People))
             if (fields.Contains(ItemFields.People))
             {
             {
                 AttachPeople(dto, item);
                 AttachPeople(dto, item);
@@ -849,17 +851,7 @@ namespace MediaBrowser.Server.Implementations.Dto
 
 
             if (fields.Contains(ItemFields.Overview))
             if (fields.Contains(ItemFields.Overview))
             {
             {
-                // TODO: Remove this after a while, since it's been moved to the providers
-                if (item is MusicArtist)
-                {
-                    var strippedOverview = string.IsNullOrEmpty(item.Overview) ? item.Overview : item.Overview.StripHtml();
-
-                    dto.Overview = strippedOverview;
-                }
-                else
-                {
-                    dto.Overview = item.Overview;
-                }
+                dto.Overview = item.Overview;
             }
             }
 
 
             if (fields.Contains(ItemFields.ShortOverview))
             if (fields.Contains(ItemFields.ShortOverview))

+ 14 - 11
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -12,7 +12,6 @@ using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
@@ -551,24 +550,28 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                     };
                     };
                 }
                 }
 
 
-                if (!string.IsNullOrEmpty(info.Path))
-                {
-                    item.Path = info.Path;
-                }
-                else if (!string.IsNullOrEmpty(info.Url))
-                {
-                    item.Path = info.Url;
-                }
-
                 isNew = true;
                 isNew = true;
             }
             }
 
 
             item.RecordingInfo = info;
             item.RecordingInfo = info;
             item.ServiceName = serviceName;
             item.ServiceName = serviceName;
 
 
+            var originalPath = item.Path;
+
+            if (!string.IsNullOrEmpty(info.Path))
+            {
+                item.Path = info.Path;
+            }
+            else if (!string.IsNullOrEmpty(info.Url))
+            {
+                item.Path = info.Url;
+            }
+
+            var pathChanged = !string.Equals(originalPath, item.Path);
+
             await item.RefreshMetadata(new MetadataRefreshOptions
             await item.RefreshMetadata(new MetadataRefreshOptions
             {
             {
-                ForceSave = isNew
+                ForceSave = isNew || pathChanged
 
 
             }, cancellationToken);
             }, cancellationToken);
 
 

+ 2 - 1
MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json

@@ -334,5 +334,6 @@
     "OptionNewPlaylist":  "New playlist...",
     "OptionNewPlaylist":  "New playlist...",
     "MessageAddedToPlaylistSuccess":  "Ok",
     "MessageAddedToPlaylistSuccess":  "Ok",
 	"ButtonViewSeriesRecording": "View series recording",
 	"ButtonViewSeriesRecording": "View series recording",
-	"ValueOriginalAirDate": "Original air date: {0}"
+	"ValueOriginalAirDate": "Original air date: {0}",
+    "ButtonRemoveFromPlaylist":  "Remove from playlist"
 }
 }

+ 22 - 2
MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs

@@ -190,9 +190,29 @@ namespace MediaBrowser.Server.Implementations.Playlists
             }, CancellationToken.None).ConfigureAwait(false);
             }, CancellationToken.None).ConfigureAwait(false);
         }
         }
 
 
-        public Task RemoveFromPlaylist(string playlistId, IEnumerable<int> indeces)
+        public async Task RemoveFromPlaylist(string playlistId, IEnumerable<string> entryIds)
         {
         {
-            throw new NotImplementedException();
+            var playlist = _libraryManager.GetItemById(playlistId) as Playlist;
+
+            if (playlist == null)
+            {
+                throw new ArgumentException("No Playlist exists with the supplied Id");
+            }
+
+            var children = playlist.LinkedChildren.ToList();
+
+            var idList = entryIds.ToList();
+
+            var removals = children.Where(i => idList.Contains(i.Id));
+
+            playlist.LinkedChildren = children.Except(removals)
+                .ToList();
+
+            await playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+            await playlist.RefreshMetadata(new MetadataRefreshOptions
+            {
+                ForceSave = true
+            }, CancellationToken.None).ConfigureAwait(false);
         }
         }
 
 
         public Folder GetPlaylistsFolder(string userId)
         public Folder GetPlaylistsFolder(string userId)

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

@@ -124,7 +124,7 @@ namespace MediaBrowser.Server.Implementations.Udp
                 {
                 {
                     Address = serverAddress,
                     Address = serverAddress,
                     Id = _appHost.ServerId,
                     Id = _appHost.ServerId,
-                    Name = _appHost.Name
+                    Name = _appHost.FriendlyName
                 };
                 };
 
 
                 await SendAsync(Encoding.UTF8.GetBytes(_json.SerializeToString(response)), endpoint);
                 await SendAsync(Encoding.UTF8.GetBytes(_json.SerializeToString(response)), endpoint);

+ 11 - 1
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -1058,10 +1058,20 @@ namespace MediaBrowser.ServerApplication
                 SupportsAutoRunAtStartup = SupportsAutoRunAtStartup,
                 SupportsAutoRunAtStartup = SupportsAutoRunAtStartup,
                 TranscodingTempPath = ApplicationPaths.TranscodingTempPath,
                 TranscodingTempPath = ApplicationPaths.TranscodingTempPath,
                 IsRunningAsService = IsRunningAsService,
                 IsRunningAsService = IsRunningAsService,
-                ServerName = string.IsNullOrWhiteSpace(ServerConfigurationManager.Configuration.ServerName) ? Environment.MachineName : ServerConfigurationManager.Configuration.ServerName
+                ServerName = FriendlyName
             };
             };
         }
         }
 
 
+        public string FriendlyName
+        {
+            get
+            {
+                return string.IsNullOrWhiteSpace(ServerConfigurationManager.Configuration.ServerName)
+                    ? Environment.MachineName
+                    : ServerConfigurationManager.Configuration.ServerName;
+            }
+        }
+
         public int HttpServerPort
         public int HttpServerPort
         {
         {
             get { return ServerConfigurationManager.Configuration.HttpServerPortNumber; }
             get { return ServerConfigurationManager.Configuration.HttpServerPortNumber; }

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

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
     <metadata>
         <id>MediaBrowser.Common.Internal</id>
         <id>MediaBrowser.Common.Internal</id>
-        <version>3.0.422</version>
+        <version>3.0.423</version>
         <title>MediaBrowser.Common.Internal</title>
         <title>MediaBrowser.Common.Internal</title>
         <authors>Luke</authors>
         <authors>Luke</authors>
         <owners>ebr,Luke,scottisafool</owners>
         <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>
         <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>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.422" />
+            <dependency id="MediaBrowser.Common" version="3.0.423" />
             <dependency id="NLog" version="3.1.0.0" />
             <dependency id="NLog" version="3.1.0.0" />
             <dependency id="SimpleInjector" version="2.5.2" />
             <dependency id="SimpleInjector" version="2.5.2" />
             <dependency id="sharpcompress" version="0.10.2" />
             <dependency id="sharpcompress" version="0.10.2" />

+ 1 - 1
Nuget/MediaBrowser.Common.nuspec

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

+ 1 - 1
Nuget/MediaBrowser.Model.Signed.nuspec

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