diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index 3e692cb22f..f7696e9639 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/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.MediaEncoding; using MediaBrowser.Controller.Net; @@ -11,6 +9,8 @@ using MediaBrowser.Model.Providers; using ServiceStack; using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -69,7 +69,8 @@ namespace MediaBrowser.Api.Subtitles 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 { /// @@ -132,6 +133,7 @@ namespace MediaBrowser.Api.Subtitles request.Index, request.Format, request.StartPositionTicks, + null, CancellationToken.None).ConfigureAwait(false); } diff --git a/MediaBrowser.Api/SystemService.cs b/MediaBrowser.Api/SystemService.cs index e31e66d19d..259b1d8921 100644 --- a/MediaBrowser.Api/SystemService.cs +++ b/MediaBrowser.Api/SystemService.cs @@ -71,6 +71,8 @@ namespace MediaBrowser.Api /// Initializes a new instance of the class. /// /// The app host. + /// The application paths. + /// The file system. /// jsonSerializer public SystemService(IServerApplicationHost appHost, IApplicationPaths appPaths, IFileSystem fileSystem) { diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 32d3dd5c8c..d3085cb680 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -34,6 +34,11 @@ namespace MediaBrowser.Controller.Entities.Audio Tags = new List(); } + public override bool SupportsAddingToPlaylist + { + get { return LocationType == LocationType.FileSystem && RunTimeTicks.HasValue; } + } + /// /// Gets or sets a value indicating whether this instance has embedded image. /// diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 695b1fd570..152d767823 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -21,6 +21,11 @@ namespace MediaBrowser.Controller.Entities.Audio SoundtrackIds = new List(); } + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + [IgnoreDataMember] public MusicArtist MusicArtist { diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 1544da7bc9..de527b68b3 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/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 ActualChildren { get diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index bce9da4d15..f1dc56ac68 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -18,6 +18,11 @@ namespace MediaBrowser.Controller.Entities.Audio return "MusicGenre-" + Name; } + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + /// /// Returns the folder containing the item. /// If the item is a folder, it returns the folder itself diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index a476f555fb..fdffa60d0a 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -52,6 +52,14 @@ namespace MediaBrowser.Controller.Entities public List ImageInfos { get; set; } + public virtual bool SupportsAddingToPlaylist + { + get + { + return false; + } + } + /// /// Gets a value indicating whether this instance is in mixed folder. /// diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 3977d869c2..b82a400fe8 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -29,6 +29,11 @@ namespace MediaBrowser.Controller.Entities.TV } } + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + [IgnoreDataMember] public override bool IsPreSorted { diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 27ca8b18da..856ed4fdf2 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -39,6 +39,11 @@ namespace MediaBrowser.Controller.Entities.TV DisplaySpecialsWithSeasons = true; } + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + [IgnoreDataMember] public override bool IsPreSorted { diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 5685edc815..ff4c5dd900 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -55,6 +55,11 @@ namespace MediaBrowser.Controller.Entities LinkedAlternateVersions = new List(); } + public override bool SupportsAddingToPlaylist + { + get { return LocationType == LocationType.FileSystem && RunTimeTicks.HasValue; } + } + [IgnoreDataMember] public int MediaSourceCount { diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 49061e05c7..2af37e84dc 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -46,5 +46,11 @@ namespace MediaBrowser.Controller /// /// The server identifier. string ServerId { get; } + + /// + /// Gets the name of the friendly. + /// + /// The name of the friendly. + string FriendlyName { get; } } } diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs index 541dfd2268..d8d836597b 100644 --- a/MediaBrowser.Controller/Library/TVUtils.cs +++ b/MediaBrowser.Controller/Library/TVUtils.cs @@ -126,7 +126,7 @@ namespace MediaBrowser.Controller.Library { var filename = Path.GetFileName(path); - if (string.Equals(path, "specials", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase)) { return 0; } diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs index 6e9bcef2ea..9e32fc32b0 100644 --- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs @@ -13,6 +13,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// The input format. /// The output format. /// The start time ticks. + /// The end time ticks. /// The cancellation token. /// Task{Stream}. Task ConvertSubtitles( @@ -20,6 +21,7 @@ namespace MediaBrowser.Controller.MediaEncoding string inputFormat, string outputFormat, long startTimeTicks, + long? endTimeTicks, CancellationToken cancellationToken); /// @@ -30,6 +32,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// Index of the subtitle stream. /// The output format. /// The start time ticks. + /// The end time ticks. /// The cancellation token. /// Task{Stream}. Task GetSubtitles(string itemId, @@ -37,6 +40,7 @@ namespace MediaBrowser.Controller.MediaEncoding int subtitleStreamIndex, string outputFormat, long startTimeTicks, + long? endTimeTicks, CancellationToken cancellationToken); /// diff --git a/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs b/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs index 796ccb004c..83d7f322d1 100644 --- a/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs +++ b/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs @@ -1,5 +1,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Session; using System; using System.Globalization; using System.IO; @@ -29,7 +30,7 @@ namespace MediaBrowser.Dlna.PlayTo if (directPlay != null) { - playlistItem.StreamInfo.IsDirectStream = true; + playlistItem.StreamInfo.PlayMethod = PlayMethod.DirectStream; playlistItem.StreamInfo.Container = Path.GetExtension(item.Path); return playlistItem; @@ -40,7 +41,7 @@ namespace MediaBrowser.Dlna.PlayTo if (transcodingProfile != null) { - playlistItem.StreamInfo.IsDirectStream = true; + playlistItem.StreamInfo.PlayMethod = PlayMethod.Transcode; playlistItem.StreamInfo.Container = "." + transcodingProfile.Container.TrimStart('.'); } diff --git a/MediaBrowser.Dlna/Profiles/Windows81Profile.cs b/MediaBrowser.Dlna/Profiles/Windows81Profile.cs index 921019f8e7..029fefc645 100644 --- a/MediaBrowser.Dlna/Profiles/Windows81Profile.cs +++ b/MediaBrowser.Dlna/Profiles/Windows81Profile.cs @@ -155,6 +155,13 @@ namespace MediaBrowser.Dlna.Profiles } }; + ExternalSubtitleProfiles = new[] + { + new SubtitleProfile + { + Format = "ttml" + } + }; } } } diff --git a/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml b/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml index 5b5125b30c..5859b2439e 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml @@ -69,9 +69,7 @@ - - srt - + \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml b/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml index 209c029b5d..28f170bc02 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml @@ -107,9 +107,7 @@ - - smi - + \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Windows 8 RT.xml b/MediaBrowser.Dlna/Profiles/Xml/Windows 8 RT.xml index 61ee595494..1847d844f8 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Windows 8 RT.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Windows 8 RT.xml @@ -63,5 +63,7 @@ - + + + \ No newline at end of file diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs index 83bc6a49ee..c7f9742008 100644 --- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs @@ -18,6 +18,20 @@ namespace MediaBrowser.LocalMetadata.Parsers { switch (reader.Name) { + case "OwnerUserId": + { + item.OwnerUserId = reader.ReadElementContentAsString(); + + break; + } + + case "PlaylistMediaType": + { + item.PlaylistMediaType = reader.ReadElementContentAsString(); + + break; + } + case "PlaylistItems": using (var subReader = reader.ReadSubtree()) diff --git a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs index abc7e3b3fa..1541c2176b 100644 --- a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs +++ b/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.Playlists; using System.Collections.Generic; @@ -42,17 +43,34 @@ namespace MediaBrowser.LocalMetadata.Savers /// Task. public void Save(IHasMetadata item, CancellationToken cancellationToken) { + var playlist = (Playlist)item; + var builder = new StringBuilder(); builder.Append(""); - XmlSaverHelpers.AddCommonNodes((Playlist)item, builder); + if (!string.IsNullOrEmpty(playlist.OwnerUserId)) + { + builder.Append("" + SecurityElement.Escape(playlist.OwnerUserId) + ""); + } + + if (!string.IsNullOrEmpty(playlist.PlaylistMediaType)) + { + builder.Append("" + SecurityElement.Escape(playlist.PlaylistMediaType) + ""); + } + + XmlSaverHelpers.AddCommonNodes(playlist, builder); builder.Append(""); var xmlFilePath = GetSavePath(item); - XmlSaverHelpers.Save(builder, xmlFilePath, new List { }); + XmlSaverHelpers.Save(builder, xmlFilePath, new List + { + "OwnerUserId", + "PlaylistMediaType" + + }); } /// diff --git a/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs b/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs index a007a95cf0..0801b73589 100644 --- a/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs +++ b/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) { var items = item.LinkedChildren - .Where(i => i.Type == LinkedChildType.Manual && !string.IsNullOrWhiteSpace(i.ItemName)) + .Where(i => i.Type == LinkedChildType.Manual) .ToList(); if (items.Count == 0) @@ -717,14 +717,20 @@ namespace MediaBrowser.LocalMetadata.Savers { builder.Append("<" + singularNodeName + ">"); - builder.Append("" + SecurityElement.Escape(link.ItemType) + ""); + if (!string.IsNullOrWhiteSpace(link.ItemType)) + { + builder.Append("" + SecurityElement.Escape(link.ItemType) + ""); + } if (link.ItemYear.HasValue) { builder.Append("" + SecurityElement.Escape(link.ItemYear.Value.ToString(UsCulture)) + ""); } - builder.Append("" + SecurityElement.Escape((link.Path ?? string.Empty)) + ""); + if (!string.IsNullOrWhiteSpace(link.Path)) + { + builder.Append("" + SecurityElement.Escape((link.Path)) + ""); + } builder.Append(""); } diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index f11c261e21..12a03a8768 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -66,6 +66,7 @@ + diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index ab9cd546aa..82e331dd87 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -48,6 +48,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles string inputFormat, string outputFormat, long startTimeTicks, + long? endTimeTicks, CancellationToken cancellationToken) { var ms = new MemoryStream(); @@ -56,6 +57,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { // Return the original without any conversions, if possible if (startTimeTicks == 0 && + !endTimeTicks.HasValue && string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase)) { 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); - UpdateStartingPosition(trackInfo, startTimeTicks); + FilterEvents(trackInfo, startTimeTicks, endTimeTicks, false); var writer = GetWriter(outputFormat); @@ -81,19 +83,30 @@ namespace MediaBrowser.MediaEncoding.Subtitles 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 GetSubtitles(string itemId, @@ -101,6 +114,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles int subtitleStreamIndex, string outputFormat, long startTimeTicks, + long? endTimeTicks, CancellationToken cancellationToken) { var subtitle = await GetSubtitleStream(itemId, mediaSourceId, subtitleStreamIndex, cancellationToken) @@ -110,7 +124,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { 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(); } + if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase)) + { + return new TtmlWriter(); + } throw new ArgumentException("Unsupported format: " + format); } diff --git a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs new file mode 100644 index 0000000000..a937175f01 --- /dev/null +++ b/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(""); + writer.WriteLine(""); + + writer.WriteLine(""); + writer.WriteLine(""); + writer.WriteLine("